GLM5.3 CyberSec w4A16本地部署[5]:卸载换显存的边界

05 · 卸载换显存的边界

  • 📦 模型权重地址
  • 📦 部署仓库地址
    一句话:本篇回答「能不能把权重或 KV 卸到主机内存,换出更多显存来做大上下文」,权重卸载不可行、KV 卸载有条件可行但换不到上下文、DCP/PCP 搁置,真正管用的只有 PP + fp8 KV + 稀疏注意力。

0. 结论

  • 权重 CPU 卸载不可行 :粒度是整个 nn.Parameter,每 MoE 层每步都要搬 256 个专家的全部权重。
  • 「省显存」与「藏时延」严格零和 :净省 =(N−p)×S,要藏住拷贝就得 p≈N,效益为0。
  • 1 GiB 显存 ≈ 19,668 token 的 KV;最激进档(卸 18 层、净省 9.94 GiB/rank)只换池 ×1.58,代价是 batch=1 慢 14~21 倍。
  • KV 卸载是驱逐层,不是显存扩展 :不能提高 max_model_len、也不能提高同时运行的长请求数;本模型上主机内存只能当 1/8 用(426.6 KiB/token = 54,596 B × 8)。
  • 1.03× 池的档位实测零收益 :store 侧写了 581 GiB,但 CPU→GPU 换入 0 次、前缀命中增量 0;要起作用得开到 2.5~4×,而那会挤掉权重页缓存(重启 430 s → ~1200 s)。
  • DCP / PCP 搁置 :DCP 的容量 ×DCP 是代码事实,但当前后端启动即断言失败,可用化 15~25 人日;PCP 不增加分片数且被 MTP + full CUDA graph 双重阻断。
  • 提升大上下文的正确方案是 PP + fp8 KV + 稀疏注意力 ,不是卸载(当前 TP=4/PP=2 + 41,37 + MTP k=3 + fp8_ds_mla + 512K,池729,280)。

1. 先把两个基础量定下来:汇率和粒度

后面所有"显存能换多少 token"的推算,都依赖这两个量,所以先讲清楚。

汇率:1 GiB 显存 ≈ 多少 token 的 KV?

拿一次基线跑的配置来说(gmu 0.95、fp8_ds_mla、TP=8/PP=1、block_size=64),日志里写了两个数:

  • 可用 KV 显存:17.26 GiB
  • 对应 KV cache 容量:338,688 tokens
    拿这两个数反推,每个 token 在每个 rank 上占的显存是:
bash 复制代码
主 attention 层   : 79 层 × 656 B  = 51,824 B
dsa indexer cache : 21 层 × 132 B  =  2,772 B
─────────────────────────────────────────────
每 token / 每 rank = 54,596 B
  • 验算:338,688 × 54,596 B ≈ 17.22 GiB,和日志的 17.26 GiB 只差 0.22%,不到一个 block 的量,对得上。

容易忽略的点:indexer cache 和主 attention 共用同一个显存池。如果只算 79 × 656,结果会偏差 5%,跟日志对不上。所以必须把 indexer 的 2,772 B 加进去。

  • 最终结论:1 GiB 显存 ≈ 19,668 token(= 1 GiB ÷ 54,596 B)。换算到每个 block(64 token),每个 rank 占 3.33 MiB。

粒度:卸载的内容

  • vLLM 的权重卸载,最小单位是一整层 decoder layer,不是单个张量、也不是单个专家。具体机制是这样的(简化描述):
  1. 模型构建时,make_layers() 把每一层的参数注册给 offloader;
  2. 运行时,prefetch 后端按"参数名段"做白名单匹配(比如你写了 --offload-params experts,它就找名字里含 .experts. 的参数);
  3. 命中的参数整张量从 CPU 搬到 GPU,没有按行、按专家切片的逻辑;
  4. 至于 MoE 的路由(top-k 选哪几个专家),发生在层 forward 内部 ,等 offloader 看到时权重已经在显存里了,offloader 对路由完全不可见。

还有一个细节:MoE 的专家权重在运行时已经是一张合并好的大张量(dim0 = 256),不是 256 个小张量。逐专家拆分只存在于 checkpoint 文件里,加载时就merge。由此推出两个很实用的结论:

