从总共 1M 到 15+ 路 × 1M:GLM-5.3 KV Cache 换出的容量突破

让每台 1T 主机内存承接长历史,把同时解码的上下文容量推向 15+ 倍;50K 单路实测近乎无损。完整记录每次决策、失败与转向。

**这次优化最大的收益,是把原来总共约 1M token 的上下文容量,推向 15+ 路、每路 1M token 历史同时 decode 的规模。**同一套 GPU,能够同时承接的长历史容量朝着 15+ 倍扩展。

原来,一份百万 token 的长对话就可能吃掉整套系统的上下文预算。现在,每台机器的 1 TiB 主机内存开始承接 MLA 历史,GPU 留下 32K 热缓存供当前计算使用。我们要支持的是多位用户各自带着百万 token 历史持续生成,而不是只把结束的会话存下来。

**15+ 路 × 1M 是本轮的容量估算规模,尚不是已经完成的并发性能实测。**下文会展开它的内存账;近乎无损的速度结论来自已经完成的 50K 对照。这两项成果分别回答“能承接多少历史”和“换出会损失多少速度”。

我们做到了一个有明确验收范围的结果:固定 50,023 token 输入、1,024 token 输出,保留 32K GPU 热缓存,MLA 历史换到主机内存后,MTP=3 三组对照的吞吐损失为 0.15%–1.30%;不开 MTP 的三组对照略快约 2%。所有完整输出都与各自基线一致。

这个结果来得并不直接。最初我们想把索引计算也交给 CPU,后来退回;热缓存一度损失约三成速度;几次局部优化看起来很有道理,完整模型却没有变快;最后那一点不稳定,又把我们从 attention kernel 带到了 Linux 内存管理和 GPU 驱动。

这篇文章按决策的形成过程展开:为什么走一条路,什么证据让我们保留它,又是什么证据迫使我们转向。

起点:把容量和当前计算的工作集分开

硬件沿用上一篇 GLM-5.3 优化文章的双机系统:每台 8 张 AMD Radeon PRO W7900D,单卡 48 GiB,EPYC 9334 平台,每台 1 TiB RAM。节点内使用 HIP P2P,节点之间通过 TCP 传递层段边界的 activation;没有 XGMI。

这次改变的是 KV 的放置。权重和每层计算仍留在负责它的设备,历史数据也留在对应节点的主机内存中。

机会来自 GLM-5.3 的稀疏注意力路径:DSA 索引器从历史中选出 Top-2048 位置,MLA attention 消费选中的 KV。部分后续层复用 selection,但各层仍有自己的 KV 内容。

因此,全历史需要保存的规模,与一轮 attention 真正消费的规模,可以分开管理。

本次 Q8G64 MLA 每行包含 512 B latent、16 B scale 和 128 B RoPE 数据,共 656 B。原始工程记录的容量账如下;它只计算 MLA 数据,不含 DSA、MTP 层、元数据、对齐或 scratch。

对象MLA 数据大小
50,000 token,单层单副本31.281 MiB
50,000 token,78 个主模型层、两份副本合计4.765 GiB
一层选中 2,048 行,单副本1.28125 MiB
32,768 行热缓存,单层单副本20.5 MiB

单份 50K 历史还不惊人,但它会随会话数量和长度累积。我们的目标是让 GPU 上主要的 MLA 历史存储被热缓存容量约束,增长部分由 RAM 承担。

这里的“热缓存”也不是只保留最近 32K token 的滑动窗口。**旧位置仍保存在 RAM,DSA 仍可以选中它们;缓存未命中时再读取。**它改变物理位置,不主动截断模型可访问的历史。

第一次决策:CPU 既存历史,也做 Indexer?

最初的方案相当自然:主机有大量内存和 CPU 核心,既然历史准备放在 RAM,就让 CPU 完成全历史打分和 Top-K,再把选中的 KV 送给 GPU。

这样还有一个调度上的诱惑:得到 selection 后,后续复用选集的层可以提前搬运历史 KV,与 GPU 的独立工作重叠。

