Prhub

#55107 [Model][ROCm] Enable DeepSeek V4 Vision

原始 PR 作者 AndreasKaratzas 合并时间 2026-09-12 00:17 文件变更 17 提交数 9 评论 23 代码增减 +1055 / -347

执行摘要

ROCm 支持 DeepSeek V4 Vision,抽公共包装层并适配视觉路由

PR body 明确说明动机:现有 DeepSeek V4 视觉 checkpoint 由 PR #54566 引入时只带一个 NVIDIA-only wrapper 和非 CUDA 守卫,导致 ROCm 用户无法使用该模型。为此作者把"平台中立的 wrapper"搬到 common/,保留 NVIDIA 兼容 shim 与 XPU 不支持桩,并打开 ROCm 的 registry、dummy-init 与 tensor-schema 路径。此外,正文还列出三条技术目标:从当前生效的文本后端构建 checkpoint 映射(NVIDIA 保留自 finalize,ROCm 延迟到通用逐层量化之后再幂等 finalize)、把图像哨兵经 bias_vl 接入 hash 与普通 ROCm MoE 层、在不改变纯文本行宽的前提下把稀疏 prefill 可见性扩展到完整图像 span。作者在评论中同时给出 4×gfx950(ROCm、TP4/EP、FP8 KV、AITER)的验收数据:OCRBench 825/1000 对 822/1000、GSM8K 1243/1319 对 1239/1319,请求错误 0/0、截断 3/3。

值得精读,尤其是正在做多平台模型适配或排查 DeepSeek V4 视觉/稀疏注意力问题的工程师。建议重点关注三处设计决策:(1) common/vl_model.py 中"外层 mapper 全量解析 + 子模型 mapper 置空 + finalizes_weights_during_load 决定是否排序"的权重加载契约,它同时解决了 NVIDIA 自 finalize 与 ROCm 延迟 finalize 的分歧;(2) combine_topk_swa_indices 的 left_visible/right_visible 窗口代数以及 APC 命中 span 内部时的 clamp,这里是整条图像可见性逻辑最容易被误改的地方;(3) DeepseekV4MoE 中 bias_vl、image_sentinel_lo、e_score_correction_bias 三者的分配条件,以及 MTP 必须透传 input_ids 的隐含依赖。阅读时建议把 common/ops/cache_utils.py 中未修的同类 clamp 缺陷一并记下,作为后续跟踪项。

讨论亮点

讨论集中在 okorzh-amd 提出的五个技术问题,作者逐条回应,最终 tjtanaa 批准。

  1. chunked prefill 与图像 span 的前置条件:okorzh-amd 指出注释里"atomically-prefilled image span"是承重前提,但"is_mm_prefix_lm is True for the vision variant ... yet that forcing exists only in vllm/platforms/cuda.py:320-331. RocmPlatform.check_and_update_config has no equivalent"。作者回应 "I mirrored CUDA's multimodal prefix-LM guard in RocmPlatform.check_and_update_config",对应 vllm/platforms/rocm.py 新增的强制 disable_chunked_mm_input 代码与 tests/test_config.py 的新用例。
  2. APC 命中落在图像 span 内部:okorzh-amd 指出 swa_end = pos + right + 1 前向越界、swa_start 未 clamp 到 gather_start,并提醒 "the same clamp looks absent from the shared common/ops/cache_utils.py kernel ... this is pre-existing and affects CUDA too, and should be split into its own PR"。作者在 ROCm kernel 补上 gather_start/seq_len 双向 clamp 并新增 APC 回归测试,但明确 "I left the shared CUDA/common change out of this PR, cause I think it can be addressed separately"——共享路径的缺陷仍然存在。
  3. 测试文件标记与 CI 可见性:okorzh-amd 指出 pytest.mark.cpu_test 不成立(amd.model 会链式导入 vllm/platforms/rocm.py,其模块级 _GCN_ARCH = _get_gcn_arch() 在非 ROCm 上会退回 torch.cuda.get_device_properties),且 "this file is named by no CI job ... otherwise none of these tests can ever fail in CI"。作者移除 cpu_test、加文件级 ROCm skipif 并写入 Basic Models lane。
  4. KV cache dtype 的可用性缺口:okorzh-amd 指出 sparse_mla.py 宣称支持 "auto",但 attention.py:105 断言 kv_cache_dtype.startswith("fp8"),导致默认参数下 vllm serve 能过校验却在运行期抛裸 AssertionError;作者随后移除测试内的 per-arch override,改为 packed 布局把 auto 解析成 fp8_ds_mla,显式不支持的 dtype 给出可读 ValueError。
  5. bias_vl 与 MTP 的冲突:okorzh-amd 指出 bias_vl 在所有 MoE 层(含 MTP block)分配,而 amd/mtp.py:159 硬编码 input_ids=None,于是视觉 checkpoint 开投机解码会在 MoE forward 深处抛 ValueError,建议配置期拒绝。作者不认同配置期拒绝,理由是 "The target mapper drops mtp.* because the separate DeepSeekV4MTP draft model remaps and loads those weights itself. NVIDIA MTP already forwards input_ids, and AMD DSpark does as well",并改为让 AMD MTP 透传 input_ids 加回归测试。

