模型推理与部署

阶段 10 · 训练好的模型如何变成人人可用的服务

模型训好了

你花了几天时间完成了预训练、SFT、DPO,终于得到了一个对话还不错的模型。现在你想让其他人也能用它。

你不能让每个人都 clone 你的代码、装 PyTorch、手动加载权重。你需要一个服务。

这一章把训练好的模型变成三种可用形态:命令行对话、OpenAI 兼容 API、Web 聊天界面。以及一个容易被忽视但很实用的能力——模型格式转换。

本章地图

推理基础

  • 加载权重
  • 命令行对话
  • 采样参数

API 服务

  • FastAPI
  • OpenAI 兼容
  • 流式输出 SSE

Web 界面

  • Streamlit
  • 实时聊天

格式转换

  • torch 到 transformers
  • LoRA 合并
  • Qwen3 兼容

高性能推理

  • vLLM 启动实战
  • 吞吐量对比
  • FastAPI 多 worker

Part 1:推理

从加载权重到生成文本的完整流程。

训练好模型只是第一步,让它"跑起来"才是目的。这一节从最基础的四步推理流水线讲起——加载权重、编码输入、自回归生成、解码输出。先搞懂命令行对话怎么跑,再去包装成 API 和 Web 服务。

  • 推理流水线:加载模型权重 → Tokenizer 编码 → model.generate() 自回归生成 → Tokenizer 解码输出文本
  • 模型加载 init_model:两条路径——原生 MiniMind .pth 权重或 HuggingFace transformers 格式,后者可用 vLLM 加速
  • 采样参数详解:temperature 控制随机性、top_p 控制候选词范围、max_new_tokens 控制回答长度
  • 命令行对话:eval_llm.py 的各种启动方式——基础用法、LoRA 权重、调整采样参数、MoE 模型、思考模式

推理流水线

四步走:加载 → 编码 → 生成 → 解码

图:模型推理的四步流水线

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一条从左到右的四步流水线,包含四个方框 - 方框1:"加载模型权重 model.half().eval()" - 方框2:"用户输入 Tokenizer 编码" - 方框3:"model.generate() 自回归生成" - 方框4:"Tokenizer 解码 输出文本" - 各元素关系:方框1用箭头指向方框2指向方框3指向方框4,形成线性流水线 - 关键标注:每个方框上方标注步骤编号"第一步/第二步/第三步/第四步" - 配色建议:浅蓝/浅灰背景,方框白色带蓝色边框,箭头橙色 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

# eval_llm.py 核心流程
model, tokenizer = init_model(args)  # 加载模型

while True:
    user_input = input("用户: ")
    messages = [{"role": "user", "content": user_input}]
    # 编码
    inputs = tokenizer.apply_chat_template(messages, return_tensors="pt")
    # 生成
    outputs = model.generate(inputs.to(device), max_new_tokens=8192,
                              temperature=0.85, top_p=0.95)
    # 解码
    response = tokenizer.decode(outputs[0][inputs.shape[-1]:])
    print(f"助手: {response}")

模型加载

支持两种权重格式:原生 torch 和 HuggingFace transformers

def init_model(args):
    tokenizer = AutoTokenizer.from_pretrained(args.load_from)
    if 'model' in args.load_from:
        # 路径1:原生 MiniMind 格式(.pth 权重)
        model = MiniMindForCausalLM(MiniMindConfig(...))
        model.load_state_dict(torch.load(ckp, map_location=args.device))
        if args.lora_weight != 'None':
            apply_lora(model)  # 可选:加载 LoRA 旁路
            load_lora(model, lora_path)
    else:
        # 路径2:HuggingFace transformers 格式
        model = AutoModelForCausalLM.from_pretrained(args.load_from)
    return model.half().eval().to(args.device), tokenizer

两条路径的区别:路径 1 直接加载 MiniMind 原生权重(.pth),路径 2 加载转换后的 HuggingFace 格式。后者可以用 vLLM 等推理加速框架。

采样参数详解

控制模型"怎么说"的三个旋钮

参数默认值作用调大调小
temperature0.85控制随机性更发散、有创意更确定、保守
top_p0.95核采样阈值候选词更多只保留高概率词
max_new_tokens8192最大生成长度回答更长回答更短

