并发与批处理
并发和批处理决定大模型推理服务能承载多少请求,也决定用户感受到的延迟。
传统 Web 服务里,并发常常可以理解成“同时有多少 HTTP 请求”。但 LLM 服务不是简单的请求处理系统。它同时受输入 token、输出 token、prefill、decode、KV Cache、调度队列和 GPU 显存影响。
一句话概括:
LLM 并发能力 = 请求数量 + token 数量 + batch 调度 + KV Cache 余量
如果只看 QPS 或 HTTP 并发,很容易误判容量。两个请求可能都是 1 个并发,但一个只有 100 个输入 token,另一个可能有 32000 个输入 token,它们对 GPU 和 KV Cache 的压力完全不同。
1. 先区分几种并发
LLM 服务里至少要区分三种并发。
| 类型 | 含义 | 主要影响 |
|---|---|---|
| 请求并发 | 同时进入服务的请求数量 | 队列长度、连接数、用户等待 |
| 序列并发 | 推理引擎中同时运行的 sequence 数量 | batch 大小、KV Cache 分配 |
| token 并发 | 同一时刻正在处理的 token 数量 | GPU 计算、显存带宽、调度效率 |
请求并发高,不一定代表 GPU 已经吃满。也可能大量请求在排队,还没有进入推理引擎。
序列并发高,也不一定代表吞吐高。如果这些 sequence 很短,或者 decode 阶段每步只有少量 token,GPU 可能仍然利用不足。
token 并发更接近 LLM 的真实工作量。压测和容量规划时,应该同时记录:
- 每秒请求数。
- 输入 token 数。
- 输出 token 数。
- 当前运行中的 sequence 数。
- waiting queue 长度。
- KV Cache 使用率。
2. Prefill 和 Decode 的调度差异
LLM 推理通常分成两个阶段。
| 阶段 | 做什么 | 主要瓶颈 | 影响指标 |
|---|---|---|---|
| Prefill | 一次性处理输入 prompt,建立 KV Cache | 算力、attention 计算、长输入 | TTFT |
| Decode | 每一步生成新 token,持续复用 KV Cache | 显存带宽、KV Cache 读写、调度 | ITL、输出 tokens/s |
2.1 Prefill
Prefill 是“读题”阶段。输入越长,模型越晚开始输出第一个 token。
长 prompt、RAG 拼接内容、长历史对话和大工具定义都会增加 prefill 成本。对于交互式产品,prefill 太重会直接拉高 TTFT。
同一个服务里,短问答和长文档总结混跑时,长 prefill 请求可能拖慢短请求。生产中常见做法是:
- 限制最大输入长度。
- 把超长上下文请求放入单独队列。
- 使用 prefix cache 复用固定前缀。
- 对长 prompt 设置更保守的并发上限。
2.2 Decode
Decode 是“逐字生成”阶段。每个活跃请求通常每轮生成一个 token,调度器把多个请求合成一个 batch,让 GPU 同时处理。
Decode 阶段常常更受显存带宽影响,因为每一步都要读取模型权重和 KV Cache。并发太低时,GPU 吃不满;并发太高时,KV Cache 和排队延迟会变差。
所以 LLM 服务优化不是简单地“把并发调高”。更准确的目标是:
在 TTFT、ITL、p99 和显存安全边界内,提高 token throughput
3. Static Batching
Static batching 是传统批处理思路:收集一批请求,组成固定 batch,一起送进模型。
它适合离线任务,例如:
- 批量摘要。
- 批量打标签。
- 批量生成评测答案。
- 同长度或近似长度的数据集推理。
优点是实现简单,吞吐稳定。缺点是对在线服务不够友好:
- 短请求可能要等长请求。
- batch 内请求结束时间不同,容易浪费算力。
- 流式输出不自然。
- 很难动态插入新请求。
对于离线批处理,可以接受更高的 E2E latency 来换吞吐;对于聊天类在线服务,用户更关心 TTFT 和流式输出是否顺滑,static batching 通常不是最佳选择。
4. Continuous Batching
Continuous batching 是现代 LLM 推理框架常用的在线调度方式。它不是等一整批请求全部结束再处理下一批,而是在每个 decode step 动态维护 batch。
可以粗略理解为:
每一轮 decode:
1. 选择一批正在运行的请求
2. 为每个请求生成下一个 token
3. 移除已经结束的请求
4. 从等待队列补充新请求
5. 继续下一轮
它的收益是:
- 新请求可以更快进入运行队列。
- 短请求结束后,slot 可以及时释放。
- GPU 更容易保持较高利用率。
- 更适合流式输出。
- 能和 PagedAttention、KV Cache block 管理配合。
但 continuous batching 也会引入调度取舍:
- 补充新请求太激进,可能拉高已有请求的 ITL。
- prefill 插入过多,可能让 decode 流式输出卡顿。
- 长输出请求占住 slot 太久,可能影响短请求。
- KV Cache 接近上限时,调度器可能需要等待、抢占或拒绝请求。
5. Batch Size 不是越大越好
batch 变大通常能提高 GPU 利用率,但不一定改善用户体验。
| batch 变大带来的收益 | batch 变大带来的代价 |
|---|---|
| 更容易摊薄 kernel 启动和调度开销 | 排队时间可能增加 |
| GPU 利用率更高 | TTFT 可能上升 |
| token throughput 更高 | ITL 可能变差或抖动 |
| 单位 token 成本可能下降 | KV Cache 和显存压力增加 |
在线服务最常见的矛盾是:吞吐和延迟不能同时无限优化。
如果目标是离线生成,可以把 batch 做大,追求总 tokens/s。
如果目标是实时聊天,要给 TTFT、ITL 和 p99 留预算,不能只追求峰值吞吐。
6. vLLM 中的两个关键参数
不同推理框架参数名不同,但思路类似。以 vLLM 为例,常见需要理解两个参数。
6.1 max_num_seqs
max_num_seqs 控制单次调度中允许同时处理的最大序列数。
它主要限制 sequence 并发。
调大可能带来:
- 更多请求同时运行。
- decode batch 更大。
- 吞吐提升。
但也可能带来:
- KV Cache 占用上升。
- TTFT 和 p99 变差。
- 长输出请求挤占运行 slot。
- 显存不足时更容易 OOM 或触发抢占。
如果服务经常排队,但 GPU 利用率和显存还有余量,可以尝试调大。
如果 KV Cache 使用率高、p99 变差或频繁 OOM,应调小或限制请求长度。
6.2 max_num_batched_tokens
max_num_batched_tokens 控制单次调度最多处理多少 token。
它限制的是 token 维度的 batch 上限,通常会影响 prefill 和 decode 的混合调度。
调大可能带来:
- 长 prompt prefill 更容易合批。
- 总 token throughput 提升。
- GPU 利用率提高。
但也可能带来:
- 单轮调度耗时变长。
- 短请求等待长 prefill。
- TTFT 上升。
- 显存和 workspace 压力增加。
调这个参数时,不能只看 QPS。更应该观察:
- prefill tokens/s。
- decode tokens/s。
- TTFT。
- ITL。
- p95 / p99 latency。
- KV Cache usage。
- waiting requests。
7. 队列和排队延迟
高并发下,延迟不一定来自模型计算本身,也可能来自排队。
一个请求的端到端耗时可以拆成:
E2E latency
= 网关 / 鉴权 / 路由
+ prompt 构造 / tokenizer
+ queue wait
+ prefill
+ decode
+ 网络传输 / 前端渲染
如果 TTFT 突然升高,要先判断是 prefill 慢,还是队列等待变长。
常见排队原因包括:
- 入口请求速率超过服务处理速率。
max_num_seqs或 KV Cache 已到上限。- 长 prompt 请求占用大量 token budget。
- 长输出请求占住运行 slot。
- tokenizer、RAG、工具调用等前处理慢。
- GPU worker 数量不足或路由不均衡。
路由层不要只按请求数分配流量。更好的路由信号包括:
- 每个 worker 的 waiting queue。
- running sequence 数。
- KV Cache 使用率。
- 当前 batch token 数。
- 最近 TTFT / ITL。
- GPU 显存余量。
8. 吞吐、TTFT、ITL 和 p99 的取舍
不同业务目标下,优化方向不一样。
| 目标 | 优先关注 | 常见策略 |
|---|---|---|
| 在线聊天 | TTFT、ITL、p99 | 控制 batch 上限、限制长上下文、流式输出、prefix cache |
| 高吞吐 API | tokens/s、成功率、GPU 利用率 | 提高 batch、增加副本、优化调度 |
| 离线批处理 | 总完成时间、成本 | 大 batch、非流式、按长度分桶 |
| 长文档总结 | TTFT、KV Cache、OOM | 限制并发、单独队列、chunked prefill、上下文裁剪 |
| 多租户服务 | p99、限流、公平性 | 按用户限流、配额、队列隔离、动态降级 |
一个常见误区是把 benchmark 的峰值 tokens/s 当成线上体验。峰值吞吐通常是在特定输入长度、输出长度和并发下测出来的,不一定能代表真实流量。
线上更应该看稳定区间:
在错误率可控、p99 可接受、显存不紧张的条件下,服务能长期维持的吞吐
9. 长短请求混跑问题
LLM 请求长度差异很大。长短请求混跑时,容易出现“少量重请求拖慢整体服务”。
典型场景:
- 用户上传长文档总结。
- RAG 召回过多 chunk。
- 工具定义很长。
- 某些用户要求生成超长答案。
- 多轮对话历史没有裁剪。
常见治理方式:
- 按输入长度分队列。
- 按最大输出长度分队列。
- 对长上下文请求降低并发。
- 对超长请求使用异步任务模式。
- 为实时聊天和批处理任务拆分不同实例。
- 对单用户设置 token budget 和并发限制。
如果业务同时有在线聊天和离线批处理,不建议完全混用同一组 worker。离线任务更适合使用独立实例,在低峰期运行,或者通过队列限速。
10. 限流和降级
LLM 服务限流不能只按请求数。更合理的是按 token、显存压力和用户配额综合限制。
可用的限制维度包括:
| 限制项 | 作用 |
|---|---|
| 最大输入 token | 控制 prefill 和 KV Cache |
| 最大输出 token | 控制 decode 时长和 KV Cache 增长 |
| 单用户并发 | 避免单个用户占满队列 |
| 单用户 token/min | 比 QPS 更公平 |
| 全局 running sequence | 控制推理引擎并发 |
| 全局 waiting queue | 避免排队无限增长 |
| KV Cache 使用率阈值 | 接近 OOM 前提前拒绝或降级 |
高负载时可以按层级降级:
- 限制新请求最大输出长度。
- 降低长上下文请求优先级。
- 对低优先级用户排队或返回限流。
- 关闭非必要工具或减少 RAG chunk 数。
- 将批处理任务转移到异步队列。
- 扩容副本或切换到更短上下文配置。
降级策略要提前设计。等到 GPU OOM 或服务超时后再处理,用户体验通常已经明显受损。
11. 离线批处理的实践
离线批处理和在线服务的目标不同。离线任务通常更关注总吞吐、成本和失败重试。
实践上可以:
- 按输入长度分桶,减少 padding 和调度浪费。
- 使用更大的 batch 或 token budget。
- 限制单条样本最大输出,避免少数样本拖慢整体任务。
- 记录每条样本的输入 token、输出 token、耗时和失败原因。
- 对失败样本单独重试,不要整批重跑。
- 把离线任务和在线 worker 隔离。
离线批处理可以牺牲 TTFT,因为没有用户盯着第一个 token。但仍然要关注 OOM、超时和长尾样本,否则任务整体完成时间会被少数异常样本拖垮。
12. 监控指标
生产环境建议至少监控这些指标。
| 指标 | 说明 |
|---|---|
| request rate | 入口请求速率 |
| successful / failed requests | 成功率和错误率 |
| TTFT | 首 token 延迟 |
| ITL | token 间延迟 |
| E2E latency | 完整请求耗时 |
| p50 / p90 / p99 | 延迟分布 |
| input tokens / request | 输入长度分布 |
| output tokens / request | 输出长度分布 |
| running requests | 正在运行的请求 |
| waiting requests | 等待调度的请求 |
| prefill tokens/s | prefill 吞吐 |
| decode tokens/s | decode 吞吐 |
| KV Cache usage | KV Cache 使用率 |
| GPU memory used / reserved | 显存占用和预留 |
| preemption / eviction | 抢占或缓存淘汰次数 |
| timeout / rate limit | 超时和限流数量 |
排障时不要只看 GPU 利用率。GPU 利用率高可能是正常高吞吐,也可能是长 prompt 压力;GPU 利用率低也不代表系统没问题,可能是 tokenizer、队列、网络、存储或调度策略拖住了服务。
13. 常见问题
13.1 并发一上来,TTFT 明显变差
可能原因:
- 请求在 waiting queue 中排队。
- 长 prompt prefill 占用 token budget。
- batch 上限过大,单轮调度时间变长。
- tokenizer 或 RAG 前处理成为瓶颈。
处理方式:
- 观察 queue wait 和 prefill 耗时。
- 限制最大输入长度。
- 将长 prompt 请求拆到单独队列。
- 调整
max_num_batched_tokens。 - 启用 prefix cache 或减少固定前缀长度。
13.2 GPU 利用率高,但用户感觉慢
可能原因:
- batch 过大,吞吐高但 p99 差。
- decode ITL 变高,流式输出卡顿。
- KV Cache 接近上限,调度器频繁等待或抢占。
- 长输出请求占住运行 slot。
处理方式:
- 同时看 TTFT、ITL 和 p99。
- 限制最大输出 token。
- 降低运行 sequence 上限。
- 对长输出任务分队列。
- 增加副本而不是继续堆单实例并发。
13.3 模型能启动,但高并发 OOM
可能原因:
- 只估算了权重显存,没有估算 KV Cache。
max_model_len太大。max_num_seqs太高。- 输入和输出 token 上限过宽。
- 显存预留不足。
处理方式:
- 降低
max_model_len。 - 降低
max_num_seqs。 - 限制输入和输出长度。
- 启用 KV Cache 量化。
- 使用更多副本或更大显存 GPU。
13.4 QPS 不高,但服务还是很慢
可能原因:
- 每个请求 token 很长。
- 输出 token 很多。
- RAG 或工具调用耗时高。
- 请求速率有突发峰值,平均 QPS 掩盖了排队。
- 路由把重请求集中到少数 worker。
处理方式:
- 按 token 而不是请求数做分析。
- 记录输入和输出长度分布。
- 观察 p95 / p99,而不是只看平均值。
- 增加基于 KV Cache 和队列长度的路由策略。
14. 压测建议
压测并发和 batch 时,建议分几组进行。
| 压测类型 | 目的 |
|---|---|
| 固定并发 | 看某个并发下的稳定性和延迟 |
| 固定请求速率 | 更接近线上流量进入方式 |
| ramp up | 找到服务开始恶化的临界点 |
| 长短请求混合 | 验证调度公平性和 p99 |
| 长上下文压测 | 验证 KV Cache 和 TTFT |
| 长输出压测 | 验证 decode、ITL 和 slot 占用 |
| 突发流量压测 | 验证队列、限流和恢复能力 |
每次压测至少记录:
- 模型名称、精度、量化方式。
- GPU 型号、数量、显存。
- 推理框架和启动参数。
- 输入 token 分布。
- 输出 token 分布。
- 并发或请求速率。
- TTFT、ITL、E2E latency。
- p50、p90、p99。
- tokens/s。
- 错误率、超时率、限流率。
- KV Cache 使用率和 OOM 情况。
只有这些条件一致,压测结果才有可比性。
15. 和其他概念的关系
- 和 KV Cache:并发越高、上下文越长,KV Cache 压力越大;batch 调度会直接影响缓存分配和释放。
- 和 上下文管理:上下文裁剪、历史摘要和 RAG 拼接会影响 prefill 成本和 KV Cache。
- 和 流式输出:流式输出主要发生在 decode 阶段,ITL 决定输出是否顺滑。
- 和 推理参数:
max_tokens、stop、采样参数会影响输出长度和请求耗时。 - 和 容量与成本规划:容量规划要按 token、并发、延迟目标和显存余量估算。
- 和 AIPerf:可以用压测工具验证固定并发、固定请求速率和长尾延迟。
16. 实践检查清单
上线前可以按这份清单检查:
- 是否记录输入 token 和输出 token 分布。
- 是否区分 fixed concurrency 和 fixed request rate 压测。
- 是否分别观察 TTFT、ITL、E2E latency 和 p99。
- 是否估算了 KV Cache,而不是只看权重显存。
- 是否设置最大输入长度和最大输出长度。
- 是否为长上下文请求设计单独队列或限流。
- 是否监控 running requests 和 waiting requests。
- 是否监控 KV Cache usage 和 GPU memory reserved。
- 是否有超时、限流和降级策略。
- 是否把在线服务和离线批处理隔离。
17. 总结
LLM 服务的并发能力不是一个单独数字。它取决于请求数、输入长度、输出长度、KV Cache、batch 调度、GPU 显存和延迟目标。
更实用的判断方式是:
先定义用户能接受的 TTFT、ITL 和 p99,
再在这个边界内调 batch、并发和 token budget,
最后用真实流量分布压测稳定吞吐。
吞吐高但 p99 很差,不是好的在线服务;延迟很好但 GPU 长期空闲,也可能成本过高。并发与批处理的核心,就是在用户体验、硬件利用率和稳定性之间找到可长期运行的平衡点。