另有 Isotr0py 对测试改用 current_platform.is_cuda_alike() 的两处简化建议(作者回 "Done :)"),以及 tjtanaa 的批准:"LGTM. Let check the Cuda ci."

实现拆解

  1. 平台中立化包装层:新增 vllm/models/deepseek_v4/common/vl_model.py(+331),把 DeepseekV4ForConditionalGeneration、_make_deepseek_v4_vl_weights_mapper、embed_input_ids、embed_multimodal 等整体迁入;nvidia/vl_model.py 从 333 行缩减为 21 行兼容 shim,仅为下游保留原来的 (expert_dtype, image_enabled) 签名,内部委托给 common 版本并注入 _make_deepseek_v4_weights_mapper(expert_dtype)。vllm/models/deepseek_v4/__init__.py 的平台分发相应调整:ROCm 分支从 vl_stub 改为 common.vl_model,CUDA 分支也指向 common,vl_stub.py 的报错文案由 "only supported on NVIDIA GPUs" 改为 "use CUDA or ROCm"。
  2. 权重映射与 finalize 时机解耦:common 版 mapper 改为接收 text_mapper: WeightsMapper,把文本后端的前缀规则整体重挂到 language_model.,并把 head.weight 重定向到 language_model.lm_head.weight、把 mtp. 置为丢弃;image_enabled 为假时额外丢弃 vision./aligner./image_。wrapper 的 __init__ 会把子模型自身的 hf_to_vllm_mapper 置为 no-op(因为 suffix 规则不幂等,lm_head.weight 会再次命中 head.weight),并用 self._weights_finalized 标记延迟 finalize;load_weights 通过 getattr(self.language_model, "finalizes_weights_during_load", True) 判断是否需要先排序以保证 NVIDIA 后端拿到连续的 language_model. 分组,ROCm 侧则由 DeepseekV4ForCausalLM.finalizes_weights_during_load = False 把 finalize 推到通用逐层量化之后。
  3. ROCm MoE 视觉路由:vllm/models/deepseek_v4/amd/model.py 的 DeepseekV4MoE.__init__ 新增 gate.bias_vl 占位与 image_sentinel_lo = IMAGE_SENTINEL_BASE_ID(仅 vision_n_layers > 0 时为非 0),并把 e_score_correction_bias 的分配条件从 elif 改为独立 if,使 hash MoE 在视觉变体下也能拿到校正偏置;两者随 FusedMoEFactory(..., bias_vl=..., image_sentinel_lo=...) 下传到专家层。forward 增加守卫:bias_vl 已分配但 input_ids is None 时直接报错,避免静默路由错误。同文件新增 DeepseekV4ForCausalLM.compute_logits_local,走 logits_processor(..., skip_gather=True),供 ROCm 侧本地 logits 消费方使用。
  4. 稀疏 prefill 的图像可见性:vllm/models/deepseek_v4/amd/rocm.py 的 _combine_topk_swa_indices_kernel 新增 left_visible_ptr/right_visible_ptr/HAS_IMAGE/SWA_WIDTH/PADDED_SWA_WIDTH,把 swa_len = min(pos+1, WINDOW_SIZE) 改为按 left/right 计算 swa_start/swa_end 并分别 clamp 到 gather_start 与 seq_len;combine_topk_swa_indices 新增 max_image_tokens/left_visible/right_visible 参数,用 swa_width = window_size + max_image_tokens 固定视觉模型行宽(即使该 batch 无图),并对左右可见性做配对校验。weight_already_preshuffled 从只看 quant_method 改为同时检查 quant_method 与 scheme,与上游 AITER 预重排后端对齐;_forward_prefill 按 num_decode_tokens 与 query_start:query_end 切分可见性张量后传入。
  5. 平台配置、MTP 与索引层:vllm/platforms/rocm.py 在 check_and_update_config 中镜像 CUDA 的逻辑,对 is_mm_prefix_lm 且多模态的模型强制 disable_chunked_mm_input = True(带 warning_once);amd/mtp.py 改为把 input_ids 透传给 mtp_block,使 MTP/DSpark 在视觉 checkpoint 下不会触发 bias_vl 守卫。vllm/models/deepseek_v4/{nvidia,amd}/model.py 与 __init__.py 的注册、dummy-init、tensor-schema 分支同步放开 ROCm。
  6. 测试与 CI 配套:新增 tests/models/test_deepseek_v4_vl_rocm.py(+458,覆盖 preshuffle 行主序权重、gate_up 只 shuffle 一次、融合 QKV 量化与 preshuffled GEMM 的 scale 布局、KV cache dtype 解析、VL mapper 映射、hash/普通 MoE 的视觉路由、MTP 透传、compute_logits_local 跳过 gather);tests/kernels/attention/test_rocm_triton_attn_dsv4.py 新增 3 个 combine_topk_swa_indices 用例(图像可见性、APC 命中 span 内部、无图时的行宽保持);tests/test_config.py 新增 test_rocm_mm_prefix_lm_disables_chunked_mm_input(4 组参数化);tests/models/test_registry.py、tests/models/multimodal/processing/test_tensor_schema.py 改用 current_platform.is_cuda_alike();.buildkite/test_areas/models_basic.yaml 把新测试文件挂到镜像到 MI300 的 lane。
