Prhub

#41205 Fix Nano Nemotron text-only weight loading

原始 PR 作者 Baekpica 合并时间 2026-05-05 02:43 文件变更 3 提交数 10 评论 12 代码增减 +130 / -5

执行摘要

修复 Nano Nemotron VL 文本模式下多模态权重加载

Fix Nano Nemotron VL weight loading when all multimodal prompt limits are zero, including --language-model-only. In this mode the multimodal tower modules are replaced by missing-stage placeholders, so adapter, vision, and audio checkpoint weights should be skipped instead of inspected or loaded.

该PR设计清晰,通过复用multimodal_config接口统一判断文本仅模式,避免了引入额外配置字段。新增测试覆盖全面,值得精读。建议关注类似多模态模型(如Qwen-VL)的文本仅模式实现,可复用此模式。

讨论亮点
  • 声音编码器缺失时的断言处理:gemini-code-assist指出当load_multimodal_weights为True但sound_encoder为None时,assert self.sound_encoder is not None会导致崩溃,建议改为条件跳过。netanel-haber倾向于保留严格断言以尽早暴露配置问题,建议将断言提前。Baekpica最终保留严格断言但调整了条件顺序:先断言再加载,仅在多模态启用且遇到音频权重时触发。
  • adapter_dict构建位置:netanel-haber询问是否即使在load_multimodal_weights为False时也需要构建adapter_dict。Baekpica确认mlp1独立存在,始终构建字典,并通过后续的continue避免实际加载适配器权重。

实现拆解

  1. 加载入口判断:在vllm/model_executor/models/nano_nemotron_vl.pyload_weights起始处,从self.model_config.multimodal_config获取配置,通过检查imagevideoaudio三种模态的get_limit_per_prompt返回值是否全为零,得到布尔值load_multimodal_weights
  2. 条件跳过多模态权重:在遍历权重时,对mlp1(适配器)、vision_model.radio_model.*(视觉)、sound(音频)权重的处理分支中,若load_multimodal_weightsFalse,则直接continue,不执行任何加载或断言。
  3. 条件调用子模块加载:在收集完权重后,仅当load_multimodal_weightsTrue时,才调用self.vision_model.load_weights(vision_weights)self.sound_encoder.load_weights(sound_weights)。语言模型权重self.language_model.load_weights(llm_weights)始终执行。
  4. 测试覆盖:新增tests/models/multimodal/test_nano_nemotron_vl.py,使用stub对象模拟三种配置场景:文本仅模式(验证多模态权重被跳过)、仅图像模式(验证视觉权重加载且音频权重缺失触发断言)、以及仅图像模式但提供音频权重(验证严格断言)。
文件 模块 状态 重要度
vllm/model_executor/models/nano_nemotron_vl.py 模型加载 modified 6.46
tests/models/multimodal/test_nano_nemotron_vl.py 模型测试 added 7.42
vllm/transformers_utils/configs/hyperclovax.py 配置工具 modified 4.15

关键符号

load_weights

关键源码片段

vllm/model_executor/models/nano_nemotron_vl.py core-logic

核心权重加载逻辑,新增 load_multimodal_weights 标志控制多模态权重加载

def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
    # 从模型配置中获取多模态配置,并判断是否所有模态的 prompt limit 都为零
    mm_config = self.model_config.multimodal_config
    load_multimodal_weights = not all(
        mm_config.get_limit_per_prompt(modality) == 0
        for modality in ("image", "video", "audio")
    )
    # 适配器参数字典始终构建,因为 mlp1 独立于多模态加载
    adapter_dict = dict(self.mlp1.named_parameters())
​
    def is_llm(name: str) -> bool:
        return name.startswith("language_model")
​
    def is_adapter_weights(weight: tuple[str, torch.Tensor]):
        return weight[0].startswith("mlp1")
​
    def is_vision_weights(name: str) -> bool:
        return name.startswith("vision_model.radio_model.")
