想象一家火爆的流水餐厅:顾客络绎不绝地进门,有人吃快餐、有人吃大餐,厨师在灶台间不停忙碌。传统餐厅的做法是“满一定人数才开一桌”,大家必须一起点菜、一起等所有人的菜都上齐了才翻台,结果高峰期桌位空着、顾客堵在门口干等。而连续批处理(Continuous Batching)就是那家用“随到随坐、吃完就走、空位立刻补新客”策略经营的聪明餐厅——它让 GPU 这张大餐桌上的推理请求不再憋屈地凑批次,而是每个用烹饪动作(迭代)细粒度地决定谁能上桌、谁该让位、谁去候餐区等待。这套机制让大模型推理服务的吞吐量大幅跃升,也让“排队吃一顿饭”的时间大大缩短。本文从这家餐厅出发,拆解连续批处理的调度哲学、抢占与恢复、分块预填充与 Split-Fuse、动态批大小调优,带你从零看懂现代推理引擎的提速秘密。

岗位关联

连续批处理是 LLM 服务端最重要的吞吐优化技术之一。掌握它,你才能真正明白 vLLM、TensorRT-LLM 等主流推理引擎为何能同时服务数千路请求,也让“系统性能工程师”“推理平台研发”这些岗位的工作不再玄学。

目录
  1. 可视化演示
  2. 静态批处理瓶颈
  3. 连续批处理原理
  4. 迭代级调度
  5. 请求抢占与恢复
  6. 分块预填充
  7. Split-Fuse 策略
  8. 动态批大小优化
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示LLM推理连续批处理的迭代级调度、Chunked Prefill分块预填充与动态批大小吞吐优化,支持模式切换。

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

迭代级调度 请求动态加入退出批次 已完成请求立即释放 B(t)=active(req,t) 每次迭代重新组批最大化吞吐

静态批处理瓶颈

流水餐厅开业前,老板定了个规矩:必须凑满 16 位客人,才让厨师开火。结果呢?下午三点,店里只有两位客人,桌子空着,厨师擦着锅发呆;晚上六点,队伍排到门口,客人眼睁睁看着空位却进不来;更糟的是,一桌子里有人吃五分钟的快餐,有人吃两小时的宴席,服务员只能陪跑到底,直到最后那位贵客放下筷子,整桌才翻台。这就是传统静态批处理的困局——它要求“满员才开席,整批一起走”。

在 LLM 推理服务里,“客人”就是一个个推理请求。静态批处理会等待请求攒够预先设定的一批数量,或等满固定时间窗,然后才开始推理;整批请求全部生成完毕,才释放资源接下一批。这带来两个问题:一是等待期间 GPU 空闲,白白烧钱;二是批内请求长短不一,最慢的那个请求决定了整批的结束时刻,所有快请求只能干等着同一个“整体完成”。

厨师们的直觉是“人多力量大”,但人多也意味着有人拖后腿。静态批处理就是让全员绑在一根绳上,GPU 的算力天花板被这群“扎堆的客人”死死压住:空闲时段利用率低、繁忙时段排队爆炸,吞吐量和响应时间双双难看。要让餐厅活起来,必须打破“整批开席”的框架。

Tbatch=max⁡r∈batchtrT_{\text{batch}} = \max_{r \in \text{batch}} t_r
整批耗时等于批内最慢请求的耗时,其他人再快也只能陪跑。
  1. 批内请求集合用一个集合标识当前批次里所有请求,它们共享一次 GPU 计算。
    r∈batchr \in \text{batch}
  2. 单个请求耗时每个请求从进入批到生成完最后一个字符所花的时间,记为独立变量。
    trt_r
  3. 取最大值整批真正的墙钟时间由最慢的那位决定,快者无法先走。
    max⁡r∈batchtr\max_{r \in \text{batch}} t_r

连续批处理原理

聪明的老板把规矩改成:一有客人进门,只要有空桌,就立刻上菜单;客人吃完离席,空位不等人,马上安排候餐区下一位入座。于是下午三点的快餐客和晚高峰的宴席客再也不用互相迁就,每一寸餐位都在持续产出营业额。这就是连续批处理的核心思想——请求级动态调度。

