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 了。
权重加载流程总结:
- index.json 解析:
weight_utils.py:filter_duplicate_safetensors_files()读取weight_map - 模型初始化:各模型类通过
maybe_prefix()递归构建 prefix 路径 - 权重映射:
default_loader.py中的Source类使用 prefix 构建完整的权重名 - 权重加载:权重名与 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 补全。
获取 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_typepacked_modules_model_mapping 展开
但是注意,像这种 KeyError 不是因为前缀不对引起的,是因为权重没展开:
针对 MoE 模型,我们需要在这里 用 packed_modules_model_mapping 展开权重。因为不同模型架构对相同功能的模块使用不同的命名方式。例如:
qwen3_moe模型使用qkv_proj表示打包的 QKV 投影层deepseek_v2模型使用fused_qkv_a_proj表示打包的 QKV 投影层
但量化描述文件(quant_model_description.json)中只有展开后的权重名(如 q_proj.weight、k_proj.weight、v_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 修复方法
- vLLM 侧:修正
maybe_prefix()调用,确保 prefix 路径与 index.json 匹配 - vLLM-Ascend 侧:
- 如果是 MoE 模型,在
packed_modules_model_mapping中添加新模型展开映射 - 确保
quant_model_description.json中的 key 与模型 prefix 路径一致 - 检查
is_layer_skipped_ascend()中的 key 查找逻辑
- 如果是 MoE 模型,在