​
    def is_sound_weights(name: str) -> bool:
        return name.startswith("sound")
​
    llm_weights, vision_weights, sound_weights = [], [], []
​
    for name, w in weights:
        if is_llm(name):
            llm_weights.append((".".join(name.split(".")[1:]), w))
        elif is_adapter_weights((name, w)):
            if not load_multimodal_weights:
                continue # 跳过适配器权重
            trimmed_name = ".".join(name.split(".")[1:])
            param = adapter_dict[trimmed_name]
            with torch.no_grad():
                default_weight_loader(param, w)
        elif is_vision_weights(name):
            if not load_multimodal_weights:
                continue # 跳过视觉权重
            hf_key = name[len("vision_model."):]
            vision_weights.append((hf_key, w))
        elif is_sound_weights(name):
            if not load_multimodal_weights:
                continue # 跳过音频权重
            assert self.sound_encoder is not None
            sound_weights.append((name, w))
​
    self.language_model.load_weights(llm_weights) # 始终加载 LLM 权重
    if load_multimodal_weights:
        self.vision_model.load_weights(vision_weights)
        if self.sound_encoder is not None and len(sound_weights) > 0:
            self.sound_encoder.load_weights(sound_weights)
tests/models/multimodal/test_nano_nemotron_vl.py test-coverage

新增测试覆盖文本仅模式跳过多模态权重等三种场景

def test_nano_nemotron_vl_skips_multimodal_weights_in_text_only_mode():
    # 通过 object.__new__ 绕过 __init__,以便手动设置内部状态
    model = object.__new__(NemotronH_Nano_VL_V2)
    language_model = _LanguageModel() # 记录加载的权重
    object.__setattr__(model, "model_config", _ModelConfig()) # 文本仅配置
    object.__setattr__(model, "language_model", language_model)
    object.__setattr__(model, "mlp1", _AdapterModule())
    object.__setattr__(model, "vision_model", _MissingMultiModalModule()) # 占位符,应跳过
    object.__setattr__(model, "sound_encoder", None)
​
    language_weight = object()
    model.load_weights([
        ("language_model.layers.0.weight", language_weight),
        ("mlp1.0.weight", object()),
        ("vision_model.radio_model.encoder.weight", object()),
        ("sound_encoder.encoder.weight", object()),
    ])
​
    # 期望仅语言模型权重被加载,其他被跳过
    assert language_model.loaded_weights == [("layers.0.weight", language_weight)]

评论区精华

Sound encoder 缺失时的 assert 处理 正确性

gemini-code-assist 指出当多模态加载启用但 sound_encoder 为 None 时 assert 会导致崩溃,建议改为条件跳过。netanel-haber 倾向于保留严格断言以尽早暴露配置问题,建议将断言提前。

结论:Baekpica 采纳了保留严格断言但调整条件顺序的方案:先断言 sound_encoder 不为 None,再加载权重。 · 已解决

adapter_dict 构建位置 设计

netanel-haber 询问是否即使在 load_multimodal_weights 为 False 时也需要构建 adapter_dict。

结论:Baekpica 确认 mlp1 独立于多模态加载,始终构建 adapter_dict。 · 已解决

风险与影响

主要风险依赖multimodal_config.get_limit_per_prompt的返回值,若返回非预期值(如负数)可能导致误判。严格断言在多模态启用但音频编码器缺失时直接崩溃,但这也是主动防御,避免静默错误。新增的三个单元测试覆盖了文本仅模式和图像仅模式的正反场景,降低了回归风险。变更仅影响Nano Nemotron VL模型,且代码修改量小(+15/-3),性能影响可忽略。

影响范围限定于NemotronH_Nano_VL_V2模型,在文本仅模式下避免启动崩溃;对已有正常多模态使用无影响。测试新增114行,覆盖了关键边界场景,提高了该模型权重加载的健壮性。对其他多模态模型无直接影响,但修复思路可借鉴。

多模态配置依赖 核心路径变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论