对应到推理引擎:不再等一批请求攒齐,而是每个请求一到达,就尝试加入 GPU 当前正在执行的批次;批次里的请求各自独立推进,谁生成了终止符(EOS),谁就立即退出,空出的“插槽”马上被新请求占据。这种做法把“批”从物理上的整批等待,变成了逻辑上的持续流水线。GPU 的有效算力被源源不断的请求填满。

这一个简单的“随到随坐”改革,就能让 GPU 利用率从 50% 左右拉升到 90% 以上。论文实验里,连续批处理对比静态批处理可以把吞吐量提升数倍到数十倍。餐厅还是那家餐厅,厨师还是那个厨师,只是不再死等满员,营业额自然翻几番。

Utilization=∑rtractiveNslots×T\text{Utilization} = \frac{\sum_{r} t_r^{\text{active}}}{N_{\text{slots}} \times T}
利用率 = 所有请求真正在 GPU 上计算的时间之和 ÷ 总可用算力时间,连续批处理让分子逼近分母。
  1. 活跃计算时间每个请求在 GPU 上实际占用的计算时长,不含排队等待时间。
    tractivet_r^{\text{active}}
  2. 插槽容量GPU 显存和算力能同时容纳的请求数,即餐位数量。
    NslotsN_{\text{slots}}
  3. 总时间窗口观察时长,比如一分钟内的系统运行时间。
    TT
  4. 相除得利用率所有请求的有效工作时间占总容量的比例,越高说明资源越不浪费。
    Utilization=∑rtractiveNslots×T\text{Utilization} = \frac{\sum_{r} t_r^{\text{active}}}{N_{\text{slots}} \times T}

迭代级调度

新餐厅的老板发现,光有“随到随坐”还不够:服务员一次端几桌的菜,端完一轮回到厨房,得重新看一下哪些客人已经吃完、哪些新客在门口等位。这个“每端一轮菜就重新分配一次任务”的动作,就是迭代级调度。它发生在 Transformer 推理的每一次解码迭代之间,频率极高,因此决策必须又快又准。

在技术实现上,每一次迭代跑完后,调度器都会做三件事:检查哪些请求已经生成了结束符,将其移出批;查看等待队列里有没有新请求,以及 GPU 当前有没有可用插槽;再把可用的插槽分配给优先级高的新请求。因为每次迭代的批成员都可能变化,这就是“动态批大小”。同时,为了不让调度本身成为瓶颈,插入和退出操作都是 O(1) 或接近 O(1) 的,绝不重排整批。

这就像服务员每转一圈就重新记一遍谁吃完了、谁要加菜。迭代级调度把“批次”从一个固定的实体变成了不断流动的状态,餐厅从此永远有菜可上、有桌可翻,GPU 再也不用干瞪眼。

St=St−1∖Ft∪NtS_t = S_{t-1} \setminus F_t \cup N_t
当前批 = 上一批去掉本次完成的请求,再加上新选入的请求,每步迭代都重新组建队伍。
  1. 上一轮批次上一解码迭代时在 GPU 上共同前进的请求集合。
    St−1S_{t-1}
  2. 移除已完成者本次迭代发现已生成结束符的请求集合,立即放走,腾出席位。
    FtF_t
  3. 加入新请求从等待队列里选出的新到请求集合,在空余插槽就绪后补位。
    NtN_t
  4. 构成新批次这两步一减一加,就得到下一轮迭代要执行的批次。
    St=St−1∖Ft∪NtS_t = S_{t-1} \setminus F_t \cup N_t

请求抢占与恢复

晚高峰来了,餐厅里突然闯进一大队贵宾(某个高优先级的大请求),但所有桌子都坐满了。这时候老板怎么办?他不能把贵宾赶走,也不能让大厅堵死。于是厨房想了个办法:挑几位刚上完开胃菜、主菜还没上的客人,客客气气地请他们先去候餐区坐一会儿,把桌子腾给贵宾;等错峰过后,再把这几位客人请回原桌,继续上没吃完的主菜。这个过程就是请求抢占(Preemption)。

