AI Hero Club

大模型高效推理 · 第 4 章

已自行标记完成0 / 8

仅保存在当前浏览器;完成标记不代表通过项目验收。

本章提纲 9 个小节
全课目录 8 章
完整课程计划 ↗
大模型高效推理 / 课程计划 ↗

CHAPTER 04 / 8建议学习 120 分钟

连续批处理:状态、时间轴与可证伪模拟

一个短请求到达时长请求还没完成,连续批处理怎样改变等待和完成时间?

本章要解决的 3 个问题
  • 实现可追溯的FIFO离散调度并检查守恒
  • 计算静态与连续批次的完成、等待和利用率
  • 识别模拟时钟与真实硬件、首词元目标的边界
课程项目 ZIP ↓

模拟对象与不被偷换的假设

前三章已经给出一次前向的计算与空间模型,现在引入多个到达请求。本章的项目是离散调度器,不执行 Transformer 前向。每个请求有唯一整数编号 id、到达刻 arrival、所需服务词元数 tokens;每个时刻的长度是一 tick,所有活跃请求每 tick 各完成一个服务词元。并行槽位数为 S。词元服务是已知的有限工作量,没有预填充、网络、采样停止或可变计算时间,因此 tick 是抽象单位,不是毫秒。

这种简化让我们隔离一个机制:同一批次中短请求完成后,空出的槽位何时交给下一个请求?静态 FIFO 在整组请求全部完成前保留组边界,结束的槽位空等;连续 FIFO 在下一 tick 开始时回收完成者,按到达时间与编号顺序补入等待者。两种策略使用完全相同的输入和单位服务假设。若连续策略表现更好,只能归因于该模型中的槽位利用差异,不能据此断言真实设备上连续批处理必有相同比例加速。

可追溯状态和一步转移

状态由尚未接纳的等待队列、活跃请求及剩余服务量、已完成记录和当前时刻组成。每 tick 先处理上一 tick 的完成回收,再接纳到达且有空位的请求,然后给当前运行者各减一,剩余量变成零者记录完成时刻为当前 tick 加一。如果系统没有运行者且下一请求尚未到达,时钟可直接跳到下次到达;这段空闲仍包含在从零到最终完成的观测区间内。排序只用事先约定字段,不能为了漂亮结果偷偷优先短任务。

调度的安全不变量包括:一个编号不能同时存在两份活跃副本;未到达请求不能开始;活跃数不超过槽位;剩余量非负;每个请求恰有一份最终完成记录。工作守恒要求所有 tick 中运行槽位数之和等于全部请求词元服务量之和。此模型没有取消或失败,因而守恒等式应精确成立。若未来加入取消,就必须把已服务、已取消和剩余工作分别记录,不能仍沿用同一个完成量断言。

W=∑igi=∑t∣Rt∣,U=WS TendW=\sum_i g_i=\sum_t |R_t|,\quad U=\frac{W}{S\,T_{end}}
g为每请求服务词元,R为当tick运行者集合,利用率以从零到最终完成的整个区间计算。

完整算例一:四个请求、两个槽位

请求零到三都在零时刻到达,服务量依次为一、四、二、三。静态策略先运行零与一:第一 tick 两者都服务,零完成;之后三个 tick 只剩一运行,直到时刻四整组结束。第二组二与三从四开始,二在六完成,三在七完成。总工作量十个服务词元,占用七个 tick、两个槽位,利用率为 10/(2×7)=5/710/(2\times7)=5/7。短请求零留下的三个空槽位是本模型中可观察的浪费。

连续策略第一 tick 后释放零,在时刻一接入请求二;二服务两 tick 于三完成,再于三接入请求三。请求一在四完成,请求三在六完成。完成时刻依次为一、四、三、六,总跨度六 tick,利用率为 10/12=5/610/12=5/6。吞吐从每 tick 十除以七变成十除以六,比例七除以六,而不是按利用率差的百分点来解释。等待时间零、一、二、三号依次为零、零、一、三,均由开始减到达计算。

在课程项目目录运行项目目录运行:检查逐tick事件与最终守恒,不读取任何硬件速度。
from lab import schedule
jobs=[{'id':i,'arrival':0,'tokens':n} for i,n in enumerate([1,4,2,3])]
a=schedule(jobs,2,False);b=schedule(jobs,2,True)
assert a['makespan_ticks']==7 and b['makespan_ticks']==6
assert a['busy_slots']==b['busy_slots']==10
assert [j['finish'] for j in b['completed']]==[1,4,3,6]
print(a['utilization'],b['utilization'])

完整算例二:改策略也可能没有收益

若两请求都在时刻三到达、服务量都为四且槽位为二,则两者从三开始并于七完成。静态与连续策略完全相同,因为没有一个提前释放而可被复用的槽位。按从零观测的利用率为八除以十四,若改从首请求到达观测则为一;两个值各有明确分母,不能挑较大者而不标区间。这个负结果可证伪“连续批处理必定更快”的过度结论,并检查时钟跳跃是否错误抹掉了初始空闲。

