跳到正文

目录

DeepSeek V4 Flash 单卡 AMD MI300X:ryanzhou 把 vLLM-ROCm 调成 168.6 tok/s 单流 + 64 流 830 tok/s 的工程复盘

一、为什么 304B MoE 要塞进一张 MI300X

DeepSeek V4 Flash 是 304B 参数的稠密 + MoE 混合模型(checkpoint deepseek-ai/DeepSeek-V4-Flash-0731,MIT 许可)。官方 vLLM recipe 只覆盖 NVIDIA 和 MI325X/MI355X,MI300X 不在其列——单卡跑生产,靠的是一整套补丁栈:先把一个 vLLM ROCm nightly 的镜像 digest 锁死(0.26.1rc1.dev229+g124154a88,ROCm 7.2.3,AITER 0.1.19),再往上逐字节打 10 个 overlay。本文拆这些 overlay 各修了什么,以及为什么缺一个都不行。

先交代 overlay 的意思,它是全文的核心词:一个运行时真正生效的 patch 文件,逐字节替换 pinned image 里的原文件。它和 diff 的分工写在仓库里——diff 只作文档,overlay 才是跑在线上的那一份。这套仓库的原则是生产二进制与上游 commit 同时锁死,任何一处 hash 对不上就停。

为什么硬上 MI300X?三项硬指标:

  • 192 GB HBM3,是 H100 SXM5 80 GB 的 2.4×
  • 5.3 TB/s 内存带宽
  • 单卡清单价约为 H100 的一半(Doubleword 估算)

304B 模型 BF16 权重 ≈ 608 GB,FP8 后 ≈ 304 GB,哪个都塞不进 80 GB 的 H100。MI300X 单卡也装不下 FP8 全量:实际加载态 156.67 GiB,是专家权重再走 MXFP4(4 位)量化压出来的体积。扣掉权重,HBM 只剩 ~36 GB,要装 KV cache、graph capture 和 kernel workspace——这就是 304B 上单卡 MI300X 的全部空间账,第十三节会回来逐项对一遍。

所以选 MI300X 不是预算问题,是容量问题:H100 只能 2 卡 TP(tensor parallel,张量并行)或更激进的量化;MI300X 单卡装下了。上下文 256K 已验证(架构上支持 1M)。

整条推理链路与 10 个 overlay 的落点,带圈数字的就是 patch 作用的位置:

10 个 overlay 对号入座(每个文件的 base commit 逐条登记在 patches/README.md):

#overlay(patches/修什么备注
1fused_compress_quant_cache.fnuz-shuffle.pyLightning Indexer FP8 缓存写成 FNUZ + 16×16 preshuffleMI300X(FNUZ)专用
2gpt_oss_triton_kernels_moe.pack128-fused-silu-fast-routing.pyMXFP4 padding mask + fused SiLU + fast routing取自 Doubleword c32932bb9,not yet upstream
3mxfp4.fused-silu.pyfused-SiLU kernel 的 gate/up interleave 布局配合 #2
4triton-kernels-matmul-ogs-opt-flags.dsv4-mi300x.pygfx942 GEMM tile 几何,21 个 shape本仓库 tuning
5aiter_pa_mqa_logits.i64.pypaged-MQA logits kernel 偏移升 64 位base:ROCm/aiter 4db400a
6rocm_aiter_mla_sparse.prefill-bh64.pyBLOCK_H=64 + 确定性 topk确定性 + 性能
7rocm_aiter_mla.dspark-causal.py投机验证的 causal mask已 upstream(vLLM 77469c9
8dspark-speculator.independent-draft-gumbel.pydraft 侧 Gumbel 噪声独立probabilistic 路径需要
9spec-decode-utils.independent-draft-gumbel.pyverify 侧 Gumbel 噪声独立probabilistic 路径需要
10kv_offload_cpu_gpu_worker.load-war.pyCPU→GPU KV 回写加 fencePR #47291 未合并

①②③⑤⑥ 修「结果对不对」;④ 一半修确定性一半修速度;⑦ 纯修速度。下面按类拆开。

一次请求怎么穿过这条链路

把 overlay 放进一次真实请求,顺序是:

  1. 请求先过 tokenizer、reasoning 和 tool parser,进 prefill。
  2. prefill 由 Lightning Indexer 做 sparse attention,tile 尺寸是 ④ 的 BLOCK_H=64。KV 写缓存走 ① 的 FNUZ 写入——字节序一错,后续 attention 的 logits 全错位,DSpark 的 acceptance 直接崩。
  3. KV 分两层:20 GB 在 GPU,96 GiB 在 CPU(/dev/shm mmap)。CPU 层回写 GPU 靠 ⑤ 的 fence,保证不覆盖 in-flight compute。
  4. 每个 token 经 MoE 路由挑专家,② 的 padding 修复保证长 prompt 下路由权重不被「不存在的专家索引」扰动。
  5. 选中的 expert 交给 AITER GEMM,⑦ 的 tile 几何决定这批 shape 能吃多少带宽。
  6. DSpark-7 对 draft token 做投机验证:⑥ 保证大 KV 池下偏移不溢出,③ 保证 future token 看不到不该看的 earlier position;验证通过才输出,同时更新 KV。

二、FNUZ vs OCP FP8:错一个字节,scale 域差一倍

模型能塞进单卡,前提是低比特;而 FP8 在 AMD 生态里有互不兼容的两个变体。MI300X(CDNA3)用 AMD 自家的 FNUZ E4M3torch.float8_e4m3fnuz),NVIDIA 和 MI355X(CDNA4)用 OCP 标准的 E4M3FN。两者都叫 E4M3,编码空间的分配却不一样:FNUZ 消掉了负零,腾出的位型挪去表示 NaN,指数偏置 8,最大有限值 224.0;OCP 保留负零、偏置 7,最大有限值 448.0。一段按 OCP 字节序写的缓存被 FNUZ 消费端读走,数值最多差 2×。