大模型推理中的“客人”占着两样资源:计算插槽和显存里的 KV 缓存(注意力机制保存的历史状态)。当新请求需要显存而当前批次已满时,调度器会选中某些请求,把它们的状态(KV Cache)从 GPU 显存交换到 CPU 内存,腾出显存给新请求;等后续迭代有空间了,再把这些状态加载回 GPU,从被抢占前的 token 位置继续生成,就像客人回到餐桌接着吃。

好的调度策略会尽量避免抢占:比如先抢占生成进度最少、恢复成本最低的请求;或者干脆用分块预填充减少长请求的内存峰值,让“抢桌”根本不发生。但抢占机制本身是安全网——没有它,遇到内存陡增时系统只能崩溃,就像餐厅只能直接赶客人出门一样不可接受。

Toverhead=∑s∈preemptedTswap(s)T_{\text{overhead}} = \sum_{s \in \text{preempted}} T_{\text{swap}}(s)
抢占不是免费的:每次让位都要把 KV 状态搬到 CPU 再搬回来,开销越大,系统越吃亏。
  1. 被抢占的请求集合当前批次中被选中让位的请求们,它们并未结束,只是暂停。
    s∈preempteds \in \text{preempted}
  2. 单次交换耗时把某个请求的全部 KV 状态从 GPU 搬到 CPU(或反向)的时间。
    Tswap(s)T_{\text{swap}}(s)
  3. 累计总开销所有被抢占请求的交换时间相加,就是这次抢占造成的系统损耗。
    ∑s∈preemptedTswap(s)\sum_{s \in \text{preempted}} T_{\text{swap}}(s)

分块预填充

之前我们把每个请求都当成“坐下来吃饭的客人”,但细看推理过程,每个客人其实有两个阶段:先看菜单点菜(预填充 Prefill,把整段提示词一次性算完),再等一道道上菜(解码 Decode,逐字生成答案)。传统实现里,点菜阶段是把整份菜单一口气全部读进去的——如果客人点了一本厚厚的册子(几千 token 的长提示),厨房就得连续忙活很久,期间所有正在上菜的其他客人都得干等,这就是预填充阻塞解码的问题。

分块预填充(Chunked Prefill)把那份厚厚的菜单切成许多小块,每块只算一小段提示词,然后让这些小块穿插在解码任务之间执行。这样长提示词的预填充不再独占 GPU,解码请求的“上菜节奏”不会被一锅端地打断。代价是预填充本身的完成时间可能变长,但换来的是整体吞吐和所有请求的平均延迟大幅改善。

分块预填充就像把一块厚牛排切成薄片,每片都能快速煎熟,厨房也就有空隙兼顾别的桌。长提示请求不再“堵死全厨房”,短请求的响应速度得到保护,整体系统的内存峰值也大大降低——一人一本厚菜单的显存压力,被摊薄成一小块一小块随到随算。

Nchunks=⌈LpromptCchunk⌉N_{\text{chunks}} = \left\lceil \frac{L_{\text{prompt}}}{C_{\text{chunk}}} \right\rceil
一个长提示词需要被切成多少块,取决于提示词总长度除以每块的最大长度,再向上取整。
  1. 提示词总长这条请求输入的全部 token 数,就像一本菜单的总页数。
    LpromptL_{\text{prompt}}
  2. 单块长度上限一次预填充最多计算的 token 数量,由显存和调度策略决定。
    CchunkC_{\text{chunk}}
  3. 相除取整总页数除以每块页数,有余数就得多切一块,所以向上取整。
    ⌈LpromptCchunk⌉\left\lceil \frac{L_{\text{prompt}}}{C_{\text{chunk}}} \right\rceil

Split-Fuse 策略

切了菜单还不够,老板发现厨房还有一种浪费:服务员上完一轮菜,发现灶台有空位,但下一道菜还没备好。如果能把“备菜”(预填充小块)和“上菜”(解码步)像拼积木一样填进同一个时间片,厨房就永远不会空转。Split-Fuse 正是这样一套融合调度策略:它把预填充任务拆成更小的块,塞进解码迭代的每一个空隙里,让 GPU 的每个计算脉冲都有人干活。