这是上一章 MiniMindForCausalLM.generate() 中讲过的 Temperature、Top-P 采样的实际应用。temperature=0 表示贪心解码(每次选概率最高的词)。

命令行对话

eval_llm.py —— 最快的验证方式

# 基础用法
$ python eval_llm.py --weight full_sft

# 使用 LoRA 权重
$ python eval_llm.py --weight full_sft --lora_weight lora_medical

# 调整采样参数
$ python eval_llm.py --weight full_sft \
    --temperature 0.5 --top_p 0.9 --max_new_tokens 2048

# 使用 MoE 模型
$ python eval_llm.py --weight full_sft --use_moe 1

# 开启思考模式
$ python eval_llm.py --weight full_sft --open_thinking 1

--historys 参数控制携带的历史对话轮数(需为偶数)。0 表示不携带历史,每次对话独立。

Part 2:API

把模型包装成 OpenAI 兼容的 API,任何客户端都能调用。

命令行对话只能自己用,想让其他人也能调用你的模型,就得把它包装成 API 服务。这一节用 FastAPI 搭建一个 OpenAI 兼容的接口——这意味着你能把 MiniMind 直接接入任何基于 OpenAI API 的应用,包括流式输出的"打字机效果"。

  • API 服务架构:客户端 → FastAPI 服务 → MiniMind 模型,POST /v1/chat/completions 端点处理请求
  • 流式输出 SSE:Server-Sent Events 实现"打字机"效果,每生成一个 Token 立即推送,用 CustomStreamer + 队列实现
  • SSE 数据格式:兼容 OpenAI 的 data: {...} 格式,客户端可直接用 OpenAI SDK 消费
  • 启动 API 服务:serve_openai_api.py 的启动方式,支持指定端口、模型参数、HuggingFace 格式

API 服务架构

图:OpenAI 兼容 API 服务的请求-响应架构

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:三个竖向排列的方框,从左到右分别是"客户端(OpenAI SDK / curl)"、"FastAPI 服务(serve_openai_api.py)"、"MiniMind 模型(model.generate())" - 请求流:客户端发送"POST /v1/chat/completions"到FastAPI服务 - 模型调用:FastAPI服务调用MiniMind模型生成 - 响应流:模型返回"SSE流式/JSON"给FastAPI,FastAPI返回"逐Token/完整"给客户端 - 各元素关系:客户端和FastAPI之间双向箭头,FastAPI和模型之间双向箭头,标注数据流向 - 关键标注:请求箭头标注POST路径,响应箭头标注输出方式 - 配色建议:浅蓝/浅灰背景,客户端蓝色,FastAPI绿色,模型橙色,关键箭头用橙色标注 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

# serve_openai_api.py 核心端点
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatRequest):
    messages = request.messages
    # 应用聊天模板
    inputs = tokenizer.apply_chat_template(messages, return_tensors="pt")
    if request.stream:
        # 流式输出
        return StreamingResponse(generate_stream(inputs, request),
                                 media_type="text/event-stream")
    else:
        # 同步输出
        outputs = model.generate(inputs, max_new_tokens=request.max_tokens)
        return format_response(outputs, request)

流式输出

逐 Token 推送,不用等完整回答生成

定义:Server-Sent Events(SSE)是一种服务器向客户端单向推送的技术。每生成一个 Token 就立即推送,用户看到"打字机"效果。

说白了,就像微信语音转文字——不用等整段话说完,一边说一边出字。

这个比喻只是为了帮你建立直观感受。在计算机内部,SSE 是一种 HTTP 长连接协议,服务器通过 data: 前缀逐行发送文本块,浏览器用 EventSource API 接收。

class CustomStreamer(TextStreamer):
    def __init__(self, tokenizer, queue):
        super().__init__(tokenizer)
        self.queue = queue

    def on_finalized_text(self, text, stream_end=False):
        # 每生成一个 token,推入队列
        self.queue.put(text)
        if stream_end:
            self.queue.put(None)  # 发送结束信号

SSE 数据格式

兼容 OpenAI 的 data: {...} 格式

data: {"id":"chatcmpl-1","object":"chat.completion.chunk",
       "choices":[{"delta":{"content":"你"},"index":0}]}