Lightning Indexer 缓存是 DeepSeek V4 的关键路径,FP8 写入。stock vLLM writer 按 OCP 写。下面两段是示意写法,只保留字节序差异,完整实现以 patches/ 对应文件为准:

# stock writer: OCP E4M3 bytes, row-major
quantized = weight.to(dtype=torch.float8_e4m3fn)         # OCP E4M3
cache[offsets] = quantized.view(torch.uint8)             # row-major

AITER 在 MI300X 上吃的是 FNUZ E4M3 + 16×16 preshuffled tile 布局。OCP 字节被当 FNUZ 解读,Lightning Indexer 输出直接错位——后面 DSpark 投机解码拿到的 logits 全错,acceptance rate 暴跌。

修复(fused_compress_quant_cache.fnuz-shuffle.py)两件事一起改:

# overlay: FNUZ + 16×16 preshuffle
quantized = weight.to(dtype=torch.float8_e4m3fnuz)       # FNUZ, FP8_MAX=224.0
shuffled = preshuffle_16x16(quantized)                    # tile-order reorder
write_with_shuffled_offsets(cache, shuffled, offs)        # match AITER consumer

FNUZ 是 CDNA2/CDNA3 这代(MI200/MI300/MI325X)的格式,AMD 从 CDNA4(MI355X)起改用 OCP。这条 patch 换到 MI355X 上必须拿掉;MI325X 虽然同为 FNUZ,tile 调优又不通用——README 专门写明「其它 AMD GPU 不适用」,两头都对不上是根因。


三、MXFP4 MoE 的 bitmatrix padding:长 prompt 把 tool name 改名的隐藏 bug

MoE 路由那关也藏着一个 bit 级 bug。DeepSeek V4 Flash 的专家权重是 MXFP4(4 位尾数 + E8M0 共享 scale),vLLM 里这条代码路径叫 gpt_oss_triton_kernels_moe——MXFP4 的 Triton kernel 最初为 gpt-oss 写,DeepSeek V4 Flash 复用同一实现。算 bitmatrix 时,padding lane 的屏蔽条件写错了(示意代码,只保留 mask 一行;完整 kernel 见 patch 文件):

# 原代码(错的):padding lane 对全局 bound 屏蔽
mask = (offs_global < nonzero_indx_size)

padding lane 应当按 logical block size 屏蔽(padding 是 block 对齐用的,不该参与路由),原代码却按全局 tensor size 屏蔽。长 prompt 下 global bound 大于 logical block,部分 padding lane 混进实际计算——这些 lane 携带的是「不存在的专家索引」,悄悄扰动路由权重。

后果很隐蔽:prompt 越长,越容易把工具调用路由到相似的别的 expert,输出跟 schema 几乎匹配、但 tool name 错位。README 里那句「near-match tool names and forgotten schemas on long prompts」说的就是它。

