AI Hero Club

大模型高效推理 · 第 3 章

已自行标记完成0 / 8

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

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

CHAPTER 03 / 8建议学习 120 分钟

从 KV 元素到账本与准入预算

给定层数、批量和序列长度,怎样在分配缓存前算出内存需求与可接纳请求数?

本章要解决的 3 个问题
  • 推导缓存、权重、临时激活的独立空间账本
  • 计算MHA与GQA的投影和缓存差异
  • 用分页碎片与安全余量建立准入条件
课程项目 ZIP ↓

读者位置与空间口径

本章承接第二章的逐层缓存接口。需要区分三种同时存在的对象:权重在请求之间共享,KV 缓存随各请求历史增长,临时激活用于当前前向计算。训练时的参数梯度和优化器状态不属于本课推理账本,但若实际进程仍保留它们,就不能从进程内存中假装扣除。空间测量必须说明是逻辑张量字节、库分配器持有字节、进程驻留内存还是设备显存。它们的差异来自副本、分配粒度、缓存池和运行时,而非单位换算。

令层数为 NN,同长度批量为 BB,缓存长度为 LL,键值头数为 hkvh_{kv},每头宽度为 dhd_h,每元素字节数为 bb。每层有 K、V 两份,均含 BLhkvdhBLh_{kv}d_h 个元素,乘上层数与字节数即得下式。该式只适用于所有层相同头结构、未压缩的稠密缓存。若各请求长短不同,可把 BLBL 换成实际长度之和;若实现把全部请求补齐到最大长度,则仍要按分配后的长度计算。

MKV=2NBLhkvdhbM_{KV}=2NBLh_{kv}d_hb
缓存逻辑字节数;不包括分配器、块表、临时副本或权重。

完整算例一:公式必须对得上实际张量

项目模型有两层、批量二、缓存长二十四、四个键值头、每头宽八,使用双精度每元素八字节。单份单层张量有 2×4×24×8=15362\times4\times24\times8=1536 个元素,占一万二千二百八十八字节。K、V 两份与两层共四份,总量四万九千一百五十二字节,恰为四十八 KiB。注意 KiB 按一千零二十四字节计算,kB 按一千字节计算;如果报四十九点一五 kB,也应写清十进制单位。项目通过 numel 乘 element_size 独立求和后与公式相等。

在课程项目目录运行项目目录运行:以实际返回缓存检查字节公式。
from lab import make_model,tokens,chunks,kv_bytes
model=make_model();_,past=chunks(model,tokens(24,2),[24])
actual=sum(t.numel()*t.element_size() for pair in past for t in pair)
expected=kv_bytes(2,2,24,4,8,8)
assert actual==expected==49152
print(actual,actual/1024)

零长度是一个有用的极限:逻辑缓存应为零,而模型权重仍在。批量从一翻到二时缓存翻倍,共享权重不翻倍。每元素从八字节改到两字节时同形状逻辑缓存缩为四分之一,但模型必须真以该 dtype 保存缓存才成立;只在报告里修改 b 不会改变已分配张量。还应检查每层 K、V 的实际 dtype,因为混合精度实现可能让归一化、权重与缓存采用不同精度。项目据实际双精度计账,没有冒充半精度推理。

GQA 不只改变缓存,还改变 K、V 投影

普通多头注意力有 hq=hkvh_q=h_{kv},且 d=hqdhd=h_qd_h。分组查询注意力保留 hqh_q 个查询头,但让多个查询头共享一组键值头,要求本课简单分组中 hqh_q 能被 hkvh_{kv} 整除。查询和输出投影仍各含 d2d^2 个权重;键和值投影各从宽度 d 输出到 hkvdhh_{kv}d_h,各含 dhkvdhdh_{kv}d_h 个权重。忽略偏置的注意力投影总数如下。减少键值头并未减少查询数,因此每个查询对可见位置的打分工作不会简单按键值头比例消失。

Pattn=2d2+2dhkvdh=2d2(1+hkvhq)P_{attn}=2d^2+2dh_{kv}d_h=2d^2\left(1+\frac{h_{kv}}{h_q}\right)
MHA、GQA、MQA 的投影参数账本;假设查询总宽度等于 d。