另一类容易遗漏的边界是请求在 tick 边界恰好到达。当前协议先回收再接纳,到达时刻等于当前 tick 的请求可立即加入本 tick;在上一 tick 开始之后才到达者不能倒流进入。若时间离散化来自真实事件,应事先规定取整方式,否则将到达时间向下取整会人为减少等待。本项目输入直接采用整数 tick,因此没有将真实秒数粗化的误差。测试同时检查延迟到达、相同长度以及所有开始时刻不早于到达。

等待、完成与公平分别是什么

定义请求 i 的等待为 wi=si−aiw_i=s_i-a_i、流经时间为 ri=fi−air_i=f_i-a_i,其中 a、s、f 分别为到达、开始服务和完成;此处 f 不再表示首词元,避免与第一章时间符号混淆。由于每个运行 tick 都服务一个词元且无抢占空档,本模型有 ri=wi+gir_i=w_i+g_i。完成时间均值不能替代最大等待,长时间排队的某个请求会被均值稀释。FIFO 只定义接纳顺序,不自动给出任意负载下有限等待的保证。

若平均到达工作量超过系统长期可提供的服务,队列会增长;提高槽位利用率不能取消这个守恒约束。还需注意同一 tick 多个槽位并行不表示单个请求的词元可以互相并行,因为自回归后续依赖此前输出。在真实模型中增大 batch 可能改善权重复用,也可能增加每轮计算时间;本模拟假定轮长不随 batch 改变,所以它专门检验调度空洞,而不预测硬件的最优 batch。

已知全部输出长度是离线模拟的便利,真实服务往往只知道上限,实际何时生成 EOS 不确定。FIFO 连续策略不需要提前知道总长来做接纳,它只在完成事件发生后释放槽位;但我们的模拟器用预设整数服务量产生完成事件。若研究长度预测或优先短请求,应把预测误差、最大等待保护与未知完成状态加入模型,并与不使用未来信息的基线比较,不能利用预知真实长度获得不可实现的成绩。

把预填充和空间约束重新接入真实系统

真实连续批处理不仅有解码完成,还要插入新提示的预填充。长提示可能占用一轮大量计算,使既有请求的输出间隔变大;分块预填充把长提示拆开交错执行,但会引入调度选择和缓存分配。要评价这类系统,状态还需记录每请求处于预填充或解码、已处理提示长度、KV 页、模型与位置身份,以及该轮允许的词元预算。本实验没有实现这些机制,连续批处理官方文档用于核对概念,项目成绩只覆盖更小的离散模型。

第三章的字节准入与本章槽位准入应联合满足。两个请求即使槽位空着,若增长到上限所需 KV 无预算,也不应接纳;反之内存足够也不代表计算服务能力足够。真实系统还可能支持抢占重算或转移缓存,此时需要记录额外服务成本和恢复身份。没有这些状态的调度表,只能回答静态与连续回填的理想差别,不能作为生产服务容量承诺。最终报告会把模拟输出与 CPU 前向时间单列,不用 tick 乘某个平均毫秒伪造端到端延迟。

独立运行标准库独立事件账本,与调度器实现分离。
jobs=[(0,0,1),(1,0,4),(2,0,2),(3,0,3)]
# An independent hand-written event ledger for the continuous case.
running=[[0,1],[1,2],[1,2],[1,3],[3],[3]]
work=sum(len(row) for row in running)
counts={i:sum(i in row for row in running) for i,_,_ in jobs}
assert work==10
assert counts=={i:n for i,_,n in jobs}
assert max(map(len,running))==2
print(work/(2*len(running)))

第一类验证是事件重放:逐 tick 检查每个编号实际服务次数、开始时刻和完成时刻,而非只看总跨度。第二类验证是变形与极限:单请求两策略一致;槽位大于请求数且同时到达时无回填收益;全部等长请求的整组边界不产生短请求空洞。若这些性质失败,应先修复状态转移,再讨论优化。当前十项测试包含代表性正例和无收益例,没有给出任意输入的形式化全称证明。

练习与项目增量

TRY IT YOURSELF

基础:事件守恒

两个槽位、三个同时到达请求,服务量为1、3、1,连续FIFO的运行者和完成时刻是什么?

提示 1
按到达→准入→每槽服务一单位→完成的tick顺序画表。
提示 2
第一个短请求完成后下一tick谁补入?按请求编号记录完成时刻,再用工作量与总槽tick算利用率。
对照参考解答
tick0运行0、1,零于1完成;tick1运行1、2,二于2完成;tick2仅运行1,一于3完成。完成时刻按编号为1、3、2,工作总量5,利用率5/6。

TRY IT YOURSELF

进阶:指标不是同义词

算例一连续策略的平均等待和平均流经时间是多少?

提示 1
等待看首服务减到达,流经看完成减到达。
提示 2
逐请求计算后再平均,并检查平均流经=平均等待+平均服务,不从最终跨度猜均值。
对照参考解答
等待分别0、0、1、3,均值1。流经时间分别1、4、3、6,均值3.5。平均服务量为2.5,满足3.5=1+2.5。静态等待为0、0、4、4,均值2,不能只由最终完成跨度猜出这些逐请求值。