修复一行(gpt_oss_triton_kernels_moe.pack128-fused-silu-fast-routing.py):

mask = (offs_local < BLOCK_SIZE) & (offs_global < nonzero_indx_size)

取自 Doubleword 的 commit c32932bb9,README 标注 not yet upstream——目前只有这个仓库在用。同一个 overlay 顺手做了 fused-SiLU 和 fast DeepSeek routing(配套的 mxfp4.fused-silu.py 补 gate/up interleave 布局),routing kernel 从 42.6 降到 11.9 µs/layer,-72%。


四、DSpark-7 投机解码在 ROCm 小头 MLA 上的因果验证

投机解码的因果验证是 ROCm 特有的坑。DeepSeek V4 Flash 自带投机解码草稿模块 DSpark(启动日志 DSpark draft model loaded: 96 params),用 static K=7 + probabilistic drafting + block rejection。NVIDIA 上游 vLLM 假设 MLA(Multi-head Latent Attention)注意力路径满足 causal flatten——ROCm 上小头 MLA 的 AITER 后端不保证这一点。

后果:投机验证阶段算 attention 时,future token 看到了本不该看到的 earlier position,speculative acceptance 虚高,输出串味。修复已进上游(vLLM commit 77469c9),仓库仍保留一份 overlay,与 upstream 在该 commit 处逐字节一致——diff 文档展示的正是那个 commit 自己的改动:

# patches/rocm_aiter_mla.dspark-causal.py
# == vllm/v1/attention/backends/mla/rocm_aiter_mla.py @ 77469c9057bec3212a64877dbbf3b9c48c22d786

为什么 upstream 已合并还要留 overlay?pinned nightly 不一定包含那个 commit。Base image 升级时,这类 overlay 才有机会按需摘掉。

DSpark-7 还有两条精细补丁(dspark-speculator.independent-draft-gumbel.py + spec-decode-utils.independent-draft-gumbel.py):draft_sample_method=probabilistic 时,draft 提议的 Gumbel 噪声必须与 rejection/recovery 噪声独立,否则投机解码在长上下文里会偏向某些 token。greedy 路径用不上这两条。


五、BLOCK_H=64 的稀疏 prefill:sparse attention trace 317 → 142 ms

sparse prefill 的 tile 尺寸,决定 512 head 下还能不能跑。stock gfx942(MI300X 的 GPU 架构代号)实现用默认 BLOCK_H,head=512 时 routed rows 过 768 后急剧劣化。

修复 rocm_aiter_mla_sparse.prefill-bh64.py

BLOCK_H = 64  # head-512 sparse prefill tile
# + deterministic torch.topk for reproducible tool calls

两条各管一头。确定性 topk 让 tool call 在相同 prompt 下走完全一样的 token 路径,回归测试才有抓手;BLOCK_H=64 纯管性能,sparse attention trace 从 317 ms 砍到 142 ms,-55%。


六、int32 溢出:20 GB KV 池把偏移顶过 4 GiB

又一个字节级的坑,在 attention kernel 内部。AITER 的 paged-MQA logits kernel(ChunkK=256)默认用 32 位偏移寻址 KV,而无符号 32 位的寻址上限就是 4 GiB。这套部署的 GPU KV 池有 20 GB,偏移越过 4 GiB 后 int32 回绕,读写直接指到别的页上。

aiter_pa_mqa_logits.i64.py(base:ROCm/aiter 4db400a)把偏移换成 64 位。它不改变任何数值语义,但没有它,KV 池开大之后出错的位置毫无规律——这类溢出 bug 的典型症状就是「偶尔错、难复现」。


七、MXFP4 OGS tile 几何与性能总表

性能侧最大的一刀落在 GEMM。gfx942 的 L2 cache 和 register file 排布与后继型号不同,stock matmul_ogs_details/opt_flags.py 在 routed rows 超过 768 后性能急降,到 1536 掉得更明显。修复 triton-kernels-matmul-ogs-opt-flags.dsv4-mi300x.py:为 21 个常出现的 gfx942 GEMM shape 重写 tile 几何,覆盖到 1536 routed rows。

前几节拆的是每类修正在做什么,这里把 README 记录的全部性能改动汇总成一张表,方便对照每项收益:

