笔记日期: 2026-08-16 作者: Zhongzhu Zhou 阅读论文: vToken: Token-Level Virtualization for Reclaimable KV Caches 论文作者: Yuanhang Gao, Xiangrui Yang, Yuanfeng Chen, Hongjia Chen, Qianru Lv, Wenfei Wu, Dongsheng Li(国防科技大学;北京大学) arXiv: 2608.13263 发表状态: arXiv 预印本,2026 年 8 月
1. 起点:两个各自都很成熟的想法,凑在一起却出问题
如果你读过一些 LLM 推理系统方面的论文,下面这两个想法你大概率都见过,而且单独拎出来看,每一个都像是”已经解决”的问题。第一个:PagedAttention(vLLM 提出)用固定大小的”块”(block)来管理 KV cache,而不是给每个请求分配一整块连续显存,这个思路借鉴了操作系统里虚拟内存分页的做法,现在几乎已经是所有主流 LLM 推理引擎管理显存的标配方式。第二个:token 级 KV 淘汰算法 —— H2O、StreamingLLM、Scissorhands 等一系列方法都观察到,并不是每一个历史 token 对预测下一个 token 都同等重要,把不重要的丢掉就能把 KV cache 的显存占用砍掉一半甚至更多,而生成质量损失很小。
把这两个想法真的放到一个系统里跑,却会发生一件意外的事:它们不能干净地组合在一起。淘汰算法做的是”逐 token”的决策。块管理器只知道怎么按”整块”来回收内存。如果一个 16-token 的块里,H2O 还想留一个 token,另外十五个 token 已经被标记为死掉,那这个块依然不能还给空闲池——它就那么占着,大部分是浪费的。这篇论文的核心实证发现是:这绝不是无关紧要的边角情况——在三种淘汰策略、两个工作负载上,由此产生的块内浪费率(intra-block waste ratio)高达 40%-60%,也就是说,你以为淘汰策略已经释放出来的内存,其实有很大一部分依然被锁死、对分配器不可见。vToken 要解决的正是这个问题——不是发明一个更好的淘汰算法,而是插入一层虚拟化,让已有的淘汰决策真正能够转化为可用的物理内存。
这篇笔记会依次讲清楚:这个粒度错配问题为什么在结构上必然发生;vToken 的 token 表设计和异步回收后端具体是怎么一步步运作的;论文为在解码过程中”动”活跃 KV 内存所做的死锁安全性和正确性论证;论文提出的六个评测问题分别得到了怎样的答案;最后再给出一些批判性的观察。
前置知识:开始之前需要了解的背景
KV cache 是什么,为什么它主导了推理服务的显存占用。 在自回归生成过程中,Transformer 在每一个解码步都要对所有历史 token 计算注意力。为了避免每一步都重新计算所有历史 token 的 Key、Value 投影,推理系统会把它们缓存下来——这就是 KV cache。对一个有 层、 个注意力头、每个头维度为 的模型来说,缓存一个 token 的 K、V 张量需要 字节。举个具体例子,一个 13B 参数、40 层、隐藏维度 5120 的 LLaMA 风格模型(FP16 精度),每个 token 大约要占 MB。在 32K 上下文长度下,光 KV cache 就会超过 25 GB——对很多推理服务的配置来说,这比模型权重本身还要大。由于 GPU 显存(HBM)是有限资源,而每个同时在服务的请求都需要自己的 KV cache,这块内存打包得有多紧凑,直接决定了你能同时服务多少请求(并发度)、每个请求能负担多长的上下文。这正是为什么 KV cache 管理(而非原始算力)通常才是 LLM 推理服务吞吐的真正瓶颈。
PagedAttention:基于块的 KV 内存管理。 vLLM 的 PagedAttention(2023)借用了操作系统里分页式虚拟内存的经典思路:与其给每个请求分配一整块连续的大缓冲区(这样会遇到经典的外部碎片问题——总体上内存是够的,但没有一块连续的、足够大的空闲区域可以分给新请求),PagedAttention 把 GPU 显存切分成固定大小的块(block,通常每块 16 个 token),并为每个请求维护一张块表(block table),把序列里的逻辑块位置映射到显存里的物理块位置,而这些物理块之间不必连续。这和操作系统页表把虚拟地址映射到物理页帧的原理完全一致。它带来的好处包括:块可以随序列增长按需分配、非连续物理存放消除了外部碎片、内容相同的块(比如共享的系统提示词前缀)可以被多个请求共享引用而不必重复存储(“前缀缓存”)。但 PagedAttention 提供的保证只到块这个粒度——它能高效地分配和释放整块内存,但它没有”这个块里的某一个 token 现在已经是垃圾了”这种概念。
Token 级淘汰算法。 另一条研究路线问的是不同的问题:给定一个 token 的位置和它被观察到的注意力模式,它是否还值得继续保留?H2O(“Heavy-Hitter Oracle”,重点命中神谕)为每个 token 维护一个滚动累积注意力分数,只保留分数最高的”重点命中者”(heavy hitters),其余全部丢弃——据报告可以在困惑度上升有限的情况下把缓存缩减多达 50%。StreamingLLM 观察到最初的几个 token(“注意力汇聚点”,attention sink)无论内容是什么都会获得不成比例的高注意力,于是保留这几个 token 加上最近的一个滑动窗口,丢弃中间的所有内容。Scissorhands 和 FastGen 基于稀疏注意力模式和逐头(per-head)token 角色提出了更精细的判定标准。Random(随机)淘汰在本文中被用作压力测试基线,它按均匀概率随机丢弃 token 以满足内存预算——之所以有用,正是因为它把幸存 token 打散得毫无规律,这对按块粒度回收来说是最坏情况。这些策略的共同点是:决策的单位是token,不是块。
为什么”干脆把块调小”不能解决问题。 听到”粒度错配”这个词,大多数人的第一反应会是:为什么不干脆把块变小一点——比如从 16 个 token 变成 4 个——这样每块被”困住”的比例不就小了吗?论文(结合之前系统研究的经验)的回答是:这是拿一个问题去换一个更糟的问题。块越小,每个序列需要的块数越多,意味着更多的元数据(每个块都需要一条表项)、每次注意力步骤更高的地址转换开销,以及——对 GPU 工作负载来说至关重要的——更多细碎、非连续的内存传输(无论是注意力计算、拷贝还是卸载路径)。GPU 显存子系统对大块、连续访存做了大量优化;为了对抗碎片而缩小粒度,恰恰攻击的是让 GPU 显存访问快起来的那个前提假设。而另一个极端——完全按 token 粒度分配(不分块)——因为 GPU 内存分配器有最小实际粒度、逐 token 记账的元数据开销会远超数据本身,同样不现实。vToken 的前提是:两个极端都没必要走,把基于块的物理层原封不动地保留下来,在它之上加一层薄薄的逻辑层,在不改变物理分配粒度的前提下暴露出 token 粒度的语义。
2. 提出方案之前,先量化这个错配问题有多严重
论文在提出任何设计之前,先用一整节篇幅确立这个错配问题确实真实存在且规模可观——这对一篇系统论文来说是正确的直觉:只有当这个错配造成的浪费真的足够大,一层额外的虚拟化才值得引入所带来的复杂度。
如何定义”浪费”。 对一个容量为 个 token 的物理块 ,设 为当前存储在里面的有效(未被淘汰)token 数,定义块利用率为 。给定 个已分配块,论文定义了块内浪费率(intra-block waste ratio):
解读一下公式(1):分子 是所有已分配块中实际存活的 token 总数;分母 是这些块的总容量。两者之比就是已分配容量中真正被使用的部分,而 就是一减去这个比值——被浪费掉的部分。当 token 级淘汰在没有虚拟化层的情况下运行时,一个 token 被标记淘汰会降低其所在块的 ,但块本身依然被完整分配着(分配器完全不知道其中一部分已经空出来了),所以 会随着淘汰的激进程度直接攀升。
如何测量。 论文在 Llama-3.1-8B 上、ShareGPT 和 LongBench 的 16K token 子集上、batch size 为 16 的条件下,用一个 H2O 风格的淘汰算法接入 vLLM,测量已分配块中利用率 的比例。