Split-Fuse 的聪明之处在于,它不是简单地把长请求切块,而是把“切下来的预填充块”作为一种灵活填充物,用来填补 GPU 因批次不整齐而产生的算力空洞。与此同时,它还可以把同一个长请求的多个分块分配到不同的迭代里,甚至让一个请求的预填充和解码部分平滑衔接。于是整个 GPU 的执行流就像拼图一样严密,几乎找不到空白时间片。

这场“填空游戏”对推理吞吐的提升是惊人的:Split-Fuse 的融合调度把预填充碎片塞进解码空隙,杜绝了 GPU 空转,专门解决“有人吃不上、有人没事干”的资源错配。餐厅的每分每秒都在出菜,算力被榨得干干净净。

Fractionfused=Tprefill+TdecodeTiter×Niter\text{Fraction}_{\text{fused}} = \frac{T_{\text{prefill}} + T_{\text{decode}}}{T_{\text{iter}} \times N_{\text{iter}}}
融合度越高,说明一个迭代窗口里预填充和解码工作交织得越满,GPU 越接近满载运转。
  1. 预填充与解码总时间系统在一个观察窗口里真正用于两类计算的总时长。
    Tprefill+TdecodeT_{\text{prefill}} + T_{\text{decode}}
  2. 迭代次数与单次时长整个观察窗口由多少次迭代构成,每次迭代固定耗时。
    Titer×NiterT_{\text{iter}} \times N_{\text{iter}}
  3. 总时间占比用有效计算时间除以总可用时间,得到 GPU 的融合利用率。
    Fractionfused=Tprefill+TdecodeTiter×Niter\text{Fraction}_{\text{fused}} = \frac{T_{\text{prefill}} + T_{\text{decode}}}{T_{\text{iter}} \times N_{\text{iter}}}

动态批大小优化

说了这么多,餐厅老板还需要一套聪明的“排桌系统”:高峰期多摆桌、低谷期收桌子;桌与桌之间保持安全距离,不能把过道挤爆。这就是动态批大小(Dynamic Batch Size)优化——批次大小不再写死,而是在每次迭代根据新到请求数、已完成请求数、显存余量、算力余量综合决定,动态伸缩。

工程实现中,调度器会维护几个关键指标:当前批的显存占用、每个请求的 KV 缓存大小、等待队列长度、还有用户设定的最大并发上限(配额)。每次迭代,调度器先执行“退场”逻辑(剔除已完成请求并释放显存),再执行“进场”逻辑(从等待队列中挑请求,直到配额占满或显存耗尽)。为了防止系统把显存吃爆,还会预留一部分缓冲空间给抢占和新请求突发。

动态批大小好比餐厅的“弹性餐桌”:客人多就加桌、客人少就撤桌,但天花板始终钉死。工程师还可以按业务调节参数——想要低延迟,就把批上限调小,让每个请求走得更快;想要高吞吐,就把上限调大,牺牲一点点响应时间来换整体产出,这就是吞吐与延迟的权衡艺术。

Bt=min⁡(Bmax⁡,  Qt+Bt−1−Dt)B_t = \min\left(B_{\max},\; Q_t + B_{t-1} - D_t\right)
当前批大小 = 上一批大小减去完成的请求,加上新进入的请求,并受最大批次配额约束;批大小永远在弹性变化。
  1. 上一批的批大小上一轮迭代结束时的活跃请求数量。
    Bt−1B_{t-1}
  2. 新增与完成本轮新加入的请求数,和本轮完成的请求数。
    Qt,  DtQ_t,\; D_t
  3. 动态更新式用“原来的批 + 进来的 - 出去的”算出新批大小。
    Qt+Bt−1−DtQ_t + B_{t-1} - D_t
  4. 配额上限保护结果不能超过系统设定的最大批大小,防止资源过载雪崩。
    min⁡(Bmax⁡,⋯ )\min(B_{\max}, \cdots)

AI 应用场景

在线 LLM 推理服务的动态调度

阶段:模型部署与在线推理

