2.3 连续批处理与调度器:吞吐量的真正天花板 理解了链路(2.1)和显存分页(2.2),最后一个核心模块是调度器。它回答一个根本问题:每一轮 GPU 迭代,到底让哪些请求一起算? 这个选择,决定了你的吞吐量上限,也决定了你的尾延迟。很多团队把全部精力花在模型和硬件上,却从没认真调过调度器,结果 GPU 明明很闲,QPS 却卡在一个离谱的低值——问题往往就出在这节讲的两块预算上。 2.3.1 静态批处理的死穴:队头阻塞 传统推理服务用"静态批处理":攒够 N 个请求一起送进 GPU,等这一批全部生成完才释放。问题在于——批次里只要有一个请求很长或很啰嗦,其余请求必须陪它一起等。这叫队头阻塞(head-of-line blocking)。
理解了链路(2.1)和显存分页(2.2),最后一个核心模块是调度器。它回答一个根本问题:每一轮 GPU 迭代,到底让哪些请求一起算? 这个选择,决定了你的吞吐量上限,也决定了你的尾延迟。很多团队把全部精力花在模型和硬件上,却从没认真调过调度器,结果 GPU 明明很闲,QPS 却卡在一个离谱的低值——问题往往就出在这节讲的两块预算上。
传统推理服务用"静态批处理":攒够 N 个请求一起送进 GPU,等这一批全部生成完才释放。问题在于——批次里只要有一个请求很长或很啰嗦,其余请求必须陪它一起等。这叫队头阻塞(head-of-line blocking)。
举例:批次 8 个请求,7 个 100 token 就结束,1 个要生成 2000 token。那 7 个早该结束的请求,得等第 8 个跑完才能一起返回。GPU 在生成第 8 个时其实还能塞别的活,却被"整批同步"的约定卡死。结果是:GPU 没闲着,但有效吞吐(完成请求数/秒)很低,用户侧感受到的延迟飙升。更糟的是,这种阻塞会级联——被拖慢的短请求又去占别的资源,形成雪崩。
vLLM 的连续批处理(Continuous Batching,又称 in-flight batching)改写了规则:每个解码迭代(每个 token 步)都重新决定批次。哪个序列生成完了,立刻移出;新到的请求或刚 prefill 完的,立刻补进当前批次。GPU 的算力被"见缝插针"地填满。
这带来三个直接收益:
连续批处理不是"无限塞",调度器受两个预算约束,而这正是你部署时最该关心的旋钮——绝大多数吞吐问题,改这两个值就能解决:
max_num_seqs:批次里同时运行的最大序列数(并发上限)。设太小,GPU 槽位用不满,等待队列积压;设太大,单步 kernel 启动开销和显存压力上升,且可能超过 KV 池。max_num_batched_tokens:一个批次里所有序列的 token 数之和上限(含 prefill 的 prompt token + decode 的 1 token/序列)。它直接限制"一步能吃进多长的 prompt 总量",是 TTFT 与吞吐的平衡点。调度逻辑简化表述为:每轮从等待队列里挑请求加入运行批次,直到任一预算触顶;已经运行的序列继续占用槽位直到生成结束。所以当你发现 num_requests_waiting 长期大于 0、GPU 利用率却不满,多半是这两个值太保守——放宽它们通常立竿见影。我的一般经验:先用中等值压测,看等待队列,只要队列不空就上调,直到 GPU 利用率饱和或触发抢占为止。
高并发下偶尔会撞上 KV 池耗尽:新请求需要块,但空闲池空了。vLLM 不会直接报错崩服务,而是用**抢占(preemption)**做优雅降级:
调度器按某种策略(如最少剩余 token 优先)选牺牲者。这保证了服务在过载时"优雅降级"而非雪崩——但你应当把它当安全阀,而不是常态:如果频繁触发抢占,说明你的并发/显存配置已经越过健康线,应回退调小 max_num_seqs 或上调 gpu_memory_utilization(在 OOM 风险可控前提下),或加卡。一次生产事故里,我们的服务因为把批预算调得过大,每秒都在 swapping,延迟反而比保守配置更差——这就是"过了甜点"的典型症状。
DeepSeek V4 是 MoE,连续批处理在它身上还有一层讲究,部署时容易踩坑:
max_num_seqs 往往对 MoE 吞吐有利——但要守住 KV 池与 kernel 启动开销的底线,不是越大越好。给一条可复用的调参路径(具体数值结合你的硬件与模型实测),这是我压过多轮后的固定打法:
max_model_len 与 gpu_memory_utilization(建议 0.85 起),保证显存安全,再来谈吞吐。num_requests_waiting。若长期 >0 且 GPU 未满,逐步上调 max_num_seqs(如 64→128→256)与 max_num_batched_tokens。max_num_batched_tokens 饿死短请求。讲调参不能只给方向,给两组我们压测时记下的真实对照,让你感知量级(硬件规模已抽象,关注相对变化)。
实验一:放宽 max_num_seqs。 固定其余参数,从 64 调到 256,等待队列从常驻 200+ 降到接近 0,GPU 利用率从 55% 升到 92%,QPS 提升约 1.7 倍,TTFT 的调度等待段从 250ms 降到 40ms。说明原瓶颈就是批预算太保守。
实验二:调过头触发抢占。 继续从 256 调到 512,max_num_batched_tokens 没同步跟上且 KV 池见底,开始频繁 swapping,P99 延迟反而比 256 时高 30%。回退到 256 才最优。说明预算有甜点,过了反而亏。
这组对照印证了 2.3.6 的调参路径:找到"吞吐最大且不抢占"的点就停,别贪。
把以下指标接进看板,调度问题一眼可见:
vllm:num_requests_waiting:等待队列长度,长期 >0 且 GPU 未满 → 预算太小。vllm:num_requests_running:运行批次大小,接近 max_num_seqs 说明批次被填满。vllm:gpu_cache_usage:KV 池占用率,接近 1 说明快触顶,要小心抢占。vllm:time_to_first_token 分位数:看延迟分布,区分 prefill 与排队。2.3.3 讲了 max_num_seqs 调大能填 GPU,但它不是孤立的——它受 KV 池大小约束(2.2)。如果你把 max_num_seqs 设到 512,但 KV 池只够 256 个序列的块,多出来的请求要么排队要么触发抢占,根本进不了批次。所以调度的甜点,是"批次填满"与"KV 池不触顶"的交点。排障时要把 2.2 的 gpu_cache_usage 和这里的等待队列一起看:等待队列空但缓存满,说明不是预算小而是显存到顶,该去 2.2 找办法而不是继续加 max_num_seqs。
连续批处理不是银弹,它有边界:每步重新组装批次有 kernel 启动与调度的固定开销,批次过大时这个开销会被放大;另外频繁的新请求进出批次,会让注意力计算的 shape 每步都变,kernel 效率略低于静态大批次。因此在超小并发(比如就 1~2 个请求)场景,连续批处理的优势不明显,反而静态批更省。但在高并发——也就是本教程的主场——它几乎是唯一正确的选择。理解边界,你才知道什么时候它值、什么时候它亏。
很多新手认为卡越多越快,于是一上来就 tensor-parallel-size 拉满。但从调度器视角看,TP 越大,单次前向的卡间通信越多,每个迭代的固定通信开销越高,在中小批次下反而可能降低 TPOT;而且多卡会瓜分 KV 池(每卡各存一份分片),单卡 KV 容量不变但通信变重。所以 TP 的选择要匹配你的请求规模和延迟目标:长 prompt、大批次、追求吞吐时大 TP 合理;短请求、低延迟、中小并发时,小 TP 甚至纯数据并行可能更优。这条判断,正是调度器与 2.2 分页、2.1 链路共同约束的结果。
很多新手认为卡越多越快,于是一上来就 tensor-parallel-size 拉满。但从调度器视角看,TP 越大,单次前向的卡间通信越多,每个迭代的固定通信开销越高,在中小批次下反而可能降低 TPOT;而且多卡会瓜分 KV 池(每卡各存一份分片),单卡 KV 容量不变但通信变重。所以 TP 的选择要匹配你的请求规模和延迟目标:长 prompt、大批次、追求吞吐时大 TP 合理;短请求、低延迟、中小并发时,小 TP 甚至纯数据并行可能更优。这条判断,正是调度器与 2.2 分页、2.1 链路共同约束的结果。
给你一句好记的排障口诀,遇事先念一遍:"队列空、缓存满,是显存到顶;队列满、GPU 闲,是预算太小;队列空、GPU 满、还有抢占,是预算过了甜点。" 这三句话覆盖了 2.3 里 90% 的现场症状,对应动作分别是去 2.2 找显存办法、调大 max_num_seqs、回退预算。把口诀贴在监控屏旁,半夜告警时你不会慌。
读到这里,你应该能一句话总结:连续批处理让 GPU 永不停工,调度器的两块预算决定吞吐天花板,而抢占机制是过载时的安全气囊。 这三节合起来,就是 vLLM 高并发的"引擎原理"全貌——理解它们,第三章的进阶优化你才会用得对,而不是照抄别人的 config。如果你只带走一句话:记住"吞吐的天花板不是卡的算力,而是调度器肯不肯把批次填满"。
最后再送一句收束全章的话:第二章三节的主角——链路、分页、调度——看似三个独立模块,实则是同一条"让 GPU 每一纳秒都不浪费"主线上的三道工序。你调高一个模块的吞吐,往往会把压力推到下一个模块;真正的高并发调优,是沿着这条线反复打磨、找到全局最粗的那根管子。带着这个系统观进入第三章,你会发现自己已经能预判大半的优化方向了。