结论 含义
每步 PCIe 搬移量 = 卸载层数 × 0.5845 GiB/rank 跟 batch 大小、跟请求实际路由到哪几个专家完全无关,是个常数
MTP 头(第 79 层)卸不掉 它不走 make_layers,是单独构造的,那 ~0.58 GiB/rank 是固定下限

两个"静默失效"的坑

  • 这两个坑的共同特点是:不会报错、不会警告,你只觉得"卸载好像没生效",但日志里干干净净。

坑 1:参数名对不上,offload 整个跳过。

bash 复制代码
# ❌ 这样写不行
--offload-params w13_weight

# ✅ 应该写
--offload-params experts
  • 原因:实际参数名是 w13_weight_packed(带 _packed 后缀)。匹配逻辑是 ".w13_weight." in ".w13_weight_packed.",结果为 False,白名单为空,所有层被静默跳过。

坑 2:多参数的传法。 --offload-params 是 nargs='+',意思是空格分隔多个词:

bash 复制代码
# ✅ 正确
--offload-params experts w2

# ❌ 错误(会被当成一个字面字符串)
--offload-params '["experts", "w2"]'
  • 写完之后建议先看一眼日志里有没有出现 Offloading N parameters... 之类的行,确认真的生效。

2. 权重卸载:为什么粒度决定了它不可行

2.1 成本模型:一层一次 28.5 ms,而一层计算只有零点几毫秒

  • 取 A800 PCIe 4.0 x16 的 H2D 有效带宽 22 GB/s(A800 常见区间 20~25 GB/s,是本机假设值、未实测),按上面的粒度算:
卸几层 净省显存/rank 每步多搬 每步新增时延 batch=1 步时 25~40 ms 下的倍数
1 层 0.55 GiB 0.58 GiB 28.5 ms ≈1.7~2.1×
9 层 4.96 GiB 5.26 GiB 257 ms ≈7~11×
18 层 9.94 GiB 10.5 GiB 513 ms 14~21×
18 层(batch=64) 9.94 GiB 10.5 GiB 513 ms 2~4×
  • 用第 1 节的汇率换算:卸 18 层净省 9.94 GiB/rank ⇒ KV +195K token(×1.58),代价是 batch=1 慢 14~21 倍、batch=64 慢 2~4 倍 。反过来问「新增时延要小于步时的 10% 需要卸几层」:batch=1(步时约 25 ms)要求 ΔT < 2.5 ms ⇒ N < 0.09 层,无可行区间 ;即使按极重的 batch=64(步时约 200 ms)算,也只允许 N < 0.7 层,比一层还少,而一层只省 0.58 GiB/rank。

2.2 零和:prefetch_step 同时是「预取深度」和「buffer 份数」

  • 这是这条路线最致命、也最容易被误读的性质。prefetch 后端建的是一个静态 buffer 池,slot_capacity = prefetch_step,第 idx 个被卸载层用 idx % prefetch_step 号 slot:
python 复制代码
self.buffer_pool = StaticBufferPool(
    param_infos=param_infos, slot_capacity=self.prefetch_step, device=device,
)
for idx, offloader in enumerate(self.module_offloaders):
    slot_idx = idx % self.prefetch_step
    offloader.assign_buffer_slot(self.buffer_pool, slot_idx)

⇒ GPU 净省 = (N − p) × S 。而能不能重叠取决于 fork 语义:拷贝在一个独立的 torch.cuda.Stream() 上,start_onload_to_static() 先在本层 forward 的 kernel 全部入队之后 才 record_event(fork_event),copy stream 等这个 event 才开搬;next_index = index + prefetch_step 又决定了这一层发起的是哪一层的拷贝。于是:

  • prefetch_step = 1:搬的是紧邻的下一层,而下一层马上就在 wait_prefetch 上阻塞 ⇒ 零重叠,28.5 ms 是纯串行新增时延;
  • prefetch_step = p:最大可重叠的计算量 = (p − 1) 层 ,而 1 层的传输 = 28.5 ms、batch=1 时 1 层 compute 只有 0.3~0.5 ms ⇒ 要藏住 1 层传输需要约 60~90 层的 lookahead;
  • 取 p = N 时传输确实全被藏住,但此时 GPU 里仍留着全部 N 份权重,净省 = 0。