图 1(论文 Fig. 1):在全部三种淘汰策略和两个工作负载上,56%-67% 的已分配块处于 50% 或以下的利用率。Random 淘汰始终是表现最差的一个(ShareGPT 上 58%,LongBench 上 67%),原因正是它把幸存 token 打散得毫无规律,最大化了”任意给定的块里总有某个存活 token 挡着它被释放”的概率——这正是论文自己给出的、最能验证粒度错配理论的压力测试。
作者由此得出的结论值得细品:这里的损失并不是因为淘汰策略本身选得不好。 这些策略确实完成了自己的任务——降低了需要存活下来的逻辑KV 数据量。真正的损失是一个纯粹的运行时抽象缺失:没有任何机制把”这个 token 死了”转化为”这块物理内存空出来了”。这个重新框定,正是把修复方案定位为一层新增的虚拟化层、而不是一个更聪明的淘汰算法的原因。
第二个代价:算法集成的摩擦成本。 除了浪费内存本身,论文还指出了第二个更”人性化”的成本。在没有虚拟化层的情况下,把一个新的淘汰算法接入 vLLM,需要策略实现者理解块管理器内部机制,把逐 token 的保留决策翻译成逐块的操作,并手动保持注意力槽位映射与幸存 token 的一致性。论文报告,直接把 H2O 集成进 vLLM(不借助 vToken)需要跨多个核心模块修改 500 行以上代码,还需要额外投入不小的调试成本来确保内存安全——而每新增一种策略(StreamingLLM、Scissorhands……)都需要付出可比的全新努力,因为这些管道逻辑在不同策略之间无法复用。这种成本不会出现在任何一张评测图表里,但它决定了实验室里一个精妙的新淘汰算法,究竟能不能真正落地到生产级推理服务栈里。
3. vToken 的设计:在策略与物理内存之间划出一条虚拟化边界
3.1 设计目标,以及为什么”全 token 粒度分配器”也不是答案
论文用两个它刻意拒绝的”显然”解法来界定设计空间,前面也已提到:完全按 token 粒度分配的分配器(会破坏 PagedAttention 内核和元数据的假设)和缩小块大小(只是把碎片/带宽的权衡重新分配了一遍,并没有消除它)。vToken 真正的动作是架构层面的,而不是算法层面的:保持基于块管理的 KV 运行时完全不变——同样的 PagedAttention 内核、同样的 CUDA Graph 兼容性、同样的块分配器——在淘汰策略和这个运行时之间插入一层薄薄的虚拟化边界。
这条边界定义的契约是刻意不对称的。在边界之上,淘汰策略只操作逻辑 token 身份:它说”请求 3 里的 token 47 不再需要了”,完全不用知道、也不用关心 token 47 目前住在哪个物理块里、那个块能不能被释放、回收究竟什么时候真正发生。在边界之下,运行时拥有逻辑到物理的映射关系,刷新注意力内核读取的槽位映射,并独立决定何时值得花代价去物理性地重新打包块。这样解耦带来的关键好处是:判定一个 token 已死和回收它的物理内存变成两个可以分开、可以推迟、可以批量处理的事件——运行时不需要在 token 一死就立刻压缩内存(那样成本很高),它可以等到碎片化严重到明显值得压缩的时候再动手。
在解码正在进行的同时维持这个契约(也就是说,当注意力内核正在读取你可能想要迁移的那块内存时),会带来三个具体挑战,任何一种实现都必须同时解决:
- (C1)双视图一致性(Dual-view consistency)。 一个 token 可以独立于同一个块里的邻居而死去,但一个块只有在所有曾经存活于其中的 token 都被迁移或丢弃之后才能被释放。运行时需要能同时捕捉逐 token 存活状态和逐块占用情况的元数据。
- (C2)解码过程中的安全回收(Safe reclamation during live decoding)。 如果你把一个 token 的 KV 条目从一个块物理搬到另一个块,注意力内核的槽位映射(告诉它每个 token 该从哪个物理地址读取)必须在下一次注意力内核启动之前更新——但你不能为了安全起见,在每一个解码步都加一个全局同步点,那会给所有请求(包括根本没有任何迁移发生的请求)都增加负担。
- (C3)策略无关、摊销成本(Policy-neutral, amortized cost)。 H2O、StreamingLLM、Random 都需要共用同一套调度器、块管理器和 worker 布局代码——间接寻址的开销必须只在回收确实值得做的时候才付出,而不是不管有没有碎片都要在每个解码步支付的固定税。
vToken 对这三点的回应,简单概括就是:一张逐请求的token 表提供 C1 所需的元数据;一个物理回收后端在后台批量处理迁移工作以满足 C3,同时驱动 C2 所需的异步拷贝;一小组调度器/worker 钩子恰好插入了 C2 所需的槽位映射刷新和同步依赖,而不需要一个全局停顿。

图 2(论文 Fig. 2):整体系统结构。注意图中的分层纪律——“KV Eviction Policies”永远只和”vToken Substrate”(Token Table + 回收后端 + Hooks)对话,而这个基座是唯一和下方未经修改的”vLLM Block Runtime”(块管理器 + 调度器 + 物理 KV 块)对话的部分。这正是让 vToken 成为一个可复用基座的架构图景:把 H2O 换成 Scissorhands,只是改变了谁在调用 Policy API,和下面的一切都无关。
3.2 Token 表:核心元数据结构
Token 表(token table) 按请求维护,是实际实现 C1 双视图的地方。对序列中每一个逻辑 token ID,它都记录该 token 当前的物理位置,以 (block_id, offset) 元组的形式,再加上一个存活位。把一个 token 标记为死亡,纯粹是一次元数据写入——当下没有任何 KV 内存被搬动或释放。这就是关键的间接寻址:它把决定淘汰什么(策略的工作)和维护物理布局(运行时的工作)分开了,而正是这一点让回收可以被推迟,而不是在每次淘汰调用时同步发生。

