最小可运行示例的价值,在于用最短路径看到真实行为,再回头补机制。
本章用两个递进的示例建立“vLLM 能运行、能生成、能并发”的基础心智模型:先用 Python API 完成一次最小推理并包一层 HTTP 服务,再用一个三条并发请求的脚本体验 Batch 推理为什么快。所有实验在环境准备的 macOS 环境上运行,聚焦行为本身,不深入引擎内部机制。
Python API 最小推理
用最简代码实现一次 vLLM 推理,建立基础认知。
目标:建立“vLLM 能运行、能生成、能服务”的最小心智模型,不深入原理,只确保能看到真实行为。
我们将在 macOS 上实现高效本地推理,作为理解平台兼容性与性能优化的实战演练。
产出物:
- 用 Python API 完成一次最小推理
- 用 curl 测试 SSE(流式输出)
- 从日志看到一次推理的生命周期(最小版 request → schedule → output)
本地推理方案
在 macOS 平台,推荐采用 Transformers 库(Hugging Face Transformers) 或自建 Flask API 服务器(Flask Web Framework) 进行推理。两者各有优势,适合不同场景。
Transformers 库适合开发测试场景,稳定性高,易于快速原型开发。
下面是一个典型的推理脚本示例,展示如何加载模型并生成响应,我们使用的是 Qwen/Qwen2.5-1.5B-Instruct 模型,这个模型大小适中(约 2.9 GB),适合本地推理。
▸
模型下载(可选)
Transformers 的 AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-1.5B-Instruct") 在本地没有模型时会尝试从 Hugging Face 自动下载(需要网络和相应权限/凭证)。
如果你希望使用本地已下载的模型,直接把模型放在某个目录下,然后传入该本地路径:
# 自动下载(若本地不存在)
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-1.5B-Instruct")
# 使用本地路径(不会触发网络下载)
model = AutoModelForCausalLM.from_pretrained("/path/to/local/Qwen-Qwen2.5-1.5B-Instruct")
如果在无网络或受限环境中运行,请提前手动下载并解压模型到本地路径,然后使用本地路径加载。
模型默认存储在 ~/.cache/huggingface/hub 目录下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
| #!/usr/bin/env python3
"""
vLLM 最小化推理示例 - 使用 Hugging Face transformers 库进行大模型推理
这个脚本演示了如何使用预训练的大语言模型进行文本生成。
"""
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
# ============================================================================
# 第一步:加载预训练的大模型和分词器
# ============================================================================
print("Loading model...")
# 模型标识符,指向 Hugging Face 模型库中的 Qwen 2.5 1.5B 指令微调版本
# Qwen/Qwen2.5-1.5B-Instruct 是一个 15 亿参数的轻量级指令跟随模型
model_name = "Qwen/Qwen2.5-1.5B-Instruct"
# AutoTokenizer: 自动加载与模型配套的分词器
# 分词器的作用是将文本转换为模型能理解的 token(令牌/词元)
# 具有相反的解码功能:将 token 转回文本
tokenizer = AutoTokenizer.from_pretrained(model_name)
# AutoModelForCausalLM: 加载因果语言模型(Causal Language Model)
# 因果语言模型:只能看到当前位置之前的 token,用于文本生成任务
#
# 数据类型选择与优化:
# - torch.float16 (FP16): GPU 推理的标准选择,节省 50% 显存,加速计算
# 但在某些 CPU 场景下可能数值不稳定
# - torch.bfloat16 (BF16): 更好的数值稳定性,特别是在 CPU 上,推荐优先使用
# BF16 保留与 FP32 相同的指数范围,只牺牲尾数精度,更适合深度学习
# - torch.float32: 精度最高但显存占用最多(2 倍 FP16),一般不用于推理
# 设备选择策略:
# - 优先选择 CUDA(NVIDIA GPU)- 性能最好,生态最完善
# - 其次 CPU - 稳定可靠,但速度较慢
# - 避免 MPS(Apple GPU)- 某些操作支持不完整,容易出现 OOM 错误
device = torch.device("cpu") # Mac 用户推荐使用 CPU,避免 MPS 兼容性问题
if torch.cuda.is_available():
device = torch.device("cuda")
# 选择 bfloat16 作为默认数据类型,兼容 GPU 和 CPU,更稳定
dtype = torch.bfloat16
# 直接指定设备而不用 device_map="auto",避免设备选择的不确定性
model = AutoModelForCausalLM.from_pretrained(model_name, dtype=dtype, device_map=device)
# ============================================================================
# 性能和稳定性优化
# ============================================================================
# 启用梯度检查点(Gradient Checkpointing):
# 虽然推理时不需要梯度,但启用此优化对以下场景有益:
# 1. 为后续微调预留显存空间
# 2. 在显存受限的环境中运行较大的模型
# 原理:不缓存所有前向传播的中间激活值,而是需要时重新计算
# 权衡:用计算时间换取显存空间(通常减少 50% 显存占用,增加约 20% 推理时间)
model.gradient_checkpointing_enable()
# 启用 eval 模式:禁用 dropout、batch normalization 等训练特定的行为
# 优势:
# 1. 输出更稳定,同样输入每次得到完全相同的输出(复现性强)
# 2. 略微提升推理速度
# 3. 避免随机失活层的影响
model.eval()
# 设置最大序列长度限制(用于防止显存溢出 OOM)
# Qwen2.5-1.5B 的完整上下文窗口可达 32k tokens,但这会占用大量显存
# 这里限制到 2048 是实际应用中的合理折衷
# 如果需要更长的上下文,可以使用 sliding window attention 等优化技术
max_sequence_length = 2048
print(f"Model loaded on {device} with dtype {dtype}!")
# ============================================================================
# 第二步:准备输入数据 - 构建对话消息
# ============================================================================
# 定义聊天消息,遵循标准的对话格式
# role: 消息的角色(user=用户提问,assistant=模型回复)
messages = [
{"role": "user", "content": "Hello, how are you?"},
]
# apply_chat_template: 将对话消息转换为模型期望的格式
# 模型被微调为处理特定的提示模板,此方法自动应用这个模板
text = tokenizer.apply_chat_template(
messages,
# tokenize=False: 返回格式化的字符串而不是 token(我们稍后手动 tokenize)
tokenize=False,
# add_generation_prompt=True: 在末尾添加特殊 token,指示模型开始生成
# 这确保了模型会继续文本而不是停止或反复输入
add_generation_prompt=True
)
# tokenizer(...): 现在将格式化的文本转换为 token ID 张量
# return_tensors="pt": 返回 PyTorch 张量而不是列表
# model_inputs 包含 input_ids(token ID 序列)和 attention_mask(注意力掩码)
model_inputs = tokenizer([text], return_tensors="pt")
# 可选:如果输入序列过长,可以截断以节省显存
# 但会丢失上文信息,需要权衡
if model_inputs.input_ids.shape[1] > max_sequence_length:
model_inputs.input_ids = model_inputs.input_ids[:, -max_sequence_length:]
if "attention_mask" in model_inputs:
model_inputs.attention_mask = model_inputs.attention_mask[:, -max_sequence_length:]
# 重要:将输入张量移动到模型所在的设备
# 如果设备不匹配(如模型在 GPU,输入在 CPU),会导致运行时错误
# model_inputs 是一个字典,包含 input_ids、attention_mask 等张量
model_inputs = {k: v.to(device) for k, v in model_inputs.items()}
# ============================================================================
# 第三步:执行模型推理 - 生成文本响应
# ============================================================================
# torch.no_grad(): 禁用梯度计算以节省显存和加速推理
# 梯度只在训练时需要,推理过程不需要
with torch.no_grad():
# model.generate: 自回归文本生成方法
# 逐个 token 地生成文本,每次预测下一个最可能的 token
generated_ids = model.generate(
# input_ids: 输入的 token ID 张量
model_inputs["input_ids"],
# attention_mask: 明确传入注意力掩码以避免 pad==eos 时无法推断掩码的问题
attention_mask=model_inputs["attention_mask"],
# max_new_tokens: 最多生成多少个新 token(不包括输入的 token)
# 限制输出长度,防止生成过长的文本和显存溢出
# 对于实时应用,建议控制在 256-512 之间以保证响应速度
max_new_tokens=256,
# temperature: 控制生成的随机性和多样性(范围 0.0-2.0)
# 0.0: 完全确定性(总是选概率最高的 token),可能导致重复和单调
# 0.7: 平衡点(推荐),既保留多样性又保持相对连贯
# 1.0: 中等随机性,适合创意写作
# > 1.0: 高度随机,可能导致不连贯或质量下降
# 对话场景推荐 0.6-0.8,创意写作推荐 0.8-1.2
temperature=0.7,
# top_p (核采样,Nucleus Sampling): 动态选择生成的 token
# 只从累计概率达到 p 的最可能的 token 中采样
# 0.95: 保留概率最高的 token,直到累计概率 ≥ 95%
# 优势:比 top_k 更自适应,避免低概率 token 造成的胡言乱语
# 比 temperature 更高效,能更好地控制生成质量
# 推荐范围:0.8-0.95(平衡质量和多样性)
top_p=0.95,
# do_sample: 采样策略选择
# True: 根据概率分布随机采样下一个 token(推荐用于对话和创意任务)
# - 多次运行同一输入会得到不同的输出
# - 结合 temperature 和 top_p 使用效果最好
# False: 贪心搜索,总是选择概率最高的 token(推荐用于翻译等确定性任务)
# - 输出完全确定,适合需要复现性的应用
# - 可能导致生成重复或单调的文本
do_sample=True
)
# ============================================================================
# 第四步:解码和输出结果
# ============================================================================
# tokenizer.decode: 将 token ID 序列转换回可读的文本
# skip_special_tokens=True: 不显示特殊 token(如 <pad>, <eos> 等)
response = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
print("\nResponse:")
print(response)
|
运行方式
运行方式如下:
(可选)创建并激活虚拟环境:
python3 -m venv .venv
source .venv/bin/activate
安装依赖(若出现 ModuleNotFoundError: No module named transformers,请执行此步):
pip install --upgrade pip
pip install transformers torch accelerate
或者使用 requirements.txt:
cat > requirements.txt <<'EOF'
transformers
torch
accelerate
EOF
pip install -r requirements.txt
运行脚本:
输出说明
输出结果如下:
Loading model...
Model loaded!
Response:
system
You are Qwen, created by Alibaba Cloud. You are a helpful assistant.
user
Hello, how are you?
assistant
Hello! I'm just an AI language model and don't have feelings in the traditional sense. However, I'm here to help answer your questions to the best of my ability. How can I assist you today?
输出由三部分组成:
- 模型加载与运行时日志,例如 “Loading model…” / “Model loaded!"。
- 模型的 Response 部分,按角色列出(system、user、assistant),其中 assistant 部分为模型实际生成的回复。
重复运行多次,你将看到不同的回复,因为模型是随机生成的。
为什么每次的回复都不同?
这是由于模型在生成文本时会根据概率分布随机选择下一个 token,因此每次生成的结果都会有所不同。这种随机性是模型的一个特性,也是其生成文本多样化的原因。
使用 Flask API 服务器
如果需要 HTTP API 接口,推荐自建 Flask API 服务器,支持并发请求,便于集成到业务系统。
下面是一个完整的 API 服务器示例代码,启动后即可通过 HTTP 请求进行推理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
| #!/usr/bin/env python3
"""
大模型 Flask API 服务器
这个脚本实现了一个 REST API 服务,提供文本生成和聊天补全功能。
API 兼容 OpenAI API 的部分接口规范,便于集成到现有的 AI 应用中。
启动方式:
python flask_api_server.py
API 端点:
- GET /health - 健康检查
- POST /v1/chat/completions - 聊天补全(对话模式)
- POST /v1/completions - 文本补全(生成模式)
"""
from flask import Flask, request, jsonify
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
import json
# ============================================================================
# Flask 应用初始化与模型加载
# ============================================================================
app = Flask(__name__)
# 全局加载模型和分词器(在应用启动时执行)
# 这样做的优势:
# 1. 避免每个请求都重新加载模型,这会非常耗时
# 2. 模型在内存中保持加载状态,减少延迟
# 缺点:
# 1. 应用启动时间较长
# 2. 内存占用始终保持较高水位
# 3. 不支持模型热更新(需要重启应用)
print("Loading model...")
model_name = "Qwen/Qwen2.5-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 在实际部署中,建议改进为:
# - 使用 bfloat16 替代 float16,提升稳定性
# - 添加 device_map="auto" 和 gradient_checkpointing_enable()
# - 设置 max_memory 限制,防止 OOM
model = AutoModelForCausalLM.from_pretrained(model_name, dtype=torch.float16)
model.eval() # 启用评估模式
print("Model loaded!")
# ============================================================================
# 端点 1: 健康检查
# ============================================================================
@app.route('/health', methods=['GET'])
def health():
"""
健康检查端点
返回:
{
"status": "ok"
}
用途:
- 负载均衡器可定期调用此端点检测服务是否可用
- 容器编排系统(K8s)可用此判断是否需要重启 Pod
- 监控系统可跟踪服务可用性
"""
return jsonify({"status": "ok"})
# ============================================================================
# 端点 2: 聊天补全(OpenAI 兼容)
# ============================================================================
@app.route('/v1/chat/completions', methods=['POST'])
def chat_completions():
"""
聊天补全端点 - 对话模式 API
兼容 OpenAI Chat Completions API:
https://platform.openai.com/docs/api-reference/chat/create
请求体格式:
{
"messages": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么我可以帮你的吗?"},
{"role": "user", "content": "今天天气怎么样?"}
],
"temperature": 0.7, # 可选,默认 0.7
"max_tokens": 256 # 可选,默认 256
}
返回格式(OpenAI 兼容):
{
"choices": [
{
"message": {
"role": "assistant",
"content": "我是一个 AI 助手,无法获取实时天气信息..."
},
"index": 0,
"finish_reason": "length"
}
]
}
"""
# 解析请求 JSON 数据
data = request.json
# 从请求中提取参数,使用默认值
# messages: 对话历史,格式为 [{"role": "user"/"assistant", "content": "..."}, ...]
messages = data.get('messages', [])
# max_tokens: 最多生成多少个新 token
max_tokens = data.get('max_tokens', 256)
# temperature: 生成的随机性(0.0-2.0)
temperature = data.get('temperature', 0.7)
# 步骤 1:使用 chat template 格式化消息
# 这会将 messages 列表转换为模型期望的提示格式
# 包含系统角色、对话历史和生成提示
text = tokenizer.apply_chat_template(
messages,
tokenize=False, # 返回字符串而不是 token ID
add_generation_prompt=True # 添加特殊 token 指示开始生成
)
# 步骤 2:分词化输入文本
model_inputs = tokenizer([text], return_tensors="pt")
# 步骤 3:执行推理生成文本
# torch.no_grad() 禁用梯度计算以加速推理和节省显存
with torch.no_grad():
generated_ids = model.generate(
model_inputs.input_ids,
attention_mask=model_inputs.get("attention_mask"), # 明确传递注意力掩码
max_new_tokens=max_tokens,
temperature=temperature,
top_p=0.95,
do_sample=True
)
# 步骤 4:解码生成的 token 序列为文本
full_response = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
# 步骤 5:提取仅 assistant 部分的响应
# 完整的 full_response 包含了输入的 chat template 和生成的文本
# 我们只需要返回 assistant 的回复部分
response = full_response.split("assistant\n")[-1] if "assistant" in full_response else full_response
# 返回 OpenAI 兼容的响应格式
return jsonify({
"choices": [
{
"message": {
"role": "assistant",
"content": response
},
"index": 0,
"finish_reason": "length"
}
]
})
# ============================================================================
# 端点 3: 文本补全(OpenAI 兼容)
# ============================================================================
@app.route('/v1/completions', methods=['POST'])
def completions():
"""
文本补全端点 - 生成模式 API
兼容 OpenAI Completions API:
https://platform.openai.com/docs/api-reference/completions/create
请求体格式:
{
"prompt": "今天天气很",
"temperature": 0.7, # 可选,默认 0.7
"max_tokens": 256 # 可选,默认 256
}
返回格式(OpenAI 兼容):
{
"choices": [
{
"text": "晴朗,适合出游。",
"index": 0,
"finish_reason": "length"
}
]
}
与 chat/completions 的区别:
- chat/completions: 用于对话场景,接收多轮对话消息
- completions: 用于文本补全,给定一个开头文本,模型继续生成
"""
# 解析请求 JSON 数据
data = request.json
# prompt: 输入的文本开头,模型会基于此生成后续内容
prompt = data.get('prompt', '')
# max_tokens: 最多生成多少个新 token
max_tokens = data.get('max_tokens', 256)
# temperature: 生成的随机性(0.0-2.0)
temperature = data.get('temperature', 0.7)
# 步骤 1:直接分词化输入 prompt(不需要 chat template)
# 因为这是纯文本补全,不涉及聊天格式
model_inputs = tokenizer([prompt], return_tensors="pt")
# 步骤 2:执行推理生成文本
with torch.no_grad():
generated_ids = model.generate(
model_inputs.input_ids,
attention_mask=model_inputs.get("attention_mask"), # 明确传递注意力掩码
max_new_tokens=max_tokens,
temperature=temperature,
top_p=0.95,
do_sample=True
)
# 步骤 3:解码生成的 token 序列为文本
completion = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
# 步骤 4:提取仅生成部分(去掉输入的 prompt)
# generated_ids 包含 [input_ids + generated_ids]
# 所以解码后的文本包含了原始 prompt 和生成内容
# 我们只返回生成的新部分:completion[len(prompt):]
# 返回 OpenAI 兼容的响应格式
return jsonify({
"choices": [
{
"text": completion[len(prompt):], # 仅返回新生成的部分
"index": 0,
"finish_reason": "length"
}
]
})
# ============================================================================
# 应用启动
# ============================================================================
if __name__ == '__main__':
"""
Flask 应用入口点
启动 Flask 开发服务器监听所有网络接口
参数说明:
- host='0.0.0.0': 监听所有网络接口(0.0.0.0 代表所有 IPv4 地址)
* 生产环境建议使用 127.0.0.1 限制本地访问,再通过反向代理(nginx)暴露
- port=8000: 监听端口号
- debug=False: 关闭调试模式(生产环境必须关闭,开启会自动重载代码)
- threaded=False: 不启用多线程
* PyTorch 模型加载和推理不是线程安全的
* 多个线程同时执行模型推理会导致错误
* 需要使用队列或进程池来处理并发请求
生产部署建议:
1. 使用 gunicorn 替代 Flask 内置服务器
2. 配置多个 worker 进程处理请求
3. 前置反向代理(nginx)进行负载均衡
4. 使用 Redis 缓存常见请求
5. 添加请求队列管理系统(如 Celery)处理长时间运行的请求
6. 启用 request timeout 防止无限挂起
启动命令:
# 开发模式
python flask_api_server.py
# 生产模式(需先 pip install gunicorn)
gunicorn -w 4 -b 0.0.0.0:8000 flask_api_server:app
"""
print("Starting API server on http://0.0.0.0:8000")
app.run(host='0.0.0.0', port=8000, debug=False, threaded=False)
|
依赖安装命令如下:
启动服务器:
python flask_api_server.py
API 测试方法如下:
健康检查:
curl http://localhost:8000/health
返回结果为 {"status": "ok"}。
聊天补全:
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "你好,介绍下你自己。"}
],
"max_tokens": 128,
"temperature": 0.7
}' | jq
返回结果为:
{
"choices": [
{
"finish_reason": "length",
"index": 0,
"message": {
"content": "您好!我叫 Qwen,是由阿里云开发的超大规模语言模型。我的目的是回答用户的问题、创作文字作品以及执行各种任务。如果您有任何问题或需要帮助,请随时告诉我,我会尽力提供支持。",
"role": "assistant"
}
}
]
}
文本补全:
curl -X POST http://localhost:8000/v1/completions \
-H "Content-Type: application/json" \
-d '{
"prompt": "人生的意义是",
"max_tokens": 64,
"temperature": 0.8
}' | jq
返回结果为:
{
"choices": [
{
"finish_reason": "length",
"index": 0,
"text": "实现自我价值,即个人成长、发展和成功的历程。这个概念与社会的经济、文化和社会结构有着密切关系。以下哪些选项可以用来解释这一观点?\nA. 经济因素:个人通过努力工作获得财务支持,为自身的发展打下坚实基础。\nB. 文化因素"
}
]
}
jq 命令
jq 是一个用于处理 JSON 数据的命令行工具,可以方便地格式化和查询 JSON 数据。
如需生产级部署,可使用 Gunicorn 多进程启动:
uv pip install gunicorn
gunicorn -w 1 -b 0.0.0.0:8000 --timeout 120 flask_api_server:app
已知限制
当前 macOS Silicon 下 vLLM 的原生能力仍在完善,存在如下限制:
vLLM CLI 不可用:vllm serve 命令在 macOS 上段错误,因 C 扩展模块兼容性问题。建议使用 Transformers 或 Flask API 作为替代方案。
仅支持 float16 和 bfloat16:Apple Silicon CPU 目前仅支持这两种数据类型,避免使用 float32,否则内存占用过大。
model = AutoModelForCausalLM.from_pretrained(
model_name,
dtype=torch.bfloat16
)
推理性能较低:CPU 推理性能有限,推荐使用小型模型(如 1.5B)。优化建议包括使用量化模型(如可用)、减小批处理大小、使用 bfloat16、限制 max_model_len。
不支持多 GPU 或 GPU 加速:macOS 上 vLLM 仅支持 CPU 推理。如需更高性能,建议迁移至 GPU 环境或云服务。
量化支持有限:Apple Silicon 量化支持较少,主要为 x86 架构。
推荐模型
为获得最佳体验,建议优先选择以下小型模型。下表对主流模型进行了对比:
| 模型 | 大小 | 推荐 | 备注 |
|---|
| Qwen/Qwen3-4B-Instruct-2507 | 4B | ⭐⭐⭐⭐⭐ | 快速,质量好,但消耗内存较大 |
| Qwen/Qwen2.5-1.5B-Instruct | 1.5B | ⭐⭐⭐⭐⭐ | 快速,质量好 |
| meta-llama/Llama-2-7b-hf | 7B | ⭐⭐⭐ | 需要更多内存 |
| TinyLlama/TinyLlama-1.1B | 1.1B | ⭐⭐⭐⭐ | 极快,质量一般 |
| Qwen/Qwen2-0.5B | 0.5B | ⭐⭐ | 最快但质量有限 |
表 1: macOS 推荐模型一览总结
本文以最小化实现为目标,展示了在 macOS 环境下通过 Python 进行本地模型推理的实用方法。推荐使用 Hugging Face Transformers 直接加载与推理以获得简单、稳定的开发体验;需要 HTTP 接口时可选用基于 Flask 的轻量服务器。文中同时说明了模型下载、运行、以及通过 curl 测试流式输出的基本流程,并列出了 macOS 下的已知限制(如 vLLM CLI 的兼容性问题、数据类型与性能约束)。最后给出若干小型模型的推荐,便于在受限资源下取得较好效果。按照本文示例操作,读者可以快速验证模型能否在本机运行、生成结果并通过日志观察一次完整的推理生命周期。
Batch 推理最小体验
Batch 推理让大模型服务从“单线程”变成“多线程”,吞吐量和效率提升肉眼可见。
概念:什么是 Batch 推理?
在之前的单条推理中,模型一次只处理一条输入(prompt)。Batch 推理(批量推理,Batch Inference)是指在一个前向传播中同时处理多条输入,充分利用硬件资源并行性。
Batch 推理是大语言模型(LLM)推理的核心优化技术,通过并发处理多条输入来提升吞吐量和 GPU 利用率。
单条 vs Batch 推理对比
下面通过流程图对比单条推理与 Batch 推理的执行方式。
单条推理(Sequential):
这是单条输入依次推理的过程:
Prompt 1 → Model → Output 1
Prompt 2 → Model → Output 2
Prompt 3 → Model → Output 3
总耗时 = 推理时间 × 3
Batch 推理(Parallel):
这是多条输入一次性并发推理的过程:
[Prompt 1, Prompt 2, Prompt 3] → Model → [Output 1, Output 2, Output 3]
总耗时 ≈ 推理时间 × 1(显著提升)
Batch 推理的优势
在实际应用中,Batch 推理带来如下显著优势:
- 吞吐量提升:同样的时间内处理更多请求。
- GPU/CPU 利用率更高:硬件计算资源被充分利用。
- 延迟降低:等待队列中的请求可以和新请求合并处理。
- 成本更低:单位时间处理的 token 数增加,推理成本下降。
Batch 推理的挑战
在实现 Batch 推理时,需注意以下挑战:
- 序列长度不一致:不同输入长度需要 Padding(填充)。
- 内存占用增加:处理多条输入需要更多 GPU 显存。
- 注意力掩码复杂:需要正确处理填充位置,保证模型只关注有效输入。
实战:实现三条并发推理
本节通过一个最小化的 Batch 推理脚本,展示如何一次性推理三条 prompt。
在运行代码前,请确保已安装相关依赖。
代码实现
以下代码展示了如何使用 Transformers 库实现三条并发推理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
| #!/usr/bin/env python3
"""
多句并发推理演示
演示如何使用 Transformers 库的 batch 推理功能,
在单次前向传播中处理多条输入,观察性能提升。
"""
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import time
# ============================================================================
# 模型加载
# ============================================================================
print("Loading model...")
model_name = "Qwen/Qwen2.5-1.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 关键:Decoder-only 模型(如 Qwen、GPT)必须使用左填充而非右填充
# 原因:自回归解码模型只能看到当前位置之前的 token
# - 右填充:[有效输入] + [padding] → 模型会错误地尝试生成 padding token
# - 左填充:[padding] + [有效输入] → 模型可以安全地忽略前面的 padding
# 正确设置 padding_side 可以避免警告,同时保证生成质量
tokenizer.padding_side = "left"
model = AutoModelForCausalLM.from_pretrained(model_name, dtype=torch.bfloat16)
model.eval()
print("Model loaded!\n")
# ============================================================================
# 定义三条输入 prompts
# ============================================================================
# 三条不同的用户问题,展示模型如何处理多样化的输入
messages_list = [
# 第一条:数学问题
[{"role": "user", "content": "请计算:1 加 3 等于几?"}],
# 第二条:创意写作
[{"role": "user", "content": "写一句关于春天的诗"}],
# 第三条:常识问题
[{"role": "user", "content": "中国的首都是哪个城市?"}],
]
print("=" * 70)
print("📋 输入的三条 Prompts")
print("=" * 70)
for idx, messages in enumerate(messages_list, 1):
print(f"\n[Prompt {idx}] {messages[0]['content']}")
# ============================================================================
# 方式 1:单条推理(对比基线)
# ============================================================================
print("\n\n" + "=" * 70)
print("⏱️ 方式 1:单条推理(顺序执行)")
print("=" * 70)
start_time = time.time()
responses_sequential = []
for idx, messages in enumerate(messages_list, 1):
# 格式化输入
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
# 单条推理
model_inputs = tokenizer([text], return_tensors="pt")
with torch.no_grad():
generated_ids = model.generate(
model_inputs["input_ids"],
attention_mask=model_inputs.get("attention_mask"),
max_new_tokens=64,
temperature=0.7,
top_p=0.95,
do_sample=True
)
response = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
response = response.split("assistant\n")[-1] if "assistant" in response else response
responses_sequential.append(response)
print(f"\n[Output {idx}] {response}")
sequential_time = time.time() - start_time
print(f"\n⏱️ 总耗时:{sequential_time:.2f} 秒")
# ============================================================================
# 方式 2:Batch 推理(并发处理)
# ============================================================================
print("\n\n" + "=" * 70)
print("⚡ 方式 2:Batch 推理(并发处理)")
print("=" * 70)
start_time = time.time()
# 步骤 1:格式化所有输入
formatted_prompts = []
for messages in messages_list:
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
formatted_prompts.append(text)
# 步骤 2:将所有输入 tokenize 成 batch
# tokenizer 会自动添加 padding 使所有序列长度一致
model_inputs = tokenizer(
formatted_prompts,
return_tensors="pt",
padding=True, # 启用 padding,短的序列会被填充到最长的长度
truncation=True, # 长序列会被截断
)
# 步骤 3:一次性推理整个 batch
with torch.no_grad():
generated_ids = model.generate(
model_inputs["input_ids"],
attention_mask=model_inputs.get("attention_mask"),
max_new_tokens=64,
temperature=0.7,
top_p=0.95,
do_sample=True
)
# 步骤 4:解码所有生成结果
responses_batch = []
for idx, generated_id in enumerate(generated_ids):
response = tokenizer.decode(generated_id, skip_special_tokens=True)
response = response.split("assistant\n")[-1] if "assistant" in response else response
responses_batch.append(response)
print(f"\n[Output {idx + 1}] {response}")
batch_time = time.time() - start_time
print(f"\n⏱️ 总耗时:{batch_time:.2f} 秒")
# ============================================================================
# 性能对比分析
# ============================================================================
print("\n\n" + "=" * 70)
print("📊 性能对比分析")
print("=" * 70)
speedup = sequential_time / batch_time
print(f"\n单条推理耗时: {sequential_time:.2f} 秒")
print(f"Batch 推理耗时: {batch_time:.2f} 秒")
print(f"性能提升倍数: {speedup:.2f}x")
print(f"总体加速: {(1 - batch_time / sequential_time) * 100:.1f}%")
|
运行脚本
请按照以下步骤运行脚本:
安装依赖:
pip install transformers torch
运行脚本:
python batch_inference.py
预期输出
运行后将得到如下输出结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| Loading model...
Model loaded!
======================================================================
📋 输入的三条 Prompts
======================================================================
[Prompt 1] 请计算:1 加 3 等于几?
[Prompt 2] 写一句关于春天的诗
[Prompt 3] 中国的首都是哪个城市?
======================================================================
⏱️ 方式 1:单条推理(顺序执行)
======================================================================
[Output 1] 1 加上 3 等于 4。
[Output 2] 春风拂面柳依依,桃花笑开映日辉。
[Output 3] 中国的首都是北京。
⏱️ 总耗时:8.40 秒
======================================================================
⚡ 方式 2:Batch 推理(并发处理)
======================================================================
[Output 1] 1 加 3 等于 4。
[Output 2] 春风吹绿江南岸,花开满园香四溢。
[Output 3] 中国的首都是北京。
⏱️ 总耗时:3.53 秒
======================================================================
📊 性能对比分析
======================================================================
单条推理耗时:8.40 秒
Batch 推理耗时:3.53 秒
性能提升倍数:2.38x
总体加速:57.9%
|
Batch 推理内部机制
本节介绍 Batch 推理的底层机制和关键流程。
输入准备
三条 prompts 的长度分别为:42 tokens(padding 后)。Tokenizer 自动将短序列补齐到最长长度,并添加 attention_mask 标记哪些位置是有效输入。
并发处理
模型在一次前向传播中处理所有 3 个输入。GPU/CPU 可以并行计算各序列的注意力和前馈网络,效果相当于同时处理 3 个请求而不是顺序处理。
输出生成
自回归生成在 batch 维度进行,每个序列独立生成,互不影响。输出形状为:[batch_size=3, seq_length]。
为什么 Batch 更快?
硬件并行性:GPU 的矩阵乘法单位可以并行处理多条序列。只需加载一次模型权重,共享计算资源,单位时间处理的 token 数大幅增加。
关键观察点
通过本节实战,可以观察到以下现象:
三条输入一起返回
单条推理需等待 3 次推理完成才能获得所有结果,Batch 推理一次前向传播同时返回 3 个结果。
响应完全独立
每条输入的响应互不影响,内容不同但处理时间大幅减少。
vLLM 自动批处理(预告)
当前代码使用的是 Transformers 库的原生 batch 功能。vLLM 的核心优势是自动化 batch 合并,不同长度的请求、不同到达时间的请求都能自动合并处理。后续章节会深入讲解 vLLM 的 scheduler 和 batch 合并机制。
内存与性能权衡
Batch size 越大性能越好,但显存占用也增加。实践中需要找到最优的 batch size。macOS 受限,建议 batch size = 2-4。
注意力掩码的作用
虽然短序列被 padding 了,但 attention_mask 防止模型关注填充位置,确保输出质量不受 padding 影响。
Batch 推理的实际应用
在不同场景下,Batch 推理的使用建议如下:
| 场景 | 建议 |
|---|
| 单条请求,实时性要求高 | ❌ 不用 Batch,直接推理 |
| 已知的多条请求(如离线处理) | ✅ 使用 Batch,性能最优 |
| 生产服务有大量并发请求 | ✅ 必须用 vLLM,自动 Batch |
| 模型服务化部署 | ✅ 推荐用 vLLM 的 Batch 合并 |
表 2: Batch 推理应用场景与建议vLLM 的自动 Batch 处理(预告)
上面的脚本展示了手动 Batch 推理的过程。vLLM 的核心优势在于自动化:
- 动态合并:不同时间到达的请求自动合并成 batch。
- 智能调度:优化 batch size,平衡吞吐量和延迟。
- 最小化 Padding:复杂的调度算法减少无效计算。
后续章节会深入讲解 vLLM 的 Scheduler、KV Cache 管理和 Batch 优化机制。
常见问题
以下为 Batch 推理相关的常见问题解答:
▸
为什么 Batch 推理比单条推理快这么多?
主要原因有三:
- 硬件并行性:GPU 的矩阵乘法单位可以同时处理多行数据,batch size 大时硬件使用率高。
- 内存带宽效率:一次加载模型权重,共享计算,单位 token 的权重访问次数减少。
- 减少开销:每次推理都有框架开销(Python GIL、CUDA 启动等),batch 化可以摊销这些开销。
简单说:单条推理像逐个运行程序,Batch 推理像并行运行程序。
▸
Batch size 多大比较好?
这取决于:
- 硬件限制:GPU 显存大小,macOS CPU 内存大小。
- 模型大小:大模型每增加一个序列就占用更多内存。
- 序列长度:长序列更消耗内存,因此 batch size 应该更小。
- 延迟要求:batch size 越大延迟越高(需要等待所有请求),实时应用需要权衡。
经验法则:从 batch size=2 开始,逐渐增加,直到显存不足或延迟过高。macOS 推荐 batch size=2-4。
▸
Padding 会影响输出质量吗?
不会,只要正确使用 attention_mask。
- Attention_mask 的作用是屏蔽填充位置,告诉模型"这些位置是填充的,不要关注"。
- 只要 attention_mask 正确,模型生成的内容完全相同,就像没有 padding 一样。
重要的是:总是传递 attention_mask,特别是在混合长度的 batch 中。
本节小结
本章通过三条并发推理的最小示例,演示了 Batch 推理的核心概念和性能优势。关键收获包括:
- Batch 推理原理:多条输入在一次前向传播中并行处理。
- 性能提升:真实的 2-3 倍加速,越大的 batch 加速越明显。
- 关键机制:padding、attention_mask 确保输出质量。
- 实际应用:生产服务中 Batch 处理是必需的,vLLM 自动化了这个过程。
下一章将深入 vLLM 的服务化架构,讲解如何在生产环境中实现自动的、动态的、高性能的 Batch 处理。
总结
两个示例构成一条递进路径:Python API 确认“模型能跑、能服务”,Batch 推理确认“并发有真实收益”。此时一切还停留在手动阶段:批要自己拼、padding 要自己管。把这些环节交给引擎自动完成,正是vLLM serve要解决的问题。