所以「把时延藏住」和「省出显存」不可兼得。 日志里 [PrefetchOffloader] Initialized {N} modules. Total GPU memory saved: {X} GB, Static buffer pool: {Y} GB 那行的 X 是搬到 CPU 的总量、不是净省 ,净省 ≈ X − Y,读日志时必须自己减。

2.3 UVA 后端

  • auto 的选路逻辑就两行:offload_group_size > 0 → prefetch,cpu_offload_gb > 0 → uva,都没有 → 什么都不做。

  • UVA 的思路是把权重放 pinned host memory,给 kernel 一个零拷贝 CUDA 视图,省掉了显式 H2D 拷贝。听起来 绕开了 PCIe 瓶颈,实际上更差:

  • MoE 的 grouped GEMM 按 tile 读权重,同一张权重会被反复读。一个 token 命中 8 个专家,和 tile 重叠后 PCIe 读放大约 2~5×。按 2× 保守估,一层每步 ≥ 57 ms------比 prefetch 的 28.5 ms 还慢。

  • 另有一个退化路径(VLLM_WEIGHT_OFFLOADING_DISABLE_UVA=1)会 patch forward,每次前向把整层 state_dict() 重新 .to(device)。对 753B 模型来说等于每层每步全量 H2D,直接不可用。

唯一还有理论价值的用法:--cpu-offload-gb 只卸一两个极少访问的 bf16 小张量。收益几百 MB/rank,相比"至少要省出 1 整层"的门槛,杯水车薪,而且读放大不可控。

2.4 开 offload ≠ 关编译 / 关图

  • 一个常见误解:"卸载只能跑 eager。" 代码和实测都不是这样。
  • Offloader 显式适配了 CUDA Graph:它注册了 wait_prefetch / start_prefetch 两个自定义算子来造数据依赖,捕获和回放前后还会同步 copy stream。config 里也没有"设了 offload 就关图"的分支。

真正的副作用在编译缓存 :OffloadConfig 的所有字段都参与 compile hash 计算。这意味着------

  • 开 / 关 / 改 offload 参数 → hash 变 → 旧的 torch_compile_cache 不能复用
  • 首启会多跑一次 inductor 编译(约 6~8 min)
  • 做实验时务必用独立的 CACHE_ROOT,别污染现网

小结(1/3)

权重卸载在"整层、无专家级选择、无 token 感知"这套粒度下,❌ 不可行 。不是"稍微降速"的问题:PCIe 带宽比 HBM 慢两个数量级,每步搬移量被层数锁死,收益和代价被 prefetch_step 严格绑定,不存在可行区间。除非上游支持"按 token 路由只取 active 专家",否则这条路对 GLM-5.3 是死路。

3. KV 卸载:机制成立,但它是驱逐层不是显存扩展

3.1 KV卸载相关解答

  • vLLM原生 OffloadingConnector + CPUOffloadingSpec,一个 --kv-transfer-config JSON 就够。先把预期纠正掉:
问题 答案 依据
能把 --max-model-len 提到 GPU 装不下的长度吗? 不能 启动检查 _check_enough_kv_cache_memory,完全不读 kv_transfer_config,仍要求 max_model_len ≤ GPU pool
运行中的请求,KV 可以「住在 CPU」吗? 不可以 命中只按 block hash 查前缀(offloading/scheduler.py:936-991),命中后要生成 GPULoadStoreSpec 拷回 GPU,请求挂到 WAITING_FOR_REMOTE_KVS 等 finished_recving
那它到底有什么用? ① prefix cache 有效容量扩大 ② 被抢占后少重算 同上;store 会对 preempted_req_ids 做 flush
能提高「同时运行的长请求数」吗? 不能 每个运行中的长请求仍要独占其全部已算 KV 的 GPU block

⇒ 一句话:它是「驱逐层 / 更大的前缀缓存」,与「提升单请求上下文」无关 。两个默认值也要注意:offload_prompt_only 默认 True(只卸 prompt block),多轮场景要显式设 false 才能把 decode 阶段的 KV 纳入;本模型 speculative_config.method == "mtp" 时 use_eagle() 为真,而没有任何 group 被标注 is_eagle_group ⇒ offloading scheduler 会把所有 group 当成 eagle group,解码阶段每个请求的尾部 chunk 不入 offload。

3.2 特殊折扣:主机内存只能按 1/8 算

一句话结论:GLM-5.3 的主机 KV offload,N GB 只能买到 N/8 GB 的有效容量。

