vLLM 侧权重是如何加载的?

理解 vLLM 如何定位和加载权重,是解决加载错误的第一步。

在 HuggingFace 格式的模型权重文件夹中,通常包含一个 model.safetensors.index.json 文件。该文件充当“地图”,记录了逻辑权重名称(Key)到物理文件(Value)的映射关系。vLLM 正是依据此文件去加载对应的 Tensor。

vllm/model_executor/model_loader/weight_utils.py 中的 filter_duplicate_safetensors_files() 函数负责解析此文件。

我们以 Qwen3-VL-30B-A3B-Instruct 为例,看看它的索引文件

{
  "metadata": {
    "total_size": 62141508064
  },
  "weight_map": {
    "lm_head.weight": "model-00013-of-00013.safetensors",
    "model.language_model.embed_tokens.weight": "model-00001-of-00013.safetensors",
    "model.language_model.layers.0.input_layernorm.weight": "model-00001-of-00013.safetensors",
    "model.language_model.layers.0.mlp.experts.down_proj": "model-00001-of-00013.safetensors",
    "model.language_model.layers.0.mlp.experts.gate_up_proj": "model-00001-of-00013.safetensors",
    "model.language_model.layers.0.mlp.gate.weight": "model-00001-of-00013.safetensors",
    "model.language_model.layers.0.post_attention_layernorm.weight": "model-00001-of-00013.safetensors",
    "model.language_model.layers.0.self_attn.k_norm.weight": "model-00001-of-00013.safetensors",
    "model.language_model.layers.0.self_attn.k_proj.weight": "model-00001-of-00013.safetensors",
    "model.language_model.layers.0.self_attn.o_proj.weight": "model-00001-of-00013.safetensors",
    "model.language_model.layers.0.self_attn.q_norm.weight": "model-00001-of-00013.safetensors",
    "model.language_model.layers.0.self_attn.q_proj.weight": "model-00001-of-00013.safetensors",
    "model.language_model.layers.0.self_attn.v_proj.weight": "model-00001-of-00013.safetensors",
    "model.language_model.layers.1.input_layernorm.weight": "model-00001-of-00013.safetensors",
    "model.language_model.layers.1.mlp.experts.down_proj": "model-00001-of-00013.safetensors",
    "model.language_model.layers.1.mlp.experts.gate_up_proj": "model-00001-of-00013.safetensors",
    "model.language_model.layers.1.mlp.gate.weight": "model-00001-of-00013.safetensors",
    ...
    }
}

我们逐层分解,去阅读 Qwen3-VL-Moe 系列模型的 vLLM modeling 代码。模型最外层是 Qwen3VLMoeForConditionalGeneration

@MULTIMODAL_REGISTRY.register_processor(
    Qwen3VLMultiModalProcessor,
    info=Qwen3VLMoeProcessingInfo,
    dummy_inputs=Qwen3VLDummyInputsBuilder,
)
class Qwen3VLMoeForConditionalGeneration(
    Qwen3VLForConditionalGeneration, Qwen3VLMoeMixtureOfExperts
):
    is_3d_moe_weight: bool = True
    packed_modules_mapping = {
        "qkv_proj": [
            "q_proj",
            "k_proj",
            "v_proj",
        ],
    }
 
    def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
        super(Qwen3VLForConditionalGeneration, self).__init__()
        config: Qwen3VLMoeConfig = vllm_config.model_config.hf_config
        quant_config = vllm_config.quant_config
        multimodal_config = vllm_config.model_config.multimodal_config
 
        self.config = config
        self.multimodal_config = multimodal_config
        self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
 
        if not multimodal_config.get_limit_per_prompt(
            "image"
        ) and not multimodal_config.get_limit_per_prompt("video"):
            self.visual = None
        else:
            self.visual = Qwen3_VisionTransformer(
                config.vision_config,
                norm_eps=getattr(config, "rms_norm_eps", 1e-6),
                quant_config=quant_config,
                prefix=maybe_prefix(prefix, "visual"), # 注意这里!!!!!
                use_data_parallel=self.use_data_parallel,
            )
 
        self.language_model = Qwen3MoeLLMForCausalLM(
            vllm_config=vllm_config, prefix=maybe_prefix(prefix, "language_model") # 注意这里!!!!!
        )
        # Whether to include the gate_up_proj mapping is determined by
        # the language model.
        self.packed_modules_mapping = (
            self.packed_modules_mapping | self.language_model.packed_modules_mapping
        )
 
        self.make_empty_intermediate_tensors = (
            self.language_model.make_empty_intermediate_tensors
        )
 
        self.use_deepstack = hasattr(config.vision_config, "deepstack_visual_indexes")
        self.deepstack_num_level = (
            len(config.vision_config.deepstack_visual_indexes)
            if self.use_deepstack
            else 0
        )
        # register buffer for deepstack
        if self.use_deepstack and self.visual is not None:
            self.deepstack_input_embeds = [
                torch.zeros(
                    vllm_config.scheduler_config.max_num_batched_tokens,
                    config.text_config.hidden_size,
                )
                for _ in range(self.deepstack_num_level)
            ]
        else:
            self.deepstack_input_embeds = None
        self.visual_dim = config.vision_config.out_hidden_size
        self.multiscale_dim = self.visual_dim * self.deepstack_num_level
 
        # Set MoE hyperparameters
        self.set_moe_parameters()