文件 模块 状态 重要度
vllm/models/deepseek_v4/common/vl_model.py 多模态包装 added 9.17
vllm/models/deepseek_v4/amd/rocm.py 稀疏注意力 modified 7.51
vllm/models/deepseek_v4/amd/model.py 专家路由 modified 7.24
tests/models/test_deepseek_v4_vl_rocm.py 模型测试 added 7.83
vllm/models/deepseek_v4/nvidia/vl_model.py 兼容垫片 modified 8.74
vllm/platforms/rocm.py 平台配置 modified 5.84
vllm/models/deepseek_v4/__init__.py 注册分发 modified 5.48
tests/kernels/attention/test_rocm_triton_attn_dsv4.py 注意力测试 modified 6.28
tests/models/test_initialization.py 初始化测试 modified 4.4

关键符号

_make_deepseek_v4_vl_weights_mapper DeepseekV4ForConditionalGeneration.__init__ DeepseekV4ForConditionalGeneration.load_weights DeepseekV4ForConditionalGeneration.embed_input_ids combine_topk_swa_indices _combine_topk_swa_indices_kernel weight_already_preshuffled DeepseekV4MoE.__init__ DeepseekV4MoE.forward DeepseekV4ForCausalLM.compute_logits_local RocmPlatform.check_and_update_config

关键源码片段

vllm/models/deepseek_v4/common/vl_model.py data-contract

新增的平台中立视觉包装类与权重映射工厂,是整个 PR 的核心;所有平台的 DeepseekV4ForConditionalGeneration 最终都指向这里,同时承载了 NVIDIA/ROCm 两种 finalize 时机的兼容契约。

# vllm/models/deepseek_v4/common/vl_model.py
def _make_deepseek_v4_vl_weights_mapper(
    text_mapper: WeightsMapper, image_enabled: bool
) -> WeightsMapper:
    """Text-checkpoint mapping rules re-rooted under ``language_model.``"""
    # 文本后端的整棵命名空间整体下沉一层;dst 为 None 表示显式丢弃
    orig_to_new_prefix = {
        src: None if dst is None else f"language_model.{dst}"
        for src, dst in text_mapper.orig_to_new_prefix.items()
    }
    # 未启用图像时直接丢弃视觉塔权重,避免出现永远不被使用的参数
    if not image_enabled:
        orig_to_new_prefix.update({"vision.": None, "aligner.": None, "image_": None})
    return WeightsMapper(
        orig_to_new_renaming=text_mapper.orig_to_new_renaming,
        orig_to_new_prefix=orig_to_new_prefix,
        orig_to_new_regex=text_mapper.orig_to_new_regex,
        orig_to_new_stacked=text_mapper.orig_to_new_stacked,
        orig_to_new_suffix={
            **text_mapper.orig_to_new_suffix,
            # lm_head 挂在 wrapper 的 language_model 下,需要单独重定向
            "head.weight": "language_model.lm_head.weight",
        },
        orig_to_new_substr={
            **text_mapper.orig_to_new_substr,
            # MTP/DSpark draft 权重由 draft 模型自己加载,target 侧必须丢弃
            "mtp.": None,
        },
    )
