☰ inferserver 的数学基础

13. 批处理、padding、chunked prefill

13.1 为什么 batching 能提速:算术强度

一个权重矩阵 \(W\in\mathbb{R}^{m\times n}\) 有 \(mn\) 个参数。处理 \(B\) 个 token(或 \(B\) 个序列各 1 个 token)时,计算量约 \(2Bmn\),但权重只需要从内存读一次(\(4mn\) 字节,float32)。只算权重这一项,定义算术强度为“每读一字节做多少次浮点运算”:

\[ \text{AI}=\frac{2Bmn}{4mn}=\frac{B}{2}\ \text{FLOP/byte}. \]

\(B=1\) 时只有 0.5,远低于现代 CPU/GPU 的“计算/带宽”比值,所以 decode 是内存带宽受限:时间主要花在读权重,而不是算。\(B\) 增大,AI 线性增长,逐渐变成计算受限,同样的权重被更多 token 摊薄,吞吐上升。这就是 continuous batching 的收益来源。

如果每个序列长度不同,要 padding 到同一宽度 \(T_{\max}\)。浪费比例是

\[ 1-\frac{\sum_{i=1}^{B}T_i}{B\cdot T_{\max}} . \]

长度差异越大,padding 越浪费;这也是为什么调度器更愿意把长度相近的请求放一起,或者用 chunked prefill 切长 prompt。

13.2 continuous batching:每一步都重新组批

传统静态 batch 要等一整批都结束才能换下一批;continuous batching 在每个解码步检查:

  1. 有新请求且资源够(块数、token 预算、batch 上限)→ 准入;
  2. 对每个 running 序列推进一个 token(或一段 prefill chunk);
  3. 遇到 EOS / 达到 max_tokens → 退出,释放块;
  4. 下一轮重新组批。

这样短请求结束后,空出的槽位立刻给新请求,GPU/CPU 不会因为“等最长的那个”而空转。吞吐的粗算可以用 Little 定律:

\[ L=\lambda W, \]

其中 \(L\) 是系统内平均并发数,\(\lambda\) 是吞吐(请求/秒),\(W\) 是平均逗留时间。并发越高,在延迟不变时吞吐越高。

13.3 chunked prefill:把长 prompt 切片

长 prompt 的 prefill 是平方成本,一次做完会长时间占住整个 batch。chunked prefill 把 prompt 切成每轮最多 prefillChunk 个 token 的片段,并受全局 maxBatchedTokens 预算约束:

\[ \text{本轮 prefill 总量}=\sum_{i}\min\big(\text{chunk},\ \text{remaining}_i\big)\le \text{maxBatchedTokens}. \]

decode 不占这个预算(代码注释明确写了),所以长 prompt 不会把已经在解码的序列饿死。

总计算量会变吗?设 prompt 长度 \(T=Kc\)。第 \(k\) 个 chunk 有 \(c\) 个 query,每个 query 要看前面约 \(kc\) 个 key,注意力成本约 \(c\cdot kc\)。总和

\[ \sum_{k=1}^{K}c\cdot kc=c^2\frac{K(K+1)}{2}\approx\frac{T^2}{2}, \]

和一次性 prefill 的 \(T^2/2\) 同阶。所以 chunking 不减少总 FLOPs,它优化的是延迟公平性和 batch 利用率:把一个大任务拆成小片,让其他请求有机会插进来。

13.4 准入与抢占:块数够不够

一个序列长度 \(T\)、每块 \(B\) 个 token、\(L\) 层,需要

\[ L\cdot\left\lceil\frac{T}{B}\right\rceil \]

个物理块。准入前要预留“当前长度 + 本次要生成的 token 数”所需的块;如果空闲块不够,调度器要么让请求继续排队,要么抢占(preempt)某个 running 序列。

本项目的抢占选择“重算”而不是“换出到内存”:被抢占的序列释放块,之后重新用 chunked prefill 把 prompt 再喂一遍。这是因为纯 Go、CPU 场景下,换出的拷贝成本和实现复杂度都不划算;重算的代价是 \(O(T^2)\) 的 prefill,但语义简单、不会引入 swap 的一致性 bug。

对应代码:cmd/server/scheduler.go(准入、预算、抢占)、model/batchstep.go(左 padding 与绝对位置)。