图 3(论文 Fig. 3):一个具体的例子,涉及两个请求。请求 0 的逻辑序列 “Four score and seven”(4 个 token)通过它的 token 表,映射到分散在 Block 5(它四个 token 中的三个:“Four”、“score”、“seven”——注意 token 2(“and”)已经被淘汰,用红色 ✗ 标出)和 Block 0 的条目。请求 1 的序列 “Just have fun” 同样映射跨越了 Block 0 和 Block 2。这张图把这个错配问题直观地展示了出来:Block 0 里保存着来自两个不同请求的各一个存活 token,尽管它的绝大部分容量既没有完全服务于其中任何一个请求,这个块也不能被释放。
Token 表恰好暴露三个操作,两个给策略用,一个内部给回收后端用:
evict_token(req_id, token_id)—— 把一个 token 标记为逻辑上已死。它的 KV 条目可能物理上依然存在,但注意力槽位映射将不再引用它,回收规划器会把它的空间当作死空间处理。sync_new_tokens(req_id, block_ids, total_len)—— 随着序列增长,登记新生成的 token;调度器传入当前的块列表和长度,表为其分配连续的逻辑 ID 并记录物理位置。apply_moves(req_id, moves)—— 只由回收后端在物理拷贝完成之后调用,原子性地更新受影响的 token 表条目的位置。
从策略的视角看,这三个调用被刻意设计为与块无关——一个策略调用 evict_token 时,永远不会知道这个 token 曾经住在哪个块里,或者这个块是否因此而变得可以释放。这正是让这个基座可以被复用的原因:H2O、StreamingLLM、Scissorhands 都只是用不同的 token 选择逻辑调用这同样三个函数。元数据成本按设计是很小的:每个长度为 的序列 ——每个 token 一条映射记录加一个存活位——与层数、注意力头数、或这些 token 对应的实际 KV 张量大小完全无关。在实现中,规范的 token 表存放在 CPU 上(简单、可移植),GPU 端维护一份查找缓存以保持稳态槽位翻译路径的速度;这份缓存在序列正常增长时是追加式更新的,只有在真正发生结构性变化(淘汰、迁移)时才会全量重建。
3.3 物理回收后端:把逻辑空洞变成真正空闲的块
回收后端是 vToken 真正”配得上”reclaimable 这个名字的地方——它是把 token 表的逻辑存活记账,转化为物理上可用、已释放的块的组件。在这个实现里,它的具体迁移机制是惰性压缩(lazy compaction):把若干低利用率块中幸存的 token 打包进更少的目标块,然后把腾空的源块归还给分配器的空闲池。后端把这个过程分为四个阶段,如图 4 所示,首尾相连。

