vLLM 0.27.x 移植成功:DeepSeek V4 Flash FP4 海光 DCU 完整报告
2026-08-26 核心成果:vLLM 0.27.x 已成功移植到海光 DCU gfx936。经 DTK 定向适配,V1 Engine 使用真实 DeepSeek V4 Flash FP4 权重完成 TP=4/8 端到端推理;Sparse MLA、MXFP4 MoE 与连续 decode 整条混合精度链路均已跑通。当前已从“功能移植”进入稳定性与性能工程阶段。
补充验证结果(2026-08-24 至 08-26)
0.1 从功能跑通到稳定性能基线
- vLLM V1 Engine 的 TP=4 / TP=8 路线均已验证;4 卡可完整装载模型。
- Sparse MLA 已从 PyTorch reference 推进到 gfx936 Triton manual OCP E4M3 decode。
- 全 256 byte 解码对拍达到
max_abs=0、bad_count=0;真实 cache 单层 cosine 为0.99999750,相对 PyTorch reference 约3.30×。 - M=1 MoE routing Q1 达到
max_abs=0、cosine=1、bit_equal=True,已进入稳定配置。
0.2 性能与 profile
| 配置 | 结果 | 结论 |
|---|---|---|
| TP=4,单请求,固定 32 token | 约 0.99 tok/s | 当前推荐基线 |
| TP=8,单请求,固定 32 token | 约 0.68–0.81 tok/s | 通信开销抵消额外计算资源 |
| TP=4,batch=8 历史摸底 | 约 2.5 aggregate tok/s | 正确性未通过,不列为正式性能 |
clean profile 显示,steady-state 约 30% GPU 时间位于 shared MoE forward,TP 通信约占 14%,另有大量 to / copy / sum / add / zeros / repeat_interleave 小操作。关闭 routing Q1 后,固定生成耗时约从 67.9s 增至 101.3s,因此 Q1 必须保留。shared-expert 辅助 stream 端到端回归,已放弃。
0.3 batch 正确性边界
batch=1–7 未出现 BOS 型结构失败;batch=8 在较长 decode 后可出现非确定性的 BOS、模板退化或 NaN。PyTorch Sparse reader 下问题仍存在,MXFP4 M=1…9 对拍通过,且 NaN 在 sampler 前已经出现。因此当前主方向是异步 stream、workspace 复用或 kernel race,而不是把问题简单归因于 Triton reader 或 M=8 GEMM。
新增 Q1 cast-elision 与 grouped batch MoE 路径均保持默认关闭,必须依次通过真实权重算子数值、端到端正确性和固定吞吐 A/B 后才能进入推荐配置。最近一次吞吐扫描因调度账户异常未能执行,未产生新的性能结论。
0.4 当前稳定开关
DSV4_USE_TRITON_SPARSE=1
DSV4_MOE_ROUTING_Q1=1
DSV4_FP8_WEIGHT_CACHE=0
DSV4_FP8_TRITON_GEMM=0
DSV4_ROCM_SHARED_EXPERTS_STREAM=0
DSV4_GREEDY_LOCAL_ARGMAX=0
DSV4_MOE_Q1_PREPARE=0
VLLM_ROCM_USE_SKINNY_GEMM=0
DSV4_MOE_BACKEND=emulation
基线报告日期:2026-08-21;最新更新:2026-08-26
最终状态:端到端推理已跑通,连续 decode 输出连贯文本
基线验证阶段:J17,COMPLETED / ExitCode 0:0
目标平台:某国产算力平台,8 × 海光 DCU gfx936,DTK 26.04
推理框架:vLLM0.27.2.dev0+g6e448d0ea.d20260819
模型:DeepSeek-V4-Flash-0731
1. 执行摘要
本轮工作的核心目标不是“让模型完成权重加载”,而是让 DeepSeek V4 Flash 的 FP4 模型在海光 DCU gfx936 上,经由 vLLM 完成真实的端到端推理:
- 8 卡 Tensor Parallel 初始化成功;
- 完整加载约 155 GiB、48 个 shard 的模型权重;
- FP4 MoE expert 权重使用 MXFP4 emulation 路径;
- FP8 主干权重在 gfx936 上使用软件反量化;
- DeepSeek V4 Sparse MLA 使用
fp8_ds_mlaKV cache; - prefill 后能够连续执行 decode;
- 生成的 token 序列和文本语义连贯;
- Slurm 工作负载以真实退出码 0 正常结束。
最终成功输出:
TOKEN_IDS: [223, 30594, 9790, 303, 70037, 12203, 10706, 804, 1175,
10539, 637, 53897, 96240, 2996, 84998, 666, 3118, 637,
58368, 621, 666, phase, 53091, 4374, 1465, 223, 1959,
17585, 42108, 303, 56463, 10162]
TOKEN_DECODE:
[' ', '你好', '呀', ',', '很高兴', '在这里', '遇到', '你', '!',
'让我', '来', '做个', '自我介绍', '吧', '~\n\n', '**', '初', '来',
'乍', '到', '**', ':', 'Deep', 'Se', 'ek', ' ', '这个', '版本',
'的我', ',', '确实是', '有点']
OUTPUT_REPR:
' 你好呀,很高兴在这里遇到你!让我来做个自我介绍吧~\n\n
**初来乍到**:DeepSeek 这个版本的我,确实是有点'
DSV4_VERDICT: COHERENT
最终作业状态:
JobID|State|ExitCode|Elapsed|MaxRSS
J17|COMPLETED|0:0|00:05:15|
J17.batch|COMPLETED|0:0|00:05:15|65899600K
J17.extern|COMPLETED|0:0|00:05:15|0
最终根因不是 FP4 MoE 数值公式、chat template、skinny GEMM 或 tokenizer,而是 gfx936 Sparse MLA 的 PyTorch decode fallback 错误理解了 fp8_ds_mla KV cache 的物理布局。
KV cache 的逻辑张量形状是:
[num_blocks, block_size, 584]
但每个 block 在物理内存中不是 64 个独立的 584-byte token 依次排列,而是:
[64 × 576-byte token data][64 × 8-byte UE8M0 scales]
旧 reader 按“每个 token 连续占 584 byte”读取,因而:
- 把其他 token 的 data 当作 scale;
- 把错误字节按 BF16 解释为 RoPE;
- 产生 NaN 和约
1e37的异常值; - attention logits 和 softmax probabilities 变成 NaN;
- 第一枚 token 由 prefill 产生,可能看似正常;
- 第二枚 token 开始进入损坏的 decode 状态,最终持续选择 token 0,即 BOS。
修复后,reader 与 C++ writer 使用完全对称的地址计算:
data = block_base + token_pos * 576
scale = block_base + block_size * 576 + token_pos * 8
验证显示:
scale_bytes = [118, 119, 119, 119, 118, 119, 118, 0]
NoPE finite = True
RoPE finite = True
attention logits finite = True
attention probs finite = True
由此形成了从物理地址、量化 scale、K cache 数值、attention logits、token 序列到最终自然语言输出的完整证据闭环。
验证目标、范围与判据
2.1 核心目标
核心目标始终是:
在海光 DCU gfx936 上,通过 vLLM 跑通 DeepSeek V4 Flash FP4 推理。
这里的“FP4”具体指模型的 MoE expert 使用 MXFP4 权重。模型并不是所有张量均为 FP4;实际 checkpoint 同时包含:
- MXFP4 expert 权重;
- FP8 E4M3 主干权重;
- UE8M0 scale;
- BF16 激活、RoPE 与部分中间状态。
因此,真实目标是跑通混合精度执行链,而不是单独验证某个 FP4 GEMM。
2.2 本轮明确不接受的“伪完成”
以下状态均不算跑通:
- 仅能导入 vLLM;
- 仅能注册
DeepseekV4ForCausalLM; - 仅能读取模型 config;
- 仅能加载全部权重;
- 仅显示
ENGINE_READY; - 仅生成第一枚 token;
- 输出为空;
- 连续生成 BOS、重复 token 或乱码;
- Python 失败但 Slurm 因脚本最后一条命令成功而显示
COMPLETED; - 只验证 BF16,而没有执行 FP4 expert 路径。
2.3 最终成功标准
本轮采用的端到端成功标准是:
- [x] 8 张 gfx936 均可见;
- [x] TP=8 初始化成功;
- [x] 完整权重加载成功;
- [x] expert dtype 确认为 FP4;
- [x] MXFP4 emulation backend 初始化成功;
- [x] FP8 软件反量化路径工作;
- [x]
fp8_ds_mlacache 初始化成功; - [x] prefill 完成;
- [x] 至少执行一次真实 decode;
- [x] decode attention 的 Q/K/logits/probs 全 finite;
- [x] 生成 32 枚非退化 token;
- [x] 输出文本语义连贯;
- [x] 工作负载退出码为 0;
- [x] Slurm 主作业与 batch step 均为
COMPLETED。
3. 软硬件环境
3.1 计算资源
实测环境:
| 项目 | 值 |
|---|---|
| 加速器 | 海光 DCU |
| 架构 | gfx936:sramecc+:xnack- |
| 卡数 | 8 |
| 每卡显存(torch 报告) | 约 68.7 GB 十进制,即约 63.98 GiB |
| Slurm 分区 | 内部 DCU 分区 |
| CPU | --cpus-per-task=64 |
| 作业内存 | --mem=240gb |
| 最长测试时限 | 02:00:00 |
3.2 软件栈
| 组件 | 版本或路径 |
|---|---|
| DTK | 26.04 |
| GCC module | 12.2.0 |
| Python | 3.12,vLLM venv |
| vLLM | 0.27.2.dev0+g6e448d0ea.d20260819 |
| 模型 | DeepSeek-V4-Flash-0731 |
| vLLM 源码 | <VLLM_SOURCE> |
| venv | <VLLM_ENV> |
| 测试目录 | <TEST_WORKSPACE> |
3.3 模型与量化配置
从运行日志读取到:
quant_config:
{
'activation_scheme': 'dynamic',
'fmt': 'e4m3',
'quant_method': 'fp8',
'scale_fmt': 'ue8m0',
'weight_block_size': [128, 128]
}
vLLM 运行时确认:
DeepSeek V4 expert_dtype resolved to 'fp4'
Using 'EMULATION' Mxfp4 MoE backend.
Using DeepSeek's fp8_ds_mla KV cache format.
Using FP8 indexer cache for Lightning Indexer.
运行环境变量
export VLLM_WORKER_MULTIPROC_METHOD=spawn
export TORCHINDUCTOR_USE_STATIC_CUDA_LAUNCHER=0
export DSV4_SHIM_DEBUG=1
export HF_HUB_OFFLINE=1
export TRANSFORMERS_OFFLINE=1
export DSV4_TP=8
export DSV4_MAXLEN=512
export DSV4_MEMUTIL=0.90
export DSV4_KV_CACHE_DTYPE=fp8
export DSV4_MOE_BACKEND=emulation
export VLLM_ROCM_USE_SKINNY_GEMM=0
调试阶段还使用:
export DSV4_SPARSE_DEBUG=1
实现路径
端到端执行链可简化为:
模型 config / tokenizer
│
▼
FP8 主干权重 ──软件反量化──► BF16 GEMM
│
├──► Q / KV / indexer / compressor
│
▼
FP4 MoE expert ──MXFP4 emulation──► MoE 输出
│
▼
DeepSeek V4 Sparse MLA
│
├── prefill
├── SWA cache write(C++ fused writer)
├── compressed/top-k cache
└── decode fallback(gfx936 PyTorch reference)
│
▼
logits → softmax → attention output
│
▼
LM head → next token
本轮曾先后穿越四类阻塞:
- Triton / DAS Triton API 与 lowering 兼容问题;
- gfx936 skinny GEMM device assertion;
- prompt/chat template 导致的输入格式误判;
- Sparse MLA KV cache reader 的物理布局错误。
其中第 4 类是导致“首 token 正常、后续全 BOS”的最终根因。
诊断方法与测试探针
主探针按照阶段打印:
### STAGE: env
### STAGE: config
### STAGE: vllm import + registry
### STAGE: engine init (the real test)
### STAGE: generate
每一阶段分别回答:
| 阶段 | 要回答的问题 |
|---|---|
| env | torch 是否能看到 8 张 gfx936,显存是否正确 |
| config | 模型架构、层数、expert、量化配置能否读取 |
| registry | vLLM 是否注册 DeepSeek V4 |
| engine init | 权重、TP、MoE、KV cache、warmup 是否全部完成 |
| generate | prefill 和连续 decode 是否产生正确 token |
生成阶段打印:
TOKEN_IDS
FINISH_REASON
TOKEN_DECODE
OUTPUT_REPR
OUTPUT
DSV4_VERDICT
Slurm 脚本显式保存 Python 返回码:
python load_dsv4.py > "$RAW" 2>&1
rc=$?
echo "PY_EXIT=$rc"
exit "$rc"
因此报告中的 Slurm 状态、Python verdict 和 raw 日志可以交叉验证。
实验过程与作业记录
6.1 汇总表
| Job | 状态 | 用时 | 到达层级 | 主要结果 |
|---|---|---|---|---|
| J01 | FAILED 1 | 06:18 | engine init | Triton lowering PassManager::run failed |
| J02 | FAILED 1 | 06:18 | engine init | Triton softmax(dim=...) API 不兼容 |
| J03 | FAILED 1 | 06:41 | engine init | 同类 softmax lowering 问题 |
| J04 | FAILED 1 | 06:16 | engine init | 同类 softmax lowering 问题 |
| J05 | FAILED 1 | 06:45 | engine init | fallback 仍触发 softmax API 问题 |
| J06 | FAILED 1 | 06:43 | generate | ENGINE_READY;skinny GEMM device assertion |
| J07 | FAILED 2 | 06:49 | generate | 禁用 skinny GEMM;无硬崩溃,输出退化 |
| J08 | COMPLETED 0 | 07:11 | generate | [32,0,0,...];探针误报 COHERENT |
| J09 | FAILED 2 | 03:32 | generate | 修正 SwiGLU 后仍 [0,0,...] |
| J10 | CANCELLED | 00:16 | 启动 | 误提交的旧脚本,主动取消 |
| J11 | FAILED 2 | 07:13 | generate | chat template 修复;首 token 你好,后续 BOS |
| J12 | FAILED 1 | 01:15 | engine init | auto KV dtype 被 fp8_ds_mla layout 拒绝 |
| J13 | FAILED 2 | 03:05 | generate | 再次确认首 token 随机、后续 BOS |
| J14 | FAILED 2 | 03:06 | generate | 诊断被 warmup 消耗,尚未捕获真实 decode |
| J15 | FAILED 2 | 04:51 | generate | 捕获真实 decode:cache K/attention 全 NaN |
| J16 | FAILED 2 | 07:58 | generate | 第一次 stride 修复不足,仍 NaN |
| J17 | COMPLETED 0 | 05:15 | 端到端 | 物理布局修复成功,输出连贯 |
说明:
FAILED 2多数表示探针主动将退化输出判为失败,并非硬件崩溃。J08显示COMPLETED 0,但输出显然错误,暴露了早期判定器缺陷。- shutdown 阶段常见的
libcudart is not loaded是 ROCm 环境清理噪声,不是导致生成失败的首个错误。
6.2 J01:首次 Triton lowering 阻塞
RuntimeError: PassManager::run failed
RuntimeError: Engine core initialization failed.
这一阶段说明:
- 模型、torch 和 worker 已经启动;
- 失败发生在编译/Lowering,而不是模型文件缺失;
- 必须逐个替换 gfx936 / DAS Triton 不支持的实现。
6.3 J02、J03、J04、J05:softmax API 兼容
多轮作业稳定复现:
triton.compiler.errors.CompilationError
TypeError("softmax() got an unexpected keyword argument 'dim'")
这是上游代码与当前 DAS Triton API 的差异。此阶段通过调整 fallback、避免不兼容 Triton 路径,逐步将失败位置推进到真实 engine warmup。
重要经验:不能围绕 tt.dot_scaled 或单一 Triton 语法无限叠补丁;对于 gfx936 不可 lower 的路径,应建立可验证的 PyTorch/BF16 reference fallback。
6.4 J06:首次 Engine Ready,首次真实硬件错误
关键进展:
Model loading took 19.99 GiB memory and 205.150379 seconds
GPU KV cache size: 85,259 tokens
ENGINE_READY in 335.4s
首次真实 generate 错误:
csrc/rocm/skinny_gemms.hip:580
wvSplitK_hf_sml_
Device-side assertion `false' failed.
这是真正的设备端错误,随后伴随 VMFault。
处理:
export VLLM_ROCM_USE_SKINNY_GEMM=0
禁用后:
- device assertion 消失;
- VMFault 消失;
- generate 能完整返回;
- 问题从“硬件崩溃”推进为“数值退化”。
6.5 J07、J08:硬崩溃消失,但输出退化
J08:
TOKEN_IDS: [32, 0, 0, ..., 0]
TOKEN_DECODE:
['>', '<|begin▁of▁sentence|>', ...]
OUTPUT_REPR: '>'
早期探针错误打印:
DSV4_VERDICT: COHERENT
原因是当时判定器只检查:
- 文本是否为空;
- 是否出现明显的少数字符重复。
由于 OUTPUT_REPR 只有一个 >,它没有满足“长度足够且字符种类少”的条件,形成假绿灯。
此后判定标准改为同时查看:
- token IDs;
- 单 token decode;
- finish reason;
- 人工检查文本;
- 连续 token 是否为 BOS。
6.6 J09:SwiGLU fallback 修复
发现 gfx936 emulation fallback 原实现:
act = F.silu(gate * 1.702) * up
与模型配置及 vLLM reference 不一致。修正为:
gate = torch.clamp(gate, max=gemm1_limit)
up = torch.clamp(up, min=-gemm1_limit, max=gemm1_limit)
act = F.silu(gate * gemm1_alpha) * up
并采用:
gemm1_alpha = 1.0
gemm1_limit = 10.0
作业结果:
TOKEN_IDS: [0, 0, ..., 0]
结论:
- SwiGLU 修复是模型公式一致性所必需;
- 但它不是连续 BOS 的唯一根因;
- 后续排查重点转移至 prefill/decode 分界。
6.7 J11:chat template 修复,定位到 decode
裸 prompt:
llm.generate(["你好,请介绍一下你自己。"])
不能保证符合 DeepSeek V4 的自定义对话模板。改为 chat API 后:
TOKEN_IDS: [30594, 0, 0, ..., 0]
TOKEN_DECODE:
['你好', '<|begin▁of▁sentence|>', ...]
OUTPUT_REPR: '你好'
这是非常关键的边界证据:
- prefill 后产生的第一枚 token 可以正确为“你好”;
- 第二枚 token 开始全部变成 BOS;
- tokenizer 和 chat template 确实影响第一枚 token;
- 但连续 decode 仍存在独立数值错误。
6.8 J12:排除简单切换 BF16/auto KV cache
尝试:
DSV4_KV_CACHE_DTYPE=auto
engine 初始化明确拒绝:
AssertionError:
DeepseekV4 fp8_ds_mla layout only supports fp8 kv-cache, got auto
这说明当前 DeepSeek V4 ROCm sparse MLA 实现与 fp8_ds_mla layout 绑定。不能通过简单设置 auto 绕开问题,必须正确实现该 layout 的读写。
6.9 J13:重复性确认
ENGINE_READY in 93.2s
TOKEN_IDS: [57297, 0, 0, ..., 0]
TOKEN_DECODE:
[' Modified', BOS, BOS, ...]
首 token 会随运行状态变化:
>你好Modified看来- 空格
0
但共同模式始终是:
[prefill token, BOS, BOS, BOS, ...]
因此不能把问题解释为固定 tokenizer 映射;故障发生在 decode 计算链。
J14:诊断探针未捕获目标 decode
首次诊断输出:
q shape = (8, 8, 512)
main_indptr = [0, 0, ..., 0]
这是 engine warmup 的 dummy invocation,不是真实请求。一次性诊断标志被 warmup 消耗,导致真实 decode 没有打印。
修正触发条件:
diag_once = (
diag
and num_queries <= 2
and not getattr(fn, "DSV4_DIAG_ONCE", False)
)
真实单请求 decode 的 num_queries=1,warmup 的 num_queries=8。
6.11 J15:首次捕获根因数值证据
真实 decode metadata:
q shape = (1, 8, 512)
main_cache shape = (35644, 64, 584)
main_cache stride= (1002240, 584, 1)
main_indices = [128, 129, ..., 138, ...]
main_indptr = [0, 11]
scale = 0.04419417382415922
qfinite = True
这证明:
- Q 是 finite;
- 索引数量为 11,与 prompt 上下文长度一致;
- query 本身不是 NaN 来源。
但 decode 后的 cache:
scale_bytes:
[87, 245, 226, 232, 107, 107, 156, 110]
nope_finite = False
nope_max = nan
rope_finite = True
rope_max = 1.0135363467859984e+37
attention:
logits_finite = False
logits_min = nan
logits_max = nan
probs_finite = False
probs_max = nan
因果链第一次完整呈现:
Q finite
→ cache decode 错误
→ NoPE NaN / RoPE 1e37
→ attention logits NaN
→ softmax NaN
→ hidden state 污染
→ LM head 退化
→ token 0 / BOS
J16:stride 修复的适用范围
第一版修复去掉:
cache.view(torch.uint8).contiguous()
改为保留原始 cache view,试图使用:
raw[block, pos]
结果仍然是:
scale_bytes = [88, 245, 226, 232, ...]
nope_finite = False
rope_max ≈ 1e37
这证明问题不仅是 .contiguous() 丢失 stride(0)。
进一步对照 C++ writer 后发现,逻辑 stride(1)=584 本身也不能用于定位单 token。物理 layout 是 block-packed,而不是 token-interleaved。
6.13 J17:物理布局修复成功
最终诊断:
scale_bytes:
[118, 119, 119, 119, 118, 119, 118, 0]
nope_finite = True
nope_max = 1.875
rope_finite = True
rope_max = 5.75 ~ 5.78125
不同 TP rank 的 attention 均正常,例如:
TP0:
logits_finite = True
logits_min = -2.2683308124542236
logits_max = 6.992861270904541
probs_finite = True
probs_max = 0.7818039655685425
TP2:
logits_finite = True
logits_min = -3.1048312187194824
logits_max = 4.642844200134277
probs_finite = True
probs_max = 0.5944044589996338
TP7:
logits_finite = True
logits_min = -2.159546136856079
logits_max = 6.589494228363037
probs_finite = True
probs_max = 0.9574916362phase
最终 token 不再退化,并形成连贯中文。
KV cache 布局问题的根因分析
7.1 表面上的逻辑张量
后端声明每个 token 占 584 byte:
448 byte FP8 NoPE
128 byte BF16 RoPE
8 byte UE8M0 scales/padding
逻辑 shape:
[num_blocks, block_size, 584]
这容易让 reader 写成:
token = cache[block, pos]
data = token[:576]
scale = token[576:584]
但这个解释是错的。
7.2 C++ writer 的真实地址
writer 采用:
block_base =
k_cache + block_idx * kv_block_stride;
token_fp8_ptr =
block_base + pos_in_block * kTokenDataBytes;
token_bf16_ptr =
token_fp8_ptr + kNopeDim;
token_scale_ptr =
block_base
+ cache_block_size * kTokenDataBytes
+ pos_in_block * kScaleBytesPerToken;
常量:
kNopeDim = 448
kRopeDim = 64 BF16 = 128 byte
kTokenDataBytes = 576
kScaleBytesPerToken = 8
block_size = 64
因此单 block 物理布局:
offset 0
│
├── token 0 data: 576 B
├── token 1 data: 576 B
├── ...
├── token 63 data: 576 B
│
├── token 0 scale: 8 B
├── token 1 scale: 8 B
├── ...
└── token 63 scale: 8 B
总有效数据:
64 × 576 + 64 × 8
= 36,864 + 512
= 37,376 byte
而运行时观察到:
main_cache.stride() = (1002240, 584, 1)
stride(0) 由 allocator/shared cache 布局决定,远大于单 block 有效数据。reader 必须保留真实 block stride;不能先 .contiguous()。
7.3 旧 reader 的两个错误
旧代码:
cache_u8 = cache.view(torch.uint8).contiguous()
raw = cache_u8.reshape(cache.shape[0], block_size, -1)
token = raw[block, pos]
data = token[:, :576]
scale_bytes = token[:, 576:584]
错误一:.contiguous() 破坏物理 stride(0)。
错误二:即使保留 stride,token[:, 576:584] 仍假设 scale 紧跟每个 token 的 data;实际 scale 集中存放在 block 尾部。
7.4 最终 reader
最终实现先构造保留 block stride 的 byte view:
raw = cache_u8.as_strided(
(cache.shape[0], block_size * 584),
(cache_u8.stride(0), 1),
)
读取 data:
data_offsets = (
pos[:, None] * 576
+ torch.arange(576, device=device)[None, :]
)
data = raw[block[:, None], data_offsets]
读取 scale:
scale_offsets = (
block_size * 576
+ pos[:, None] * 8
+ torch.arange(8, device=device)[None, :]
)
scale_bytes = raw[block[:, None], scale_offsets]
UE8M0 解码:
k_scales = torch.exp2(scale_bytes.float() - 127.0)
FP8 NoPE:
k_nope = data[:, :448].contiguous().view(fp8_dtype).float()
k_nope *= k_scales[:, :7].repeat_interleave(64, dim=1)
BF16 RoPE:
k_rope = (
data[:, 448:576]
.contiguous()
.view(torch.bfloat16)
.float()
)
scale 参数的确定依据
最终 scale bytes:
[118, 119, 119, 119, 118, 119, 118, 0]
前 7 个 byte 对应 448 个 NoPE 元素的 7 个 64-element group。
例如:
encoded 118 → scale = 2^(118 - 127) = 2^-9
encoded 119 → scale = 2^(119 - 127) = 2^-8
最后一个 byte 是 padding,值为 0,不参与 7 个实际 group 的反量化。
这与 writer 的 7 个 UE8M0 scale + 1 byte pad 设计完全一致。
假设检验与适用范围
FP4 MoE 实现异常假设
证据不支持。
- FP4 backend 正常初始化;
- 权重加载完成;
- 修复 cache reader 后未修改 FP4 MoE,输出即恢复连贯;
- 因此连续 BOS 的最终根因不在 FP4 expert 主路径。
但 SwiGLU 参数修复仍应保留,因为它修复了模型公式一致性。
tokenizer 与 chat template 根因假设
只部分成立。
- chat template 修复后第一枚 token 从无意义符号变为“你好”;
- 但第二枚 token 之后仍全 BOS;
- 因而 template 解释了输入格式问题,不能解释 decode 数值退化。
KV cache 配置的影响
不成立。
当前 DeepSeek V4 ROCm Sparse MLA layout 强制要求:
fp8_ds_mla
auto 在 engine init 被明确拒绝。
contiguous 操作的影响
不成立。
.contiguous() 确实会丢失 block stride,但逻辑 token stride 同样不能代表物理布局。必须同时修复:
- block stride;
- data/scale 分区地址。
首 token 结果与整体数值正确性
不成立。
第一枚生成 token 来源于 prefill。只有请求下一枚 token 时才进入单 token decode。此次故障恰好位于 prefill → decode 的状态转换。
因此端到端测试至少需要:
max_tokens >= 2
更稳妥的是 16 或 32。
Slurm 作业状态与模型正确性
不成立。
J08 是典型反例:
State = COMPLETED
ExitCode = 0
TOKEN_IDS = [32, 0, 0, ...]
框架没有抛异常,不代表数值正确。兼容性报告必须同时检查:
- scheduler state;
- workload exit code;
- raw traceback;
- token IDs;
- 输出文本;
- 中间数值 finite 状态。
实现修改
9.1 gfx936 兼容 shim
sitecustomize.py 在 worker spawn 后自动应用,用于补足当前 Triton/vLLM API 差异,并避免启动时过早 import Triton。
涉及的兼容点包括:
- Triton language API placeholder/backport;
reduce_or;- JITFunction metadata;
- HCU compiler arch 默认值;
- knobs runtime hooks;
- vLLM FP8 linear 软件反量化。
这些是 engine 能启动和完成权重加载的基础设施。
9.2 FP8 主干软件反量化
gfx936 不支持 vLLM 所期待的原生 FP8 _scaled_mm 路径,因此 FP8 linear 使用 BF16 软件反量化 fallback。
这条路径牺牲性能,但建立了正确性基线。
9.3 MXFP4 MoE emulation
DSV4_MOE_BACKEND=emulation
权重保持 FP4 checkpoint 格式,由 emulation expert 路径按需反量化和计算。该路径用于首先证明模型功能正确。
9.4 SwiGLU 参数一致性
将错误的固定 1.702 公式替换为模型配置一致的:
alpha = 1.0
limit = 10.0
9.5 禁用 gfx936 skinny GEMM
VLLM_ROCM_USE_SKINNY_GEMM=0
避免 wvSplitK_hf_sml_ 的 device assertion。
9.6 Sparse MLA decode fallback
gfx936 上 Triton FP8→BF16 conversion 不能稳定 lower,因此使用 PyTorch reference decode。
最终关键修复位于:
vllm/v1/attention/ops/rocm_aiter_mla_sparse.py
修复内容是 fp8_ds_mla block-packed cache 的物理寻址。
10. 最终验证详情
10.1 Engine 初始化
ENGINE_READY in 228.9s
此前不同节点、缓存状态和 JIT 状态下,初始化时间在约 93 秒至 338 秒之间波动,因此单次 engine ready 时间不能直接作为稳定性能结论。
10.2 KV cache 数值
所有 8 个 TP worker 均得到一致的 scale bytes:
[118, 119, 119, 119, 118, 119, 118, 0]
并且:
NoPE finite = True
NoPE abs max = 1.875
RoPE finite = True
RoPE abs max ≈ 5.75–5.78125
10.3 Attention 数值
所有 TP rank:
logits_finite = True
probs_finite = True
logits 范围和最大 softmax probability 不完全相同,这是各 rank 持有不同 head shard 的正常表现。
10.4 输出质量
32 枚 token 不包含连续 BOS,中文语义连贯,格式包含自然标点和 Markdown:
你好呀,很高兴在这里遇到你!让我来做个自我介绍吧~
**初来乍到**:DeepSeek 这个版本的我,确实是有点
10.5 资源
MaxRSS = 65899600K
这是 batch step 的 CPU RSS。GPU 侧历史日志显示:
Model loading took approximately 19.99 GiB per worker/device
GPU KV cache size approximately 85K tokens
这些数字受节点、JIT、allocator 和 cache 配置影响,仅用于本轮功能验证记录。
日志分析与错误判定
11.1 Usage report JSON 错误
多轮日志出现:
Exception in thread Thread-1 (_report_usage_worker)
json.decoder.JSONDecodeError
它发生于 usage-report 后台线程,不是 engine 或模型计算的首个阻塞。最终成功作业中也可出现类似噪声。
11.2 ROCm shutdown 中的 libcudart assertion
失败作业退出清理阶段常见:
AssertionError:
libcudart is not loaded in the current process,
try setting VLLM_CUDART_SO_PATH
这是 vLLM shutdown 路径尝试导入 CUDA allocator 的 ROCm 兼容问题。它出现在生成结果和探针 verdict 之后,不应被误判为本次推理失败的根因。
11.3 FIRST BLOCKER 的读取规则
应按以下顺序判断:
- 找 raw 日志中最早的 workload traceback;
- 确认它是否发生在主执行路径;
- 区分后台线程、shutdown 和主推理;
- 对照
ENGINE_READY、TOKEN_IDS、verdict; - 最后查看 Slurm state 和 exit code。
12. 复现实验
12.1 Slurm 资源
#SBATCH -p 内部 DCU 分区
#SBATCH --gres=dcu:8
#SBATCH --cpus-per-task=64
#SBATCH --mem=240gb
#SBATCH --time=02:00:00
12.2 环境
module purge
module load compiler/gcc/12.2.0 compiler/dtk/26.04
source <VLLM_ENV>/bin/activate
export LD_LIBRARY_PATH=<DTK_ROOT>/gcvm/lib:\
<DTK_ROOT>/lib:\
<DTK_ROOT>/lib64:\
<DTK_ROOT>/comgr/lib64:\
$LD_LIBRARY_PATH
export PYTHONPATH=<TEST_WORKSPACE>:\
<TRITON_KERNELS>:\
$PYTHONPATH
12.3 vLLM 参数
llm = LLM(
model=MODEL,
tensor_parallel_size=8,
max_model_len=512,
kv_cache_dtype="fp8",
moe_backend="emulation",
enforce_eager=True,
gpu_memory_utilization=0.90,
trust_remote_code=True,
)
对话输入应使用模型 chat template:
llm.chat(
[[
{
"role": "user",
"content": "你好,请介绍一下你自己。",
}
]],
SamplingParams(
max_tokens=32,
temperature=0,
ignore_eos=True,
),
)
12.4 结果检查
必须检查:
ENGINE_READY
TOKEN_IDS
TOKEN_DECODE
OUTPUT_REPR
DSV4_VERDICT
PY_EXIT
sacct State / ExitCode
不应只 grep OUTPUT:,因为 BOS 等 special token 可能被 tokenizer 从最终文本中省略。
代码版本与辅助文件
本地仓库:
| 文件 | 用途 |
|---|---|
probes/load_dsv4.py |
分阶段端到端加载与生成探针 |
probes/load_dsv4.slurm |
基础 Slurm 脚本 |
probes/sitecustomize.py |
spawn worker 自动兼容 shim |
probes/patch_sparse_diag_min.py |
Sparse MLA Q/cache/attention 数值诊断 |
probes/patch_sparse_physical_layout.py |
最终物理 cache layout 修复脚本 |
dsv4-vllm-handoff.md |
更早阶段的 torch/vLLM/mmac 移植历史 |
dsv4-dcu-record.html |
项目过程记录 |
远端关键源码:
<VLLM_SOURCE>/
vllm/v1/attention/ops/rocm_aiter_mla_sparse.py
远端保留了修复前备份和诊断脚本,便于对比。
结论与适用范围
14.1 已经证明
- DeepSeek V4 Flash checkpoint 可在单节点 8 × gfx936 上完整加载;
- vLLM TP=8 可初始化;
- FP4 expert 权重可通过 MXFP4 emulation 执行;
- FP8 主干可通过软件反量化执行;
- Sparse MLA prefill 和 decode 可执行;
fp8_ds_mlacache 在修复物理 reader 后数值正确;- 单请求 greedy 生成 32 token 可输出连贯中文;
- Slurm 工作负载正常完成。
14.2 尚未证明
- 长上下文至 512 token 全覆盖的稳定性;
- 多并发请求;
- prefix caching 的复杂复用场景;
- chunked prefill 的全部边界;
- C4/C128 compressed extra cache 的所有长度组合;
- 多轮对话;
- EOS 正常停止行为;
- temperature/top-p 非 greedy 采样;
- 长时间服务稳定性;
- 显存碎片和反复请求回收;
- 数学精度相对官方 CUDA/BF16 reference 的系统误差;
- production 吞吐和 P50/P95 latency;
- 原生 gfx936 FP4 kernel 替代 emulation 后的端到端表现。
因此当前应表述为:
FP4 功能链路和连续 decode 已跑通,具备正确性基线;生产级稳定性、精度和性能验收仍需后续测试。
后续工作
15.1 第一优先级:固化回归测试
增加至少以下回归:
- 单请求、32 token、中文 chat;
- 单请求、128 token;
- 两轮对话;
- 8 个不同 prompt;
- 两个并发请求;
- prompt 长度跨越 block 边界;
- SWA window 边界;
- C4/C128 compressor 边界;
- prefix caching 开关 A/B;
- 运行 50 次检查偶现 NaN。
每次必须检查:
all token IDs
all special tokens
finite checks
Slurm exit code
VMFault/device assertion
15.2 第二优先级:将诊断变为测试断言
把一次性打印升级为可选断言:
assert torch.isfinite(k_nope).all()
assert torch.isfinite(k_rope).all()
assert torch.isfinite(logits).all()
assert torch.isfinite(probs).all()
只在 DSV4_SPARSE_VALIDATE=1 时启用,避免生产性能损失。
15.3 第三优先级:精度对拍
建议分层:
- cache writer/reader round-trip;
- 单 token attention output;
- 单层 hidden state cosine;
- 每层 hidden state;
- next-token logits top-k;
- greedy token sequence;
- 小数据集 perplexity。
参考基线可以是:
- CUDA 官方/上游实现;
- BF16 cache/reference attention;
- CPU 小尺寸 reference。
15.4 第四优先级:性能
当前 PyTorch decode fallback 会产生:
- 高级索引;
- 临时 tensor;
- FP8→FP32;
- Python query 循环;
- 多次 kernel launch;
- host sync 型
.item()。
它适合作为正确性 reference,不适合作为最终高性能实现。
下一步应将正确的物理地址公式移植到:
- gfx936 可 lower 的 Triton kernel;或
- HIP/C++ 专用 decode kernel。
优化前必须保留本轮 reference 实现作为 oracle。
15.5 第五优先级:清理调试代码
生产化前:
- 将
DSV4_SPARSE_DEBUG保留为默认关闭; - 删除非必要同步打印;
- 保留物理布局注释;
- 增加单元测试覆盖 block-packed layout;
- 将补丁整理为可 review 的 git commit;
- 将
VLLM_ROCM_USE_SKINNY_GEMM=0变成 gfx936 平台默认或明确 capability gate。
工程经验总结
16.1 shape 不等于物理 layout
量化 cache 常用“方便表达容量”的逻辑 shape,但 writer 可能为向量化、TMA、coalescing 或 scale 分区采用完全不同的物理布局。
reader 必须以 writer 的地址公式为事实来源。
16.2 调试 prefill/decode 必须分别埋点
warmup、prefill 和 decode 可能使用不同:
- shape;
- kernel;
- cache slot;
- backend;
- tensor stride。
“第一次函数调用”通常只是 warmup,不代表真实请求。
首个 token 的诊断意义
它把故障范围缩小到:
prefill 写 cache
→ cache 持久化
→ decode metadata
→ decode cache reader
→ decode attention
而不是整个模型任意位置。
16.4 数值诊断要形成层级链
本轮有效诊断顺序:
Q finite?
indices/indptr correct?
raw scale bytes plausible?
decoded K finite?
logits finite?
probs finite?
output token normal?
这比仅看最终乱码高效得多。
16.5 负结果也必须记录
SwiGLU 修复、chat template 修复、去除 .contiguous() 都是有价值的:
- 有的修复了独立 bug;
- 有的缩小了范围;
- 有的证明假设不充分。
完整报告不能只保留最终两行 diff。
17. 最终结论
截至 2026-08-26,vLLM 0.27.x 已成功移植到海光 DCU gfx936;DeepSeek V4 Flash FP4 已通过 V1 Engine 完成 TP=4/8 端到端推理,并进入稳定性与性能优化阶段。
最终成功不是通过回退到纯 BF16 模型实现,而是保持:
- FP4 expert;
- MXFP4 emulation;
- FP8 主干软件反量化;
- FP8
fp8_ds_mlaKV cache; - Sparse MLA;
- TP=8。
决定性修复是使 gfx936 Sparse MLA PyTorch decode fallback 按 C++ writer 的 block-packed 物理布局读取 KV cache。
最终证据同时覆盖:
- scheduler;
- engine;
- 权重与 backend;
- cache bytes;
- dequantized K;
- attention logits/probs;
- token IDs;
- 自然语言输出;
- workload exit code。
因此可以正式确认:
vLLM 0.27.x 在海光 DCU gfx936 上的功能移植已经成功;DeepSeek V4 Flash FP4 的真实权重、V1 Engine、TP=4/8 和连续 decode 均已跑通。当前进入高 batch 稳定性、精度与性能优化阶段。