优化效果
21 个 A8W8 GEMM shape 调优单/双流 decode +42–62%,8–64 流 +10–35%
Fused SiLU + fast DeepSeek routing + batch-sensitive expert tileNative C1 decode 34.5 → 56.6 tok/s(+64%)
BLOCK_H=64 sparse prefillPrefill 7.9–8.5K tok/s;sparse-attn trace -55%
Static K=7 + 概率 drafting + causal verify119.5 tok/s 单流(正确输出)
2,048 token budget + 1,024 long-prefill cap短请求 TTFT(time to first token,首 token 延迟)在 52K cold prefill 后从 8.2 s → 0.5 s
20 GB GPU KV + 96 GiB CPU tier1.93M token 长度等效容量;7 个 256K 请求同时接

每条优化对应一到多个 overlay,组合起来才把单流从 stock 的 34.5 tok/s 拉到 168.6 tok/s(含 DSpark)。


八、CPU KV 那条 fence:vLLM #47282 的 WAR gap

最微妙的一处,在 KV 回写。vllm/v1/kv_offload/cpu/gpu_worker.py 的 load 路径把 evicted prefix-cache 从 CPU 内存(/dev/shm,~103 GB mmap)写回 GPU KV 时,不等 in-flight compute 完成——compute 流还没对这些页做完读写,load 流的回写就插进来了。overlay 文件名里的 load-war 点破了性质:load 的写插在 compute 未完成的访问前面,典型的 WAR(write-after-read)冒险。

vLLM issue #47282 记录了这个 bug,PR #47291 提出 fence 修复,至今未合并(overlay 的 base 是 #46278 合并后的状态)。仓库把 PR 的 fence 逻辑挂上:

# compute stream 写完才发起 load
compute_stream.synchronize()
load_stream.wait_stream(compute_stream)

这条 fix 只有 --kv-offloading-backend native 时需要;CPU KV tier 开到 96 GiB 的代价,就是这条 fence 必须在。


九、生产数字:126 → 830 tok/s 的并发曲线

README 贴的最终 sweep 数据(每流 400-word 真实 prompt,temperature=1.0, top_p=0.95,C1–C8 各 512 输出 token,C64 256):

StreamsAggregate tok/sMedian per-streamTTFT p50
1126.2168.6 tok/s1.026 s
2145.4152.70.939 s
4316.8108.60.369 s
8542.390.31.027 s
64830.216.42.190 s

两列口径不同:Aggregate 是整个并发跑批的总吞吐,Median per-stream 是单个请求稳态 decode 的中位数。单流那行两个数对不上(126.2 vs 168.6),标题与正文引用的单流能力取后者。

三条规律:

  1. 单流几乎吃满 HBM 带宽——168.6 tok/s decode,这是 memory-bound 工作负载的典型上限。
  2. 8 流 542 tok/s aggregate,每流 90 tok/s——batch 让多个请求共享一次权重读取,单位带宽被摊薄,但总吞吐涨 4.3×。
  3. 64 流 830 tok/s 时单流跌到 16.4 tok/s——DSpark acceptance 在长 prompt + 高并发下退化,这是 DSpark 的固有 trade-off。

Prefill 数据更亮:tuned kernel 让 uncached prefill 跑到 7.9–8.5K tok/s。生产 profile 用 2,048-token budget 拉延迟隔离,fresh prompt 实测 6,988–7,019 tok/s;1,024-token long-prefill cap 让短请求排在 52K cold prefill 后面的 TTFT 从 8.2 s 砍到 0.5 s。


十、830 tok/s 是不是到头了

830 tok/s @ 64 流 vs 168.6 tok/s @ 1 流,为什么是 830,而不是更高?拿带宽粗算一遍。

MI300X HBM3 带宽 5.3 TB/s。decode 阶段每生成一个 token 只需读取激活参数的权重(不是全部 304B)。假设 MoE top-k 激活约 B_active ≈ 50B 参数、FP8 每参数 1 byte:

单流理论 decode 上限 = 5.3 TB/s ÷ (50B × 1 byte) ≈ 106 tok/s

实测单流 168.6 tok/s 高于这个数,靠 DSpark 投机解码:一次 forward 验证多个 draft token,单 token 的带宽成本被摊薄。这笔账只够做量级校验,不精确——它没算 KV cache 读取、attention、routing kernel 的额外 memory traffic,也没有 DSpark acceptance 的精确测量。把 106 当纯带宽上限、按投机放大粗略外推,能落在 160–210 tok/s 区间,和 168.6 同量级。