图 4(论文 Fig. 4):后端(1)监控碎片化情况,规划哪些 token 该迁移到哪里;(2)分配一个目标块,在专用的迁移 CUDA 流上启动异步拷贝,同时记录迁移列表;(3)一旦拷贝落地,更新 token 表使后续的槽位查找反映新的物理位置;(4)刷新槽位映射,把腾空的源块归还给分配器。
阶段 1——回收资格判定(Reclamation eligibility)。 后端为每个块维护当前存活 token 数量 (每当一个 token 被淘汰或一个块被分配/释放时增量更新——不需要全表扫描),并由此推导出公式(1)定义的块内浪费率 。 值高说明系统范围内很多块都处于低利用率。为了避免不断重复检查,规划器把这些检查摊到多个调度迭代之间,而不是在每一个单独的步骤都跑一遍。
阶段 2——考虑余量的准入(Headroom-aware admission)。 这里有一个值得细品的微妙之处:惰性压缩是异地(out-of-place)操作——你需要先有一个可用的目标块,才能开始把存活 token 从碎片化的源块中搬出来并释放它们。但如果你等到空闲块池完全耗尽才尝试回收,你等到的恰恰是回收最难安全进行的那一刻。vToken 的做法是,在整体 KV 块预算之内(不是额外的内存,是从现有池子里划出来的)预留一小块有限的”疏散余量”(evacuation headroom)目标块容量,并且当全局浪费率 超过一个可调阈值 (论文报告的默认值是经验调优得到的 )同时空闲块数量接近低水位线时,主动尝试回收。当余量紧张时,调度器会施加准入背压(admission backpressure)——它会优先完成回收,而不是让新请求消耗掉压缩所需的最后一批空闲块。
阶段 3——迁移规划(Relocation planning)。 对每个正在考虑的请求,后端选出符合条件的低利用率源块(至多达到一个可配置的批大小 ),计算它们总共包含多少存活 token ,并且只有当预计能够净释放的块数量确实为正——也就是说,把 个存活 token 打包进 个目标块,能腾出的源块数量严格多于被当作目标消耗掉的块数量——才会继续执行。如果现有余量无法提供足够的目标块,这次规划就会被推迟(或者候选集被缩小),而不是被强行执行。产生的迁移列表会尽量保留逻辑 token 的顺序(有利于注意力局部性),把每个目标块填满之后再挪到下一个,每一次迁移都记录一个 (token_id, 源块, 源偏移, 目标块, 目标偏移) 元组。
阶段 4——分阶段感知的异步拷贝(Stage-aware asynchronous copy)。 这是系统工程细节最丰富的一环,因为如果天真地把回收拷贝和解码重叠起来,情况可能会变得更糟,而不是更好。一个解码步分为三个资源特征迥异的阶段:注意力前向(受 HBM 带宽限制——它在持续不断地流式读取 KV 数据)、FFN 前向(计算受限)、以及前向之后的收尾工作(采样、调度——主要是 CPU 侧逻辑,GPU 利用率很低)。如果你在某个步骤中随意的时刻启动迁移拷贝,它会和注意力阶段争抢本就稀缺的 HBM 带宽——而这恰恰是注意力本身正在渴求的资源——所以天真的重叠尝试反而可能拖慢它本该并行运行的那个解码步。
vToken 的解决办法是,刻意把拷贝安排在当前步骤的前向计算已经返回之后才启动,放在自己专属的迁移 CUDA 流上。因为该步骤的槽位映射早已是从(计划迁移之前的)旧token 表构建出来的,正在进行的注意力/FFN 计算不会被一次在它之后才开始的迁移搞坏。拷贝随后与采样、调度、下一步的预处理并行进行——这些阶段都不接触 KV cache,所以不会和拷贝争抢 HBM 带宽。在下一个步骤的前向计算启动之前,worker 会在迁移流上插入一个轻量级的 GPU 侧 wait_event 依赖——如果拷贝已经在前向之后的收尾窗口里完成了(常见情况),这就是一个空操作;如果没有,下一步的内核只需要在 GPU 侧等待,而不会让 CPU 主机线程停下来。相比一个 CPU 侧(主机侧)的同步屏障,这明显更省——因为 CPU 从不会因为等待 GPU 事件而阻塞,只有下一个 GPU 内核会等,而且只在拷贝真的还没完成的时候才等。
3.4 槽位映射与调度器集成
在原版 vLLM 中,注意力内核通过**槽位(slot)**来读取 KV cache——线性索引,直接由 计算得到,其中 block_id 和 offset 都直接来自块表。这个直接公式之所以成立,是因为在没有 vToken 的情况下,一个 token 一旦被分配了物理位置就再也不会改变。vToken 打破了这个假设(token 可以在迁移中移动),所以槽位映射函数必须改写成查询 token 表:
这次间接寻址在输入准备阶段(构建注意力内核启动前的批次槽位映射张量)每个 token 只执行一次,而不是在注意力内核内部执行——所以注意力内核本身的代码完全不用改,唯一变化的是喂给它的槽位映射张量,里面的值现在会反映当前(可能已迁移过)的位置。
三个调度器/worker 钩子把这一切接入了 vLLM 的执行循环:
- 调度器侧预留钩子(Scheduler-side reservation hook) —— 在准入新的 KV 分配之前,维护 §3.3 阶段 2 所述的有限疏散余量,确保余量不会被普通的请求准入悄悄消耗掉。
- Worker 侧回收钩子(Worker-side reclamation hook) —— 在 worker 的执行路径中,评估回收机会,为选中的请求启动异步 KV 拷贝操作,并重写受影响的逻辑布局元数据。
- 预注意力同步钩子(Pre-attention synchronization hook) —— 在注意力读取可能已被迁移的 KV 条目之前,插入基于流的依赖(§3.3 阶段 4 中的 GPU 侧
wait_event),确保迁移后的数据在被消费之前一定可见。
与 CUDA Graph 的兼容性。 这一点值得停下来专门说说,因为很容易想当然地以为迁移内存就必须放弃 CUDA Graph(它把一系列固定的 GPU 操作捕获并重放,以降低启动开销——对延迟敏感的解码来说这非常重要)。vToken 避免了这一点:KV cache 张量本身和图捕获的输入缓冲区从不移动或改变形状;每次重放之前更新的,只是那个可变槽位映射缓冲区的内容,依据 token 表来更新。迁移拷贝完全在被捕获的图之外、在自己的流上运行,事件依赖在任何可能读取被迁移条目的重放之前才插入。所以 vToken 保留 CUDA Graph 执行的方式,是把槽位映射当作一个固定图的动态输入数据来处理,而不是绕开图或重新捕获图——这个设计选择在实践中很重要,因为重新捕获 CUDA Graph 代价不小,会侵蚀掉整个系统本来想要争取的那部分延迟收益。
3.5 正确性不变式——“解码过程中安全”到底意味着什么
在解码正在进行时去动 KV 内存,是这个设计里最令人紧张的部分,所以论文给出的四条正确性不变式值得认真看待,而不是一带而过。它们是”vToken 不会搞坏你的生成结果”这句话所归约成的、精确可检验的具体主张。
- I1(Token 守恒)。 对每个请求,活跃逻辑 token ID 集合以及它们的 K/V 张量内容,在每一次迁移事件前后都保持不变。一次迁移可以改变一个 token 的物理
(block, offset),但绝不会改变它的逻辑身份,也绝不会改变它实际缓存的值。 - I2(唯一映射)。 每个活跃逻辑 token 恰好映射到一个物理
(block, offset),每个被占用的物理槽位最多被一个逻辑 token 声明——不存在别名,不存在重复写入。apply_moves通过原子性地同时清空源槽位、写入目标槽位来强制这一点,确保永远不存在一个”源和目标都声称自己持有活跃拷贝”的窗口期。 - I3(预注意力可见性)。 一个被迁移的 KV 条目,只有在它的拷贝完成之后才会被注意力读取——通过 §3.3/3.4 中的 CUDA 事件 +
wait_event机制强制实现,这把本该是一个全 GPU 范围同步屏障的东西,替换成了一个范围窄得多、基于流的依赖。 - I4(布局感知规划)。 一个迁移计划只会从受影响请求的块列表和存活状态的一致快照中提交,永远不允许和同一个请求上正在进行的计划重叠,如果预计的块数量没有严格减少就会被直接拒绝(也就是说,vToken 拒绝执行任何实际上不会释放任何东西的”回收”)。共享前缀块被保守地排除在任何计划之外(详见下文第 6 节)。
论文明确说明了每条不变式是如何被验证的:I1 和 I2 在实际实验中对每一次迁移事件都进行在线检查(哈希对比,详见下文第 4.6 节);I3 被视为结构性的——它必然遵循已经建立起来的 CUDA 事件拓扑关系,通过单元测试来断言,而不是在每次运行中都测量;I4 由规划器自身的准入门槛在流程上强制执行。这是一种合理的分工:那些能够廉价持续检查的不变式(I1、I2——比较哈希)被持续检查;那种按构造成立的结构性保证(I3)被测试而非在每次运行中测量;而那种属于准入策略的不变式(I4)由规划器逻辑按构造强制执行。
算法 1:vToken 的策略适配过程(根据图 6 和 §3.2-3.4 重新整理)
为了把这些零散部件变成一个具体的、可以逐步执行的过程——下面是把论文图 6 及其配套文字整理成的伪代码:
算法 1:带回收的 vToken 逐请求解码步骤
输入:请求 r,新生成的 token B_new,当前序列长度 L,
淘汰策略 P
输出:请求 r 的解码结果,回收在后台伺机进行
1. SyncNewTokens(r, B_new, L)
# 共享适配器:在 token 表中登记新 token 的
# 逻辑 ID 和物理位置
2. V <- P.SelectVictims(r)
# 策略钩子:H2O 给 token 打分并选出分数最低的
# 受害者;StreamingLLM 选出汇聚点+窗口之外的
# token;Scissorhands 选出低持久性 token;
# Random 均匀采样
3. for each token t in V:
EvictToken(r, t)
# 共享适配器:在 token 表中把 t 标记为逻辑死亡;
# 此时还没有触碰任何物理内存
4. S <- BuildSlotMapping(r)
# 共享运行时:对每个存活 token,计算
# slot = token_table[t].block_id * block_size
# + token_table[t].offset(公式 2)
5. ReclaimAsync(r)
# 共享运行时:检查资格(阶段 1),在余量下
# 准入(阶段 2),规划迁移(阶段 3),在本步骤
# 前向返回之后于迁移流上启动异步拷贝
# (阶段 4)——不会阻塞下面的第 6 步
6. Decode(r, S)
# worker:用槽位映射 S 执行注意力/FFN;
# 如果之前某次迁移的拷贝尚未完成,一个
# GPU 侧 wait_event 只会阻塞需要它的那次
# 注意力内核启动,而不会阻塞 CPU 主机线程
对照图 6 本身对”每一行由谁实现”的说明来读这段伪代码,会很有启发:第 1、3、4、5 行都是每个策略无需修改即可复用的共享运行时代码;只有第 2 行(SelectVictims)是策略特定的。这正对应论文报告的新策略集成成本”1-2 个文件、不到 50 行代码”,相比之下不用 vToken 的、按块原生方式集成需要”4-6 个文件、500 行以上”——整套 token 表/槽位映射/回收机制只需要写一次,就能被当前和未来的每一种淘汰策略共享,新策略的作者只需要写第 2 行。
论文自己的图 6 用一张带编号的过程表格(已在上面复现为算法 1)配合一张两行的集成成本对比表来呈现同样的分工;由于原始图 6 本身是文字/表格性质而非图表,我们把它的内容直接融入了上面的伪代码和文字叙述中。
3.6 一个具体数字例子:追踪回收在一个块上具体发生了什么
为了把上面的四个阶段变得更具体,值得用一个小而具体的数字例子过一遍。设块容量 ,当前全局碎片化阀值 。假设某个时刻,回收后端监控到三个候选源块 A、B、C,它们当前的存活 token 数分别为 、、(均满容量为 16)。
阶段 1:资格判定。 先计算这三个块单独的利用率:,,。代入公式(1),若这三块代表了当前考察范围内的全部已分配块(),则
远高于阀值 ,回收后端判定这三块碽碎化已经具备可操作性,进入下一阶段。
阶段 2:余量准入。 回收后端需要目标块才能开始搬迁。假设当前空闲块池剩余 2 个块,而预留的“疏散余量”另有 1 个块可用。总共 3 个目标块可用额度,而下面这步规划需要的目标块数为 。因为 ,准入通过。
阶段 3:迁移规划。 三块存活 token 总数 。因为 ,这 12 个存活 token 恰好可以全部打包进一个新分配的目标块 N。预计的净收益:消耗 1 个目标块,释放 3 个源块 A/B/C,净能释放 2 个块。因为 ,这个计划被确认为有利可图,迁移列表将依次记录(token ID, 源块/偏移, 目标块/偏移)。
阶段 4:异步拷贝。 假设当前解码步的前向计算在时刻 返回。迁移拷贝在 之后立即在专属的迁移流上启动,与采样/调度/下一步预处理并行进行。假设拷贝在 完成,而前向后的收尾工作(采样、调度)需要 。因为拷贝时间(0.3ms)小于收尾窗口(0.5ms),下一步前向启动前的 wait_event 检查会发现拷贝早已完成,因此这次等待是一个无操作。若反过来拷贝需要 (超过收尾窗口),下一步就会在 GPU 侧真正等待剩余的 ,但这个等待仍然只阻塞需要读取被迁移数据的那个注意力内核启动,而不会让 CPU 主机线程停下来。
这个例子验证了一个直觉:只要回收在前向返回后、下一步前向启动前的这段收尾窗口内完成,就完全不会在关键路径上引入任何可观测的延迟。论文 §5.5 报告的异步拷贝“无显式同步停顿”的现象,正是因为实际部署中迁移拷贝多数情况下都能在收尾窗口内完成。
4. 评测:虚拟化真的转化成可用容量了吗?
评测围绕六个明确的问题展开,值得直接照着这个结构走:(1)token 级淘汰是否真的把容量困在了半空块里(论文 §5.2);(2)在完全相同的 token 级决策下,vToken 是否改善了淘汰的帕累托前沿(§5.3);(3)在显存压力下,它是否扩展了活跃 KV 容量前沿(§5.4);(4)开销和重叠成本来自哪里(§5.5);(5)对运行时参数有多敏感(§5.6);(6)它是否与前缀缓存这样的生产环境特性保持正确的兼容(§5.7-5.8)?
实验设置。 全部实验运行在单张 NVIDIA H100(80GB)上。主要对比使用 Mistral-7B 和 Llama-3.1-8B,在 ShareGPT 和 LongBench 上进行,另外还单独加了一个 Qwen2.5-14B 的容量前沿检验。三种淘汰策略(H2O、Scissorhands、Random)对照三种系统变体:Native vLLM(未修改、完全保留、不做任何淘汰——代表”什么都不淘汰”的基线)、Naive-Evict(应用与 vToken 完全相同的 token 级淘汰决策,但关闭 vToken 的物理回收后端,让半存活的块继续保持分配状态——这是关键的消融基线,因为 Naive-Evict 和 vToken 之间的任何差异,都在固定淘汰决策本身的前提下,单独隔离出了物理回收本身的价值)、以及 vToken(token 表间接寻址和物理回收都开启)。
4.1 内存效率:回收是否真的把被困住的内存要回来了?