原因:vLLM 有一个"TP 组内共用一份 KV 副本"的去重优化,但它要求整个 KV pool 是纯 MLA 且 spec 完全一致 。GLM-5.3 的 DSA indexer cache 也混在同一个 pool 里(head_size=132 vs 主 attention 的 head_size=576),spec 不统一 ⇒ 去重条件不满足 ⇒ 8 个 rank 各存一份完全相同的副本。

bash 复制代码
有效 KV = 主机内存 ÷ 8(world_size)

canonical_layout = true 解决不了------num_copies 仍由 replicated_layout 决定,而且默认配置根本没把这个参数传到 worker,属于半实现状态。

按 426.6 KiB/token(= 54,596 B × 8)折算:

主机内存 有效 token 对比 GPU 池 (338,688)
100 GB 228,928 0.68×
150 GB 343,360 1.01×
300 GB 686,784 2.03× ← 推荐起点
700 GB 1,602,560 4.73×
(若去重生效) 12,807,360 37.8× ← 被 indexer 挡掉了

📌 磁盘 tier 更不划算。 可以把 KV 落本地盘,但 FS tier 每个 block 写整行(含空槽)且文件名带 _r{rank},实际放大 8×8 = 64×。一块 2.5 TB 盘只换约 57 万 token(1.7× GPU 池)。此条为代码推导,未实测。

3.3 实测:1.03×池的档位,换入0次

  • 测量设计用的是共享长前缀负载:48 个互不相同 的 8,192-token 长前缀(合计 393K token)+ 各自的短问题;第 1 轮顺序铺满(把最前面的挤出 GPU 池),第 2 轮只重发前 8 个 (对照组里它们全部 已被挤出)。A/B 只差 --kv-transfer-config:
判据 OFF(基线) ON(cpu_bytes_to_use=150 GB)
服务能否起来 --- ✅ Creating offloading spec with name: CPUOffloadingSpec;KV offloading: EAGLE/MTP draft attention groups [0] detected
/dev/shm/vllm_offload_*.mmap --- 149,994,999,808 B ≈ 150.0 GB(预期 149,979,648,000 B ⇒ 容量模型实测吻合)
卸载容量(token) --- 343,360 (= 150 GB ÷ 426.6 KiB/token)⇒ ÷8 实测坐实
GPU KV cache size 338,688 334,464 (−1.2%,profiling 差异)⇒ 卸载不改变 GPU 池
第 2 轮被挤出前缀 TTFT 8/8 全部重算,≈15.4 s 8/8 仍然重算,≈15.6 s ⇒ 没有收益
vllm:kv_offload_store_bytes_total(GPU→CPU) 无此指标 624,358,039,552 B = 581 GiB(1,792 次、98.9 s)⇒ 确实在写
...{transfer_type="CPU_to_GPU"} --- 0 ⇒ 一次都没有换入过
prefix_cache_hits_total 增量 25,280(含上一次探针残量) 0

根因:

text 复制代码
CPU 层容量 = 343,360 token
GPU 池容量 = 334,464 token
比值       = 1.03×

CPU 层装不下比 GPU 池更多的历史 。而两边的驱逐都是 LRU:「被 GPU 挤出的 block」恰好就是「被 CPU 层最早挤出的那批」,不存在「GPU 没有、CPU 却有」的窗口 ⇒ 换入次数恒为 0。这与 vLLM 官方 KV Offloading 文档的 Tuning Tips 逐字一致("a smaller CPU tier just mirrors what the GPU already holds and adds no hit rate")。

  • 修正后的选型规则 :CPU 层要起作用,必须 ≥ 2.5~4× GPU 池的 token 数 。按 426.6 KiB/token 折算,现实天花板是 2~3×(300~450 GB),再往上就会与「启动快」直接冲突:
目标倍率 cpu_bytes_to_use 宿主 RAM 占用 与权重页缓存的关系
1.0×(本次) 150 GB 140 GiB(pinned、不可回收) 还能放下 390 GB 权重页缓存
2.0× 300 GB 280 GiB 页缓存剩约 670 GB,够
4.0× 600 GB 560 GiB 页缓存只剩约 390 GB ⇒ 刚好卡在权重工作集上,重启会变慢
8.0× 1.2 TB --- ❌ 超过参考机的 1007 GB 物理内存

