0. 背景
本文主要介绍 hugging face
托管的 LLM
如何下载,并用 transformers repo
推理
1. 相关工具介绍
hugging face
是一个托管 LLM
的网站,类似 github / dockerhub
,可以上传下载大模型,也可以在线推理等
transformers
是一个 hugging face
维护的开源大模型代码库,包含约 300
个开源大模型的网络结构,可以配合 hugging face
下载的大模型,实现一键推理
2. 模型文件下载
2.1 下载工具
hugging face
有专用工具 huggingface-cli
,作用类似于低配版 git
,可以实现远程 hugging face repo
的登录、登出、上传、下载等
对于 Qwen2-0.5B-Instruct
模型,huggingface-cli
可直接免登录下载,而例如 Llama3.2-1B
等模型需要向 meta
提交申请并获得同意后下载
2.2 下载文件内容
hugging face
模型文件格式较为固定,一般为:
config.json
:
这个文件包含了模型的配置信息,比如模型的架构参数、词表大小、激活函数类型等
它通常用于在加载模型时,确保模型的配置与原始模型一致
generation_config.json
:
这个文件包含了生成文本时的配置信息,比如最大长度、停止序列、解码相关配置(温度、top-k
、top-p
、惩罚系数)等,以及依赖的 transformers
版本
这些参数影响模型生成文本的行为
LICENSE
:
这个文件包含了模型的许可证信息,说明了你可以如何使用这个模型,以及使用时需要遵守的法律条款
merges.txt
:
这个文件通常用于字节对编码(Byte Pair Encoding, BPE
)分词器,它包含了词汇的合并规则
BPE
是一种用于创建词汇表和编码文本的算法
model.safetensors
:
这是一个保存模型权重的文件,使用了 SafeTensors
格式
这是一种高效的文件格式,用于存储和加载深度学习模型的权重
README.md
:
这个文件包含了模型的说明文档,通常包括模型的简介、如何使用模型、训练细节、性能指标等信息
tokenizer_config.json
:
这个文件包含了分词器的配置信息,比如分词器的类型、是否使用 BPE
等
tokenizer.json
:
这个文件包含了分词器的预训练信息,比如词汇表、特殊标记等
它用于将文本转换为模型可以理解的数值输入
vocab.json
:
这个文件包含了模型的词汇表,即模型在训练时使用的所有单词或标记的列表
分词器使用这个词汇表将文本分割成模型可以理解的输入
Qwen2-0.5B-Instruct
中的 "Instruct"
是指此模型是经过指令遵循微调后的模型
2.3 模型参数解析
下载的文件中 model.safetensors
就是模型参数
safetensors
是由 Hugging Face
开发的一种新型文件格式,专门用于安全地存储和加载机器学习模型的权重,这种格式特别关注模型的安全性、隐私保护和快速加载
安全性:safetensors
格式的设计目标之一是提高模型存储的安全性,它不允许执行代码,从而减少了模型文件被恶意篡改的风险
快速加载:与传统的 PyTorch
序列化格式(如 .pth
或 .bin
)相比,safetensors
可以更快地加载模型权重,尤其是在 CPU
和 GPU
上
跨语言和跨框架兼容性:safetensors
支持在不同的编程语言和机器学习框架之间共享模型权重,例如可以在 Python
中序列化并在 C++ / Java / JavaScript
中加载
懒加载:支持懒加载,即可以选择只读取文件中的一部分张量,这对于分布式设置中的多节点或 GPU
非常有用
如何解析一个 model.safetensors
文件?
1 2 3 4 5 6 from safetensors import safe_opentensors = {} with safe_open("model.safetensors" , framework="pt" , device="cpu" ) as f: for key in f.keys(): tensors[key] = f.get_tensor(key)
3. 构建模型并加载预训练参数
1 2 3 4 from transformers import AutoModelForCausalLMmodel = AutoModelForCausalLM.from_pretrained( model_path, torch_dtype="auto" , device_map="auto" )
hugging face
的 transformers
库提供了一个 AutoModelForCausalLM
类,可以根据模型路径,读取配置文件并自动加载模型
4. 输入构建
输入构建分成三个部分:
初始化 tokenizer
prompt
指令化
text to token id
(tokenize
)
4.1 初始化 tokenizer
1 2 from transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained(model_path)
transformers
会自动读取 tokenizer
相关配置文件,创建对应的 tokenizer
对象
4.2 prompt
指令化
1 2 3 4 5 6 7 8 9 10 11 12 13 prompt = "艾萨克牛顿是谁?" messages = [ { "role" : "system" , "content" : "You are Qwen, created by Alibaba Cloud. You are a helpful assistant." , }, {"role" : "user" , "content" : prompt}, ] text = tokenizer.apply_chat_template( messages, tokenize=False , add_generation_prompt=True , )
上面的 message
是 Qwen2
指令微调后模型的输入格式,包含了 role
和 content
两个字段
具体来说需要两个步骤:
prompt to message
:将 prompt
转换为 message
格式
message to chat
:在 message
中插入特殊字符,形成 chat
格式,特殊字符主要包括:
<|im_start|>
:表示对话开始
<|im_end|>
:表示对话结束
\n
:间隔 role
和 content
最终生成的 text
实际内容为:
1 <|im_start |>system \nYou are Qwen, created by Alibaba Cloud. You are a helpful assistant.<|im_end |>\n <|im_start |>user \n艾萨克牛顿是谁?<|im_end |>\n <|im_start |>assistant
一些额外知识:
prompt
输入模板信息在 tokenizer_config.json
中被定义过,key
为 chat_template
: 1 {% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n' }}{% endif %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}
模板用到的代码是 Jinja2
写的,Jinja2
是一种模板引擎,tokenizer
会根据这个模板生成 chat
格式的 text
代码中的 messages
和 add_generation_prompt
是模板的输入参数
4.3 tokenize
(text to token id
)
1 model_inputs = tokenizer([text], return_tensors="pt" ).to(model.device)
这个过程就是 tokenize
,将 text
转换为 token id
首先根据 tokenizer.json
中声明的 token encode
方法(例如 BPE
),将 text
分词
然后根据 vocab.json
中的词汇表,将分词后的词转换为 token id
(查表)
最后将 token id
转换为 tensor
格式(tensor of uint64 dtype
)
5. 模型前向传播
1 2 3 4 generated_ids = model.generate( **model_inputs, max_new_tokens=512 , )
generate
方法是 transformers
提供的一个高级封装方法,可以实现 LLM
的推理
max_new_tokens
参数表示最大生成的 token
数量
generate
方法会自动迭代调用模型的前向传播方法和解码方法,实现文本生成
本章节的重点关注 generate
的模型推理过程,可以分成:
token id to token embedding
position emebedding
构造
attention mask
构造
decoder layer forward
5.1 token id to token embedding
nn.Embedding
本质就是查表,将 token id
转换为 token embedding
5.2 position emebedding
构造
Qwen2
模型使用的 position embedding
方法是 RoPE (rotary position embedding)
5.2.1 RoPE
介绍
RoPE
的核心思想是将 query embedding
和 key embedding
旋转一定角度,让模型感知到序列中的位置信息。
RoPE
的输入是:
position id
(例如:[0, 1, 2, ..., seq_len - 1]
)
query embedding (shape = [seq_len, head_dim])
key embedding (shape = [seq_len, head_dim])
RoPE
的输出是:
经过 RoPE
处理后的 query embedding
经过 RoPE
处理后的 key embedding
RoPE
的计算过程是:
构造 cos ∈ R s e q _ l e n × h e a d _ d i m \cos\in\mathbb{R}^{seq\_len\times head\_dim} cos ∈ R s e q _ l e n × h e a d _ d i m 和 sin ∈ R s e q _ l e n × h e a d _ d i m \sin\in\mathbb{R}^{seq\_len\times head\_dim} sin ∈ R s e q _ l e n × h e a d _ d i m 两个矩阵
用两个矩阵去旋转 query embedding
和 key embedding
RoPE
的实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 inv_freq = 1.0 / (1000000 ** (torch.arange(0 , dim, 2 , dtype=torch.int64).float ().to(device) / dim)) freq = inv_freq_expanded.float () @ position_ids_expanded.float () emb = torch.cat((freqs, freqs), dim=-1 ) cos = emb.cos()[None , None , ...] sin = emb.sin()[None , None , ...] def rotate_half (x ): x1 = x[..., : x.shape[-1 ] // 2 ] x2 = x[..., x.shape[-1 ] // 2 :] return torch.cat((-x2, x1), dim=-1 ) q = (q * cos) + (rotate_half(q) * sin) k = (k * cos) + (rotate_half(k) * sin)
以上 RoPE
的公式解释:
x m , i ′ = x m , i ⋅ cos ( m θ i ) − x m , i + d / 2 ⋅ sin ( m θ i ) x'_{m,i}=x_{m,i}\cdot \cos (m\theta_i)-x_{m,i+d/2}\cdot \sin (m\theta_i) x m , i ′ = x m , i ⋅ cos ( m θ i ) − x m , i + d / 2 ⋅ sin ( m θ i )
x m , i + d / 2 ′ = x m , i ⋅ cos ( m θ i ) + x m , i + d / 2 ⋅ sin ( m θ i ) x'_{m,i+d/2}=x_{m,i}\cdot \cos (m\theta_i)+x_{m,i+d/2}\cdot \sin (m\theta_i) x m , i + d / 2 ′ = x m , i ⋅ cos ( m θ i ) + x m , i + d / 2 ⋅ sin ( m θ i )
其中:
x m , i x_{m,i} x m , i 是 query embedding
或 key embedding
的第 m m m 个位置的第 i i i 个维度的值
θ i = 100000 0 − 2 i d \theta_i=1000000^{-\frac{2i}{d}} θ i = 1 0 0 0 0 0 0 − d 2 i
m θ i = m ⋅ 100000 0 − 2 i d m\theta_i=m\cdot 1000000^{-\frac{2i}{d}} m θ i = m ⋅ 1 0 0 0 0 0 0 − d 2 i ,m m m 是位置 id
d d d 是 head_dim
Qwen
模型中的 RoPE
实现代码是 RoPE
的一个简化版本,主要是为了提高计算效率,官方的 RoPE
计算公式如下:
x m , 2 i ′ = x m , 2 i ⋅ cos ( m θ i ) − x m , 2 i + 1 ⋅ sin ( m θ i ) x'_{m,2i}=x_{m,2i}\cdot \cos (m\theta_i)-x_{m,2i+1}\cdot \sin (m\theta_i) x m , 2 i ′ = x m , 2 i ⋅ cos ( m θ i ) − x m , 2 i + 1 ⋅ sin ( m θ i )
x m , 2 i + 1 ′ = x m , 2 i ⋅ cos ( m θ i ) + x m , 2 i + 1 ⋅ sin ( m θ i ) x'_{m,2i+1}=x_{m,2i}\cdot \cos (m\theta_i)+x_{m,2i+1}\cdot \sin (m\theta_i) x m , 2 i + 1 ′ = x m , 2 i ⋅ cos ( m θ i ) + x m , 2 i + 1 ⋅ sin ( m θ i )
R d Θ , m x = [ x 1 x 2 x 3 x 4 ⋮ x d − 1 x d ] ⊗ [ cos ( m θ 1 ) cos ( m θ 1 ) cos ( m θ 2 ) cos ( m θ 2 ) ⋯ cos ( m θ d / 2 ) cos ( m θ d / 2 ) ] + [ − x 2 x 1 − x 4 x 3 ⋮ − x d x d − 1 ] ⊗ [ sin ( m θ 1 ) sin ( m θ 1 ) sin ( m θ 2 ) sin ( m θ 2 ) ⋯ sin ( m θ d / 2 ) sin ( m θ d / 2 ) ] \mathbf{R}_{d\Theta,m}\mathbf{x} = \begin{bmatrix} \mathbf{x}_1 \\ \mathbf{x}_2 \\ \mathbf{x}_3 \\ \mathbf{x}_4 \\ \vdots \\ \mathbf{x}_{d-1} \\ \mathbf{x}_d \end{bmatrix} \otimes \begin{bmatrix} \cos(m\theta_1) \\ \cos(m\theta_1) \\ \cos(m\theta_2) \\ \cos(m\theta_2) \\ \cdots \\ \cos(m\theta_{d/2}) \\ \cos(m\theta_{d/2}) \end{bmatrix} + \begin{bmatrix} -\mathbf{x}_2 \\ \mathbf{x}_1 \\ -\mathbf{x}_4 \\ \mathbf{x}_3 \\ \vdots \\ -\mathbf{x}_d \\ \mathbf{x}_{d-1} \end{bmatrix} \otimes \begin{bmatrix} \sin(m\theta_1) \\ \sin(m\theta_1) \\ \sin(m\theta_2) \\ \sin(m\theta_2) \\ \cdots \\ \sin(m\theta_{d/2}) \\ \sin(m\theta_{d/2}) \end{bmatrix} R d Θ , m x = ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ x 1 x 2 x 3 x 4 ⋮ x d − 1 x d ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ ⊗ ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ cos ( m θ 1 ) cos ( m θ 1 ) cos ( m θ 2 ) cos ( m θ 2 ) ⋯ cos ( m θ d / 2 ) cos ( m θ d / 2 ) ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ + ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ − x 2 x 1 − x 4 x 3 ⋮ − x d x d − 1 ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤ ⊗ ⎣ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎢ ⎡ sin ( m θ 1 ) sin ( m θ 1 ) sin ( m θ 2 ) sin ( m θ 2 ) ⋯ sin ( m θ d / 2 ) sin ( m θ d / 2 ) ⎦ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎥ ⎤
区别在于:
官方的 RoPE
是同一个位置的相邻维度(2i 和 2i+1
)之间旋转
Qwen
的 RoPE
是同一个位置的跳跃维度(i 和 i+d/2
)之间旋转
5.3 attention mask
构造
实现中,attention mask
实际上并不需要构造,因为 torch.nn.functional.scaled_dot_product_attention
的 is_causal
参数可以自动实现 attention mask
具体来说:
torch.nn.functional.scaled_dot_product_attention
的 is_causal == True
时,会自动构造一个 attention bias
,shape = [query_len, key_len]
,其中 bias[i, j] = -inf if i > j else 0
在 query @ key.transpose(-2, -1)
得到 attention score
后,会自动加上这个 attention bias
然后再经过 softmax
,这样就实现了 causal attention
而且 causal attention
在 GPT-like LLM
中,只在 prefill
阶段使用,generate
阶段不使用(或者说隐式使用)
5.4 decoder layer forward
LLM
的计算量几乎都体现在 decoder layer forward
上
从 layer norm
位置角度来看,Qwen2
模型属于 pre-LM
,即:
x = x + M H A ( L a y e r N o r m ( x ) ) x=x+MHA(LayerNorm(x)) x = x + M H A ( L a y e r N o r m ( x ) )
x = x + F F N ( L a y e r N o r m ( x ) ) x=x+FFN(LayerNorm(x)) x = x + F F N ( L a y e r N o r m ( x ) )
x = L a y e r N o r m ( x ) x = LayerNorm(x) x = L a y e r N o r m ( x )
模型包含 24
层 decoder layer
,每层包含 2
个子层:
multi-head self-attention
(MHA
)
feed forward network
(FFN
)
为了减小计算量和参数量,Qwen2
模型使用了 GQA (grouped query attention)
方法:
对于 query
来讲,每层 MHA
包含 14
个头,每个头的 head_dim = 64
对于 key
和 value
来讲,每层 MHA
包含 2
个头,每个头的 head_dim = 64
计算时,需要将 key
和 value
的 head
通过 repeat
扩展到 14
个头
同时,Qwen2
使用了 KV cache
算法,即:
在 prefill
阶段,key
和 value
会被缓存下来
在 generate
阶段,之前的 key
和 value
会被直接使用不再计算,只计算最后一个 token
的 query / key / value
在 prefill
阶段:
输入为 hidden state
,shape = [1, 36, 896]
,其中 36
是 seq_len
,896
是 hidden_dim
输入逐层经过 decoder layer
,最后输出 hidden state
,和输入 shape
一样(shape = [1, 36, 896]
)
在这一阶段,每一层的每个头的每个位置(token position
)的 key
和 value
都会被缓存下来,shape = [2, 1, 24, 2, 36, 64]
,其中:
2
是 key / value
1
是 batch_size
24
是 layers
36
是 seq_len
2
是 key_value_head_num
64
是 head_dim
每个 decoder layer
层的 MHA
都需要 causal attention
,即 is_causal = True
输出的 hidden state
的 最后一个 token position
的 feature
(shape = [1, 896]
) 会被输入到 fc
层,输出 shape = [1, 151936]
,151936
是 vocab_size
然后通过 decode method
得到最终的 token id
在 generate
阶段:
输入为 预填充阶段最终输出的 token id
对应的 token embedding
,shape = [1, 1, 896]
输入逐层经过 decoder layer
,最后输出 hidden state
,和输入 shape
一样(shape = [1, 1, 896]
)
在这一阶段,每一层的每个头的之前的位置(token position
)的 key
和 value
都会被直接使用,不再计算;只计算最后一个 token
的 query / key / value
,并将计算得到的 key / value
缓存下来,缓存的 key / value
会拼接到之前的 key / value
上,形成新的 key / value
,shape = [2, 1, 24, 14, 36 + 1, 64]
每个 decoder layer
层的 MHA
的输入 shape
如下:
query
: shape = [1, 14, 1, 64]
key / value
: shape = [1, 14, 37, 64]
is_causal = False ,因为 query
长度为 1
,不需要 causal attention
最后一层 decoder layer
的输出经过 fc
和 decode method
得到最终的 token id
重复直到生成的 token id
数量达到 max_new_tokens
或生成的 token id
为 eos
为止
6. decode method
decode method
实际上属于模型 forward
过程中的一部分,但是为了方便理解,这里单独拎出来讲
decode method
的作用是将 vocab logits
映射到 token id
最简答的 decode method
是 argmax
方法,即取 vocab logits
中最大的值对应的 token id
,但是这种方法会导致生成的文本过于单一
实际上 Qwen2
使用了五个串联的 decode method
,分别是:
RepetitionPenaltyLogitsProcessor
TemperatureLogitsWarper
TopKLogitsWarper
TopPLogitsWarper
SoftmaxSampler
6.1 RepetitionPenaltyLogitsProcessor
RepetitionPenaltyLogitsProcessor
的作用是惩罚重复的 token id
,即:
在预测的 vocab logits
中,找到之前所有 token id
对应的 logits
如果 logits > 0
,则将 logits
除以一个 penalty
,penalty
的值取 1.1
如果 logits < 0
,则将 logits
乘以一个 penalty
,penalty
的值取 1.1
目的是让模型更倾向于生成不重复的 token id
6.2 TemperatureLogitsWarper
TemperatureLogitsWarper
的作用是调整 vocab logits
的温度,即:
将 vocab logits
除以一个 temperature
,temperature
的值取 0.7
目的是提高模型的生成多样性
6.3 TopKLogitsWarper
TopKLogitsWarper
的作用是截断 vocab logits
,即:
保留 vocab logits
中最大的 k
个值,其他值置为 -inf
,k
的值取 20
目的是降低模型的生成多样性,提高生成的准确性
6.4 TopPLogitsWarper
TopPLogitsWarper
的作用是截断 vocab probs
,即:
将 vocab logit
通过 softmax
变成 vocab probs
将 vocab probs
从大到小排序,累加到 p
大于 top-p
为止,保留这些 vocab logits
,其他值置为 -inf
top-p
的值取 0.8
,最终至少需要保留一个 token id
目的是降低模型的生成多样性,提高生成的准确性
6.5 SoftmaxSampler
将 vocab logits
(此时只有少量元素不是 -inf
) 通过 softmax
变成 vocab probs
根据 vocab probs
采样一个 token id
,作为最终的输出
7. 总结
本文主要介绍了 hugging face
和 transformers
的使用方法,以及 Qwen2-0.5B-Instruct
模型的推理过程
GPT-like
模型的推理过程主要包括 tokenize
、RoPE
、attention mask
、decoder layer forward
、decode method
等步骤
Qwen2
模型使用了 RoPE
、GQA
、KV cache
等技术,提高了模型的计算效率和参数量
decode method
使用了 RepetitionPenaltyLogitsProcessor
、TemperatureLogitsWarper
、TopKLogitsWarper
、TopPLogitsWarper
、SoftmaxSampler
等方法,提高了模型的生成多样性和准确性