图 7(论文 Fig. 7)是最直接回答”这个机制是否有效”的配对比较。面板(a)显示,相对 Naive-Evict,vToken 把平均内存利用率提高了 Llama-3.1-8B 上 21.88%,Mistral-7B 上 21.67%,而且在各策略、各工作负载上都保持一致——这不是绑定在某个模型或某种提示词长度分布上的偶然结果。面板(b)——可以说是更具操作意义的指标——显示相对 Naive-Evict,vToken 把每请求保留块数减少了 27.2%-72.3%。保留块数量才是真正决定准入余量的量:同样的总预算下,每个请求占用的块越少,能同时驻留的请求就越多。两个面板从不同角度讲的是同一个故事——在 Naive-Evict 里,只要一个块还含有任何存活 token 就一直保持分配状态,所以 token 级的空洞在没有 vToken 主动压缩的情况下永远无法变成可复用空间。
4.2 SLA 约束下的吞吐量:回收的容量能否转化为服务性能?
单独证明内存效率的提升固然不错,但真正重要的系统问题是:在真实的延迟约束下,这种提升会不会体现在服务吞吐量上。论文把 SLA 阈值定义为参考并发度 下 Naive-Evict 的 p95 延迟的 倍(给两个变体相同的延迟预算,留 5% 余量吸收运行间的噪声),然后做闭环并发度扫描。

图 8(论文 Fig. 8)显示 vToken(红色)在每一张吞吐图里都持续位于 Naive-Evict(蓝色)之上,在每一张 p95 延迟图里都持续位于其下方——星号()标出了满足 SLA 的最高吞吐点。在 Mistral-7B 上,vToken 把被选中的可行吞吐提高了 9.9%-37.3%,把 p95 延迟降低了 9.9%-27.5%;Llama-3.1-8B 上呈现相同的模式(平均吞吐提升 18.9%,平均延迟降低 14.7%),最大的收益集中在 Random 淘汰下——Scissorhands 的吞吐提升达到 33.3%-103.7%,Llama 上 Random 提升最高达 37%。论文给出的机制性解释值得记住:相比 H2O(其保留的”重点命中者”往往结构上更聚集),Random 和 Scissorhands 把幸存 token 打散得更没规律,所以 Naive-Evict 困住而无法回收的内存比例对这些策略来说更严重——相应地,vToken 就有更多空间可以回收。这个收益是帕累托前沿的整体移动,而不只是”跑更大的 batch size”: 并不总是被测试的最大并发度,这证实了这个提升真的是在回收淘汰之后已经在逻辑上存在的容量,而不是在推动更激进的批处理。
4.3 显存压力下的容量前沿:最核心的结果
这个实验问的是这套系统问题里最尖锐的一个版本:在固定的 KV 块预算下,到底能有多少请求真正同时驻留?使用 Llama-3.1-8B、LongBench、H2O 和 12K token 输出长度,H100 在 gpu_mem_util=0.35 下暴露出 5,427 个可用 KV 块,在 0.50 下暴露出 11,519 个。在这个设置下,一个完全保留的请求需要 1,020 个块,而 H2O 淘汰之后理想紧凑打包所需的空间只有 512 个块——完全保留所需和理想压缩结果所需之间大约有 2 倍的差距,而整个问题就在于每种系统变体到底能捕获这个差距的多少。

