一个请求为什么越生成越占内存?
模型权重没有变化,服务刚启动时也能正常运行,可是同时回答的人变多、长对话变多之后,显存接近上限。第 02 单元已经提醒你,权重只是内存账的一项。现在打开“请求状态”:自回归模型逐步生成,每一步都要利用此前处理过的上下文,运行时要保存一些可复用的信息。
先把 token 想成 tokenizer 输出的离散编号,不能固定当作一个汉字。模型通过多层计算,把这些编号变成表示,再预测下一个 token。对标准因果注意力,当前位置只能看到自己及此前位置。某一层中,已有位置的 key 和 value 不会因为后面新增 token 而重新改变,因此可以缓存;新位置用自己的 query 访问这些已有 key/value。
这份 KV cache 是模型执行状态,不是回答文本、向量检索数据库,也不是整段 attention 分数矩阵。缓存避免反复为旧位置计算同样的 K/V,代价是占用内存并在后续注意力计算中读取。它没有让“生成后面的 token”变成一次完全不依赖上下文的常量工作。上下文越长,缓存和相关读取可能越多。
prefill 与 decode:同一请求的两个阶段
prefill 处理输入 token,建立各层的 KV 状态,并得到首次采样所需的输出;decode 随后反复处理新位置,使用已有缓存预测下一步,再追加新 K/V。这里的“先后”是请求依赖关系,不要求整个服务把所有人的 prefill 全做完再统一 decode。多个请求可以处于不同阶段,如何共同执行由第 07 单元的调度器决定。
prefill 通常有更多输入位置可并行处理;decode 通常每条序列只增加少量新位置,可能更容易受权重或状态读取限制。但阶段名称不是瓶颈证明:批量、长度、精度、kernel 和设备都改变实际情况。长输入 TTFT 高,应分开看排队和 prefill;首字很快却生成拖尾,则要查看 ITL、输出长度及结束原因。
按 K 与 V 的维度推导容量
先采用能清楚复算的假设:标准 Transformer 的全注意力缓存,未分片、未量化,独立请求不共享前缀,各条序列等长。每层 K 与 V 各保存 N 条序列、T 个位置、Hkv 个 KV 头、每头 D 个元素;每元素 B 字节,模型共 L 层。维度在实际张量里可以重排,但元素总数不因此改变。
由图得到 KV bytes = 2×L×Hkv×D×B×T×N。MHA 的 KV 头数通常与 query 头数相同;GQA 让一组 query 头共享一份 KV,Hkv 比 query 头数少;MQA 是一个 KV 头的情形。你要读取模型配置中的实际 KV 头数,不能看到 32 个 query 头就给所有模型代入 32。
同样的 query 维度,缓存可以相差四倍
取 L=32、Hkv=8、D=128、B=2、T=2048、N=8。每层 K/V 合计 2×8×2048×8×128×2=67,108,864 bytes;乘 32 层得到 2,147,483,648 bytes,即 2GiB。
只把 Hkv 从 8 改成 32,元素数增加四倍,得到 8GiB。这两个值是同一假设下的 GQA 与 MHA 对照,不是把运行中的 MHA 模型改一个配置就能无损变成 GQA。架构与权重必须匹配,质量与行为也需验证。
每条序列每新增一个位置,此配置增加 2×32×8×128×2=131,072 bytes,即 128KiB。8 条序列各再缓存 512 个位置,会额外增加 512MiB,默认 2GiB 变成 2.5GiB。权重仍然相同,增长来自请求生命周期;这解释了“能加载”与“能稳定接请求”为什么不同。
实验:预测增长,再计算
先在学习记录中写下三次预测:默认配置;只把 Hkv 改为 32;恢复默认后只把 T 改为 8192。每次保留其余条件。计算器只做上述缓存模型,不调用真实模型,也不判断整张 GPU 能否部署。
默认条件:KV payload 为 2GiB;尚未计入权重、工作区与分配开销。
计算后再核对与排错
三次分别是 2、8、8GiB。若差 4 倍,检查 Hkv 与 query 头数;若差 1024 倍,检查字节与 GiB 转换;若实测更高,先拆出权重、工作区与实际分配。按配置之外的架构计算得到一个数字,不会让模型真的拥有那种缓存结构。
单独把 N 从 8 改到 16,理论 payload 变为 4GiB。不能推断吞吐也会翻倍;第 07、08 单元会加入调度、时延与质量约束。
分页改变分配方式,不改变独立 token 的表示
上式数的是有效缓存元素。现实分配可能为每条请求预留大块连续区域,而请求实际长度难以提前知道;预留太多会浪费空间,释放不同长度区域还可能留下碎片。分页缓存把逻辑序列拆成固定大小的块,再由映射表找到物理块,随增长分配,不要求一条序列始终占据一整段连续物理空间。
它能减少某些预留和碎片浪费,但在精度、头数、维度相同的条件下,一个独立 token 的 KV 元素数不会因为“分页”这个词变小。例如用每块 16 个位置管理上面的缓存:一条长度 18 的序列需要两个块,容量为 32 个位置。有效 payload 为 18×128KiB=2.25MiB,块容量为 4MiB;末块仍有 14 个空位置,另有映射元数据。这个简化例不等于实际引擎的全套分配器。
前缀共享解决的是另一个问题:完全匹配且可复用的前缀状态不必在每个请求中重复存储。复用必须与模型、token 序列、位置及相关执行条件匹配,不能凭自然语言“意思差不多”共享 KV。新权重或不同适配器可能让旧状态失效;也不能把缓存命中样本混入冷缓存测量后,宣称所有请求都同样快。
请求长度不一致时,在理想独立且按实际长度存储的模型中,可把 T×N 换成 ∑Tᵢ;如果运行时统一按最长序列 padding 或预分配,账本又不同。张量并行还可能分片或复制 KV 头,不能直接把整机容量除以设备数。滑动窗口、MLA、混合状态模型等需各自的存储模型,本课公式不替它们作保证。
交付:一份会随请求增长的内存账
- 复算与对照。记录 GQA 默认 2GiB、MHA 对照 8GiB及各增加 512 位置后的变化。解释减少 Hkv、减少精度字节和减少并发分别改变哪个因子。
- 容量反例。可用预算为 22GiB,权重 13.04GiB,工作区和运行时预留 3GiB。8 条 GQA 序列的 T 从 2048 长到 8192 后还能按同一假设容纳吗?写出总账和退出条件。
- 陌生分配。每块 16 位置,一条请求有 33 个缓存位置;按默认每位置 128KiB,计算有效 payload、块容量与末块空位。说明为什么分页不等于“每 token 少一半”。
验收与参考
增加 512 位置:GQA 多 0.5GiB,MHA 多 2GiB,因此分别为 2.5 与 10GiB。长到 8192 时 GQA 为 8GiB,总账约 13.04+8+3=24.04GiB,超出 22GiB;需要限制长度/并发、改变表示或提供更多容量并重新测量。33 位置需要 3 块、48 位置,payload=4.125MiB,块容量=6MiB,末块空 15 位置。
怎样算完成:从张量维度推到字节,清楚区分有效 payload 与分配容量;总账保留权重及工作区;至少指出一条公式不覆盖的架构或运行时情况。只有“计算器显示 2GiB”不满足验收。
读资料时,分别检查“表示”与“管理”
Transformers · How caching works:先找每层 K/V 如何追加;读后画一次 forward 前后的缓存长度,并说明新采样 token 何时形成自身 KV。GQA 论文:关注 query 头与 KV 头的关系,用配置维度复核本课的四倍差异。不要把架构转换视为无须验证的部署开关。
PagedAttention 论文:带着“有效元素与预留容量哪里不同”阅读内存管理部分。读后用 18 位置的例子解释块分配,再写出共享与分页分别减少哪种重复或浪费。资料核对于 2026-10-04;论文中的硬件与负载结果不能直接迁移为你环境的速度承诺。
下一步:这些状态怎样共享设备时间?
你现在知道每个请求占多少状态、状态怎样增长,却还不知道请求应当怎样排队、何时插入执行以及何时退出。第 07 单元把连续批处理和分块 prefill 放到可回放的调度 trace 中,检查更高吞吐是否同时满足首字、后续间隔和失败约束。
留下本课的产出
本课产出:带假设的 KV 容量账。写下条件、证据、结果和仍不确定的事。保存不代表自动通过,下面的状态由你按验收条件自检。
记录只保存在当前浏览器。