KV 缓存管理
目的
为什么等待工具执行的智能体仍然占用加速器?
智能体保留在上下文中的每个 token 都会在加速器内存中留下注意力状态,而汇总一个服务节点所承载的各条轨迹后,这些状态的规模会超过模型权重本身。聊天请求会保留这些状态几秒钟,随后将其释放。智能体则会跨越数十轮交互保留这些状态,在探索不同方案时将其分叉,并在测试套件运行或等待人工批准某项操作期间让它们闲置。如果服务系统将每条轨迹都视为一个不透明、按最坏情况预留的整体,就会在大部分预留内存尚未使用时耗尽内存,在每一轮重新计算相同的共享指令,并为保持暂停轨迹的状态就绪而使活跃轨迹得不到足够资源。解决办法已经明确:将注意力状态存放在分支可共享的内存页中,跨轮次复用前缀,并针对每次暂停决定保留、迁移还是丢弃状态。只有智能体运行框架配合,这些措施才能带来收益,因为上下文会增长到多长、前缀是否保持稳定,以及每个工具预计运行多久,都由运行框架决定。用 H·S·A 的术语来说,较长的任务跨度会转化为服务系统必须实际存储的状态,因此,这些状态的成本由存储它们的引擎和塑造它们的运行框架共同决定。
学习目标
- 根据模型结构参数、上下文长度和精度,计算一条轨迹的 KV 内存占用。
- 解释为什么在按最坏情况预留资源时,未知的轨迹长度、分叉和工具执行期间的暂停会造成内存闲置。
- 解释分页块和写时复制如何让各分支共享同一个前缀,而无需复制它。
- 诊断运行框架的哪些决策会提高或破坏跨轮次的前缀缓存命中率。
- 根据预计的工具等待时间,为暂停的轨迹选择保留、卸载或重新计算状态。
- 根据上下文长度、前缀共享程度和工具等待时间,估算一个服务资源池能够支撑多少条并发轨迹。
从上下文 token 到 KV 状态
回到那个在 计算一条轨迹中上下文布局的成本 中已核算过上下文布局成本的编码智能体,此时它已进入第二十轮交互。其上下文包含系统消息和工具定义、任务状态和工作文件,以及最新的工具结果,其中包括上一次测试运行产生的堆栈跟踪,总计约三万二千个 token。智能体刚刚请求运行框架执行集成测试套件,该测试需要四分之三分钟。在这四十五秒内,模型不输出任何内容,但为全部三万二千个 token 计算出的注意力状态仍驻留在加速器上,等待测试日志。上下文工程 决定了此次调用应看到哪些 token。本章追踪这些 token 进入服务系统的过程;在这里,它们成为加速器内存的主要消耗来源。本章将探讨保留、共享和释放这些状态的成本。
基座模型 用一句话定义了键值(KV)缓存。对于上下文中的每个 token,每一层都会保留后续 token 在注意力计算中使用的键向量和值向量,使解码过程无需重新计算它们。其大小仅由模型结构参数决定。一个模型若有 \(L\) 层、\(H_{\text{kv}}\) 个键值头,每个头的维度为 \(d_{\text{head}}\),且每个存储元素占用 \(b\) 字节,那么它为每个 token 保留的字节数为
\[m_{\text{token}} = 2 \cdot L \cdot H_{\text{kv}} \cdot d_{\text{head}} \cdot b \tag{1}\]
其中,系数 2 用于计入键和值两者。将 公式 1 按上下文长度扩展后,一条包含 \(S\) 个 token 的轨迹会持有 \(M_{\text{KV}} = S \cdot m_{\text{token}}\) 字节的状态,其大小随每轮保留的内容线性增长。
模型一旦确定,这个公式就成为一项预算。以一个采用分组查询注意力的 700 亿参数模型为例,其中多个查询头共享一个键值头 (Ainslie 等 2023年):80 层,8 个键值头,每个头的维度为 128,并采用 16 位存储。加速器服务延迟 估算了该模型采用 8 位权重、部署在单个加速器上时的单次调用成本,这是能够容纳该模型的最小部署。同时运行多条智能体轨迹的服务则将 16 位权重分布在一个配备八个加速器的节点上,从而使节点的大部分内存可用于注意力状态;本章所做的容量估算针对的就是这种节点部署。图 1 展示了一个智能体上下文如何映射到这一配置,下面的计算示例则将其转化为并发上限。
粗略估算 0.1: 估算智能体轨迹的注意力状态容量
计算:
每个词元的内存占用为 \(m_{\text{token}} =\) 2 \(\times\) 80 \(\times\) 8 \(\times\) 128 \(\times\) 2 字节 \(=\) 327,680 字节,约为 0.33 MB。
在这三种长度下,每条轨迹的内存占用分别为 2.7 GB、10.7 GB 和 42.9 GB。
16 位权重占用 141.2 GB,运行时为激活和缓冲区预留 20 GB,因此剩余的 KV 池容量为 687.2 GB \(-\) 141.2 GB \(-\) 20 GB \(=\) 526.0 GB。
将池容量除以各自的内存占用,可得并发轨迹数分别为 195、48 和 12。
系统层面的洞见:同一个节点可以容纳 195 个短对话,却只能容纳 12 条代码仓库规模的轨迹。决定一个节点能够服务多少个智能体的是上下文长度,而非算术运算,因此,智能体运行框架在上下文中保留的每个词元都会占用共享且容量固定的内存池。
这一量化事实为推理服务系统确立了严格的容量边界。考虑一个企业级推理节点,它配备 8 个独立加速器,每个加速器拥有 80 GB 高带宽内存,总内存池容量为 \(\text{HBM}_{\text{total}} = 640 \text{ GiB}\)。这个 700 亿参数模型以 16 位精度加载,在整个集群中占用约 \(\text{Mem}_{\text{weights}} \approx 140 \text{ GiB}\) 的内存。额外预留 \(\text{Mem}_{\text{runtime}} \approx 20 \text{ GiB}\),用于预填充前向传播过程中的通信缓冲区、NCCL 暂存空间和激活工作空间后,剩余约 \(\text{Mem}_{\text{kv\_available}} = 480 \text{ GiB}\) 专用于动态注意力缓存。活跃智能体轨迹的理论最大并发上限 \(B_{\max}\),在上下文长度为 \(S\) 时,严格由以下向下取整的商决定:
\[B_{\max} = \left\lfloor \frac{\text{HBM}_{\text{total}} - \text{Mem}_{\text{weights}} - \text{Mem}_{\text{runtime}}}{\text{Mem}_{\text{seq}}} \right\rfloor = \left\lfloor \frac{480 \text{ GiB}}{S \cdot 320 \text{ KiB}} \right\rfloor\]
当智能体序列较短(\(S = 8,192\))时,集群可以轻松承载 \(B_{\max} = \lfloor 480 / 2.5 \rfloor = 192\) 条并发轨迹。但一旦这些智能体摄入复杂的代码库,并运行使上下文增长至 \(S = 131,072\) 的多步执行轨迹,集群的并发能力就会骤降至 \(B_{\max} = \lfloor 480 / 40 \rfloor = 12\) 条活跃轨迹。第十三个并发请求无法被调度;它会立即进入队列,或导致服务引擎因加速器内存耗尽而故障终止。将每个工作集维持在接近其有效大小的运行时(原则 4),其运行状态会更接近短上下文形态。当上下文管理失效或缺失时,服务引擎必须能够承受 131,072 个 token 的情况。
工具等待会进一步加剧这一瓶颈。内存滞留摩擦 量化了同一节点上一个暂停会话所带来的这种压力,即工具等待内存开销。工具运行期间,轨迹的注意力状态闲置在加速器内存中,服务系统必须作出选择:是保留其他轨迹所需的数十 GB 内存,还是迁移或丢弃这些状态,并在观测结果返回时付出重新加载或完整预填充的代价。模型无法作出这一选择。内存是一项资源约束,而不变量闭包原则(不变闭包原则)将资源约束置于模型下方的运行时中,因此这一决策由服务系统负责。引入分页机制后,节 6 将再次讨论这一问题。
图 2 展示了这一困境的物理规模。如 图 2 左图所示,前沿语言模型的上下文窗口在 2018 年至 2026 年间扩大了 4,000 多倍,从 GPT-1 的 512 个 token 增长到 Gemini 1.5 和 2.0 的 2,097,152 个 token。尽管这一扩展使智能体能够摄入整个代码库并保留大量工具交互记录,却直接撞上了右图所示的硬件加速器物理内存上限。以 16 位精度存储时,每个 token 的内存占用为 \(320\text{ KiB}\),采用分组查询注意力 (Ainslie 等 2023年) 的单条 70B 模型轨迹(\(B=1\))在 128,000 个 token 时会固定占用 40 GB 高带宽内存(相当于 NVIDIA H100 加速器内存的一半)。在 1,000,000 个 token 时,这一条轨迹就占用 320 GB 的 KV 缓存——达到整块 H100 GPU 的 80 GB 容量的 4.0 倍,甚至达到下一代 192 GB B200 加速器容量的 1.7 倍。当四个并发智能体在这一上下文长度下运行(\(B=4\))时,KV 内存总需求激增至超过 1.28 TB,令整个多 GPU 集群不堪重负。这道 KV 缓存内存墙使内存分配从后台实现细节转变为制约智能体扩展能力的首要因素,因此必须采用虚拟内存抽象、动态块分页 (Kwon 等 2023年) 和分层卸载。
这两种趋势的交汇揭示了为何物理内存容量已取代算术吞吐量,成为长时程智能体系统的根本制约因素。如 图 2 的右图所示,将上下文长度扩展至一百万个 token 时,每条 70B 模型序列需要 320 GB 的键值缓存内存,是 NVIDIA H100 GPU 的 80 GB 容量的四倍。即使扩展至具有 480 GB 动态内存池的八加速器节点,在这一上下文长度下仅为两个并发智能体提供服务(\(B = 2\),640 GB),就会完全耗尽集群内存。这一瓶颈推动机器学习系统工程采取双管齐下的架构应对措施:通过 PagedAttention (Kwon 等 2023年) 实现运行时虚拟内存分页,以消除物理内存的内部碎片和外部碎片;以及采用多头潜在注意力(Multi-Head Latent Attention,MLA)(DeepSeek 2024年) 等算法创新,将键值投影压缩为低秩潜在向量,使内存占用最多降至原来的 1/5.7。缺少这些系统抽象,在多轮上下文窗口中支持生产环境的智能体工作流,在物理资源和经济成本上仍将难以实现。
如果每条智能体轨迹都具有恒定且可静态预测的序列长度,那么确定这部分内存的大小并对其进行分区,就会简化为一个简单的静态分配问题。运行时可以将其 480 GB 可用高带宽内存(HBM)划分为十二个固定大小为 40 GB 的连续缓冲区,为每个执行槽分配一个缓冲区。然而,在生产环境中,智能体轨迹具有极强的非确定性:一条轨迹可能在 1,200 个 token 内成功并终止,另一条可能遇到编译器语法错误而迭代至 45,000 个 token,第三条则可能陷入递归循环,耗尽整个 131,072 个 token 的窗口。当引擎根据最坏情况下的上下文长度预先分配连续物理内存缓冲区时,绝大多数已分配的加速器内存完全未被写入,处于闲置状态,而新到达的请求却因系统认为容量已耗尽而遭到拒绝。那么,轨迹长度的这种动态变化如何在连续内存分配器中引发灾难性的物理内存碎片化?服务系统又必须引入哪些架构机制,才能消除由此产生的浪费?
内存碎片化
加速器运行时中的连续内存分配器迫使系统在灾难性的内存耗尽与普遍的容量不足之间做出难以维持的运维权衡。推理引擎为键值(KV)缓存分配内存时,必须容纳最终长度本质上无法预测的序列。生成自回归序列的模型无法预先声明其终止位置;生成何时结束,完全取决于采样得到的输出 token、序列结束 token 的出现,或智能体运行框架施加的外部终止条件。然而,传统 GPU 张量库要求采用稠密、连续的内存分配,才能高效执行矩阵—向量乘法。动态且不可预测的逻辑增长与刚性连续物理分配之间的这种不匹配,构成了现代机器学习服务系统内存管理的核心困境。
可变序列长度使朴素的连续 KV 缓存分配成为严重内存浪费的根源,导致加速器 HBM 中 60% 至 80% 的容量滞留在未被引用的预留空间和无法分配的物理空洞中。 在多轮智能体工作负载中,序列生命周期会被外部环境调用打断,生成预算也变化极大,因此固定预留和连续块分配器会迅速导致有效服务吞吐量骤降。运行时因认为容量已耗尽而拒绝新到达的请求,而物理内存总线却报告仍有数十 GB 名义上未被写入的空间。要理解这种浪费的结构性根源,就必须考察内存如何在动态推理生命周期中被配置、划分并滞留。
静态超额预留的局限
管理 KV 缓存内存最早且最直接的策略,是按照最坏情况进行静态预留。在这种方式下,服务引擎为每条获准进入系统的序列分配一个连续物理缓冲区,其大小足以容纳模型架构或部署配置所支持的最大可能序列长度 \(S_{\max}\)。如果加速器承载的模型配置了 \(S_{\max} = 32{,}768\) 个 token 的上下文窗口,引擎就会在请求到达时立即为恰好 \(32{,}768\) 个键向量和值向量预留内存,此时距离执行第一个解码步骤还很早。
分配悖论 在连续内存架构中,分配器要么预先按最坏情况下的序列长度超额预留内存,使内存滞留在未使用的预留空间中;要么在运行时动态调整缓冲区大小,导致加速器总线上代价极高的内存复制和同步停顿。
静态预留消除了生成期间的动态内存管理。由于每个请求都拥有不可变的预分配内存区域,运行时在解码过程中无需调用 GPU 内存分配函数、执行流同步或调整指针。token 位置 \(t\) 对应的键槽和值槽的物理地址,可以直接通过统一的步长运算计算得出。从运维角度看,静态预留保证了获准进入系统的请求不会在生成中途发生内存不足(OOM)故障。
这种运维简便性所付出的系统代价,是极其严重的利用率不足。在实际工作负载中,序列长度并不均匀,而是服从高度偏斜的分布。令随机变量 \(S \in [1, S_{\max}]\) 表示获准进入系统的请求的总序列长度,其中包括提示 token \(S_{\text{prompt}}\) 和生成的输出 token \(S_{\text{gen}}\),其分布由经验概率质量函数 \(P(S = s)\) 描述。分配给一个请求的物理内存固定为 \(M(S_{\max}) = S_{\max} \cdot m_{\text{token}}\) 字节,其中 \(m_{\text{token}}\) 表示每个 token 在所有层中的键状态和值状态合计占用的字节数。一条已完成序列所消耗的有效内存的期望值为:
\[\mathbb{E}[M_{\text{useful}}] = m_{\text{token}} \sum_{s=1}^{S_{\max}} s \cdot P(S = s)\]
静态预留下的期望内存分配效率 \(\eta_{\text{static}}\),由有效内存期望值与最坏情况下预留内存之比给出:
\[\eta_{\text{static}} = \frac{\mathbb{E}[M_{\text{useful}}]}{M(S_{\max})} = \frac{1}{S_{\max}} \mathbb{E}[S]\]
当智能体运行时为代码补全、对话轮次交互和工具调用等混合任务提供服务时,序列长度的中位数 \(\text{Med}(S)\) 通常仅占 \(S_{\max}\) 的很小一部分。对于配置为 \(S_{\max} = 32{,}768\) 个 token、已完成轨迹平均消耗 \(\mathbb{E}[S] = 2{,}048\) 个 token 的应用,期望分配效率低于 \(6.25\%\)。已分配物理内存中剩余的 \(93.75\%\) 完全未被写入,只是为永远不会被采样的 token 保留着。由于加速器内存具有严格的容量上限,这种人为增大的单请求内存占用将并发限制为少数几条并发流,从而压低请求的总体吞吐量。
另一种工程方法尝试采用动态连续重分配,仿照 C++ 的 std::vector 等可变容量数组结构的行为。运行时首先分配一个与提示长度 \(S_{\text{prompt}}\) 相匹配的小型连续缓冲区。当自回归循环生成的 token 超出当前容量时,引擎会分配一块更大的连续内存区域,将已累积的 KV 缓存条目从旧缓冲区复制到新缓冲区,并释放原有分配。
图 3 展示了动态扩容的时间线和内存开销,涵盖三个独立的执行阶段。在步骤 1 中,容量为 \(N\) 的初始缓冲区一旦耗尽,主机运行时便调用 cudaMalloc,分配一个扩容后具有 \(2N\) 个槽位的缓冲区,引入控制平面同步延迟,阻塞内核调度。在步骤 2 中,运行时发起经由内部 HBM 总线的设备到设备内存复制,将历史键值投影转移到新的内存区域;在此传输窗口内,具有 \(N\) 个槽位和 \(2N\) 个槽位的两个缓冲区必须同时驻留在设备内存中,导致内存用量相对于初始基线出现短暂的 \(3.0\times\) 激增。最后,在步骤 3 中,调用 cudaFree 会释放已不再使用的、具有 \(N\) 个槽位的缓冲区,留下一个孤立的外部碎片空洞;若不对全部内存进行紧缩,该空洞便无法合并。
这种动态扩容范式在大规模并行加速器上执行时,会引发严重的系统病态行为。首先,调用 cudaMalloc 和 cudaFree 等内存管理例程会产生显著的控制平面延迟。这些例程属于重量级操作,会使设备流串行化,触发主机与设备之间的同步,并扰乱异步内核调度流水线。其次,经由高带宽内存(HBM)复制高维张量状态会消耗内存总线带宽,而这些带宽原本可用于解码阶段受内存带宽限制的矩阵—向量乘法。第三,在重分配窗口期间,服务引擎必须同时保留旧内存缓冲区和新内存缓冲区,造成短暂的内存消耗峰值,往往恰好触发该策略原本试图避免的内存不足崩溃。
连续内存浪费
静态预留和动态连续分配的失败,均源于经典操作系统内存病态行为在加速器的极端约束下发挥作用。KV 缓存中的内存浪费可明确划分为两类架构问题:内部碎片和外部碎片(表 1)。
| 碎片维度 | 主要架构根因 | 在连续 KV 运行时中的表现 | 朴素分配器的缓解措施为何失效 |
|---|---|---|---|
| 内部碎片 | 粗粒度的过量分配和按最坏情况预留的缓冲区。 | 当前生成步骤 \(t\) 与最大序列容量 \(S_{\max}\) 之间的预留内存槽位。 | 减少预留容量会提高生成过程中因内存不足而中止的概率。 |
| 外部碎片 | 生命周期不同的连续内存区域导致内存呈棋盘式交错分布。 | 空闲物理内存片段散布在活跃缓冲区之间,无法容纳新的连续内存请求。 | 内存紧缩需要同步复制全部缓存的 GPU 张量,从而阻塞正在进行的生成。 |
| 瞬时重复占用 | 连续重分配和几何增长策略。 | 动态缓冲区复制和扩容期间,物理内存占用发生重叠。 | 摊销式倍增减少了复制频率,却增加了瞬时预留峰值。 |
当存储分配器以固定增量向某个实体分配内存,且分配量超过该实体当前运行所需的容量时,就会产生内部碎片。在连续 KV 缓存架构中,内部碎片表现为两种不同形式:预留浪费和几何增长余量。
预留浪费是指当前已写入的序列长度 \(t\) 与预留边界 \(S_{\text{alloc}}\) 之间的差额。即使分配器使用预测启发式方法而非 \(S_{\max}\) 来动态确定缓冲区大小,任何满足 \(S_{\text{alloc}} > t\) 的分配仍会留下 \(S_{\text{alloc}} - t\) 个无法供其他并发请求使用的槽位。由于标准张量内核要求物理地址连续,活跃序列尾部尚未写入的槽位无法分配给独立的序列。
当运行时试图通过按几何级数扩展缓冲区来降低重新分配频率时,就会产生几何扩容余量——例如,每当达到容量边界时,就将缓冲区容量翻倍。一个请求在生成到 \(1{,}025\) 个 token 时结束,而其所在缓冲区的容量为 \(2{,}048\) 个 token,因此会留下 \(1{,}023\) 个 token 的内部余量。在包含数十个并发请求的批次中,几何扩容余量会持续导致已分配内存池容量中 \(25\%\) 至 \(33\%\) 的部分闲置。
随着到达时间和完成长度各不相同的请求不断分配和释放连续内存块,外部碎片会逐渐形成。考虑一个初始化为统一连续内存池的加速器内存空间。
这种碎片问题在空间上的表现如 图 4 所示。最初,运行时满足不同大小的连续内存分配需求:请求 \(A\) 占用 \(4\text{ GiB}\),请求 \(B\) 占用 \(1\text{ GiB}\),请求 \(C\) 占用 \(6\text{ GiB}\)。注意,在活跃缓冲区内部,预留空间浪费已经十分严重——请求 \(A\) 仅使用 \(1.8\text{ GiB}\),其预留空间为 \(4\text{ GiB}\);而请求 \(C\) 仅使用 \(2.4\text{ GiB}\),其分配空间为 \(6\text{ GiB}\)。当请求 \(B\) 离开时,其 \(1\text{ GiB}\) 内存块会返回空闲链表。然而,当新请求 \(D\) 到达并需要一段连续的 \(2\text{ GiB}\) 内存区域时,它无法占用大小为 \(1\text{ GiB}\) 的空洞,该空洞由 \(B\) 释放。即使空闲内存池中的可用内存总量达到 \(5\text{ GiB}\)(中间空洞中的 \(1\text{ GiB}\) 加上尚未分配的尾部区域中的 \(4\text{ GiB}\)),请求 \(D\) 仍会立即遭遇分配停滞,因为没有任何单个连续内存段足够大,能够满足其预留请求。
随着数百个请求在服务引擎中不断进入和退出,物理地址空间会碎片化为已分配区域与不连续空闲区间交替分布的棋盘状格局。最终,系统会陷入一种病态状态:所有空洞中的空闲物理内存总量可能达到 \(20\) GB,但一个需要连续 \(4\) GB 缓冲区的新请求仍会被拒绝,因为最大的单个连续内存块仅有 \(1.5\) GB。
在传统的中央处理器(CPU)操作系统中,用户空间的外部碎片由硬件内存管理单元(MMU)屏蔽。操作系统将不连续的 \(4\) KB 物理页框映射到连续的虚拟地址空间,使进程能够以连续方式访问内存,而无需物理内存连续。加速器架构以往缺乏类似的、用于张量执行的硬件管理按需分页机制。执行缩放点积注意力的 CUDA 内核在计算指针偏移时,假定连续内存各维度上的步长是规则的:
\[\text{Offset}(l, h, t, d) = l \cdot \text{stride}_L + h \cdot \text{stride}_H + t \cdot \text{stride}_T + d \cdot \text{stride}_D\]
如果 token 维度 \(t\) 分散在不连续的物理内存区域中,这种步长运算就会失效,产生错误的指针引用,除非显式重写底层注意力内核,使其能够通过地址转换表进行寻址。
在连续内存分配模式下,解决外部碎片需要进行内存紧缩:暂停推理流水线,在物理 HBM 中搬移活跃的 KV 张量,将相邻的空闲空洞合并为一个连续区域,并更新所有内部张量指针。在高性能加速器上,内存紧缩的开销高得难以承受。通过内存总线搬移数十 GB 的 KV 数据会引入毫秒级延迟,违反实时服务级别目标(SLO),并使并行计算的线程束停滞。因此,朴素的服务引擎完全避免内存紧缩,任由外部碎片不断增长,直到引擎被迫限制请求接纳。
轨迹中的内存频繁分配与释放
静态对话请求的序列长度呈现出适度的变化,而智能体工作负载会将内存碎片化放大到极端程度。自主智能体并不执行不间断的线性生成;它们在迭代的推理、执行和验证循环中运行。智能体轨迹的特点是内存分配与释放频繁、完成时点难以预测,以及计算长时间暂停(多轮上下文增长与内存分配时间线)。
| 轨迹轮次 | 操作 | 本轮词元数(提示 / 生成) | 累计上下文 | 环境等待与状态 |
|---|---|---|---|---|
| 第 1 轮 | 生成计划 | 400 / 150 | 550 个词元 | 已调用编译器;进程空闲 4.2 秒 |
| 第 2 轮 | 错误诊断 | +1,200(stderr) / 80 | 1,830 个词元 | 已调用测试运行器;进程空闲 12.0 秒 |
| 第 3 轮 | 补丁重构 | +3,500(代码库) / 850 | 6,180 个词元 | 正在验证多文件编辑 |
考虑一个执行自动化软件修复的智能体的内存占用。在第一轮中,智能体根据包含 \(1{,}000\) 个词元的提示生成一条简单的补丁命令,生成 \(120\) 个词元。随后,它在沙箱容器内调用外部编译器工具。在编译器执行并返回输出所需的四秒内,智能体进程不产生任何词元。
在连续内存系统中,服务运行时在处理这条暂停的轨迹时面临严峻的两难选择。如果引擎在工具执行期间始终将智能体的连续 KV 缓存保留在 HBM 中,就会占住物理内存,使其他活跃请求无法使用这些内存。更糟糕的是,如果引擎为预期的后续轮次预先分配了充裕的连续缓冲区,那么整块预留空间都会闲置并被锁定。如果引擎转而释放连续缓冲区,为其他请求腾出空间,就必须丢弃全部已累积的状态,导致编译器返回错误日志后,必须执行代价高昂的完整上下文预填充重计算。
智能体工作负载的完成长度分布天然具有多峰特征。简单查询在工具确认某个前置条件后几乎立即终止(\(S < 500\))。复杂查询则会遭遇递归性失败,生成堆栈跟踪,重新向模型提交提示,并不断累积上下文,直到序列接近物理窗口边界(\(S > 30{,}000\))。
当短生命周期轨迹与长生命周期轨迹在同一个物理内存池中并发执行时,分配与释放的频率会急剧上升。短生命周期请求快速分配并释放小块内存,在内存空间中留下局部空洞。与此同时,生命周期长、容量需求高的智能体请求会锁定大块连续内存,每次持续数分钟。由此形成的内存拓扑构成了连续内存分配器最恶劣的运行环境:分配大小不可预测,释放时机不确定,两者交错发生,而所在地址空间既不允许物理内存不连续,也无法进行低成本的内存紧缩。
量化可用容量:KV 分配效率指标
为了严谨地诊断和比较服务运行时中分配器的性能,我们必须形式化描述物理内存分配与有效词元容量之间的关系。令 \(M_{\text{pool}}\) 表示加速器上分配给 KV 缓存子系统的物理内存总容量,以字节为单位。该内存池的容量通常通过从设备物理内存总量中扣除静态模型参数权重 \(M_{\text{weights}}\) 和预填充矩阵乘法所需的临时激活内存 \(M_{\text{act}}\) 得到,其中设备物理内存总量为 \(M_{\text{device}}\):
\[M_{\text{pool}} = M_{\text{device}} - M_{\text{weights}} - M_{\text{act}}\]
在服务引擎运行期间的任意离散时间步 \(t\),令 \(\mathcal{R}(t)\) 表示当前已获准进入系统的所有活跃请求的集合。对于每个请求 \(i \in \mathcal{R}(t)\),令 \(s_i(t)\) 表示截至此时已处理或生成的有效词元数。用于存储仍然有效且参与注意力计算的键值表示的有效内存总量 \(M_{\text{useful}}(t)\) 定义为:
\[M_{\text{useful}}(t) = m_{\text{token}} \sum_{i \in \mathcal{R}(t)} s_i(t)\]
相应地,令 \(A_i(t)\) 表示分配器为请求 \(i\) 在时刻 \(t\) 实际预留的物理内存区域大小,以词元槽位数计。为活跃请求分配的已分配内存总量 \(M_{\text{allocated}}(t)\) 为:
\[M_{\text{allocated}}(t) = m_{\text{token}} \sum_{i \in \mathcal{R}(t)} A_i(t)\]
内存池中的剩余内存构成尚未分配的物理容量,记为 \(M_{\text{free}}(t) = M_{\text{pool}} - M_{\text{allocated}}(t)\)。在连续内存分配器中,这部分空闲容量分散在 \(K\) 个离散且互不连续的空闲内存空洞中:
\[M_{\text{free}}(t) = \sum_{k=1}^{K} H_k(t)\]
其中,\(H_k(t)\) 为第 \(k\) 个连续空闲内存段的大小,以字节为单位。
我们使用三个归一化、无量纲的指标来评估系统的内存健康状况。首先,分配效率 \(\eta_{\text{alloc}}(t)\) 衡量当前已提交内存中用于存储实际 token 状态,而非内部预留空闲空间的比例:
\[\eta_{\text{alloc}}(t) = \frac{M_{\text{useful}}(t)}{M_{\text{allocated}}(t)} = \frac{\sum_{i \in \mathcal{R}(t)} s_i(t)}{\sum_{i \in \mathcal{R}(t)} A_i(t)}\]
其次,外部碎片率 \(\Phi_{\text{ext}}(t)\) 量化未分配的物理内存因碎片过小而无法利用的程度:这些碎片无法满足需要大小为 \(R_{\text{req}}\) 字节的连续缓冲区的新请求:
\[\Phi_{\text{ext}}(t) = 1 - \frac{\sum_{k: H_k(t) \ge R_{\text{req}}} H_k(t)}{M_{\text{free}}(t)}\]
当所有空闲内存都位于单个完整的连续区域中时,\(\Phi_{\text{ext}}(t) = 0\)。当空闲内存被分割得没有任何单个空闲块能够容纳 \(R_{\text{req}}\) 时,\(\Phi_{\text{ext}}(t) = 1\),这表示完全的外部碎片化;即使 \(M_{\text{free}}(t) > 0\),分配器也必须拒绝新的任务。
最后,有效 KV 缓存占用率 \(\eta_{\text{effective}}(t)\) 定义了物理 KV 内存池总容量中用于有效工作的绝对比例:
\[\eta_{\text{effective}}(t) = \frac{M_{\text{useful}}(t)}{M_{\text{pool}}}\]
粗略估算 0.2: 量化 NVIDIA H100 上连续 KV 缓存的碎片化
参数与 KV 内存占用: 模型参数占用: \[M_{\text{weights}} \approx 70 \times 10^9 \times 2 \text{ bytes} \approx 140 \times 10^9 \text{ bytes} \approx 130.38 \text{ GiB}\] 由于权重超出单块 GPU 的容量,模型通过张量并行分布在两块 H100 GPU 上(\(TP=2\))。在每块 GPU 上: \[M_{\text{weights, per-GPU}} = \frac{130.38 \text{ GiB}}{2} = 65.19 \text{ GiB}\] 预留 \(4.81 \text{ GiB}\) 用于激活工作内存(\(M_{\text{act}}\))及 CUDA 运行时开销后,剩余容量为: \[M_{\text{pool}} = 80.00 - 65.19 - 4.81 = 10.00 \text{ GiB} \quad (10{,}737{,}418{,}240 \text{ bytes})\]
在一块 GPU 的本地分片上(\(H_{\text{kv, local}} = 8 / 2 = 4\) 个头),每个 token 的键值状态内存占用为: \[m_{\text{token}} = 2 \times L \times H_{\text{kv, local}} \times d_{\text{head}} \times P = 2 \times 80 \times 4 \times 128 \times 2 = 163{,}840 \text{ bytes/token} \approx 160 \text{ KiB/token}\]
场景 A:静态最坏情况预留: 运行时配置可容纳 \(S_{\max} = 16{,}384\) 个 token 的连续缓冲区。 为单个请求预留所需的内存为: \[A_{\text{static}} = 16{,}384 \times 160 \text{ KiB} = 2{,}684{,}354{,}560 \text{ bytes} = 2.50 \text{ GiB}\] GPU 能够接纳的最大并发批大小为: \[B_{\max} = \left\lfloor \frac{M_{\text{pool}}}{A_{\text{static}}} \right\rfloor = \left\lfloor \frac{10.00 \text{ GiB}}{2.50 \text{ GiB}} \right\rfloor = 4 \text{ requests}\]
假设活跃的智能体工作负载呈现一种经验序列长度分布,其平均长度为 \(\mathbb{E}[S] = 2{,}048\) 个 token。当四个请求均以平均长度运行时: \[M_{\text{useful}} = 4 \times (2{,}048 \times 160 \text{ KiB}) = 1.25 \text{ GiB}\] \[M_{\text{allocated}} = 4 \times 2.50 \text{ GiB} = 10.00 \text{ GiB}\] \[\eta_{\text{alloc}} = \frac{1.25 \text{ GiB}}{10.00 \text{ GiB}} = 12.5\%\] \[\eta_{\text{effective}} = \frac{1.25 \text{ GiB}}{10.00 \text{ GiB}} = 12.5\%\] 可用 KV 缓存容量中,高达 \(8.75 \text{ GiB}\)(\(87.5\%\))的容量完全闲置,被困于内部预留碎片中。并发数被限制为四,导致服务吞吐量急剧下降。
场景 B:存在外部碎片的动态连续分配: 为减少静态浪费,运行时切换为动态连续的二次幂伙伴分配。系统接纳了十个请求,其序列长度各不相同,取自经验分布,实际占用总量为 \(M_{\text{useful}} = 4.20 \text{ GiB}\),分配的缓冲区总量为 \(M_{\text{allocated}} = 6.40 \text{ GiB}\)(\(\eta_{\text{alloc}} = 65.6\%\))。
内存池中剩余的空闲内存为 \(M_{\text{free}} = 10.00 - 6.40 = 3.60 \text{ GiB}\)。然而,由于此前请求的到达和完成,这 \(3.60 \text{ GiB}\) 被分散在五个彼此隔离、不连续的内存段中: \[\{H_k\} = \{512 \text{ MiB}, \ 256 \text{ MiB}, \ 1024 \text{ MiB}, \ 768 \text{ MiB}, \ 1024 \text{ MiB}\}\]
第十一个请求到达,其提示词包含 \(4{,}096\) 个 token,需要初始预留 \(1{,}536 \text{ MiB}\)(\(1.50 \text{ GiB}\))。
尽管系统拥有 \(3.60 \text{ GiB}\) 的空闲物理内存,但最大的单个连续内存区间仅为 \(\max(H_k) = 1{,}024 \text{ MiB} < 1{,}536 \text{ MiB}\)。 该请求的外部碎片率为: \[\Phi_{\text{ext}} = 1 - \frac{0}{3.60 \text{ GiB}} = 100\%\] 该请求被拒绝,或在准入队列中阻塞。尽管 HBM 中实际空闲的内存超过所需内存的两倍,分配器仍无法满足连续内存预留需求。
对生产环境推理服务的实证评估证实了这些分析所揭示的局限。在对大语言模型(LLM)服务系统的开创性特征分析中,Kwon 等 (2023年) 表明,在服务于对话模型和代码生成模型的生产工作负载中,传统服务运行时的有效内存占用率 \(\eta_{\text{effective}}\) 仅介于 \(20\%\) 和 \(40\%\) 之间。加速器内存中剩余的 \(60\%\) 至 \(80\%\) 完全损失于内部预留冗余和外部碎片空洞。
Checkpoint 0.1: 评估注意力缓存中的内存碎片
在分析 KV 缓存的分页虚拟内存抽象之前,请检验你对物理内存分配局限的理解:
分页 KV 分配
每个面对不可预测的请求生命周期的动态内存分配器,都会遇到早期分时操作系统中已指出的经典虚拟内存困境:强制动态增长的数据结构占用连续物理地址,必然迫使系统在预先过量预留与外部内存碎片之间作出不可接受的权衡。当推理运行时承载数十个并发生成请求时,为每个序列的键值(KV)投影分配连续内存块,会使加速器的高带宽内存(HBM)变成布满不可用空隙的破碎网格。由于自回归解码每次输出一个 token,直到出现不可预测的终止 token,运行时无法预知一条轨迹会在十二个 token 后终止,还是会扩展到八千个 token。尝试让序列原地增长的连续分配器,要么预先按最坏情况预留上下文窗口,导致多达 80% 的加速器内存闲置;要么周期性地重新分配内存,并通过内部总线实际复制数 GB 的张量状态,以合并出连续内存区间。
分页 KV 分配通过将操作系统中虚拟分页这一基础抽象直接引入加速器内存层次结构,解决了这一物理内存困境 (Kwon 等 2023年)。服务引擎将逻辑 token 序列与其物理存储布局解耦,把 KV 缓存划分为大小统一且固定的物理内存块,分散存储在不连续的 HBM 页框中。每个序列的块表在每个生成步骤中动态地将逻辑序列位置映射到物理块地址,从而完全消除外部碎片,并将内部浪费限制在最后一个块末尾未使用的槽位内。关键在于,这一间接寻址层通过写时复制,将分支轨迹(例如思维树探索、并行候选生成和工具辅助推演)所需的操作,从代价高昂的张量深拷贝转变为常数时间的、采用引用计数的指针操作。
Definition 0.1: 分页注意力 KV 缓存块表
分页注意力 KV 缓存块表 是一种对硬件进行抽象的页映射结构 \(T: l \mapsto p\),将逻辑词元序列索引 \(l = \lfloor t/B \rfloor\) 转换为非连续物理张量页框 \(p \in [0, N_{\text{pool}}-1]\),这些页框驻留在加速器的高带宽内存(HBM)中。
- 意义:将智能体轨迹中动态、非确定性的词元扩展与固定的物理内存分配解耦,消除外部碎片,并通过写时复制(CoW)实现零拷贝树分支。
- 区别:连续张量分配会预先为最坏情况下的上下文长度 \(S_{\max}\) 预留空间,使高达 80% 的加速器内存闲置于未写入的预留区域;与之不同,分页块表按需分配固定大小的物理块(\(B\) 个词元),并将内存浪费严格限制为最后一个块的内部碎片 \(\mathbb{E}[\Delta_{\text{tail}}] = (B-1)/2\)。
- 常见误区:选择的块大小 \(B\) 过小(\(B < 8\)),会增加块表间接寻址元数据,并在 GEMV 内存合并访问过程中导致线程束分歧;块大小过大(\(B > 64\)),则会加剧内部碎片,并使分支轨迹采样之间的共享粒度变粗。
虚拟内存类比: 在经典操作系统中,页表将连续的虚拟地址映射到非连续的物理 DRAM 页框。在 PagedAttention 中,块表将连续的词元序列位置 \(t \in [0, S-1]\) 映射到从设备端 HBM 块池中分配的非连续物理张量缓冲区。
PagedAttention 虚拟内存模型
PagedAttention 的核心抽象是将生成词元的逻辑序列与其对应的键和值投影的物理存储分离。在传统推理引擎中,请求的 KV 缓存表示为一个形状为 \([2, L, H_{\text{kv}}, S_{\max}, d_{\text{head}}]\) 的单体连续张量,其中 \(L\) 表示 Transformer 层数,\(H_{\text{kv}}\) 表示键值头数,\(S_{\max}\) 表示最大序列长度,\(d_{\text{head}}\) 表示每个头的隐藏维度。在 PagedAttention 中,物理内存池则在运行时初始化阶段预先分配为一组大小一致、非连续的物理块。
每个物理块存储固定数量的词元 \(B\) 的键向量和值向量,这一数量称为块大小。形式上,物理块 \(p\) 为恰好 \(B\) 个词元在所有层和所有头上的键值数据预留存储空间:
\[M_{\text{block}} = 2 \cdot L \cdot H_{\text{kv}} \cdot B \cdot d_{\text{head}} \cdot b_{\text{elem}}\]
其中,\(b_{\text{elem}}\) 是每个张量元素的字节精度(FP16 或 BF16 通常为 2 字节,FP8 为 1 字节)。注意力状态的大小 推导了底层 \(m_{\text{token}} = 2 L H_{\text{kv}} d_{\text{head}} b\) 张量的几何结构,并比较了多头架构与分组查询架构的注意力缓存内存占用。当新到达的请求以提示长度 \(S_{\text{prompt}}\) 开始执行时,服务运行时不会为理论上限 \(S_{\max}\) 分配空间,而是查询集中式物理块管理器,仅分配存储初始提示所需的最少块数:
\[N_{\text{blocks}} = \left\lceil \frac{S_{\text{prompt}}}{B} \right\rceil\]
序列中的逻辑词元位置通过一种称为块表的间接寻址结构映射到物理内存地址。对于任意逻辑词元索引 \(t \in [0, S-1]\),其位置均可唯一分解为逻辑块编号 \(l\) 和块内词元偏移量 \(o\):
\[l = \left\lfloor \frac{t}{B} \right\rfloor, \qquad o = t \pmod B\]
运行时为每个序列维护一个块表 \(T: l \mapsto p\),将逻辑块索引 \(l\) 映射到加速器内存中的物理块标识符 \(p\)。在解码阶段,当模型生成词元 \(x_t\),且当前步骤为 \(t\) 时,服务运行时计算 \(l\) 和 \(o\)。若 \(o \neq 0\),则将该词元的键和值投影直接写入已分配的物理块 \(T[l]\) 中偏移量为 \(o\) 的位置。若 \(o = 0\),则前一个物理块已完全填满;运行时拦截分配缺页事件,从其空闲列表环形缓冲区中弹出一个空闲物理块标识符 \(p_{\text{new}}\),将 \(p_{\text{new}}\) 追加到块表中,使 \(T[l] = p_{\text{new}}\) 成立,并将投影写入偏移量为 0 的位置。
这种虚拟地址转换和写时复制(CoW)分支机制详见 图 5。逻辑 token 流被划分为大小一致的块,每块包含 \(B=4\) 个 token。运行时为序列 A 和序列 B 维护独立的块表,将逻辑块索引映射到加速器 HBM 池中分散且不连续的物理块。当序列 B 从序列 A 分叉以探索另一种智能体工具调用时,两个序列最初指向相同的物理分配:逻辑块 0 映射到物理块 7,逻辑块 1 映射到物理块 3,其引用计数均递增至 \(\text{ref\_count} = 2\)。当序列 B 随后在逻辑块 2 中生成与序列 A 不同的 token 时,内存管理器拦截该写入,并分配一个新的物理块(物理块 12)供序列 B 独占使用,同时保持序列 A 的逻辑块 2 映射到物理块 1。这种零拷贝共享使思维树探索和多轮提示能够即时产生分支,而无需复制历史注意力状态。
在不连续的物理块上执行缩放点积注意力,需要专门的 GPU 解码内核。在 FlashAttention 等标准注意力内核中,线程束通过沿连续内存地址线性步进来计算内积。在 PagedAttention 中,解码内核接收序列的块表作为辅助输入参数。在键值收集阶段,线程束将块表中的物理块指针加载到片上共享内存或寄存器中。线程束遍历逻辑块 \(0 \le l \le \lfloor (S-1)/B \rfloor\),从不连续的 HBM 位置读取相应的物理块,并计算它们相对于当前查询向量 \(q_t\) 的注意力权重。PagedAttention 虚拟内存抽象层次 形式化描述了逻辑序列维度到物理内存和控制硬件结构的映射。
| 组件 | 逻辑维度 | 物理表示 | 硬件位置 |
|---|---|---|---|
| token 序列 | 索引 \(t \in [0, S-1]\) | 离散 BPE 整数 token 流 | 主机 DRAM / 智能体上下文 |
| KV 存储 | 逻辑块 \(l = \lfloor t/B \rfloor\) | 物理块 ID \(p \in [0, N_{\text{pool}}-1]\) | 加速器 HBM 块池 |
| 地址映射 | 数组索引 \(l\) | 块表数组 \(T[l] = p\) | 主机控制平面与 GPU SRAM |
| 块内偏移 | 偏移量 \(o = t \pmod B\) | 字节偏移量 \(o \cdot 2 \cdot H_{\text{kv}} \cdot d_{\text{head}} \cdot b_{\text{elem}}\) | 合并访存的 GPU 缓存行 |
| 空闲内存 | 未分配容量 | 由块 ID 组成的空闲列表栈 / FIFO 队列 | 主机运行时内存管理器 |
通过块表组织内存访问,物理内存分配器无需寻找大于 \(M_{\text{block}}\) 的连续物理空间。由于全局池中的所有块大小相同,任意空闲物理块都能满足任意活跃序列的分配请求,从而彻底消除外部内存碎片。
块大小选择中的量化权衡
分页分配虽然消除了外部碎片,却引入了两种相互制约的系统开销:每个序列最后一个块中的内部碎片,以及指针间接寻址造成的硬件内存访问低效。选择物理块大小 \(B\) 是一个经典的系统优化问题,需要在内存利用率与内存总线吞吐量之间取得平衡。
内部碎片的产生,是因为序列长度并非块大小 \(B\) 的整数倍。对于长度为 \(S\) 的序列,分配的物理块数量为 \(\lceil S / B \rceil\),但最后一个物理块包含未使用的 token 槽位:
\[\Delta_{\text{tail}} = (B - (S \pmod B)) \pmod B\]
假设序列完成时的长度对 \(B\) 取模所得的余数在 \(\{0, 1, \dots, B-1\}\) 上均匀分布,则每个活跃序列中浪费的 token 槽位数的期望恰好为:
$$[_{}] =
对于同一示例长度分布,大小为 {python} PagedWaste.block_small_str 个 token 的块浪费了已分配内存的 {python} PagedWaste.frag_small_pct_str,而大小为 {python} PagedWaste.block_large_str 个 token 的块浪费了 {python} PagedWaste.frag_large_pct_str。此前能够接纳 {python} StaticReservation.reservations_str 个最坏情况下的预留分配的节点,现在可容纳 {python} PagedWaste.conc_paged_str 条平均长度的轨迹,数量约为原来的十六倍。
然而,减小 \(B\) 会降低加速器内存子系统的硬件效率。现代加速器架构(例如 NVIDIA Hopper 或 Blackwell GPU)通过宽内存总线组织高带宽内存访问,最小事务粒度为 32 或 64 字节,并以 128 字节的扇区缓存数据。在解码注意力内核(GEMV)中,同一线程束内的线程协作,使用 128 位内存指令(LDG.E.128)将键向量和值向量从 HBM 流式加载到向量寄存器中。如果 \(B\) 设置得过小,物理块就无法与硬件事务边界对齐。此外,较小的块大小会使块表大小增大至原来的 \(1/B\) 倍。对于长度为 32,768 个 token 的上下文,\(B = 4\) 要求每个序列具有 8,192 个块表条目,仅为跟踪地址指针就会消耗过多的 GPU 寄存器文件空间和片上共享内存,同时还会在块边界处导致频繁的线程束分歧。
# Empirical verification of tail fragmentation versus block size B
def calculate_cache_utilization(batch_size, avg_seq_len, b_size, m_token):
total_tokens = batch_size * avg_seq_len
allocated_blocks = sum((s + b_size - 1) // b_size for s in seq_lens)
allocated_bytes = allocated_blocks * b_size * m_token
used_bytes = total_tokens * m_token
return used_bytes / allocated_bytes # Effective memory efficiency eta实证性能剖析表明,\(B = 16\) 和 \(B = 32\) 处于当前 GPU 微架构的最佳区间。在 \(B = 16\) 时,每个块内的连续张量数据(每个头 \(16 \times d_{\text{head}} \times b_{\text{elem}}\) 字节)足够大,能够充分利用 128 字节的内存总线事务,使解码注意力内核的 HBM 读取带宽达到理论值的 \(95\%\) 以上。同时,尾部浪费被限制在平均每条流 \(7.5\) 个词元的范围内,因此在大规模生产批次中,容量损失可以忽略不计。
粗略估算 0.3: 确定 70B 参数模型的物理块大小
首先,计算单块 GPU 上每个 token 的内存占用。在 \(TP =\) 8 的条件下,\(H_{\text{kv}} =\) 8 个键值头均匀分配到 8 块 GPU 上,因此每块 GPU 的每一层有 \(H_{\text{kv, GPU}} =\) 1 个键值头:
\[m_{\text{token, GPU}} = 2 \cdot L \cdot H_{\text{kv, GPU}} \cdot d_{\text{head}} \cdot b_{\text{elem}} = 2 \cdot 80 \cdot 1 \cdot 128 \cdot 2\text{ bytes} = 40,960\text{ bytes} = 40\text{ KiB}\]
在整个节点上(\(TP=8\)),每个 token 的总内存占用为 \(8 \times 40\text{ KiB} = 320\text{ KiB}\)。
现在,针对块大小 \(B =\) 16 和 \(B =\) 64,评估单块 GPU 上的物理块参数:
物理块占用(\(B =\) 16): \[M_{\text{block, GPU}} = 16 \cdot 40\text{ KiB} = 640\text{ KiB}\]
物理块占用(\(B =\) 64): \[M_{\text{block, GPU}} = 64 \cdot 40\text{ KiB} = 2,560\text{ KiB} = 2.5\text{ MiB}\]
假设每块 GPU 提供 80 GB 的物理 HBM3 内存。模型权重在每块 GPU 上占用 \(140\text{ GB} / 8 = 17.5\text{ GiB}\)。为激活缓冲区预留 2.5 GB 后,恰好剩下 \(60\text{ GiB}\)(\(62,914,560\text{ KiB}\))专用于分页 KV 缓存池。
\(B =\) 16 时的缓存池容量: \[N_{\text{blocks}} = \frac{62,914,560\text{ KiB}}{640\text{ KiB}} = 98,304\text{ blocks}\] \[\text{Token Capacity} = 98,304 \cdot 16 = 1,572,864\text{ tokens per GPU}\]
包含 \(N_{\text{seq}} =\) 256 个并发序列的批次下的尾部碎片:
对于 \(B =\) 16:每个序列的预期浪费量为 \((16 - 1)/2 =\) 7.5 个 token。 \[\text{Node Waste} = 256 \cdot 7.5 \cdot 40\text{ KiB} = 76,800\text{ KiB} = 75\text{ MiB}\] \[\text{Fragmentation Ratio} = \frac{75\text{ MiB}}{61,440\text{ MiB}} \approx 0.12\%\]
对于 \(B =\) 64:每个序列的预期浪费量为 \((64 - 1)/2 =\) 31.5 个 token。 \[\text{Node Waste} = 256 \cdot 31.5 \cdot 40\text{ KiB} = 322,560\text{ KiB} = 315\text{ MiB}\] \[\text{Fragmentation Ratio} = \frac{315\text{ MiB}}{61,440\text{ MiB}} \approx 0.51\%\]
即使批次很大,分页分配也能将内部内存浪费限制在缓存总容量的 \(1\%\) 以下,而连续静态预留通常会造成 \(60\%–80\%\) 的容量损失。
通过写时复制分支实现零复制审慎推理
在智能体架构中,执行过程很少沿着单一的线性轨迹推进。审慎推理 通过生成备选方案来获取证据(原则 2),因此,思维树搜索、推测性候选采样、自洽性评估和工具调用重试都会从一个共享前缀分出多个分支(测试时计算)。一个规划智能体可能先接收包含系统提示、环境规范和工具文档的八千 token 上下文,随后生成四条推测性子轨迹,以探索不同的执行策略。
在连续内存分配模型下,创建分支是一项开销高、延迟大的操作。由于每个子序列都必须将新生成的 token 追加到自身的连续内存区域中,推理服务引擎必须在物理内存中复制父序列的整个 KV 缓存张量。对于上文分析的 70B 参数模型,将一个包含 \(8,192\) 个 token 的前缀深复制到四个子分支,需要在物理内存中复制:
\[\text{Data Duplicated} = 4 \cdot (8,192\text{ tokens} \cdot 320\text{ KiB/token}) = 4 \cdot 2.56\text{ GiB} = 10.24\text{ GiB}\]
通过加速器内存总线写入 \(10.24\text{ GiB}\) 会使推理流水线停顿,浪费稀缺的 HBM 容量,并大幅减少系统能够支持的并发审慎推理 智能体数量。
分页 KV 分配通过引用计数和写时复制(CoW)实现零复制分支,从而消除这项开销。全局池中的每个物理块 \(p\) 都维护一个关联的整数引用计数器 \(\text{ref\_count}[p]\),由主机运行时的内存管理器跟踪。当父序列分出一个或多个子序列时,推理服务运行时执行三项常数时间操作:
表复制: 为每个子序列创建一个新的逻辑块表,并将父序列块表中的物理块标识符复制到子序列的表中: \[T_{\text{child}}[l] \leftarrow T_{\text{parent}}[l] \quad \forall l \in \left[0, \left\lfloor \frac{S_{\text{prefix}}-1}{B} \right\rfloor \right]\]
引用计数递增: 对于子序列表中映射的每个物理块,运行时将其引用计数器加一: \[\text{ref\_count}[T_{\text{child}}[l]] \leftarrow \text{ref\_count}[T_{\text{child}}[l]] + 1\]
元数据注册: 将子序列作为一个活跃的独立请求注册到调度器,其初始序列长度为 \(S_{\text{child}} = S_{\text{prefix}}\)。
关键在于,在这次分支操作期间,没有任何张量数据通过加速器内存总线被复制。父序列与所有子分支的共享前缀都指向完全相同的物理 HBM 块。
# Host-side reference-counted block reclamation
def release_sequence_blocks(block_table, ref_counts, free_list):
for phys_block in block_table:
ref_counts[phys_block] -= 1
if ref_counts[phys_block] == 0:
free_list.append(phys_block) # Constant-time O(1) return to pool
block_table.clear()内存的物理分化通过写时复制(Copy-on-Write)在 token 生成过程中惰性发生。当子分支 \(A\) 生成其第一个独立 token 时,它会写入其最后一个块 \(l_{\text{tail}} = \lfloor S_{\text{prefix}} / B \rfloor\)。如果待写入块的引用计数严格大于一(\(\text{ref\_count}[T_A[l_{\text{tail}}]] > 1\)),直接写入该块就会破坏父分支和兄弟分支的注意力上下文。内存管理器会拦截此次写入,从空闲列表中分配一个全新的物理块 \(p_{\text{new}}\),将共享块中的 \(B\) 个 token 复制到 \(p_{\text{new}}\),递减原始块的引用计数,并更新 \(T_A[l_{\text{tail}}] \leftarrow p_{\text{new}}\)。分支 \(A\) 内的后续生成步骤会继续写入 \(p_{\text{new}}\),此时 \(\text{ref\_count} = 1\),不再触发进一步的复制。
当智能体规划分支被剪枝时,例如编译器验证工具针对候选代码片段返回不可恢复的语法错误时,运行时会销毁该序列。内存管理器遍历被剪枝序列的块表,递减每个映射物理块的引用计数。任何引用计数降至零的块都会立即归还至空闲列表环形缓冲区。如果兄弟分支仍在基于该前缀积极探索其他假设,共享前缀块就会在 HBM 中保持有效,其引用计数仅反映共享程度的降低。
通过将逻辑 token 序列映射到非连续、采用引用计数的物理块,PagedAttention 提供了支持动态、分支化智能体工作负载所需的基础虚拟化层。然而,这一机制完全在正在运行的活跃请求或显式父子分叉的边界内运作。当一个全新的用户请求到达服务集群,并包含一段很长且完全相同的系统提示词时,或者两个互不相关的智能体实例恰好共享完全相同的工具定义前言时,会发生什么?为了避免在没有显式继承关系的独立请求之间重复计算相同的键值投影,服务系统必须从请求内页表扩展到全局前缀索引。
跨轮次前缀缓存
当推理运行时收到的请求,其 token 序列与先前执行的工作负载共享起始前缀时,重新计算这些起始 token 的键和值状态只会造成计算和内存浪费。在自主智能体架构中,这种浪费并非微小的效率损失,而是运行瓶颈。每次调用代码编辑或任务规划智能体时,都会先添加数千个 token 的静态框架内容,包括系统指令、操作边界、少样本示范轨迹以及详细的 OpenAPI 工具模式,然后才引入用户简短且动态变化的查询。如果服务集群将每个传入请求视为孤立的事务单元,加速器的矩阵乘法引擎就必须在每小时数万个请求中,反复将相同的 token 序列投影为键值张量。对于大型语言模型,对包含 4,096 个 token 的系统前言进行预填充会消耗数百太次浮点运算,并独占内存总线带宽,从而增加首 token 延迟(Time to First Token,TTFT),并使请求吞吐量急剧下降。
前缀缓存通过将保留的物理内存块组织为具有全局索引的前缀搜索结构,实现跨独立服务请求的 KV 缓存分配虚拟化。通过按 token 序列前缀为物理 KV 块建立索引,推理引擎将冗余的提示词预填充操作从受计算限制的矩阵乘法转变为常数时间的缓存查找,从而释放加速器容量,用于正在进行的自回归生成。
Radix 前缀树的结构层次及其与内存回收队列的交互如 图 6 所示。每个树节点将一个连续的 token 序列(prefix_tokens)直接绑定到一个有序的物理 PagedAttention 块 ID 列表(block_ids)。根据活跃共享情况,节点被划分为两种运行状态:节点 1(系统提示词指令)和节点 2(Python 编码暂存区)维持活跃的执行流(\(\text{ref\_count} \ge 1\)),并被固定在 HBM 中。相反,节点 3(Bash shell)和节点 4(Git 状态)等已完成的执行分支已降至 \(\text{ref\_count} = 0\),因而进入休眠状态。主机调度器将这些休眠叶节点链接到一个按访问时间戳排序的双向最近最少使用(LRU)淘汰队列中。在内存压力下,物理分配器会安全地从 LRU 队首回收内存(节点 4,将块 31 归还至空闲列表),防止共享父前缀被过早淘汰,并且只有在节点 3 的子指针被剪除后,才会将淘汰资格向上级联至节点 3。
前缀字典树冗余
在单轮对话服务中,时间局部性主要局限于单个独立序列的自回归解码循环。多轮智能体架构打破了这一假设。运行迭代式编辑—编译—测试循环的自主软件工程智能体会生成一系列独立的推理请求,这些请求之间存在大量重叠词元:基础仓库指令、格式化的文件树、当前可用的工具签名,以及截至当前轮次积累的对话历史。此外,基于同一系统提示词实例化的并发工作智能体,即使执行不同任务,也会共享完全相同的初始上下文块。如果通过 PagedAttention 分配的物理内存块在请求结束时立即被释放,服务系统就会丢弃几乎必然在几毫秒内再次被请求的键张量和值张量。
基数树压缩 标准字典树在每条边上存储一个词元,形成深度大且包含大量指针的树结构。基数树将无分支的线性序列压缩为单条边,从而最大限度地减少主机内存遍历开销,并将 BPE 词元数组直接匹配到连续的物理块 ID 数组 (Zheng 等 2024年)。
要保留这些物理块,需要一种索引机制,能够在没有显式父子谱系关系的情况下识别不同请求之间共有的词元序列。以完整提示词字符串为键的朴素哈希映射无法满足这一要求,因为它只能识别完整提示词的精确匹配;对最终查询词元的任何修改都会使整个缓存条目失效。服务引擎需要一种前缀搜索索引,能够识别从词元索引零开始的最长公共子序列。
字典树是用于前缀索引的一种自然数据结构,其中从根节点出发的每条路径都代表一个词元序列。在标准词元级字典树中,每条边代表一个词元标识符,每个节点代表一个唯一前缀。然而,在包含 32,768 个词元的上下文窗口中,为每个词元存储一个专用节点,会带来难以承受的主机 CPU 内存开销,并在查找过程中造成严重的指针追逐问题。由于智能体工作负载由长段不可变的连续文本(如工具定义)组成,其间仅偶尔出现分支点(如可变的工具输出),因此前缀索引主要由无分支的线性链构成。
为了尽量降低树的深度和 CPU 遍历延迟,SGLang (Zheng 等 2024年) 等现代推理运行时将前缀索引实现为基数树(或紧凑转移字典树)。在基数树中,中间没有分支点的边会被压缩为一条边,其中保存一个连续的词元标识符数组。基数树中的每个节点都将这一词元切片直接绑定到一个有序的物理 KV 缓存块标识符数组,这些标识符由加速器的 PagedAttention 内存池维护。
| 节点字段 | 类型 / 表示形式 | 架构功能 |
|---|---|---|
prefix_tokens |
int32[] |
沿边存储的、经压缩的连续 BPE 词元标识符切片。 |
block_ids |
int32[] |
为词元切片提供底层存储的有序物理 PagedAttention 块指针。 |
children |
Map<int32, Node*> |
指向下游分支的指针,以分支的下一个词元为键。 |
parent |
Node* |
指向上游前缀节点的指针(根节点为 null)。 |
ref_count |
uint32 |
当前正在读取此节点或向其追加内容的活跃解码请求数。 |
last_access |
uint64 |
最近一次缓存命中或遍历的单调时钟时间戳。 |
如 表 2 中形式化描述的那样,基数树充当物理块池的权威索引。当请求完成其生成周期时,服务运行时不会将其物理块归还到全局空闲链表。相反,它会将相应叶节点的引用计数降为零,将这些物理块标记为休眠状态。这些块仍然映射在加速器 HBM 中,如果后续请求遍历相同的前缀路径,就可以立即重新关联这些块。
Definition 0.2: Radix 前缀缓存
最长前缀匹配
当一个新请求携带词元化后的提示词 \(\mathbf{t} = [t_0, t_1, \dots, t_{S-1}]\) 到达时,服务运行时会搜索 Radix 树,确定可从缓存中获取的最大前缀长度 \(S_{\text{match}}\)。主机调度器从根节点开始,迭代地将 \(\mathbf{t}\) 的切片与边标签进行匹配。如果某条边与提示词的一个子串匹配,引擎便处理这些词元,记录关联的物理 block_ids,并使用后续词元 \(t_{i}\) 作为查找键,查询子节点映射。当出现词元不匹配或提示词已全部处理完毕时,遍历终止,从而得到最长匹配路径。
虽然在主机 CPU 上执行最长前缀匹配在计算上很简单,但将 \(S_{\text{match}}\) 转换为可用的物理块时,会暴露出硬件导致的量化难题:块边界对齐问题。PagedAttention 将键和值状态组织为大小统一的物理块,块大小为 \(B\)(通常为 \(B = 16\) 或 \(B = 32\) 个词元),以尽量减少外部碎片,并优化张量运算期间的内存总线对齐。只有当块内的词元槽位全部填满,且其内容在数学上已确定、无需再修改时,该块才能在独立请求之间安全共享。
考虑这样一种情形:新请求与已缓存的系统提示词匹配了 \(S_{\text{match}} = 100\) 个词元,而服务引擎采用的块大小为 \(B = 16\)。这 100 个匹配词元跨越:
\[\lfloor 100 / 16 \rfloor = 6 \text{ full blocks (tokens } 0 \dots 95 \text{)}\]
并留下不完整的尾部:
\[100 \pmod{16} = 4 \text{ tokens (tokens } 96 \dots 99 \text{)}\]
这 4 个尾部词元占据某个物理块的前四个槽位,其余 12 个槽位为空。如果服务运行时直接将这个不完整的块分配给新请求,就会立即产生竞态条件。如果新请求开始生成或追加新的查询词元,就会将后续的键和值投影写入槽位 \(4 \dots 15\)。如果另一个并发请求或未来请求匹配同一个 100 词元前缀,但在词元 100 处发生分歧,两个请求就会尝试将相互冲突的键和值张量写入同一物理块的槽位,从而破坏注意力状态。
运行时必须采用以下两种架构策略之一,解决这一不完整块难题:
- 写时复制(CoW)尾部分配:运行时从空闲列表中分配一个新的物理块,通过 PCIe 或高速内存总线,将这 4 个尾部词元的键和值投影从缓存块复制到新块中,并将新块映射到该请求的私有页表。
- 块对齐截断:运行时将可用的缓存命中长度向下取整到最近的完整块边界: \[S_{\text{usable}} = \left\lfloor \frac{S_{\text{match}}}{B} \right\rfloor \times B\] 剩余的 \(S_{\text{match}} \pmod B\) 个词元被视为缓存未命中。运行时将这些尾部词元纳入新增后缀的预填充,与该请求独有的查询词元一起计算其键和值投影。
在生产环境的运行时中,块对齐截断的性能始终优于写时复制分配。在现代加速器架构(如 NVIDIA H100 或 AMD MI300X)上,为复制少量词元而调度内存复制内核所产生的主机开销,加上内存总线同步屏障的开销,超过了在高吞吐量的融合 GEMM 预填充内核中计算这些相同词元所需的时间。因此,生产环境中的 Radix 分配器强制执行严格的块边界不变量:只有大小为 \(B\) 且完全填满的块才会被后续请求继承,而不足一个块的尾部则在初始预填充阶段重新计算。
粗略估算 0.4: 基数树前缀复用带来的 TTFT 改善与计算节省
该智能体的标准系统指令、格式规则和工具定义模式共同构成一个包含 \(S_{\text{prefix}} = 4{,}096\) 个词元的不变前缀。
1. 物理 KV 内存占用: 应用 **离散 token 表示** 中确立的 GQA 张量物理内存占用计算方法(\(m_{\text{token}} = 320\text{ KiB/token}\)): \[M_{\text{KV}} = S_{\text{prefix}} \cdot m_{\text{token}} = 4{,}096 \times 320\text{ KiB} = 1{,}310{,}720\text{ KiB} = 1.25\text{ GiB}\] 通过张量并行将这一内存占用分摊到 8 个 GPU 上后,每个加速器仅需 \(160\text{ MiB}\) 的 HBM。
2. 预填充计算节省: 应用 加速器服务延迟 中基础引擎的 Roofline 模型,计算提示上下文时,线性投影每个词元需要 \(2N\) FLOPs,此外还需计入具有二次复杂度的自注意力部分(\(4 \cdot L \cdot d_{\text{model}} \cdot S_{\text{prefix}}^2\)): \[\text{FLOPs}_{\text{linear}} = 2 \cdot (70 \times 10^9) \cdot 4{,}096 \approx 5.734 \times 10^{14}\text{ FLOPs} = 573.4\text{ TFLOPs}\] \[\text{FLOPs}_{\text{attn}} = 4 \cdot 80 \cdot 8{,}192 \cdot (4{,}096)^2 \approx 4.40 \times 10^{13}\text{ FLOPs} = 44.0\text{ TFLOPs}\] \[\text{FLOPs}_{\text{total}} = \text{FLOPs}_{\text{linear}} + \text{FLOPs}_{\text{attn}} = 573.4 + 44.0 = 617.4\text{ TFLOPs}\]
3. 延迟影响: 这个 8 GPU H100 节点的理论稠密 BF16 峰值算力合计为 \(8 \times 989 = 7{,}912 \text{ TFLOPS}\)。在批大小为 1 的张量并行预填充中,采用现实条件下的模型 FLOPs 利用率(MFU)\(45\%\),集群持续计算吞吐量为: \[\text{Throughput}_{\text{sustained}} = 0.45 \times 7{,}912 \text{ TFLOPS} \approx 3{,}560 \text{ TFLOPS}\] 不使用前缀缓存时,为系统提示计算 KV 状态会使首词元时间(TTFT)延长: \[T_{\text{prefill}} = \frac{617.4 \text{ TFLOPs}}{3{,}560 \text{ TFLOPS}} \approx 0.1734 \text{ seconds} \approx 173.4 \text{ ms}\] 当基数树前缀命中时,可以跳过全部 4,096 个词元的预填充。主机 CPU 解析块表所需时间小于 \(50 \ \mu\text{s}\)。如果传入查询引入 32 个新词元,加速器仅对这 32 个词元执行预填充,使其关注已缓存的 4,096 个词元,并在 \(6 \text{ ms}\) 以内完成。前缀缓存消除了 \(167.4 \text{ ms}\) 的延迟,使 TTFT 降低 \(28\times\),同时节省了超过 \(600 \text{ TFLOPs}\) 的加速器计算工作量。
在多节点加速器集群上为大量自主智能体提供服务时,必须在全局范围内协调这种前缀局部性。集群路由结构对传入提示生成指纹,并将请求分派到持有匹配前缀块的节点,而非在各工作节点上维护彼此隔离、互不协调的基数树。
图 7 详细展示了三阶段分布式路由流水线。在阶段 1,传入的智能体提示(\(S = 40{,}000\) 个词元)被分割为每块 16 个词元的分块,并转换为滚动 Merkle 哈希链 \(h_k = \text{Hash}(h_{k-1} \parallel c_k)\),从而生成密码学前缀指纹。在阶段 2,集中式集群前缀目录依据亲和性与负载的联合目标 \(w^* = \arg\max [\mu \cdot (\text{Match}/S) - (1-\mu) \cdot (L_j / L_{\max})^\gamma]\) 评估候选工作节点,在提示命中率与工作节点执行队列深度之间取得平衡。在阶段 3,提示被分派至胜出的节点(工作节点 1),该节点已保留 32,000 个缓存前缀词元(前缀命中率为 \(80\%\))。工作节点 1 跳过最初的 2,000 次分块分配,仅对剩余的 8,000 个新后缀词元执行预填充计算,在保持集群负载均衡的同时显著缩短 TTFT。
动态基数树淘汰
加速器的高带宽内存(HBM)容量有严格上限。推理引擎无法无限期保留休眠的前缀块,否则会挤占活跃请求在解码阶段分配所需的内存。当总体内存利用率接近容量上限时,运行时必须回收用于存储缓存前缀的物理块。
在基数树中管理驱逐,与在扁平键值缓存中管理驱逐,在结构上存在本质差异。在扁平缓存中,任何键值对都可以独立清除。在基数树中,节点之间存在严格的结构依赖:只要任一子节点仍驻留在内存中,就不能驱逐其父节点。由于注意力层中的键和值表示依赖于从根节点开始累积的序列,驱逐内部节点会破坏其所有后代节点所需的物理前缀上下文。
为维持这一依赖不变量,运行时采用由引用计数和访问时间戳控制的叶节点优先驱逐策略:
- 状态分类: 基数树中的每个节点根据其
ref_count处于两种运行状态之一。具有 \(\text{ref\_count} \ge 1\) 的节点处于固定状态;一个或多个正在运行的推理请求正在引用该节点,因此在任何情况下都不能将其驱逐。具有 \(\text{ref\_count} = 0\) 的节点处于休眠状态;它对应一个已完成的请求,仅为潜在的复用而保留,可以作为驱逐候选。 - 驱逐候选跟踪: 主机调度器维护一个驱逐优先队列,其中包含所有休眠的叶节点。该队列根据节点的
last_access时间戳,按最近最少使用(LRU)策略排序。 - 迭代剪枝: 当物理块分配器的可用内存低于低水位阈值 \(\tau_{\text{low}}\) 时,便会触发一轮驱逐。分配器从驱逐队列中弹出最近最少使用的叶节点,将分配给它的物理
block_ids释放回全局空闲块池,并从其父节点的children映射中移除该叶节点。 - 级联候选资格: 如果剪枝操作使父节点的子节点映射变为空,且父节点自身的
ref_count为零,则该父节点转变为叶节点。调度器将该父节点插入驱逐优先队列,使其可在后续迭代中被回收。
def evict_radix_blocks(free_blocks: List[int], target_free: int) -> None:
while len(free_blocks) < target_free and lru_leaf_queue:
leaf = lru_leaf_queue.pop_oldest()
if leaf.ref_count > 0:
continue
for block_id in leaf.block_ids:
free_blocks.append(block_id)
parent = leaf.parent
del parent.children[leaf.prefix_tokens[0]]
if len(parent.children) == 0 and parent.ref_count == 0:
lru_leaf_queue.insert(parent)这种叶节点优先的遍历保证根节点和高层分支节点在缓存中驻留的时间最长,因为它们表示共享范围最广的前缀骨架,例如系统提示词。表示短暂、一次性对话轮次的动态叶分支会最先被剪除,从而保留前缀层次结构中缓存命中概率最高的基础层。
哪些因素会降低命中率
一个词元的注意力状态取决于它之前的所有词元,因此缓存只能复用到第一个不同词元之前的状态(原则 5)。前缀缓存从左向右匹配,一旦出现差异,后续状态便无法复用。引擎无法强制采用有利于缓存的布局;它只能让这样的布局获得收益,而决定结果的每一项输入都由智能体运行框架控制。编排下一次调用 已经列出了破坏前缀的变化:位于开头且每次调用都会变化的字段、在轨迹中途发生变化的工具列表、非确定性序列化,以及每一次上下文压缩。从缓存的角度看,它们具有同一种特征:每一种变化都会将第一个不同的词元移向上下文开头,引擎因而需要重新对该词元之后的每个块执行预填充。前三种情况可以通过布局避免,上下文压缩则无法避免。因此,智能体运行框架会降低压缩频率,并以大批次执行压缩(上下文压缩),接受每个批次中有一个轮次开销较高。
衡量这些效果的指标是前缀命中率,即提示词中由缓存提供、无需预填充的词元所占的比例:
\[H_{\text{prefix}} = \frac{\sum_{i} S_{\text{match}}^{(i)}}{\sum_{i} S_{\text{prompt}}^{(i)}} \tag{2}\]
其中,请求 \(i\) 的提示词长度为 \(S_{\text{prompt}}^{(i)}\),匹配的前缀长度为 \(S_{\text{match}}^{(i)}\)。智能体运行框架应当像记录词元数和成本一样,按轨迹记录这一指标。运行框架变更后命中率下降,通常是该变更将易变词元移向上下文前部的最早迹象。
服务提供商的提示词缓存
大多数智能体开发者通过托管 API 调用模型,而不是自行运行引擎;他们接触到的同一机制表现为一项计费功能。服务提供商缓存提示词前缀的 KV 状态,并以常规输入费率的一小部分对缓存输入词元计费。有些接口会自动复用任何长度足够的匹配前缀。另一些接口则要求调用方标记缓存断点,即应当存储前缀的截止位置,并可能对写入缓存的词元额外收费。缓存条目会在空闲时间超过存活时间后过期,这一时间通常为数分钟,因为服务提供商在共享集群上采用了与基数缓存相同的叶节点优先淘汰策略。
这对智能体运行框架有三方面影响。缓存断点应当位于 编排下一次调用 所示布局中稳定区域的边界处,即工具定义之后和长期保留的历史记录之后,使每个区域只需缓存一次便可重复使用。存活时间将缓存与工具等待时间关联起来。如果工具的运行时间超过存活时间,工具返回时缓存已经变冷,下一轮就需要按全额输入价格为整个上下文付费;这是 节 6 中保留或淘汰决策在服务提供商侧的对应形式。最后,命中率会直接体现在账单中,表现为 API 报告的缓存输入词元与未缓存输入词元之比,因此它成为运行框架能够跟踪的成本最低的缓存健康指标。
前缀缓存免除了对轨迹中已经发送的全部内容进行预填充的需要,却无法免除对新内容的预填充。在智能体中,新词元以大块形式到达,每次对应一条工具观测结果。
分块预填充调度
为自回归语言模型提供服务的推理引擎,需要在同一块加速器芯片上处理两种根本不兼容的计算模式。预填充阶段同时接收包含数百或数千个词元的提示词,执行稠密矩阵—矩阵乘法(GEMM),在张量核心上实现较高的计算效率。相比之下,解码阶段在每次迭代中为每个序列恰好生成一个词元,执行受内存带宽限制的矩阵—向量乘法(GEMV);其性能严格受限于高带宽内存(HBM)的传输速度,而非峰值算术吞吐量。当一个较长的智能体提示词——例如复杂的工具定义、包含 8,192 个词元的系统提示词,或多轮对话轨迹——到达一个正在为二十条活跃解码流提供服务的引擎时,朴素的运行至完成调度器会造成灾难性的队头阻塞。加速器必须将所有张量计算单元投入新提示词的大规模矩阵运算,使受内存带宽限制的解码循环停顿数百毫秒,并导致词元间延迟(ITL)的尾部尖峰,从而违反服务级别协议。
分块预填充通过将整体提示切分为大小一致的词元块,并在同一执行批次内将这些词元块与活跃的解码请求共同调度,解决了这种物理资源冲突。分块调度不再将预填充和解码视为互斥的运行模式,而是利用二者互补的硬件需求:计算密集的预填充块将批次的算术强度提升至接近加速器 Roofline 模型的脊点,而活跃的解码词元则分摊了将模型权重矩阵从 HBM 流式传输到 SRAM 所需的内存带宽。
预填充与解码的相互干扰困境
预填充与解码之所以发生冲突,是因为它们会使不同的资源达到饱和。解码受内存带宽限制(原理 1),而预填充受计算限制,如 加速器服务延迟 所推导。在 Roofline 模型下,工作负载的运算强度 \(\mathcal{I}\) 衡量的是所执行的浮点运算次数与经加速器内存总线传输的字节数之比:
\[\mathcal{I} = \frac{\text{Floating Point Operations (FLOPs)}}{\text{Memory Traffic (Bytes)}}\]
考虑一款加速器,其用于 16 位浮点运算的稠密张量核心吞吐量 \(T_{\text{peak}}\) 为 \(1{,}000\text{ TFLOPS}\)(\(10^{15}\text{ FLOPs/s}\)),HBM 带宽 \(B_{\text{mem}}\) 为 \(3.35\text{ TB/s}\)(\(3.35 \times 10^{12}\text{ Bytes/s}\))。硬件脊点 \(\mathcal{I}^*\) 表示达到峰值计算吞吐量所需的最低算术强度:
\[\mathcal{I}^* = \frac{T_{\text{peak}}}{B_{\text{mem}}} = \frac{1{,}000 \times 10^{12}\text{ FLOPs/s}}{3.35 \times 10^{12}\text{ Bytes/s}} \approx 298.5\text{ FLOPs/Byte}\]
任何运算强度为 \(\mathcal{I} < \mathcal{I}^*\) 的计算都受内存带宽限制,张量核心在等待操作数从 HBM 传入时处于空闲状态。任何满足 \(\mathcal{I} \ge \mathcal{I}^*\) 的计算都受计算能力限制,并使张量核心完全饱和。
Roofline 脊点 运算强度 \(\mathcal{I}^* = T_{\text{peak}} / B_{\text{mem}}\) 定义了加速器从受内存带宽限制转为受计算能力限制的精确边界。对于现代加速器,这一阈值通常超过 200–300 FLOPs/字节。
在活跃序列的解码阶段,生成单个词元需要针对一个词元位置执行完整的 Transformer 模型(\(S=1\))。对于参数量为 \(P\) 的模型,计算单个词元需要将整个参数集共 \(2P\) 字节(每个参数为 16 位)从 HBM 读取到片上 SRAM,同时执行约 \(2P\) FLOPs 的矩阵乘法。对于包含 \(B_{\text{decode}}\) 个并发解码序列的批次,总计算量为 \(2 \cdot P \cdot B_{\text{decode}}\) FLOPs,而权重传输量仍固定为 \(2P\) 字节(暂时忽略 KV 缓存的传输量)。因此,解码的运算强度为:
\[\mathcal{I}_{\text{decode}} \approx \frac{2 \cdot P \cdot B_{\text{decode}}}{2 \cdot P} = B_{\text{decode}}\text{ FLOPs/Byte}\]
即使批次大小达到相当可观的 \(B_{\text{decode}} = 64\),运算强度也仅达到 \(64\text{ FLOPs/Byte}\),不足硬件脊点 \(\mathcal{I}^* \approx 298.5\) 的四分之一。加速器仍严重受内存带宽限制,超过 \(75\%\) 的运行周期都在等待权重矩阵经内存总线流式传入。
相反,预填充阶段会一次性处理长度为 \(S_{\text{prompt}}\) 的提示。同样的 \(2P\) 字节权重只需流式传输一次,但会在全部 \(S_{\text{prompt}}\) 个词元之间复用,从而得到如下运算强度:
\[\mathcal{I}_{\text{prefill}} \approx \frac{2 \cdot P \cdot S_{\text{prompt}}}{2 \cdot P} = S_{\text{prompt}}\text{ FLOPs/Byte}\]
当智能体运行时提交包含 \(S_{\text{prompt}} = 4{,}096\) 个词元的提示时,\(\mathcal{I}_{\text{prefill}} \approx 4{,}096\text{ FLOPs/Byte}\),远高于脊点 \(\mathcal{I}^*\)。预填充使加速器的矩阵乘法单元达到饱和,并以峰值计算利用率运行。
当推理引擎顺序处理这两个阶段时,便会陷入系统层面的两难。在运行至完成的调度方式下,新到达的预填充请求会抢占解码循环,或在队列中排到解码循环之前。对于一个拥有 700 亿参数的模型,处理包含 8,192 个 token 的提示可能会独占加速器 800 至 1,500 毫秒。在此期间,引擎中所有活跃的解码序列都会停滞。向下游用户或智能体监督循环流式传输的 token 会完全中断,导致 token 间延迟(ITL)分布出现极大的方差。人类用户会感到输出卡顿,而自主智能体控制循环则会将其视为推理子系统的瞬时故障,可能因此触发 RPC 客户端超时,或降低反馈响应能力。
分块预填充机制
为了消除队头阻塞,同时保留提示处理的高算术强度,推理调度器必须将整体预填充拆分为易于处理的片段。分块预填充将包含 \(S_{\text{prompt}}\) 个 token 的新到达提示分解为一系列大小均为 \(C_{\text{chunk}}\) 的较小块(通常为 256、512 或 1,024 个 token)。
假设一个提示包含 \(S_{\text{prompt}}\) 个 token,索引从 \(0\) 到 \(S_{\text{prompt}} - 1\)。当块大小为 \(C_{\text{chunk}}\) 时,调度器将提示划分为 \(K = \lceil S_{\text{prompt}} / C_{\text{chunk}} \rceil\) 个连续切片。第 \(k\) 个块(其中 \(1 \le k \le K\))覆盖以下 token 范围:
\[I_k = \left[ (k - 1) C_{\text{chunk}}, \, \min(k \cdot C_{\text{chunk}}, S_{\text{prompt}}) - 1 \right]\]
服务引擎在一次前向传播迭代中,将块 \(I_k\) 与其他活跃请求一同计算。物理实现上的挑战在于跨块边界计算多头注意力,同时保持与整体前向传播的数学等价性。
图 8 详细展示了分块注意力的内部执行流程,其中一个包含 2,048 个 token 的提示被切分为大小均为 \(C_{\text{chunk}} = 512\) 个 token 的块。在执行迭代 \(k=3\)(覆盖 token 1024 至 1535)时,加速器仅为这 512 个活跃 token 生成查询 \(\mathbf{Q}_3\)、键 \(\mathbf{K}_3\) 和值 \(\mathbf{V}_3\)。为了在不重新执行整个提示的线性投影的情况下计算注意力,注意力内核直接从 HBM 中此前已填充的 PagedAttention 块加载历史键值投影 \(\mathbf{K}_{1:2}\) 和 \(\mathbf{V}_{1:2}\),并将它们拼接为扩展序列 \(\mathbf{K}_{\le 3} \in \mathbb{R}^{1536 \times D_{\text{kv}}}\)。线程块将这些块流式传输至片上 SRAM,以计算因果注意力:对块 1 和块 2 应用完整的双向可见性,对块 3 应用下三角因果掩码;随后,将 \(\mathbf{K}_3\) 和 \(\mathbf{V}_3\) 追加到全局内存中新分配的分页块中。
在迭代 \(k\) 中,模型仅为当前块 \(I_k\) 内的 token 生成查询、键和值投影:
\[\mathbf{Q}_k = \mathbf{X}_k \mathbf{W}_Q, \quad \mathbf{K}_k = \mathbf{X}_k \mathbf{W}_K, \quad \mathbf{V}_k = \mathbf{X}_k \mathbf{W}_V\]
其中,\(\mathbf{X}_k \in \mathbb{R}^{|I_k| \times D}\) 表示块 \(k\) 的激活。由于自回归语言模型采用因果掩码,块 \(k\) 中的 token 必须关注块 \(1\) 至 \(k-1\) 中的所有先前 token,以及块 \(k\) 自身内部位于其之前的 token。块 \(1\) 至 \(k-1\) 的键和值已在此前的迭代中计算完成,并存储在分页 KV 缓存中不连续的物理页内。
因此,块 \(k\) 的注意力计算通过将历史缓存投影与新计算的投影拼接来构建其键矩阵和值矩阵:
\[\mathbf{K}_{\le k} = \left[ \mathbf{K}_{\text{cached}, 1:k-1} \,\|\, \mathbf{K}_k \right] \in \mathbb{R}^{\left(\sum_{j=1}^k |I_j|\right) \times D_{\text{kv}}}\]
\[\mathbf{V}_{\le k} = \left[ \mathbf{V}_{\text{cached}, 1:k-1} \,\|\, \mathbf{V}_k \right] \in \mathbb{R}^{\left(\sum_{j=1}^k |I_j|\right) \times D_{\text{kv}}}\]
当前分块的注意力输出计算如下:
\[\mathbf{O}_k = \text{Softmax}\left( \frac{\mathbf{Q}_k \mathbf{K}_{\le k}^\top}{\sqrt{d_{\text{head}}}} + \mathbf{M}_k \right) \mathbf{V}_{\le k}\]
注意力掩码 \(\mathbf{M}_k\) 强制保证因果性。对于历史词元(\(j < (k-1) C_{\text{chunk}}\)),掩码允许完全双向注意力,因为所有历史词元都位于分块 \(k\) 中每个词元之前。对于分块 \(k\) 内的当前词元,掩码采用标准因果掩码,阻止词元 \(t \in I_k\) 关注词元 \(t' > t\)。
在现代 GPU 内核架构(如 FlashDecoding 和 FlashAttention-3)中,该操作避免在加速器全局内存中显式生成中间注意力矩阵。内核将历史 KV 块从 HBM 直接流式加载到 SRAM 分块中,并在处理各块的过程中持续维护在线 softmax 统计量——动态更新的行最大值 \(m_i\) 和归一化因子 \(l_i\):
\[m_{\text{new}} = \max(m_{\text{prev}}, m_{\text{block}}), \quad l_{\text{new}} = l_{\text{prev}} e^{m_{\text{prev}} - m_{\text{new}}} + l_{\text{block}} e^{m_{\text{block}} - m_{\text{new}}}\]
部分注意力累加器通过 \(\mathbf{O}_{\text{new}} = \mathbf{O}_{\text{prev}} e^{m_{\text{prev}} - m_{\text{new}}} + \mathbf{O}_{\text{block}} e^{m_{\text{block}} - m_{\text{new}}}\) 实时重新缩放,从而完全消除中间注意力 logits 带来的内存带宽流量。
计算出 \(\mathbf{O}_k\) 后,引擎将 \(\mathbf{K}_k\) 和 \(\mathbf{V}_k\) 写入全局 KV 缓存中新分配的分页块中。关键在于,分块 \(k\) 的中间隐藏激活会在迭代结束时被丢弃,与标准前向传播完全相同。模型直到分块 \(K\)(提示的最后一个分块)执行完毕才输出词元,此时从位置 \(S_{\text{prompt}} - 1\) 的 logit 分布中采样,生成第一个输出词元。
分块以少量冗余计算换取有界的迭代延迟。注意力机制使用长度为 \(C_{\text{chunk}}\) 的查询,对随每个分块逐渐增长的键(\(C_{\text{chunk}}, 2C_{\text{chunk}}, \dots, K C_{\text{chunk}}\))进行计算。然而,由于历史词元的键和值投影从内存中读取,而非根据权重重新计算,参数矩阵乘法的总次数保持不变。
词元预算调度
分块预填充使推理引擎能够构建同时包含预填充分块和单词元解码请求的混合批次。在 Sarathi-Serve 和现代 vLLM 实现等系统中,这种调度受严格的词元预算约束,该预算记为 \(C_{\text{budget}}\)。
在每个调度周期中,引擎检查待处理的预填充分块队列以及正在运行的解码序列集合。调度器选择预填充分块与解码词元的组合,使该次迭代处理的词元总数不超过 \(C_{\text{budget}}\):
\[\sum_{i \in \mathcal{P}_{\text{admitted}}} M_{\text{chunk}, i} + \sum_{j \in \mathcal{D}_{\text{admitted}}} 1 \le C_{\text{budget}}\]
其中,\(\mathcal{P}_{\text{admitted}}\) 是获准处理的预填充分块集合,\(M_{\text{chunk}, i} \le C_{\text{chunk}}\) 是预填充分块 \(i\) 的词元数,\(\mathcal{D}_{\text{admitted}}\) 是获准处理的解码序列集合。推理调度策略 比较了这些调度形态在运行中的权衡。
| 调度策略 | 迭代组成 | 硬件瓶颈 | 对解码延迟的影响 | 对 TTFT 的影响 |
|---|---|---|---|---|
| 整体预填充 | 仅包含 \(S_{\text{prompt}}\) 个词元 | 受计算限制(\(\mathcal{I} \gg \mathcal{I}^*\)) | ITL 出现显著尖峰(\(500\text{--}1{,}500\text{ ms}\)) | TTFT 最小(单次前向传播) |
| 未批处理解码 | 仅包含 \(B_{\text{decode}}\) 个词元 | 受内存限制(\(\mathcal{I} \ll \mathcal{I}^*\)) | ITL 低,但计算单元处于空闲状态 | 不适用(不接纳预填充请求) |
| 分块协同调度 | \(C_{\text{chunk}}\) 预填充 \(+\) \(B_{\text{decode}}\) 个词元 | 屋顶线模型下的平衡(\(\mathcal{I} \approx \mathcal{I}^*\)) | ITL 可预测(\(30\text{--}60\text{ ms}\) 尾部) | TTFT 略有增加(\(5\text{--}15\%\)) |
词元预算抽象在两项关键的服务等级协议(SLA)之间取得平衡:首词元时间(TTFT)和词元间延迟(ITL)。
首词元时间表示客户端提交请求到接收到第一个生成词元之间的延迟:
\[\text{TTFT} = t_{\text{queue}} + \sum_{k=1}^K t_{\text{iter}}(k)\]
在整体预填充方式下,\(K=1\),因此提示在一次高度优化的迭代中完成处理。在分块预填充方式下,\(K > 1\)。提示必须经过 \(K\) 次调度迭代的等待,才能输出一个词元。此外,每次迭代都会产生内核启动、块表查找以及对先前存储的 KV 页执行注意力计算的开销。因此,分块预填充使孤立请求的 TTFT 增加 5% 至 15%。
词元间延迟表示活跃序列输出相邻词元 \(t\) 和 \(t+1\) 之间经过的时间:
\[\text{ITL} = t_{\text{emit}}(t+1) - t_{\text{emit}}(t)\]
在整体式服务中,如果在十个解码序列处于活跃状态时接纳一项包含 4,096 个词元的预填充任务,这十个序列的 ITL 就会从基线 \(35\text{ ms}\) 飙升至超过 \(800\text{ ms}\)。这意味着尾部(P99)延迟出现 \(22\times\) 的恶化。在采用 \(C_{\text{chunk}} = 512\) 的分块预填充方式下,预填充工作被分散到八次均衡的迭代中。每次迭代的运行时间约为 \(50\text{ ms}\)。在每次迭代中,所有活跃的解码序列都会推进一个词元,同时处理 512 个预填充词元。P99 ITL 始终被严格限制在 \(50\text{ ms}\) 附近,完全消除了延迟尖峰。
整体执行与分块协同调度之间的时序动态和调度差异如 图 9 所示,其中面板 (a) 展示了队首阻塞导致的解码停顿,面板 (b) 展示了通过词元预算限定迭代规模以消除这种停顿的方式。在整体执行的时间线上,接纳一项包含 4,096 个词元的预填充任务会使张量核心连续独占运行 800 ms,导致十个正在运行的解码流无法获得计算资源,并引发严重的 P99 词元间延迟尖峰。在分块协同调度方式下,调度器将预填充任务拆分为八个各含 512 个词元的块,并在有限的词元预算(\(C_{\text{budget}}\))内,将每个块与活跃的解码词元配对处理。每次混合迭代约在 50 ms 内完成,使所有正在运行的解码流都能在每个周期中推进一个词元,同时平滑加速器利用率并消除队首阻塞造成的停顿。
粗略估算 0.5: 干扰量化与分块预算
问题 1:在整体式运行至完成的调度方式下,执行延迟和 ITL 尖峰是多少?
一次包含 32 个序列的解码迭代需要流式读取全部 \(140\text{ GB}\) 的模型权重,以执行 \(2 \cdot P \cdot B_{\text{decode}} = 2 \times (70 \times 10^9) \times 32 = 4.48 \times 10^{12}\text{ FLOPs} = 4.48\text{ TFLOPs}\)。 内存读取时间为: \[t_{\text{mem, decode}} = \frac{140 \times 10^9\text{ Bytes}}{3.0 \times 10^{12}\text{ Bytes/s}} \approx 46.7\text{ ms}\] 张量核心的模型 FLOPs 利用率(MFU)实际达到 50%(有效吞吐量为 \(400\text{ TFLOPS}\))时,计算时间为: \[t_{\text{comp, decode}} = \frac{4.48 \times 10^{12}\text{ FLOPs}}{400 \times 10^{12}\text{ FLOPs/s}} \approx 11.2\text{ ms}\] 由于 \(t_{\text{mem}} > t_{\text{comp}}\),该解码迭代受内存带宽限制,耗时 \(\max(46.7, 11.2) = 46.7\text{ ms}\)。基线 ITL 为 \(46.7\text{ ms}\)。
当包含 \(4{,}096\) 个 token 的整体预填充执行时,其计算负载为: \[\text{FLOPs}_{\text{prefill}} = 2 \cdot P \cdot S_{\text{prompt}} = 2 \times (70 \times 10^9) \times 4{,}096 = 5.7344 \times 10^{14}\text{ FLOPs} = 573.44\text{ TFLOPs}\] 假设稠密 GEMM 达到的 MFU 为 \(60\%\)(有效算力为 \(480\text{ TFLOPS}\)): \[t_{\text{comp, prefill}} = \frac{573.44 \times 10^{12}\text{ FLOPs}}{480 \times 10^{12}\text{ FLOPs/s}} \approx 1{,}194.7\text{ ms} \approx 1.195\text{ seconds}\] 32 个活跃的解码流停滞 \(1.195\text{ seconds}\)。ITL 从 \(46.7\text{ ms}\) 骤增至 \(1{,}195\text{ ms}\),延迟恶化幅度为 \(25.6\times\)。
问题 2:使用 \(C_{\text{chunk}} = 512\) 对预填充进行分块时,迭代延迟是多少?
提示被划分为 \(K = 4{,}096 / 512 = 8\) 个块。在每次迭代中,调度器将一个包含 512 个 token 的预填充块与 32 个解码 token 联合调度,共计 \(512 + 32 = 544\) 个 token。
权重流式读取时间保持固定,为 \(t_{\text{mem}} = 46.7\text{ ms}\)(因为全部 544 个 token 共享相同的模型参数)。 544 个 token 所需的计算量为: \[\text{FLOPs}_{\text{iter}} = 2 \times (70 \times 10^9) \times 544 = 7.616 \times 10^{13}\text{ FLOPs} = 76.16\text{ TFLOPs}\] 在有效吞吐量为 \(480\text{ TFLOPS}\) 时: \[t_{\text{comp, iter}} = \frac{76.16 \times 10^{12}\text{ FLOPs}}{480 \times 10^{12}\text{ FLOPs/s}} \approx 158.7\text{ ms}\] 由于此时计算时间超过内存流式读取时间(\(158.7\text{ ms} > 46.7\text{ ms}\)),该批次的执行耗时约为 \(158.7\text{ ms}\)。
原先灾难性的 1,195 ms 停顿随后接快速解码的过程,被替换为全部 32 个活跃解码流每隔 \(158.7\text{ ms}\) 各推进一个 token,连续进行 8 个周期。最大 ITL 从 \(1{,}195\text{ ms}\) 降至 \(158.7\text{ ms}\),既保持了可预测的流式输出性能,又使加速器计算核心保持饱和。
分块预填充表明,受内存带宽限制的工作负载与受计算能力限制的工作负载可以配对执行,以优化加速器整体资源利用率。通过将 token 块大小 \(C_{\text{chunk}}\) 设定为满足 \(t_{\text{comp}} \approx t_{\text{mem}}\),调度器能够达到内存总线与张量核心同时完成工作的平衡点,从而最大化整个集群每秒生成的 token 总数。
Checkpoint 0.2: 评估分页分配与分块预填充
在分析内存卸载、保留和换入换出策略之前,请检验你对分页注意力机制的理解:
保留、驱逐、重计算或卸载
有了分页机制,前文在 节 1 中暂未处理的暂停问题现在可以得到解决。当自回归解码循环输出一个工具调用 token 序列,要求执行一次编译、在隔离容器中运行耗时数秒的测试套件,或由人工操作员进行交互式审批时,模型的前向推理会立即停止。在智能体等待外部执行结果期间,分配给该轨迹的物理键值缓存块仍被固定在设备的高带宽内存(HBM)中。如果服务引擎在持续数秒或数分钟的执行暂停期间无限期保留这些物理块,空闲轨迹就会独占加速器容量,造成人为的内存压力,限制活跃解码,并迫使新到达的请求进入准入队列。反之,如果运行时过早驱逐这些块以释放 HBM,恢复执行的轨迹就必须对数万个历史上下文 token 重新进行预填充计算,浪费数千 TFLOP 的计算量,并增加恢复后的首 token 延迟。
当智能体轨迹因工具执行或人工斟酌而暂停时,服务系统必须在四种物理内存策略之间作出选择:驻留保留、完全驱逐并在随后重新计算、通过异步 PCIe 流传输卸载至主机 DRAM,以及采用二级存储分层。最优决策边界由解析推导的盈亏平衡等待时间确定,该时间权衡了预留加速器内存容量的机会成本、互连的传输带宽,以及重新运行预填充阶段的计算延迟。
内存回收策略
在对话服务中,请求执行是连续的:初始预填充阶段处理提示词,随后立即进入不间断的自回归解码阶段,持续输出词元,直到遇到停止分隔符或达到序列长度上限。智能体工作负载打破了这一假设。智能体轨迹由多次短暂的词元生成组成,其间穿插着时长呈重尾分布的外部执行间隔。代码编辑智能体可能生成一条包含五十个词元的 shell 命令,耗时两百毫秒,然后空闲四十五秒,等待构建系统编译依赖项并运行单元测试。研究智能体可能输出一条搜索查询,然后暂停,等待浏览器工作进程下载、解析并抓取若干网页文档。
在多租户服务集群中,加速器 HBM 是决定最大并发批大小的关键瓶颈 \(B\);将 \(M_{\text{KV}}\) GB 的非活跃注意力状态固定保留达 \(T_{\text{wait}}\) 的空闲时长,会产生工具等待内存开销,内存滞留摩擦 已对单个会话的这一开销进行了量化,这里将其写为内存与时间的乘积:
\[\mathcal{C}_{\text{hold}} = M_{\text{KV}} \cdot T_{\text{wait}}\]
这一以 GB 与秒相乘得到的乘积,表示从活跃调度池中扣留的容量。如果一个推理实例为某个因运行六十秒测试套件而停滞的智能体预留了 \(6\text{ GB}\) 的物理 KV 缓存,那么它会消耗 \(360\text{ GB}\cdot\text{s}\) 的 HBM 驻留量,却不执行任何浮点运算。在持续的请求到达率下,累积的内存时间开销会使物理块分配器饱和,迫使运行时调度器抢占活跃的解码流,或拒绝新到达的智能体步骤。
工具等待内存开销 \(\mathcal{C}_{\text{hold}} = M_{\text{KV}} \cdot T_{\text{wait}}\) 揭示了智能体服务的根本不对称性:内存容量随实际时间的流逝而被持续占用,而加速器计算单元仅在活跃的前向传播期间得到利用。
为应对这一约束,推理管理程序可以采用四种不同的内存管理策略,表 3 对比了这些策略在各硬件层级上的表现:
- 驻留保留: 服务运行时在整个工具执行窗口内,将所有物理 KV 块保留在加速器 HBM 中。该策略仅以优化恢复延迟为目标:当外部环境返回执行结果时,引擎将新的观测词元追加到现有块表中,并以零恢复延迟继续预填充(\(T_{\text{resume}} = 0\))。然而,其机会成本随 \(T_{\text{wait}}\) 线性增长,因此在内存紧张或等待时间较长时会造成灾难性后果。
- 驱逐与重新计算: 服务运行时在发出工具调用后立即释放该轨迹的物理 KV 块,将页面归还给分配器的空闲池。当工具返回观测结果时,运行时会重建完整的提示词历史,将此前的系统提示词、历史对话、工具调用词元和新收到的观测结果拼接起来,并从头重新执行预填充前向传播。该策略不产生 HBM 保留开销(\(\mathcal{C}_{\text{hold}} = 0\)),但会将物理内存困境转化为二次增长的计算开销,消耗宝贵的张量核心资源,并使恢复延迟增加一次长时间预填充前向传播的完整耗时(\(T_{\text{resume}} = T_{\text{prefill}}\))。
- 主机 DRAM 卸载(PCIe 换入换出): 服务引擎通过外设互连(如 PCIe Gen5 或 NVLink-C2C)发起异步直接内存访问(DMA)传输,将该轨迹的物理 KV 块以流传输方式从 HBM 移至页面锁定的主机系统内存。传输完成后,GPU 物理块即可回收。当工具发出完成信号时,运行时会分配新的 GPU 物理块,并将保存的注意力状态以流传输方式移回 HBM。该策略使加速器内存占用与空闲等待时间解耦,同时避免重新计算带来的大量浮点运算开销,其性能转而受主机总线传输带宽 \(B_{\text{xfer}}\) 的限制。
- 分层二级存储(NVMe 与远程内存): 对于涉及人在回路审批、异步安全审查或定时批处理作业的操作,当 \(T_{\text{wait}}\) 跨越数分钟、数小时甚至数天时,主机系统 DRAM 也过于稀缺,无法用作无限期的暂存空间。运行时会将序列化的 KV 块从主机 DRAM 迁移到本地固态硬盘(通过 PCIe 连接的 NVMe),或解耦的网络内存池(例如通过 RDMA 访问的远程存储集群)。恢复延迟会增加到数百毫秒或数秒,但加速器 HBM 和主机 DRAM 中的活跃内存占用均降至零。
| 策略 | 物理存储层级 | 有效带宽(\(B_{\text{xfer}}\)) | 恢复延迟(\(T_{\text{resume}}\)) | 加速器计算开销 | 失效情形 |
|---|---|---|---|---|---|
| 驻留保留 | 加速器 HBM | \(\sim 3.0\text{--}4.8\text{ TB/s}\) | \(0\text{ ms}\) | \(0\text{ FLOPs}\) | HBM 耗尽;批处理饥饿 |
| 驱逐与重计算 | 丢弃(无存储) | 不适用 | \(T_{\text{prefill}}(S)\) | \(2 P S\text{ FLOPs}\) | 计算饱和;首 token 延迟(TTFT)高 |
| 主机 DRAM 卸载 | 主机系统内存 | \(32\text{--}64\text{ GB/s}\) (PCIe) | \(M_{\text{KV}} / B_{\text{pcie}}\) | \(0\text{ FLOPs}\) | 互连总线争用 |
| 分层存储 | 本地 NVMe / RDMA | \(7\text{--}14\text{ GB/s}\) (NVMe) | \(M_{\text{KV}} / B_{\text{nvme}}\) | \(0\text{ FLOPs}\) | I/O 队列停滞;磁盘磨损 |
分析机制:盈亏平衡决策边界
在这四种策略中采用固定策略,会导致严重的运行异常。始终保留 KV 状态的引擎,在智能体遇到耗时较长的构建步骤时,会因内存不足(OOM)故障而崩溃。反之,始终进行重计算的引擎,会将运行能耗的一半以上浪费在重新计算静态系统提示词和文件缓冲区上。服务运行时必须评估一个分析决策边界,该边界由序列长度 \(S\)、预期等待时间 \(T_{\text{wait}}\)、加速器计算吞吐量和互连传输带宽共同决定。
考虑一个自回归 Transformer 模型,它具有 \(P\) 个参数、\(L\) 层、\(H_{\text{kv}}\) 个键值头,头维度为 \(d_{\text{head}}\),每个元素的字节数为 \(b_{\text{elem}}\)(其中,16 位半精度对应 \(b_{\text{elem}} = 2\),8 位量化对应 \(b_{\text{elem}} = 1\))。每个 token 的键值投影所占用的内存为:
\[m_{\text{token}} = 2 \cdot L \cdot H_{\text{kv}} \cdot d_{\text{head}} \cdot b_{\text{elem}}\]
对于在序列长度为 \(S\) 时暂停的轨迹,其物理注意力状态的总内存占用为 \(M_{\text{KV}} = S \cdot m_{\text{token}}\)。
现在评估通过重计算重建该状态所需的时间,以及通过互连流式传输该状态所需的时间。采用驱逐与重计算时,加速器必须对 \(S\) 个 token 执行一次预填充前向传播。在现代仅解码器架构中,预填充阶段的计算成本主要来自线性投影和前馈网络中的矩阵乘法,每个 token 约需 \(2 P\) 次浮点运算,此外还有二次方级的注意力计算成本 \(4 L S^2 d_{\text{model}}\)。对于不超过三万二千个 token 的典型上下文长度,线性参数项占主导地位。假设加速器集群执行预填充时的有效持续计算吞吐量为 \(R_{\text{prefill}}\)(单位为每秒浮点运算次数,已计入模型 FLOPs 利用率,即 MFU),则重计算延迟为:
\[T_{\text{recomp}}(S) \approx \frac{2 P \cdot S}{R_{\text{prefill}}}\]
相比之下,通过主机互连(例如 PCIe Gen5 x16)卸载物理 KV 状态时,若有效持续单向传输带宽为 \(B_{\text{pcie}}\),则所需的重新加载时间为:
\[T_{\text{reload}}(S) = \frac{M_{\text{KV}}}{B_{\text{pcie}}} = \frac{S \cdot m_{\text{token}}}{B_{\text{pcie}}}\]
由于注意力状态始终可以根据其 token 重建(原则 5),驱逐是一项成本决策,比较 \(T_{\text{recomp}}(S)\) 与 \(T_{\text{reload}}(S)\) 即可作出判断。这两个量都随序列长度 \(S\) 线性增长。因此,重新加载延迟与重计算延迟之比与序列长度无关:
\[\frac{T_{\text{reload}}}{T_{\text{recomp}}} = \frac{S \cdot m_{\text{token}} / B_{\text{pcie}}}{2 P \cdot S / R_{\text{prefill}}} = \frac{m_{\text{token}} \cdot R_{\text{prefill}}}{2 P \cdot B_{\text{pcie}}}\]
如果这一比值小于一,那么通过外设总线流式传输注意力状态,严格快于在张量核心上重新计算该状态。
图 10 展示了这一权衡的定量动态。左图绘制了一个 70B 模型在 \(S = 16{,}384\) 个词元(\(5.0\text{ GiB}\) 的 KV 状态)下,执行延迟随外部工具暂停时长的变化。完整提示重计算会在张量核心上产生固定的 \(5.735\text{ s}\) 开销,而双向 PCIe Gen5 x16 传输可在 \(T_{\text{round-trip}} = 215\text{ ms}\) 内将状态流式传出并传回。因此,盈亏平衡交叉点位于 \(T^* = 215\text{ ms}\)。右图展示了由此形成的、以序列长度和等待时长为维度的二维决策区域:短于 \(215\text{ ms}\) 的工具暂停属于 HBM 驻留保留区域,以避免 DMA 流水线气泡;介于 \(215\text{ ms}\) 与数十秒之间的暂停属于通过 PCIe 换入换出至主机 DRAM 的区域(相较于重计算,延迟优势最高可达 \(53.6\times\));持续数分钟以上的长时间暂停则转入二级存储卸载区域。
这一优势的幅度在很大程度上取决于物理互连架构:
- PCIe Gen5 x16 互连: 标准独立加速器节点提供 \(B_{\text{pcie}} \approx 64\text{ GB/s}\) 的理论吞吐量(扣除协议帧封装开销后,持续有效 DMA 吞吐量为 \(52\text{--}54\text{ GB/s}\))。
- 一致性芯片间互连(NVLink-C2C): 现代紧耦合异构平台(如 NVIDIA GH200 Grace Hopper 和 GB200 Grace Blackwell)以一致性芯片间链路取代外设 PCIe 插槽,提供 \(B_{\text{c2c}} \approx 900\text{ GB/s}\) 的带宽——持续带宽提升超过 \(16\times\)。在 NVLink-C2C 上,在 CPU DRAM 与 GPU HBM 之间流式传输物理 KV 块可在数毫秒内完成,使最低可行卸载阈值 \(T_{\text{wait}}^*\) 从数百毫秒降至 20 毫秒以下。
换入换出与重计算之间的交叉点取决于算术密度与互连带宽之间的平衡。当 \(\frac{m_{\text{token}}}{2P} < \frac{B_{\text{pcie}}}{R_{\text{prefill}}}\) 时,通过 PCIe 传输注意力投影比通过矩阵乘法生成这些投影更快。在 NVLink-C2C 上,这一阈值降至原来的 1/16。
为将空闲时长 \(T_{\text{wait}}\) 纳入考量,运行时必须比较内存持有成本与换入换出成本。令 \(\mu_{\text{HBM}}\) 表示容量压力下 GPU 内存的影子价格(每字节每秒的机会成本),令 \(\mu_{\text{DRAM}}\) 表示主机内存的影子价格(\(\mu_{\text{DRAM}} \ll \mu_{\text{HBM}}\))。卸载一条轨迹需要传出时间 \(T_{\text{egress}} = M_{\text{KV}} / B_{\text{pcie}}\) 和传入重载时间 \(T_{\text{ingress}} = M_{\text{KV}} / B_{\text{pcie}}\)。物理状态占用的存储空间只有在 \((T_{\text{wait}} - T_{\text{ingress}})\) 这一时间区间内才能被释放。因此,只有当等待时间严格超过往返传输延迟时,卸载在物理上才可行:
\[T_{\text{wait}} > \frac{2 \cdot M_{\text{KV}}}{B_{\text{pcie}}}\]
如果 \(T_{\text{wait}} \le \frac{2 M_{\text{KV}}}{B_{\text{pcie}}}\),引擎就无法在工具返回之前完成往返传输;在这一时间窗口内尝试卸载,会人为引入流水线停顿,此时模型空闲的唯一原因是等待自身状态重新载入。对于超过这一最低物理阈值的等待时间,驻留保留产生持有成本 \(\mu_{\text{HBM}} \cdot M_{\text{KV}} \cdot T_{\text{wait}}\),而换入换出产生传输开销与主机驻留成本 \(\mu_{\text{DRAM}} \cdot M_{\text{KV}} \cdot T_{\text{wait}}\)。令两项成本相等,即可得到盈亏平衡等待时间 \(T_{\text{wait}}^*\):
\[T_{\text{wait}}^* \approx \frac{2 \cdot M_{\text{KV}}}{B_{\text{pcie}}} \cdot \left( \frac{\mu_{\text{HBM}}}{\mu_{\text{HBM}} - \mu_{\text{DRAM}}} \right) + \Delta T_{\text{stall}}\]
其中,\(\Delta T_{\text{stall}}\) 表示调度器愿意在恢复执行的步骤中容忍的延迟开销。当内存容量受限(\(\mu_{\text{HBM}} \gg \mu_{\text{DRAM}}\))时,\(T_{\text{wait}}^*\) 趋近于往返传输时间。
Systems Perspective 0.1: 空闲智能体的桎梏与 KV 驻留开销
在这些暂停期间,将数 GB 的键值状态保留在高带宽内存中,会产生极高的空闲驻留开销。一块 \(80\text{ GB}\) 的 H100 GPU 若承载十六个同时处于暂停状态的智能体,其全部内存容量都可能被不输出任何词元的休眠轨迹占满,导致活跃解码任务无法获得内存,并触发队头阻塞。然而,直接驱逐这些块又会带来同样高昂的重计算开销:恢复执行时重新计算数千个前缀词元,会浪费宝贵的张量核心周期,并使首词元延迟骤增。
系统洞见:高带宽内存是临时的执行空间,而非归档存储介质。可靠的智能体运行时通过将执行与保留解耦来克服驻留开销。加速器内存保存正在生成词元的轨迹的注意力状态;一旦某条轨迹的预期等待时间超过盈亏平衡时间,运行时便通过异步 DMA 将其状态换出至主机 DRAM 或 NVMe。
粗略估算 0.6: 量化四十五秒工具等待的成本
变量:
- 待保留的状态:\(M_{\text{KV}} =\) 10.7 GB(来自容量估算)。
- 主机链路:每个加速器持有八分之一的状态,并拥有独立的 64 GB/s 链路,因此节点迁移状态的速率最高可达 \(B_{\text{link}} =\) 512 GB/s。
- 预填充:节点峰值速率的 45 percent,即 \(R_{\text{prefill}} \approx\) 3,560 TFLOP/s(假设的持续利用率)。
计算:
重新加载:10.7 GB / 512 GB/s \(\approx\) 21 ms,因此移出并移回的往返时间为 42 ms。
重新计算:\(2 N S =\) 2 \(\times\) 70.6 十亿 \(\times\) 32,768 \(\approx\) 4.63 \(\times 10^{15}\) 次运算,在持续速率下耗时 1.3 s。
在整个等待期间保持状态驻留,会占用 10.7 GB \(\times\) 45 s \(\approx\) 483 GB·s 的加速器内存。
结果:重新加载比重新计算快约 62×,且 42 ms 的往返时间远低于等待时间的千分之一。即使链路只能维持其额定带宽的四分之一,重新加载仍具有约 15× 的优势。
系统洞见:对于任何运行时间超过几十毫秒的工具,卸载都能在几乎整个等待期间释放加速器内存,而恢复执行的成本几乎可以忽略。只有当工具运行时间短于往返时间时,保留状态才是合适的选择;只有当主机内存而非加速器内存是稀缺层级时,重新计算才是合适的选择。
异步分页换入换出
在推理运行时中实现物理块换入换出,需要与分页内存管理器深度集成。PagedAttention 建立的虚拟内存抽象将固定不变的逻辑序列块映射到不连续分配的物理页。为支持多层级换入换出,必须将页表扩展为双重驻留结构。
这一分层体系的物理架构和异步预取机制如 图 11 所示。服务系统将物理内存划分为三个层级:第 1 层(加速器 HBM,为活跃的 token 生成提供 3.0 TB/s 带宽)、第 2 层(锁页主机 DRAM,通过 54 GB/s 的 PCIe Gen5 DMA 连接,用于秒级工具暂停)和第 3 层(企业级 NVMe 存储阵列,为长时间的人机交互提供数 TB 的持久存储)。内存管理器使用带有驻留标签(TIER_HBM、TIER_HOST、TIER_NVME)的扩展块表,跟踪这些层级中的页框。如底部执行时间线所示,流水线预取掩盖了恢复执行时的停顿:当加速器张量核心对传入工具观测的第一个分块执行预填充时,独立的 DMA 复制引擎同时将历史块从主机 DRAM 流式传回 HBM,使重新加载延迟完全隐藏在正在进行的算术运算之中。
当智能体轨迹从活跃生成转入外部工具等待时,服务监管器启动块迁移:
- 驱逐调度与锁页: 调度器选择预测等待时间满足 \(T_{\text{wait}} > T_{\text{wait}}^*\) 的候选轨迹。引擎在预先分配的锁页主机 DRAM 缓冲池中分配一组连续的物理块页框。必须通过 POSIX
mlock或 CUDA 主机内存注册(cudaHostAlloc)锁定主机内存;标准操作系统的可分页内存会在直接内存访问期间引发主机操作系统缺页异常,导致 PCIe 传输带宽急剧下降。 - 异步 DMA 派发: 运行时向专用的双向 CUDA 复制流发出非阻塞复制调用(
cudaMemcpyAsync)。由于现代加速器架构提供了可与主张量计算引擎并发运行的独立复制引擎,这种传输会在后台进行,不会占用相邻流上正在运行的活跃解码批次的计算周期。 - 块表重映射: DMA 传输完成后,异步主机回调更新轨迹的块表条目,将其位置标签从
TIER_GPU切换为TIER_HOST,并记录主机页框索引。释放的 GPU 物理页框编号立即被推入分配器的空闲块 FIFO 队列,供并发请求立即预留。
# Empirical Block Table Migration Interface
def swap_out_trajectory(seq_id: int, block_table: BlockTable,
gpu_alloc: PageAllocator, host_alloc: PageAllocator,
copy_stream: Stream) -> None:
gpu_frames = block_table.get_physical_frames(seq_id, Tier.GPU)
host_frames = host_alloc.allocate_blocks(count=len(gpu_frames))
# Asynchronously stream blocks over PCIe copy stream
copy_stream.issue_dma(src=gpu_frames, dst=host_frames,
bytes_per_block=BLOCK_BYTE_SIZE)
# Remap logical pointers and release device HBM frames
copy_stream.record_callback(
lambda: (block_table.remap(seq_id, Tier.HOST, host_frames),
gpu_alloc.free_blocks(gpu_frames))
)返回路径上的主要工程挑战是隐藏重新加载延迟。如果引擎等到工具返回最终载荷后才发出换入请求,模型就会经历一个时长等于 \(M_{\text{KV}} / B_{\text{pcie}}\) 的流水线空泡。现代智能体运行时采用两种系统技术来消除这一停顿:
提前推测性预取:工具执行很少是持续时间完全未知的黑盒。对持续集成运行、Web API 往返调用或终端执行进行性能剖析,可以得到可预测的延迟下界。如果某次工具调用的实测运行时间中位数为三秒、最短为两秒,那么在 \(t = 1.8\text{ seconds}\) 时刻触发的定时器就可以在外部进程发出完成信号之前,启动异步加载,将该轨迹的 KV 块重新载入 GPU HBM。如果工具在三秒时完成,其历史注意力状态就已经驻留在加速器内存中,从而将用户感知到的恢复延迟降至零。
流水线化分块重新加载与观测预填充:工具执行结束时,会返回一个长度为 \(S_{\text{obs}}\) 的观测字符串(例如 bash 命令的标准输出)。引擎必须对这 \(S_{\text{obs}}\) 个 token 进行预填充,才能继续该轨迹。在分块预填充调度下,引擎将传入的观测拆分为独立的块(例如,每块五百一十二个 token)。当第 0 块在张量核心上执行前向投影 GEMM 时,复制引擎同时通过 PCIe 总线,以流式方式传输第 1 块至第 \(N\) 块所需的历史 KV 块。通过交错执行 DMA 传输与矩阵计算,传输延迟被完全隐藏在观测本身必须经历的预填充阶段之中。
分层辅助存储
虽然主机系统 DRAM 能为持续数秒的工具执行提供高效的暂存空间,但它无法解决人类参与的智能体交互所面临的扩展限制。在现代部署环境中,自主智能体经常会暂停,以请求人类用户确认破坏性操作、审查拉取请求或提供澄清反馈。人类响应时间服从对数正态分布,跨度从数分钟到数小时。在数千个暂停的智能体会话中,将数 GB 的注意力状态持续保存在主机 DRAM 中,会耗尽系统内存,挤占主机操作系统缓冲区所需的空间,并限制集群部署密度。
为了适应跨度较大的空闲时间窗口,运行时分层协议向下扩展至非易失性辅助存储(本地 NVMe SSD 阵列)和解耦的远程内存节点(图 11 中的第 3 层)。
当智能体进入等待人类交互的状态时,主机监督程序使用直接 I/O(O_DIRECT 或 Linux io_uring)或 GPUDirect Storage(GDS),将其注意力块从锁页主机 DRAM 迁移至异步磁盘引擎。通过绕过操作系统页缓存,引擎可以在 NVMe 存储控制器与内存缓冲区之间直接流式传输序列化的张量块,传输速率接近线路速率(每块驱动器 \(7\text{--}14\text{ GB/s}\))。
对于大小为 \(5\text{ GB}\) 的注意力状态,从企业级 NVMe 阵列读取序列化缓存大约需要四百至七百毫秒。如果人类监督者花了三分钟阅读终端中的差异并批准部署,那么五百毫秒的冷启动重新加载延迟完全无法察觉。因此,辅助存储分层可将服务成本降低一个数量级:活跃轨迹占用稀缺的 HBM;短暂工具暂停期间的轨迹驻留在主机 DRAM 中;而休眠且需要人类批准才能继续的轨迹则安全地持久化到廉价的闪存介质中,在不占用集群易失性内存的情况下,保留无损的前缀复用能力。
动态保留、逐出、重新计算或卸载注意力状态的能力,确保推理引擎不会因外部工具调用不规律的空闲节奏而崩溃。然而,知道如何换入换出单条轨迹,并不能回答基础设施运营者面临的宏观架构问题:为了在不陷入内存抖动的情况下,承载按目标到达率进入系统的并发、分支智能体,需要在整个集群中配置多少物理内存总量?要回答这一问题,就必须对多轮交互聚合工作负载下的注意力缓存容量边界进行建模。
智能体工作负载的容量
基于标准对话流量模型为自主智能体部署配置高带宽内存,会直接导致灾难性的运行故障:内存不足引发的抖动、紧急页面驱逐,以及持续数秒的尾延迟尖峰。在传统对话服务中,传入请求的提示较短,生成的响应长度适中,通常只有几百个 token,并在完成后永久终止。基础设施工程师为此类聊天工作负载确定集群规模时,可以放心地假设序列长度分布是平稳的,为每条活跃流预留适量且固定的物理键值内存。自主智能体轨迹违背了该对话模型所依赖的每一项统计假设。执行多轮软件工程、验证或研究工作流的智能体,会在数十乃至数百个推理步骤中单调累积上下文,反复摄入大量工具输出、编译器诊断信息和文件快照,直到提示长度经常超过数万个 token(\(M \gg 10^4\))。此外,当宿主监督程序执行沙箱中的编译器或等待网络 RPC 时,智能体轨迹会出现长时间且极不规则的空闲间隔,其间还穿插着从同一祖先状态分叉出多条搜索路径的推测性分支。
智能体基础设施的服务容量不能按活跃请求并发数的标量倍数来确定;必须将其设计为一个动态的物理容量包络,由多轮交互中累积的 token 总量、前缀复用拓扑,以及高内存压力下的尾延迟分位数共同决定。 当并发轨迹的 token 总需求超过物理高带宽内存(\(C_{\text{HBM}}\))容量时,底层分页内存管理器只能暂停正在进行的解码、通过 PCIe 总线将页面块迁移至主机内存,或彻底丢弃非连续块。由此产生的延迟代价并不体现在模型的平均吞吐量上,而是体现在首 token 时间(TTFT)和 token 间延迟(ITL)的尾部分位数上。因此,为智能体工作负载确定推理集群规模,需要将神经网络架构的结构几何特征和多轮轨迹的随机行为转化为严格的容量边界。
工作负载动态:多轮智能体的差异
推理引擎的物理内存占用取决于对话请求与智能体执行图之间的差异。在标准对话系统中,序列长度的分布呈单峰形态,且有严格的上下界,因此服务运营者可以在活跃计算批次槽位与高带宽内存块之间配置可预测的比例。每个请求在输出序列结束 token 时便会释放其内存块,这一时刻也是 被动请求边界 的请求边界,因此内存回收频繁、确定,并且与 token 输出紧密耦合。
分组查询注意力(GQA)通过让多个查询头共享一对键值头来减轻物理缓存压力(\(H_{\text{kv}} \ll H_q\))。对于查询头与 KV 头之比为 \(8:1\) 的模型,每个 token 的物理内存占用会缩小一个数量级,从而在物理 HBM 池容量相同的情况下,使并发能力按比例提升。
智能体工作负载从三个物理维度打破了这种运行规律:上下文累积速率、执行的时间特征,以及图拓扑。首先,智能体会话中的上下文累积是单调的,并且不时伴随大规模的阶跃式注入。宿主运行时并非仅交换简短的自然语言语句,而是在每一轮向语言模型传递大量环境反馈。一次工作区搜索工具或测试套件运行器的调用,就可能将两万个 token 的原始终端输出直接注入提示缓冲区。在一条十轮轨迹中,提示上下文 \(M\) 并非只是线性增长;随着智能体保留对话历史、工具执行清单和中间暂存记录,累积效应会不断叠加。仅根据初始轮次确定内存容量,必然会在后续轮次中导致内存饥饿或抖动。
其次,智能体请求的时间生命周期本质上是异步且呈双峰形态的。人类用户通常用数十秒阅读并作出回应,而智能体运行时则交替经历高强度自回归解码的突发阶段,以及模型完全空闲的长时间暂停阶段。当宿主环境编译代码、启动沙箱容器或等待缓慢的外部 API 响应时,轨迹累积的键值状态仍驻留在加速器内存中。如果服务运行时将这些物理页面保留在 HBM 中,以确保下一轮恢复时的延迟接近零,那么这些已分配的块就会完全闲置,使其他竞争流中正在进行的解码陷入内存饥饿。反之,如果运行时过早驱逐或卸载这些页面,后续的预填充阶段就必须承担从主机到设备的 PCIe 传输(\(B_{\text{xfer}}\))或完整的二次复杂度 token 重计算(\(2P\))所带来的延迟开销。
第三,智能体执行图通常是非线性的。自主问题求解经常依赖推测执行、束搜索或思维树探索,在这些过程中,智能体从同一检查点分叉出两个或更多子智能体,并行评估不同的工具调用。在具备写时复制语义的分页内存架构中,这些子轨迹共享其祖先的前缀块,而不复制物理状态。然而,一旦这些分支流发生分歧并生成相互冲突的候选 token,其页表就会分裂。总体物理内存分配从紧凑的共享主干转变为由独立叶节点分配构成的发散树,显著提高单位实际时间内的总体块消耗速率。在确定基础设施集群规模时,如果未显式考虑前缀共享比例和分支分歧因子,就会严重低估容量需求。
容量的解析表达式
确定推理节点的物理加速器内存容量,首先需要考虑基础模型的结构参数和服务运行时的分配预算。加速器节点上可用的高带宽内存总量记为 \(C_{\text{HBM}}\),必须容纳四类互不重叠的物理内存占用:静态模型参数权重(\(M_{\text{weights}}\))、预填充和解码矩阵运算期间存储中间层激活所需的动态暂存存储器缓冲区(\(M_{\text{activations}}\))、为 CUDA 上下文开销和分配器对齐预留的运行时系统余量(\(M_{\text{runtime}}\)),以及为键值张量分配的物理分页内存池(\(M_{\text{KV}}\))。全局容量约束如 公式 3 所示:
\[C_{\text{HBM}} = M_{\text{weights}} + M_{\text{activations}} + M_{\text{runtime}} + M_{\text{KV}} \tag{3}\]
模型权重的内存占用不随工作负载的并发度变化。对于参数量为 \(N_{\text{params}}\)、以每个参数 \(b_{\text{weight}}\) 字节的数值精度存储,并按张量并行度 \(TP\) 和流水线并行度 \(PP\) 分布的模型,每个加速器设备的静态权重内存占用由 公式 4 给出:
\[M_{\text{weights}} = \frac{N_{\text{params}} \cdot b_{\text{weight}}}{TP \cdot PP} \tag{4}\]
激活内存 \(M_{\text{activations}}\) 随最大分块预填充大小 \(C_{\text{chunk}}\)、隐藏维度和注意力头数量增大而增加,但始终受静态调度阈值约束。剩余容量即完全用于分页注意力块的物理内存预算(公式 5):
\[M_{\text{KV}} = C_{\text{HBM}} - \left( M_{\text{weights}} + M_{\text{activations}} + M_{\text{runtime}} \right) \tag{5}\]
单个 token 所占用的物理 KV 缓存状态大小记为 \(m_{\text{token}}\),完全由底层 Transformer 架构决定:Transformer 层数 \(L\)、键值注意力头数量 \(H_{\text{kv}}\)、每个头的维度 \(d_{\text{head}}\),以及所存储状态的数值精度,以字节为单位,记为 \(b_{\text{elem}}\)(16 位浮点数通常为两字节,FP8 表示则为一字节)。由于每个注意力块在所有层中都保留键投影向量和值投影向量,因此整个模型中每个 token 的物理内存开销定义如 公式 1 所示:
\[m_{\text{token}} = 2 \cdot L \cdot H_{\text{kv}} \cdot d_{\text{head}} \cdot b_{\text{elem}}\]
当在 \(TP\) 个设备上进行并行化时,每个设备上的单 token 内存占用为 \(m_{\text{token}} / TP\)。对于目标并发度为 \(B\) 条同时活跃轨迹的情况,若每条轨迹的平均提示上下文长度为 \(M_{\text{avg}}\),平均生成响应长度为 \(K_{\text{avg}}\),则在无前缀共享时,所需的物理 KV 内存总量由 公式 6 给出:
\[M_{\text{KV}}^{\text{demand}} = B \cdot (M_{\text{avg}} + K_{\text{avg}}) \cdot m_{\text{token}} \tag{6}\]
在生产环境的智能体部署中,完全相同的前缀共享会改变上述表达式。如果智能体集群基于共同的基础系统指令、代码仓库映射或工具定义执行任务,则并发轨迹中平均占比为 \(S_{\text{shared}}\) 的提示 token 会在基数树中去重。此外,在分支轨迹中,子流引用共同的父级块表。引入有效前缀共享效率因子 \(\sigma \in [0, 1)\),其中 \(\sigma = 0\) 表示序列完全不同,\(\sigma \to 1\) 表示结构完全去重,则有效内存需求变为(公式 7):
其中,\(\text{Pool}_{\text{headroom}}\) 表示分配器必须维持的空闲物理块余量,用于防止突发解码步骤期间发生分配失败。
将 公式 7 应用于 节 1 中的节点,预留 {python} AgentCapacity.headroom_pct_str 的余量,剩余可用容量为 {python} AgentCapacity.usable_str。如果每条轨迹携带 {python} AgentCapacity.ctx_tokens_str 个上下文词元,并生成 {python} AgentCapacity.out_tokens_str 个词元,且不共享任何内容,则该节点可容纳 {python} AgentCapacity.b_unshared_str 条轨迹。如果这些上下文词元中的 {python} AgentCapacity.shared_tokens_str 个词元构成共用的系统提示词、工具集和代码仓库映射,并且仅存储一份({python} AgentCapacity.shared_gb_str),则每条轨迹的私有状态缩减为 {python} AgentCapacity.unique_tokens_str 个上下文词元加上其输出,同一节点可容纳 {python} AgentCapacity.b_shared_str 条轨迹,为原来的 {python} AgentCapacity.share_ratio_mult_str 倍。
粗略估算 0.7: 为 8-GPU 节点上的多轮智能体群配置 HBM 容量
步骤 1:计算每个 GPU 的静态内存分配量 模型权重占用: \[M_{\text{weights}} = \frac{70 \times 10^9 \times 2}{8} = 17.5 \times 10^9\text{ bytes} = 17.5\text{ GB}\] 运行时环境、CUDA 上下文和中间激活缓冲区在每个 GPU 上预留 \(M_{\text{activations}} + M_{\text{runtime}} = 4.5\text{ GB}\) 的静态内存。每个设备的非 KV 预留内存总量为 \(17.5 + 4.5 = 22.0\text{ GB}\)(整个节点合计 \(176\text{ GB}\))。
步骤 2:确定可用的分页 KV 缓存内存池 每个 GPU 上剩余可用于分页 KV 块池的物理内存为: \[M_{\text{KV}}^{\text{device}} = 80.0\text{ GB} - 22.0\text{ GB} = 58.0\text{ GB}\] 整个 8-GPU 节点的 KV 缓存内存池总容量为 \(M_{\text{KV}} = 8 \times 58.0 = 464.0\text{ GB}\)。
步骤 3:计算每个词元的内存占用 在所有层中,单个词元的键投影和值投影合计占用: \[m_{\text{token}} = 2 \cdot 80 \cdot 8 \cdot 128 \cdot 2 = 327,680\text{ bytes} \approx 320\text{ KiB}\] \(TP=8\) 张量并行组中的每个 GPU 恰好存储: \[m_{\text{token}}^{\text{device}} = \frac{327,680}{8} = 40,960\text{ bytes} = 40\text{ KiB/token}\] 8-GPU 节点的总词元容量为: \[N_{\text{capacity}} = \frac{464.0 \times 10^9\text{ bytes}}{327,680\text{ bytes/token}} \approx 1,416,015\text{ tokens}\]
步骤 4:确定无共享智能体工作负载下的并发容量 假设智能体处理代码仓库上下文,平均累积上下文为 \(M_{\text{avg}} = 32,768\) 个词元(\(32\text{k}\)),平均解码响应为 \(K_{\text{avg}} = 2,048\) 个词元,且完全不共享前缀(\(\sigma = 0\))。按 \(\text{Pool}_{\text{headroom}} = 10\%\) 的安全余量规划容量,可得到 \(0.90 \times 1,416,015 = 1,274,413\text{ tokens}\) 的可用词元容量。 可持续支持的最大并发智能体轨迹数为: \[B = \left\lfloor \frac{1,274,413}{32,768 + 2,048} \right\rfloor = \left\lfloor \frac{1,274,413}{34,816} \right\rfloor = 36\text{ concurrent trajectories}\]
步骤 5:确定结构化前缀共享下的并发容量 现在考虑相同的智能体工作负载,采用标准化的基础系统提示词和共享的代码仓库 AST 索引,其中包含所有智能体共用的 \(S_{\text{shared}} = 24,576\) 个词元,使每个智能体仅需保留 \(8,192\) 个独有的轮次词元(\(M_{\text{unique}} = 8,192, K_{\text{avg}} = 2,048\))。 静态共享前缀占用: \[M_{\text{shared}} = 24,576 \times 327,680\text{ bytes} \approx 8.05\text{ GB}\] 剩余的可用词元池(\(464.0 \times 0.90 - 8.05 = 409.55\text{ GB}\))专用于各智能体的独有状态: \[B_{\text{shared}} = \left\lfloor \frac{409.55 \times 10^9}{(8,192 + 2,048) \times 327,680} \right\rfloor = \left\lfloor \frac{409.55 \times 10^9}{3.355 \times 10^9} \right\rfloor = 122\text{ concurrent trajectories}\] 通过在分页 Radix 树中从结构上隔离并共享不变的工作空间上下文,相同的物理硬件可支持并发智能体密度提升 \(3.38\times\),而无需更改模型权重或精度。
要理解硬件进步如何改变这些容量上限,可以考察现代不同代际加速器的运行能力范围。KV 缓存容量范围对比 对比了部署 700 亿参数模型的 8-GPU NVIDIA H100 节点与 8-GPU NVIDIA B200 节点。从 HBM3 向高密度 HBM3e 的转变,使每个设备的内存从 80 GB 扩展至 192 GB,从根本上改变了静态权重存储与动态 KV 缓存容量之间的比例。
| 节点架构 | 节点 HBM 总容量(\(C_{\text{HBM}}\)) | 模型权重与额外开销 | 净 KV 缓存池容量(\(M_{\text{KV}}\)) | 上下文长度(\(M + K\)) | 并发数:冷缓存(\(B_{\text{cold}}\)) | 并发数:共享 60%(\(B_{\text{shared}}\)) | 最大内存带宽 |
|---|---|---|---|---|---|---|---|
| 8× H100 SXM5 | 640 GB | 176 GB | 464 GB | 8,192 (8k) | 155 | 298 | 26.8 TB/s |
| 8× H100 SXM5 | 640 GB | 176 GB | 464 GB | 32,768 (32k) | 38 | 84 | 26.8 TB/s |
| 8× H100 SXM5 | 640 GB | 176 GB | 464 GB | 65,536 (64k) | 19 | 44 | 26.8 TB/s |
| 8× H100 SXM5 | 640 GB | 176 GB | 464 GB | 131,072 (128k) | 9 | 22 | 26.8 TB/s |
| 8× B200 SXM | 1,536 GB | 200 GB | 1,336 GB | 8,192 (8k) | 448 | 861 | 64.0 TB/s |
| 8× B200 SXM | 1,536 GB | 200 GB | 1,336 GB | 32,768 (32k) | 112 | 244 | 64.0 TB/s |
| 8× B200 SXM | 1,536 GB | 200 GB | 1,336 GB | 65,536 (64k) | 56 | 127 | 64.0 TB/s |
| 8× B200 SXM | 1,536 GB | 200 GB | 1,336 GB | 131,072 (128k) | 28 | 65 | 64.0 TB/s |
KV 缓存容量范围对比 中的数据揭示了上下文扩展所面临的物理限制。在 H100 节点上,若运维人员试图支持长达 128k 个词元的深度智能体轨迹,则同时可承载的独立流不足十条。如果工作负载骤增至十二个并发请求,系统就会从受内存约束的计算状态转变为物理页耗尽状态。节点必须转而通过 PCIe 进行同步换入换出,或者完全拒绝传入的交互轮次。相反,在 B200 节点上,可用 KV 缓存内存池近三倍的扩容(\(M_{\text{KV}}\))使瓶颈重新转向并发解码过程中内存总线带宽的饱和。
服务遥测动态
推理集群的资源配置不能完全依赖静态的离线公式;它需要实时观测内存遥测数据,以检测稳定服务与抖动之间相变的发生。分页注意力运行时必须提供低开销的监测手段,详细呈现内存分布、缓存复用和分配器争用情况。决定节点健康状况的主要指标是物理 KV 缓存利用率,记为 \(U_{\text{KV}} \in [0, 1]\)(公式 8):
\[U_{\text{KV}} = \frac{N_{\text{allocated\_blocks}}}{N_{\text{total\_blocks}}} \tag{8}\]
其中,\(N_{\text{total\_blocks}} = \lfloor M_{\text{KV}} / \text{BlockSize} \rfloor\)。传统无状态计算集群将服务器利用率超过 90% 作为目标,以最大限度提高硬件效率;而承载智能体轨迹的推理引擎在 \(U_{\text{KV}}\) 接近 1 时,会出现严重的非线性不稳定性。由于自回归生成会随着每条流输出词元而动态分配新的物理块,在 \(U_{\text{KV}}\) 较高的状态下运行,会导致剩余余量不足以应对同时进行的解码。当所有物理块耗尽时,运行时无法无限期暂停解码步骤,否则就会违反服务级别目标。
前缀缓存命中率 \(H_{\text{prefix}}\) 直接决定实际所需的预填充计算量。当 \(H_{\text{prefix}} \to 1\) 时,传入的交互轮次会完全跳过具有二次复杂度的注意力计算,将原本受计算限制的预填充阶段转变为无需计算的内存查找。
当 \(U_{\text{KV}}\) 越过临界阈值(通常为 \(\tau_{\text{high}} \approx 0.85\) 至 \(0.90\))时,分页内存分配器会进入紧急回收状态。为衡量达到该阈值之前的内存复用效率,运维人员会跟踪前缀缓存命中率 \(H_{\text{prefix}}\)(公式 2):
\[H_{\text{prefix}} = \frac{\sum_{i=1}^{R} S_{\text{match}}^{(i)}}{\sum_{i=1}^{R} S_{\text{prompt}}^{(i)}}\]
这种崩溃会触发灾难性的反馈循环:前缀命中的丧失迫使每一轮都重新执行完整的预填充计算,推高 GPU 计算利用率,并增加首 token 延迟(TTFT)。
内存利用率与服务性能之间的非线性关系体现为 图 12 中的抢占墙。在稳定运行区间(\(U_{\text{KV}} < 0.70\))内,性能中位数保持平稳:\(p50\) token 间延迟维持在 \(30\text{ ms}\) 附近,\(p50\) TTFT 维持在 \(120\text{ ms}\) 附近,因为内存查找直接访问驻留在 HBM 中的数据,不会发生分配器争用。然而,一旦占用率进入警告区间,并突破 \(\tau_{\text{high}} = 0.88\) 处的准入上限,尾延迟便会急剧攀升。尽管延迟中位数看似不受影响,\(p99\) 尾延迟却会从 \(45\text{ ms}\) 呈指数级上升,超过 \(3{,}000\text{ ms}\),原因包括同步 PCIe 换入换出停顿、Radix 树中的紧急叶节点驱逐,以及生成过程中的线程抢占。仅跟踪 \(p50\) 指标的调度器无法察觉这种性能退化,直到正在运行的智能体循环开始超时。
如性能曲线所示,活跃流的 token 间延迟取决于矩阵向量乘法(GEMV)过程中从 HBM 流式读取模型权重和 KV 块所需的时间;只要物理块驻留在加速器内存中,\(p50\) ITL 就不随全局已分配块数变化。
当利用率进入抢占区间时,尾部指标会急剧攀升。当一批同时发生的 token 输出尝试从已经耗尽的块池(\(U_{\text{KV}} \to 1.0\))中分配块时,引擎必须同步执行缓解措施。在卸载策略下,运行时会选择待换出的轨迹,暂停其执行,并通过 PCIe 总线将其物理页块传输到主机 DRAM。虽然这可以防止内存不足导致的崩溃,但主机到设备的传输延迟会使活跃流和暂停的流都出现停顿。如果引擎改用重计算策略,被驱逐的块就会被丢弃;当暂停的轨迹恢复执行时,引擎必须针对整个累积序列长度重新执行预填充阶段,使原本二十毫秒的解码步骤变成持续数秒的预填充停顿。因此,尽管 \(p50\) ITL 仍被限制在三十毫秒以内,\(p99\) ITL 却会飙升至数秒以上,直接违反交互式智能体监督循环所要求的响应性保障。
为防止这种延迟崩溃,具有韧性的智能体服务架构会实施与内存遥测联动的多级准入控制。服务系统同时监控瞬时利用率 \(U_{\text{KV}}\) 和所有活跃流的上下文增长速率。调度器会在 \(\tau_{\text{high}}\) 处设置准入上限,以防物理块池达到完全饱和。当利用率超过这一屏障时,新的智能体轮次会被放入外部调度队列,活跃轨迹中的推测性分支则会暂时受到限制。通过在新请求的排队延迟与执行中请求的灾难性尾延迟退化之间进行权衡,基础设施能够维持物理内存的完整性,确保活跃的智能体轨迹顺利完成,而不至于陷入内存换入换出的深渊。
准确配置物理内存容量并管理动态缓存分配的能力,能够确保单个服务节点承载高密度的智能体轨迹,而不会因内存耗尽而失效。然而,即使基础设施运维人员以数学上的精确性确定硬件容量,并实施严格的准入边界,生产部署仍经常遭遇系统性退化,其根源并非物理硬件约束,而是存在缺陷的运行假设。工程师经常陷入关于注意力内存在生产规模下如何表现的架构误区——认为上下文的线性扩展意味着内存的线性增长,认为无论采用何种缓存替换策略,前缀去重都普遍有益,或者认为虚拟化消除了端到端容量规划的必要性。揭示这些根本性误解,是弥合理论上的内存虚拟化与可靠生产系统之间差距的关键。
谬误与陷阱
设计虚拟化注意力内存系统,需要协调高带宽内存分配器的确定性机制与自主智能体运行时随机、非单调的执行模式。由于物理 KV 缓存将自回归注意力算子的原始张量抽象为非连续的块表和共享基数树,系统架构师常常将传统数据库或虚拟内存的抽象套用到这些结构上,而这些结构遵循的是完全不同的物理与数学约束。
当这些概念上的套用在生产负载下失效时,由此引发的故障并不会表现为明确的编译时异常或局部的运行时崩溃。相反,它们会造成灾难性的系统颠簸、静默的认知信息损坏,以及多租户尾延迟性能的严重崩溃。本节探讨在底层注意力缓存与高层智能体执行之间建立联系时出现的四种普遍谬误与陷阱。
谬误: KV 缓存是智能体的第二层语义记忆。
一种普遍的架构误解,将推理引擎的物理 KV 缓存视为中间语义存储层,在层次结构上位于临时的逻辑上下文窗口与持久的外部数据库之间。按照这种观点,由于缓存能在多个对话轮次中保留先前处理过的 token 的已计算数学表示,工程师便假定它可以作为任务事实的情景记忆,支持查询、修改或验证。基于这一假设设计的系统,会尝试检查缓存的注意力状态以获取事实断言,通过原地修改缓存来更新过时的世界状态,或依赖缓存驻留来保证分布式智能体任务之间的语义连续性。
这一前提混淆了对不变算子的数学加速与语义状态的保留。存储在加速器高带宽内存中的键向量和值向量,是在每个 Transformer 层的前向传播过程中生成的中间线性投影。对于 token 位置为 \(i\) 的智能体轨迹,给定层输入 \(\mathbf{x}_i^{(l)} \in \mathbb{R}^{d_{\text{model}}}\),缓存条目为 \(\mathbf{k}_i^{(l)} = \mathbf{x}_i^{(l)} \mathbf{W}_K^{(l)}\) 和 \(\mathbf{v}_i^{(l)} = \mathbf{x}_i^{(l)} \mathbf{W}_V^{(l)}\)。应用旋转位置嵌入(RoPE)时,每个键向量 \(\mathbf{k}_i^{(l)}\) 都会在二维坐标切片内通过正交变换矩阵 \(\mathbf{R}_{\Theta, i}^d\) 进行旋转,将该激活与其绝对序列索引 \(i\) 不可分割地绑定。
这些缓存张量完全不具备独立的语义可寻址性。无法通过查询存储的键向量 \(\mathbf{k}_i^{(l)}\) 来判断某个变量赋值是否仍然有效,也无法单独提取值向量 \(\mathbf{v}_i^{(l)}\) 来更新已过期的 API 凭据或修改某项环境事实。只有在缩放点积注意力计算中将这些表示整体组合起来,它们才具有运算意义:
\[\mathbf{a}_t^{(l)} = \text{Softmax}\left(\frac{\mathbf{q}_t^{(l)} (\mathbf{K}_{1:t}^{(l)})^T}{\sqrt{d_{\text{head}}}}\right) \mathbf{V}_{1:t}^{(l)}\]
由于注意力是作用于整个历史前缀 \(1 \dots t\) 的整体算子,从一个上下文中抽取孤立的 KV 激活块并将其拼接到另一个上下文中,会破坏位置相位关系,并打断 token 之间的上下文依赖。
此外,KV 缓存完全不提供认知层面的验证。无论提交给模型的是何种 token 字符串,它都会忠实地生成并保留其中间激活,赋予已验证的工具输出、过时的环境观测、模型幻觉以及恶意提示注入同等的物理地位。将 KV 缓存视为语义记忆,是把派生量误当成了存储。注意力状态是精确 token 前缀的函数(原则 5),因此它不包含 token 本身未包含的任何内容,同时也保留了其中的错误。
持久的语义状态应严格归属于宿主监督程序的确定性存储架构——外部关系数据库、预写日志,以及经过模式验证的产物树。推理引擎的 KV 缓存是一种不具备特权的物理暂存存储器,其唯一的架构用途是在自回归解码过程中消除冗余的广义矩阵—向量乘法。
陷阱: 无论等待时间多长,都让暂停状态保持驻留。
当自主智能体轨迹暂停自回归生成,以调用外部工具、派发子智能体或等待人工授权时,宿主监督器必须决定如何处置该序列的 KV 缓存所占用的物理存储。智能体服务部署中一个普遍的陷阱是无限期地将这些物理 KV 块固定在加速器的高带宽内存(HBM)中,其依据是:保持缓存驻留可以避免外部调用返回时重新计算提示词所带来的延迟代价。
这一策略的失效模式是容量被闲置占用。以我们的典型 700 亿参数 GQA 基础模型为例(\(m_{\text{token}} \approx 320\text{ KiB/token}\),其具体表述见 **离散 token 表示**)。一个正在执行复杂代码仓库重构轨迹、当前有效上下文长度为 \(S = 32{,}768\) 个词元的智能体,会占住以下容量:
\[M_{\text{KV}} = S \cdot m_{\text{token}} = 32{,}768 \times 320\text{ KiB} = 10{,}485{,}760\text{ KiB} = 10\text{ GiB}\]
如果该智能体在隔离的沙箱容器内派发一套异步集成测试,且测试需要 \(T_{\text{wait}} = 45\text{ seconds}\) 才能完成,那么保持这部分分配的内存驻留,就会使 10 GB 稀缺的加速器容量闲置四分之三分钟,这相当于 内存滞留摩擦 在较小上下文下的工具等待内存开销。在配备 80 GB HBM 的加速器上,这一条休眠轨迹独占了物理内存总量的 12.5%,却没有执行任何浮点运算。
在多租户工作负载下,这种被闲置占用的容量使活跃请求无法获得所需的分配块。PagedAttention 内存管理器承受了人为造成的内存压力,迫使服务调度器推迟新到达的预填充请求、缩小活跃解码批次,并触发对其他正在运行的请求的非自愿抢占式换出。
从经济与运维角度看,这一权衡具有显著的不对称性。按照 节 6 中盈亏平衡分析笔记本给出的节点持续预填充吞吐量(\(R_{\text{prefill}} = 4.0 \times 10^{14}\text{ FLOPs/s}\)),在轨迹恢复时完整地重新计算包含 \(32\text{k}\) 个词元的前缀,大约需要:
\[T_{\text{recomp}} \approx \frac{2 \cdot (70 \times 10^9) \cdot 32{,}768}{4.0 \times 10^{14}\text{ FLOPs/s}} \approx 11.5\text{ s}\]
而通过 PCIe Gen5 \(\times 16\) 总线,以 \(B_{\text{pcie}} \approx 50\text{ GB/s}\) 的有效带宽卸载这些块,所需时间为:
\[T_{\text{reload}} = \frac{10\text{ GiB}}{50\text{ GB/s}} \approx 215\text{ ms}\]
每个方向均需上述时间,因此这里相对于重新计算仍具有相同的 \(53\times\) 优势,与 \(16\text{k}\) 个词元时一致,因为这一比值不依赖于序列长度。将 10 GB 加速器 HBM 固定占用 45 秒,以避免一次 430 毫秒的往返传输,体现了严重的硬件资源分配失当。系统必须实施基于租约的保留策略。一旦预期工具等待时间超过盈亏平衡时间 \(T_{\text{wait}}^*\)(该时间在 节 6 中推导得出,并在内存压力下趋近于往返传输时间),运行时就必须解除这些块的固定驻留,并将其迁移到主机 DRAM。
谬误: 前缀缓存复用能够保证上下文是最新的。
在采用基数树前缀缓存的系统中,工程师经常认为,只要传入提示词与已有前缀键逐词元匹配,达到 100% 的一致性,对应的注意力激活就代表对任务状态的一次有效且最新的评估。这一谬误假定,词元域中的句法等价能够保证运行域中的语义时效性。
前缀缓存通过精确的词元标识向量 \(\mathbf{t}_{1:k} = (t_1, t_2, \dots, t_k)\) 为序列建立索引。它完全无法感知最初采样这些词元时所依据的带外环境。考虑这样一种智能体工作流:系统提示词包含工作区元数据、环境变量或模式定义:
System: You are an automated refactoring agent.
Workspace status: branch=main; clean; HEAD=a1c4f9.
Active file: src/runtime/allocator.c (hash: e4b2...8f).如果外部持续集成守护进程、另一个工作智能体或用户修改了磁盘上的 src/runtime/allocator.c,工作区的实际状态就会立即与其文本表示发生偏离。如果宿主运行时在准备下一次提示时,没有使描述旧文件的文本失效,或者错误地重新提交了上一轮的提示结构,服务引擎就会在其基数树中检测到完整的前缀匹配。
引擎会立即将传入请求的物理块表关联到历史 KV 节点,完全跳过预填充计算。注意力头会将新生成的查询 token 与缓存的中间激活进行注意力计算,而这些激活编码的是过时的文件内容。模型以高置信度和低延迟生成响应,但其推理在结构上已与当前真实状态脱节。
这种失效模式也会出现在分词边界处。如果提示构建例程动态拼接字符串,却不强制遵循规范的 token 边界对齐方式,那么相同的字符串可能因末尾空白或标点的不同而被分词为不同的整数 token 序列。反过来,不同的提示也可能产生相同的 token 子序列,从而在简单的哈希表中发生碰撞。
防御措施是使 上下文失效 失效,并在产生陈旧状态的位置实施。只有文件本身才是权威依据;由它派生的每一种视图,包括已准备好的文本以及根据该文本计算出的注意力状态,都会在文件被写入时变得过时(原则 7)。运行时会在下一次调用之前将过时的观测标记为失效或进行刷新。这一修改会改变观测所在位置的 token,因此缓存仍可复用该位置之前的稳定前缀。将工作区摘要绑定到前缀开头也能打破过时的匹配,但这会将变化最频繁的 token 放在最前面,违背 编排下一次调用 中按变化频率递增排列的布局,而且每次文件写入都会使整个缓存分支失效。
陷阱: 根据孤立请求的平均值规划容量。
基础设施工程师在为智能体服务确定加速器集群规模时,通常使用以平均序列长度和独立泊松到达过程为参数的经典排队论来计算硬件需求。他们计算平均提示长度 \(\bar{S}_{\text{prompt}}\)、平均生成长度 \(\bar{S}_{\text{gen}}\),将两者之和乘以每个 token 的平均内存占用 \(\bar{m}_{\text{token}}\),再用可用的加速器 HBM 容量除以这一静态平均值,以确定多租户并发上限。
这种容量规划方法应用于智能体工作负载时,会产生灾难性的失误。智能体交互违背了无记忆、同分布到达过程的所有标准假设。它们表现出两种结构性现象,导致物理 KV 缓存占用严重偏离高斯分布:
相关的轨迹分支: 在思维树探索、推测执行或多智能体辩论等搜索策略下,单个根请求会动态生成 \(B\) 个并行分支。尽管 PagedAttention 通过引用计数,在全部 \(B\) 条轨迹之间共享共同的根前缀块,但每个分支的私有叶节点内存分配会同时增长: \[M_{\text{branching}} = M_{\text{root}} + \sum_{b=1}^{B} S_{\text{gen}, b} \cdot m_{\text{token}}\] 单个任务的内存消耗可能在数十个解码步骤内突然增加一个数量级,造成巨大且相互关联的阶跃式内存需求峰值,远远超出按平均情况预留的余量。
工具等待导致的并发膨胀: 根据利特尔定律,稳定系统中活跃并发请求的平均数量为 \(N = \lambda W\),其中 \(\lambda\) 为到达率,\(W\) 为总驻留时间。在传统的无状态推理中,\(W\) 严格等于生成时长(\(S_{\text{gen}} / \text{decode\_throughput} \approx 2\text{--}8\text{ seconds}\))。在执行循环包含编译器调用、代码检查、API 轮询和数据库事务的智能体系统中,驻留时间会延长,将外部等待状态也包含在内: \[W_{\text{agent}} = T_{\text{generation}} + T_{\text{tool\_wait}}\] 其中,\(T_{\text{tool\_wait}}\) 通常介于 15 至 120 秒之间。如果服务运行时在这些暂停期间保持 KV 缓存驻留,那么有效并发量 \(N\) 会膨胀至原来的 \(5\times\) 至 \(20\times\) 倍,而输入请求到达率 \(\lambda\) 完全相同。
当动态内存占用接近加速器 HBM 的物理边界(饱和阈值 \(\mu \to 1\))时,这种重尾分布的方差会引发非线性的排队延迟。服务引擎耗尽其空闲物理块池。新到达的预填充请求无法获得初始块表,因而在准入边界处停滞。与此同时,正在运行的自回归解码循环会在生成过程中停滞,因为分配器无法为新生成的 token 映射物理块。
图 13 的时间序列曲线展示了这种陷入内存饱和的崩溃过程。在常规的独立请求流下,内存占用稳定地保持在安全运行上限 \(80\%\) 以下。相比之下,经历相关分支扩展(例如思维树探索)和工具等待导致的并发膨胀的智能体工作负载,会表现出快速的阶跃式内存占用增长。内存需求突破运行上限 \(80\%\),发生于 \(t \approx 55\text{ s}\);随后触及物理抖动边界 \(100\%\),发生于 \(t \approx 72\text{ s}\)。此时,服务引擎被迫执行级联式紧急抢占,驱逐正在解码的序列并裁剪休眠的前缀块。这会导致前缀缓存命中率骤降,并触发无休止的完整提示重计算,使加速器张量核心持续处于饱和状态。
为解决这种内存资源匮乏,运行时必须执行紧急抢占,强制驱逐或换出活跃序列。驱逐共享前缀节点会破坏后续轮次的缓存命中率,而暂停正在进行的解码则会迫使系统在之后执行代价高昂的完整重计算。系统进入持续抖动状态:加速器计算利用率骤降,因为张量核心将计算周期用于重新计算已被驱逐的中间状态,而非处理新 token。
尽管非高峰时段的平均延迟指标可能看起来尚可,但第 99 和第 99.9 百分位的首 token 延迟(TTFT)和 token 间延迟(ITL)会激增数个数量级。稳健的容量配置必须使用双峰重尾分布对智能体工作负载建模,根据分支并发峰值确定内存池容量,并在物理 HBM 达到饱和之前就严格执行准入负载卸除。
上述谬误与陷阱凸显了一个基本的系统事实:物理 KV 缓存既不是自主知识库,也不是可以通过简单启发式方法管理的弹性资源。应对这些失效模式,需要超越孤立的优化,将完整的内存层次结构整合为统一的架构框架。为阐明内存虚拟化、前缀去重、分块调度和动态驱逐如何共同构成可靠的执行基础,接下来我们将进入本章小结。
小结
服务系统通过将逻辑 token 序列与高带宽内存(HBM)的物理布局解耦,借助不连续的分页块实现张量分配虚拟化,并针对预填充和解码阶段不对称的算术计算需求调度执行,从而分配和复用活跃轨迹的物理注意力状态。键值(KV)缓存管理本质上是一个加速器资源分配问题,在架构上独立于运行时的逻辑上下文选择和宿主应用的持久化存储层。物理 KV 缓存是通过静态投影权重对上下文 token 进行投影而生成的中间衍生活化张量(\(W_K, W_V\))。它存在的唯一运行目的,是在自回归解码循环中以加速器 HBM 容量换取算术计算吞吐量,将每一步的 \(\mathcal{O}(S^2)\) 注意力重计算替换为 \(\mathcal{O}(S)\) 受内存带宽限制的矩阵—向量乘积。
在现代 Transformer 架构的物理结构下,每个 token 的注意力状态占用固定数量的加速器内存,即 \(m_{\text{token}} = 2 \cdot L \cdot H_{\text{kv}} \cdot d_{\text{head}} \cdot b_{\text{elem}}\) 字节,其中 \(L\) 表示 Transformer 层数,\(H_{\text{kv}}\) 表示分组查询注意力或多查询注意力下的键值头数量,\(d_{\text{head}}\) 表示每个头的投影维度,\(b_{\text{elem}}\) 表示以字节为单位的数值精度格式。由于智能体工作负载通过递归工具调用、多轮对话循环和推测性轨迹采样引入动态、非单调的序列扩展,按最坏情况下的序列长度(\(S_{\max}\))静态配置内存,会因内部碎片和外部碎片而浪费高达 80% 的加速器内存。通过分页分配将这一空间虚拟化,可利用每个请求的块表,将逻辑 token 偏移映射到任意物理页框,从而将内部碎片限制在最后一个块的尾部,同时完全消除外部碎片。通过 Radix 树将这种块虚拟化扩展到前缀缓存,可在分支轨迹和通用提示词框架之间共享物理页,使服务系统能够跳过冗余的预填充计算,而无需重复分配底层张量。
在高负载的多租户并发环境中,平衡这些分页结构还会带来相应的调度挑战。由于上下文预填充执行的是受计算能力限制的矩阵—矩阵乘法(GEMM),而 token 解码执行的是受内存带宽限制的矩阵—向量乘法(GEMV),不受约束的预填充会造成严重的队头阻塞,导致解码延迟恶化。分块预填充调度将到达的序列切分为计算量有界的块,并将这些块与批量解码步骤一起执行,以分摊这种干扰,稳定 token 间延迟。当内存饱和迫使系统逐出缓存时,系统必须评估是保持驻留、通过 PCIe 将页面写入主机 DRAM、按需重新计算状态,还是直接逐出。由于 KV 状态完全由提示词 token 派生而来,逐出带来的是算术重计算成本,而非持久状态丢失,因此在加速器总线带宽、内存容量和处理延迟之间形成了可预测的权衡。
要点回顾: 将激活虚拟化,释放加速器
- KV 内存占用取决于模型结构、序列长度和活跃并发量。 每个 token 的物理存储成本 \(m_{\text{token}} = 2 L H_{\text{kv}} d_{\text{head}} b_{\text{elem}}\) 严格由模型架构和精度决定。在智能体服务运行时中,大量 token 和多轮轨迹会使执行过程迅速从预填充阶段受计算能力限制的矩阵乘法(GEMM),转变为自回归解码阶段受内存带宽限制的矩阵—向量运算(GEMV),使加速器 HBM 容量成为整体系统并发量的主要上限。
- 分页分配和精确前缀共享消除了人为碎片和冗余计算。 将可变长度的逻辑 token 序列映射到不连续的物理内存块,可消除外部碎片,并将内部碎片限制在最后一个尾块内。将可复用的物理块组织成前缀树(如 Radix 树),可在分支轨迹和共享系统提示词之间实现写时复制共享,在不改变模型输出的情况下,大幅降低首 token 延迟(\(TTFT\))。
- 调度和逐出策略是由工作负载驱动的延迟决策。 预填充阶段和解码阶段直接竞争加速器内存总线带宽和计算单元。按分块预填充预算切分提示词求值过程,可在预填充吞吐量与解码尾延迟(\(ITL\))之间取得平衡。当内存压力达到峰值时,选择保留缓存块、通过 PCIe 卸载缓存块,还是逐出后重新计算,取决于请求到达分布、内存传输成本以及轨迹恢复的经验概率。
- 逐出 KV 状态影响的是重计算成本,而非持久的任务知识。 KV 缓存是一种易失、临时的加速器优化机制,并非权威的持久化层。在内存压力下逐出或换出物理页表,会在智能体轨迹恢复时带来延迟代价,但完全不会损害系统正确性;任何被逐出的注意力状态都可以从底层输入 token 序列逐比特重新生成。
分页、前缀共享和卸载都建立在注意力状态的同一个属性之上(原则 5)。注意力状态是精确 token 前缀的函数,因此只能在完全相同的前缀之间共享,并且可以随时从 token 重建。正是这一属性决定了逐出只会带来重计算成本,而绝不会造成任务知识丢失,也决定了重新加载与重新计算的成本比不依赖于序列长度,从而使本章能够将暂停决策归结为一个盈亏平衡时间。这也解释了为什么主机运行时可以按步骤、工具调用和轨迹进行推理,而服务引擎则将这些过程的注意力状态视为可根据成本进行分页、共享或丢弃的字节数据。
后续内容: 从服务状态到持久信息
解决这一矛盾,需要将临时的加速器加速机制与权威的长期存储解耦。本章解决了推理引擎内部易失性 KV 激活的物理分配与复用问题,而 长期记忆(持久存储)将研究持久信息层:持久化向量存储、关系数据库、倒排搜索索引和动态检索流水线。KV 缓存保存的是派生的注意力状态,引擎可以随时将其丢弃。持久存储保存的是源记录本身,其任务是在不限时长的运行期间,为智能体记录、组织并提供这些记录。