还有一个必须算进容量的放大系数:store 是重复发生的 。581 GiB ÷ 426.6 KiB = 1.43M token 当量 ,而两轮逻辑 token 只有约 460K ⇒ 约 3.1× 重复写 (解码推进 / 分块会反复落同一批 block)。用 store_threshold ≥ 2 可以过滤掉只用一次的块。

3.4 落地的代价与开关

项 值
每 block 每次搬运(单 rank) 3,494,144 B(3.33 MiB);走 C++ ops.swap_blocks_batch(DMA)
128K 前缀换入 2,048 block × 3.33 MiB = 7.16 GB/rank ,8 rank 合计 57.2 GB ⇒ 单 rank ≈0.29~0.36 s,整机 0.3~0.6 s
这段时延的位置 在该请求的 TTFT 关键路径上 (等 finished_recving),但与其它请求的计算重叠
主机内存 /dev/shm 里的 mmap,启动时 cudaHostRegister 成 pinned + MADV_POPULATE_WRITE ⇒ 直接吃 RAM、不可回收 ,会挤压权重页缓存(下次重启变慢)
启动开销 分配 + pin 数百 GB 会明显拖慢启动;cudaHostRegister 失败只 warning(退化成 unpinned DMA,更慢)
容器要求 --ipc=host(本部署已是;否则 /dev/shm 只有 32/64 MB,直接 RuntimeError)
与分配器的互斥 PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True 与任何 KV connector 互斥(vllm/config/vllm.py:1005-1021),命中即 ValueError、容器 Exited(1)。配了 KV_TRANSFER_CONFIG 时脚本自动去掉该环境变量;要保留就 CUMEM_ALLOCATOR=1 走 --enable-cumem-allocator

落地形态是一个环境变量:KV_TRANSFER_CONFIG 直接透传 --kv-transfer-config 的 JSON 原文,留空即关闭(deploy.sh 的注释里给了 150 GB 的示例;常开建议 cpu_bytes_to_use = 300000000000 + offload_prompt_only=false + 视情况 store_threshold=2)。

小结(三分之二) :KV 卸载到主机内存 ⚠️ 有条件可行 ------「并发 / 多轮前缀复用」能提升,「单请求上下文」提升不了;条件是把 CPU 层开到 2.5~4× GPU 池,并接受主机内存 ÷8 与页缓存被挤压。

4. DCP / PCP:理论容量增益存在,当前不可用

4.1 容量乘数(代码事实)

--decode-context-parallel-size 在 v0.28 已存在(v0.29 新增 --dcp-q-replicate,v0.30 强制声明)。DCP 复用 TP rank(config/parallel.py:342),受 tp % dcp == 0 约束。

容量增益的机理:每请求 block 数与 max_model_len 除以 DCP(kv_cache_interface.py:267-306),池大小不变 ⇒ 有效容量 ×DCP。本质是收回 TP 组内 MLA latent KV 的复制份,不新增显存。上限由 TP 决定(TP=4 时最大 DCP=4)。

4.2 当前后端不可用(四条阻塞)