data: {"id":"chatcmpl-1","object":"chat.completion.chunk",
       "choices":[{"delta":{"content":"好"},"index":0}]}

data: [DONE]

客户端用 OpenAI SDK 可以直接消费:

from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")

response = client.chat.completions.create(
    model="minimind",
    messages=[{"role": "user", "content": "你好"}],
    stream=True
)
for chunk in response:
    print(chunk.choices[0].delta.content or "", end="")

启动 API 服务

# 启动服务(默认端口 8000)
$ python scripts/serve_openai_api.py --weight full_sft

# 指定端口和模型参数
$ python scripts/serve_openai_api.py \
    --weight full_sft --port 8080 \
    --hidden_size 768 --num_hidden_layers 8

# 使用 HuggingFace 格式模型
$ python scripts/serve_openai_api.py \
    --load_from ../transformers_minimind/

API 兼容 OpenAI 格式,可以直接用 OpenAI SDK、curl、或任何支持 OpenAI API 的工具调用。这意味着你能把 MiniMind 接入任何基于 OpenAI API 的应用。

Part 3:Web

用 Streamlit 快速搭建一个网页聊天界面。

API 是给开发者用的,普通用户需要一个更友好的界面。这一节用 Streamlit 几十行代码搭一个网页聊天室——左侧调参数,右侧聊天。虽然内容不多,但它是模型从"实验品"变成"产品"的关键一步。

  • Streamlit 界面:web_demo.py 的启动方式、界面布局——左侧模型配置面板(温度、Top-P、最大长度、LoRA、MoE),右侧聊天区域

Web 界面

web_demo.py —— 几十行代码的 Web 聊天

# 启动 Web 界面
$ streamlit run scripts/web_demo.py -- --weight full_sft

图:MiniMind Web 聊天界面布局

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个网页聊天界面的线框图 - 左侧:模型配置面板(温度滑块、Top-P 滑块、最大长度输入、LoRA 选择下拉框、MoE 开关) - 右侧:聊天区域(用户消息气泡蓝色、助手回复气泡灰色、底部输入框) - 顶部:标题栏"MiniMind 对话" - 配色建议:浅灰背景,蓝色用户气泡,灰色助手气泡,橙色配置元素 - 画面比例:16:9 - 避免:过度装饰、3D 效果

Part 4:转换

让 MiniMind 模型兼容更广泛的生态系统。

原生 .pth 权重只能用 MiniMind 代码加载——不能用 vLLM 加速、不能上传 HuggingFace Hub。转换成 transformers 格式后,你的模型就进入了整个 HuggingFace 生态。这一节讲解两种转换路径、Qwen3 架构兼容原理,以及 LoRA 合并转换的完整流程。

  • 为什么需要转换:原生 .pth 只能 MiniMind 代码加载,Transformers 格式可用 vLLM/SGLang 加速、可上传 HuggingFace Hub
  • 两种转换方式:转为 Qwen3 兼容格式(推荐,可用 vLLM)或保留 MiniMind 格式(兼容 transformers 库)
  • Qwen3 兼容原理:MiniMind 架构与 Qwen3 高度相似——RMSNorm、RoPE、GQA、SwiGLU 全部一致,转换后可直接用 vLLM 获 5-10 倍加速
  • LoRA 合并转换:先合并 LoRA 到基础模型,再转格式,两步搞定

为什么需要转换

原生 .pth 权重只能用 MiniMind 代码加载

原生格式

  • 只能用 MiniMind 代码加载
  • 不能使用 vLLM 加速
  • 不能使用 HuggingFace Hub
  • 适合:学习和实验

Transformers 格式

  • 可用 HuggingFace 生态加载
  • 可用 vLLM / SGLang 加速
  • 可上传到 HuggingFace Hub
  • 适合:生产部署

两种转换方式

convert_model.py 提供两个函数

图:MiniMind 模型格式转换的两种路径

【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:一个自上而下的分支流程图 - 顶部方框:"MiniMind .pth 权重" - 菱形判断:"选择转换方式" - 左分支:"convert_torch2transformers 转为Qwen3格式 兼容vLLM/SGLang" - 右分支:"convert_torch2transformers_minimind 保留MiniMind格式 兼容transformers库" - 各元素关系:顶部方框指向菱形判断,菱形分出左右两条路径 - 关键标注:左分支旁标注"推荐,可用于vLLM加速",右分支旁标注"保持原生格式" - 配色建议:浅蓝/浅灰背景,起始白色,判断黄色菱形,左分支绿色,右分支蓝色,关键箭头用橙色标注 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色

