显存与调度
第 31 章 已确立调度器最重要的安全规则:执行词元计划之前,先为它预留所需状态。本章把这条规则一直落实到物理 KV 缓存块。分配器必须核算不断增长的序列、共享前缀、请求取消和内存压力;调度器则必须选择能够装进这些块的任务,同时兼顾延迟、公平性和进度。
多套系统奠定了主要技术。Orca 在 2022 年提出迭代级调度 (Yu et al. 2022)。vLLM 的 PagedAttention 在 2023 年把分块分配用于 KV 状态 (Kwon et al. 2023)。SGLang 在 2024 年用 RadixAttention 组织可复用前缀 (Zheng et al. 2024)。Sarathi-Serve 在同机调度器内限制每轮预填充量,DistServe 则把预填充与解码放进不同的资源池 (Agrawal et al. 2024; Zhong et al. 2024)。Mooncake 后来又把放置问题扩展成由存储支撑、覆盖整个集群的 KV 层次结构 (Qin et al. 2025)。这些机制可以组合使用,并不是一套必须按历史顺序采用的步骤。
从保留词元到物理块
把每个保留词元对应的逻辑 KV 载荷记为
其中, 是 Transformer 层数, 是每层的键值头数, 是一个头的维度, 是每个缓存元素的字节数,因子二代表键和值。 表示切分、对齐、元数据或分配器开销之前,每个词元需要的逻辑字节数。
假设一个分配块能够保存 个词元位置的状态。对于请求 , 是需要保留的提示词和已生成词元数, 表示逻辑块数, 表示已分配的 KV 字节容量, 表示最后一个块中尚未使用的词元位置数。那么
这个上界准确说明了固定块能保证什么:末块浪费少于每条非共享序列的一个块。分页并不会消除块表、对齐、引用计数、预留容量,也不会消除进程中其他位置的闲置内存。
下面三类浪费不能混为一谈:
| 浪费 | 原因 | 固定块带来的改变 |
|---|---|---|
| 过度预留 | 按请求声明的最大长度分配容量,但请求从未增长到该长度 | 随序列增长逐块分配 |
| 外部碎片 | 大小不一的连续内存段留下空洞,较大的新请求无法利用 | 任何空闲固定块都能满足下一次块申请 |
| 末块浪费 | 最后一个块没有填满 | 将浪费限制在少于 个词元位置 |
下面的可运行示例进行精确的离散记账。四个请求分别保留 [37, 81, 130, 211] 个词元,并且都声明最大长度为 256。示例报告不同块大小下分配的词元槽位与末块浪费。它不会把块表条目虚构成运行时成本,因为这一侧的取舍必须通过内核测量才能确定。
from math import ceil
lengths = [37, 81, 130, 211]
declared_max = 256
print("block entries allocated tail-waste")
for block_tokens in [1, 8, 16, 32, 64]:
entries = sum(ceil(length / block_tokens) for length in lengths)
allocated = entries * block_tokens
waste = allocated - sum(lengths)
print(f"{block_tokens:>5} {entries:>7} {allocated:>9} {waste:>10}")
contiguous = len(lengths) * declared_max
print("contiguous max-reservation slots:", contiguous)
块越大,块表条目越少,但末块浪费可能越多。这两个数字都不是完整的性能模型。部署时应根据序列长度分布、缓存布局、内核实现和实测延迟选择 。
块表消除连续内存要求
PagedAttention 分页注意力(PagedAttention) 把请求的逻辑 KV 序列存进大小固定、物理位置无需相邻的块 (Kwon et al. 2023)。设 为物理块标识符的集合,请求 的块表可以写成如下映射:
这里,逻辑块索引 映射到物理块 。例如,即使第 2、7、9 号块散落在内存池中,一个占用三个块的请求仍可以使用下面这张块表:
| 逻辑块 | 0 | 1 | 2 |
|---|---|---|---|
| 物理块 | 7 | 2 | 9 |
注意力内核会通过这层间接映射读取状态。这个设计借鉴了虚拟内存,却并不是硬件页表:映射由服务运行时和注意力内核显式管理,也不一定涉及按需缺页、地址转换后备缓冲器(TLB)或操作系统换页。
在 vLLM 论文评估的模型、负载及 FasterTransformer 和 Orca 基线下,完整的 vLLM 系统在延迟相近时实现了二至四倍吞吐量 (Kwon et al. 2023)。这个结果包含块管理器、调度器、内核和共享机制,不能把它视为分块分配本身能够带来、且与硬件无关的固定倍数。
分配是词元计划的一部分
连续批处理(continuous batching) 允许活跃请求集合在不同迭代之间变化,但每轮迭代不必推进所有常驻请求。一个计划可以包含解码步骤或预填充分块;某个请求也可能因为优先级、容量或公平性策略而不执行任何任务。如果计划为请求 安排 个新位置,那么它需要新增的块数为
这里, 是请求当前保留的长度, 是本轮计划新增的位置数, 是块表需要增加的条目数。假设完成允许的缓存逐出后,还有 个可用块,那么内存可行性要求
其中, 是计划选中的请求集合。内存可行性与调度器的词元预算或计算预算是两项独立约束。计划即使装得进内存,也可能无法满足当前的延迟约定。
分配器需要遵循“预留、执行、提交”协议。启动模型计算之前,它必须以原子方式预留全部新块;执行成功后,再提交映射与词元状态;取消或失败会回滚尚未提交的预留。一个有用的守恒检查是
这四项分别是空闲块、为计划中的迭代预留的块、被活跃或缓存状态引用的已提交块,以及内存池总容量。任何仍被引用的块都不得重新分配,每次状态转换都必须保持等式成立。
flowchart TD
A[构建词元计划] --> B[计算所需新增块数]
B --> C{可回收容量是否足够?}
C -->|否| D[执行已配置的内存压力策略]
D --> M[推迟或终止本次计划]
C -->|是| E[以原子方式预留块]
E --> F[执行模型计算]
F --> G{执行是否成功?}
G -->|否| H[回滚预留]
G -->|是| I[提交映射与 KV 状态]
I --> J{请求或缓存条目是否释放?}
J -->|否| N[保留仍被引用的已提交块]
J -->|是| K[减少块引用计数]
K --> L[把零引用块放回空闲池]共享前缀会改变块的所有权
当两个请求对应的 KV 状态完全相同时,它们可以共享同一个物理前缀块。可见文本相同还不够。缓存身份通常至少包括词元 ID、模型权重或版本、启用的适配器、位置处理方式、注意力与缓存格式,以及任何会影响前缀的多模态状态。即使数学上可以共享,租户与隔离策略也可能禁止这样做。
设 为请求 引用的物理块集合, 为常驻请求集合。正在使用的物理块数为
这里,并集让共享块只计算一次。每个物理块 都有一个引用计数 ,记录所有活跃请求和保留缓存的所有者。只有相关引用全部移除且 后,这个块才能回收。
前缀缓存(prefix caching) 会建立一份索引,把兼容的词元前缀映射到这些块。SGLang 的 RadixAttention 用基数树保存前缀,查找可复用的最长前缀,跟踪活跃节点的引用,并在内存压力下逐出可复用的叶节点状态 (Zheng et al. 2024)。完整的共享块必须保持不可变;出现分叉的后缀会获得不同的块。若实现允许共享一个仍可写入的未满块,就必须采用写时复制或等价规则。
前缀缓存带来四个运维问题:
- 身份: 哪些模型、适配器、位置、缓存格式、模态和租户字段共同组成缓存命名空间?
- 所有权: 哪些活跃请求和索引中保留的条目正在引用每个块?
- 价值: 一次命中能省下多少预填充词元和多少时间?
- 逐出: 活跃任务需要空间时,应移除哪个未固定的前缀?
只报告请求级命中率,可能掩盖大部分实际价值。还应报告匹配的前缀词元数或字节数,因为命中八个词元与命中八千个词元,节省的计算并不相同。
内存压力需要明确策略
动态分配能比按最大长度预留接纳更多任务,却不能保证每条已接纳序列都能增长到声明的上限。这里的内存压力,是指空闲池无法满足 ,系统必须在几种不同操作之间作出选择:
| 操作 | 影响的状态 | 恢复执行时的成本 |
|---|---|---|
| 逐出可复用的前缀状态 | 没有活跃所有者的缓存块 | 以后未命中时重新计算前缀 |
| 推迟接纳 | 等待中的请求 | 增加排队延迟,不丢失模型计算 |
| 抢占并重新计算 | 活跃请求 | 丢弃 KV 状态,之后重新执行模型计算 |
| 卸载后再载入 | 活跃或缓存块 | 传输、存储排队与同步 |
| 明确拒绝或报错 | 视约定而定,影响等待中或活跃的请求 | 请求失败,必须反映在服务指标中 |
这些操作不能互换。逐出一个没有活跃所有者的缓存条目,不会改变任何正在运行的请求;抢占活跃任务则会。卸载能够保留状态,却会新增一条数据路径,而这条路径可能比重新计算更慢。优先级可以保护交互流量,也可能让较早到达的批处理请求长期得不到执行,因此选择被抢占的受害请求时,还必须考虑等待时间或公平性约束,而不能只看内存评分。
采取压力策略后是否重试,本身就是策略的一部分。这不是允许调度器无限重试。如果无法回收容量,调度器必须推迟、拒绝或明确报告失败。不可见的重试循环会把已知的分配失败变成无休止的排队。
分块预填充共享每轮预算
长预填充可能占用一整轮迭代,使活跃解码请求的词元间隔变长。Sarathi-Serve 把预填充分块,再把这些块与解码任务合并执行 (Agrawal et al. 2024)。设 为选中的解码请求, 为各请求计划执行的解码位置数, 为选中的预填充请求, 为各自的分块大小, 为经过性能分析得到的每轮词元预算。一个简化约束是
在这个式子中,如果没有采用多词元解码, 通常为一。词元数只是执行时间的近似指标:上下文长度、内核、阶段组合、并行布局和硬件,都会改变每个词元对应的工作量。
分块只能限制一轮计划纳入多少预填充工作,并不能消除预填充与解码之间的全部干扰。块太小会增加内核启动、重复调度,还可能降低内核效率。只保护解码的策略可能让新预填充长期得不到执行,进而恶化首词元时间。因此,调度器除了目标批大小,还必须测量各类队列的等待时间和延迟。
只有数据路径划算时才移动 KV 状态
预填充与解码分离会为两个阶段分配不同的设备池。这样可以消除同机内核争用,也能解除部分资源和并行选择之间的绑定;但所需 KV 状态到达解码池之前,请求无法开始解码。设 为启动与同步延迟, 为传输的 KV 载荷字节数, 为这条路径的实测带宽。单独计算传输时,有
式中使用实测带宽,而不是链路标称带宽。端到端延迟还包括源端和目的端的队列、争用、重试和背压。传输可以与其他任务重叠,但使用这些状态的一方仍需要清晰的就绪与所有权协议。
DistServe 在选定的 TTFT 和 TPOT 达标率目标下,分别优化预填充与解码资源 (Zhong et al. 2024)。Mooncake 把同一项状态移动问题扩展到 GPU 内存、CPU 动态随机存取存储器(DRAM)、SSD 和网络链路 (Qin et al. 2025)。这些系统展示的是具体设计和实测负载,并不能证明阶段分离或分层存储普遍更优。
缓存感知的路由还必须同时考虑局部性和负载。设 为工作节点 的预测排队延迟, 为提示词长度, 为兼容的缓存前缀长度, 为在该节点计算 个未缓存词元的预测时间, 为传输与路由开销。一种示意性的完成时间估计为
这个估计式只是起点。容量、公平性、故障域、预测误差和租户隔离,都可能推翻选择最低估计值的决定。
没有一种分配或放置策略能在所有负载下胜出。小块能更严格地限制末块浪费,却会产生更多块表条目;保留前缀只有在它被逐出之前出现兼容复用才有价值。存储路径较慢时,重新计算可能优于卸载;算力紧张时,卸载也可能更划算。同机部署免去 KV 传输,阶段分离则可以隔离两个阶段的队列,并独立调整资源池规模。交叉点取决于提示词和输出长度分布、输入负载、SLO、并行方式、拓扑和故障策略。
同时验证分配器与调度器
正确性测试不能只检查请求能否成功完成,还应覆盖以下情况:
- 在预留与提交之间取消一个请求,确认所有已预留块都回到内存池。
- 让多个请求共享前缀并按不同顺序结束,证明共享块只会在最后一个引用消失之后回收。
- 注入执行与传输故障,确认已提交映射始终有效,未提交映射全部回滚。
- 耗尽内存池,确认配置的逐出、抢占、推迟、拒绝和公平性规则被明确执行,而不是落入重试循环。
- 改变模型版本、适配器、位置处理、模态或租户命名空间,确认不兼容的前缀无法命中缓存。
在负载下,应报告四组指标:
- 内存池状态: 空闲块、预留块、已提交块和被引用的唯一物理块数;末块浪费;分配失败;峰值占用水位。
- 复用与回收: 匹配的前缀词元数、命中率与逐出率、复用距离、重新计算的词元数、卸载字节数和重新载入延迟。
- 调度: 按请求类别统计的队列等待时间、预填充分块大小、解码批组成、抢占、拒绝和公平性结果。
- 服务结果: TTFT、词元间隔、TPOT、有效吞吐量、接纳率、KV 传输字节数与延迟、加速器时间和成本。
第 31 章 给出了资源匹配的负载评估方法。本章新增的要求是守恒:核对从接纳到释放期间的每个已调度词元和每个物理块。
第 8 章 通过层数、KV 头数、头维度和缓存精度,决定每个词元对应的逻辑 KV 字节数。本章决定如何分配、共享、回收和移动这些状态。第 33 章 会改变一次模型执行能够接受多少词元,从而改变词元计划和未来的块需求。第 34 章 可以减少缓存字节数,也可能改变内核实际能达到的性能。这些下层条件中任何一项发生变化,都必须重新测量内存策略。
收益与边界
分块分配为调度器提供了精确的可行性检验,引用计数保证前缀共享安全。内存压力策略明确哪些任务可以丢弃、移动、延迟或拒绝;分块预算控制同机部署中两个阶段的干扰;传输模型则把状态与计算分离的代价显式呈现出来。
这些机制可以独立使用,也经常组合使用。分配器负责维护内存与所有权不变量;调度器则要依据负载的延迟、公平性、接纳率和成本约定,在可安全执行的计划中作出选择。混淆两者的职责,会让系统看似很快,却在第一次请求取消、缓存未命中、流量突发或块池耗尽时失效。
延伸阅读
- Yu et al., “Orca: A Distributed Serving System for Transformer-Based Generative Models” (用于自回归模型服务的迭代级调度), 2022. usenix.orgOrca 引入迭代级调度,让生成模型服务器在每个解码步骤后重组批次,而不必等待静态批次全部结束。
- Kwon et al., “Efficient Memory Management for Large Language Model Serving with PagedAttention” (vLLM 中基于分块的 KV 缓存分配), 2023. arXiv:2309.06180vLLM 通过分块分配与共享 KV 缓存来减少碎片,并提高服务系统可同时驻留的序列数量。
- Zheng et al., “SGLang: Efficient Execution of Structured Language Model Programs” (用基数树管理可复用的词元前缀), 2024. proceedings.neurips.ccSGLang 使用压缩有限状态机与跳跃式前向处理,减少结构化输出中确定性片段的顺序解码工作。
- Agrawal et al., “Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve” (分块预填充与面向停顿的批处理), 2024. arXiv:2403.02310Sarathi-Serve 将长预填充拆成多个块并与解码共同调度,以限制生成停顿并保留批处理机会。
- Zhong et al., “DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving” (在联合延迟目标下分离部署预填充与解码), 2024. arXiv:2401.09670DistServe 将预填充与解码放到独立 GPU 池,并用在指定 TTFT 与 TPOT 达标率下可持续的到达率定义 goodput。
- Qin et al., “Mooncake: Trading More Storage for Less Computation—A KVCache-centric Architecture for Serving LLM Chatbot,” 2025. usenix.orgMooncake 在分布式缓存层次中管理 KV 状态,以存储与传输容量换取更少的重复预填充计算。
评论
登录后评论