解决的问题:传统静态批处理下,GPU 要么空转等人齐,要么被长提示请求堵死,用户问一句“帮我写周报”要等半分钟。连续批处理让每个请求到达即进入推理流水线,服务端吞吐量可提升数倍,用户体验直线上升。

具体案例:vLLM 等推理框架实现连续批处理和 PagedAttention,在单张 A100 上同时服务数千个并发对话请求。用户消息到达后立即被调度进当前迭代批次,短文回复几秒返回,长文写作持续流式输出,GPU 利用率稳定在 90% 以上。

离线批量推理作业的提速

阶段:数据标注与批量推理

解决的问题:离线跑千万条数据的标注任务时,请求长度差异极大——短的几十 token,长的上万 token。静态批处理会让几千条短请求陪一条长请求“跑马拉松”,白白浪费算力。连续批处理按迭代动态调整批次,长短请求交错执行。

具体案例:某数据标注平台用脚本把 500 万条文本送入推理服务,服务端开启连续批处理与分块预填充后,长文本的预填充被切成小块穿插在短文本的解码之间,总任务耗时缩短 60%,GPU 资源成本同步下降一半以上。

混合负载场景的资源平衡

阶段:多租户推理平台

解决的问题:同一个 GPU 集群既要服务对话交互,又要跑大规模批量任务,还偶发高优 VIP 请求。静态调度完全无法应对这种混合流量。连续批处理配合请求抢占,让高优请求随时插队,低优请求被抢占后从断点恢复,不丢失任何进度。

具体案例:某 SaaS 平台用同一条推理服务承载聊天、文档摘要、代码补全三类负载。通过给不同类型的请求设置不同优先级与抢占策略,高峰期 VIP 请求的响应时间保持在 1 秒内,批量任务的吞吐只下降 15%,且无请求因抢占而重新计算。

Python 演示

python64 行
import randomfrom collections import deque  class Request:    def __init__(self, rid, total_tokens):        self.rid = rid        self.total = total_tokens        self.remaining = total_tokens     def step(self):        self.remaining -= 1        return self.remaining <= 0  class ContinuousScheduler:    def __init__(self, max_batch, max_steps):        self.max_batch = max_batch        self.max_steps = max_steps        self.queue = deque()        self.batch = []        self.done = 0        self.wait_time = 0     def add_request(self, req):        self.queue.append(req)     def step(self):        finished = [r for r in self.batch if r.step()]        self.done += len(finished)        self.batch = [r for r in self.batch if not r.is_done()]        while len(self.batch) < self.max_batch and self.queue:            self.batch.append(self.queue.popleft())        return len(self.batch)  class StaticScheduler:    def __init__(self, max_batch, max_steps):        self.max_batch = max_batch        self.max_steps = max_steps        self.queue = deque()        self.batch = []        self.done = 0     def add_request(self, req):        self.queue.append(req)     def step(self):        for r in self.batch:            r.step()        return len(self.batch)  if __name__ == "__main__":    requests = [Request(i, random.randint(2, 10)) for i in range(30)]     cs = ContinuousScheduler(max_batch=4, max_steps=100)    for r in requests:        cs.add_request(r)    for _ in range(100):        cs.step()        if not cs.batch and not cs.queue:            break    print("continuous done:", cs.done)
目的:用极简模拟器对比连续批处理与静态批处理在相同请求流下的完成数量,直观展示连续调度的高吞吐优势。
  1. class Request:每个推理请求对象,remaining 表示还需生成多少个 token
  2. def step(self):每调用一次,模拟一次解码迭代,生成一个 token 并返回是否结束
  3. class ContinuousScheduler:连续批处理调度器:维护等待队列、当前批次和完成计数
  4. finished = [r for r in self.batch if r.step()]这一步模拟迭代级考勤:先让批内所有请求前进一轮,再筛出完成的
  5. self.batch = [r for r in self.batch if not r.is_done()]完成的请求立即退出批次,空出席位
  6. while len(self.batch) < self.max_batch and self.queue:空位立刻从等待队列补新请求进来,这就是连续批处理的核心循环
  7. class StaticScheduler:对照组:不检查完成、不补新请求,必须等待最慢的请求完成

常见误区

误区连续批处理不就是把 batch size 从 4 改到 64 吗?批越大吞吐越高,参数调大就完事了。