TRY IT YOURSELF

挑战:识别无效线上结论

某报告将模拟6tick乘单请求实测1毫秒,称四请求线上6毫秒完成。指出缺失项并给出可验证的下一层模型。

提示 1
抽象tick需要什么真实执行成本模型才能换成墙钟毫秒?
提示 2
列batch/长度相关前向、prefill、队列、网络与空间准入,说明单请求一轮时间为何不能直接乘多请求tick。
对照参考解答
单请求轮长不等于多请求轮长,预填充、队列、网络、缓存预算与停止均缺失。下一层应真实执行指定batch与长度的前向,记录每轮活跃请求和实际时间,并将预填充、解码及空间准入接入事件日志;在此之前只能报告六tick的抽象调度跨度。

本章项目增量是 schedule 及保存下来的逐 tick 运行者列表。它提供调度机制的可重放证据,不伪装成网络服务。第五章转入量化:与缓存等价和调度顺序不同,量化主动改变数值函数,因此要引入误差预算与质量评价,再到第七章在固定条件下测量 CPU 时间。

先验证整数工作域,再讨论终止

schedule 现在先检查 slots 是正整数;每条 job 必须含 id、arrival、tokens,三者 type 恰为 int,排除 Python 的 bool,arrival≥0、tokens≥1,id 唯一。小数、NaN、无穷、字符串、缺字段立即给 ValueError。若让 tokens=1.5 进入原循环,剩余量按 1 递减会经过 0.5、−0.5 而永不等于零,完成条件永远不触发。 合法有限整数输入下,令总剩余工作 W 为所有未完成请求 remaining 的和。运行一个非空 tick 至少完成一个词元,使 W 严格下降至少一;空闲时直接跳到下一个有限 arrival,接纳至少一个任务。W 起始是有限非负整数,故运行 tick 数不超过总词元量,每个任务最终仅完成一次。这个终止论证依赖输入域,不能只证明示例整数而放任其他数值进入。

BUILD THE MECHANISM

实现有停止保证的连续批处理

实现整数tick的FIFO调度,同一项目任务记录仍使用id/arrival/tokens。 下载本课完整项目,在项目根目录运行;只编辑 learner/starter.py,已有构造框架由项目提供。

输入与输出 精确整数排除bool;id唯一、arrival>=0、tokens和slots>=1。
每tick最多slots个服务;完成时间是服务tick+1;空闲跳向下一到达。

在课程项目目录运行待完成的学习任务
# schedule
def schedule(jobs,slots):
    raise NotImplementedError('schedule')
提示 1

先列到达→准入→服务→完成四个顺序。

提示 2

每个有工作tick让正整数剩余总量减少,不能让1.5越过0。

提示 3

完成者的空槽下一tick回填;不要把模拟tick换算成毫秒。

独立检查 .venv/bin/python -m learner.check --stage schedule --output runs/learner-schedule.json

迁移检验 改到达时间和长度分布,先预测哪个请求延后;核对总服务量守恒,不要求连续调度在所有输入都更快。

参考实现与解释
在课程项目目录运行参考实现
# schedule
def schedule(jobs,slots):
 if type(slots)is not int or slots<1:raise ValueError('positive integer slots')
 rows=[];seen=set()
 for job in jobs:
  if set(job)!={'id','arrival','tokens'}or any(type(job[k])is not int for k in job):raise ValueError('exact integer id/arrival/tokens')
  if job['arrival']<0 or job['tokens']<1 or job['id']in seen:raise ValueError('invalid/duplicate job')
  seen.add(job['id']);rows.append(dict(job))
 waiting=sorted(rows,key=lambda j:(j['arrival'],j['id']));active=[];time=0;finished={};trace=[]
 while waiting or active:
  if not active and waiting:time=max(time,waiting[0]['arrival'])
  while waiting and waiting[0]['arrival']<=time and len(active)<slots:active.append(waiting.pop(0))
  served=[j['id']for j in active];trace.append({'tick':time,'served':served})
  for job in active:job['tokens']-=1
  for job in active:
   if job['tokens']==0:finished[job['id']]=time+1
  active=[j for j in active if job_remaining(j)>0];time+=1
 return {'finished':finished,'trace':trace,'makespan':time}

CHECK YOUR UNDERSTANDING

检验理解

先作答,再决定是否展开解析。选择题用于发现理解缺口;独立实现与迁移任务仍需另行检验。

1. 本模拟一tick表示什么?

查看解析tick是模型定义的离散服务单位。
2. 连续FIFO在何时补空位?

查看解析先回收完成者,再接纳已到达等待者。
3. busy_slots应等于什么?

查看解析每运行槽位每tick贡献一个服务词元,形成精确守恒。
4. 等长同时到达且槽位足够时?

查看解析没有提前释放且可回填的空洞,就没有该机制带来的收益。

自行标记用于记录学习进度,不代表通过项目验收。