​
​
class DeepseekV4ForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP, SupportsEagle3):
    # MoE router 需要原始 token id 来识别图像哨兵 token 并应用 bias_vl
    requires_raw_input_tokens = True
​
    def __init__(self, *, vllm_config, prefix: str = "") -> None:
        # ... vision / aligner / image_start 等参数构造略 ...
        # 外层 mapper 已经把 HF 名字完整解析到 wrapper 命名空间;
        # AutoWeightsLoader 剥掉 "language_model." 前缀后会把权重委托给子模型,
        # 因此子模型自身的 mapper 必须是 no-op:它的 suffix 规则并不幂等
        # (例如 "lm_head.weight".endswith("head.weight") 会二次命中)。
        text_mapper = self.language_model.hf_to_vllm_mapper
        self.language_model.hf_to_vllm_mapper = WeightsMapper()
        self.make_empty_intermediate_tensors = (
            self.language_model.make_empty_intermediate_tensors
        )
​
        self.hf_to_vllm_mapper = _make_deepseek_v4_vl_weights_mapper(
            text_mapper, image_enabled
        )
        # ROCm 侧延迟且幂等 finalize 的哨兵标志
        self._weights_finalized = False
​
    def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
        # NVIDIA 文本后端在 load_weights 内部自 finalize,必须一次拿到
        # 连续的 "language_model." 分组,因此这里要先按名字排序;
        # ROCm 后端把 finalizes_weights_during_load 置为 False,
        # 由外层在通用逐层量化之后再做一次延迟 finalize。
        child_finalizes = getattr(
            self.language_model, "finalizes_weights_during_load", True
        )
        mapped = self.hf_to_vllm_mapper.apply(weights)
        if child_finalizes:
            mapped = iter(sorted(mapped, key=lambda x: x[0]))
        loader = AutoWeightsLoader(self)
        # 之后由 AutoWeightsLoader 按连续分组把 "language_model." 前缀的
        # 权重委托给子模型的 load_weights
vllm/models/deepseek_v4/amd/model.py core-logic

ROCm 文本模型侧接入视觉路由与本地 logits 的落点:bias_vl/image_sentinel_lo 的分配与下传、forward 守卫、finalizes_weights_during_load 与 compute_logits_local 都在这里。

# vllm/models/deepseek_v4/amd/model.py
class DeepseekV4MoE(nn.Module):
    def __init__(self, vllm_config, prefix: str = "", fuse_heterogeneous_shared_expert: bool = False):
        # ... 配置读取略 ...
        self.gate.e_score_correction_bias = None
        self.gate.tid2eid = None
        self.gate.bias_vl = None
        # 视觉模型才用 IMAGE_SENTINEL_BASE_ID 作为哨兵下界,纯文本走 0
        self.image_sentinel_lo = (
            IMAGE_SENTINEL_BASE_ID if getattr(config, "vision_n_layers", 0) > 0 else 0
        )
        is_hash_moe = extract_layer_index(prefix) < config.num_hash_layers
        self.hash_indices_dtype = torch.int32
        if is_hash_moe:
            # 用 randint 而非 empty,避免 dummy 模式下垃圾值触发非法访存
            self.gate.tid2eid = nn.Parameter(
                torch.randint(
                    0,
                    config.n_routed_experts,
                    (config.vocab_size, config.num_experts_per_tok),
                    dtype=self.hash_indices_dtype,
                ),
                requires_grad=False,
            )
        # 由 elif 改为独立 if:视觉变体下 hash MoE 同样需要 correction bias
        if getattr(config, "topk_method", None) == "noaux_tc" and (
            not is_hash_moe or getattr(config, "vision_n_layers", 0) > 0
        ):
            self.gate.e_score_correction_bias = nn.Parameter(
                torch.empty(config.n_routed_experts, dtype=torch.float32),
                requires_grad=False,
            )
        # 图像 token 的额外路由偏置,仅在视觉 checkpoint 下分配
        if getattr(config, "vision_n_layers", 0) > 0:
            self.gate.bias_vl = nn.Parameter(
                torch.empty(config.n_routed_experts, dtype=torch.float32),
                requires_grad=False,
            )