我们已有 CPU Q8 打分、blocked 布局和向量化实现,于是补上固定 worker team、局部直方图、Top-K 工作区复用、增量 mirror、回滚和预取。这条路值得验证,不能只靠“CPU 比 GPU 慢”就否定。

真机结果却给了两个独立的否定理由。

第一是速度。50K 测试中,GPU 基线约 14.03 token/s,CPU DSA 候选只有 10.92–11.32 token/s。CPU 算术之外,query 回传、线程协调、选集上传和 GPU 等待也进入关键路径。历史上 131K 的 CPU 测量不能简单乘以 50/131,因为活跃 worker 数也随上下文改变。

第二是正确性。CPU Q8 query 的最终选集与 GPU BF16 query 的精确路径存在 cutoff 差异,固定贪心输出发生分叉。CPU 内部计算自洽,并不等于它与原 GPU 路径等价。

决定:保留 GPU exact DSA,关闭 CPU selection;继续推进 MLA 历史的 RAM 驻留。

这次退回缩小了问题。存储容量扩展可以独立成立,不必等 CPU 索引路线成功。提前预取仍是有价值的重叠机会,但不能预先记成零开销。

第二次决策:先让 CPU 管理 GPU 热缓存

保留 GPU selection 后,早期热缓存路径仍需要把选集交回主机:CPU 查映射、判断命中、整理缺失行,再提交上传和 GPU attention。

这条路首先验证了容量方向,但性能很差。2026 年 9 月 7 日的早期战役中,真实 32K 热缓存配置经历 packed 复制和批量 mirror 修正后,三轮中位仍约 10.41 token/s。该阶段报告相对其参考值约慢 29%;这些早期数字包含不同诊断条件,不能与后文最终验收直接拼成严格 A/B。

这里先暴露了两个具体问题。

**一个是换出分支丢掉了原来的压缩复制路径。**此前 owner 量化一次,再把每行 656 B 的 packed KV 复制给 peer。进入热缓存后,fallback 变成两端各自 append、重新量化,也损失了原有通道重叠。于是先把 packed 复制接回热缓存。

**另一个是小传输的次数。**逐层、逐 token 更新主机镜像,78 层的 owner 与 peer 合计可以产生 156 次流操作。尝试把每行分成三段并增加在途传输后,操作数变为 468,性能反而退到约 8.8 token/s。

这个实验否定了“增加异步并发就能解决传输成本”的直觉。数据本来就很小,更多提交可能比更多字节更贵。

改成每 64 行批量回传后,操作数量大幅下降,全量缓存阶段确实改善。但进入真正热缓存后,仍有明显差距。

随后我们尝试把 peer 镜像维护移入 worker 锁段,又让 append kernel 双写连续日志。三轮中位分别约 10.3910.06 token/s,没有得到预期的量级收益。

**决定:保留批量日志和必要的生命周期修复;撤回“锁迁移、日志双写足以解决主要差距”的判断。**实现完成与性能假设成立,需要分别验收。

一次必须承认的误判:占比最高,不等于回退来自那里

我们曾给热缓存 attention 加同步 trace。测得的总时间里,kernel 占 77.4%,主机锁与 remap 约占 22%。当时据此判断:剩余问题主要在窗口 attention kernel。

后续复核推翻了这个归因方法。

这份 profile 测的是候选自身的绝对时间构成,没有证明 GPU 常驻与换出之间的差额发生在同一位置。常驻路径本来也要执行 attention。另外,同步与 trace 把完整运行压到约 4.21 token/s,已经明显改变了原有提交和重叠关系。

据此提出的 selected gather 方向,检查源码时又发现已有相关实现。需要验证的是实际热路径和依赖,不能把已有机制重新当作待实现方案。

我们调整测量方法:诊断只用来定位,正式性能关闭 profile、trace 和额外同步;每个候选配同版本 GPU 基线,完整输出一致后再比较吞吐。需要归因的始终是两条路径的差额。

第三次决策:让 GPU 直接管理热缓存

