0. 总览
这是我和 AI 智能体共同探讨写作的一篇博文,用来帮助梳理大语言模型推理服务的相关知识与实践。这篇文章的主线是:一个开源模型如何从社区里的文件,变成服务器上可被业务调用的 LLM API 服务。这条链路看似只是“下载模型、启动服务、发请求”,实际涉及模型文件格式、GPU 显存管理、请求调度、KV cache、并发、量化、性能指标等多个层次。
这些问题琐碎、复杂,又各有各的重要。这让我想到了在“上古”时代,大家热衷于探索计算机中“从加电到系统启动”、“从 fork 到子进程执行”、“从输入一个 URL 到浏览器完成页面渲染”等水面以下鲜为人知的过程。
庐山烟雨浙江潮。在人类把越来越多的工作——交付也好、让位也罢——给 AI 智能体的当下和未来,愿我们能始终保留探索、理解和表达的渴望。
0.1 什么是 LLM inference
LLM inference 指的是:给一个已经训练好的语言模型输入一段文本,让它继续预测后续 token 的过程。它和 training 的区别在于,inference 不再更新模型参数,只做前向计算;training 则要计算损失、反向传播并更新权重。
以“北京是中国的”这个 prompt 为例,模型不会一次性“写出答案”,而是做下一个 token 预测:
输入:北京是中国的
预测下一个 token:首
输入:北京是中国的首
预测下一个 token:都
输入:北京是中国的首都
预测下一个 token:。
这就是自回归生成(autoregressive generation)。绝大多数文本 LLM 都基于 Transformer 架构中的 causal decoder:每个位置只能看见自己之前的 token,因此天然适合“根据上文预测下一个 token”。
0.2 inference/serving/chat completion/generation 分别指什么
这几个词经常混用,但它们处在不同抽象层:
| 术语 | 关注点 | 简单理解 |
|---|---|---|
| inference | 模型计算 | 把 token 喂给模型,得到下一个 token 的概率分布 |
| generation | 生成算法 | 根据概率分布和采样策略,决定真正输出哪个 token |
| serving | 工程系统 | 把模型包装成可并发访问、可监控、可扩缩容的服务 |
| chat completion | API 形态 | 接收 system/user/assistant 消息,返回 assistant 回复 |
举个例子:temperature=0.7、top_p=0.9 属于 generation;/v1/chat/completions 属于 API;batching、排队、限流、metrics 属于 serving;矩阵乘法、attention、KV cache 属于 inference 内部计算。
现代开源 serving 框架通常会兼容 OpenAI Chat Completions API,这样上层应用可以用同一套 SDK 切换云模型和自部署模型。
0.3 模型从社区到 API 服务经历哪些环节
一个典型 lifecycle 可以拆成下面几步:
其中最容易被低估的是中间三步:加载、分配、调度。一个模型能否跑起来,首先取决于权重和 KV cache 能否放进显存;一个服务能否跑得好,则取决于调度器能否在吞吐和延迟之间做合理权衡。
例如,同样是 8B 模型,单人本地聊天时可能很流畅;一旦放到线上同时接收 100 个长上下文请求,瓶颈就可能从“模型权重放不下”变成“KV cache 被并发请求撑爆”。
0.4 vLLM/Ollama/SGLang/TGI/llama.cpp 分别解决什么问题
这些工具都能“跑模型”,但设计目标不同:
| 工具 | 更适合的场景 | 核心关键词 |
|---|---|---|
| vLLM | GPU 服务端高吞吐推理 | PagedAttention、continuous batching、OpenAI-compatible server |
| Ollama | 本地开发、个人电脑、快速体验 | 模型管理、Modelfile、本地 REST API、GGUF |
| SGLang | 复杂推理程序、高性能服务、结构化输出 | RadixAttention、structured generation、OpenAI-compatible API |
| TGI | Hugging Face 生态中的生产化 serving | Rust router、streaming、metrics、HF 集成 |
| llama.cpp | CPU/端侧/消费级设备 | C/C++、GGUF、低比特量化、本地运行 |
截至 2026 年 7 月,Hugging Face 的 Text Generation Inference 仓库已在 2026-03-21 归档并进入 maintenance mode;它仍然值得学习,因为很多 serving 概念如 continuous batching、SSE streaming、Prometheus metrics 都非常典型,但新项目选型通常会优先看 vLLM、SGLang 或专用后端。
1. 模型来源与命名
1.1 开源模型通常发布在哪里
开源或 open-weight 模型通常发布在以下地方:
- Hugging Face Model Hub:目前最主流的模型托管平台,模型通常以一个 Git 仓库形式存在,包含权重、配置、tokenizer、model card 等。
- ModelScope:国内常用模型社区,访问和下载速度对国内服务器更友好。
- Ollama Library:面向 Ollama 的模型分发入口,用户可以用
ollama pull qwen2.5这类命令直接拉取。 - 项目方官网或 GitHub release:例如 llama.cpp 生态中的 GGUF 文件经常由社区作者转换后发布在 Hugging Face。
Hugging Face 上的模型名一般是 组织名/仓库名,例如 Qwen/Qwen2.5-Coder-7B-Instruct。这个名字不是随便起的,它同时传达了模型家族、版本、能力方向、规模和训练阶段。
1.2 模型名字怎么看
以 Qwen/Qwen2.5-Coder-7B-Instruct 为例,可以拆成几段:
Qwen / Qwen2.5 - Coder - 7B - Instruct
组织 家族版本 能力方向 参数规模 指令微调版本
常见字段含义如下:
| 字段 | 含义 | 例子 |
|---|---|---|
| 模型家族 | 基础架构和训练路线 | Llama、Qwen、Mistral、Gemma、DeepSeek |
| 版本号 | 同一家族的迭代 | Llama 3.1、Qwen2.5、Mistral v0.3 |
| 参数规模 | 参数数量近似值 | 7B、8B、14B、32B、70B |
| 能力方向 | 训练数据或任务偏向 | Coder、Math、Vision、Reasoning |
| 训练阶段 | 是否面向对话和指令 | Base、Chat、Instruct |
| 精度/量化 | 权重存储格式 | BF16、FP16、INT8、AWQ、GPTQ、GGUF |
Base 模型像“只读了大量书但没学会按人类指令办事的学生”;Instruct 或 Chat 模型则经过指令微调或偏好对齐,更像“知道如何回答问题的助教”。做服务时,如果目标是聊天、问答、代码助手,通常优先选 Instruct/Chat;如果要继续训练、做领域适配或研究模型行为,Base 更合适。
Coder、Math、Vision、Reasoning 则描述能力方向,不是训练阶段。比如 Qwen2.5-Coder 这类模型通常在代码数据、单文件补全、debug、仓库级代码理解、工具调用等任务上做了更强训练;它仍然可以同时是 Instruct 模型。也就是说,Coder 回答“擅长什么”,Instruct 回答“是否学会按指令对话”。
8B 表示大约 80 亿参数。参数越多通常能力越强,但显存、延迟、吞吐成本也更高。粗略估算,BF16/FP16 每个参数 2 字节,8B 权重仅权重就约 16 GB;70B 则约 140 GB,还不包括 KV cache 和 runtime buffer。
还有一类越来越常见的命名是 35B-A3B、235B-A22B。这里的 A 通常表示 activated parameters,常见于 Mixture-of-Experts(MoE)模型。例如官方 Qwen3.5-35B-A3B model card 标注为 35B total、3B activated:总参数约 35B,但每个 token 只路由到其中一部分专家,实际参与该 token 计算的参数约 3B。注意这不等于显存只需要装 3B 参数;如果不做专家 offload 或分布式切分,服务端仍然要管理完整专家权重。
1.3 选择模型前要问什么
选模型不是“越大越好”,而是把任务、硬件、性能目标三者对齐。可以先问五个问题:
- 任务是什么:聊天、代码、RAG、摘要、分类、agent、批量离线生成,还是 embedding?
- 输入和输出多长:短问答和 128K 长上下文 RAG 对系统的压力完全不同。
- 硬件是什么:单张 24 GB 消费卡、A100 80 GB、H100、多机多卡,还是 CPU/Apple Silicon?
- 服务目标是什么:更在意 TTFT、输出吞吐、P99 延迟、成本,还是质量?
- 框架是否支持:模型架构、量化格式、chat template、context length 是否被 vLLM/SGLang/Ollama 支持?
一个实际选择例子:如果要在一张 24 GB GPU 上搭一个代码助手,可以先考虑 7B/8B 级别的 Qwen2.5-Coder 或同级模型;如果是公司内部 RAG 问答,可能更关注长上下文、中文能力和吞吐;如果是本地离线使用,Ollama + GGUF 量化模型可能比完整服务端框架更省心。
1.4 上下文窗口怎么看
上下文窗口(context window)是模型一次生成时能“看见”的 token 总预算,通常包括输入 prompt、历史对话、工具返回、RAG 检索片段,以及本次将要生成的输出。一个 128K context 模型并不是说可以输入 128K token 后再无限输出,而是输入和输出共享这块预算。
某个模型的上下文窗口为什么是某个值,通常由三层因素共同决定。第一层是训练时见过的位置范围:模型在预训练或后训练中通常只在某个最大长度内学习位置关系,这会体现在 config.json 的 max_position_embeddings、max_model_len 或类似字段里。第二层是位置编码机制:现代 LLM 多用 RoPE 这类相对位置编码,能通过 RoPE scaling、YaRN 等方法把原生窗口外推到更长范围,但外推越远,质量越需要实测验证。第三层是推理系统限制:即使模型理论上支持 128K,serving 框架也必须能分配足够 KV cache,并在可接受的 TTFT、ITL 和显存成本下跑起来。
因此,model card 上的上下文窗口不是一个纯数学常数,而更像“训练设定 + 架构设计 + 工程验证”共同给出的服务承诺。比如一个模型原生训练到 32K,通过 YaRN 外推到 128K,含义是它在配置和验证上支持 128K;但如果线上请求几乎都是短 prompt,强行把 max_model_len 开到最大,反而可能浪费 KV cache 预算。
例如 Qwen3.5-35B-A3B 的 model card 标注了 262,144 token 原生上下文,并可扩展到约 1,010,000 token。这个数字说明 runtime 可以配置很长的 max_model_len,但实际能否线上使用,还要看显存、TTFT、KV cache 和请求长度分布。
可以把上下文窗口想成考试时允许带进考场的一摞资料。窗口越大,能带的资料越多;但资料越厚,读题(prefill)越慢,便签(KV cache)越占显存。长上下文服务的难点不是“模型宣称支持 128K”,而是在线上并发里让很多用户同时使用长上下文时仍然满足延迟和成本目标。
“能放进上下文”也不等于“模型能稳定用好”。超长上下文可能出现检索位置偏置、注意力稀释、无关信息干扰等问题。因此工程上常配合上下文压缩:
- 摘要压缩:把历史对话或长文档先压成短摘要,牺牲细节换取空间。
- RAG 检索:只把最相关片段放进 prompt,而不是把整库塞进去。
- 滑动窗口:保留最近若干轮对话,丢弃较早内容。
- 工具结果裁剪:对搜索、日志、网页结果做截断、去重、重排。
- 长上下文外推:用 RoPE scaling 等方法扩展位置编码,例如 Qwen3 文档中提到的 YaRN。
上下文压缩和 KV cache 优化是两类不同问题:前者减少“要读的内容”,后者降低“读过内容的缓存成本”。例如 prefix cache 没有压缩语义信息,只是复用相同前缀的计算结果;RAG 则是真的改变了进入模型的文本。
2. 模型文件结构
2.1 模型目录里有哪些文件
一个 Hugging Face 风格的 causal LM 目录通常长这样:
model/
config.json
generation_config.json
tokenizer.json
tokenizer_config.json
special_tokens_map.json
model.safetensors
model-00001-of-00004.safetensors
model-00002-of-00004.safetensors
model.safetensors.index.json
README.md
各类文件的职责可以这样理解:
| 文件 | 作用 |
|---|---|
config.json |
描述模型结构,例如层数、hidden size、attention heads、context length |
*.safetensors / *.bin |
真正的模型权重,也就是大量矩阵参数 |
tokenizer.json |
文本和 token id 之间的转换规则 |
tokenizer_config.json |
tokenizer 的附加配置,常包含 chat template |
generation_config.json |
默认生成参数,例如 temperature、top_p、eos token |
README.md |
model card,说明训练、用途、限制、license、示例 |
可以把模型目录理解成一个可执行程序的“安装包”:权重是主体二进制,config 是硬件说明书,tokenizer 是输入法,model card 是用户手册。
2.2 权重文件是什么
权重文件存储的是神经网络中所有可学习参数。在线性层里,它们通常是矩阵;在 embedding 层里,它们像一张 token id 到向量的查表;在 attention 和 MLP 中,它们决定了模型如何把上下文变换成下一个 token 的 logits。
logits 可以理解成模型给词表里每个候选 token 打的“未归一化分数”。例如 prompt 是“北京是中国的”,模型可能给 首、省、城市 等候选 token 分别打分;这些分数经过 softmax 才会变成概率,生成策略再根据概率选择下一个 token。因此,模型前向计算本身只是产出分数分布,真正输出哪个 token 还取决于 temperature、top-p、beam search 等解码策略。
现代模型常用 safetensors 格式,因为它避免了 Python pickle 反序列化的安全问题,并支持更高效的张量加载。大模型权重经常被切成多个 shard,例如 model-00001-of-00008.safetensors,原因很朴素:单个文件太大,不便于下载、校验和并行加载。
一个容易混淆的点是:权重文件不是模型代码。真正执行前向计算的是 PyTorch、Transformers、vLLM、SGLang 或 llama.cpp 中的模型实现;权重文件只是把这些实现中的矩阵填上具体数值。也就是说,config.json 告诉 runtime“这是一栋 32 层楼的建筑”,权重文件告诉 runtime“每层楼具体摆了哪些钢筋水泥”。
2.3 tokenizer 是什么
tokenizer 负责把人类文本转换成模型能处理的整数序列。模型并不直接理解“北京”或“GPU”,它看到的是 token id,例如:
文本:北京是中国的首都
token:["北京", "是", "中国", "的", "首都"]
id: [xxxxx, xxxx, xxxxx, xxx, xxxxx]
实际切分不一定符合人类词语边界。英文中 unbelievable 可能被拆成 un、believ、able;中文中一个词也可能被拆成多个 token。tokenizer 的差异会直接影响:
- 同一段文本算多少 token,从而影响上下文长度和计费。
- prompt 是否按模型训练时的格式组织。
- stop token、BOS/EOS、role token 是否正确。
聊天模型还依赖 chat template。例如同样的消息:
user: 你好
assistant: 你好,有什么可以帮你?
不同模型可能需要被格式化成 [INST]...[/INST]、<|user|>...<|assistant|> 或其他控制 token。如果 chat template 用错,模型就像看到一份格式混乱的试卷,能力会明显下降。
2.4 模型配置决定什么
config.json 决定 runtime 如何搭建模型结构。几个常见字段:
| 配置 | 含义 | 影响 |
|---|---|---|
num_hidden_layers |
Transformer 层数 | 影响计算量和权重大小 |
hidden_size |
每个 token 的隐藏向量维度 | 影响矩阵乘法规模 |
num_attention_heads |
Query attention heads 数量 | 影响 attention 并行结构 |
num_key_value_heads |
KV heads 数量 | 影响 KV cache 大小,GQA/MQA 会减少它 |
max_position_embeddings |
原生上下文长度 | 影响最多能看多长输入 |
rope_scaling |
RoPE 外推配置 | 影响长上下文扩展 |
torch_dtype |
默认精度 | 影响显存占用和硬件兼容 |
例如 Qwen2.5-Coder-7B-Instruct 的 model card 会列出层数、attention heads、KV heads 和 context length。对 serving 工程来说,num_key_value_heads 特别关键:它决定每个 token 要缓存多少 K/V 向量,进而影响高并发长上下文时的显存压力。
3. 模型加载与运行
3.1 启动服务时发生了什么
当执行类似 vllm serve Qwen/Qwen2.5-Coder-7B-Instruct 时,系统大致会做这些事:
这里有两个时间概念:冷启动时间和请求延迟。冷启动是服务从 0 到 ready 的时间,主要花在下载、磁盘读取、权重加载、kernel 初始化;请求延迟则是服务 ready 后处理每个请求的时间。一个 70B 模型冷启动可能需要数分钟,但不代表每个请求都要等数分钟。
这里的 kernel 指的是 GPU kernel,不是 Linux/macOS 的 system kernel。按照 CUDA Programming Guide 的语境,kernel 是运行在 GPU 上的一段并行计算函数,例如矩阵乘法、attention、RMSNorm、采样前的 logits 处理等。推理框架会尽量使用高度优化的 kernel,减少 Python 调度和显存读写开销。
模型加载失败的常见原因也和这些步骤对应:模型架构 runtime 不支持、权重格式不支持、显存不够、CUDA/driver/PyTorch 版本不匹配、gated model 没有 token 权限等。
3.2 显存主要被谁占用
LLM serving 的显存通常被四类东西占用:
- 模型权重:近似
参数量 × 每参数字节数。8B BF16 约 16 GB,8B INT4 约 4 GB 再加 scale 等元数据。 - KV cache:随 batch size、上下文长度、生成长度线性增长,长上下文和高并发时可能超过权重本身。
- activation 和 workspace:前向计算中的临时张量、CUDA graph、attention kernel 工作区等。
- runtime buffer:调度器、通信 buffer、tokenizer/CPU buffer、多卡通信等开销。
KV cache 的粗略估算公式是:
KV bytes ≈ 2 × layers × kv_heads × head_dim × tokens × batch × bytes_per_value
其中 2 代表 K 和 V。这个公式解释了为什么“并发”和“长上下文”会迅速吃掉显存:如果每个请求都有 8K token,上下文中的每个 token 都要在每一层留下 K/V 记录;batch 从 1 变 16,KV cache 也近似放大 16 倍。
可以把权重看成餐厅固定装修,KV cache 看成每桌客人正在使用的餐具和便签。装修开店时就占空间;客人越多、点单越长,桌面上的临时物品越多。
3.3 单卡和多卡怎么跑
单卡部署最简单:整个模型权重、KV cache 和计算都在一张 GPU 上。多卡部署的目标是解决两个问题:单卡放不下和单卡算不快。
常见并行方式如下:
| 并行方式 | 做法 | 适合场景 |
|---|---|---|
| Tensor Parallelism | 把一个矩阵切到多张 GPU 上一起算 | 大模型单层太大,常见于 vLLM/SGLang |
| Pipeline Parallelism | 把不同层放到不同 GPU 上,像流水线一样传递 | 模型层数多、跨节点部署 |
| Data Parallelism | 多个副本各自处理不同请求 | 单副本能放下,但需要更高 QPS |
| Expert Parallelism | MoE 模型中把专家分散到不同 GPU | DeepSeek-V3、Mixtral 等 MoE 模型 |
vLLM parallelism 文档 和 SGLang 文档 都覆盖了这些部署形态。实践中,单机多卡优先考虑 tensor parallel;如果模型已经能单卡放下而只是 QPS 不够,多个副本加负载均衡往往更简单。
3.4 推理硬件基础
CPU 也能跑 LLM,尤其配合 llama.cpp 和 GGUF 量化模型时,本地实验很方便。但 CPU 的并行矩阵计算能力和内存带宽通常远弱于服务器 GPU,所以它更适合小模型、低并发、离线或端侧场景。GPU 推理的优势在于:大量矩阵乘法可以并行执行,显存带宽高,Tensor Core 能加速 FP16/BF16/FP8/INT8 等低精度计算。
一个 GPU 可以粗略拆成几类部件:
| 部件 | 作用 | 对 LLM 推理的影响 |
|---|---|---|
| HBM/GDDR 显存 | 存权重、KV cache、临时张量 | 决定模型和并发能不能放下 |
| 显存带宽 | 权重和 KV cache 每秒能搬多少数据 | decode 阶段经常卡在这里 |
| SM | GPU 的主要计算单元 | 执行大量并行线程 |
| Tensor Core | 专门加速矩阵乘法 | 对 FP16/BF16/FP8/INT8 推理很关键 |
| NVLink/PCIe | GPU 间或 CPU-GPU 间通信 | 影响多卡 tensor parallel 和 KV 传输 |
实际运行时,CPU 负责发起 kernel launch,GPU 在大量 SM 上并行执行 kernel。矩阵乘法会被切成许多 tile,Tensor Core 对这些小块做高吞吐乘加;attention kernel 则要在 Q/K/V、mask、softmax、输出投影之间组织计算和显存访问。很多推理优化的本质,不是“换一个数学公式”,而是减少不必要的数据搬运、kernel launch 和中间张量写回。
常见硬件可以按定位理解:
| GPU | 常见定位 | 直观理解 |
|---|---|---|
| RTX 4090/5090 | 消费级开发、单机实验 | 便宜但显存和多卡互联受限 |
| L20 | 国内云上常见推理卡 | 适合中小模型和成本敏感推理 |
| L40S | 数据中心推理、图形和多模态 | 48GB 显存,兼顾 AI 与图形负载 |
| A100 | 上一代训练和推理主力 | 40/80GB 显存,生态成熟 |
| H100 / H200 | 高端训练和推理 | HBM 带宽高,FP8 和 Transformer Engine 友好 |
| B200/GB200 | Blackwell 代大规模 AI 集群 | 面向更大模型、更高吞吐和更低 token 成本 |
对 LLM serving 来说,GPU 型号不能只看 FLOPS。短 prompt 大 batch 的 prefill 更吃算力,长输出 decode 更吃显存带宽,长上下文高并发更吃显存容量和 KV cache 管理。选择硬件时,应同时看显存容量、显存带宽、低精度支持、互联方式和实际租赁成本。
4. 请求处理流程:Prefill 与 Decode
4.1 请求进入服务后经历哪些阶段
先看一个常见 LLM serving 架构。不同框架命名不同,但组件职责大体类似:
其中 API Server 负责 HTTP、鉴权、OpenAI-compatible API 和流式返回;Tokenizer 把消息格式化并转成 token id;Request Queue 暂存待处理请求;Scheduler 决定哪些请求在下一次 GPU forward 中被合并;Model Worker/GPU Runtime 真正执行模型前向计算;KV Cache Manager 管理每个请求的历史 K/V;Detokenizer 把输出 token 转回文本;Metrics/Logs/Traces 则用于压测和线上诊断。
一次在线请求从客户端到模型输出,通常经历:
注意,客户端感受到的延迟不只是 GPU 计算时间,还包括网络、HTTP 处理、tokenization、排队、detokenization 和流式传输。压测时如果只看 GPU kernel 时间,会低估真实服务延迟。
4.2 Prefill 阶段要理解什么
Prefill 是模型“读题”的阶段:服务把完整 prompt 一次性送入模型,计算所有输入 token 的 hidden states,并为后续生成建立 KV cache。因为 prompt 中的 token 已经全部知道,prefill 可以高度并行,所以它通常更偏 compute-bound。
例子:
prompt: 请总结下面这份 4000 token 的报告...
prefill: 一次性处理这 4000 个输入 token,建立 KV cache,并产生第一个输出 token
Prefill 直接决定 TTFT(Time To First Token)的下限。prompt 越长,模型“读题”越久;如果前面还有其他请求排队,TTFT 还会继续上升。
一些系统优化也发生在 prefill 附近。例如 vLLM 的 Automatic Prefix Caching 会复用相同前缀的 KV cache;这对 RAG、长系统 prompt、多轮对话很有价值。类比一下:如果每个学生都交同一份试卷前半部分,老师没必要每次重新读一遍公共题干。
4.3 Decode 阶段要理解什么
Decode 是模型“逐字答题”的阶段。自回归模型必须等上一个 token 出来后,才能把它追加到上下文里预测下一个 token,因此 decode 在单条序列内部是串行的。
第 1 步:prompt -> 北
第 2 步:prompt + 北 -> 京
第 3 步:prompt + 北京 -> 是
Decode 阶段有两个关键事实:
- 每生成一个 token 都要读一遍模型权重,所以它常常 memory-bound。
- KV cache 避免了重复计算历史 token,但每步仍要读取历史 K/V 来做 attention。
为什么 batching 能提升吞吐?假设服务同时有 16 个请求都在 decode。GPU 读取一次权重,可以顺便为 16 个请求各生成一个 token。单个请求未必更快,但系统总输出 token/s 会变高。
这也是 latency-throughput trade-off 的来源:batch 大,GPU 利用率高,吞吐好;但请求可能等 batch、等调度、和其他请求共享资源,单请求延迟变差。
4.4 生成策略属于哪里
模型前向计算输出的是 logits,也就是每个 token 的分数。生成策略负责把分数变成真正输出的 token。
常见参数:
| 参数 | 作用 | 直观理解 |
|---|---|---|
temperature |
调整分布尖锐程度 | 越低越保守,越高越发散 |
top_p |
只在累计概率前 p 的 token 中采样 | 从“高概率候选集合”里抽 |
top_k |
只保留概率最高的 k 个 token | 限制候选菜单大小 |
max_tokens |
最多生成多少 token | 防止输出无限变长 |
stop |
遇到指定字符串/token 停止 | 例如遇到 </answer> 停止 |
repetition_penalty |
惩罚重复 token | 减少复读 |
如果 temperature=0 或接近 0,模型更像做选择题,总选最高分答案;如果 temperature 较高,则更像写作时保留更多表达可能。Beam search 会在每一步保留多个候选序列,适合翻译、图像描述这类输入约束较强的任务,但开放式聊天里可能更容易产生保守、重复的回答。工程上,生成参数不仅影响质量,也影响性能:max_tokens 越大,最坏 E2E latency 越高;beam search、parallel sampling 会增加每个请求的计算和 KV cache 压力。
什么时候停止生成也很重要。最常见的停止条件有四种:
- 模型生成 EOS token。EOS 是 End Of Sequence,表示模型认为序列结束。
- 达到
max_tokens或max_new_tokens上限。 - 命中用户配置的 stop string,例如生成到
</answer>。 - 客户端取消、服务端 timeout,或调度器因资源压力中断请求。
EOS 是 tokenizer 词表里的特殊 token,stop string 则是服务层按文本匹配的停止规则。GenerationConfig 里常见的 eos_token_id、forced_eos_token_id、stop_strings 就对应这些控制。二者经常一起用:EOS 让模型自然结束,stop string 用来适配业务协议或结构化输出。
还有一类优化叫 speculative decoding:让小模型先草拟多个 token,大模型一次性验证。类比写论文时让助研先打草稿,导师只负责批改和确认。如果草稿命中率高,decode 可以明显加速。
4.5 什么是 PD 分离
PD 分离(prefill-decode disaggregation)指把 prefill 和 decode 放到不同 worker、不同 GPU,甚至不同机器上执行。动机很直接:prefill 更偏 compute-bound,decode 更偏 memory-bound;把它们混在同一批 GPU 上时,长 prompt 的 prefill 可能打断正在 decode 的请求,拉高 ITL,而 decode 的高频小步计算也会影响 prefill 调度。
可以把它想成餐厅里把“读菜单和备菜”和“逐道上菜”拆成两个工位。前者适合大块时间集中处理,后者需要稳定、频繁、低抖动地出菜。拆开后,每个工位能按自己的瓶颈配置资源。
研究系统 Splitwise 和 DistServe 都讨论了这种 phase splitting / disaggregation 思路。收益是降低 prefill 和 decode 的相互干扰,并可分别优化 TTFT 和 TPOT;代价是系统复杂度更高,需要在阶段之间传输 KV cache 或中间状态,对网络、调度器和容错都有更高要求。小规模单机部署通常不必一开始就做 PD 分离,但长上下文、高并发、严格 SLO 的生产集群会越来越关注它。
5. Serving 框架对比
5.1 vLLM
vLLM 的核心目标是 GPU 服务端高吞吐推理。它最出名的设计是 PagedAttention:借鉴操作系统虚拟内存分页思想,把 KV cache 分块管理,减少连续大块显存分配带来的碎片和浪费。
直观地说,传统 KV cache 管理像给每个请求预留一整排连续座位;如果客人中途离开或增加,座位容易碎片化。PagedAttention 像把座位拆成小块,按需分配、可复用、可共享,从而容纳更多并发请求。
vLLM 适合:
- 部署 Hugging Face 上的主流 open-weight 模型。
- 提供 OpenAI-compatible API。
- 追求较高吞吐和较成熟的生产部署能力。
- 需要量化、多卡、prefix caching、benchmark 工具链。
它不只是“模型 runner”,而是一个面向 serving 的系统:调度、batching、KV cache 管理、metrics、并行策略都很重要。学习 vLLM,重点不是背启动命令,而是理解它为什么能在同等显存下塞进更多有效请求。
5.2 Ollama
Ollama 更像“本地模型运行器 + 模型管理工具”。它降低了本地跑模型的门槛:ollama run llama3.1 就能拉模型、启动 runtime、进入对话。它默认提供本地 API,官方文档中默认 base URL 是 http://localhost:11434/api,并支持 OpenAI compatibility。
Ollama 的一个重要概念是 Modelfile,类似 Dockerfile:用 FROM 指定基础模型,用 SYSTEM 写系统提示词,用 PARAMETER 固定 temperature、context length 等运行参数。
它适合:
- 个人电脑、本地开发、离线演示。
- 快速比较不同模型。
- 把 GGUF 模型包装成简单 API。
- 对极致吞吐要求不高,但希望易用。
Ollama 通常不是大规模线上 GPU serving 的首选,但非常适合建立直觉:模型如何被拉取、加载、运行、暴露 API,以及量化模型在本地硬件上如何权衡速度和质量。
5.3 SGLang
SGLang 的定位是高性能 serving 和复杂 LLM program 执行。它不只关心“单次 chat completion”,还关心多轮调用、结构化输出、分支控制、工具调用、agent workflow 等更复杂的推理程序。
其论文 SGLang: Efficient Execution of Structured Language Model Programs 提出了前端语言和后端 runtime 的组合;后端中的 RadixAttention 用于复用共享前缀的 KV cache。对多轮对话、few-shot prompt、RAG 模板这类“很多请求共享长前缀”的工作负载,这类优化很关键。
可以把 SGLang 想成“懂得复用草稿纸的考试系统”:如果 100 道题都共享同一段材料,它不会让模型每次从头读材料,而是尽量复用已经算过的前缀状态。
它适合:
- 结构化输出,如 JSON、regex、schema-constrained generation。
- agent、多步推理、复杂 prompt program。
- 高吞吐、低延迟、多卡 serving。
- 需要快速跟进新模型和 reasoning parser 的场景。
5.4 其他相关框架
除了 vLLM、Ollama、SGLang,还常见以下系统:
- llama.cpp:C/C++ 实现,主打 GGUF、CPU/Metal/CUDA/端侧推理,是很多本地工具的底座。
- TensorRT-LLM:NVIDIA 面向高性能推理的优化库,适合愿意为特定硬件做更深优化的生产场景。
- Triton Inference Server:通用推理服务框架,可服务多种模型后端,不只 LLM。
- LMDeploy:国内常见的 LLM 部署工具链,支持 TurboMind、量化和服务化。
- Text Generation Inference:Hugging Face 早期生产 serving 代表,现在更适合作为历史和架构参考。
选型时可以先问:我要的是本地易用、服务端吞吐、复杂程序执行、硬件极致优化,还是通用推理平台?不同答案会指向不同工具。
6. 并发、Batch 与调度
6.1 什么是并发
“并发”在 LLM serving 里至少有四种口径:
| 口径 | 含义 |
|---|---|
| 客户端并发 | 同时有多少客户端连接或请求在发起 |
| 排队并发 | 有多少请求已经进入系统但还没完成 |
| running sequences | 有多少序列正在被调度器处理 |
| batch size / batched tokens | 某次 GPU forward 实际合并了多少序列或 token |
用户说“并发 100”,可能只是压测工具同时开 100 个连接;但 GPU 看到的可能是某一时刻 batch 里有 32 条 decode 序列,另有 68 条排队或等待 prefill。分析问题时必须先明确口径。
一个小例子:咖啡店门口有 100 人排队,不代表咖啡机同时在做 100 杯。真正同时制作的杯数取决于咖啡机容量、店员调度、每杯饮品复杂度和是否有人点超大杯。
6.2 并发和吞吐是什么关系
并发升高通常会先提升吞吐,因为 GPU 从“小批量低利用率”变成“更大 batch、更高利用率”。但超过某个点后,吞吐会趋于饱和,延迟开始快速上升。
可以用一个简化关系理解:
平均并发 ≈ QPS × 平均 E2E latency
这就是 Little’s Law 在服务系统中的直觉版本。假设服务稳定处理 10 QPS,每个请求平均 5 秒完成,那么系统里平均大约有 50 个请求处于未完成状态。
LLM 的特殊性在于,不同请求的“大小”差异很大:一个 50 token 输入、20 token 输出的请求,和一个 8000 token 输入、2000 token 输出的请求,对 GPU 的占用完全不是一个量级。因此只说 QPS 不够,必须同时说 input/output token 分布。
6.3 Serving 调度器要解决什么
LLM serving 调度器的任务是把请求组织成 GPU 能高效处理的批次,同时尽量控制延迟。它要处理几类矛盾:
- prefill 计算重,decode 更频繁但每步较小。
- 新请求希望尽快 prefill 以降低 TTFT,老请求希望 decode 连续进行以降低 ITL。
- batch 大有利于吞吐,但会增加等待和干扰。
- 长请求占用 KV cache 久,短请求又不应该被饿死。
continuous batching 的思想是:不要等一整个固定 batch 全部完成后再接新请求,而是在每个 decode step 动态加入新请求、移除完成请求。这比传统静态 batching 更适合 LLM,因为每个请求输出长度不同。
chunked prefill 则是把很长的 prefill 切成块,避免一个超长 prompt 长时间占住 GPU,导致正在 decode 的请求 token 间隔变大。vLLM 的 optimization 文档 中就把 chunked prefill 作为调优点之一。
6.4 什么是容量规划
容量规划回答的问题是:在给定模型、硬件和 SLO 下,服务最多能承载多少负载。SLO 是 Service Level Objective,即服务质量目标,例如 P99 TTFT < 1s、P99 ITL < 80ms、错误率低于某个阈值。它不是“系统极限能跑多快”,而是“在用户体验可接受的条件下能跑多大负载”。
正确的压测方式不是只问“最大 QPS 是多少”,而是固定场景:
模型:Qwen2.5-Coder-7B-Instruct
硬件:1 × A100 80GB
输入长度分布:P50 1K,P95 8K
输出长度分布:P50 256,P95 1K
SLO:P99 TTFT < 1s,P99 ITL < 80ms
目标:满足 SLO 下的最大 goodput
如果没有这些约束,QPS 数字几乎不可比较。短请求 benchmark 可以把 QPS 做得很好看,但一上线到长上下文 RAG,就会因为 prefill 和 KV cache 压力完全不同而失真。
工程上可以分三类压测:短问答压测看 API overhead 和 decode;长 prompt 压测看 prefill 和 prefix cache;长输出压测看 decode 吞吐和显存稳定性。
7. 可观测性 Observability
如果说前几节是在解释“LLM serving 如何工作”,那么可观测性(observability)关心的是:服务运行时到底发生了什么,为什么变慢,哪里快要违反 SLO,改动之后是否真的变好。
在云原生系统里,可观测性通常围绕 metrics、logs、traces 展开;LLM serving 也一样,但会多出一组模型相关信号:输入/输出 token 长度、TTFT、ITL、KV cache、batch、prefix cache、GPU 显存、调度队列、goodput 等。它不是简单做一个 GPU 利用率面板,而是要把“用户请求”“模型阶段”“调度器状态”“硬件资源”连起来看。
7.1 观测对象与信号
一个 LLM 服务可以按层观测:
常见观测信号如下:
| 信号 | 记录什么 | 典型用途 |
|---|---|---|
| Metrics | 数值型时间序列 | 看趋势、报警、SLO、容量规划 |
| Logs | 离散事件和错误上下文 | 查失败请求、OOM、超时、异常栈 |
| Traces | 一次请求跨组件的阶段耗时 | 定位慢在排队、prefill、decode 还是网络 |
| Profiles | kernel、CPU、内存等细粒度性能剖析 | 优化热点、排查低 GPU 利用率 |
| Benchmarks | 受控负载下的实验结果 | 比较模型、框架、参数和硬件 |
一个简单类比:metrics 像体检指标,logs 像病历,traces 像从挂号到检查到开药的全流程时间线,benchmarks 则像标准化体能测试。
7.2 延迟指标
LLM serving 的延迟指标要按阶段拆:
| 指标 | 含义 | 主要对应阶段 |
|---|---|---|
| TTFT | Time To First Token,请求发出到第一个 token 返回 | 排队 + tokenization + prefill |
| ITL / TPOT | Inter-Token Latency / Time Per Output Token,相邻输出 token 间隔 | decode |
| E2E latency | End-to-End latency,完整响应耗时 | 全流程 |
| queue time | 请求在调度器里等待的时间 | 调度 |
| prefill latency | 处理输入 prompt 的时间 | prefill |
| decode latency | 生成输出 token 的累计时间 | decode |
一个简化公式:
E2E ≈ TTFT + TPOT × (output_tokens - 1)
如果输出很短,TTFT 主导体验;如果输出很长,TPOT 主导体验。流式聊天中,用户最先感知的是 TTFT,随后感知的是 token “蹦出来”的速度,也就是 ITL/TPOT。非流式接口则会把全部内容攒到最后返回,用户只感受到 E2E latency,但服务端仍然应该记录 TTFT 和 ITL,否则很难判断慢在 prefill 还是 decode。
7.3 吞吐与资源指标
吞吐有多个口径:
| 指标 | 单位 | 解释 |
|---|---|---|
| QPS / RPS | req/s | 每秒完成多少请求 |
| input throughput | input tok/s | 每秒处理多少输入 token |
| output throughput | output tok/s | 每秒生成多少输出 token |
| total token throughput | tok/s | 输入和输出 token 总量 |
| per-GPU throughput | tok/s/GPU | 多卡场景下的资源效率 |
对 LLM 来说,单看 QPS 很危险。假设两个服务都是 10 QPS:服务 A 每个请求输出 20 token,服务 B 每个请求输出 1000 token,它们的 GPU 压力完全不同。更稳妥的表达是:“在某个输入/输出长度分布下,满足某个延迟 SLO 时,每张 GPU 每秒能产出多少 output tokens。”
还要同时看资源和调度指标:
- GPU utilization、显存使用量、显存带宽、功耗、温度。
- KV cache 使用量、block 使用率、prefix cache hit rate。
- scheduler queue length、running requests、waiting requests。
- preemption / eviction 次数。
- HTTP 4xx/5xx、timeout、cancel、OOM。
- cost per 1M tokens。
vLLM Production Metrics 就暴露了 generation tokens、prefix cache hits、preemptions、speculative decoding、KV connector 等指标;这些指标比单纯的 GPU utilization 更接近 LLM serving 的真实瓶颈。
7.4 分位数 SLO 与 Goodput
线上服务不要只看平均值。平均 TTFT 可能是 300ms,但 P99 TTFT 是 5s,用户投诉通常来自后者。
P50 TTFT = 300ms:一半请求首 token 延迟小于 300ms
P90 TTFT = 900ms:90% 请求小于 900ms
P99 TTFT = 5s:99% 请求小于 5s,最慢 1% 更糟
LLM 服务的长尾来自很多地方:个别超长 prompt、调度器排队、KV cache preemption、GPU kernel 抖动、网络慢客户端、模型冷加载、输出长度差异。博士生做实验时尤其要注意:只报告 mean latency 往往不够,至少应报告 P50/P90/P99 和长度分布。
Goodput 指的是满足 SLO 的有效吞吐。例如系统每秒完成 100 个请求,但只有 70 个请求满足 TTFT < 1s 且 TPOT < 80ms,那么 goodput 是 70 req/s,而不是 100 req/s。
一个常见误区是追求最高 output tok/s。极限吞吐通常靠大 batch 换来,可能显著牺牲 TTFT 和 P99;真正上线时,要在 SLO 约束下比较吞吐。
7.5 日志与 Trace
Metrics 适合看趋势,但一次具体请求为什么慢,通常要靠日志和 trace。建议至少记录这些字段:
| 字段 | 例子 | 用途 |
|---|---|---|
| request id | req-123 |
串联 API server、scheduler、worker 日志 |
| model | Qwen2.5-Coder-7B-Instruct |
多模型服务定位问题 |
| input/output tokens | 1024 / 256 |
解释延迟和成本 |
| queue / prefill / decode time | 20ms / 300ms / 4s |
阶段归因 |
| finish reason | eos / length / stop / error |
判断是否异常终止 |
| error code | timeout / oom / cancelled |
失败分类 |
Trace 则把一次请求拆成多个 span,例如:
HTTP receive
-> auth / routing
-> apply chat template
-> tokenize
-> queue wait
-> prefill
-> decode step loop
-> detokenize
-> stream response
OpenTelemetry 是云原生里常用的开放标准,用来生成、收集和导出 traces、metrics、logs。对 LLM 应用层,还可以使用 Langfuse 或 OpenLLMetry 这类工具观测 RAG、agent、tool call、prompt 版本、用户反馈和成本。它们和 serving runtime 的 metrics 不是替代关系:前者更偏应用链路,后者更偏模型服务和 GPU 资源。
日志里要谨慎处理 prompt 和输出内容。生产环境通常需要脱敏、采样、权限控制和保留周期策略,否则 observability 很容易变成隐私和合规风险。
7.6 监控技术栈
一个常见开源栈可以长这样:
关键组件:
| 组件 | 作用 |
|---|---|
| Prometheus | 拉取和存储时间序列 metrics,用 PromQL 查询和报警 |
| Grafana | 展示 dashboard,关联 metrics、logs、traces |
| OpenTelemetry | 标准化 traces、metrics、logs 的采集和导出 |
| NVIDIA DCGM Exporter | 暴露 GPU 利用率、显存、功耗、温度等指标给 Prometheus |
| Loki / Tempo / Jaeger | 存储日志和 trace,帮助做请求级问题定位 |
一个最小可用方案是:serving 框架暴露 /metrics 或 Prometheus 格式指标,Prometheus 定时 scrape,Grafana 展示 TTFT/ITL/QPS/GPU/KV cache 面板,Alertmanager 根据 P99、错误率、OOM、显存水位报警。等到业务复杂后,再补 OpenTelemetry tracing 和应用层 LLM observability。
7.7 压测与基准测试
压测是主动制造负载,监控是被动观察线上运行。两者要结合:压测告诉你系统极限和拐点,监控告诉你线上是否正在接近这些拐点。
为了让实验可复现、可比较,压测至少固定:
- 模型名、权重精度、量化方式。
- serving 框架版本和启动参数。
- GPU 型号、数量、driver、CUDA。
- 输入 token 长度分布。
- 输出 token 长度分布或
max_tokens。 - stream / non-stream。
- 并发模式:固定 concurrency、固定 request rate,还是 burst。
- 采样参数:temperature、top_p、stop。
- warmup 次数和统计窗口。
常见工具:
| 工具 | 适合做什么 |
|---|---|
| vLLM benchmark | vLLM 自带 benchmark,输出 request/input/output throughput、TTFT、TPOT、ITL 等 |
| AIPerf | 新一代 GenAI benchmarking 工具,支持多种 endpoint、数据集、并发和请求率控制 |
| GenAI-Perf | NVIDIA 旧工具,文档已说明逐步转向 AIPerf,但仍是理解 LLM benchmark 指标的好材料 |
| Grafana k6 / Locust | 通用 HTTP 负载测试,适合压 API gateway、鉴权、代理层,但需要自己统计 token 指标 |
读 benchmark 结果时,第一反应应该是看 workload,而不是直接比较榜单数字。短问答、长 prompt、长输出、多轮对话、RAG、agent 工具调用,会得到完全不同的 TTFT、ITL、吞吐和显存曲线。
8. 量化与显存优化
8.1 为什么需要量化
量化的目标是用更少 bit 表示原本高精度的数值。LLM 推理中最常见的是权重量化和 KV cache 量化。
最简单的显存估算:
7B FP16/BF16 权重 ≈ 7 × 10^9 × 2 bytes ≈ 14 GB
7B INT4 权重 ≈ 7 × 10^9 × 0.5 bytes ≈ 3.5 GB
量化的收益有三类:
- 省显存:模型能放进更小的 GPU,或同一张 GPU 支持更大 batch。
- 提速度:decode 往往受显存带宽限制,数据更小,搬运更快。
- 降成本:同等负载可以用更少或更便宜的硬件。
代价是精度损失和 kernel 复杂度。不是所有量化都会变快:如果硬件或 runtime 没有高效 kernel,反量化开销可能抵消收益。
8.2 权重量化
权重量化作用于模型固定参数。常见方法:
| 方法 | 典型格式 | 关键思想 |
|---|---|---|
| LLM.int8() | INT8 混合精度 | 大部分用 INT8,离群维度保留高精度 |
| GPTQ | W4/W3 | 用近似二阶信息补偿量化误差 |
| AWQ | W4A16 | 根据激活分布保护重要权重通道 |
| SmoothQuant | W8A8 | 把激活离群值难度迁移到权重上 |
| QLoRA | NF4 + LoRA | 冻结 4-bit 基座,只训练低秩适配器 |
| GGUF K-quants | Q4_K_M、Q5_K_M 等 | llama.cpp/Ollama 本地推理常用格式 |
理解量化可以从一个标尺类比开始:原来用毫米级尺子测长度,现在只给 16 个刻度。刻度越少,记录越省空间,但误差越大。优秀的量化方法就是想办法把刻度放在“最常出现、最重要”的数值区域,而不是平均撒开。
在服务端 GPU 上,AWQ/GPTQ/FP8 等是否值得用,要看框架支持和 kernel 效率。vLLM 的 quantization 文档 列出了支持的量化后端;Ollama/llama.cpp 则更多围绕 GGUF 量化模型。
8.3 KV cache 优化
KV cache 不是模型文件里下载来的东西,而是推理时动态产生的。每个请求的每个历史 token,都会在每层 attention 中留下 Key 和 Value,用于后续 token 查询。
它解决的是“不要重复读题”的问题:生成第 100 个 token 时,模型不需要重新计算前 99 个 token 的全部中间状态,只需要复用 KV cache。但代价是显存随上下文长度和并发线性增长。
这里要和 1.4 节的上下文压缩区分开:上下文压缩是在模型计算前减少输入 token 或历史信息,KV cache 优化是在模型已经保留这些 token 后,降低缓存它们的显存和调度成本。前者改变 prompt,后者改变 runtime 管理方式。
KV cache 优化有几类:
- dtype 降低:例如 FP16/BF16 改成 FP8 或 INT8。
- KV cache quantization:如 KIVI、KVQuant 等研究。
- prefix cache:复用相同前缀的 KV。
- paged/block 管理:减少显存碎片,例如 PagedAttention。
- eviction/offloading:长上下文场景下把部分 KV 移出 GPU 或丢弃不重要 token。
KIVI 的一个重要观察是:Key 和 Value 的分布不同,Key 更适合 per-channel 量化,Value 更适合 per-token 量化。这说明 KV cache 量化不是简单把所有数压成 INT4,而要根据张量统计特性设计。
vLLM 已有 quantized KV cache 相关支持。实践中,短上下文、低并发场景开启 KV 量化未必更快;长上下文、高并发、显存紧张时,KV cache 优化才更容易体现价值。
8.4 量化选择要问什么
量化选型可以按下面的问题走:
- 目标是省权重显存,还是省 KV cache 显存?
- 场景是本地 CPU/消费级 GPU,还是服务端 A100/H100?
- 框架是否原生支持该量化格式?
- 是否有高效 kernel,还是只是“能跑”?
- 精度损失是否在业务可接受范围内?
- benchmark 是否覆盖真实输入输出长度和并发?
几个经验判断:
- 本地运行优先看 GGUF 和 Ollama/llama.cpp 生态。
- 单卡显存放不下模型时,先考虑权重量化。
- 长上下文并发撑爆显存时,再重点考虑 KV cache 量化或 prefix cache。
- H100/Blackwell 等新硬件上,FP8 往往更值得关注。
- 量化后必须重新评估质量,特别是代码、数学、工具调用和结构化输出场景。
最终,量化不是单独的“压缩技巧”,而是 serving 系统设计的一部分。它改变权重大小、KV cache 容量、batch 上限、decode 带宽压力,最终反映到 TTFT、ITL、吞吐和 goodput 上。