# 转换为 Qwen3 兼容格式(推荐,可用于 vLLM)
$ python scripts/convert_model.py \
    --convert_type torch2transformers \
    --torch_path ../out/full_sft_768.pth \
    --transformers_path ../transformers_minimind/

# 转换为 MiniMind transformers 格式
$ python scripts/convert_model.py \
    --convert_type torch2transformers_minimind \
    --torch_path ../out/full_sft_768.pth \
    --transformers_path ../transformers_minimind_custom/

兼容原理

MiniMind 的架构与 Qwen3 高度相似

组件MiniMindQwen3兼容?
归一化RMSNormRMSNorm✅ 一致
位置编码RoPERoPE✅ 一致
注意力GQAGQA✅ 一致
前馈网络SwiGLUSwiGLU✅ 一致
权重绑定不共享可选✅ 可配置

因为架构一致,转换后可以直接用 vLLM 部署,获得比原生 PyTorch 快 5-10 倍的推理速度。

LoRA 合并转换

先合并 LoRA,再转格式

# 步骤 1:合并 LoRA 到基础模型
$ python -c "
from model.model_lora import merge_lora
from model.model_minimind import MiniMindConfig, MiniMindForCausalLM
config = MiniMindConfig()
model = MiniMindForCausalLM(config)
merge_lora(model, '../out/lora_medical_768.pth', '../out/merged_768.pth')
"

# 步骤 2:转换合并后的模型
$ python scripts/convert_model.py \
    --convert_type torch2transformers \
    --torch_path ../out/merged_768.pth \
    --transformers_path ../transformers_medical/

Part 5:高性能

转换成 Qwen3 格式后,用 vLLM / SGLang 把吞吐量拉满。

前面 Part 4 把 MiniMind 权重转成了 Qwen3 兼容格式——这不是为了好玩,而是为了借用成熟的推理引擎。vLLM 和 SGLang 这类引擎原生支持 Qwen3 架构,能提供 PagedAttention、连续批处理(continuous batching)、前缀缓存等优化,把单卡吞吐量提升一个数量级。MiniMind 自带的 FastAPI 服务适合学习和调试,真正要扛并发、压性能时,就该切换到这些引擎。

  • vLLM 启动实战 —— 一条命令把转换后的模型变成 OpenAI 兼容的高吞吐服务
  • FastAPI 生产部署 —— 多 worker 并发、超时控制、稳定性
  • 性能对比 —— 原生 PyTorch vs vLLM,什么场景该用哪个

vLLM 启动实战

转换成 Qwen3 格式后,一条命令起高吞吐服务

vLLM 不认识 MiniMind 的私有架构,但它认识 Qwen3。上一节 convert_torch2transformers 把权重转成 Qwen3Config 结构后,vLLM 就能直接加载。

# 1. 安装 vLLM(需要 CUDA 环境)
$ pip install vllm

# 2. 一条命令启动 OpenAI 兼容服务
$ python -m vllm.entrypoints.openai.api_server \
    --model ./transformers_minimind_768/ \
    --served-model-name minimind \
    --dtype bfloat16 \
    --max-model-len 2048 \
    --port 8000

# 3. 用同样的 chat_api.py 调用(接口完全兼容)
$ python scripts/chat_api.py

vLLM 默认开启了几个关键优化:PagedAttention(把 KV-Cache 像虚拟内存一样分页管理,减少显存碎片)、连续批处理(新请求随时插入正在处理的 batch,不必等整批完成)、前缀缓存(相同 system prompt 只算一次)。这就是它比原生 PyTorch 快的原因。

吞吐量对比

原生 PyTorch vs vLLM,差在哪

方案并发能力吞吐量(相对)显存效率适用场景
MiniMind FastAPI单进程串行1×低(KV-Cache 预分配)学习、调试、低并发
vLLM / SGLang连续批处理10×~20×高(PagedAttention)生产、高并发、压性能
不要盲目上 vLLM:MiniMind 只有 64M 参数,模型本身极小。在单用户场景下,原生 PyTorch 的延迟反而可能更低(少了引擎开销)。vLLM 的优势在高并发——当同时有几十个请求涌入时,连续批处理才能体现价值。自己玩用 FastAPI,要扛流量再上 vLLM。