正解完全不是。连续批处理的核心是调度时机的变化——从“按请求批次调度”变为“按解码迭代调度”。即使总的 max_batch 没变,只要允许请求在任意迭代动态进出,GPU 就能持续干活。机械地调大 batch size 只会让显存爆掉,而且如果批还是静态的,快请求照样慢请求陪跑。

为何常见:人们容易把“吞吐提升”直接归因于“并行度变高”,忽略了本质是消除了“等人齐”的空闲等待。

误区被抢占的请求是不是就丢了?用户那句话也算白问了,我还得重发一次请求。

正解抢占是“请去候餐区等位”,不是“赶出餐厅”。被抢占的请求会保存其 KV 缓存状态(必要时交换到 CPU),等显存有空余时恢复到原来的生成位置继续推进,用户感知最多是延迟变长,答案不会丢失。

为何常见:抢占的英文 preempt 在日常语境里带“打断”的意味,加上部分初学者没接触过状态交换的细节,容易把抢占和抛弃混淆。

误区把长提示切成小块穿插执行,那长提示词的回答速度不是变慢了吗?我宁愿让短请求等一等,也别委屈我的长文档!

正解单个长请求的预填充总耗时确实可能略微增加,但换来的是整个系统的吞吐和所有请求的平均延迟大幅下降。对长请求本身的“完成时间”而言,分块预填充通常还能通过尽早开始解码来缩短首 token 延迟。系统层面看,整体收益远大于单请求损失。

为何常见:把“单请求的局部最优”误当成“系统的全局最优”,忽略了排队效应——长请求独占 GPU 时,身后所有短请求都被迫排队,总账更亏。

误区既然请求随到随走、空位随时补新,那是不是永远不会出现资源不够的情况?抢占不是多此一举吗?

正解请求到达是随机的,显存分配是瞬时的。当高优先级大请求突然到达或显存碎片化严重时,调度器必须有能力“赶人让位”,否则只能 OOM 崩溃。抢占是安全阀,平时也许不用,但关键时刻救命。

为何常见:初学者只看到“随到随坐”的理想模型,忽略了真实系统中显存上限、内存碎片、优先级差异这些残酷变量。

误区调度器每次迭代都在改批大小?那我让它每秒都重新计算一次批次,是不是总能保持最优?

正解每步都重新计算会带来调度开销、请求搬移开销和 KV 缓存重排成本。实际实现会设置调度频率(如每 N 个迭代调度一次)、批大小上限和最小步长,在灵活性和开销之间取平衡。过度动态反而可能引发频繁抢占,吞吐暴跌。

为何常见:把“理论上的最优调度”等同于“不需要成本的调度”,忘了调度本身也要消耗 GPU 宝贵的时钟周期。

面试题

Q1. 连续批处理相比静态批处理,最核心的改进是什么?

单选题 · easy

  • A.把 batch size 调得更大
  • B.在每次解码迭代动态调度请求进出批次← 正确答案
  • C.用 CPU 替代 GPU 做推理
  • D.预先缓存所有请求的答案

答案:B。连续批处理的本质是调度粒度从“请求级批次”细化到“迭代级”,允许请求在任意迭代完成时退出、新请求随时补位。调大 batch size、换硬件都只是辅助手段,C 和 D 更是跑偏了方向。

Q2. 静态批处理的“整批耗时”由什么决定?

单选题 · easy

  • A.批内所有请求耗时的平均值
  • B.批内最短请求的耗时
  • C.批内最慢请求的耗时← 正确答案
  • D.GPU 的时钟频率

答案:C。因为整批请求必须全部完成后才能释放资源,所以墙钟时间等于最慢请求的耗时,即 max(t_r)。平均数会被快请求稀释,最短值更是完全不对,GPU 频率只是影响单请求速度的底层因素之一。

Q3. 分块预填充(Chunked Prefill)主要解决了什么问题?

单选题 · mid

  • A.显存中 KV 缓存的碎片化
  • B.长提示预填充阻塞解码请求的问题← 正确答案
  • C.CPU 与 GPU 之间的传输带宽不足
  • D.请求队列的优先级排序混乱