例如 d=128,hq=8,dh=16d=128,h_q=8,d_h=16。普通多头的投影为六万五千五百三十六个;两键值头的分组查询为四万零九百六十个,比例是零点六二五,而缓存比例是四分之一。若误写“所有注意力参数也缩为四分之一”,会漏掉未变的 Q、O。前馈、嵌入和归一化参数还应另加,所以整模型参数缩减比例更小。项目实际模型仍是普通多头,本章的分组计算器只做账本,不把改头数当成已实现的输出等价优化。

把训练好的多个头简单平均到较少键值头,通常会改变函数,需要训练或校准与质量验证。原始 GQA 研究讨论过从已有多头检查点转换再继续训练的方案;本课不复现其训练结果,也不引用其硬件速度作为本实验成绩。对本项目而言,GQA 是一个明确的架构备选,属于需要另立检查点和质量证据的改变。缓存复用的精确证明不能证明改架构后精确等价,这两类改变应在最终报告分栏。

从逻辑长度到实际分配

连续预留若按最大长度分配,短请求会占住未来可能用不到的槽位。分页思想把每请求的逻辑词元位置映射到固定大小块,增长时按块取得空间。设一块容纳 SS 个词元,请求 i 当前长为 LiL_i,需块数为 ⌈Li/S⌉\lceil L_i/S\rceil,总槽位为各块数之和乘 S。最后一块未用槽位是内部碎片;块表和引用计数还会额外占空间。逻辑不连续的物理块需要注意力内核支持映射访问,单纯计算 ceil 并不会让当前模型变成分页推理器。

U=∑iLi,A=S∑i⌈Li/S⌉,W=A−UU=\sum_i L_i,\quad A=S\sum_i\left\lceil L_i/S\right\rceil,\quad W=A-U
U 为真实词元,A 为已分配槽位,W 为内部浪费槽位。

完整算例二:块越小不代表系统越快

请求长度为十七、三十一、二,块大小十六,则块数分别二、二、一,总共五块八十槽,实际使用五十槽,浪费三十槽。若每槽 KV 成本一千字节,仅缓存载荷分配八万字节,其中三万字节未使用。块大小改为八,块数三、四、一,总六十四槽,浪费十四槽;但块数由五变八,块表与索引管理增加。此处只证明内部碎片减少,没有测到寻址开销、物理连续性或内核带宽,所以不能推断块八必快。

安全准入还要为未来增长留预算。设当前可用预算为 F,预留不可动用余量 R,每个将被接纳请求按约定最大长度估计需 K 字节缓存,则同质请求的简单容量上界为 max⁡(0,⌊(F−R)/K⌋)\max(0,\lfloor(F-R)/K\rfloor)。这是假设权重等固定占用已扣除、请求独立占用且临时激活被余量覆盖的保守算术;如果当前 F 是设备总容量而没有扣权重,就会高估。若只按当前一词元占用准入,又没有增长限额,稍后会同时溢出。

取可用一千零二十四字节、预留六十四、每请求一百九十二字节,余量后剩九百六十,最多五个请求。若余量增到一百二十八,结果变成四个。这个不连续跳变来自向下取整,不能用“平均每请求占一百九十二”把小数容量解释为可接纳半个请求。异质长度应逐请求扣预算,且生成过程中应在增长前检查下一块是否能分配;失败时可排队、拒绝或按已声明策略抢占,不能继续写越界。

独立运行独立标准库验证:块碎片、准入取整和分组投影。
import math
lengths=[17,31,2]
def ledger(block):
    allocated=sum(math.ceil(n/block) for n in lengths)*block
    return allocated,allocated-sum(lengths)
assert ledger(16)==(80,30) and ledger(8)==(64,14)
assert (1024-64)//192==5 and (1024-128)//192==4
d,hq,hkv,dh=128,8,2,16
assert 2*d*d+2*d*hkv*dh==40960
print(ledger(16),ledger(8))

把峰值和生命周期放回账本

