Transformer inference
概述
参考:
架构
一个可以提供 NLP Inference(推理) 服务的架构,通常由下面几部分组成:
- 权重文件 # 这就是 Model,也可以称为”模型文件“。通常名为 model.safetensors 或 pytorch_model.bin。那些动辄几个 G 的文件就是这个权重文件。
- 配置文件 # 加载 分词器、权重 时的依据。是一组文件
- 处理代码 # 处理 用户输入 与 模型输出。总结一下:所有除了 ”模型“ 本身这个黑箱计算之外的工作,都需要代码。
关联文件与配置
权重相关文件
- model-safetensors # 模型的权重文件。也有可能名为 pytorch_model.bin。那些动辄几个 G 的文件就是这个权重文件。
- config.json # 模型结构配置。例如层数、隐藏层大小、注意力头数及 Transformers API 的调用关系等,用于加载、配置和使用预训练模型。
- configuration_chatglm.py # 是 config.json 文件的类表现形式,模型配置的 Python 类代码文件,定义了用于配置模型的 ChatGLMConfig 类。
- generation_config.json # 模型参数的默认值
分词器相关文件
- merges.txt # 词表文件。包含了 Token 合并的规则,用于将子 Token 合并成一个 Token。
- vocab.json # 词表文件。包含了 TokenID 到 Token字符串 的映射关系,以及其他与 Token 相关的信息。
- Tips: merges.txt 决定"怎么切",vocab.json 决定"切完之后每个 Token 叫什么 ID"。
- tokenizer.json # 可以理解为
vocab.json+merges.txt+ 其他数据的合并版。把 词表、合并规则、特殊 Token、etc. 所有数据打包在一起。 - tokenizer_config.json # 分词器的"行为配置文件",不存数据,存的是"怎么用这个分词器"的元信息。e.g. 是否添加特殊 Token、是否小写、etc. 。
推理流程
参考:
[!Note] 我们在 Hugging Face 的模型仓库中,通常可以看到除了代码之外的其他所有文件。至于代码,其实就是 transformers 依赖库。
还有人用 Model Packeage, Model Artifact, Model Repository, etc. 称呼这一整套完整的内容。
[!Note] 下面的演示基于 Qwen3-0.6B 模型
聊天模型接受消息列表(聊天记录)作为输入。每条消息都是一个字典,包含 role 和 content 键。要发起聊天,只需添加一条 role 为 user 消息即可。
还可以选择添加一条 system 消息,为模型提供行为指令。e.g.
{"role": "system", "content": "你是一位乐于助人的助手。"},
chat = [
{"role": "user", "content": "你好!"}
]
经过 Transfromer 处理(e.g. 分词、etc.)后,传给 Model。
Model 模型推理完成后,我们会看到模型的回复。像这样:
[
{
"generated_text": [
{"role": "user", "content": "你好!"},
{"role": "assistant", "content": "你好!有什么可以帮助你的吗?"}
]
}
]
如果想继续对话,需要将模型的回复更新到 chat 中。可以通过两种方式更新:
- 一是将文本追加到
chat中(使用assistant角色) - 二是将 chat 的值直接替换成
response[0]["generated_text"], 其中包含完整的聊天记录,包括传入的消息以及回复的消息。
之后,在 chat 中继续追加新的 user 角色及其 content,继续对话。
通过重复此过程,我们可以随心所欲地持续聊天,直到模型超出上下文窗口或内存不足为止。
加载分词器与模型
加载分词器
tokenizer: PreTrainedTokenizerBase = AutoTokenizer.from_pretrained(model_name)
加载模型
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto",
)
分词器处理输入
准备要输入给模型的文本
prompt = "hi, i'm DesistDaydream"
messages = [
{"role": "user", "content": prompt},
]
应用聊天模板
聊天语言模型自带 chat templates(聊天模板),这些模板定义了模型期望的聊天格式。我们可以使用 Transformer 的 apply_chat_template() 方法访问这些模板。利用分词器的 tokenizer_config.json 配置,将用户输入渲染成模型可以理解的格式,添加 ControlTokens(控制 Token),e.g. <|user|> 、 <|assistant|>、<|end_of_message|> 、etc. ,这些 ControlTokens 使模型能够识别聊天结构。聊天格式多种多样,即使模型是从同一个基础模型微调而来,它们也可能使用不同的 ControlTokens 或 格式!
text = tokenizer.apply_chat_template(
messages, # 要使用模板处理的消息列表
tokenize=False, # 是否将文本转换为 Token 序列。默认为 Ture。关闭后只输出渲染后的文本。
# return_tensors="pt", # 当 `tokenize` 参数为 `True` 时,可以传入该参数,将返回特定框架的张量。否则只返回 TokenIDs
# 'pt': 返回 PyTorch 的 `torch.Tensor` 对象。'np':返回 NumPy 的 `np.ndarray` 对象。
add_generation_prompt=True, # 是否在文本末尾添加生成提示。默认为 False 。 与 enable_thinking 配套使用,才能关闭思考模式。
enable_thinking=False, # 在思考和非思考模式之间切换。默认为 True 。
)
当我们打印 text 时,可以看到类似下面得内容:
>>> print(text)
<|im_start|>user
hi, i'm DesistDaydream<|im_end|>
<|im_start|>assistant
<think>
</think>
[!Attention] 这里面有点拧巴,其实应该只有 im_start 和 im_end 及其中间的部分,后面是因为关闭了思考模式出现的。这是因为 Qwen3-0.6B 训练时规定的就是这样的。
[!tip] 这个示例使用得是 Qwen3-0.6B 模型。可以看到,该模型的 ControlTokens 是
<|im_start|>,<|im_end|>, etc. 。这就是不同模型的聊天模板不同导致的。每个模型配套的资源都是有关联的,我们不能拿 A 模型的分词器去给 B 模型使用,模型内部是无法识别这些 Token 的。
编码
接下来,需要对这些已经渲染好的字符串进行 tokenize(分词),得到 Token sequence(Token 序列)(默认使用 merges.txt 文件?)
token_sequence = tokenizer.tokenize(text)
token_sequence 结果如下:
['<|im_start|>', 'user', 'Ċ', 'hi', ',', 'Ġi', "'m", 'ĠDes', 'ist', 'Day', 'dream', '<|im_end|>', 'Ċ', '<|im_start|>', 'assistant', 'Ċ', '<think>', 'ĊĊ', '</think>', 'ĊĊ']
[!important] 这个数组里,每个元素都是一个 Token,合起来就是 Token sequence
使用 Vocabulary(词表) 将 Token sequence 转为 Token IDs()(默认使用 vocab.json 文件?)
token_ids = tokenizer.convert_tokens_to_ids(token_sequence)
token_ids 结果如下:
[151644, 872, 198, 6023, 11, 600, 2776, 3874, 380, 10159, 56191, 151645, 198, 151644, 77091, 198, 151667, 271, 151668, 271]
使用 Token IDs 构造 Tensor(张量)。
input_ids = torch.tensor([token_ids]).to(model.device)
input_ids 结果如下:
tensor([[151644, 872, 198, 6023, 11, 600, 2776, 3874, 380,
10159, 56191, 151645, 198, 151644, 77091, 198, 151667, 271,
151668, 271]], device='cuda:0')
根据不同的模型处理输入(因果模型、掩码模型),这里使用的示例是 Qwen3-0.6B,需要添加掩码
attention_mask = torch.ones_like(input_ids)
inputs_ids_with_mask = {"input_ids": input_ids, "attention_mask": attention_mask}
inputs_ids_with_mask 结果如下:
{'input_ids': tensor([[151644, 872, 198, 6023, 11, 600, 2776, 3874, 380,
10159, 56191, 151645, 198, 151644, 77091, 198, 151667, 271,
151668, 271]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}
[!important] 传入模型的内容本质就是 input_ids 只不过 Qwen3-0.6B 这个模型需要 attention_mask 参数。但是还有很多模型不需要。所以 mask 只是 input_ids 的附属物。本质还是 input_ids
模型推理
output_ids = model.generate(**inputs_ids_with_mask, max_new_tokens=32768)
推理结果是一个 Tensor(张量),由于推理是黑盒,每次推理结果可能并不相同,以其中一次结果为例:
tensor([[151644, 872, 198, 6023, 11, 600, 2776, 3874, 380,
10159, 56191, 151645, 198, 151644, 77091, 198, 151667, 271,
151668, 271, 9707, 11, 358, 2776, 3874, 380, 10159,
56191, 0, 3555, 594, 389, 697, 3971, 30, 11162,
236, 101, 144232, 151645]], device='cuda:0')
之后我们需要将反向走一遍分词器处理输入的逻辑,把 Tensor 一步步解码成人类可读的信息
[!TODO] 推理原理是黑盒,略。如果有数学功底的话,可以在这里引入对 模型内部推理逻辑 的笔记。
分词器处理输出
解码
解构 Tensor 到 Token IDs。
为了性能,通常会在这里就去掉用户输入,使用
[len(input_ids[0]) :]只保留模型生成的部分返回给用户。
output_token_ids = output_ids[0][len(input_ids[0]) :].tolist()
结果如下:
[9707, 11, 358, 2776, 3874, 380, 10159, 56191, 0, 3555, 594, 389, 697, 3971, 30, 11162, 236, 101, 144232, 151645]
去掉 Control Token
output_token_ids_filtered = []
for tid in output_token_ids:
if tid not in tokenizer.all_special_ids:
output_token_ids_filtered.append(tid)
转换 Token IDs 为 Token sequence。
output_token_sequence = tokenizer.convert_ids_to_tokens(output_token_ids)
结果如下:
['Hello', ',', 'ĠI', "'m", 'ĠDes', 'ist', 'Day', 'dream', '!', 'ĠWhat', "'s", 'Ġon', 'Ġyour', 'Ġmind', '?', 'ĠðŁ', 'İ', '¨', '⾨']
合词,将所有 Token 合并成字符串
output_text = tokenizer.convert_tokens_to_string(output_token_sequence) # type: ignore
结果如下:
Hello, I'm DesistDaydream! What's on your mind? 🎨✨
下面的内容与解码流程无关
模型的推理输出的内容是这样的,上面 解构 Tensor 的时候我们把 think 以及前面的内容全都去掉了
<|im_start|>user
hi, i'm DesistDaydream<|im_end|>
<|im_start|>assistant
<think>
</think>
Hello, I'm DesistDaydream! What's on your mind? 🎨✨<|im_end|>
[!quote] 其实
convert_ids_to_tokens(),decode()这些方法里都有有 skip_special_tokens 参数,可以处理 ControlToken,有太多太多的包装方法可以直接处理。只不过这里为了演示流程,所以都没用这些。
应用聊天模板
将模型的回复,填充到 {"role": "assistant", "content": "AssistantReply"} 的 AssistantReply 处。
随后将这条消息 append 到 message 中,以供下一次对话一起传模型继续聊天。
messages.append({"role": "assistant", "content": output_text})
最终,messages 将会变为:
[
{
"role": "user",
"content": "hi, i'm DesistDaydream"
},
{
"role": "assistant",
"content": "Hello, I'm DesistDaydream! What's on your mind? 🎨✨"
}
]
最终
至此,与模型的一次对话完成,后续再想对话,只需再次对 messages.append({"role": "user", "content": prompt}}),把历史上下文都带着再次执行一遍推理流程,即可通过重复此过程,我们可以随心所欲地持续聊天,直到模型超出上下文窗口或内存不足为止。
实际上,相对最简单的方式是:
# 输入
messages = [
{"role": "user", "content": "hi, i'm DesistDaydream"},
]
# 编码
inputs_ids_with_mask = tokenizer.apply_chat_template(
messages, # 要使用模板处理的消息列表
tokenize=True, # 是否将文本转换为 Token 序列。默认为 Ture。关闭后只输出渲染后的文本。
return_tensors="pt", # 返回 Tensor。只有当 tokenize 为 True 时才有效。
add_generation_prompt=True, # 是否在文本末尾添加生成提示。默认为 False 。
enable_thinking=False, # 在思考和非思考模式之间切换。默认为 True 。
).to(model.device)
# 推理
output_ids = model.generate(**inputs_ids_with_mask, max_new_tokens=32768) # type: ignore
# 解码
assistant_reply = tokenizer.decode(
output_ids[0][len(inputs_ids_with_mask["input_ids"][0]) :],
skip_special_tokens=True,
)
# 输出
messages.append({"role": "assistant", "content": assistant_reply})
transformers 库甚至还有更直接的 pipeline 方法。随着发展,肯定包装得会越来越完善,调用起来越来越简单。
数据结构
参考:
发送给模型的消息通常包含两部分
- conversation # 对话,i.e. 人们日常交流以及代码里常用 messages。
- tools # 工具。LLM 会根据工具信息,以及 conversation 中的内容判断是否需要使用工具。
TODO: 待整理。
从上面的推理流程可以看出来
所有语言模型(无论是否经过聊天训练)都遵循一个 TokensSequence(Token 序列)。训练因果语言模型时,通常先在庞大的文本语料库上进行 “pre-training(预训练)”,从而创建一个“基础”模型。这些基础模型随后通常会针对聊天进行 “fine-tuned(微调)”,这意味着使用格式化为消息序列的数据进行训练。
Conversation
conversation 可用的常见 role 包括:
- user # 来自用户的消息
- assistant # 模型推理的结果
- system # 用于指示模型如何行动的指令(通常位于聊天开始处)
- tool # 工具执行后的响应结果
常见的 messages。每一次对话都是一个元素。下面是一个只有用户输入 ”你好“ 的 messages
[
{"role": "user", "content": "你好"},
]
对于多模态模型来说,输入的 content 应该包含类型,比如:
messages = [
{"role": "user", "content": [{"type": "text", "text": "你好"}]},
]
[!Attention] 非多模态的模型基本无法识别这种消息结构。
如果有 图片、音视频、etc. 、etc. ,还可以使用其他 type,并指定 文件系统路径 或 URL,由分词器读取后,转换为张量
Tools
参考:
[!Attention] 模型本身无法直接调用工具
模型会响应结构化的信息,由
tool_call包裹,就像这样:<tool_call>格式化的工具调用参数</tool_call>。我们需要编写程序处理这些信息,由程序去执行具体的工具。
一个工具的典型结构示例如下:
tools := [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如:北京",
}
},
"required": ["city"],
},
},
}
]
通常,模型响应的信息中,工具调用应该放在 "role":"assistant" 的 messages 中,使用 tool_calls 键
{
"role": "assistant",
"content": "获取天气",
"tool_calls": [
{
"id": "call_00_tO9WlaA7bYyLG89jjiVV4rwe",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"name\": \"get_weather\", \"arguments\": {\"city\": \"北京\"}}"
}
}
]
}
工具的执行结果应该放在 "role":"tool"。之后,程序将这些 messages 交给模型进行分析。继续执行后续任务
最佳实践
性能和内存使用情况
https://huggingface.co/docs/transformers/en/conversations#performance-and-memory-usage
Transformer 默认以全精度 float32 加载模型,对于一个 80 亿的模型来说,这需要大约 32GB 的内存!使用 torch_dtype=“auto” 参数可以减少内存占用,该参数通常会对使用 bfloat16 训练的模型使用 bfloat16 精度。
反馈
此页是否对你有帮助?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.