我们以 language_model 部分为例,继续拆解:

class Qwen3MoeLLMForCausalLM(Qwen3MoeForCausalLM):
    def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
        super(Qwen3MoeForCausalLM, self).__init__()
        self.config = vllm_config.model_config.hf_config.text_config
        self.quant_config = vllm_config.quant_config
        self.model = Qwen3MoeLLMModel(
            vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model") # 注意这里!!!!!
        )
        self.lm_head = ParallelLMHead(
            self.config.vocab_size,
            self.config.hidden_size,
            quant_config=self.quant_config,
            prefix=maybe_prefix(prefix, "lm_head"), # 注意这里!!!!!
        )
        if self.config.tie_word_embeddings:
            self.lm_head.weight = self.model.embed_tokens.weight
        self.logits_processor = LogitsProcessor(self.config.vocab_size)
        self.make_empty_intermediate_tensors = (
            self.model.make_empty_intermediate_tensors
        )
 

注意有 maybe_prefix的这两行,这里就是对应着 index.json 的前缀:

关键源码位置vllm/vllm/model_executor/models/utils.py:663-673 中的 maybe_prefix() 函数。

"lm_head.weight": "model-00013-of-00013.safetensors", // lm_head到此为止了
"model.language_model.embed_tokens.weight": "model-00001-of-00013.safetensors", // 但是model还可以继续拆解

继续拆解 Qwen3MoeLLMModel

@support_torch_compile
class Qwen3MoeModel(nn.Module):
    def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
        super().__init__()
 
        config = vllm_config.model_config.hf_text_config
        quant_config = vllm_config.quant_config
        parallel_config = vllm_config.parallel_config
        eplb_config = parallel_config.eplb_config
        self.num_redundant_experts = eplb_config.num_redundant_experts
 
        self.padding_idx = config.pad_token_id
        self.vocab_size = config.vocab_size
        self.config = config
        self.embed_tokens = VocabParallelEmbedding(
            config.vocab_size,
            config.hidden_size,
            quant_config=quant_config,
            prefix=f"{prefix}.embed_tokens", # 注意这里!!!!!
        )
        self.start_layer, self.end_layer, self.layers = make_layers(
            config.num_hidden_layers,
            lambda prefix: Qwen3MoeDecoderLayer(vllm_config=vllm_config, prefix=prefix),
            prefix=f"{prefix}.layers", # 注意这里!!!!!
        )
        self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
        self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
            ["hidden_states", "residual"], config.hidden_size
        )
        # Track layers for auxiliary hidden state outputs (EAGLE3)
        self.aux_hidden_state_layers: tuple[int, ...] = ()

vLLM 的权重加载是一个递归的字符串拼接过程。model.safetensors.index.json 里面的 key 严格对应了代码中模块初始化时传递的 prefix 路径。在量化模型适配过程中,经常会报 KeyError,也就是这个 key 不匹配。了解了模型是如何加载的,我们就知道如何修复这个 KeyError 了。

权重加载流程总结

  1. index.json 解析weight_utils.py:filter_duplicate_safetensors_files() 读取 weight_map
  2. 模型初始化:各模型类通过 maybe_prefix() 递归构建 prefix 路径
  3. 权重映射default_loader.py 中的 Source 类使用 prefix 构建完整的权重名
  4. 权重加载:权重名与 index.json 中的 key 精确匹配

vLLM-Ascend 侧量化权重加载通路