多流更难用带宽直接解释。8 流 aggregate 542 tok/s、64 流 830 tok/s,单流都明显低于 106 的纯带宽上限——MoE 的 batch 让多个请求共享一次权重读取,aggregate 吞吐不随流数线性翻倍,而是被共享带宽和 kernel 调度共同压住。64 流时单流只剩 16.4 tok/s,说明瓶颈已经不在权重带宽,而在 routing kernel 在大 batch 下的延迟、KV offload fence 的 synchronize 开销,和 CU(compute unit,MI300X 有 304 个)的调度争抢。

所以 830 是 DSpark 加速被 batch 摊薄 + routing 延迟 + fence 开销叠加后的均衡点,不是「带宽到头了」。想再往上,要么关 DSpark 走 native decode(单流降低,aggregate 未必低),要么等 AMD AITER 优化大 batch routing kernel。

也别拿这张表的数字当通用 benchmark:README 自己写了,DSpark acceptance 随 prompt 变化,这些数据只对这份固定 image 成立。


十一、MI300X 单卡 vs H100 双卡:算一笔总账

H100 装不下 304B FP8(80 GB HBM),必须 2 卡 tensor parallel。下面是一张估算对比(价格基于 2026 年公开 list price 和 Doubleword 的成本估算,所有价格均为估算):

维度MI300X 单卡H100 SXM5 双卡(TP=2)
HBM 容量192 GB160 GB(2× 80 GB)
HBM 带宽5.3 TB/s6.7 TB/s(2× 3.35 TB/s)
FP8 峰值算力2.61 PFLOPS~3.95 PFLOPS(2× 1.975)
304B FP8 部署✅ 单卡装下✅ 需 TP 切分
单流 decode168.6 tok/s(实测)~120 tok/s(估算,TP all-reduce 开销 ~30%)
GPU 采购价(估算)~$15–20K~$60–80K($30–40K/张)
部署复杂度overlay 栈 10 patches标准 vLLM TP,无 patch
长期维护需跟 vLLM upstream 合并进度主线支持

三个要点:

  1. 显存账是决定性的:MI300X 单卡 192 GB 装下 304B FP8 + 20 GB KV + workspace;H100 单卡 80 GB 连模型都放不下,必须 TP=2。TP all-reduce 每步引入 ~0.3 ms 延迟(估算),单流 decode 降到 ~120 tok/s。
  2. 采购价差 3–5×:单卡 MI300X 清单价约为 H100 的一半(~$15–20K vs ~$30–40K/张),双卡 H100 总价 $60–80K。对 304B MoE 场景,MI300X 单卡性价比碾压。
  3. 维护成本是 MI300X 的短板:10 个 overlay patch 需要跟 vLLM upstream 同步。PR #47291(KV fence)一旦合并,overlay 可摘;FNUZ patch 在迁到 MI355X(OCP FP8)后也可摘。但在 MI300X(gfx942)上,这些 patch 是生产必需。

长期看三个变量:vLLM 0.27+ 是否合并 PR #47291(影响 overlay 数量)、MI355X 量产时间(FNUZ patch 可摘、OCP 原生支持)、ROCm 7.3/7.5 的 AITER 兼容性(tuning table 是否需要重做)。


十二、首次部署要踩的 11 个坑

下面的步骤来自 compose.yaml 的实际部署命令,每一步都标出容易卡住的地方。

1. 拉 vLLM ROCm nightly image

image digest 必须完全一致(vllm/vllm-openai-rocm@sha256:e68d18b2...),不可改成 latest tag。不同 commit 的 nightly 内部函数行号不一样,overlay patch 的行号会对不上,vLLM 启动直接 crash。pull 下来用 docker images --digests | grep vllm 确认 sha256。

2. 拉 model snapshot(revision pin 死)

REVISION='7872f01b1d1fe23eabc4c98b48bffcef5a386062',不要拉 main。main branch 随时可能更新权重和 config,FP8 scale table 和 AITER GEMM tuning table 是针对特定 revision 调的,换了就 mismatch。

3. sha256sum -c SHA256SUMS 校验全部 overlay 和 diff 文件

任何一个 hash 对不上就停。常见原因:git pull 时 line ending 被 CRLF 污染(Windows clone)、或者编辑器自动加了 BOM。用 git config core.autocrlf input 然后 re-clone。