​
        # ... shared_experts / TP 切分略 ...
​
        self.experts = FusedMoEFactory(
            # ... 其余参数略 ...
            e_score_correction_bias=self.gate.e_score_correction_bias,
            hash_indices_table=self.gate.tid2eid,
            bias_vl=self.gate.bias_vl,
            image_sentinel_lo=self.image_sentinel_lo,
            swiglu_limit=self.swiglu_limit,
            router_logits_dtype=torch.float32,
            # ... routed_experts_cls 等略 ...
        )
​
    def forward(self, hidden_states: torch.Tensor, input_ids: torch.Tensor | None = None):
        if self.gate.tid2eid is not None and input_ids is None:
            raise ValueError("DeepSeek V4 hash MoE routing requires input_ids.")
        # 分配了 bias_vl 就必须拿到原始 token id,否则无法识别图像哨兵;
        # 这条守卫会把上游(如 MTP)未透传 input_ids 的错误暴露出来
        if self.gate.bias_vl is not None and input_ids is None:
            raise ValueError("DeepSeek V4 vision MoE routing requires input_ids.")
​
        org_shape = hidden_states.shape
        final_hidden_states = self.experts(
            hidden_states=hidden_states,
            router_logits=hidden_states,
            input_ids=input_ids,
        )
        return final_hidden_states.view(org_shape)
​
​
class DeepseekV4ForCausalLM(nn.Module, SupportsPP, SupportsEagle3):
    # ROCm 后端不在 load_weights 内部 finalize,交由外层延迟且幂等地执行
    finalizes_weights_during_load = False
​
    def compute_logits_local(self, hidden_states: torch.Tensor) -> torch.Tensor:
        # skip_gather=True 只在本 rank 上算 local 词表 logits,供 ROCm 侧消费
        return self.logits_processor(self.lm_head, hidden_states, skip_gather=True)
tests/models/test_deepseek_v4_vl_rocm.py test-coverage

新增的 ROCm 视觉回归套件(+458),覆盖 FP8 预重排权重布局、KV cache dtype 解析、VL mapper 映射、MoE 视觉路由与 MTP 透传,是这次跨模块改动的主要安全网。