新的证据把注意力带回提交链:GPU 已经算出 selection,却要让 CPU 等待回读,再做映射和打包,才能继续提交 attention。一次选集回传的主机等待曾达到约 0.7–1.2 ms;其中包含此前 GPU 排队,不能把它全算成复制时间,但它确实暴露了控制往返。

于是改变缓存管理的位置。选集留在 GPU,GPU 直接查热槽;命中读显存,未命中读已注册的 RAM mirror,形成当前 attention 使用的紧凑选集。

GPU exact DSA ──→ 历史位置 selection
                    GPU hot gather
                    ├─ 命中:读显存热槽
本节点 RAM 历史 ────└─ 未命中:读注册的主机内存
                    紧凑 KV → GPU attention

新增 KV → GPU recent 环 → 分批回传 → RAM 历史

主机内存因此承担历史容量,GPU 保持索引、缓存查找和 attention 的连续执行。CPU 仍负责资源与生命周期管理,但不再为每次 selection 主持一轮查表和搬运。

它也没有让 PCIe 读取免费。早期某些采样窗口 miss 不到 1%,说明这个负载具有缓存复用机会;不能由此保证所有输入都有相同命中率。

最初 GPU 管理版本约 11.72 token/s,仍有首次注册停顿。提前注册并补齐多行路径后,甚至有一轮退到 5.13 token/s。我们继续保留方向,但不把“稳态事件间隔接近常驻”当作完整吞吐通过。

GPU 管缓存也会犯错:跨 block 可见性与置换长扫描

MTP 把单行路径里不明显的问题放大了。一次 verify 会处理多行,多个 query 可能选到同一个缺失位置。

早期实现允许同一次 gather 内,一个 block 读另一个 block 刚填的热槽。真实 MTP 测试出现异常 scale,随后没有有限的可选 token。

我们把可见性边界收紧:**本轮新填的槽,到下一轮才作为命中来源;同轮重复 miss 直接读取 RAM。**本轮正在使用的槽先 pin,新增但尚未回传到 RAM 的 token 则由 recent 环保护。

之后完整 1,024-token MTP 输出与同版本常驻一致,但吞吐只有 16.175 对 27.530 token/s,仍慢约 41.2%。两边接受的 draft 都是 665 个,差距不能归因于 MTP 接受率。

接下来把小批 verify 也接回 owner→peer packed KV 复制,候选升到 20.035 token/s。这是值得保留的完整执行路径修复。

缓存置换又带来一个反直觉结果。使用带引用位的时钟置换时,在“热窗已满、引用位全部置位、只有 1% miss”的合成压力形态下,少数 miss block 要负责长距离清扫,gather 达到约 1.03–1.07 ms。miss 更多时,反而可能因参与清扫的 block 增多而更快。

改成跳过当前 pin 槽的环形替换后,同一探针降到约 0.032 ms(2,048 行)。但是完整 MTP 运行只有 19.767 token/s,没有体现探针的巨大收益。

决定:保留更简单的置换实现,消除已证实的压力形态;否定“这个局部热点就是完整差距”的推断。

第四次决策:离开 attention,检查提交线程在做什么

缩小到活跃 decode 线程的 CPU 采样,发现约 21.1% CPU cycles 落在主机 memcpy。调用链最终落到只读 MoE 权重对象的 clone:每次向 worker 提交工作,都可能深拷贝 Dense router。

这与 KV 的语义无关,却真实地影响了同一条执行链。修复让只读权重组通过 Arc 共享,保留算子和权重数值。

我们也给 GPU 常驻基线加入同一修复。v15 同版本 MTP 对照达到 25.425 对 26.832 token/s,损失收窄到 5.24%。这是显著改善,但仍略超 5% 门槛;21.1% 的 CPU 采样占比也不能换算成等比例端到端收益。

后续继续复用 recent 环承载 peer 日志,省掉重复复制;移交 prefill 已有的缓冲,减少热缓存切换时的分配与释放;小日志用 compute 拷贝和打包,减少 DMA 队列操作。

这些改变没有一次性解决问题。某轮 no-MTP 进入 4.80%,下一轮 MTP 又损失 9.87%。日志里不断出现 0.6–1.2 秒的长停顿,即使大多数事件间隔已经接近常驻。