前向过程中会短暂同时持有旧缓存、拼接后的新缓存、打分矩阵和 logits。逻辑缓存求和只量最终返回张量,不能证明峰值等于该值。尤其当前实现显式构造注意力矩阵,其预填充临时量按长度平方增长;FlashAttention 采用重排计算减少这类中间物化和数据搬运,但不把模型的合法注意力关系改成近似稀疏关系。当前项目没有该内核,因此只把它列为可进一步研究的实现机制,不提供任何虚构的速度或峰值记录。

第一类检查是维度与单调性:长度、层数、批量、每元素字节翻倍时对应逻辑成本翻倍,键值头减少时缓存按比例变小。第二类检查是独立观察对象:实际张量求和对公式,分配槽位对真实长度,预算对剩余整数容量。这些检查分别约束不同层次,无法相互替代。只有真实服务运行中跟踪高水位、临时峰值和回收行为,才有资格承诺不会因特定请求混合而耗尽内存;本实验尚未实现该服务。

总参数也可以用实物清点而不是猜测“几百万模型”。逐项累加 model.parameters 的 numel 得到元素数,再按各自 element_size 求字节,能够发现绑定权重、重复引用与混合 dtype 带来的口径问题。本模型头部不与嵌入绑定,因而词表嵌入与输出投影分别占空间;若未来改为共享同一存储,应按唯一底层存储核对,而不是将参数名字出现次数直接相加。保存文件还可能有容器元数据和对齐,因此磁盘检查点大小与参数裸字节也不必相等。

练习与项目增量

TRY IT YOURSELF

基础:缓存容量

四层、批量三、长度100、两键值头、每头16、每元素2字节,求缓存逻辑字节。

提示 1
先数单层单种K/V张量的标量数,再乘两种、层数与元素字节。
提示 2
核对heads指键值头而非查询头,完成后换算KiB;公式里的2已经包含K和V。
对照参考解答
代入2×4×3×100×2×16×2=153600字节,即150 KiB。这里是K、V和全部层总和,不应再次乘二。若批量改为一,则为51200字节。

TRY IT YOURSELF

进阶:分组投影

d=64,查询头4,每头16,键值头1。不计偏置,注意力投影和MHA各有多少参数?

提示 1
Q、O仍是d到d,K、V输出宽度改为键值头数乘每头维数。
提示 2
分别计算两组投影,再与四个d×d矩阵比较;缓存头数比例不等于全部投影参数比例。
对照参考解答
分组为2×64²+2×64×1×16=10240;MHA为4×64²=16384,比例0.625。缓存比例是1/4,二者不同,因为Q、O的宽度未变。

TRY IT YOURSELF

挑战:识别容量结论缺口

报告说最终缓存占8 MiB,设备空闲10 MiB,因此一定不会溢出。指出至少三个未检查条件并给出修正。

提示 1
最终逻辑缓存与运行峰值不是同一对象,下一步还会多出什么?
提示 2
列临时激活、拼接副本、增长与分配器项,再决定准入预留,并用真实峰值检查而非静态相减。
对照参考解答
未检查临时激活、拼接副本和下一步缓存增长,也可能未计分配器保留与碎片。修正为先区分最终逻辑量与峰值,按最大允许增长计算准入,在分配前检查预算并留余量;通过真实峰值记录验证。只由两个静态数字不能给出必不溢出的结论。

项目增量是 kv_bytes、pages、capacity 三个可独立检验的账本函数,以及实际缓存字节证据。第四章将用有限槽位讨论调度,但那里的槽位是服务并行位置,不等于本章字节页块。两个模型相连时必须有从每个活跃请求到缓存预算的映射;当前实验把它们分开验证,避免将离散调度吞吐当作真实显存管理已经完成。

CHECK YOUR UNDERSTANDING

检验理解

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

1. 每层KV的系数2来自哪里?

查看解析每层分别存储键和值。
2. GQA减少键值头后通常不变的是?

查看解析查询头总宽度与输出总宽度保持d,K、V可变窄。
3. pages函数实现了什么?

查看解析它只计算ceil与浪费,不实际分配或寻址KV。
4. 最终张量字节能否代表峰值?

查看解析生命周期重叠与运行时开销需要额外测量。

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