深度边界:SGLang 是另一个高性能引擎,优势在复杂提示词结构(多轮、分支)的编译优化。MiniMind 的 rollout_engine.py(阶段 8 RL 训练用)已经集成了 SGLang 客户端,这里不再展开。

FastAPI 并发

不用 vLLM 时,怎么让自带服务扛住多请求

MiniMind 自带的 serve_openai_api.py 默认单进程运行,同一时刻只处理一个请求。要扛并发,用 uvicorn 的多 worker 模式:

# 单 worker(默认,适合调试)
$ python scripts/serve_openai_api.py

# 多 worker 并发(生产部署)
$ uvicorn scripts.serve_openai_api:app \
    --host 0.0.0.0 --port 8000 \
    --workers 4

# 配合 gunicorn 更稳定(Linux)
$ gunicorn scripts.serve_openai_api:app \
    -w 4 -k uvicorn.workers.UvicornWorker \
    --bind 0.0.0.0:8000

每个 worker 是独立进程,各加载一份模型。worker 数量建议等于 GPU 数(每卡一个 worker),或等于 CPU 核数(纯 CPU 推理时)。注意:4 个 worker = 4 倍显存,64M 模型还好,大模型要谨慎。

流式输出的坑:多 worker 模式下,负载均衡器(如 nginx)必须关闭缓冲(proxy_buffering off),否则 SSE 流会被攒成一坨再返回,失去"逐字输出"的效果。

Part 6:排错

部署中常见的问题。

部署阶段的问题通常不是算法问题,而是工程配置问题——显存不够、权重不匹配、API 返回空、精度丢失。这一节把最常见的坑列出来,给你排查思路。最后两个动手练习帮你熟悉 API 调用和采样参数调优。

  • 常见错误诊断:CUDA OOM(确认 .half()、减小 max_new_tokens、用 CPU)、输出乱码(权重与配置不匹配)、API 响应为空(EOS token 问题)、转换后效果变差(精度损失)
  • 动手练习:启动 API 并用 curl 调用观察 JSON 格式;对比不同 temperature 下的回答差异(0.1 vs 1.5)

常见错误诊断

CUDA Out of Memory

症状:推理时报显存不足

修复:① 确认用了 .half();② 减小 max_new_tokens;③ 用 CPU 推理 --device cpu

输出乱码

症状:生成结果是随机字符

原因:权重文件与模型配置不匹配

修复:确认 hidden_size、num_layers、use_moe 参数与训练时一致

API 响应为空

症状:curl 返回空内容

原因:模型没有正确生成 EOS token

修复:检查 tokenizer 的 eos_token_id 是否正确

转换后效果变差

症状:转换后推理结果与原始不同

原因:精度损失(float32→float16)

修复:用 float16 而非 bfloat16 转换,或验证权重是否完整

动手练习

练习 1:启动 API 并调用

  1. 启动 serve_openai_api.py
  2. 用 curl 发送请求:curl -X POST http://localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{"model":"minimind","messages":[{"role":"user","content":"你好"}]}'
  3. 观察返回的 JSON 格式

练习 2:对比采样参数

用同一个问题,分别设置 temperature=0.1 和 temperature=1.5,观察回答的差异。

预期结果

temperature=0.1:回答确定性强,每次几乎一样。temperature=1.5:回答多样性强,每次不同。

知识全景图

推理基础

  • 加载权重 eval
  • Tokenizer 编解码
  • Temperature Top-P 采样

API 服务

  • FastAPI 端点
  • OpenAI 兼容格式
  • SSE 流式输出

Web 界面

  • Streamlit 聊天
  • 参数调节面板

格式转换

  • torch 到 transformers
  • Qwen3 架构兼容
  • vLLM 加速部署
  • LoRA 合并转换

阶段 10 完成

训练好的模型现在可以通过命令行、API、Web 三种方式使用了。

恭喜!你已经完整走过了从零训练一个大语言模型的全部流程。

1 / X