我们没有删掉前 128 个 token,也没有只取 p50 来宣布完成。用户实际等待的这些停顿必须计入原有完整 decode 口径。

第五次决策:从慢 kernel 追到操作系统页迁移

设备时间线显示,长停顿未必落在 hot gather。一次采集中 gather 最大只有约 0.054–0.056 ms,却有约 792 ms 的同步等待,整条流水线几乎没有设备工作。

这时,把 profiler 中一个被拖长的 kernel 名称当作根因,会再次走错路。我们开始对齐两台机器的系统设置与驱动行为。

关闭 NUMA 自动平衡:方向正确,证据还不够

发现 Amd-1 的 kernel.numa_balancing=0,Amd-2 为 1,长停顿集中在后者。统一关闭后,第一组 MTP 损失只有 0.51%,看起来已经成功。

但第三组交错复测损失 5.49%,门禁停止。决定保留系统设置一致性,继续寻找剩余长尾,不用平均值掩盖失败组。

预先写入注册范围的尾页:一个被实验否定的解释

另一个假设是:RAM mirror 预留容量中的尾页尚未实际写入,后续追加首次触页影响了 GPU 映射。于是注册前把 spare capacity 写零,保持有效 KV 字节和逻辑长度不变。

该候选仍出现 7.35% 损失和秒级间隔。预触页没有解决稳定性,不能把它写成最终根因。

主动内存整理:终于取得调用链证据

进一步使用 BPF 对齐实际推理进程、失效地址和恢复 worker,捕获到这样的路径:

kcompactd
  → proactive_compact_node
  → migrate_pages
  → try_to_migrate_one
  → amdgpu_hmm_invalidate_hsa
  → 相关队列恢复工作

这次证据确认了主动页整理、HMM 失效与队列恢复路径。失效地址采样中,大量事件并不与显式注册的 KV mirror 相交,所以也不能再把它们解释为“KV 尾页首次写入”。这次采样没有复现之前完整的一秒停顿,因而我们没有声称每次长停顿都已逐一归因。

两台机器随后将 vm.compaction_proactiveness 从 20 调为 0,NUMA 自动平衡保持关闭。Linux 官方文档说明,0 会关闭主动整理,页迁移可能造成应用延迟尖峰;本次因果判断还要靠同设置下的完整对照。Linux 内核文档

这些是实验机的运行时调整,原值和恢复命令均留档,没有修改开机配置。它们属于这次验收条件,不能省略,也不能当作所有 GPU 机器通用的调优配方。

六组交错对照:到这里,才肯定“近乎无损”

最终使用同一 v23 二进制,固定 50,023 输入、1,024 输出、greedy seed01。换出组启用 32,768 行热缓存,对照组保持 GPU 常驻;双方关闭 NUMA 自动平衡与主动 compaction,关闭 profile、trace、attach 和 CPU DSA。

每次重新启动实验运行时,交错安排常驻与换出顺序。吞吐沿用首个文本事件至 SSE DONE 的原有口径,没有删除停顿。

模式RAM 历史 + GPU 热缓存(token/s)GPU 常驻(token/s)换出吞吐损失
MTP=3127.83927.8830.157%
MTP=3227.92327.9660.150%
MTP=3327.73828.1021.297%
no-MTP114.04213.788−1.846%
no-MTP214.02913.753−2.007%
no-MTP314.02913.747−2.056%

全部 12 个请求完成,完整输出文件 hash 与各自模式的参考一致。负损失表示换出略快;我们把它理解为这套负载下基本打平,不把它外推成换出必然加速。

这个结果支持的是单路长历史 decode。初始 prefill、追加输入、多会话容量和并发吞吐还需要各自验证。

容量优化必须通过会话生命周期

一次连续生成跑通,还不足以支撑真实对话。用户会继续追加输入,MTP 会拒绝草稿并回滚,会话还会保存和恢复。

成功路径的完整性边界是:新增 KV 可以先在 GPU recent 环和在途回传中;**导出或恢复全量历史之前,必须补交日志、等待回传,并确认 CPU 行数追上逻辑行数。**不能把已提交的异步复制当作已经落入 RAM。

