Qwen代码层面解密

Instruct Model
“bos_token_id”: 151643,
“eos_token_id”: 151645, //instruct model的eos_token_id修改为<|im_end|>, base model本来都是<end_of_text>

解密1:ChatTemplate and Function Call

{%- if tools %} #是否存在工具类,在对话最开始提供
    {{- '<|im_start|>system\n' }}
    {%- if messages[0]['role'] == 'system' %}
        {{- messages[0]['content'] }}
    {%- else %}
        {{- 'You are Qwen, created by Alibaba Cloud. You are a helpful assistant.' }}
    {%- endif %}
    {{- "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
    {%- for tool in tools %}
        {{- "\n" }}
        {{- tool | tojson }}
    {%- endfor %}
    {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
{%- else %} #正常对话第一步走这个流程
    {%- if messages[0]['role'] == 'system' %}
        {{- '<|im_start|>system\n' + messages[0]['content'] + '<|im_end|>\n' }}
    {%- else %}
        {{- '<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n' }}
    {%- endif %}
{%- endif %}
{%- for message in messages %} #遍历多轮对话信息
    {%- if (message.role == "user") or (message.role == "system" and not loop.first) or (message.role == "assistant" and not message.tool_calls) %}
        {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
    {%- elif message.role == "assistant" %}
        {{- '<|im_start|>' + message.role }}
        {%- if message.content %}
            {{- '\n' + message.content }}
        {%- endif %}
        {%- for tool_call in message.tool_calls %}
            {%- if tool_call.function is defined %}
                {%- set tool_call = tool_call.function %}
            {%- endif %}
            {{- '\n<tool_call>\n{"name": "' }}
            {{- tool_call.name }}
            {{- '", "arguments": ' }}
            {{- tool_call.arguments | tojson }}
            {{- '}\n</tool_call>' }}
        {%- endfor %}
        {{- '<|im_end|>\n' }}
    {%- elif message.role == "tool" %}
        {%- if (loop.index0 == 0) or (messages[loop.index0 - 1].role != "tool") %}
            {{- '<|im_start|>user' }}
        {%- endif %}
        {{- '\n<tool_response>\n' }}
        {{- message.content }}
        {{- '\n</tool_response>' }}
        {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
            {{- '<|im_end|>\n' }}
        {%- endif %}
    {%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}  #一般需要add_generation_prompt=True,直到模型输出eos_token
    {{- '<|im_start|>assistant\n' }}
{%- endif %}

解密2: Qwen的tokenizer底层原理,word2id的映射关系

'<|im_start|>system\nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end|>\n<|im_start|>user\nGive me a short introduction to large language model.<|im_end|>\n<|im_start|>assistant\n

['<|im_start|>', 'system', 'Ċ', 'You', 'Ġare', 'ĠQ', 'wen', ',', 'Ġcreated', 'Ġby', 'ĠAlibaba', 'ĠCloud', '.', 'ĠYou', 'Ġare', 'Ġa', 'Ġhelpful', 'Ġassistant', '.','<|im_end|>', 'Ċ', '|im_start|>', 'user', 'Ċ', 'Give', 'Ġme', 'Ġa', 'Ġshort', 'Ġintroduction', 'Ġto', 'Ġlarge', 'Ġlanguage', 'Ġmodel', '<|im_end|>','Ċ', '<|im_start|>', 'assistant', 'Ċ']

tensor([[151644,   8948,    198,   2610,    525,   1207,  16948,     11,   3465,553,  54364,  14817,     13,   1446,    525,    264,  10950,  17847,13, 151645,    198, 151644,    872,    198,  35127,    752,    264,2805,  16800,    311,   3460,   4128,   1614,     13, 151645,    198,151644,  77091,    198]])

疑问1:Ċ代表什么,似乎代表所有\n,空格等间隔符," You",“You”,"\nYou"的分词结果均不一致

tokenize分词结果解析:“i love You” [‘i’, ‘Ġlove’, ‘ĠYou’]
“i loveYou”[‘i’, ‘Ġlove’, ‘You’]
“I love\nYou”[‘I’, ‘Ġlove’, ‘Ċ’, ‘You’]

You的id为2610,而ĠYou为1446,不存在“ You”,"\nYou"的token,而分别用’Ċ’'Ġ’代替,tokenizer的分词流程和原理是什么呢?

首先tokenizer由normalizers,pre_tokenizers,model and post_tokenizers四部分组成,这部分基础可以参考huggingface-tokenizer doc

normalizer:NFC
NFC是“组合形式”(Canonical Composition),它将字符表示为其最“复合”的形式。
这意味着,如果一个字符可以通过Unicode中的组合规则由两个或更多的字符组合而成,那么在NFC中,这个字符就会被表示为一个单独的复合字符。
pre_tokenizer:Split and ByteLevel
通过Split组件,将文本分解成单词、缩写、数字和标点符号等基本单元。
通过ByteLevel组件,处理Unicode字符,确保多字节字符被正确识别。
model: ByteLevel BPE
post_processor: ByteLevel

code流程:PreTrainedTokenizer的tokenize方法会调用QwenTokenizer重写的_tokenize方法,处理BPE分词和special token的合并

解密3: tie_word_embeddings的实现

对于小参数模型,为了让中间的Decoder Layer占比提高,可以将nn.Embedding层和lm_head使用相同的权重矩阵
nn.Embedding是用于快速检索token_id->token_embedding的矩阵,形状为vocab_sizehidden_size
lm_head是token分类的预测头,形状为hidden_size
vocab_size

self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)

本例中vocab_size为151936,tokenizer预设了151664个,留了很多可以作下游special token 做SFT微调

在Qwen2ForCausalLM中定义:self._tie_or_clone_weights(output_embeddings, self.get_input_embeddings())
可以看到在torch下权重是浅克隆,参数更新对两头都有效

def _tie_or_clone_weights(self, output_embeddings, input_embeddings):
    """Tie or clone module weights depending of whether we are using TorchScript or not"""
    if self.config.torchscript:
        output_embeddings.weight = nn.Parameter(input_embeddings.weight.clone())
    else:
        output_embeddings.weight = input_embeddings.weight

    if getattr(output_embeddings, "bias", None) is not None:
        output_embeddings.bias.data = nn.functional.pad(
            output_embeddings.bias.data,
            (
                0,
                output_embeddings.weight.shape[0] - output_embeddings.bias.shape[0],
            ),
            "constant",
            0,
        )
    if hasattr(output_embeddings, "out_features") and hasattr(input_embeddings, "num_embeddings"):
        output_embeddings.out_features = input_embeddings.num_embeddings

解密4: Qwen2ForCausalLM的forward函数

这里对Qwen2Model基础模型的forward暂时不深入讨论

""" 入参说明
input_ids: torch.LongTensor = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None, [0-n_positions-1]位置索引
past_key_values: Optional[List[torch.FloatTensor]] = None,#Pre-computed hidden-states in attention for KV cached
inputs_embeds: Optional[torch.FloatTensor] = None, #没啥用,不想用look up embedding
labels: Optional[torch.LongTensor] = None, #[0-vocab_size]或者-100的整数token_id作为label,其中-100在交叉熵损失函数中是忽略的
use_cache: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None, #whether or not to return hidden of all layers
return_dict: Optional[bool] = None, #return ModelOutput class
cache_position: Optional[torch.LongTensor] = None, #position_id not affected by padding
num_logits_to_keep: int = 0, # hidden_states[:, -num_logits_to_keep:, :] 只计算最后num个token的预测结果,在生成阶段一般取1来降低显存占用
**loss_kwargs,
"""

首先Qwen2ForCausalLM调用Qwen2Model的forward函数,入参基本和上述内容一致,获得多层处理后的hidden_states

参数说明
seq_len:39, hidden_size=896, layer=24
last_hidden_state和hidden_state表示最后一层和所有层的hidden,但是这里我的output_hidden_states=False
past_key_values存放了24层Attention Block里面的Key_list, value_list, 形状为batch, n_head,n_token, d_head,list长度为24
num_logits_to_keep设置为1,只计算最后一个token的预测结果,形状为batch, 1, vocab_size

在这里插入图片描述
得到的BaseModelOutputWithPast包含上述内容,其中last_hidden_state通过lm_head投影得到一组logits,表示tokens的预测概率分布
在这里插入图片描述
如果入参中包含labels,则会计算loss用于模型训练

labels (torch.LongTensor of shape (batch_size, sequence_length), optional):
Labels for computing the masked language modeling loss. Indices should either be in [0, ..., config.vocab_size] or -100 (see input_ids docstring). Tokens with indices set to -100 are ignored (masked), the loss is only computed for the tokens with labels in [0, ..., config.vocab_size].

输入labels, input_ids->hidden_states->logits,即可采用交叉熵损失函数计算分类loss
值得注意地,在LLM的训练过程中,每个token的下一个token就是该token的标签,所以这是一种自监督损失函数,只需要确定mask attention不产生信息泄露即可训练
调用:model(input_ids=model_inputs.data['input_ids'],attention_mask = model_inputs.data['attention_mask'],labels = model_inputs.data['input_ids'])

def ForCausalLMLoss(
    logits, labels, vocab_size: int, num_items_in_batch: int = None, ignore_index: int = -100, **kwargs
):
    # Upcast to float if we need to compute the loss to avoid potential precision issues
    logits = logits.float()
    # Shift so that tokens < n predict n
    shift_logits = logits[..., :-1, :].contiguous()
    shift_labels = labels[..., 1:].contiguous()
##这两行代码将 logits 和 labels 进行平移操作。在因果语言模型中,模型需要预测下一个词,因此对于每个位置 i,模型的输入是词 i,目标是预测词 i+1。通过将 logits 和 labels 平移一位,我们可以确保模型的输出(logits)与正确的目标标签(labels)对齐。
## 这样logits保留前n-1位,labels保留后n-1位
    # Flatten the tokens
    shift_logits = shift_logits.view(-1, vocab_size)
    shift_labels = shift_labels.view(-1)
    # Enable model parallelism
    shift_labels = shift_labels.to(shift_logits.device)
    loss = fixed_cross_entropy(shift_logits, shift_labels, num_items_in_batch, ignore_index, **kwargs)
    return loss

在这里插入图片描述
显然labels保留了后面的n-1个,logits保留了前面的n-1个,自监督是LLM训练的基础

解密5:Qwen2网络模型结构

Qwen网络结构总览图,来自B站UP主良睦路程序员绘制的网络结构我觉得很清晰,不过UP主绘制的是LLAMA的网络,但是Qwen和LLAMA的网络结构可以说是一模一样
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Qwen2 0.5B版本
Qwen2ForCausalLM(
  (model): Qwen2Model(
    (embed_tokens): Embedding(151936, 896)
    (layers): ModuleList(
      (0-23): 24 x Qwen2DecoderLayer(
        (self_attn): Qwen2Attention(
          (q_proj): Linear(in_features=896, out_features=896, bias=True)
          (k_proj): Linear(in_features=896, out_features=128, bias=True) #Group size=7
          (v_proj): Linear(in_features=896, out_features=128, bias=True)
          (o_proj): Linear(in_features=896, out_features=896, bias=False)
          (rotary_emb): Qwen2RotaryEmbedding()
        )
        (mlp): Qwen2MLP(
          (gate_proj): Linear(in_features=896, out_features=4864, bias=False)
          (up_proj): Linear(in_features=896, out_features=4864, bias=False)
          (down_proj): Linear(in_features=4864, out_features=896, bias=False)
          (act_fn): SiLU()
        )
        (input_layernorm): Qwen2RMSNorm((896,), eps=1e-06)
        (post_attention_layernorm): Qwen2RMSNorm((896,), eps=1e-06)
      )
    )
    (norm): Qwen2RMSNorm((896,), eps=1e-06)
    (rotary_emb): Qwen2RotaryEmbedding()
  )
  (lm_head): Linear(in_features=896, out_features=151936, bias=False)
)

解密6: Qwen2RotaryEmbedding()细节原理

首先提一下苏剑林博客的正余弦绝对位置编码的基础概念
在这里插入图片描述
容易看出,对于不同的token position k, 存在一个正余弦交替的位置编码向量代表这个token的绝对位置信息;使用正余弦交替具备一定的推导规律和[-1,1]的上下界,在早期比较常用;
疑惑1:
d i v _ t e r m = 1000 0 − 2 i / d ∗ k div\_term = 10000^{-2i/d}*k div_term=100002i/dk代表什么,base选择10000的好处在哪里?
首先 1000 0 − 2 i / d 10000^{-2i/d} 100002i/d随着位置编码维度的趋近于 d / 2 d/2 d/2,这个负幂指数函数是单调递减的,例如d_model=512时,结果如下:
在这里插入图片描述
s i n ( k ∗ d i v _ t e r m ) sin(k*div\_term) sin(kdiv_term) c o s ( k ∗ d i v _ t e r m ) cos(k*div\_term) cos(kdiv_term)都是一个开始变化大,结尾变化小(低维高频、高频低维)的编码分布,如k=10的位置编码结果如下:
在这里插入图片描述
k=300的位置编码结果如下:
在这里插入图片描述
可以看到这种正余弦位置编码具备良好的数据范围,编码的向量存在低维高频、高维低频的信息分布;接下来分析位置编码的“远程衰减性”,即位置编码内积运算的结果应该和toknes之间的距离负相关,越远的tokens位置编码相似度越低,在Transformer中可以让模型更多关注“局部注意力”;
下图为k=1的位置编码与k=2-500位置编码的内积运算结果,显然满足远程衰减性质
在这里插入图片描述
综上,回顾刚刚提出的 1000 0 − 2 i / d 10000^{-2i/d} 100002i/d中base=10000的原因,其实主要是长度覆盖范围的问题,base过低如等于100时,
在这里插入图片描述
曲线明显更加平滑了,原来k=10的位置编码如下:
在这里插入图片描述
每个位置的位置编码的波长变得更加平滑,不同位置的位置编码间,波长差异明显减小了;在通过选择较大的base可以保证不同位置之间的波长差异足够大,从而使得即使是在很长的序列中,相邻位置也能有明显的区别。这样做的目的是为了确保模型能够区分出不同位置的token,即使它们相隔很远。
比较大的base, b a s e − 2 i / d base^{-2i/d} base2i/d曲线比较sharp,可以防止短周期内出现重复的位置信息,从而保障了长度衰减能力,base=100的时候,长度衰减能力很快就会失效,因为sin cos的周期性在相邻区间产生了相似的位置编码
在这里插入图片描述

# sincos位置编码的简单实现
class PositionalEncoding(nn.Module):
    def __init__(self, d_model, dropout=0.1, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)

        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1)
        div_term = torch.exp(torch.arange( #0-d/2-1范围的, 0,2,4,6...的2i或2i+1维度分量的div_term
            0, d_model, 2).float() * (-math.log(base) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term) #max_len, d_model//2 一次赋值所有sin的内容
        pe[:, 1::2] = torch.cos(position * div_term) #max_len, d_model//2 一次赋值所有cos的内容
        pe = pe.unsqueeze(0).transpose(0, 1) #max_len, 1, d_model
        self.register_buffer('pe', pe)

    def forward(self, x):
        """
        x: [seq_len, batch_size, d_model]
        """
        x = x + self.pe[:x.size(0), :]
        return self.dropout(x)

Rope的原理

复数是数学中一种基本的数类型,它们由实数和虚数单位 ( i ) 组成,其中 ( i ) 定义为 ( i^2 = -1 )。一个复数通常写作 ( a + bi ),这里 ( a ) 和 ( b ) 是实数,( a ) 被称为复数的实部,而 ( b ) 被称为复数的虚部。
在这里插入图片描述
一个复数表示 e i θ e^{i\theta} eiθ基于欧拉公式可以得到其相应的旋转坐标表示
在这里插入图片描述
基于上述有关复数、极坐标和欧拉公式的概念,我们可以简单推导Rope的原理了(证明过程请见苏神博客

  1. 定义目标

对于二维向量 q = ( q 0 , q 1 ) q=(q_0,q_1) q=(q0,q1), k = ( k 0 , k 1 ) k=(k_0,k_1) k=(k0,k1),我们希望定义一个位置编码函数,能够表示 f ( q , m ) f(q,m) f(q,m), f ( k , n ) f(k,n) f(k,n)后, q , k q,k q,k的向量内积(Attention)运算后能够表示 m − n m-n mn的相对位置关系
即求解满足 < f ( q , m ) , f ( k , n ) > = g ( q , k , m − n ) <f(q,m),f(k,n)>=g(q,k,m-n) <f(q,m),f(k,n)>=g(q,k,mn) f f f的一个解

我们知道向量内积运算结果满足以下定义
< q , k > = q 0 ∗ k 0 + q 1 ∗ k 1 = R e [ q k ∗ ] <q,k>=q_0*k_0+q_1*k_1=Re[qk^*] <q,k>=q0k0+q1k1=Re[qk],其中 k ∗ k^* k是复数域上k的共轭复数,即 k 0 − i k 1 k_0-ik_1 k0ik1,根据复数乘法运算规律可以知道 q k ∗ qk^* qk的实数域部分和向量内积的相等性;

因此上式求解等同
< f ( q , m ) , f ( k , n ) > = g ( q , k , m − n ) = R e [ f q f k ∗ ] <f(q,m),f(k,n)>=g(q,k,m-n)=Re[f_qf_k^*] <f(q,m),f(k,n)>=g(q,k,mn)=Re[fqfk],通过假设 f ( q , 0 ) = q , f ( k , 0 ) = k f(q,0)=q,f(k,0)=k f(q,0)=q,f(k,0)=k
求解 < f ( q , m ) , f ( k , n ) > = f q f k ∗ = G ( q , k , m − n ) <f(q,m),f(k,n)>=\textcolor{red}{f_qf_k^*=G(q,k,m-n)} <f(q,m),f(k,n)>=fqfk=G(q,k,mn),这里假设存在一个复数 G ( q , k , m − n ) G(q,k,m-n) G(q,k,mn),其实数域等于 g ( q , k , m − n ) g(q,k,m-n) g(q,k,mn)
在复数域的极坐标上得到方程组
f ( q , m ) = R f ( q , m ) e i Θ f ( q , m ) f ( k , n ) = R f ( k , n ) e i Θ f ( k , n ) G ( q , k , m − n ) = R G ( q , k , m − n ) e i Θ G ( q , k , m − n ) \begin{aligned} \boldsymbol{f}(\boldsymbol{q}, m) &= R_f (\boldsymbol{q}, m)e^{\text{i}\Theta_f(\boldsymbol{q}, m)} \\ \boldsymbol{f}(\boldsymbol{k}, n) &= R_f (\boldsymbol{k}, n)e^{\text{i}\Theta_f(\boldsymbol{k}, n)} \\ \boldsymbol{G}(\boldsymbol{q}, \boldsymbol{k}, m-n) &= R_G (\boldsymbol{q}, \boldsymbol{k}, m-n)e^{\text{i}\Theta_G(\boldsymbol{q}, \boldsymbol{k}, m-n)} \end{aligned} f(q,m)f(k,n)G(q,k,mn)=Rf(q,m)eiΘf(q,m)=Rf(k,n)eiΘf(k,n)=RG(q,k,mn)eiΘG(q,k,mn)
2. Rope定义
上述方程组的一个解可以是 f ( q , m ) = ∣ ∣ q ∣ ∣ e i ( θ q + m θ ) = q e i m θ f(q,m) = ||q||e^{\text{i}(\theta_q+m\theta)}=qe^{im\theta} f(q,m)=∣∣q∣∣ei(θq+mθ)=qeimθ,第二个等号成立因为这就是极坐标旋转的几何含义,||q||是q的模长, m θ m\theta mθ是q逆时针旋转的幅角度
验证一下
f ( q , m ) = q e i m θ f ( k , n ) = k e i n θ G ( q , k , m − n ) = f ( q , m ) f ( k , n ) ∗ = q e i m θ k e − i n θ = q k e i ( m − n ) θ \begin{aligned} \boldsymbol{f}(\boldsymbol{q}, m) &= qe^{im\theta} \\ \boldsymbol{f}(\boldsymbol{k}, n) &= ke^{in\theta} \\ \boldsymbol{G}(\boldsymbol{q}, \boldsymbol{k}, m-n) =f(q,m)f(k,n)^*&= qe^{im\theta}ke^{-in\theta}=qke^{i(m-n)\theta} \end{aligned} f(q,m)f(k,n)G(q,k,mn)=f(q,m)f(k,n)=qeimθ=keinθ=qeimθkeinθ=qkei(mn)θ
f ( q , m ) f(q,m) f(q,m)实现了绝对位置建模的同时,以向量内积的方式建模了相对距离m-n!!!

  1. Rope实现
    f ( q , m ) = ∣ ∣ q ∣ ∣ e i ( θ q + m θ ) = q e i m θ f(q,m) = ||q||e^{\text{i}(\theta_q+m\theta)}=qe^{im\theta} f(q,m)=∣∣q∣∣ei(θq+mθ)=qeimθ,可以很容易基于欧拉公式得到, e i m θ e^{im\theta} eimθ可以表示为复数 c o s m θ + i s i n m θ cosm\theta+isinm\theta cosmθ+isinmθ,通过复数的运算原理, q e i m θ qe^{im\theta} qeimθ等价于旋转矩阵与向量q的相乘,而这也是复数运算的几何意义,该变换实际上对应着向量的旋转;
    f ( q , m ) = ( cos ⁡ m θ − sin ⁡ m θ sin ⁡ m θ cos ⁡ m θ ) ( q 0 q 1 ) = R m q \boldsymbol{f}(\boldsymbol{q}, m) =\begin{pmatrix}\cos m\theta & -\sin m\theta\\ \sin m\theta & \cos m\theta\end{pmatrix} \begin{pmatrix}q_0 \\ q_1\end{pmatrix}=R_mq f(q,m)=(cosmθsinmθsinmθcosmθ)(q0q1)=Rmq
    在这里插入图片描述
    内积线性叠加性:内积是线性的,可以分解为多个部分的内积之和
    二维到n维的扩展:通过将高维向量分解为多个二维向量,并对每个二维向量应用相同的旋转矩阵。
    不同的 θ \theta θ:代表不同维度上的旋转角度,确保每个二维子空间独立旋转,这个原理类似之前的sincos位置编码中,对每个位置m的位置编码,不同维度有类似的设计,应该是为了增加不同位置之间的波长差异,防止在较远的position出现相似的位置编码造成远程衰减的失效
# easy code for Rope
由于R_m的稀疏性,可以将其分解为sin,cos两个部分,与整理后的q进行逐元素*实现上述旋转位置编码
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.int64).float().to(device) / dim))
#inv_freq 代表 theta,即10000^{-2i/d_model}, 从公式可以看到只需要一半,即i从[0-d/2-1]进行取值
freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2)
# m*theta, inv_freq*position ,每个position计算一个位置编码
emb = torch.cat((freqs, freqs), dim=-1)
#这里复制了一份,匹配q,k的长度,因为freqs 目前只有一半,这种复制直接在后面添加
 cos = emb.cos()
sin = emb.sin()

def rotate_half(x):
    """将后面一半元素反转到前面,并取负数"""
    x1 = x[..., : x.shape[-1] // 2]
    x2 = x[..., x.shape[-1] // 2:]
    return torch.cat((-x2, x1), dim=-1)

q_embed = (q * cos) + (rotate_half(q) * sin)
 k_embed = (k * cos) + (rotate_half(k) * sin)

从Qwen的代码看,与上文提到的两个元素一组进行翻转的公式不太一样了,以q为例子,真实的代码计算结果为:
在这里插入图片描述
这是一种高效的代码实现,不过是将向量q视为两部分,以类似二维向量旋转的公式来处理N维,将一半的元素视为一个整体元素进行翻转,二维情况的实现如下:
f ( q , m ) = ( cos ⁡ m θ − sin ⁡ m θ sin ⁡ m θ cos ⁡ m θ ) ( q 0 q 1 ) = ( c o s m θ c o s m θ ) ∗ ( q 0 q 1 ) + ( s i n m θ s i n m θ ) ∗ ( − q 1 q 0 ) \boldsymbol{f}(\boldsymbol{q}, m) =\begin{pmatrix}\cos m\theta & -\sin m\theta\\ \sin m\theta & \cos m\theta\end{pmatrix} \begin{pmatrix}q_0 \\ q_1\end{pmatrix}=\begin{pmatrix}cos m\theta\\cos m\theta \end{pmatrix}*\begin{pmatrix}q_0\\ q_1 \end{pmatrix}+\begin{pmatrix}sin m\theta\\sin m\theta \end{pmatrix}*\begin{pmatrix}-q_1\\ q_0 \end{pmatrix} f(q,m)=(cosmθsinmθsinmθcosmθ)(q0q1)=(cosmθcosmθ)(q0q1)+(sinmθsinmθ)(q1q0)

进一步地,旋转位置编码如何拓展到视觉场景Rope 2D,为了让patch的Embeding包含位置(x,y)的信息,可以采用以下方式,与1D的情况相比,位置编码的一半维度用于表示x,一半表示y, θ \theta θ取0-d/4-1即可
R x , y = ( cos ⁡ x θ − sin ⁡ x θ 0 0 sin ⁡ x θ cos ⁡ x θ 0 0 0 0 cos ⁡ y θ − sin ⁡ y θ 0 0 sin ⁡ y θ cos ⁡ y θ ) \boldsymbol{\mathcal{R}}_{x,y}=\left( \begin{array}{cc:cc} \cos x\theta & -\sin x\theta & 0 & 0 \\ \sin x\theta & \cos x\theta & 0 & 0 \\ \hdashline 0 & 0 & \cos y\theta & -\sin y\theta \\ 0 & 0 & \sin y\theta & \cos y\theta \\ \end{array}\right) Rx,y= cosxθsinxθ00sinxθcosxθ0000cosyθsinyθ00sinyθcosyθ

f ( q , x , y ) = ( cos ⁡ x θ − sin ⁡ x θ 0 0 sin ⁡ x θ cos ⁡ x θ 0 0 0 0 cos ⁡ y θ − sin ⁡ y θ 0 0 sin ⁡ y θ cos ⁡ y θ ) ( p 1 p 2 p 3 p 4 ) f(q,x,y) = \left( \begin{array}{cc:cc} \cos x\theta & -\sin x\theta & 0 & 0 \\ \sin x\theta & \cos x\theta & 0 & 0 \\ \hdashline 0 & 0 & \cos y\theta & -\sin y\theta \\ 0 & 0 & \sin y\theta & \cos y\theta \\ \end{array}\right)\begin{pmatrix}p_{1}\\p_{2}\\ \hdashline p_{3}\\p_{4} \end{pmatrix} f(q,x,y)= cosxθsinxθ00sinxθcosxθ0000cosyθsinyθ00sinyθcosyθ p1p2p3p4
在这里插入图片描述

基于Rope的长度外推能力(位置编码不同维度的频率角度)

## NTK
    def _dynamic_frequency_update(self, position_ids, device):
        """
        dynamic RoPE layers should recompute `inv_freq` in the following situations:
        1 - growing beyond the cached sequence length (allow scaling)
        2 - the current sequence length is in the original scale (avoid losing precision with small sequences)
        """
        seq_len = torch.max(position_ids) + 1
        if seq_len > self.max_seq_len_cached:  # growth
            inv_freq, self.attention_scaling = self.rope_init_fn(
                self.config, device, seq_len=seq_len, **self.rope_kwargs
            )
            self.register_buffer("inv_freq", inv_freq, persistent=False)  # TODO joao: may break with compilation
            self.max_seq_len_cached = seq_len

        if seq_len < self.original_max_seq_len and self.max_seq_len_cached > self.original_max_seq_len:  # reset
            self.register_buffer("inv_freq", self.original_inv_freq, persistent=False)
            self.max_seq_len_cached = self.original_max_seq_len
def _compute_dynamic_ntk_parameters(
    config: Optional[PretrainedConfig] = None,
    device: Optional["torch.device"] = None,
    seq_len: Optional[int] = None,
    **rope_kwargs,
) -> Tuple["torch.Tensor", float]:
#### 和标准Rope完全一致
    # seq_len: default to max_position_embeddings, e.g. at init time
    seq_len = seq_len if seq_len is not None and seq_len > max_position_embeddings else max_position_embeddings

    # Compute the inverse frequencies
    base = base * ((factor * seq_len / max_position_embeddings) - (factor - 1)) ** (dim / (dim - 2))
    inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.int64).float().to(device) / dim))
    return inv_freq, attention_factor


