“川味居”是高新区最火的一家川菜馆。每天中午,后厨灶台前挤满了服务员的催单声:“李师傅,3 号桌的水煮鱼!”“李师傅,7 桌的麻婆豆腐!”老办法是一单一炒:开火、倒油、下料、翻勺、出锅——灶台有一半时间在空烧。直到李师傅想明白一个道理:与其一单一单炒,不如把同时段下的单攒成一锅。一锅水煮鱼的油温,可以同时汆熟三份鱼片;同一把勺,可以同时翻四份宫保鸡丁。于是他在出菜口放了几个托盘,凑满四盘就一起下锅;若两分钟还没凑满,也先开火,免得客人饿急。这就是“批处理”:用一点点等待,换来灶台(显卡)的满负荷运转。批量推理(Batching),正是大模型服务端把“一单一炒”改成“整托下锅”的工程艺术。

岗位关联

对 AI 从业者来说,Batching 是决定大模型服务吞吐与成本的核心工程杠杆。掌握动态批处理、队列、显存与连续批处理,你就能用同样的显卡数量服务 3-5 倍的流量,这是推理系统工程师的核心竞争力。

目录
  1. 可视化演示
  2. 动态批处理
  3. 请求队列管理
  4. 显存占用控制
  5. 批大小自动调整
  6. 连续批处理
  7. 生产部署实践
  8. 性能评估调优
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示批量推理Batching工程的核心机制,通过基础/核心/应用/扩展四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

批量推理Batching工程:在推理服 在推理服务端处理并发请求时的批 请求队列管理与优先级调度策略 核心机制可视化 批量推理Batching工程整体架构与关键组件

动态批处理

李师傅的秘诀其实就一句话:别让锅闲着。以前一单一炒,锅在等菜、菜在等人,燃气白白烧。现在他在出菜口放了托盘,服务员把新单放进同一个托盘,凑满四盘就喊一声“下锅!”。如果两分钟还没凑满,也先开火——菜可以少,锅不能空。这个“凑满四盘”和“两分钟”就是批处理的两个旋钮:一个管上限,一个管等待。

大模型推理服务面临的困境一模一样。每个用户请求过来,GPU 都要重新加载一遍上百亿参数的模型权重,执行一次前向计算。如果来一个算一个,GPU 就像那口空烧的锅,利用率常常不到 20%。动态批处理(Dynamic Batching)的做法是:把到达时间接近的请求收集起来,凑成一个批次(Batch)一起送进模型。多条请求共享同一次权重加载和更稠密的矩阵运算,吞吐立刻翻几倍。

工程上这套机制依赖两个关键参数:max_batch_size 规定一锅最多几盘,防止单批过大导致显存爆掉、延迟失控;max_wait_ms 是窗口倒计时,超过这个时间还没凑满也必须下发,防止请求“饿死”在队列里。两个条件任一满足,调度器就把当前排队中的请求整批送入模型。模型内部,一批请求会经历两个阶段:先把输入一起算一遍(预填充 Prefill),再一个 token 一个 token 地并行生成回答(解码 Decode)。批处理之所以能提速,正是因为解码这一步的耗时不会随批大小线性上涨。

最后回到厨房:一锅四盘菜的秘诀就在于油温同一份、火候同一灶、翻勺同一遍。动态批处理做的正是这件事——用可以接受的等待,换取灶台永远满负荷。记住,批处理从来不是免费的午餐,而是一场“延迟换吞吐”的交易,交易是否划算,要看接下来的每一个工程细节。

tbatch(B)=tprefill(B)+Lmax×tdecode(B)\displaystyle t_{batch}(B) = t_{prefill}(B) + L_{max} \times t_{decode}(B)
公式说明:一批请求的总耗时 = 预填充阶段耗时 + 最长序列长度 × 每解码一个 token 的耗时。批大小 B 增大时,各项耗时都在增长,但增速远小于 B 倍。
  1. 预填充阶段开销t_prefill(B) 是批内所有输入 token 一次性前向计算的时间。它随 B 增长但远慢于线性——因为模型权重只需要加载一次,多条输入共享同一份权重矩阵运算。
    tprefill(B)≈tprefill(1)+c×B\displaystyle t_{prefill}(B) \approx t_{prefill}(1) + c \times B
  2. 最长序列长度L_max 是批内所有请求目标输出长度的最大值。解码过程逐 token 推进,整批必须等最慢的那条生成完,这正是批处理延迟的代价来源。
    Lmax=max⁡i∈[1,B]li\displaystyle L_{max} = \max_{i \in [1,B]} l_i
  3. 解码单步耗时t_decode(B) 是每生成一个 token 的耗时。由于 B 条序列的 token 在同一个 Transformer 前向中并行计算,显存带宽被摊薄,单 token 成本随 B 下降。
    tdecode(B)<tdecode(1)×B\displaystyle t_{decode}(B) < t_{decode}(1) \times B
  4. 对照串行基线若 B 条请求完全串行,总耗时约为每条请求各自的预填充加解码之和。批处理把这一堆线性累加压缩成“一次预填充 + 一条最长解码路径”,这就是收益来源。
    tserial=∑i=1B(tprefill(1)+li×tdecode(1))\displaystyle t_{serial} = \sum_{i=1}^{B} \big(t_{prefill}(1) + l_i \times t_{decode}(1)\big)