大段 append prefill 又与 decode 不同。一次要处理大量新行,当前实现会先从 RAM 恢复所需全历史 GPU MLA KV,完成追加,随后 decode 再转回热缓存。小尾块不能被误判成 verify,owner 和 peer 也必须保持一致。

第一次真实大段 append 没通过,暴露的却是 MTP terminal hidden:收尾保存了原始 BF16 残差,后续路径期待 final norm 后的 F32 hidden。修复统一了归一化边界,并处理旧缓存恢复。

v25 随后完成同版本对照:命中 50,085 行历史,追加 9,854 token,最终 prompt 为 59,939 token,生成 1,024 token。完整输出一致。

大段 append 对照换出GPU 常驻
首 token 时间17.905 s18.350 s
Decode26.850 token/s27.004 token/s

单组 decode 损失 0.57%。这说明换出后的长对话可以继续追加,但它仍是单组追加验收,不替代前面的六组单路测试。

十路实验:逻辑上省了显存,实际 allocation 仍能把卡撑满

准备十份独立 50K 历史时,第六份 prefill 在尾节点 OOM。1 Hz 采样最高约 47.98 GiB,还没有进入十路共同 decode。

问题在通用 GPU 内存池。小的长期 KV、热缓存和会话工作区,可以 best-fit 借到 prefill 释放的大块 allocation,并长期占住。逻辑上只用几十 KiB 或 MiB,物理上却可能扣留远大于此的空间;统计又只报逻辑字节,进一步低估占用。

因此我们给长期缓存分配加上精确容量限制,主要 KV/DSA 统计改用实际 allocation 大小,短期 activation 保留原池策略。测试先用大块污染内存池,再验证小热缓存不会占住那些大块。

修复后,v28 完成十份长历史及后续十路 append,整组实际吞吐 98.56 token/s,采样观察到的显存最高约 45.12 GiB

接着调整并发调度:MTP 深度改为 1 后,完整请求吞吐到 125.26 token/s;每段 decode 合批上限从 4 改为 1,达到 145.67 token/s。后一组与前一组输入、完整输出及 usage 一致。多路工作可以持续推进流水线,单路有效的深推测和较大合批,不一定适合十路。

再单独放宽 A0 的已就绪工作合批上限,得到 145.28 token/s,基本持平,没有证据支持保留,因而撤去该改动。

这些并发数字按实际 completion token 总数除以整个 append 请求墙钟计算,包含请求首尾;没有把 SSE 文本事件数直接当成 token 数。十路结果也没有 GPU 全常驻的对应性能对照,不能用它宣称并发换出无损。最新并发候选尚需回归单路门禁。

容量收益:从总共约 1M,走向 15+ 路各自 1M

把收益落到使用场景上:原来的总预算约 1M token,如果一位用户带着百万 token 历史,这份预算就基本被占满。换出后,我们希望让 15+ 位这样的用户同时处于 decode,每一路都保有自己的百万 token 历史。

维度原来的容量口径换出后的容量估算目标
所有活跃会话的历史总量约 1M token15M+ token
每路携带 1M 历史时的规模约 1 路15+ 路同时 decode
MLA 全历史的主要存储位置GPU 显存每节点 1 TiB 主机内存
GPU 上的 MLA 历史数据随历史长度增长每路保留 32K 热缓存,按需读取旧位置

这就是“容量几乎放开了”的工程含义:在相同 GPU 硬件上,把可承接的活跃长历史规模推向 15+ 倍。它扩展的是多会话的历史容量;15+ 路同时生成时的单路速度,仍由计算、索引、传输和调度共同决定。

15 路百万历史,RAM 装得下吗?

按本文相同的主模型 MLA 格式,取 1M = 1,048,576 token,78 层、owner/peer 两份副本:

单路 1M 历史 = 1,048,576 × 656 B × 78 × 2
             ≈ 99.94 GiB

15 路合计   ≈ 1,499.06 GiB(约 1.464 TiB) 主模型 MLA 历史
两台主机 RAM 合计 = 2 TiB