@torch.no_grad()
    def forward(self, x, position_ids):
        if "dynamic" in self.rope_type:
            self._dynamic_frequency_update(position_ids, device=x.device)

在应用NTK Rope时,每次对输入的x和position_ids,都会先在_dynamic_frequency_update函数看一下当前的长度是否超过了max_position_embeddings,如果超过则需要进行NTK内插,否则使用original_inv_freq,不做任何改变

在需要做内插的位置编码中,直接修改base,从而影响所有位置上的位置编码
base = base * ((factor * seq_len / max_position_embeddings) - (factor - 1)) ** (dim / (dim - 2))
当factor=1时,上式等于base*k
在这里插入图片描述
d / ( d / 2 ) d/(d/2) d/(d/2)在d较高时接近1,则K可以看做外推的factor,但是实际应用起来基本外推4倍左右PPL就会上升了,这一点在YARN中得到了改进

YARN描述

NTK,YARN来自同样的作者团队,位置编码的内插方案作用在Embeding上而不是position上,这与之前的很多技术方案不一致;考虑到Rope的位置编码后对相对位置关系的建模
< f ( q , m ) , f ( k , n ) > = R e [ q k ∗ e i ( m − n ) θ i ] <f(q,m),f(k,n)> = Re[qk^*e^{i(m-n)\theta_i}] <f(q,m),f(k,n)>=Re[qkei(mn)θi]
当m-n出现out of distribution的问题,模型可能会发生无法预测的行为造成效果下降,从极坐标旋转的角度看, e i ( m − n ) θ i e^{i(m-n)\theta_i} ei(mn)θi表示将向量逆时针旋转 ( m − n ) θ i (m-n)\theta_i (mn)θi,从sincos位置编码画的图可以看出 θ i = 1000 0 − 2 i / d \theta_i = 10000^{-2i/d} θi=100002i/d是单调递减函数:相同的m-n设置下,位置编码的低维元素旋转速度快,高维元素旋转速度慢,即低维高频、高频低维