4. mkdir -p aiter-cache crash-dumps

compose.yaml 把这两个目录 bind mount 进容器,容器内跑 vLLM 的用户要有写权限。目录不存在时 docker compose up 会自动创建——但 owner 是 root,镜像里以非 root(uid 1000)跑的进程写不进去,启动报 PermissionError。提前 mkdir -pchown 1000:1000

5. chmod +x vllm-entrypoint.sh

这个脚本做一件关键的事:清理 stale /dev/shm mapping。上一次容器非正常退出时,/dev/shm/vllm_offload_*.mmap(103 GB CPU KV pool 的 mmap 文件)会残留。不清理就重启,docker 重新 bind mount 时新旧 mapping 冲突,CPU KV tier 初始化报 mmap failed: Device or resource busy

6. cp Caddyfile.example Caddyfile

改三个值:hostname(你的域名)、email(Let’s Encrypt 注册邮箱)、remote_ip(允许访问的 CIDR 白名单)。不改 Caddy 启动失败——Let’s Encrypt 申请证书需要有效域名,白名单为空则所有请求被 403。

7. docker compose config -q

校验 yaml 语法。注意这只检查 yaml 能不能解析,不检查 image digest 对不对、volume 路径存不存在、环境变量是否完整。过了这步不等于能 up -d

8. docker compose up -d 然后 docker compose logs -f inference

等大约 5 分钟。vLLM ROCm nightly 首次启动要做 AITER GEMM tuning(把 21 个 gfx942 shape 的 tuning table 写到 aiter-cache/),这一步只跑一次,耗时和主机 CPU 强相关(1–3 分钟)。之后才是 model loading(156.67 GiB,约 90 秒)。

9. 健康信号 7 条全部出现

docker compose logs 里按顺序等:

  • Model loading took 156.67 GiB
  • DSpark draft model loaded: 96 params
  • GPU KV cache size: 1,927,444 tokens
  • Maximum concurrency for 262,144 tokens per request: 7.35x
  • Created mmap file /dev/shm/vllm_offload_...mmap (103.08 GB)
  • Capturing CUDA graphs (FULL)
  • Application startup complete

少任何一条都是配置问题。最常见缺失的是第 5 条(mmap 创建失败 → CPU KV tier 没起来)和第 6 条(graph capture 报 HSA_STATUS_ERROR_OUT_OF_RESOURCES → HBM 不够,KV pool 开太大)。

10. 烟测两条 curl 后,先跑一个 uncached prefill

curl -fsS "https://your-host/v1/models"
curl -sS "https://your-host/v1/completions" -H 'Content-Type: application/json' \
  -d '{"model":"deepseek-ai/DeepSeek-V4-Flash-0731","prompt":"Calculate 17 * 23. Answer with the number only.","temperature":0,"max_tokens":32}'

第一条验证 API 活着,第二条验证 inference 能出结果。但第二条的 prompt 很短,走的是 cached prefill 路径。在 admit 生产流量之前,手动发一条长 prompt(>4000 token)触发 uncached prefill,把 kernel 暖开——8.9K token 的首次 TTFT 5.3 s,warm 后同长度 1.7 s。不跑这一步,第一个真实用户会替你撞上那 5 秒。

11. 上线前把输出质量基线跑一遍

overlay 栈改了路由、KV 缓存和投机解码,吞吐对了不等于输出对了。仓库自带一套验收 fixture:BFCL 工具调用基准 74–76/90 exact match、tool-calling fixtures、380K token 长文 needle 检索。升级 image 或 overlay 后都该重跑一遍——尤其 ①②③⑤ 这类动数值路径的 patch,跑分看不出错,fixture 才看得出来。


十三、HBM 的剩余空间账

最后对一遍第一节欠下的空间账。暖态高水位里,占 HBM 的是三块:Model 156.67 GiB + 20 GB GPU KV pool + AITER kernel 与 CUDA graph,合计 204.5 GiB。96 GiB CPU KV tier 住在系统内存(/dev/shm mmap),不占 HBM——它是给「容量」兜底的,不是给显存加码的。MI300X 物理 205.8 GB,HBM 只剩 ~1.3 GiB

README 写得很直白:

A 30 GB KV pool loads but fails during graph capture with HSA_STATUS_ERROR_OUT_OF_RESOURCES. Do not raise --kv-cache-memory-bytes; monitor HBM usage for growth.