这里必须按两台机器合计 2 TiB 计算,不能把全部历史算到一台 1 TiB 主机上。MLA 历史跟随所属层分散驻留;owner/peer 双副本已经包含在上述数字中,也不意味着每台都保存完整模型的全部历史。

以 15 路为例,主模型 MLA 历史合计约 1.464 TiB;按两节点近似均分,每台约 0.732 TiB。实际按各节点负责的层数分配,仍需逐节点核算余量。**从主模型 MLA 历史载荷看,15 路并没有用尽双机内存,容量可以继续向 15+ 路扩展。**这是编码尺寸推导的容量依据,尚不是整机内存峰值或最大并发路数实测。

与此同时,15 路的 32K 热缓存,主模型两份副本跨全部 GPU 的载荷合计约 46.85 GiB。这一项不随每路历史从 50K 增长到 1M 而同比增长,正是容量扩展的关键。

完整服务预算还要加上 GPU DSA 历史、token 映射、MTP 层、各会话工作区、分配与注册开销,以及 prefill/大段 append 恢复全历史时的瞬态显存。因此,RAM 能容纳这些 MLA 字节,是 15+ 路目标的容量基础,还不是整套系统在这个规模下的验收结论。

目前完成的速度与生命周期实测是:50K 单路近乎无损、约 60K 大段追加,以及十份 50K 长历史的完整并发请求。**下一步最有价值的容量验收,就是把每路历史推到 1M、把活跃路数推到 15+ 路,同时记录真实内存占用、首 token 延迟和持续 decode 吞吐。**当前的 1M admission 配置也要与新的总容量目标匹配。

最后留下的决策

路线或判断处理结果决定性证据
CPU 完成最终 DSA selection本轮退回速度下降,固定贪心输出分叉
MLA 历史放 RAM,GPU 保留热工作集保留六组单路对照通过,完整输出一致
CPU 主持每次 selection 的热槽管理主路径转向 GPU 管理回读等待打断提交链
增加更多逐行异步拷贝否定操作数增多,吞吐反而下降
批量日志、packed owner/peer 复制保留减少重复处理,补齐单行与 MTP 路径
锁迁移或日志双写足以填平差距否定实现后完整吞吐没有量级改善
kernel 占比高,所以回退一定在 kernel撤回绝对占比不能解释常驻/换出差额
简化 GPU 置换保留,但不夸大收益压力探针明显改善,完整模型未同步提速
只读权重深拷贝改为共享保留同版本完整对照差距收窄
只关 NUMA 自动平衡就足够否定“足够”重复门禁仍失败
预触注册尾页就是长尾解法否定秒级停顿与 7.35% 失败组仍在
统一 NUMA、关闭主动整理后验收本机保留内核路径证据与六组交错对照支持
小逻辑 buffer 就等于小显存占用否定十路准备 OOM,实际 allocation 超额
并发时继续增加 A0 合批本次撤去完整吞吐基本持平

这轮工程最值得复用的,是把历史容量、当前工作集和控制依赖分别处理。RAM 提供容量,GPU 热缓存维持局部访问,GPU 自己消费 selection,批量日志维护新增历史;主机和驱动的内存行为,也必须进入端到端测量。

每一次转向都有一个共同标准:完整请求有没有变好,输出是否一致,失败能否复现。沿着这个标准,我们才把“旁边还有 1T 内存”的硬件余量,变成了可用的长历史容量,并把目标从总共约 1M 推向 15+ 路各自 1M 的同时解码。


资料与口径:依据 zLLM 工程记录 docs/glm53-cpu-kv-decode.md(2026-09-05 至 09-08)、双机基线记录 docs/glm53-rocm-amd12-baseline-20260906.md,并核对本地 GPU 热缓存实现。正式单路数据来自记录中的 cp0-v23 六组门禁;追加为 append-v25,并发为 concurrency-v28concurrency-tuning。历史探索、诊断探针、正式配对和后续并发候选在文中分别标注,不能跨版本合并成同一条严格性能曲线。本文没有重新运行远端 benchmark。

← 回到所有文章