# 阻塞点 位置
1 启动断言要求 return_lse_for_decode,本后端 can_return_lse_for_decode = False cp_utils.py:45;backend.py:873
2 即使绕过断言,mla_attention.py:953 断言 lse is not None,overlay 显式返回 None mla_attention.py:953
3 DCP 需全局→本地 index 映射(triton_filter_and_convert_dcp_index),本后端 kernel 中 DCP 常量硬编码为 no-op(sparse_utils.py:257 注释 # DCP disabled)⇒ 静默读错 KV(不崩、无异常、结果错误) sparse_utils.py:257-260
4 Indexer DCP 合并依赖 CuteDSL,面向 Blackwell/Hopper,sm80 未验证 sparse_attn_indexer.py:53-77

4.3 若强行启用:代价与风险

  • 改造量 3~4 处:
  1. triton_mla_sparse_attention 输出 LSE(split 路径已计算,未写出);
  2. overlay 设 can_return_lse_for_decode = True,返回 (attn_out, lse);
  3. index 路径改走 triton_filter_and_convert_dcp_index;
  4. 验证/实现 sm80 下 top-k 合并。

附加约束:头数不可截断;reorder_batch_threshold=1;autotune 按 gathered 头数预热。

搁置理由(四条):

  • ROI 不成立。 纯 flag 的 PP=4 约 0.5~1.5 人日即可将单请求顶至模型上限。DCP 仅增加聚合容量(≈3.17M vs ≈1.19M,均为外推值,未实测),15~25 人日换此增量不划算。
  • 存在静默错误路径。 强制 MQA 下 prefill 走 forward_mqa,但 DCP combine 仅传 decode 的 seq_lens(mla_attention.py:959)⇒ prefill 行 LSE 被置 -inf ⇒ combine 内核将输出写为 0(common.py:121)。无 NaN、无异常,内容错误。修复需改上游 mla_attention.py,验收须覆盖 mixed prefill+decode 批。
  • 必须并入 fp8_kv 补丁链。 deploy.sh 每路径仅一次 bind mount,triton_mla_sparse_kernel.py 已被 fp8 overlay 占用,不能独立开 patches/dcp/。
  • 上游基准不支持零代价假设。 即便在可运行硬件上,sparse-MLA DCP/PCP decode 回退 0.66×(dcp4)、0.22×(pcp4+dcp4 PIECEWISE)、0.57×(FULL+MoE dedup)。本质是用 decode 带宽换容量。

PCP 更早排除 :不增加 KV 分片数;被 MTP + full CUDA graph 双重阻断(pcp_manager.py:125);需额外 GPU。

--block-size 加大为容量中性(分子分母抵消,须 64 倍数);--num-gpu-blocks-override 不造显存(超出即 OOM);--kv-cache-memory-bytes 仍受物理显存上限。

4.4 其他"省显存"手段排除

手段 结论
gmu 0.95→0.973 池 +7.4%~10.1%,单流≈无损;仅短 prefill 成立(余量 ~82 MiB),长 prefill OOM
max_model_len 327K→352K 单请求 +7.4%;不可由池容量倒推(600K prefill 瞬态激活超标 → OOM → 容器退出)
CUDA graph 捕获 9 档→4 档 池仅 +4.0%,单流 −13%,conc16 −19%,拒绝
int4 KV(656→296 B/层) ×2.22 理论增益,有损(RoPE 64 维亦压 4-bit),精度未验证
DP / 双 TP=4 实例 需 16 卡;单卡权重 97.5 GiB > 80 GiB,装不下

5. 正解:PP + fp8 KV + 稀疏注意力

  • 容量提升来自三条改变显存侧粒度的手段,而非改变数据搬运时机:
手段 作用对象 机制 实测收益 代价
PP 本 rank 层数 num_layers 变为本 rank 层数 PP=2 ×2.34;PP=4 ×3.52(token 口径) 流水线气泡;与投机解码的交互
fp8 KV 每 token 每层字节 656 B vs bf16 1152 B,RoPE 64 维不量化 池 ×1.71(TP8/PP1 口径) 高并发有代价,单流≈0
稀疏注意力 (DSA) 每 token 存多少 KV indexer 保留 top-k 索引(132 B/token/层) 与 fp8 叠加,每 token 每 rank 压至 54,596 B 内核 overlay 工作量
权重卸载 权重搬运时机 整层重传 净省 = 0(延迟零和) batch=1 慢 14~21×
KV 卸载 KV 存储位置 block 级 LRU,命中拷回 prefix cache 有效容量 ×2.0/×4.7;不提高 max-model-len 主机内存 ÷8;挤压页缓存
DCP/PCP KV 分片数 收回 TP 复制 理论 ×DCP(推断);当前不可用 15~25 人日 + 静默错误风险

核心区分:PP / fp8 / DSA 改变"显存里要放多少";卸载改变"何时从主机搬入"。前者换容量,后者(在 PCIe 带宽与整层/整块粒度下)只能换 prefix cache 命中率,换不到上限。

当前定稿配置 :TP=4 / PP=2 + PP_LAYER_PARTITION=41,37 + MTP k=3 + fp8_ds_mla + max_model_len 524288。

指标 值
KV 池 729,280 token
单流吞吐 80.0 tok/s(自由)/ 71.9(受控中位)
conc16 吞吐 297~310 tok/s
128K 并发 5 条
退化率 0/200

其中 PP_LAYER_PARTITION=41,37(将 2 个 MoE 层从 PP1 挪至 PP0)本身贡献 +22% KV 并将最紧余量抬高 38%(余 2,479 MiB),依据见 02、03。

红线:1M 上下文当前不可用。 必须走 PP=4,而 2026-10-02 实测 PP=4 即使关闭投机解码仍有 20% 退化(开 MTP k=3 时 45%),失败形态为首 token 即多语言乱码。根因:DSA 跨层索引共享修复仅覆盖 PP=2 边界。deploy.sh 中 check_pp_dsa_gate() 已硬拦截。启用 1M 需将该修复扩展至 PP>2,属工程改动,非参数调整。

6. 踩坑与证否

我们以为 实际 为什么错 现在的规则
KV 卸载能换更长上下文 它是驱逐层,请求 KV 必须全在 GPU 上;只增 prefix cache 命中,不提高 max_model_len 把"有效容量"(前缀历史)和"上限"(max_model_len)混为一谈 容量结论只认两条:GPU KV cache size 日志 + 真发一个请求
卸载池开大点总没坏处(选了 150 GB) 150 GB ≈ 1.03× 池 ⇒ 换入 0 次,store 写了 581 GiB 全是白写 没意识到 LRU 在两层上重复;容量不是单调收益 CPU 层 ≥ 2.5~4× GPU 池才算起点;A/B 必须同时看 store_bytes 与 CPU→GPU 换入两个方向
8 个 TP rank 共用一份主机副本 num_copies = 8,N GB 主机内存只买到 N/8 GB 有效 KV 本模型 DSA indexer(132 B/page)与主 KV(656 B/page)同池但 page size 不同 ⇒ 退化成 UniformTypeKVCacheSpecs,replicated_layout 不生效 看 /dev/shm mmap 文件大小,直接除以每 token 字节数验证
权重卸载 = 用一点速度换显存 严格零和:event 在计算入队后才记录 ⇒ p=1 零重叠;要重叠则 p≈N,净省为 0 只看到"有独立 copy stream",没看 event 记录时机与 slot_capacity 绑定 不再评估此路线;仅"模型装不下"时当容量手段;有效写法是 --offload-params experts(写 w13_weight 会静默不生效)
开 offload 会拖累 CUDA Graph 捕获 不需要 --enforce-eager;真正副作用是 compile hash 变了 ⇒ 存量编译缓存作废 --- 实验必须用独立 CACHE_ROOT
DCP 理论 ×8 是既有能力 上限由 TP 决定(TP=4 ⇒ 最大 DCP=4);且当前后端启动即断言,可用化 15~25 人日,含一条会静默把 prefill 输出置 0 的上游修复 把"公式是代码事实"当成"今天就能用" 仅在需求真超 1.05M 单请求 / 1.19M 聚合时考虑;第一步先在单卡 spike dcp_indexer_cutedsl 能否在 sm80 编译

方法学教训 :凡"卸载 / 缓存 / 复用"类实验,结论必须同时看写入侧与读取侧 。本轮 KV 卸载 A/B 之所以可信,是因为四条判据同时成立:store 在写、换入为 0、TTFT 没变、命中计数为 0。如果只看"服务起来了 + 日志打了 Creating offloading spec",会得出完全相反的结论。

7. 复现命令

bash 复制代码
# ── A. 观察当前缺省形态(关掉卸载)的容量基线 ─────────────────────────
# 当前是 TP=4/PP=2 + 41,37 + MTP k=3 + fp8_ds_mla + 512K;池子应为 729,280
./deploy.sh start
./deploy.sh logs | grep -E "Available KV cache memory|GPU KV cache size"

# ── B. KV 卸载 A/B
# B1. 关闭(对照组)
KV_TRANSFER_CONFIG='' CACHE_ROOT=deploy/.cache-offload PORT=8022 \
  ./deploy.sh start
# B2. 打开(150 GB ≈ 1.03× GPU 池;这是"零收益"那一档,用来复现换入 0 次)
KV_TRANSFER_CONFIG='{"kv_connector":"OffloadingConnector","kv_role":"kv_both","kv_connector_extra_config":{"spec_name":"CPUOffloadingSpec","cpu_bytes_to_use":150000000000,"offload_prompt_only":false}}' \
  CACHE_ROOT=deploy/.cache-offload PORT=8022 \
  ./deploy.sh start

# 启动校验(三个关键字都要出现)
./deploy.sh logs | grep -E "Creating offloading spec|Created mmap file|EAGLE/MTP draft attention groups"
# 容量自检:mmap 大小应等于 num_blocks × 27,955,200 B(= 3,494,144 B × 8 副本)
ls -l /dev/shm/vllm_offload_*.mmap
#    150 GB 档预期 ≈ 149,994,999,808 B;token 数 = 150 GB ÷ 426.6 KiB ≈ 343,360

# ── C. 判据:写入侧与读取侧必须分开看 ────────────────────────────────
curl -s localhost:8022/metrics | grep -E 'kv_offload_(store|load)_bytes_total'
#    实测:store = 624,358,039,552 B(581 GiB);transfer_type="CPU_to_GPU" = 0  ← 换入 0 次
curl -s localhost:8022/metrics | grep -E 'prefix_cache_hits_total'

# ── D. 前缀复用 A/B 探针(判据是"被挤出的前缀能否免重算")────────────
#    默认 338688 是 gpu-pool-tokens 缺省,换到当前形态时按实际池子传
python3 scripts/bench_prefix_reuse.py --prefixes 48 --prefix-tokens 8192 \
  --round2-first 8 --gpu-pool-tokens 334464 --base-url http://localhost:8022/v1 \
  --label offload-on --out /tmp/prefix_reuse_on.json
#    对照:把同一个探针指向未开卸载的实例(--label offload-off)

# ── E. 权重卸载:只做静态检查,不要在生产上跑 ────────────────────────
#    flag 全集(help 需绕开 device 推断)
docker run --rm --entrypoint bash -e VLLM_TARGET_DEVICE=cpu \
  vllm/vllm-openai:v0.28.0-glm53-sm80 -lc 'vllm serve --help=all' \
  | sed -n '/^OffloadConfig:/,/^MultiModalConfig:/p'
#    ⚠️ 反例:--offload-params w13_weight 匹配不到 w13_weight_packed;
#       日志里 [PrefetchOffloader] 的 "Total GPU memory saved" 是搬到 CPU 的总量,
#       净省 ≈ 该值 − Static buffer pool 的大小。

# ── F. 顺带验证"主机内存 ÷8"的判据(无 GPU,纯 Python)──────────────
docker run --rm --entrypoint bash vllm/vllm-openai:v0.28.0-glm53-sm80 -lc 'python3 - <<PY
import torch
from vllm.v1.kv_cache_interface import MLAAttentionSpec, UniformTypeKVCacheSpecs
main = MLAAttentionSpec(block_size=64, num_kv_heads=1, head_size=576, dtype=torch.uint8, cache_dtype_str="fp8_ds_mla")
idx  = MLAAttentionSpec(block_size=64, num_kv_heads=1, head_size=132, dtype=torch.uint8)
specs = {f"layer.{i}.attn": main for i in range(79)}
for i in [0,1,2,6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74]:
    specs[f"layer.{i}.indexer.k_cache"] = idx
u = UniformTypeKVCacheSpecs.from_specs(specs)
print(type(u).__name__, u.page_size_bytes, len(u.kv_cache_specs))
PY'
#    预期:UniformTypeKVCacheSpecs 3089664 100  ⇒ type(...) is MLAAttentionSpec 为 False
#    ⇒ replicated_layout=False ⇒ num_copies = world_size = 8
相关推荐
bjxiaxueliang18 小时前
一文自建STUN:在Ubuntu上分步搭建STUN服务
linux·运维·ubuntu
陈让然19 小时前
Ubuntu Podman 容器镜像构建
linux·ubuntu
江厌011 天前
两张卡部署两个模型:物理分卡、MIG、张量并行的选择与可复现配置
github·模型部署·vllm
Techblog of HaoWANG1 天前
Ubuntu 下将 Ollama 服务配置为监听 0.0.0.0:11434 完整实践
linux·数据库·ubuntu·ollama
张文君1 天前
ubuntu iscsi客户端 关闭磁盘
linux·运维·ubuntu
阿星_1 天前
Linux Ubuntu 查看 SQLite 工具
linux·ubuntu·sqlite
张文君1 天前
Ubuntu iSCSI CHAP认证与Win11挂载
linux·运维·ubuntu
JANGHIGH2 天前
基于ADE20K数据集的SETR模型训练
ubuntu
丰色木夕2 天前
Ubuntu22.04下源码编译安装USD-Brep
c++·ubuntu·nvidia·usd-brep