# tests/models/test_deepseek_v4_vl_rocm.py
pytestmark = pytest.mark.skipif(
    not current_platform.is_rocm(), reason="ROCm-specific tests"
)
​
​
def test_rocm_moe_wires_vision_routing_on_hash_and_regular_layers(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    from vllm.models.deepseek_v4.amd import model as rocm_model
    from vllm.models.deepseek_v4.common.mm_preprocess import IMAGE_SENTINEL_BASE_ID
​
    captured: list[dict] = []
​
    class FakeGate(nn.Module):
        def __init__(self, **kwargs) -> None:
            super().__init__()
​
    def fake_factory(**kwargs):
        # 拦截 FusedMoEFactory 参数,验证视觉路由确实被下传
        captured.append(kwargs)
        return nn.Identity()
​
    monkeypatch.setattr(rocm_model, "GateLinear", FakeGate)
    monkeypatch.setattr(rocm_model, "FusedMoEFactory", fake_factory)
    monkeypatch.setattr(rocm_model, "get_tensor_model_parallel_world_size", lambda: 1)
    monkeypatch.setattr(rocm_model, "get_tensor_model_parallel_rank", lambda: 0)
​
    config = SimpleNamespace(
        hidden_size=16, n_routed_experts=4, num_experts_per_tok=2,
        moe_intermediate_size=8, swiglu_limit=None, norm_topk_prob=True,
        scoring_func="sqrtsoftplus", num_hash_layers=1, vocab_size=32,
        topk_method="noaux_tc", vision_n_layers=1, n_shared_experts=None,
    )
    vllm_config = SimpleNamespace(
        model_config=SimpleNamespace(hf_config=config), quant_config=None
    )
​
    # layer 0 是 hash MoE,layer 1 是普通 MoE,两条路径都要接上 bias_vl
    hash_moe = rocm_model.DeepseekV4MoE(vllm_config, prefix="model.layers.0.ffn")
    regular_moe = rocm_model.DeepseekV4MoE(vllm_config, prefix="model.layers.1.ffn")
​
    assert hash_moe.gate.tid2eid is not None
    assert regular_moe.gate.tid2eid is None
    for moe, factory_kwargs in zip((hash_moe, regular_moe), captured, strict=True):
        assert moe.gate.e_score_correction_bias is not None
        assert moe.gate.bias_vl is not None
        assert factory_kwargs["bias_vl"] is moe.gate.bias_vl
        assert factory_kwargs["image_sentinel_lo"] == IMAGE_SENTINEL_BASE_ID

评论区精华

ROCm 缺少 disable_chunked_mm_input 强制逻辑,图像 span 可能被 chunk 边界切开 正确性

okorzh-amd 指出注释中 "atomically-prefilled image span" 是承重前提,但 `is_mm_prefix_lm` 对视觉变体为 True,而这套强制逻辑只存在于 vllm/platforms/cuda.py,RocmPlatform.check_and_update_config 没有等价实现,chunk 边界落在图像 span 内会让索引越出 gathered KV workspace。建议镜像 CUDA 代码块,或上提到 Platform.check_and_update_config。

结论:作者在 `vllm/platforms/rocm.py` 中镜像了 CUDA 的多模态 prefix-LM 守卫,并对 `is_mm_prefix_lm` 且多模态的模型强制 disable_chunked_mm_input,同时补了 tests/test_config.py 的参数化用例。 · 已解决

APC 命中落在图像 span 内部时 SWA 边界未 clamp 正确性

okorzh-amd 指出 `swa_end = pos + right + 1` 会向前越界、`swa_start` 也未 clamp 到 `gather_start`,APC 前缀缓存命中 span 中间时会欠跑该请求的 SWA 区域,且 `--disable-chunked-mm-input` 并不能修复;并提醒共享的 common/ops/cache_utils.py 内核似乎存在同样缺陷,属于 pre-existing 且影响 CUDA,建议拆到独立 PR。

结论:作者在 ROCm 新内核上补齐 `swa_start`/`swa_end` 双向 clamp 并新增 APC-mid-span 回归测试,但明确把共享 CUDA/common 的同类修复留在本 PR 之外单独处理,该缺陷仍然存在。 · partially_resolved

新测试文件的 cpu_test 标记不成立且无 CI job 引用 测试

okorzh-amd 指出三个测试会经 `amd.model` 链式导入 `vllm/platforms/rocm.py`,其模块级 `_GCN_ARCH = _get_gcn_arch()` 在没有非 ROCm 逃逸路径的情况下会落到 `torch.cuda.get_device_properties("cuda")`;同时该文件没有被任何 Buildkite lane 显式列举,`-m cpu_test` 的 lane 也不覆盖 tests/models/*.py。

结论:作者移除 `pytest.mark.cpu_test`、加文件级 ROCm skipif,并把该文件写进镜像到 MI300 的 Basic Models lane。 · 已解决

KV cache dtype "auto" 与稀疏 MLA 断言冲突 设计

okorzh-amd 指出 tests/models/test_initialization.py 中的 per-arch override 只是掩盖可用性缺口:sparse_mla.py 宣称支持 "auto",但 attention.py 断言 `kv_cache_dtype.startswith("fp8")`,默认参数启动 `vllm serve deepseek-ai/DeepSeek-V4-Flash-Vision-Exp` 能通过配置校验却在运行期抛裸 AssertionError。建议把 auto 强制成 fp8_ds_mla,或抛出点名 `--kv-cache-dtype fp8` 的 ValueError 并从支持列表移除 auto。

结论:作者移除测试专用 override,改为 packed 布局把 auto 解析为 `fp8_ds_mla`,显式不支持的 dtype 给出可操作的 ValueError。 · 已解决

bias_vl 与 MTP 硬编码 input_ids=None 冲突 正确性

okorzh-amd 指出 `bias_vl` 会在所有 MoE 层(含 MTP block)分配,而 `amd/mtp.py` 硬编码 `input_ids=None`,视觉 checkpoint 开投机解码会在 MoE forward 深处抛 ValueError,建议在配置期直接拒绝并提示 vision 变体不支持 MTP/DSpark。

结论:作者不同意配置期拒绝:target mapper 丢弃 `mtp.` 是因为独立 draft 模型会自行重映射加载这些权重,且 NVIDIA MTP 与 AMD DSpark 都会透传 input_ids;最终改为让 AMD MTP 透传 input_ids 并加回归测试。 · 已解决

测试中平台判断应改用 is_cuda_alike style

Isotr0py 在 tests/models/multimodal/processing/test_tensor_schema.py 与 tests/models/test_registry.py 上建议把 `current_platform.is_cuda() or current_platform.is_rocm()` 简化成 `current_platform.is_cuda_alike()`。

结论:作者回复 "Done :)",两处均已简化。 · 已解决

风险与影响

  1. 共享 kernel 的同类缺陷未修:common/ops/cache_utils.py 中同族的 combine kernel 仍缺少 gather_start clamp,okorzh-amd 判断这是"pre-existing and affects CUDA too",本 PR 只修 ROCm 侧。这意味着 CUDA 上的 APC 命中图像 span 场景仍存在越界索引风险,需要一个后续 PR 收口,否则容易在后续重构中被误认为已解决。
  2. 行宽从 window_size 变为 window_size + max_image_tokens:combine_topk_swa_indices 现在对视觉模型固定更宽的 swa_width 并做 128 对齐,这会放大 combined_indices 的显存占用与 kernel 的 PADDED_SWA_WIDTH;纯文本模型依赖 max_image_tokens=0 才能保持原状,若调用方(_forward_prefill 之外的路径)未传该参数,行为需要确认。
  3. KV cache dtype 语义变化:packed 布局下 "auto" 被解析为 fp8_ds_mla,显式非 fp8 则报 ValueError。这是面向用户的契约变更,影响所有依赖默认值启动 DeepSeek V4 的用户脚本,文档与示例需要同步。
  4. 强制 disable_chunked_mm_input 的吞吐代价:ROCm 上所有 is_mm_prefix_lm 多模态模型都会被静默改为不分块输入,长上下文多模态请求的显存峰值与调度行为会变化,属于正确性优先的取舍,但缺少性能回归数据。
  5. 投机解码路径的耦合:bias_vl 分配在包括 MTP block 的所有 MoE 层,input_ids 依赖 AMD MTP 透传才成立;若后续有新的 draft 实现(或 mtp.py 再次改成不透传),会在 MoE forward 深处抛出 ValueError 而非配置期失败,排障成本高。
  6. PR 历史混杂与大幅返工:9 个 commit 中包含 #55095 的 cudagraph fallback 提交与多次 Merge origin/main,head commit 还移除了自定义的 row-major FP8 workaround 以采纳上游 #56433 的 AITER preshuffle 修复。weight_already_preshuffled 同时检查 quant_method 与 scheme 是为了避免二次 shuffle,但这条路径的正确性依赖 AITER 后端的 preshuffles_weight 标记在所有量化持有者上都正确设置,属于较脆的隐式契约。

对用户:ROCm(gfx950/MI300X)用户首次可以直接以 DeepseekV4ForConditionalGeneration 架构启动 DeepSeek-V4-Flash-Vision-Exp,无需 NVIDIA 卡;同时默认 KV cache dtype 的解析行为变化,脚本中显式传 --kv-cache-dtype bfloat16 之类的组合会得到新的报错信息。对系统:vllm/models/deepseek_v4/ 的目录结构从"平台各写一份"转为"common wrapper + 平台 thin shim",后续 XPU/CUDA 的视觉能力扩展只需按同一模式补 shim;MoE 路由契约新增 bias_vl/image_sentinel_lo 两个参数,任何自定义 MoE 工厂或 FusedMoE 调用方都需要感知。对团队:CI 层面把新测试文件显式写进 .buildkite/test_areas/models_basic.yaml 并镜像到 MI300,说明 vLLM 的 AMD CI 仍然依赖逐文件列举而非 glob,新增测试文件的可见性需要人工维护,这是后续容易遗漏的协作成本。

核心路径变更 共享 Kernel 遗留缺陷未修 KV cache dtype 契约变更 MTP 与视觉路由的隐式耦合 stacked PR 合并历史混杂 CI 覆盖依赖手工列举 lane

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论