图 9(论文 Fig. 9):Native vLLM 和 Naive-Evict 都在并发度 之后变得不可行(标为 ×)——Naive-Evict 在这里几乎帮不上什么忙,因为它保留的 token 依然散落在大部分仍被分配的块中。相比之下,vToken 一直保持可行到 ,在完全相同的块预算下把最大可行并发度提高了 60%,而且它在新边界()处的吞吐接近它自己在 时的峰值——也就是说,它在接近新极限时是优雅退化,而不是崩溃。

图 10(论文 Fig. 10)直接解释了图 9 中前沿移动背后的机制:它绘制了两种 gpu_mem_util 设置下归一化 KV 块需求与活跃并发度的关系。在 0.35 下,Native vLLM 和 Naive-Evict 都在 时就触及 100% 预算线;vToken 的需求曲线增长得慢得多,直到 才越过这条线。在更大的 0.50 预算下,同样的关系把验证过的可行并发度从 翻倍到 。在 Qwen2.5-14B(一个完全不同的模型和规模)上单独做的检验显示了相同的定性 2 倍前沿扩展(原生方案/Naive-Evict 在 失败;vToken 一直保持可行到 ,直到 才失败),这是论文用来证明这个现象并非 Llama-3.1-8B 特定头数/层数配置产物的证据。
4.4 开销与重叠:异步设计是否真的物有所值?
论文的 Figure 11(一个三面板的开销/重叠分解图)在这里以文字形式呈现,而不是复现成图片,因为我们的图片预算集中在上面的架构、机制和核心容量前沿结果上;关键数字整理如下。这里有两个结果对判断这套设计是否具备生产可行性、而不只是算法上的巧妙,很重要。第一,一个纯间接寻址消融实验——安装 vToken 的钩子但关闭实际的淘汰/回收,唯一被测量的成本是稳态槽位表查找路径——显示相对未修改的 Native vLLM,在 和 下吞吐量和 p95 延迟的变化都低于 1.0%。这是一个重要的负面结果:它说明 vToken 确实产生的开销,完全来自压力触发的回收机制,而不是不管碎片化是否触发都要付出的固定税。第二,一个强制同步消融实验(去掉异步设计,强制每次迁移拷贝都要显式的 CPU 侧等待)直接验证了异步设计的价值:正常的异步路径显示迁移拷贝没有显式的同步停顿(拷贝在多个解码步之间保持挂起,以低成本与其他工作重叠),而强制同步的消融暴露出每次迁移事件真实的 CPU 阻塞。在这个压力测试下,异步回收在两个测试的并发度下都同时改善了吞吐量和解码 p95(相对 Naive-Evict),同时接近(虽然没有完全达到)理论上”强制同步但没有显式等待”的基线——论文小心地把这个实验定性为对重叠机制的压力测试,而不是典型运行点的稳态成本,因为一个系统越接近它的容量前沿,回收触发的频率就远低于这次刻意加压测量中的频率。
4.5 对运行时参数的敏感性
在 H2O 下对块大小、淘汰比例、碎片化阈值 做的单因素扫描显示:块大小——vToken 默认的 16(vLLM 分配器支持的最小值)取得了吞吐/延迟权衡上最好的表现,更大的尺寸(32、48)以牺牲可回收容量为代价换来更差的性能,证实了单纯增大块不能替代 token 级回收;淘汰比例——随着比例从 0.3 扫描到 0.7,保留 KV 容量大幅下降,同时吞吐提升、延迟降低,说明 vToken 能把更激进的淘汰直接转化为回收的容量(不过论文小心地指出,选择一个不损害精度的淘汰比例仍然是淘汰策略本身的责任,不是 vToken 的);碎片化阈值 ——这是三者中影响最小的一个,测试范围内对容量和延迟只有轻微影响,说明 vToken 不需要精细调节这个触发阈值就能表现得相当不错。
4.6 前缀缓存兼容性与生成质量验证
对任何真正考虑部署这套系统的人来说,还有两项最终检查很重要。前缀缓存: vToken 当前的实现保守地把共享前缀块排除在迁移之外(以避免破坏多个请求都依赖的数据),同时让每个请求私有的后缀块保持可回收。在 8K token 共享前缀 + 8K token 私有后缀、共享度分别为 1、2、4、8 的测试下:对于共享度 的情况,所有共享前缀候选都被正确跳过,所有一致性检查都通过,而且相对启用了前缀缓存的 Naive-Evict,vToken 依然把保留块减少了 28.6%-42.2%,这些收益完全来自私有后缀的回收(两个变体获得相同的前缀缓存命中率)。正确性与生成稳定性: 论文在所有评测过的工作负载/策略上,对每一次迁移事件前后都哈希对比保留 token ID 的有序集合加上它们的 K/V 张量内容,并报告这些检查在每一个事件上都通过(在实证层面验证了 I1/I2 不变式,而不只是按构造成立)。此外,在一个 144 组配对生成的比较中(Llama-3.1-8B、ShareGPT、确定性解码),vToken 和 Naive-Evict 之间的平均 ROUGE-L F1 差异只有 ,93.1% 的样本对差异不超过 0.01,输出长度比的中位数为 0.99——说明 vToken 的间接寻址和迁移没有引入超出底层淘汰策略本身之外的、可观测的质量漂移。
4b. 与其他内存回收思路的对比
把 vToken 放到更宽的设计空间里看,有助于看清它的组合到底新在哪里。
| 方案 | 是否降低回收粒度(能否处理 token 级空洞) | 是否保持 PagedAttention 内核不变 | 是否在解码过程中异步完成 | 是否需要新的每策略集成代码 |
|---|---|---|---|---|
| 缩小块大小(4到/8到) | 部分缓解,但重新引入元数据/带宽权衡 | 否(块大小变化影响内核假设) | 不适用 | 不需要,但未解决根本问题 |
| 完全 token 粒度分配器 | 是(完全解决) | 否(需要侵入式 CUDA 改动) | 不适用 | 不需要,但代价是彻底重写运行时 |
| 仅 Naive-Evict(不回收) | 否(半存活块永远无法释放) | 是 | 不适用 | 不需要,但完全不解决碽碎化问题 |
| vToken | 是(token 表间接寻址 + 惰性压缩) | 是 | 是(分阶段异步拷贝) | 不需要(1-2个文件,<50 行) |
这张表把第 2 节已经提到过的两个“显而易见”方案(缩小块大小、完全 token 粒度分配)与 vToken 实际选择的路径并排在一起,直接用四个维度展示了 vToken 选择“在现有物理层之上加一层薄虚拟化”这个方案、而不是修改分配器本身或彻底重写内核的根本原因:它是唯一同时在四个维度上都给出肯定答案的选项。
5. 设计选择讨论:替代方案与权衡取舍在哪里可能失效
为什么用惰性压缩,而不是每次淘汰就立刻压缩(eager compaction)? “立刻压缩”这个显而易见的替代方案会在一个块跌破某个利用率阈值时立刻压缩它。这样能让碎片化始终维持在更低水平,但代价是持续不断的后台拷贝流量——每一次淘汰调用都可能触发一次压缩,持续地与其他工作争抢 HBM 带宽,而不是只在全局浪费率 越过 时才争抢。vToken 这种阈值门控、批量处理的做法,是用一定量的”陈旧”碎片(内存在技术上是可回收的,但还没被压缩)去换取显著更低的后台拷贝流量。边界条件是:如果一个工作负载的淘汰速率是突发性的(长时间没有淘汰,然后突然大量淘汰),基于阈值的触发机制可能会滞后于突发的碎片化尖峰,恰好在内存压力最大的时候短暂地欠回收——论文默认的 是针对测试工作负载经验调优的结果,论文没有描述任何能更快检测并响应碎片化尖峰的自适应机制。
为什么用 CPU 常驻的规范 token 表,而不是把一切都放在 GPU 上? 把规范表放在 CPU 上,实现起来更简单、更容易正确推理(主机侧数据结构比 GPU 驻留的数据结构更容易调试),而且和现有的、主要运行在 CPU 上的 vLLM 调度器代码保持兼容。代价是每次稳态槽位映射计算都需要查询一份 GPU 驻留的 CPU 表缓存,而不是直接查询规范表,这就在两份副本之间加了一个必须保持一致的同步接口(在结构性变化时刷新)。一个完全驻留在 GPU 上的替代表会避免这个缓存一致性问题,但会让每一个当前依赖 token 存活状态推理的主机侧调度决策都变得更复杂——论文的选择是以在 worker 侧引入一个小的缓存刷新机制为代价,来换取调度器本身保持简单。
为什么把共享前缀块完全排除在迁移之外,而不是用写时复制(copy-on-write)? 这个保守选择——永远不迁移通过前缀缓存被多个请求共享的块——牺牲了一部分潜在的回收空间(仅回收私有后缀捕获到的收益,比能够压缩共享块本身要少,尤其是在共享度很高、一个请求的大部分 KV 占用可能就是那个共享前缀的情况下)。论文明确点名写时复制是自然的扩展方向:只有当预期的回收收益明显能覆盖先复制一份共享块的额外拷贝成本时,才去迁移一个共享块。当前的设计接受了一个真实的、可量化的可回收容量上限,换来的是一个简单得多的正确性论证——不存在一个请求的压缩悄悄破坏另一个仍然依赖某个块的请求视图的风险。
6. 这套设计隐含假设的死锁安全性论证(论文中的讨论,这里说得更明白一点)
这篇论文主要讲的是 token 表和回收后端,但仔细读 §3.3-3.4 之间的字里行间,异步拷贝设计还隐含地假设了一些关于 GPU 调度的东西,值得说清楚,因为这正是那种在相邻的部署场景中会悄悄失效的假设。阶段 4 里基于 wait_event 的依赖假设迁移流的拷贝操作和下一次注意力内核都能被 GPU 驱动及时调度——如果迁移拷贝碰巧排在同一设备上大量其他不相关内核工作的后面(这在一个同时运行多个模型或多路请求流、共享 SM 的多租户推理节点上是很现实的场景),wait_event 可能会比论文的稳态开销数字所暗示的等待更久,因为那些数字是在一块专门用于这次单一评测、否则处于空闲状态的 H100 上测出来的。论文没有把这描述成一个正确性风险(这个设计无论调度延迟如何都保持无死锁,因为这里没有像某些竞品设计那样,让相互独立调度的 block 之间做忙等自旋——vToken 的依赖是一个规范的 CUDA 事件等待,而不是一个自旋循环),但对于打算把这套系统和其他 GPU 工作负载放在一起部署、而不是在孤立的评测环境里跑的人来说,这确实是一个值得标注出来的延迟可预测性风险。
6b. 相关工作:vToken 在设计版图中的位置
论文把相关工作归纳为三类,理解这三类划分,有助于看清 vToken 到底新在哪里、又借用了哪些已有思路。
LLM 推理系统。 vLLM 提出的 PagedAttention 是一种基于块的 KV cache 管理器,能减少外部碎片、支持前缀共享;这个设计已经被 TensorRT-LLM、LightLLM 等系统广泛采用。但这些系统提供的是一个高效的物理层(substrate),它们的内存管理接口本身依然是面向块的:分配、回收、槽位映射都是以块为单位表达的,而不是以单个 token 为单位。其他系统通过并行化、调度、卸载等手段改善服务能力,但都没有提供一层运行时抽象,让 token 级的淡汰策略能够回收部分使用过的 KV 块。
KV Cache 优化。 另一类工作通过改变缓存内容、缓存表示形式或消费缓存的注意力计算本身来减轻内存压力。量化与压缩类方法减小 KV 表示尺寸,与 vToken 正交;H2O、StreamingLLM、Scissorhands、FastGen 等 token 淘汰策略决定哪些 token 应该被保留,它们的主要贡献是策略设计本身,而不是在基于块的服务运行时上实现这些决策所需的内存基座。还有一些较新的系统开始更接近运行时 KV 管理本身:CacheGen 针对紧凑缓存传输,Quest 通过选择有用 KV 条目来降低注意力计算成本,DiffKV 对 K/V 精度、token 保留、逐头布局做差异化处理以压缩缓存本身,需要一个混合精度的分页管理器,与 vToken 在均匀 FP16 KV 上运作的基座正交;PagedEviction 将对齐的负担转移给策略本身,要求淘汰必须页对齐,这会限制策略侧的决策空间,也不能直接兼容 H2O/StreamingLLM 这类重要性排序与页边界无关的现有策略;vToken 让策略保持对页边界无感,在运行时层解决对齐问题。
内存虚拟化抽象。 vToken 的思路与操作系统经典的虚拟内存分页机制同源:都是在一个固定的物理分配粒度之上,引入一层逻辑地址空间来解耦“需求粒度”与“物理分配粒度”。不同之处在于,传统虚拟内存面对的是通用计算负载,而 vToken 针对的是一个高度结构化、带有注意力内核对内存布局有严格假设的工作负载(注意力内核需要能够通过槽位映射找到正确的物理位置),这让它必须额外处理 CUDA Graph 兼容性、预注意力同步等传统 OS 分页机制不需要处理的约束。
7. 局限性,论文自己承认的部分
论文对若干范围边界表现得很坦诚。这个原型专门针对单节点、单 GPU 的解码快路径,刻意把 token/块粒度错配问题与分布式调度或跨设备 KV 搬移隔离开来——张量并行部署下,每个 TP 分片都有各自独立的 KV 块池,token 表间接寻址和回收就需要按分片各自运作,跨分片协调仍然是现有调度器的责任(论文点名了这个扩展方向,但没有实现或评测)。共享前缀块被保守处理(如上所述,排除在迁移之外),而不是通过写时复制。评测通过所测试的模型隐式覆盖了 MHA 式注意力,但论文认为(没有专门做 MoE 服务规模的实验来验证)GQA 和 MQA 变体只会改变物理 KV 张量形状,不会改变 token 身份或槽位重映射语义——这个架构层面的说法是合理的,但论文没有在头数/分片大小比例可能以不同方式与回收规划器的假设互动的规模上做实证压力测试。原型所依赖的 vLLM 版本要求最小块大小为 16,这正是为什么块大小敏感性扫描(上文 4.5 节)被限制在 token 的块——更小块的配置可能与回收设计有不同的互动方式,但没有被测试过。
8. 批判性分析
这篇论文本身的具体缺陷。 第一,评测中的每一个定量结果都来自单一硬件配置——一块 NVIDIA H100——论文完全没有报告在其他 GPU 型号上、多 GPU 张量并行设置下、或任何形式的与共存工作负载争抢资源情况下的测量;考虑到死锁安全性和重叠论证都隐含地依赖于 GPU 调度器能多及时地调度迁移流(上文第 6 节讨论过),对于一篇核心卖点是”可用于生产部署”的系统论文来说,缺少任何有资源争抢或多租户场景下的测量,是一个真实的缺口。第二,论文对它所有的核心百分比提升数字(21.88% 的内存利用率提升、27.2%-72.3% 的块数缩减、9.9%-103.7% 的吞吐范围)都没有报告跨重复运行的方差或置信区间——对于跨度像”9.9% 到 103.7%“这么宽的数字,而且这些数字来自已知存在明显运行间差异(来自热状态、调度抖动)的 GPU 基准测试,缺少误差棒或重复实验的统计数据,让人很难判断这个宽范围究竟有多少反映的是真实的策略相关行为,有多少只是测量噪声。第三,碎片化阈值敏感性分析(4.5 节)测试的范围是 ——一个相当窄的、围绕所选默认值 0.25 的区间——没有探索极端情况(接近零的阈值几乎持续回收,或接近 1.0 的阈值基本从不触发),而这本可以更清楚地界定这个设计对该参数的真实敏感度,而不只是确认在一个已经比较合理的窄区间内它不算过于敏感。
作者低估或省略的局限性。 论文核心的容量前沿数字(4.3 节)是用 H2O 配置隐含的单一固定淘汰比例测量的——但真实部署需要选择一个在生成质量和内存节省之间权衡的淘汰比例,而论文没有讨论容量前沿收益对这个选择有多敏感,除了 4.5 节里那个更窄的(测量吞吐/延迟而非容量前沿本身指标的)淘汰比例扫描。论文对大规模下维护 token 表本身的 CPU 侧成本也明显没有细谈——虽然每序列 的元数据占用以原始字节数来看被量化为微不足道,但论文没有报告调度器侧记账工作(更新逐块存活计数、跑资格检查、规划迁移)的 CPU 侧计算成本,随着并发度扩展到生产级多租户推理集群真正会跑的数十到数百个同时追踪的请求时会怎样,而不只是测试过的并发度(开销实验里峰值约 ,其他地方 -32)这种相对温和的规模。最后,其他并发 GPU 工作可能延迟迁移流完成的持续性风险(第 6 节讨论过)在论文的评测中完全没有测量,尽管这直接关系到报告的低于 1% 的稳态开销和重叠机制压力测试,能否推广到真实的多租户推理节点,而不只是一块专用的评测 GPU。
具体的、可操作的改进建议。 (1)为核心的吞吐、延迟、内存利用率数字报告方差/置信区间(比如至少 3-5 次重复运行),尤其考虑到部分报告的范围本身就已经相当宽了。(2)把 的敏感性分析扩展到更宽的范围(比如 0.05 到 0.90),真正刻画这个调参曲面,而不只是探测所选默认值附近的一小段。(3)至少增加一个测量 vToken 在 GPU 资源争抢下表现的实验——例如在同一设备上共置一个不相关的 CUDA 工作负载,看迁移流的完成延迟(以及相应的 wait_event 停顿风险)是否会明显恶化,因为这是对论文近乎零的稳态开销主张能否推广到专用基准测试环境之外最直接的威胁。(4)报告 CPU 侧调度器开销(而不只是 GPU 侧内核开销)作为并发追踪请求数量的函数,把测试规模扩展到远超目前 的水平,因为生产级多租户部署通常同时追踪数以百计的并发序列,即便 token 表记账成本在测试规模下明显可以忽略不计,在那个规模下也有可能变得不可忽略。
8b. 术语缩写快查表
- KV cache:自回归生成中缓存的 Key/Value 张量,用于避免重复计算。
- PagedAttention:vLLM 提出的基于块的 KV cache 管理机制,借鉴操作系统虚拟内存分页思路。
- 块内碽碎化(intra-block fragmentation):一个已分配块内部部分空间被浪费的现象,与块与块之间的外部碎片相对。
- H2O / StreamingLLM / Scissorhands / Random:本文评测的四种 token 级 KV 淘汰策略。
- Token 表(token table):vToken 的核心元数据结构,记录每个逻辑 token 到物理
(block, offset)的映射及存活位。 - 惰性压缩(lazy compaction):本文采用的具体物理回收机制,把低利用率块中的存活 token 打包到新块中。
- (块内浪费率):公式(1)定义的已分配容量中未被实际使用的比例。
- (碎片化阀值):触发回收尝试的 阀值,默认 0.25。
- SLA 阈值:评测中用于定义可行并发度边界的延迟约束(本文中为 Naive-Evict 基准 p95 延迟的 1.05 倍)。
- :满足 SLA 约束下吸吐量最大的并发点。
9. 复现性说明
论文对其原型技术栈的说明足够精确,可以在精神上复现:vLLM v0.18.0 配合 PyTorch v2.10.0,在单张 NVIDIA H100(80GB)上评测。新增的三个组件——TokenTable、ReclamationManager、CUDACopyEngine——都明确点名,这为任何想要独立重新实现的人提供了具体的目标,不过论文(作为一篇这个篇幅的系统论文很常见)在本文阅读的版本中没有附带公开代码链接。评测的模型(Mistral-7B、Llama-3.1-8B,以及额外的 Qwen2.5-14B 容量检验)都是公开可获取的权重,工作负载(ShareGPT、LongBench)都是标准的、公开可用的数据集,所以整个实验设置即便没有确切的原型代码也是完全可以重建的。没有完全说明的是精确的 CUDA/驱动版本锁定、除了核心的 默认值之外每个实验具体使用的占用率/阈值参数、以及产生 4.4 节开销分解结果的确切性能分析工具链——想要精确复现所报告的百分比提升的读者,需要根据描述的 vLLM 集成自行重建这些细节,或直接联系作者。
10. 对基于 PagedAttention 的推理服务栈意味着什么
如果你已经在一套基于 PagedAttention 的推理服务系统上跑着 token 级 KV 淘汰(H2O、StreamingLLM,或者自定义策略),这篇论文第 2 节的问题刻画,无论你是否打算采用 vToken 本身,都值得拿来对照检查一下自己的部署:测量一下淘汰运行之后,你分配的 KV 块里有多少比例利用率低于 50%。如果这个数字接近论文测出的 56%-67%,你很可能正在把大量当前不可见的 GPU 内存留在桌面上——这是你的淘汰策略已经在逻辑上释放掉、但你的块管理器完全不知道的内存。vToken 的核心思路可以推广到这个具体实现之外:任何位于块粒度内存管理器之下、承载着 token 级(或更一般地,亚块级)存活决策的场景,都是同一种虚拟化边界处理方式的候选对象——把”判定某样东西已死”和”回收它的物理内存”解耦,推迟后者,并让它在独立计算的背后伺机批量执行。这个模式值得记在心里,远远超出 RMSNorm 相关的归一化技巧或这个具体的 KV cache 用例。