请求队列管理

川味居门口有个叫号台。中午十二点,客人像潮水一样涌进来,二十张单子同时压在出菜口;下午三点,又冷清得只有两桌。叫号小姐姐手里攥着一沓排号单:先来的先安排,VIP 客人可以往前挪,等超过十五分钟的客人会送一碟泡菜安抚。要是队伍排到了大门外,就得客气地请客人改天再来——这叫限流。

推理服务的请求到达同样忽高忽低,绝不会匀速。生产系统中,请求必须先进入一个请求队列(Request Queue),由调度器从队列中取请求来组批。队列是请求与 GPU 之间的缓冲层:没有它,突发流量会直接把服务打崩;有了它,GPU 可以在低峰期“多等等攒一锅”,高峰期“凑满就赶紧下锅”。队列设计有三个核心维度:结构(FIFO 主序还是按优先级排列)、超时(排队太久直接淘汰并返回兜底结果)、长度上限(队满时对新请求做背压/拒绝,保护服务本身)。

排队这件事有一个非常优雅的数学定律,叫利特尔法则(Little's Law)。它把系统里最重要的三个指标串在了一起:平均队长、到达率和平均等待时间。

叫号台的本质是一个“蓄水池”:它不产生新客人,但让客流变得可管理。请求队列在推理服务里扮演的正是这个角色——当流量洪峰到来时,队列帮你稳住阵脚,让动态批处理有“货”可凑、让 GPU 有活可干。但别忘了,蓄水池的水位每高一寸,等位客人的脸色就难看一分。

L=λ×W\displaystyle L = \lambda \times W
公式说明:一个达到稳态的系统里,平均排队人数等于平均请求到达率乘以平均逗留时间。三条变量知道任意两条,第三条就锁死了。
  1. 平均队长系统内正在排队和正在处理的请求总数。在批推理服务里,它就是出菜口托盘上攒着的“待下锅订单”数量。
    L=E[队列长度+处理中请求数]\displaystyle L = \mathbb{E}[\text{队列长度} + \text{处理中请求数}]
  2. 到达率单位时间进入队列的新请求数。饭点突增时 λ 飙升,这就是突发流量。
    λ=NarrivedΔt\displaystyle \lambda = \frac{N_{arrived}}{\Delta t}
  3. 逗留时间一个请求从入队到处理完成的总耗时,包含排队等待加上批处理时间,也就是客人的“从等位到上菜”。
    W=Wqueue+Wprocess\displaystyle W = W_{queue} + W_{process}
  4. 三者关系稳态下三者严格满足等式。它告诉你:想让 W 降下来,要么减小 L(限流或扩容),要么接受 λ 不变时两者必然联动。这对设定队列长度上限有直接指导意义。
    L=λ×W\displaystyle L = \lambda \times W

显存占用控制

川味居后厨有一台双开门保鲜冰柜,容量有限得精打细算。每道半成品菜(正在生成中的回答)都要占一个保鲜盒:鱼片腌好了放一层,宫保鸡丁半成品放一层。问题在于,这些盒子是随着做菜过程不断长大的——菜越做越多,盒子越占越大。冰柜总共那么大,还要留位置给备料(模型权重)和案板(激活值)。盒子用完不腾,新菜就没处放;盒子大小不一放多了,中间碎成一小块一小块的空隙,大盒子根本塞不进去——这就是碎片。

大模型推理时,每一条请求的 K/V Cache(注意力缓存的 Key 和 Value 张量)会随着 token 生成不断增长,是显存消耗的绝对大头。如果只按输入长度估算显存,上线必炸——因为生成过程才是显存杀手。工程上要做显存预算(Memory Budget):总显存被划分为模型权重、激活值、KV Cache 和预留余量四块,任何一块超限都会 OOM 崩掉整个服务。现代推理框架还会用 PagedAttention 这类技术,把 KV Cache 切成小块分页管理,像操作系统管理内存一样消除碎片、按需分配。

要估算 KV Cache 的占用量,可以用下面这个公式:

保鲜盒规划得好,灶台才能同时开很多锅;显存规划得好,batch 才敢往大开。李师傅每天都在冰柜门上贴一张预算表,哪格放什么、剩多少余量,一目了然。推理服务的显存水位,也需要这样一张随时可查的“冰柜地图”——这是批处理能安全跑起来的地基。

Mkv=2×B×Lˉ×D×H×2 bytes\displaystyle M_{kv} = 2 \times B \times \bar{L} \times D \times H \times 2 \ \text{bytes}
公式说明:KV Cache 总量 = K、V 两份 × 批大小 × 平均序列长度 × 层数 × 隐藏维度 × 每个元素字节数。
  1. 批大小当前批内请求数。队列越长、锅越大,保鲜盒占位就越多,这是唯一可以直接调控的变量。
    B=batch size\displaystyle B = \text{batch size}
  2. 平均序列长度解码过程中每条序列的长度持续增长,因此 L̄ 必须按平均生成长度或上限 max_model_len 来估算,而不是只看输入 token 数。
    Lˉ=1B∑i=1BLi\displaystyle \bar{L} = \frac{1}{B}\sum_{i=1}^{B} L_i
  3. 层数与隐藏维度D 是 Transformer 层数,H 是每层隐藏维度。模型越大,每 token 每层要缓存的元素越多,KV Cache 随模型规模指数级膨胀。
    Munit=D×H\displaystyle M_{unit} = D \times H
  4. K 与 V 双份注意力计算同时需要 Key 和 Value 两组缓存,所以乘第一个 2。
    Mkv=2×B×Lˉ×D×H\displaystyle M_{kv} = 2 \times B \times \bar{L} \times D \times H
  5. 精度字节数FP16 半精度下每个元素占 2 字节,再乘一次,最终得到以字节为单位的显存占用。
    Mkv=2×B×Lˉ×D×H×2 bytes\displaystyle M_{kv} = 2 \times B \times \bar{L} \times D \times H \times 2 \ \text{bytes}

批大小自动调整

李师傅练就了一项本事:看客流调锅数。饭点门外排长队,他一锅炒八盘;晚上快打烊,一锅炒两盘就出锅。怎么调?他靠试——上次加了量客人没投诉,说明锅还能更大;上次等太久被投诉,赶紧收一收。同时他还留了个心眼:一次只调一点点,绝不一上来就从两盘跳到八盘,免得锅气全乱。

固定 batch 大小没法应对波动的流量。自适应批大小(Adaptive Batch Size)算法实时观察三个信号:队列水位(排队人数)、延迟与 SLA 的距离(客人有没有等急)、显存水位(冰柜还剩多少)。当排队水位高于目标值时,说明“货多锅小”,应该加大批次;当延迟超标或显存吃紧时,就减小批次。为了防止抖动,调整幅度要小、方向要准,并且随时准备回头。

这个“试探-调整”的过程可以用一个简洁的增量公式描述:

自适应批大小等于把李师傅的“手感”变成了仪表盘上的量化规则。它不需要人盯着监控面板手动改参数,而是让系统自己像经验丰富的厨师长一样,在客流变化时提前调整锅的容量。这也是从“能跑”到“跑得好”的关键一步。

Bt+1=clip(Bt+η⋅sign(Qt−Q∗), Bmin, Bmax)\displaystyle B_{t+1} = \text{clip}\big(B_t + \eta \cdot \text{sign}(Q_t - Q^*), \ B_{min}, \ B_{max}\big)
公式说明:下一轮的批大小 = 当前批大小 + 步长 × 水位偏差方向,并夹在上下限之间。方向对了就继续走,错了就回头。
  1. 当前批大小本轮的“锅容量”,是我们唯一要动态调节的控制变量。每次调整都基于上一轮的结果。
    Bt=本批次请求数\displaystyle B_t = \text{本批次请求数}
  2. 步长单次调整的幅度。η 太小反应慢,流量洪峰来了跟不上;η 太大系统震荡,吞吐忽高忽低像过山车。
    η=单次调整量\displaystyle \eta = \text{单次调整量}
  3. 水位偏差方向Q_t - Q* 大于 0 说明队列越排越长,该加大批次;小于 0 说明系统空闲,该减小批次降低延迟。sign 只取正负号,避免单次偏差过大导致过度反应。
    Δ=sign(Qt−Q∗)\displaystyle \Delta = \text{sign}(Q_t - Q^*)
  4. 更新与保护乘上步长后还要用 clip 裁剪到安全区间,保证显存不超预算、吞吐不掉到地板以下。
    Bt+1=clip(Bt+η⋅Δ, Bmin, Bmax)\displaystyle B_{t+1} = \text{clip}(B_t + \eta \cdot \Delta, \ B_{min}, \ B_{max})

连续批处理

老办法有个尴尬场景:一锅六盘菜,五盘三分钟就出锅了,最后一盘佛跳墙要炖半小时。其余五盘在出菜口晾着,锅也占着,谁也不能动。李师傅被这个场景折磨了很久,直到他换了思路——哪盘好了立刻端走,腾出的锅位马上放新菜,不等那盘佛跳墙。锅永远满载,翻台率立刻上了一个台阶。这就是“连续批处理”:别再等最慢的那盘。

在传统静态批处理(Static Batching)中,一个 batch 内所有序列同步解码,直到最长的那条序列生成完毕,整批才一起释放。序列长短差异越大,短序列的空转浪费就越严重,这种浪费叫“气泡”(Bubble)。连续批处理(Continuous Batching,也叫迭代级调度 Iteration-level Scheduling)改变了规则:在每个解码迭代结束时检查所有序列,完成的序列立刻离开批次、释放显存槽位,新请求立刻补进来。GPU 的每一个迭代都近乎满载。

批次利用率可以用下面的公式量化:

连续批处理犹如流水线出菜,不设“齐了才开席”的门槛,灶台每一秒都在替餐厅赚钱。如今主流的推理框架(vLLM、TensorRT-LLM 等)都默认启用这种调度方式,它把批处理的收益又抬高了一截——但这并不意味着前面的动态批处理和队列管理可以省略,它们负责“组队”,连续批处理负责“在队内高效运转”,是前后台的关系。

η=∑i=1KliK×lmax\displaystyle \eta = \frac{\sum_{i=1}^{K} l_i}{K \times l_{max}}
公式说明:批次利用率 = 各序列实际解码长度之和 ÷(批内请求数 × 最长序列长度)。它衡量的是“占坑时间”中有多少被真正用于干活。
  1. 批次大小同一时刻并行处理的序列数,也就是同时占着锅的菜盘数。
    K=批次内请求数\displaystyle K = \text{批次内请求数}
  2. 实际序列长度每条请求真正生成的 token 数,长短不一——有的客人只点一道快手菜,有的点佛跳墙。
    li=第 i 条序列的 token 数\displaystyle l_i = \text{第 } i \text{ 条序列的 token 数}
  3. 最长序列静态批处理必须等它完成才释放整个批次,所有短序列都在为它陪跑。
    lmax=max⁡ili\displaystyle l_{max} = \max_{i} l_i
  4. 利用率分子是有效计算量,分母是占坑总容量。η 越接近 1 越好;若长短悬殊,η 会显著小于 1,说明静态批处理的浪费有多大。
    η=∑i=1KliK×lmax\displaystyle \eta = \frac{\sum_{i=1}^{K} l_i}{K \times l_{max}}

生产部署实践

新餐厅开业前,李师傅做了三件事:提前一天把高汤熬好、配菜备齐(模型加载与预热);把停水停电的应急预案写在厨房墙上(故障恢复);开业头三天只接待预定客人,慢慢放量(限流压测)。没有这些准备,开业当天就是灾难片现场。生产环境部署批推理服务,道理完全相同。

选对框架是第一步。vLLM、TensorRT-LLM、Text Generation Inference(TGI)都内置了动态批处理和连续批处理能力,没必要从零造轮子。第二步是配参:max_num_seqs 控制最大并发序列数,gpu_memory_utilization 控制显存占用比例(通常留出 10%-20% 余量),max_model_len 直接决定 KV Cache 预算上限。第三步是故障恢复设计:请求排队超时要能优雅降级(返回兜底回答或建议稍后重试),后端实例 OOM 时要有熔断机制,不能把整个网关拖垮。

容量规划需要回答一个问题:单实例到底能扛多少 QPS?这要通过压测找到“最优批大小”来回答:

开业前的试营业,就是为了找到“一桌最多坐几个人、后厨一次能接几锅”的答案。生产部署没有玄学:选好框架、配好参数、做好预案、压测定容,四步走完,服务才能在大流量面前站得住脚。李师傅常说,后厨的从容,都是开业前流过的汗换来的。

QPSmax=Bopttbatch(Bopt)s.t. P99≤SLA\displaystyle QPS_{max} = \frac{B_{opt}}{t_{batch}(B_{opt})} \quad \text{s.t.} \ P_{99} \le SLA
公式说明:单实例最大吞吐 = 最优批大小 ÷ 该批大小下单批端到端耗时,且必须满足 P99 延迟不超过服务等级协议(SLA)。
  1. 最优批大小压测曲线上的吞吐拐点与延迟 SLA 边界,取两者中更靠前的一个作为 B_opt。再大,延迟就超线了。
    Bopt=min⁡(B拐点,BSLA)\displaystyle B_{opt} = \min(B_{拐点}, B_{SLA})
  2. 单批耗时该批大小下端到端时间,包含排队等待、预填充阶段和解码阶段三部分。压测时测的是真实分布,不是平均值。
    tbatch(Bopt)=tqueue+tprefill+Lmax×tdecode\displaystyle t_{batch}(B_{opt}) = t_{queue} + t_{prefill} + L_{max} \times t_{decode}
  3. 单实例吞吐批大小除以单批耗时,得到理论最大 QPS。乘上实例数就是集群总容量,这是容量规划的核心依据。
    QPSmax=Bopttbatch(Bopt)\displaystyle QPS_{max} = \frac{B_{opt}}{t_{batch}(B_{opt})}
  4. SLA 约束所有配置都以 P99 不超过阈值为硬前提。P99 超标,哪怕吞吐再高也要调小 B_opt——客人的耐心是有限的。
    P99≤SLA 阈值\displaystyle P_{99} \le \text{SLA 阈值}

性能评估调优

周末打烊后,李师傅对着账本复盘:翻台率(吞吐量)、等位时长(延迟)、食材成本(成本效率)。他发现周六翻台率高但等位也长,说明“锅数”不够用;周中翻台率低,说明凑单策略太贪——单子少还死等满锅,客人全跑了。于是他把窗口参数调小,下周报表立刻改善。数据不会骗人,但前提是你得把数据记下来。

评估批推理系统,核心指标有四类:吞吐量(每秒完成请求数 req/s 或每秒产出 token 数)、延迟(首 token 时延 TTFT、单 token 时延 TPOT、P99 尾延迟)、显存水位、成本效率。特别要注意,批处理是用延迟换吞吐的交易,所以“吞吐翻倍、P99 翻三倍”这种结果并不值得庆祝——必须用 SLA 作为约束来评价。监控面板上,吞吐的绿色曲线和 P99 的红色曲线往往呈镜像走势,最优配置就在两线交汇点附近。

成本效益的量化,是评判批处理策略值不值的最终标准:

只有把账算明白,才知道批处理省下的燃气够不够给客人送果盘。李师傅的账本记录着每一种调料的花销,你的监控面板也应该记录着每一个 token 的成本。性能调优不是凭感觉拧参数,而是看报表、找瓶颈、做假设、再验证的循环——数据是厨师长最可靠的舌头。

Ctoken=CGPU×TrunNtokens\displaystyle C_{token} = \frac{C_{GPU} \times T_{run}}{N_{tokens}}
公式说明:平均每个 token 的推理成本 = 硬件单位时间成本 × 运行时长 ÷ 周期内产出的总 token 数。这是推理服务的“食材成本账”。
  1. 硬件成本GPU 单位时间成本,包含采购摊销、电力和机房费用。这是分母的“单价”。
    CGPU=每 GPU 每小时成本\displaystyle C_{GPU} = \text{每 GPU 每小时成本}
  2. 运行时长统计周期内服务持续运行的时间。跑得越久,固定成本摊得越薄。
    Trun=统计周期时长\displaystyle T_{run} = \text{统计周期时长}
  3. 产出 token 数周期内所有请求生成的 token 总量,包括预填充阶段的输入 token 和解码阶段的输出 token。
    Ntokens=输出 tokens+输入 tokens\displaystyle N_{tokens} = \text{输出 tokens} + \text{输入 tokens}
  4. 单位成本三者相除得到每个 token 的平均成本。对比不同批处理策略时,谁的 C_token 低,谁就更划算——这既是内部调优的标尺,也是对外定价的依据。
    Ctoken=CGPU×TrunNtokens\displaystyle C_{token} = \frac{C_{GPU} \times T_{run}}{N_{tokens}}

AI 应用场景

企业内部统一 LLM API 网关

阶段:推理服务部署与流量治理

解决的问题:多个业务线(客服、写代码助手、文档摘要)各自独立部署大模型服务,GPU 利用率往往不到 20%,且流量高峰互相错开、无法互补。通过统一网关汇聚所有请求,用动态批处理把不同业务的请求混批,用请求队列做优先级调度(客服请求优先于批量任务),整体吞吐可提升 3-5 倍。

具体案例:某互联网公司的推理平台将 6 个业务线的 12 个独立服务收敛为 2 个共享推理集群,统一入口加动态批处理,GPU 数量从 40 块降到 16 块,P99 延迟反而因为队列治理更稳定。

离线批量内容生成

阶段:数据生产与离线任务调度

解决的问题:广告文案改写、商品摘要生成、数据标注预标注等任务没有实时交互需求,传统做法是逐条调用模型,慢且贵。这类场景适合“超大 batch + 长窗口”策略:把窗口拉到秒级、批大小顶到显存预算上限,牺牲少量等待换取接近满载的吞吐。

具体案例:某电商团队需要在夜间批量生成 100 万条商品摘要。他们用离线批处理脚本配合连续批处理框架,设置 max_batch_size=64,窗口 2 秒,整夜跑完,每千 token 成本比在线调用降低约 70%。

多租户共享 GPU 的显存隔离

阶段:资源管理与多租户调度

解决的问题:不同团队共享同一个 GPU 集群时,一个租户的长序列请求(比如超长文档分析)可能瞬间吃光全部 KV Cache 显存,导致邻居租户请求 OOM。通过显存预算分配、自适应批大小和队列水位监控,把每个租户的显存上限锁死,同时动态调整批大小适配各自流量。

具体案例:某大模型平台用显存预算配置为每个租户划定 KV Cache 比例,配合 PagedAttention 的分页管理,将多租户场景下的 OOM 故障率从每周 10+ 次降到接近 0。

Python 演示

python67 行
"""动态批处理调度模拟器:用窗口和最大批大小两个旋钮控制批次划分"""from dataclasses import dataclassfrom typing import List  @dataclassclass Request:    req_id: int    arrive_ms: float    gen_len: int = 32  class DynamicBatcher:    """一锅一锅地接单:凑满 max_batch 或窗口到期就下锅"""     def __init__(self, max_batch: int = 4, window_ms: float = 50.0,                 token_ms: float = 2.0):        self.max_batch = max_batch        self.window_ms = window_ms        self.token_ms = token_ms        self.batch: List[Request] = []        self.stats: List[tuple] = []     def push(self, req: Request, now_ms: float) -> None:        self.batch.append(req)        self._maybe_flush(now_ms)     def _maybe_flush(self, now_ms: float) -> None:        if not self.batch:            return        first_arrive = self.batch[0].arrive_ms        window_hit = (now_ms - first_arrive) >= self.window_ms        if len(self.batch) < self.max_batch and not window_hit:            return        self.flush(now_ms)     def flush(self, now_ms: float) -> None:        batch = self.batch        self.batch = []        longest = max(r.gen_len for r in batch)        process_ms = longest * self.token_ms        finish_ms = now_ms + process_ms        for r in batch:            latency = finish_ms - r.arrive_ms            self.stats.append((len(batch), process_ms, latency))  def main():    batcher = DynamicBatcher(max_batch=4, window_ms=50)    times = [0, 8, 16, 24, 60, 66, 72, 78, 120, 130]    reqs = [Request(i, t) for i, t in enumerate(times)]    now = 0.0    for r in reqs:        now = r.arrive_ms        batcher.push(r, now)    batcher.flush(now + 1)     count = len(reqs)    total_lat = sum(s[2] for s in batcher.stats)    print(f"共 {count} 个请求,划分为 {len(batcher.stats)} 个批次")    for i, (size, process, _) in enumerate(batcher.stats, 1):        print(f"第 {i} 批:批大小 {size},批处理耗时 {process}ms")    print(f"平均端到端延迟:{total_lat / count:.1f} ms")  if __name__ == "__main__":    main()
目的:模拟一个动态批处理调度器,演示“满批触发 + 窗口超时触发”两种条件下,请求如何被划分成批次,以及不同批次配置如何影响平均延迟。
  1. self.batch: List[Request] = []当前正在“凑单”的托盘,新请求先放进来攒着
  2. self._maybe_flush(now_ms)每次有新请求到达都检查一次:该不该下锅?
  3. window_hit = (now_ms - first_arrive) >= self.window_ms窗口超时触发:第一个请求等太久,哪怕没凑满也得开火
  4. if len(self.batch) < self.max_batch and not window_hit: return两个触发条件(满批、超时)都没满足时继续攒单
  5. longest = max(r.gen_len for r in batch)一批菜里最费火的那道决定出锅时间,即批处理耗时由最长序列决定
  6. latency = finish_ms - r.arrive_ms端到端延迟包含排队等待和处理时间,是评估批处理策略的关键输出

常见误区

误区batch size 开到显存能塞下的最大值不就行了?反正 GPU 并行算得过来,批越大吞吐越高。

正解批大小与吞吐不是单调关系。批增大时,单批耗时(尤其解码阶段)随之增大,KV Cache 显存线性上升,P99 延迟快速恶化。吞吐曲线通常先升后降,存在一个最优拐点。正确做法是压测扫点,找到“吞吐拐点”与“延迟 SLA 边界”中更靠前的那一个。李师傅一锅炒二十盘的后果,就是前面十盘全凉了——锅的容量要配得上菜的性质。

为何常见:单看公式 T = B / t_batch 时,B 在分子,直觉上觉得 B 越大越好,忽略了 t_batch 也在变大,而且变大得越来越快。

误区动态批处理不就是等 50ms 再一起发吗?设置一个固定延时窗口就够了。

正解固定窗口只是触发策略之一,而且是效果最差的那一种。真正的动态批处理是“窗口超时、批次满员、强制冲刷”三类条件任意满足即下发;更强的实现还会结合队列水位、显存余量动态调节窗口时长和批大小上限。固定窗口在流量稀疏时白白增加延迟,在流量洪峰时又来不及扩容,必须配合满批触发才能兼顾吞吐与延迟。

为何常见:大多数教程和简化的示例代码都用“等一段时间再发”来演示动态批处理,让人误以为窗口是唯一机制。

误区模型 7B 参数大概 14GB,输入最长 2048 token,算下来没超显存啊,怎么一上线就 OOM?

正解漏掉了 KV Cache 随解码动态增长这个大头。每条请求每生成一个 token,KV Cache 就要追加 2 × D × H × 2 字节;batch 内所有请求累计起来非常可观,且预填充阶段的激活值也会瞬时暴涨。正确的预算公式应为:模型权重 + 激活值峰值 + 2 × B × L_max × D × H × 2 字节 + 预留余量。李师傅的冰柜里,备料和半成品菜是分格管理的,每一格都要算清楚。

为何常见:KV Cache 不像模型权重那样“看得见摸得着”,是运行时动态分配的,新手容易忽略它随生成的增长性。

误区vLLM 的 continuous batching 不就是动态批处理换了个名字吗?反正都是攒一批再算。

正解两者处于不同层级。动态批处理在“请求进入 batch 之前”做决策,解决的是攒够什么条件才组批;连续批处理在“batch 内部迭代中”做决策,解决的是每一步解码后哪些序列完成、能否释放槽位补新请求。序列长度参差时,连续批处理能把利用率 η 从 0.5 提升到接近 1。它们是互补的前后台关系,不是同义反复。

为何常见:两者都带"batch"字样,且框架文档常把调度层(动态批处理)和执行层(连续批处理)混着介绍,读者容易混淆层级。

误区压测报告显示吞吐从 500 req/s 涨到 2000 req/s,翻了四倍,直接上线!

正解批处理本质是“用延迟换吞吐”的交易。吞吐翻四倍时,P99 可能从 300ms 涨到 3s,首个 token 的等待时间也大幅变长,用户体感极差。生产上线必须同时盯三类指标:吞吐、延迟(TTFT/TPOT/P99)、显存水位,并以前置 SLA 为硬约束。只看吞吐的后果是上线后用户投诉、半夜回滚——就像只看翻台率、不看等位时长的餐厅,迟早被差评淹没。

为何常见:吞吐是最容易量化和汇报的指标,而延迟分布和 SLA 需要额外埋点与统计计算,容易被图省事的团队跳过。

面试题

Q1. 动态批处理中,以下哪一项不是触发批次下发的典型条件?

单选题 · easy

  • A.队列中请求数达到最大批大小
  • B.首个请求等待时间超过最大窗口
  • C.GPU 利用率下降到阈值以下← 正确答案
  • D.调度器主动强制冲刷当前批次

答案:C。动态批处理的触发条件围绕“满批”和“超时”展开:队列满员、窗口超时、强制冲刷都常见,而 GPU 利用率下降不会触发组批,恰恰相反,组批是为了提升利用率。这就像后厨不是因为锅凉了才开火,而是因为托盘满了或客人等久了。

Q2. 关于利特尔法则 L = λW,以下理解正确的是?

单选题 · mid

  • A.延迟越低则吞吐一定越高
  • B.到达率翻倍则等待时间减半
  • C.稳态平均队长等于到达率乘逗留时间← 正确答案
  • D.队长超过服务能力后请求开始丢失

答案:C。利特尔法则说的是稳态下 L、λ、W 三者满足乘法关系,C 是其标准表述。A 与 B 都颠倒了因果,延迟与吞吐的取舍关系取决于 SLA 约束而非简单反比;D 描述的是队列溢出后的背压现象,是超限后的结果而非法则本身。

Q3. 在 KV Cache 显存估算公式 M = 2 × B × L × D × H × 2 字节中,第一个"2"代表什么?

单选题 · mid

  • A.FP16 半精度占 2 字节
  • B.Key 和 Value 各一份← 正确答案
  • C.多头注意力有两组权重
  • D.缓存池预留双倍预算

答案:B。公式里的两个“2“含义不同:第一个 2 是 Key 和 Value 两份缓存,最后一个 2 才是 FP16 每个元素占 2 字节。B 选项准确描述第一个 2。这就像保鲜盒要分”荤素”两层放,同一道菜的两个半成品部位都要占空间,但每个位置的具体大小是另一回事。

Q4. 关于连续批处理和静态批处理,以下说法正确的有?

多选题 · extreme

  • A.连续批处理在迭代级调度序列← 正确答案
  • B.静态批处理等最长序列完成再整批释放← 正确答案
  • C.序列长度差异大时连续批处理利用率更高← 正确答案
  • D.连续批处理能彻底消除全部排队延迟

答案:ABC。连续批处理的核心就是迭代级调度(A 对),静态批处理必须等最长序列完成才释放整批(B 对),两者在序列长短差异大时利用率差距显著、连续批处理明显占优(C 对)。D 错在“彻底消除”过于绝对——连续批处理消除的是批次内的气泡,队列排队等待依然存在。

Q5. 推理服务中请求队列的首要作用是什么?

单选题 · easy

  • A.提高单条请求执行速度
  • B.承接突发流量解耦到达与处理← 正确答案
  • C.减少模型权重的显存占用
  • D.替代动态批处理的等待窗口

答案:B。请求队列的核心价值是缓冲:让不稳定的到达流量与稳定的批处理节奏解耦。B 准确。A 中队列反而可能增加等待;C 与显存无关;D 中队列和窗口是配合关系,队列为组批提供“货源”,窗口是触发条件,不能互相替代。

Q6. 自适应批大小调整常用的输入信号包括哪些?

多选题 · mid

  • A.当前队列水位与目标水位← 正确答案
  • B.延迟与 SLA 目标的距离← 正确答案
  • C.KV 缓存剩余显存量← 正确答案
  • D.模型训练时的参数量

答案:ABC。自适应批大小依据系统实时状态做决策:队列水位决定“货多不多”,延迟距 SLA 的距离决定“客人急不急”,剩余显存决定“锅还能不能更大”。D 是模型训练时的静态属性,与运行时调度无关。

Q7. 压测一个新模型服务时,正确寻找最优批大小的流程是?

单选题 · mid

  • A.直接设置为显存允许的最大批
  • B.逐步增大批大小找吞吐延迟交叉点← 正确答案
  • C.根据模型参数量推导固定公式
  • D.先设窗口为无限大再测延迟

答案:B。最优批大小必须在真实压测中寻找:逐步增大批大小,观察吞吐上升曲线与 P99 延迟曲线,在“吞吐拐点”和“SLA 边界”之间取交集。A 忽视了延迟约束,C 中参数量不能直接推导出最优批,D 设无限窗口会掩盖窗口超时触发带来的真实排队行为。

Q8. 生产环境显存预算中,不应包含以下哪一项?

单选题 · hard

  • A.模型权重参数本体
  • B.前向激活值中间结果
  • C.全部请求的历史输入文本← 正确答案
  • D.KV 缓存与显存预留余量

答案:C。显存预算四大项是:模型权重、激活值峰值、KV Cache、预留余量。C 中“全部请求的历史输入文本”并不常驻显存——输入 token 在计算完成后只保留在 KV Cache 中,原始文本由调用方保存。这就像冰柜只存备料和半成品菜,不存客人已经吃完的菜单。

Q9. 以下哪些是提升批量推理吞吐的合法手段?

多选题 · mid

  • A.采用连续批处理减少气泡← 正确答案
  • B.动态调整批大小适配流量← 正确答案
  • C.无限增大队列长度防止丢请求
  • D.用分页注意力管理 KV 缓存← 正确答案

答案:ABD。连续批处理(A)和分页注意力(D)是经典的吞吐优化手段,动态调整批大小(B)则让系统在不同流量下都保持高效。C 中“无限增大队列”会带来内存耗尽和延迟失控,是典型的隐藏炸弹——队列长度必须有上限并配合背压。

Q10. 为什么批处理能加速推理而不是简单线性叠加?

单选题 · easy

  • A.权重加载与前向计算被多个序列分摊← 正确答案
  • B.GPU 频率会随利用率自动提高
  • C.模型在批处理时自动降低精度
  • D.请求之间共享同一个输出结果

答案:A。批处理加速的核心在于多条请求的 token 在同一个 Transformer 前向中并行计算,权重矩阵只需加载一次、被多个序列共享,显存带宽被摊薄。B 中 GPU 频率与利用率无关,C 中精度由配置决定而非批处理自动触发,D 中每个请求的输出各不相同,不存在共享输出。