m-n的取值为 0 − L t r a i n 0-L_{train} 0Ltrain,可以认为低维元素是充分训练的,而高维元素没有充分训练,如何定量的标识“充分”程度呢?既然是旋转,则可以求解每个 θ i \theta_i θi对应的周期,判断该元素是否在圆圈内充分训练:
T i = 2 π θ i T_i=\frac{2\pi}{\theta_i} Ti=θi2π
0 − L t r a i n 0-L_{train} 0Ltrain上,位置编码每个元素的最大旋转圈数为
γ i = L t r a i n T i = L t r a i n ∗ θ i 2 π \gamma_i=\frac{L_{train}}{T_i}=\frac{L_{train}*\theta_i}{2\pi} γi=TiLtrain=2πLtrainθi
γ i \gamma_i γi就是m-n取最大值的时候,位置编码旋转的圈数,这个圈数必须>1,才说明该m-n距离是得到充分训练的,否则需要内插,压缩旋转角度到训练期间见过的长度范围,定义如下:
θ i n e w = [ γ i + ( 1 − γ i ) L t r a i n L t e s t ] θ i , γ i = { 1 , r i > τ 0 , r i < 1 r i − 1 τ − 1 , others \begin{equation}\theta_i^{new} = \left[\gamma_i + (1 - \gamma_i)\frac{L_{train}}{L_{test}}\right]\theta_i,\quad \gamma_i = \left\{\begin{aligned}&1,&r_i > \tau \\ &0,&r_i < 1 \\ &\frac{r_i - 1}{\tau - 1},&\text{others} \end{aligned}\right.\end{equation} θinew=[γi+(1γi)LtestLtrain]θi,γi= 1,0,τ1ri1,ri>τri<1others

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值