答案:B。长提示一次性预填充会长时间独占 GPU,让正在解码的短请求全部卡住。分块预填充把提示切成小块穿插执行,确保解码“上菜节奏”不被堵死。它不是直接解决显存碎片或传输带宽的手段。

Q4. 请求被抢占后,系统如何恢复它的继续生成?

单选题 · mid

  • A.从零开始重新计算整个请求
  • B.丢弃该请求,等待用户重发
  • C.保存 KV 缓存,恢复后从断点继续← 正确答案
  • D.切换到更小的模型重新推理

答案:C。被抢占请求的 KV 缓存会被保存或交换到 CPU,调度器等到显存充足后再把它加载回 GPU,从被抢占前的 token 位置继续解码。选 A 会浪费算力,选 B 是丢请求,选 D 改变模型不可行。

Q5. Split-Fuse 策略的核心思想是什么?

单选题 · mid

  • A.把长请求拆成多个独立请求并行处理
  • B.将预填充碎片填进解码迭代的空隙← 正确答案
  • C.用多个 GPU 分片加速单个请求
  • D.让所有请求强制使用相同长度的提示

答案:B。Split-Fuse 把预填充任务拆成小片,像拼图一样填进解码迭代因批次不齐而产生的算力空洞,让 GPU 持续满载。它不会改变请求的语义,也不会强制提示长度统一,更不是模型并行技术。

Q6. 某调度器设定 max_batch=8,当前批有 6 个请求,其中 2 个完成了生成。此时等待队列有 5 个新请求,本轮最多能加入几个?

单选题 · hard

  • A.2 个
  • B.4 个← 正确答案
  • C.5 个
  • D.6 个

答案:B。完成 2 个请求后,批内余下 4 个,距离 max_batch=8 还差 4 个空位,所以最多补入 4 个新请求。剩下的 1 个只能留在队列里等下一轮。这体现的就是动态批大小的“弹性补位”逻辑。

Q7. 以下哪些属于连续批处理带来的收益?

多选题 · mid

  • A.GPU 利用率提升← 正确答案
  • B.系统吞吐量增加← 正确答案
  • C.单请求延迟必然下降
  • D.快请求不再被慢请求拖住← 正确答案

答案:ABD。连续批处理消除了“等批次齐”的空闲,利用率和吞吐量显著提升;快请求完成后立即释放资源,不再陪慢请求跑到底。但单请求的最坏延迟未必下降,极端情况下还因抢占变长,所以 C 错。

Q8. 关于请求抢占,下列说法正确的是?

多选题 · mid

  • A.抢占可以把 KV 状态交换到 CPU← 正确答案
  • B.抢占后请求需要从头重新计算
  • C.抢占是应对显存不足的安全机制← 正确答案
  • D.抢占不会产生任何额外开销

答案:AC。抢占会把被选中请求的 KV 缓存交换到 CPU 腾出显存,属于安全阀机制。B 错在不需要从头算,状态恢复后从断点续跑;D 错在 swap 本身有时延开销,所以调度器会尽量少抢占。

Q9. 动态批大小优化中,哪些策略能防止显存过载?

多选题 · hard

  • A.设置最大批大小配额← 正确答案
  • B.预留一定显存缓冲空间← 正确答案
  • C.让批大小随请求数无限增长
  • D.引入抢占机制兜底← 正确答案

答案:ABD。配额上限直接封顶,预留缓冲应对突发请求,抢占作为最后兜底。C 是灾难性的,无限增长必然 OOM 或让单请求延迟暴涨,完全不可取。

Q10. 预填充(Prefill)和解码(Decode)两个阶段分别有什么特点?

多选题 · easy

  • A.预填充并行计算整段提示词← 正确答案
  • B.解码逐 token 串行生成答案← 正确答案
  • C.预填充的算力需求通常高于单步解码← 正确答案
  • D.解码阶段可以跳过预填充直接输出

答案:ABC。预填充一次性并行处理提示词,计算量大;解码每次只生一个 token,串行推进且内存访问密集。D 完全错误——解码必须依赖预填充生成的 KV 状态才能开始。