结论是 KV cache 池不能开大:你以为 30 GB 留给 KV,启动时报 HSA_STATUS_ERROR_OUT_OF_RESOURCESrocm-smi --showmeminfo vram 是生产必备监控,任何多几百 MB 都要警觉。

CPU KV 96 GiB 不是备份——它真正承担了 1.93M token 的长度等效容量。7 个 256K 请求能同时接,靠的就是 CPU tier 兜底。


十四、工程含义与采用建议

ryanzhou 这个仓库值得借鉴的,是三件事:

  1. pinned image + byte-for-byte overlay + SHA-256 校验——把「生产用的二进制」和「上游的某个 commit」同时锁定。overlay 是运行时真正生效的文件,diff 只作文档。GitHub Actions 自动化部署可以照这个模式做。
  2. 每一处 patch 都对应一个上游 issue / commit / PR——overlay 不是随手改的,都能回溯到来源。patches/README.md 把每个 overlay 的 base SHA 列得一清二楚,连「已 upstream 但 nightly 还没带上」这种情况都单独交代。
  3. 生产栈的细节是照着真实运维写的——vllm-entrypoint.sh 清 stale /dev/shm mapping、SHA256SUMS 校验每个 runtime artifact、Caddy IP allowlist + flush_interval -1 保流式响应。这些不是 demo 需要的,是线上才需要的。

照不照搬,看你的处境:

  • 该上的:手头有 MI300X 存量、要跑 304B 级 MoE 单卡推理、且能接受跟 vLLM upstream 同步 overlay 的团队。HBM 容量是唯一硬门槛,MI300X 单卡装得下,H100 得双卡或更激进的量化。
  • 不必急的:只有 H100 且能接受 TP=2、或并发要求不高、或想等 vLLM 主线合并 PR #47291 后再入场的团队。这套 overlay 栈不是通用配方,是特定 image + 特定 revision 的定制方案,抄作业必须整套搬。
  • 从哪开始:照附录的最短路径跑通一遍,先确认 7 条健康信号齐全、烟测通过,再谈调 KV pool 和并发曲线。

单卡 MI300X 跑 304B MoE,决定因素就是 HBM 容量:NV H100 要双卡或更激进的量化,AMD MI300X 单卡装下。对 304B 这类 MoE checkpoint,route 的取舍在这里,不在性价比。


附录:跑起来的最短路径

# 1. 主机:一张 MI300X(gfx942,304 CUs,~192 GiB HBM),~235 GiB RAM,~500 GB 磁盘
# 2. 拉固定 image 和模型
VLLM_IMAGE='vllm/vllm-openai-rocm@sha256:e68d18b2ba50298661bfc49baf01158fbf036645c2362cccf3e8a7a79fe6c69a'
MODEL='deepseek-ai/DeepSeek-V4-Flash-0731'
REVISION='7872f01b1d1fe23eabc4c98b48bffcef5a386062'

docker pull "$VLLM_IMAGE"
docker run --rm --entrypoint hf -v /root/.cache/huggingface:/root/.cache/huggingface \
  "$VLLM_IMAGE" download "$MODEL" --revision "$REVISION"

# 3. 准备文件
cp Caddyfile.example Caddyfile  # 改 hostname + email + remote_ip CIDR
mkdir -p aiter-cache crash-dumps
chmod +x vllm-entrypoint.sh
sha256sum -c SHA256SUMS         # 首次启动前必校 overlay

# 4. 起栈
docker compose config -q
docker compose up -d
docker compose logs -f inference

# 健康信号(全部出现才算 healthy,~5 分钟):
# Model loading took 156.67 GiB
# DSpark draft model loaded: 96 params
# GPU KV cache size: 1,927,444 tokens
# Maximum concurrency for 262,144 tokens per request: 7.35x
# Created mmap file /dev/shm/vllm_offload_...mmap (103.08 GB)
# Capturing CUDA graphs (FULL)
# Application startup complete

# 5. 烟测
curl -fsS "https://your-host/v1/models"
curl -sS "https://your-host/v1/completions" -H 'Content-Type: application/json' \
  -d '{"model":"deepseek-ai/DeepSeek-V4-Flash-0731","prompt":"Calculate 17 * 23. Answer with the number only.","temperature":0,"max_tokens":32}'

参考

参与讨论

使用 GitHub 登录。欢迎补充事实、异议与实践。