打开量化模型(目前还没有官方提供的 Qwen3-VL-Moe 的权重,先用 Qwen3-Moe 临时看看),我们会发现比浮点模型多了一个 quant_model_description.json

{
    "model.visual.patch_embed.proj.weight": "FLOAT",
    "model.visual.patch_embed.proj.bias": "FLOAT",
    "model.visual.pos_embed.weight": "FLOAT",
    "model.visual.blocks.0.norm1.weight": "FLOAT",
    "model.visual.blocks.0.norm1.bias": "FLOAT",
    "model.visual.blocks.0.norm2.weight": "FLOAT",
    "model.visual.blocks.0.norm2.bias": "FLOAT",
    "model.visual.blocks.0.attn.qkv.weight": "W8A8",
    "model.visual.blocks.0.attn.qkv.quant_bias": "W8A8",
    "model.visual.blocks.0.attn.qkv.input_scale": "W8A8",
    "model.visual.blocks.0.attn.qkv.input_offset": "W8A8",
    "model.visual.blocks.0.attn.qkv.deq_scale": "W8A8",
    "model.visual.blocks.0.attn.qkv.bias": "W8A8",
    "model.visual.blocks.0.attn.proj.weight": "W8A8",
    "model.visual.blocks.0.attn.proj.quant_bias": "W8A8",
    "model.visual.blocks.0.attn.proj.input_scale": "W8A8",
    "model.visual.blocks.0.attn.proj.input_offset": "W8A8",
    "model.visual.blocks.0.attn.proj.deq_scale": "W8A8",
    "model.visual.blocks.0.attn.proj.bias": "W8A8",
    "model.visual.blocks.0.mlp.linear_fc1.weight": "W8A8",
    "model.visual.blocks.0.mlp.linear_fc1.quant_bias": "W8A8",
    "model.visual.blocks.0.mlp.linear_fc1.input_scale": "W8A8",
    "model.visual.blocks.0.mlp.linear_fc1.input_offset": "W8A8",
    "model.visual.blocks.0.mlp.linear_fc1.deq_scale": "W8A8",
    "model.visual.blocks.0.mlp.linear_fc1.bias": "W8A8",
    "model.visual.blocks.0.mlp.linear_fc2.weight": "FLOAT",
    "model.visual.blocks.0.mlp.linear_fc2.bias": "FLOAT",
    "model.visual.blocks.1.norm1.weight": "FLOAT",
    "model.visual.blocks.1.norm1.bias": "FLOAT",
    "model.visual.blocks.1.norm2.weight": "FLOAT",
    "model.visual.blocks.1.norm2.bias": "FLOAT",
    "model.visual.blocks.1.attn.qkv.weight": "W8A8",
    "model.visual.blocks.1.attn.qkv.quant_bias": "W8A8",
    "model.visual.blocks.1.attn.qkv.input_scale": "W8A8",
    "model.visual.blocks.1.attn.qkv.input_offset": "W8A8",
    ...
}

这里的 Key 和 Value 是权重名和数据类型,因为有些部分比较敏感,量化会损失精度,所以保持 FLOAT 类型,有的地方使用不同的量化算法,如 W8A8 或者 W8A8_DYNAMIC。不同的数据类型 vLLM-Ascend 在计算时会调用不同的算子,如 matmul 算子就有量化版本和浮点版本。因此需要这个字典来让 vLLM-Ascend 后端知道该走什么分支。

vllm_ascend/quantization/utils.py 中的 get_quant_method 方法就是用来指派这个的。在运行量化模型时,这里也经常发生 KeyError

关键源码位置

  • vllm-ascend/vllm_ascend/quantization/utils.py:45-130 - get_linear_quant_type()get_quant_method_modelslim()
  • vllm-ascend/vllm_ascend/quantization/quant_config.py:103-138 - get_quant_method()

KeyError 修复

我们的 vLLM 适配,主要要修改的就是在 vLLM 和 vLLM-Ascend 中发生的 KeyError

我们以 Qwen3-VL-Moe 系列模型讲解如何修复。

使用主线版本 vLLM 和 vLLM-Ascend:

# Install vLLM.
git clone https://github.com/vllm-project/vllm
cd vllm
# 主线 vLLM-Ascend 对应的 vLLM commit 可以从vLLM-Ascend的CI配置中获取
# https://github.com/vllm-project/vllm-ascend/blob/c331503677d6c57a2387ed7c5c58a4ce956674d9/.github/workflows/vllm_ascend_test_pr_full.yaml#L73
git checkout ad32e3e19ccf0526cb6744a5fed09a138a5fb2f9
VLLM_TARGET_DEVICE=empty pip install -v -e .
 
# Install vLLM Ascend.
git clone  --depth 1 https://github.com/vllm-project/vllm-ascend.git
pip install -v -e vllm-ascend

拉起服务:

vllm serve /home/model_weights/Qwen3-VL-30B-A3B-Instruct-w8a8/ --served-model-name qwen  --enable-expert-parallel --allowed-local-media-path / --quantization ascend --port 8123

在 vLLM 中临时修复前缀

一般来说,会先在 vLLM 侧报 KeyError。

这里的临时修复的方法很简单,比对 model.safetensors.index.json 修改对应嵌套层级的 maybe_prefix 就行(在主线里暂时没报这个错,所以这里没有截图,在 v0.11.0 及之前的版本是会报错的):

self.visual = Qwen3_VisionTransformer(
                config.vision_config,
                norm_eps=getattr(config, "rms_norm_eps", 1e-6),
                quant_config=quant_config,
                prefix=maybe_prefix(prefix, "model.visual"), # 从 visual 改为 model.visual
                use_data_parallel=self.use_data_parallel,
            )

在 vLLM-Ascend 中修复

修补结果可以看这个 commit

如果在 vllm_ascend/quantization/quant_config.py 或者 vllm_ascend/quantization/utils.py 获取权重数据类型的时候报 KeyError,这个时候我们就可以比对 quant_model_description.json 文件中的 key,在 get_quant_method 方法中增加针对不同 model_type 判断逻辑来把 key 补全。

Image Image

获取 model_type

要是确保 vLLM 上下文已正确初始化。get_current_vllm_config() 依赖于全局状态,必须在 set_current_vllm_config() 上下文内或模型初始化完成后调用。

from vllm.config import get_current_vllm_config
 
vllm_config = get_current_vllm_config()
model_type = vllm_config.model_config.hf_config.model_type

packed_modules_model_mapping 展开

但是注意,像这种 KeyError 不是因为前缀不对引起的,是因为权重没展开:

Image

针对 MoE 模型,我们需要在这里packed_modules_model_mapping 展开权重。因为不同模型架构对相同功能的模块使用不同的命名方式。例如:

  • qwen3_moe 模型使用 qkv_proj 表示打包的 QKV 投影层
  • deepseek_v2 模型使用 fused_qkv_a_proj 表示打包的 QKV 投影层

但量化描述文件(quant_model_description.json)中只有展开后的权重名(如 q_proj.weightk_proj.weightv_proj.weight)。

就比如我们如果需要适配 Qwen3-VL-MoE 模型,就需要把它的 model_type 补上:

packed_modules_model_mapping = {
    "qwen3_moe": {
        "qkv_proj": [
            "q_proj",
            "k_proj",
            "v_proj",
        ],
        "gate_up_proj": [
            "gate_proj",
            "up_proj",
        ],
        "experts":
        ["experts.0.gate_proj", "experts.0.up_proj", "experts.0.down_proj"],
    },
    "qwen3_vl_moe": {   # 增加针对此的展开
        "qkv_proj": [
            "q_proj",
            "k_proj",
            "v_proj",
        ],
        "gate_up_proj": [
            "gate_proj",
            "up_proj",
        ],
        "experts":
        ["experts.0.gate_proj", "experts.0.up_proj", "experts.0.down_proj"],
    },
    "deepseek_v2": {
        "gate_up_proj": ["gate_proj", "up_proj"],
        "experts":
        ["experts.0.gate_proj", "experts.0.up_proj", "experts.0.down_proj"],
        "fused_qkv_a_proj": ["q_a_proj", "kv_a_proj_with_mqa"]
    },

总结:vLLM 与 vLLM-Ascend 修复方法

  1. vLLM 侧:修正 maybe_prefix() 调用,确保 prefix 路径与 index.json 匹配
  2. vLLM-Ascend 侧
    • 如果是 MoE 模型,在 packed_modules_model_mapping 中添加新模型展开映射
    • 确保 quant_model_description.json 中的 key 与模型 prefix 路径一致
    • 检查 is_layer_skipped_ascend() 中的 key 查找逻辑