opensource_community
完整技术报告 · 基线记录 2026-08-21 · 最新成果更新至 2026-08-26 · 公开脱敏版

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 从功能跑通到稳定性能基线

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
推理框架:vLLM 0.27.2.dev0+g6e448d0ea.d20260819
模型:DeepSeek-V4-Flash-0731

1. 执行摘要

本轮工作的核心目标不是“让模型完成权重加载”,而是让 DeepSeek V4 Flash 的 FP4 模型在海光 DCU gfx936 上,经由 vLLM 完成真实的端到端推理:

  1. 8 卡 Tensor Parallel 初始化成功;
  2. 完整加载约 155 GiB、48 个 shard 的模型权重;
  3. FP4 MoE expert 权重使用 MXFP4 emulation 路径;
  4. FP8 主干权重在 gfx936 上使用软件反量化;
  5. DeepSeek V4 Sparse MLA 使用 fp8_ds_mla KV cache;
  6. prefill 后能够连续执行 decode;
  7. 生成的 token 序列和文本语义连贯;
  8. 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”读取,因而:

修复后,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 同时包含:

因此,真实目标是跑通混合精度执行链,而不是单独验证某个 FP4 GEMM。

2.2 本轮明确不接受的“伪完成”

以下状态均不算跑通:

2.3 最终成功标准

本轮采用的端到端成功标准是:


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

本轮曾先后穿越四类阻塞:

  1. Triton / DAS Triton API 与 lowering 兼容问题;
  2. gfx936 skinny GEMM device assertion;
  3. prompt/chat template 导致的输入格式误判;
  4. 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 端到端 物理布局修复成功,输出连贯

说明:

6.2 J01:首次 Triton lowering 阻塞

RuntimeError: PassManager::run failed
RuntimeError: Engine core initialization failed.

这一阶段说明:

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

禁用后:

6.5 J07、J08:硬崩溃消失,但输出退化

J08:

TOKEN_IDS: [32, 0, 0, ..., 0]
TOKEN_DECODE:
['>', '<|begin▁of▁sentence|>', ...]
OUTPUT_REPR: '>'

早期探针错误打印:

DSV4_VERDICT: COHERENT

原因是当时判定器只检查:

由于 OUTPUT_REPR 只有一个 >,它没有满足“长度足够且字符种类少”的条件,形成假绿灯。

此后判定标准改为同时查看:

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]

结论:

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: '你好'

这是非常关键的边界证据:

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 会随运行状态变化:

但共同模式始终是:

[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

这证明:

但 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 实现异常假设

证据不支持。

但 SwiGLU 参数修复仍应保留,因为它修复了模型公式一致性。

tokenizer 与 chat template 根因假设

只部分成立。

KV cache 配置的影响

不成立。

当前 DeepSeek V4 ROCm Sparse MLA layout 强制要求:

fp8_ds_mla

auto 在 engine init 被明确拒绝。

contiguous 操作的影响

不成立。

.contiguous() 确实会丢失 block stride,但逻辑 token stride 同样不能代表物理布局。必须同时修复:

  1. block stride;
  2. 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, ...]

框架没有抛异常,不代表数值正确。兼容性报告必须同时检查:


实现修改

9.1 gfx936 兼容 shim

sitecustomize.py 在 worker spawn 后自动应用,用于补足当前 Triton/vLLM API 差异,并避免启动时过早 import Triton。

涉及的兼容点包括:

这些是 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 的读取规则

应按以下顺序判断:

  1. 找 raw 日志中最早的 workload traceback;
  2. 确认它是否发生在主执行路径;
  3. 区分后台线程、shutdown 和主推理;
  4. 对照 ENGINE_READY、TOKEN_IDS、verdict;
  5. 最后查看 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 已经证明

14.2 尚未证明

因此当前应表述为:

FP4 功能链路和连续 decode 已跑通,具备正确性基线;生产级稳定性、精度和性能验收仍需后续测试。

后续工作

15.1 第一优先级:固化回归测试

增加至少以下回归:

  1. 单请求、32 token、中文 chat;
  2. 单请求、128 token;
  3. 两轮对话;
  4. 8 个不同 prompt;
  5. 两个并发请求;
  6. prompt 长度跨越 block 边界;
  7. SWA window 边界;
  8. C4/C128 compressor 边界;
  9. prefix caching 开关 A/B;
  10. 运行 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 第三优先级:精度对拍

建议分层:

  1. cache writer/reader round-trip;
  2. 单 token attention output;
  3. 单层 hidden state cosine;
  4. 每层 hidden state;
  5. next-token logits top-k;
  6. greedy token sequence;
  7. 小数据集 perplexity。

参考基线可以是:

15.4 第四优先级:性能

当前 PyTorch decode fallback 会产生:

它适合作为正确性 reference,不适合作为最终高性能实现。

下一步应将正确的物理地址公式移植到:

优化前必须保留本轮 reference 实现作为 oracle。

15.5 第五优先级:清理调试代码

生产化前:


工程经验总结

16.1 shape 不等于物理 layout

量化 cache 常用“方便表达容量”的逻辑 shape,但 writer 可能为向量化、TMA、coalescing 或 scale 分区采用完全不同的物理布局。

reader 必须以 writer 的地址公式为事实来源。

16.2 调试 prefill/decode 必须分别埋点

warmup、prefill 和 decode 可能使用不同:

“第一次函数调用”通常只是 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() 都是有价值的:

完整报告不能只保留最终两行 diff。


17. 最终结论

截至 2026-08-26,vLLM 0.27.x 已成功移植到海光 DCU gfx936;DeepSeek V4 Flash FP4 已通过 V1 Engine 完成 TP=4/8 端到端推理,并进入稳定性与性能优化阶段。

最终成功不是通过回退到纯 BF16 模型实现,而是保持:

决定性修复是使 gfx936 Sparse MLA PyTorch decode fallback 按 C++ writer 的 block-packed 物理布局读取 KV cache。

最终证据同时覆盖:

因此可以正式确认:

vLLM 0.27.x 在海光 DCU gfx936 上的功能移植已经成功;DeepSeek V4 Flash FP4 的真实权重、V1 Engine、TP=4/8 和连续 decode 均已跑通。当前进入高 batch 稳定性、精度与性能优化阶段。