上下文没有超长,为什么 LLM 并发一上来仍然 OOM?
一个请求的 prompt 只有 8000 tokens,模型标称支持 32768 tokens,单请求测试也能正常回答。于是把并发从 1 调到 16,服务立刻显存不足。
“没有超过上下文窗口”只说明单条序列的 token 数合法,不代表显存足够。模型推理还要存:
1 | 模型权重 |
长上下文、输出长度、并发和精度共同决定容量。理解这个问题,几乎能把 LLM 应用中的 token、Attention、KV Cache、采样、RAG、量化、吞吐和评估串在一起。
一、LLM 不是应用,只是概率生成引擎
Decoder-only LLM 的核心任务可以简化为:
1 | 给定已有 token x1...xt |
它擅长语言生成、总结、翻译、代码和模式转换,但不自动拥有这些能力:
- 私有或最新业务事实;
- 数据库事务和权限;
- 精确计算与副作用幂等;
- 工具参数安全;
- 输出 schema 合法性;
- 引用与事实一致性。
一个完整应用通常是:
1 | 身份/权限 |
模型负责提出候选输出,应用负责决定哪些输入可见、哪些操作允许、哪些结果可以被接受。
二、Token:所有容量计算的共同单位
模型不直接读取“字”或“单词”,而是由 tokenizer 转成 token ID。同一句中文、代码或 JSON,在不同 tokenizer 下的 token 数可能不同。
上下文窗口通常包含:
1 | system/developer 指令 |
所以标称 32768 并不表示可以塞入 32768 输入 token,再额外生成 4096。
一份可执行的 token budget
1 | from __future__ import annotations |
token 数必须由目标模型的 tokenizer 计算,字符数只能做粗略预估。聊天模板还会加入角色标记和特殊 token,因此最好对最终序列计数。
安全余量用于模板差异、工具回合和估算误差,但不能替代精确计数。
三、Transformer 与 Attention:上下文为什么昂贵
原始 Transformer 用 Attention 建模 token 间关系。简化的 scaled dot-product attention:
1 | Attention(Q, K, V) = softmax(QKᵀ / √d) V |
- Q:当前位置在寻找什么;
- K:每个位置提供的匹配特征;
- V:匹配后汇总的内容;
- causal mask:生成模型不能看到未来 token。
标准全量 Attention 的矩阵随序列长度平方增长。现代内核、分块、滑动窗口或其他结构能改善真实成本,但“上下文加倍不会永远只增加一倍开销”仍是重要直觉。
Transformer block 不只有 Attention,还包括前馈网络、归一化和残差连接。把 LLM 简化成“只有 Attention”适合入门,但不适合做完整性能或参数量估算。
四、Prefill 与 Decode:同一次请求有两个不同阶段
1. Prefill
模型一次处理输入 prompt,建立各层 KV Cache。长 prompt 会增加首 token 延迟(TTFT)和计算量。
2. Decode
模型逐 token 生成。每生成一个 token,都读取之前的 KV Cache,再追加当前 token 的 K/V。Decode 常受显存带宽、并发调度和 KV 容量影响。
因此“端到端 5 秒”还不够定位问题。至少分开:
| 指标 | 回答的问题 |
|---|---|
| Queue time | 请求是否在等容量 |
| TTFT | 用户多久看到第一个 token |
| Prefill time/tokens | 输入处理是否过重 |
| Inter-token latency | token 流是否流畅 |
| Output tokens/s | 单请求生成速度 |
| E2E latency | 用户总共等多久 |
| Aggregate tokens/s | 服务总体吞吐 |
当前 vLLM 指标也显式区分 queue、prefill、decode、TTFT 和 inter-token latency。只监控平均请求耗时,会把排队和模型计算混在一起。
五、KV Cache:并发 OOM 的常见主因
生成第 N 个 token 时,如果重新计算前面所有 token 的 K/V 会非常浪费。KV Cache 保存每层历史 K 和 V,用显存换速度。
对常见 decoder 架构,可用下面公式粗估未考虑框架对齐/分页的原始 KV 字节:
1 | 2(K 和 V) |
1 | def kv_cache_gib( |
这个公式不是所有模型的精确答案。需要从模型配置确认:
- 是 MHA、GQA 还是 MQA;
- KV head 数与 head dimension;
- KV cache dtype;
- 跨层是否共享或使用特殊结构;
- 推理框架的 block、对齐、碎片和保留空间。
但它说明了关键关系:平均缓存 token 数或并发翻倍,KV 原始容量近似翻倍。
六、权重能放下,不代表服务能跑
权重粗估:
1 | def weight_gib(parameters: int, bits_per_parameter: int) -> float: |
真实占用还包括量化 scale/zero-point、未量化层、临时 workspace、CUDA graph、通信 buffer 和运行时。4-bit 权重也不意味着整个服务只有“参数量 × 0.5 字节”。
量化主要减少权重,KV Cache 可能仍是 FP16/BF16,除非框架与硬件支持并显式配置 KV 量化。因此长上下文高并发下,权重量化后仍可能由 KV 主导。
七、Batch 与 Continuous Batching
静态 batch 要等整个批次完成;生成长度不一时,短请求可能陪长请求等待。Continuous batching 允许完成的序列离开,新请求加入,提高 GPU 利用率。
提高并发或 batch 通常增加总吞吐,却可能恶化单请求 TTFT/ITL。容量规划必须明确目标:
1 | 离线任务:优先 aggregate tokens/s 和单位成本 |
如果 KV 已满,新请求会排队、抢占或被拒绝。盲目增大最大并发只会把 OOM 变成更长排队。
八、长上下文不是“把所有内容都塞进去”
长窗口解决的是“允许容纳”,不是“模型一定能有效利用”。无关信息会:
- 增加 TTFT、KV 和费用;
- 稀释关键证据;
- 引入矛盾版本;
- 扩大 prompt injection 面;
- 让引用定位更困难。
证据选择应该受 token budget 驱动:
1 | from dataclasses import dataclass |
这是简单贪心,不保证全局最优,也没有处理来源多样性和重复内容;它的价值是让“为什么某条证据没进 Prompt”可以被解释和测试。
九、采样参数:先区分确定性解码与采样
模型输出 logits,softmax 后得到候选 token 概率。常见参数:
do_sample=False:通常走 greedy 或 beam 等非采样策略;temperature:采样前缩放 logits,越低通常越集中;top_k:只保留概率最高的 K 个候选;top_p:保留累计概率达到阈值的最小候选集合;max_new_tokens:输出 token 上限;stop/EOS:停止生成。
Hugging Face Transformers 当前文档也明确区分 greedy、multinomial sampling 和 beam search。不同推理服务的默认值可能来自模型配置或服务配置,应用应显式发送并记录关键参数。
生成策略也需要版本化
1 | from dataclasses import dataclass |
即使固定 seed,GPU kernel、并发调度、模型服务版本和量化也可能影响完全复现。业务测试不应依赖长文本逐字相等;优先验证 schema、事实、引用和禁止项。
低 temperature 不能消除幻觉。它只改变输出分布,不给模型增加事实来源。
十、Prompt:把数据和指令分开,但不要迷信措辞
一个 RAG Prompt 通常包含:
1 | 系统职责与边界 |
1 | 你是文档问答助手。 |
标签和文字能减少歧义,但不是安全边界。文档可能包含恶意指令,模型仍可能服从。真正的安全边界必须在应用层:权限、工具白名单、参数校验、最小凭证、输出过滤与人工审批。
Prompt 要有版本号。改一个句子也可能影响拒答、工具选择和输出格式,应经过固定评估集和 canary,而不是直接覆盖线上模板。
十一、RAG:解决知识问题,不解决所有行为问题
RAG 查询链路:
1 | 问题 |
适合 RAG:私有知识、频繁更新、需要引用、文档可检索。
适合微调:稳定输出风格、特定格式、领域行为、模型不会的任务模式。微调不适合频繁更新事实,也不提供可追踪引用。
常见失败必须分层诊断:
| 失败 | 应看哪里 |
|---|---|
| 正确 chunk 没进候选 | chunk、embedding、关键词、ANN Recall |
| 候选有但排序靠后 | reranker、query 改写 |
| Prompt 中有证据但答错 | 模型、Prompt、冲突证据 |
| 引用编号不存在 | 输出校验与重试/拒绝 |
| 返回越权原文 | 服务端权限,而不是 Prompt |
十二、结构化输出:模型结果永远要校验
只在 Prompt 中写“输出 JSON”不够。应用要使用模型/服务支持的结构化输出能力,并在本地再次校验:
1 | from dataclasses import dataclass |
JSON 合法不代表操作被授权。模型生成 delete_user(id=7) 时,服务端仍需检查当前身份、目标范围、幂等与审批。
十三、Tool、MCP、Workflow 与 Agent 的关系
这些概念处在不同层:
| 概念 | 解决什么 |
|---|---|
| Tool/function calling | 模型提出结构化工具调用 |
| MCP | 客户端与服务器标准化发现/调用工具、资源等 |
| Workflow | 程序预先决定步骤和分支 |
| Agent | 模型在循环中决定下一步和工具 |
MCP 让接入方式标准化,不会自动授予权限,也不会让第三方工具返回内容变可信。每个 server/tool 仍需要身份、授权、输入限制、超时和审计。
固定的“检索一次—生成一次”应使用 workflow。只有任务确实需要动态规划、多轮工具和停止决策时才使用 Agent:
1 | 观察 -> 决策 -> 工具 -> 新观察 -> ... -> 最终答案 |
Agent 必须有最大轮数、token/费用/时间预算、工具白名单、重复调用检测和高风险人工确认。多 Agent 不会自动提高正确率,反而增加消息、延迟和状态故障面。
十四、Memory:对话历史不是无限追加
短期记忆通常是当前对话消息;长期记忆是经过筛选、结构化并获准保存的用户偏好或事实;RAG 是外部知识检索。三者不能混为一个不断增长的消息列表。
历史压缩应保留:
- 用户当前目标与约束;
- 未完成决定;
- 必要实体和工具结果引用;
- 原消息/摘要版本的可追踪关系。
摘要也是模型生成结果,可能丢失或篡改细节。关键交易、权限和任务状态必须来自数据库,不从会话摘要恢复。
十五、训练、对齐与微调速查
1 | 预训练:大量数据预测 token,获得基础能力 |
这些阶段解决的问题不同。SFT 可以教格式,但不能保证实时知识;偏好对齐改善行为倾向,但不等同于事实验证;LoRA 降低可训练参数,不代表推理时所有成本都按同样比例下降。
训练数据质量、许可证、隐私和评估泄漏比“样本越多越好”更重要。
十六、精度、量化与准确率不是一回事
| 格式 | 粗略字节/参数 | 常见用途 |
|---|---|---|
| FP32 | 4 | 基准、部分训练状态 |
| FP16/BF16 | 2 | 推理与混合精度训练 |
| FP8/INT8 | 1 | 支持硬件上的低精度计算/量化 |
| INT4 | 0.5 | 权重量化推理 |
“精度”在这里是数值表示格式;“准确率/质量”是任务表现。量化收益取决于硬件、kernel、模型和 workload,可能降低显存但不提速,也可能让代码、数学、长上下文或少数语言退化。
量化发布必须对比未量化基线:任务质量、困惑度/生成稳定性、TTFT、ITL、吞吐、显存和功耗。不要只测一个聊天问题。
十七、评估:不要用单一总分决定上线
不同应用的正确性不同:
1 | 分类:准确率、F1、校准 |
固定评估集要版本化,并按语言、领域、长度、风险和难度切片。整体均值可能掩盖一个高风险子集完全退化。
线上记录应包含 request/trace ID、模型与 Prompt 版本、输入/输出 token、采样参数、检索 chunk ID、工具调用、分阶段耗时、完成原因和安全结果。敏感 Prompt、文档和工具返回默认不应全文入日志。
十八、安全:模型上下文中的内容都可能不可信
主要风险:
- prompt injection:文档/网页诱导模型忽略系统规则;
- 数据泄露:跨租户检索、日志或工具返回暴露敏感信息;
- 工具滥用:模型构造越权、危险或高成本调用;
- 资源耗尽:超长输入、超大输出、工具循环;
- 供应链:远程模型代码、插件、MCP server 或依赖不可信。
防线应是确定性的:
1 | 服务端授权 > 模型口头遵守 |
十九、排查“上下文合法但 OOM”的顺序
- 统计每条活跃序列实际 prompt + 已生成 token;
- 按真实 layers、KV heads、head dimension、dtype 粗估 KV;
- 确认权重、量化元数据和未量化层真实占用;
- 查看并发、排队、KV 使用率、抢占与碎片;
- 检查是否预留过大的
max_new_tokens或服务级最大序列; - 分离 prefill 与 decode,确认瓶颈是算力、带宽还是容量;
- 降低最大并发/上下文/输出,观察容量曲线;
- 评估 KV dtype、并行策略和推理框架配置;
- 用真实流量长度分布压测,不用全是短 prompt 的 demo;
- 给系统和异常峰值留余量,不把显存配置到理论 100%。
二十、上线检查清单
- 是否用目标 tokenizer 对最终聊天模板计数?
- token budget 是否预留输出、工具 schema 和安全余量?
- 是否按真实架构和活跃 token 总数估算 KV?
- 容量测试是否覆盖真实输入/输出长度与并发分布?
- 是否分别监控 queue、TTFT、prefill、ITL、decode 和 E2E?
- 采样策略、Prompt、模型和量化是否有版本?
- RAG 是否服务端过滤权限并校验 citation?
- 工具参数是否 schema 校验、再次授权并幂等执行?
- Agent 是否有轮数、时间、token、费用和工具边界?
- 结构化输出是否在本地再次验证?
- 固定评估集是否同时覆盖质量、安全、延迟和成本?
- 是否有降级、限流、取消、回滚和敏感日志策略?
二十一、总结
LLM 应用中的很多概念,最终都汇聚到三份契约:
- 容量契约:token、输出、并发、权重、KV 和延迟;
- 行为契约:Prompt、采样、RAG、工具、结构化输出;
- 安全契约:身份、权限、幂等、预算、审计与评估。
上下文窗口只约束单条序列,KV Cache 才把长度与并发一起变成显存压力;低 temperature 只改变采样,不创造事实;RAG 提供证据,不自动保证引用和权限;Tool/MCP 提供连接,不自动授权;Agent 提供动态决策,也带来循环、费用和副作用风险。
把这些边界变成可计算预算、可验证 schema、可观测指标和固定评估集,LLM 才从一个“看起来会回答”的模型,变成一个容量可规划、行为可测试、失败可控制的系统组件。