首页 > 基础资料 博客日记

vLLM 权重加载机制全解析:从挑战到理想架构

2026-04-11 18:00:02基础资料围观1

这篇文章介绍了vLLM 权重加载机制全解析:从挑战到理想架构,分享给大家做个参考,收藏极客资料网收获更多编程知识

本文已于 2026.04.11 发表于公众号知乎

head

 

1. 权重加载要解决什么问题?

01-infographic-three-challenges

 在阅读 vLLM 的权重加载实现之前,先理解它要解决的核心问题。

大模型的权重通常以 checkpoint 文件的形式存储在磁盘上。权重加载的任务就是:把这些文件中的张量,正确地填入模型(推理代码)的每一个参数中。这件事看似简单——读文件、按名字匹配、拷贝数据——但有三个问题使它变得复杂。

问题一:张量并行(Tensor Parallelism)下的权重切分与内存控制

vLLM 支持将一个模型拆分到多张 GPU 上并行推理,这就是张量并行(TP)。TP 的核心思想是:把一个大矩阵按行或列切成若干份,每张 GPU 只持有其中一份,各自计算后再通过通信(AllReduce / AllGather)合并结果。

以一个 [4096, 4096] 的线性层权重为例,如果 TP=2:

  • • 列并行(Column Parallel):权重按列切分,GPU-0 持有 [4096, 2048],GPU-1 持有另一半。每张 GPU 用完整输入乘以自己的半个权重,得到半个输出,最后 AllGather 拼接。
  • • 行并行(Row Parallel):权重按行切分,GPU-0 持有 [2048, 4096],GPU-1 持有另一半。输入也被切分,各自计算后 AllReduce 求和。

问题:权重加载时不能简单地把整个张量拷贝到参数中,而是需要根据当前 GPU 的 rank,从完整权重中截取出属于自己的那一片,如何实现这个"截取"?同时,由于 checkpoint 存储的是完整权重,而每个 GPU 只需要 1/TP 的切片,如何保证加载过程中不出现内存、显存 OOM?

问题二:QKV 融合与 Gate-Up 融合

为了减少 kernel launch 开销和提高 GPU 利用率,vLLM 将多个逻辑上独立的权重融合(fuse)为一个物理参数:

QKV 融合:Transformer 的注意力层有 Q、K、V 三个投影矩阵。在 checkpoint 中它们是三个独立的权重(q_proj.weightk_proj.weightv_proj.weight),但 vLLM 将它们拼接为一个 qkv_proj.weight,这样一次 GEMM 就能同时计算 Q、K、V,减少了两次 kernel launch。

Gate-Up 融合:FFN 层中的 gate_proj 和 up_proj 同理被融合为 gate_up_proj,一次 GEMM 替代两次。

问题:checkpoint 中没有 qkv_proj 这个 key,只有 q_projk_projv_proj。加载时如何做映射?

问题三:meta 设备初始化与延迟物化(materialize,即在真实设备上分配实际内存)

PyTorch 提供了一种特殊的 meta 设备(device="meta"):在 meta 设备上创建的张量只记录 shape、dtype、stride 等元信息,不分配任何实际内存。这对大模型非常有用——一个 500B 参数的模型如果直接在 GPU 上初始化空参数,仅参数本身就需要约 1000GB 显存(FP16),远超单卡容量。

vLLM 在在线量化和 Transformers Backend 等场景中使用 meta 设备来延迟分配内存。

问题:当参数位于 meta 设备时,不能直接 copy_ 数据到参数中(meta 张量没有实际存储)。权重加载如何处理这些"虚拟"参数?


带着这三个问题,我们来看 vLLM 的实际实现。

2. 权重加载体系概述

02-flowchart-loading-pipeline

本节系统性地介绍 vLLM 的权重加载流程。

2.1 整体流程

vLLM 的权重加载分为四个阶段:模型初始化 → 权重读取 → 权重分发 → 后处理。

┌─────────────────────────────────────────────────────────────────────┐
│                    BaseModelLoader.load_model()                     │
│                                                                     │
│  ① initialize_model()          构建模型结构(空参数)                │
│         │                                                           │
│  ② load_weights(model, ...)    读取 checkpoint 并分发到参数          │
│         │                                                           │
│  ③ process_weights_after_loading()  量化后处理(repacking 等)       │
│         │                                                           │
│  ④ model.eval()                返回推理就绪的模型                    │
└─────────────────────────────────────────────────────────────────────┘

2.2 权重读取:从文件到迭代器

DefaultModelLoader 是最常用的加载器。它将 checkpoint 文件(safetensors / PyTorch bin)转换为 Iterable[tuple[str, torch.Tensor]] 迭代器——每个元素是一对 (权重名, 张量)

# vllm/model_executor/model_loader/default_loader.py — DefaultModelLoader.load_weights()
weights_to_load = {name for name, _ in model.named_parameters()}
loaded_weights = model.load_weights(self.get_all_weights(model_config, model))

get_all_weights() 内部调用 safetensors_weights_iterator() 等函数,逐文件、逐 key 地 yield 出 (name, tensor) 对。这里的流式迭代器(yield)避免了一次性将整个 checkpoint 读入内存——每次只读取一个张量,处理完毕后即可释放,CPU 内存峰值仅为单个最大张量的大小。此阶段 yield 出的张量位于 CPU 主存中,保持 checkpoint 原始的 key 命名和完整 shape。

2.3 权重分发:两种模式并存

权重迭代器被传入 model.load_weights(),由模型层面决定如何将每个 (name, tensor) 分发到对应参数。当前存在两种分发模式:

2.3.1 模式 A:手动遍历(传统模式,逐步被替代)

顶层模型类(继承 nn.Module,如 QWenLMHeadModel)的 load_weights 方法手动遍历迭代器,逐条处理 key 重命名、融合映射、shard_id 注入,最终调用 param.weight_loader(param, loaded_weight, shard_id) 完成加载。以 qwen.pyvllm/model_executor/models/qwen.py,Qwen-1 代模型)为例:

# 典型的手动遍历模式(vllm/model_executor/models/qwen.py — QWenBaseModel.load_weights)
def load_weights(self, weights):
    stacked_params_mapping = [
        # (param_name, shard_name, shard_id)
        ("gate_up_proj", "w2", 0),
        ("gate_up_proj", "w1", 1),
    ]
    params_dict = dict(self.named_parameters())
    loaded_params: set[str] = set()
    for name, loaded_weight in weights:
        if "rotary_emb.inv_freq" in name:
            continue
        for param_name, weight_name, shard_id in stacked_params_mapping:
            if weight_name not in name:
                continue
            name = name.replace(weight_name, param_name)
            ...
            param = params_dict[name]
            weight_loader = param.weight_loader
            weight_loader(param, loaded_weight, shard_id)
            break
        else:
            ...
            param = params_dict[name]
            weight_loader = getattr(param, "weight_loader", default_weight_loader)
            weight_loader(param, loaded_weight)
        loaded_params.add(name)
    return loaded_params

2.3.2 模式 B:自动递归(AutoWeightsLoader 模式,当前主流方向)

顶层模型类(继承 nn.Module,如 Qwen3ForCausalLM)的 load_weights 创建 AutoWeightsLoader 实例,由它按模块树自动分发权重。AutoWeightsLoader 接收顶层模型实例(即整棵模块树的根节点),按 . 分割权重名,逐级匹配子模块或参数,采用三级优先策略:

AutoWeightsLoader._load_module(prefix, module, weights)
  ├─ ① 若 module 有 load_weights 方法 → 委托给它(模块级优先)
  ├─ ② 按 prefix 匹配子模块 → 递归 _load_module(子模块递归)
  └─ ③ 按 prefix 匹配参数 → 调用 param.weight_loader(参数级处理)
# 典型的 AutoWeightsLoader 模式(vllm/model_executor/models/qwen3.py — Qwen3ForCausalLM.load_weights)
def load_weights(self, weights):
    loader = AutoWeightsLoader(
        self,
        skip_prefixes=(["lm_head."] if self.config.tie_word_embeddings else None),
    )
    return loader.load_weights(weights)

AutoWeightsLoader 核心实现

理解 AutoWeightsLoader 的内部机制对后续分析缺陷至关重要。它的核心在于两个方法:load_weights(入口)和 _load_module(递归引擎)。

# vllm/model_executor/models/utils.py — AutoWeightsLoader 核心实现(简化)

class AutoWeightsLoader:
    def __init__(self, module: nn.Module, *, skip_prefixes=None, ...):
        self.module = module  # 顶层模型实例(整棵模块树的根节点)

    def load_weights(self, weights, *, mapper=None) -> set[str]:
        """入口方法:启动从根节点开始的递归加载"""
        if mapper is not None:
            weights = mapper.apply(weights)
        # 从根模块开始递归
        autoloaded_weights = set(self._load_module("", self.module, weights))
        return autoloaded_weights

    def _load_module(self, base_prefix, module, weights) -> Iterable[str]:
        """递归引擎:对每个模块执行三级优先策略"""

        # ① 模块级优先:若子模块自身定义了 load_weights,委托给它
        #    注意:跳过根模块自身(self.module),避免无限递归
        if module != self.module:
            module_load_weights = getattr(module, "load_weights", None)
            if callable(module_load_weights):
                yield from module_load_weights(weights)  # 委托
                return  # 该模块的权重已由其自身处理完毕

        child_modules = dict(module.named_children())
        child_params = dict(module.named_parameters(recurse=False))

        # 按权重名的第一段前缀分组,逐组处理
        for child_prefix, child_weights in self._groupby_prefix(weights):
            prefix = self._get_qualname(base_prefix, child_prefix)

            if child_prefix in child_modules:
                # ② 子模块递归:匹配到子模块,递归进入
                yield from self._load_module(
                    prefix, child_modules[child_prefix], child_weights
                )
            elif child_prefix in child_params:
                # ③ 参数级处理:匹配到参数,调用 param.weight_loader
                yield from self._load_param(
                    prefix, child_params[child_prefix], child_weights
                )
            else:
                raise ValueError(f"No module or parameter named {prefix!r}")

    def _load_param(self, base_prefix, param, weights) -> Iterable[str]:
        """参数级加载:调用参数上挂载的 weight_loader"""
        for weight_name, weight_data in weights:
            weight_loader = getattr(param, "weight_loader", default_weight_loader)
            weight_loader(param, weight_data)
            yield self._get_qualname(base_prefix, weight_name)

关键调用链:load_weights → AutoWeightsLoader → load_weights(递归)

这里存在一个容易混淆但非常重要的递归结构——顶层模型的 load_weights 创建了 AutoWeightsLoader,而 AutoWeightsLoader 在递归过程中又会调用子模块的 load_weights

趋势:手动遍历中的"路由分发"部分正在被 AutoWeightsLoader 替代。 对比同一系列模型的演进可以清晰看到这一趋势:早期的 qwen.pyvllm/model_executor/models/qwen.py,Qwen-1)使用约 30 行手动遍历代码同时处理路由和融合,而后续的 qwen3.pyvllm/model_executor/models/qwen3.py,Qwen-3)将路由职责交给 AutoWeightsLoader,顶层仅需 4 行代码。

2.3.3 字段融合映射:stacked_params_mapping 机制

如第 1 章"问题二"所述,vLLM 将多个逻辑独立的权重融合为一个物理参数(如 q_proj + k_proj + v_proj → qkv_proj)。但 checkpoint 中只有原始的分开 key,没有融合后的 key。stacked_params_mapping 就是解决这个映射问题的机制——它告诉加载器"这个 checkpoint key 应该填到融合参数的哪个位置"。

映射表的结构

每条映射是一个三元组 (param_name, shard_name, shard_id)

  • • param_name:融合后的参数名(模型中实际存在的参数)
  • • shard_name:checkpoint 中的原始 key 片段
  • • shard_id:该原始 key 在融合参数中的位置标识
stacked_params_mapping = [
    # (param_name, shard_name, shard_id)
    ("qkv_proj", "q_proj", "q"),    # q_proj → qkv_proj 的 Q 区域
    ("qkv_proj", "k_proj", "k"),    # k_proj → qkv_proj 的 K 区域
    ("qkv_proj", "v_proj", "v"),    # v_proj → qkv_proj 的 V 区域
    ("gate_up_proj", "gate_proj", 0),  # gate_proj → gate_up_proj 的第 0 分片
    ("gate_up_proj", "up_proj", 1),    # up_proj   → gate_up_proj 的第 1 分片
]

加载过程

当遍历到 checkpoint key model.layers.0.self_attn.q_proj.weight 时:

  1. 1. 匹配到 shard_name="q_proj",将 key 中的 q_proj 替换为 qkv_proj,得到 model.layers.0.self_attn.qkv_proj.weight
  2. 2. 带上 shard_id="q" 调用 weight_loader(param, loaded_weight, shard_id="q")
  3. 3. weight_loader 内部根据 shard_id 计算偏移量,将数据写入 qkv_proj 参数的 Q 区域

融合映射在模式 A 和模式 B 中都在使用。 无论是手动遍历(模式 A)还是 AutoWeightsLoader 递归分发(模式 B),融合映射的处理逻辑都由各模型文件自行实现——在 load_weights 方法中定义 stacked_params_mapping 并遍历匹配。具体的处理代码可参见 2.3.1 中模式 A 的完整示例。

AutoWeightsLoader 与 stacked_params_mapping 的分层协作

  • • AutoWeightsLoader 负责递归分发——按模块树自动将权重路由到对应的子模块/参数,替代的是模式 A 中手动 for name, loaded_weight in weights + params_dict[name] 的路由逻辑;
  • • stacked_params_mapping 负责字段融合映射——将 checkpoint 中分开的 q_proj/k_proj/v_proj 映射到融合后的 qkv_proj,并注入 shard_id

部分较新的模型同时使用两者,形成分层协作:顶层模型类用 AutoWeightsLoader 做递归分发,AutoWeightsLoader 递归到某个子模块时发现它有 load_weights 方法便委托给它,子模块内部再用 stacked_params_mapping 处理融合映射。

以 qwen3_next.pyvllm/model_executor/models/qwen3_next.py)为例,顶层 Qwen3NextForCausalLM 使用 AutoWeightsLoader,而中间层 Qwen3NextModel 使用 stacked_params_mapping

# 顶层:Qwen3NextForCausalLM.load_weights — 使用 AutoWeightsLoader 递归分发
class Qwen3NextForCausalLM(...):
    def load_weights(self, weights):
        loader = AutoWeightsLoader(self, skip_prefixes=["mtp."])
        return loader.load_weights(weights)

# 中间层:Qwen3NextModel.load_weights — 使用 stacked_params_mapping 处理融合映射
class Qwen3NextModel(nn.Module):
    def load_weights(self, weights):
        stacked_params_mapping = [
            ("qkv_proj", "q_proj", "q"),
            ("qkv_proj", "k_proj", "k"),
            ("qkv_proj", "v_proj", "v"),
            ("gate_up_proj", "gate_proj", 0),
            ("gate_up_proj", "up_proj", 1),
        ]
        params_dict = dict(self.named_parameters())
        loaded_params: set[str] = set()
        for name, loaded_weight in weights:
            for param_name, weight_name, shard_id in stacked_params_mapping:
                if weight_name not in name:
                    continue
                name = name.replace(weight_name, param_name)
                param = params_dict[name]
                weight_loader = param.weight_loader
                weight_loader(param, loaded_weight, shard_id)
                break
            else:
                param = params_dict[name]
                weight_loader = getattr(param, "weight_loader", default_weight_loader)
                weight_loader(param, loaded_weight)
            loaded_params.add(name)
        return loaded_params

调用链如下:

Qwen3NextForCausalLM.load_weights()
  └─ AutoWeightsLoader.load_weights(weights)
       └─ _load_module("", Qwen3NextForCausalLM, weights)
            └─ _load_module("model", Qwen3NextModel, grouped_weights)
                 └─ Qwen3NextModel.load_weights(grouped_weights)  ← 委托(优先级 ①)
                      └─ 遍历 stacked_params_mapping,处理融合映射
                           └─ param.weight_loader(param, loaded_weight, shard_id)

2.4 参数级加载:weight_loader 的职责

无论哪种分发模式,最终都会调用参数上的 weight_loader 来完成实际的数据拷贝。weight_loader 负责处理 TP 分片(从完整权重中 narrow 出当前 rank 的切片)和 融合偏移(将多个子权重拼接到同一个参数的不同区域)。

在深入两代参数体系之前,先理解 nn.Parameter 本身。nn.Parameter 本质上就是 torch.Tensor——它直接继承自 Tensor,只额外做了两件事:

  1. 1. 默认 requires_grad=True:普通 Tensor 默认不参与梯度计算,而 Parameter 默认参与。这是它作为"可学习参数"的语义标识。
  2. 2. 自动注册到 nn.Module:当一个 Parameter 被赋值为 Module 的属性时(如 self.weight = nn.Parameter(...)),Module 的 __setattr__ 会自动将其注册到 _parameters 字典中,使其能被 named_parameters() 遍历到、被优化器发现、被 state_dict() 序列化。

除此之外,nn.Parameter 没有任何额外的数据存储或方法

vLLM 中存在两代参数体系,分别以不同方式在这个"纯粹的 Tensor 子类"上附加权重加载能力:

 v1(nn.Parameter + 动态属性)v2(BasevLLMParameter 子类)
类型 PyTorch 原生 nn.Parameter BasevLLMParameter 及其子类
weight_loader 来源 由 set_weight_attrs 或直接赋值动态挂载 构造函数传入,作为正式类属性
TP 分片逻辑 在 weight_loader 函数内手动 narrow + copy_ 由参数子类的 load_column_parallel_weight() 等方法封装
代表 ColumnParallelLinear.weight_loader(v1) ModelWeightParameter.load_column_parallel_weight()(v2)

2.4.1 v1:nn.Parameter + 动态属性

v1 的做法利用了 Python 的动态属性机制,绕过了类型系统的约束。如上所述,nn.Parameter 本质上只是一个 Tensor,本身不具备 weight_loader 属性。v1 通过 setattr或直接赋值,将 weight_loader 强行注入到 nn.Parameter 实例上:

# 方式一:通过 set_weight_attrs 间接注入(vllm/model_executor/utils.py)
def set_weight_attrs(weight: torch.Tensor, weight_attrs: dict[str, Any] | None):
    if weight_attrs is None:
        return
    for key, value in weight_attrs.items():
        assert not hasattr(weight, key), f"Overwriting existing tensor attribute: {key}"
        setattr(weight, key, value)  # 本质是 setattr,动态挂载任意属性

# 使用示例(vllm/model_executor/layers/linear.py — ColumnParallelLinear.__init__)
# bias 是原生 nn.Parameter,通过 set_weight_attrs 挂载 weight_loader 和 output_dim
self.bias = Parameter(torch.empty(self.output_size_per_partition, dtype=params_dtype))
set_weight_attrs(self.bias, {"output_dim": 0, "weight_loader": self.weight_loader})

# 方式二:直接赋值(vllm/model_executor/layers/mamba/linear_attn.py — MiniMaxText01RMSNormTP)
self.weight = nn.Parameter(torch.ones(int(hidden_size / self.tp_world)))
self.weight.weight_loader = self.weight_loader  # 直接在 nn.Parameter 上挂载动态属性

这种做法的问题在于:nn.Parameter 的类型定义中没有 weight_loader 属性,类型检查器无法校验、不同模块挂载的 weight_loader 签名也各不相同(详见 4.3.1 节)。

2.4.2 v2:BasevLLMParameter 子类体系

v2 的 BasevLLMParameter 是更好的设计。它继承 nn.Parameter,将 weight_loader 作为构造函数的正式参数,通过 @property 暴露为类属性,具备完整的类型约束:

# vllm/model_executor/parameter.py — BasevLLMParameter
class BasevLLMParameter(Parameter):
    def __init__(self, data: torch.Tensor, weight_loader: Callable):
        # weight_loader 是构造函数的正式参数,而非动态挂载
        self._weight_loader = weight_loader
        self.tp_rank = get_tensor_model_parallel_rank()
        self.tp_size = get_tensor_model_parallel_world_size()

    @property
    def weight_loader(self) -> Callable:
        return self._weight_loader

# 使用示例(vllm/model_executor/layers/quantization/fp8.py — Fp8LinearMethod.create_weights)
weight = ModelWeightParameter(  # ModelWeightParameter 继承自 BasevLLMParameter
    data=torch.empty(output_size_per_partition, input_size_per_partition, dtype=weight_dtype),
    input_dim=1,
    output_dim=0,
    weight_loader=weight_loader,  # 通过构造函数传入,类型明确
)
layer.register_parameter("weight", weight)

此外,v2 还将 TP 分片逻辑封装为参数自身的方法(如 load_column_parallel_weight()load_merged_column_weight()),而非散落在外部的 weight_loader 函数中,内聚性更好。

2.5 后处理:process_weights_after_loading

process_weights_after_loading 负责将权重从存储格式转为运行时 kernel 所需的格式,完成量化权重的 repacking、scale 计算、格式转换等操作。它的调用时机取决于加载场景:

默认场景(非在线量化):在整个模型的所有权重加载完成后统一调用。从 BaseModelLoader.load_model 的流程可以清楚看到这一顺序:

# vllm/model_executor/model_loader/base_loader.py — BaseModelLoader.load_model
self.load_weights(model, model_config)                              # ← 先加载完所有权重
process_weights_after_loading(model, model_config, target_device)   # ← 再统一后处理

process_weights_after_loading 接收整个 model(根 nn.Module),内部通过 model.named_modules() 遍历所有子模块,逐个检查是否有 quant_method 并调用后处理:

# vllm/model_executor/model_loader/utils.py
def process_weights_after_loading(model, model_config, target_device):
    for _, module in model.named_modules():
        quant_method = getattr(module, "quant_method", None)
        if isinstance(quant_method, QuantizeMethodBase):
            with device_loading_context(module, target_device):
                quant_method.process_weights_after_loading(module)

在线量化场景(layerwise reload):后处理是逐层进行的——每层权重加载完毕后立即执行该层的 process_weights_after_loading,将全精度权重转为低精度格式后释放,再处理下一层。这样 GPU 上同一时刻只需持有一层的全精度权重,峰值显存大幅降低。

2.6 关键参与者总结

权重加载关键参与者

 在整个流程中,有两个最核心的参与者需要重点理解:模块级的 load_weights 方法和参数级的 weight_loader 属性。它们分别承担了权重加载的"调度"和"执行"职责。

2.6.1 模块级:Module.load_weights

load_weights 是 vLLM 各模型类自行定义的约定方法,框架通过 hasattr(module, "load_weights") 检测模块是否实现了该方法,如果有则调用。它负责权重的路由与调度——决定每个 checkpoint 权重应该交给哪个参数来处理。它出现在两个层次:

  • • 顶层模型类(如 Qwen3ForCausalLM):作为整个权重加载的入口,由 BaseModelLoader 调用。顶层 load_weights 要么手动遍历迭代器(模式 A),要么创建 AutoWeightsLoader 委托递归分发(模式 B)。
  • • 中间层子模块(如 Qwen3NextModel):当 AutoWeightsLoader 递归到某个子模块时,如果该子模块有 load_weights 方法,就优先委托给它。子模块的 load_weights 通常负责处理融合映射(stacked_params_mapping)等该层特有的逻辑。

load_weights 的核心职责包括:

  1. 1. key 重命名:将 checkpoint key 映射为模型参数名
  2. 2. 融合映射:通过 stacked_params_mapping 将分开的 checkpoint key(q_projk_projv_proj)映射到融合参数(qkv_proj),并注入 shard_id
  3. 3. 路由分发:将处理后的 (name, tensor) 交给对应参数的 weight_loader 完成实际加载

2.6.2 参数级:param.weight_loader

weight_loader 是挂载在 nn.Parameter(或其子类 BasevLLMParameter)上的可调用属性,负责权重的实际写入——将一个 checkpoint 张量正确地填入参数的数据存储中。它是权重加载链条的最后一环,处理两件关键事情:

  • • TP 分片:根据当前 rank 从完整权重中 narrow 出属于自己的 1/TP 切片
  • • 融合偏移:根据 shard_id 计算偏移量,将数据写入融合参数的正确区域

weight_loader 的典型调用方式:

# 非融合权重:2 参数调用
weight_loader(param, loaded_weight)

# 融合权重:3 参数调用,带 shard_id
weight_loader(param, loaded_weight, shard_id)

weight_loader 是一种通用的参数级加载协议,并不局限于线性层。任何需要自定义权重写入逻辑的层都可以为其参数提供 weight_loader。常见的来源包括:

  • • 线性层:ColumnParallelLinear.weight_loaderMergedColumnParallelLinear.weight_loaderQKVParallelLinear.weight_loader 等,处理 TP 分片和融合偏移
  • • Embedding 层:VocabParallelEmbedding.weight_loader,处理词表分片
  • • Mamba 层:mamba_v2_sharded_weight_loader,处理 SSM 投影的交错分片
  • • MoE 层:FusedMoE 中的 weight_loader,处理专家权重的分发
  • • v2 参数子类:BasevLLMParameter 及其子类自身就携带 weight_loader 属性,将加载逻辑内聚到参数类型中

这些 weight_loader 被"挂载"到参数上,由外部框架通过参数间接调用。这种设计使得外部框架无需知道参数属于哪种层,只需调用 param.weight_loader 即可。

2.6.3 两者的协作关系

Module.load_weights(调度层)
  │  "这个 checkpoint key 应该交给哪个参数?shard_id 是什么?"
  │
  ▼
param.weight_loader(执行层)
  │  "我拿到了数据和 shard_id,按 TP 分片规则写入正确位置"
  │
  ▼
参数数据更新完成

简言之:load_weights 解决"谁来加载"的问题,weight_loader 解决"怎么加载"的问题。前者是调度逻辑,后者是执行逻辑。

以上是 vLLM 权重加载体系的全貌。


3. 问题解答

03-framework-tp-sharding-flow

 本节回到第 1 章提出的三个问题,结合第 2 章介绍的体系,逐一给出 vLLM 的解决方案。

解答一:TP 下的权重切分与内存控制

切分机制:权重加载时,通过 narrow(截取)操作从完整权重中取出属于当前 rank 的切片,再 copy_ 到参数中。这个"截取"操作就是 weight_loader 的核心职责之一。

具体来说,ColumnParallelLinear.weight_loadervllm/model_executor/layers/linear.py)会根据 tp_rank 和 tp_size 计算当前 rank 的起始位置和分片大小,然后在 CPU 张量上执行 narrow

param_data = param.data
shard_size = param_data.shape[output_dim]
start_idx = self.tp_rank * shard_size
loaded_weight = loaded_weight.narrow(output_dim, start_idx, shard_size)
param_data.copy_(loaded_weight)

RowParallelLinear 的逻辑类似,只是切分维度不同。

显存侧:参数只分配 1/TP 的空间

模型初始化时,vLLM 在 GPU 设备上下文中创建模型(vllm/model_executor/model_loader/base_loader.py):

with target_device:  # GPU
    model = initialize_model(vllm_config=vllm_config, ...)

此时 ColumnParallelLinearRowParallelLinear 等并行层会根据 tp_size 计算分片后的尺寸,只在 GPU 上分配 [4096, 4096/TP] 大小的参数,而非完整的 [4096, 4096]。因此,GPU 显存从一开始就只占 1/TP。

内存侧:逐张量读取 + CPU 上 narrow

checkpoint 权重的读取采用流式迭代模式(vllm/model_executor/model_loader/weight_utils.py):

# safetensors 的默认加载方式:逐张量按需读取到 CPU
with safe_open(st_file, framework="pt") as f:
    for name in f.keys():
        param = f.get_tensor(name)  # 读取单个张量到 CPU
        yield name, param

safetensors 的 safe_open 使用 mmap 机制,get_tensor() 只将当前请求的单个张量从磁盘读入 CPU 内存,而非一次性加载整个文件。随后在 weight_loader 中,narrow 操作在 CPU 张量上执行,截取出当前 rank 需要的 1/TP 切片,再通过 param_data.copy_(loaded_weight) 跨设备拷贝到 GPU:

# ColumnParallelLinear.weight_loader — narrow 在 CPU 上执行
loaded_weight = loaded_weight.narrow(output_dim, start_idx, shard_size)  # CPU 上截取
param_data.copy_(loaded_weight)  # CPU → GPU 拷贝,只传输 1/TP 的数据

这样,CPU 内存峰值 ≈ 单个最大张量的完整大小(通常几百 MB),而非整个模型的大小;GPU 显存始终只持有 1/TP 的参数。

注意:每个 rank 都独立读取完整的 checkpoint 文件。虽然每个 rank 最终只需要 1/TP 的数据,但它们都会遍历所有 checkpoint 文件、读取每个完整张量、然后各自 narrow 出自己的切片。这意味着磁盘 I/O 是 TP 倍冗余的——这是当前设计的一个 trade-off,用 I/O 冗余换取实现简单性(无需 rank 间协调分工读取)。fastsafetensors 和 instanttensor 等加载器尝试通过分布式 I/O 来优化这一点。

解答二:QKV 融合与 Gate-Up 融合的加载

这就是 stacked_params_mapping 和 shard_id 机制存在的原因——它们告诉加载器"这个 checkpoint key 应该填到融合参数的哪个位置"。详细的映射表结构、加载过程和示例代码参见 2.3.3 节。

简要来说,加载时需要:

  1. 1. 识别出 q_proj 应该映射到 qkv_proj 的第 0 个分片(shard_id="q");
  2. 2. 将 q_proj 的数据写入 qkv_proj 参数的对应区域;
  3. 3. 对 k_projv_proj 重复上述过程,分别写入各自的区域。

解答三:meta 设备初始化与延迟物化

vLLM 在两个场景中使用 meta 设备,并通过不同的物化策略来处理,下面以 Online Quantization(在线量化)为例:

当用户指定在线量化(如 FP8 per-tensor)时,模型加载的目标是:读取全精度 checkpoint → 在线量化为低精度 → 存储量化后的权重。如果先在 GPU 上分配全精度参数,再量化为 FP8,那么在量化完成前,GPU 上需要同时持有全精度权重和量化后权重,峰值内存翻倍。

为了解决这个问题,在线量化方法(如 Fp8OnlineLinearMethodvllm/model_executor/layers/quantization/fp8.py)将权重创建在 meta 设备上:

weight = ModelWeightParameter(
    data=torch.empty(output_size_per_partition, input_size_per_partition,
                     device="meta",  # 不分配实际内存
                     dtype=params_dtype),
    ...
)

然后通过 逐层物化(layerwise reload) 机制(vllm/model_executor/model_loader/reload/layerwise.py)处理:

  1. 1. 权重加载时,先将 checkpoint 数据缓冲在 CPU 内存中,不立即写入参数(此时参数位于 meta 设备,无法写入)。具体来说,online_process_loader 拦截 weight_loader 调用,将调用参数(包含来自 checkpoint 迭代器的 CPU 张量引用)缓存到 LayerReloadingInfo.loaded_weights 列表中;
  2. 2. 当一层的所有权重都缓冲完毕后,才物化(materialize)该层——在 GPU 上分配真实内存;
  3. 3. 将缓冲的权重加载到物化后的参数中;
  4. 4. 立即执行量化处理(process_weights_after_loading),将全精度权重转为 FP8;
  5. 5. 释放全精度权重,只保留量化后的结果。

这样,GPU 上同一时刻只需要持有一层的全精度权重,量化完成后立即释放,峰值显存大幅降低。


4. 设计缺陷分析

04-infographic-design-flaws

 本节逐一分析 vLLM 权重加载体系中不合理的设计。尽管我称它们为“设计缺陷”,但它们对 vLLM 系统的稳定、性能没有任何影响,大多数时候仅影响到了人类程序员,阅读的时候要多绕几个弯,开发的时候需要额外关注更多细节。

4.1 【非必要的分离设计带来开发负担】AutoWeightsLoader 的防递归与双向依赖

AutoWeightsLoader 是独立于模型的工具类,由模型的 load_weights 创建,但它又会反过来调用子模块的 load_weights,形成双向依赖。

案例 A:防递归的防御代码

vllm/model_executor/models/utils.py — AutoWeightsLoader._load_module()

# Avoid infinite recursion since this function is typically
# called inside load_weights of the module itself
if module != self.module:
    module_load_weights = getattr(module, "load_weights", None)
    if callable(module_load_weights):
        loaded_params = module_load_weights(weights)

module != self.module 这行检查的存在,说明框架意识到了递归风险——如果顶层模块的 load_weights 创建了 AutoWeightsLoader,而 AutoWeightsLoader 又调用了同一个模块的 load_weights,就会无限递归。这是设计缺陷的症状,好的设计不应该有这种初看莫名其妙的防御。

案例 B:双向依赖的调用链

Model.load_weights()
  └─ 创建 AutoWeightsLoader(self)
       └─ AutoWeightsLoader._load_module()
            └─ 调用 child_module.load_weights()   ← 反向调用

AutoWeightsLoader 在多个模型文件中被创建,每个模型的 load_weights 都是 AutoWeightsLoader 的创建者,同时又是它的潜在调用目标。这种双向依赖增加了理解和维护的负担。

4.2 【不内聚】融合 key 映射散落在模型层,而非内聚于融合算子

融合层(如 MergedColumnParallelLinearQKVParallelLinear)将多个 checkpoint key 合并到一个参数中,但映射关系由每个模型文件自行定义,而非由融合算子自身声明, 导致多个模型文件均有类似的 stacked_params_mapping 定义。

案例:多个模型文件重复定义几乎相同的映射表

stacked_params_mapping = [
    # (param_name, shard_name, shard_id)
    ("qkv_proj", "q_proj", "q"),
    ("qkv_proj", "k_proj", "k"),
    ("qkv_proj", "v_proj", "v"),
    ("gate_up_proj", "gate_proj", 0),
    ("gate_up_proj", "up_proj", 1),
]

问题本质:融合算子(如 MergedColumnParallelLinear)知道自己由哪些子权重组成,但它不声明这个信息,而是让每个使用它的模型文件去重复声明。这违反了"信息应由拥有者管理"的内聚原则。

4.3 【核心缺陷】nn.Parameter 承载了不属于自己的职责,导致参数对象不纯粹

如 2.4 节所述,nn.Parameter 本质上只是一个带 requires_grad 标志的 torch.Tensor,是纯粹的数据容器。但 vLLM 通过动态属性将权重加载的调度逻辑(weight_loader)等挂载到 nn.Parameter 上,使其承载了不属于自己的职责。这个根因导致了三个层面的问题:动态挂载绕过类型系统(4.3.1)、weight_loader 两版本共存的版本分裂(4.3.2)、以及 meta 设备物化被迫使用 __class__ hack(4.3.3)。

4.3.1 表现一:在原生 nn.Parameter 上动态挂载 weight_loader(绕过类型系统)

nn.Parameter 是 PyTorch 原生类型,不具备 weight_loader 属性。vLLM 利用 Python 动态语言特性,通过两种方式强行注入该属性。

案例 A:直接赋值

self.weight.weight_loader = self._weight_loader  # 动态挂载

案例 B:通过 set_weight_attrs 间接注入

def set_weight_attrs(weight: torch.Tensor, weight_attrs: dict[str, Any] | None):
    for key, value in weight_attrs.items():
        setattr(weight, key, value)  # 本质仍是 setattr

BasevLLMParameter(继承自 nn.Parameter)已将 weight_loader 改为正式的类属性,是对这一问题的改进。

4.3.2 表现二:weight_loader v1/v2 两版本共存(版本分裂)

vllm/model_executor/layers/linear.py 中维护了白名单 WEIGHT_LOADER_V2_SUPPORTED,量化方法在白名单中则使用 v2(BasevLLMParameter 子类的 load_column_parallel_weight() 等方法),否则使用 v1(外部函数手动 narrow + copy_)。两个版本并存意味着:新增量化方法时需要决定支持哪个版本并手动加入白名单,已有代码中两种风格混杂。

根因:v1/v2 两版本共存只是表面现象,根源在于权重加载的调度逻辑被挂在了参数上——v1 和 v2 本质上都是在参数级别做调度,只是实现风格不同。如果将调度职责提升到模块级(由 nn.Module 的 load_weights 方法负责),参数不再承载 weight_loader,只保留自服务的分片能力,白名单机制和版本分裂也就自然消失了(详见第 5 章理想态设计)。

4.3.3 表现三:meta 设备物化依赖 __class__ hack(连锁反应)

参数对象不纯粹的另一个连锁反应体现在 meta 设备物化上。当参数位于 meta 设备时,需要在真实设备上创建一个等价的参数对象。由于 nn.Parameter 上通过 setattr 挂载了 weight_loaderoutput_dim 等动态属性,这些属性存储在实例的 __dict__ 中,无法通过标准的 nn.Parameter(data, requires_grad) 构造器重建。因此,materialize_meta_tensor() 只能绕过正常的对象构造流程,使用 __class__ + __dict__ 拷贝的 hack:

def materialize_meta_tensor(meta_tensor: torch.Tensor) -> torch.Tensor:
    tensor = torch.empty_strided(
        size=tuple(meta_tensor.size()),
        stride=tuple(meta_tensor.stride()),
        dtype=meta_tensor.dtype,
        requires_grad=False,
    )
    tensor.__class__ = meta_tensor.__class__    # ← __class__ hack:强制改变类型
    tensor.__dict__ = meta_tensor.__dict__.copy()  # ← 拷贝动态属性(weight_loader 等)
    return tensor

如果原生 nn.Parameter 上没有这些动态属性(__dict__ 为空),直接走标准构造器 nn.Parameter(real_data) 即可,__class__ hack 和 __dict__ 拷贝都不再需要。对于 BasevLLMParameter 子类,其 __dict__ 中的分片元数据(_output_dimtp_rank 等)是参数自身的固有属性,可以通过给基类添加 materialize_on 方法来正规处理(走 __new__ 而非 __init__,避免构造器副作用,同时继承分片元数据),同样不需要 __class__ hack。


5. 理想态设计

05-framework-ideal-architecture

 基于第 4 章的缺陷分析,本节贴着缺陷逐一展开理想态设计方向。核心思想是:引入 nn.Module 基类承担递归加载职责(消除 AutoWeightsLoader),融合映射内聚到融合算子,nn.Parameter 去掉 weight_loader 等动态属性,所有定制加载逻辑由参数的拥有者(nn.Module 派生类)通过实现 load_weights 来完成。

5.1 消除 AutoWeightsLoader:引入 nn.Module 基类(对应缺陷 4.1)

5.1.1 问题回顾

如 4.1 节所述,AutoWeightsLoader 是一个独立于模型的工具类,由模型的 load_weights 创建,但它又会反过来调用子模块的 load_weights,形成双向依赖。module != self.module 这种防递归检查的存在,本身就说明了设计上的不自然。

根本原因在于:递归遍历模块树并分发权重这件事,本应是模块体系自身的能力,而不应由一个外部工具类来承担。

5.1.2 理想设计:vLLMModule 基类

引入一个继承自 nn.Module 的基类 vLLMModule,将 AutoWeightsLoader 的递归分发逻辑内化为基类的默认 load_weights 实现:

class vLLMModule(nn.Module):
    """vLLM 模块基类,提供递归权重加载的默认实现。"""

    def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
        self._maybe_materialize()           # ★ 加载前:物化 meta 参数
        weights = self._apply_fused_routing(weights)  # ★ 融合路由

        for child_prefix, child_weights in self._groupby_prefix(weights):
            if child_prefix in child_modules:
                child_module.load_weights(child_weights)   # 委托给子模块
            elif child_prefix in child_params:
                self._load_single_param(param, child_weights)  # 叶子参数

        self._maybe_post_process()          # ★ 加载后:量化后处理

    def _load_single_param(self, param, weights):
        """默认使用 copy_ 加载单个参数。"""
        param.data.copy_(weight_data)

    def _maybe_materialize(self):
        """如果直接参数在 meta device 上,物化到真实设备。
        非 Layerwise Reload 场景下 any() 检查立即返回 False,近乎零开销。"""
        # 遍历 named_parameters(recurse=False),对 meta 参数执行 materialize

    def _maybe_post_process(self):
        """Layerwise reload 模式下执行量化后处理。"""
        # 检查 _layerwise_reload 标志和 quant_method

    def _groupby_prefix(self, weights):
        """按权重名的第一段前缀分组,驱动递归分发。"""

5.1.3 改造后的效果

之前(双向依赖,防递归 hack):

Qwen3ForCausalLM.load_weights()
  └─ 创建 AutoWeightsLoader(self)          ← 外部工具类
       └─ if module != self.module:         ← 防递归 hack

之后(单向继承,自然递归):

Qwen3ForCausalLM(vLLMModule).load_weights()   ← 继承基类,无需 override
  └─ 基类递归分发 → child_module.load_weights() → 自然多态

关键改变:

  1. 1. 消除了 AutoWeightsLoader 外部工具类——递归分发是模块体系自身的能力
  2. 2. 消除了防递归 hack——基类的 load_weights 只递归调用子模块,不会调用自身
  3. 3. 顶层模型类变得极简——大多数不需要 override load_weights,直接继承基类即可;只有融合线性层、MoE 层等才 override

5.1.4 个性化定制能力

由于每个模块自己实现 load_weights,过滤等个性化逻辑自然有了归属:通用的 checkpoint key 过滤(如跳过 rotary_emb.inv_freq)可以在基类中统一处理,模型特有的过滤(如 skip_prefixes)由具体模块在自己的 load_weights 中处理。当前 AutoWeightsLoader 承担的 skip_prefixesskip_substrs 等职责都可以按此方式自然分解。

5.2 融合映射内聚到融合算子(对应缺陷 4.2)

5.2.1 问题回顾

如 4.2 节所述,融合层(MergedColumnParallelLinearQKVParallelLinear)将多个 checkpoint key 合并到一个参数中,但映射关系由每个模型文件通过 stacked_params_mapping 自行定义,导致多个模型文件重复声明几乎相同的映射表。

5.2.2 理想设计:融合层自己声明映射关系

融合算子知道自己由哪些子权重组成,应该由它自己声明这个信息。融合层通过 override load_weights 方法,在内部完成 checkpoint key 到 shard_id 的映射:

class MergedColumnParallelLinear(ColumnParallelLinear):
    def __init__(self, ..., shard_names: list[str]):
        self.shard_names = shard_names  # e.g. ["gate_proj", "up_proj"]

    def load_weights(self, weights):
        for name, loaded_weight in weights:
            # 从权重名推断 shard_id
            # e.g. "gate_proj.weight" → shard_id=0, param_suffix="weight"
            shard_id = self._infer_shard_id(name)
            if shard_id is not None:
                param.load_merged_column_weight(loaded_weight, shard_id=shard_id)
            else:
                param.load_column_parallel_weight(loaded_weight)

5.2.3 路由前的融合映射:递归调度层的融合路由

5.2.2 节中融合层接收的权重 key 仍是 checkpoint 原始名称(如 gate_proj.weight),但基类按 prefix 匹配子模块时只有 gate_up_proj,没有 gate_proj,路由会失败。

解决方案:基类的递归调度逻辑在路由前,自动扫描子模块的 shard_names 属性,构建融合路由表——当 checkpoint key 的前缀(如 gate_proj)命中路由表时,直接将该权重路由到对应的融合子模块(如 gate_up_proj)。调度归调度,处理归处理——路由逻辑留在递归调度层,融合层只负责接收权重并处理 shard_id。

路由流程(以 MLP 为例):

MLP.load_weights(weights)
  │  _build_fused_routing() → {"gate_proj": "gate_up_proj", "up_proj": "gate_up_proj"}
  │
  │  路由阶段(按融合路由表分发):
  │    "gate_proj.weight" → 命中路由表 → 路由到 gate_up_proj
  │    "up_proj.weight"   → 命中路由表 → 路由到 gate_up_proj
  │    "down_proj.weight" → 未命中     → 正常 prefix 匹配
  │
  └─ gate_up_proj → MergedColumnParallelLinear.load_weights → 推断 shard_id
     down_proj    → RowParallelLinear.load_weights

融合路由表由基类自动从子模块的 shard_names 构建,零硬编码;没有融合子模块的普通模块则零开销跳过。

5.2.4 改造后的效果

之前(映射散落在模型层):

# 每个模型文件都要重复定义
stacked_params_mapping = [
    ("qkv_proj", "q_proj", "q"), ("qkv_proj", "k_proj", "k"), ...
    ("gate_up_proj", "gate_proj", 0), ("gate_up_proj", "up_proj", 1),
]
# 模型层手动遍历、替换 key、注入 shard_id
for param_name, weight_name, shard_id in stacked_params_mapping:
    name = name.replace(weight_name, param_name)
    param.weight_loader(param, loaded_weight, shard_id)

之后(映射内聚于融合算子,路由由基类递归调度层自动完成):

# 模型层不再需要 stacked_params_mapping
# 基类递归调度层自动扫描子模块的 shard_names,构建融合路由表
# checkpoint key 通过融合路由表路由到融合层
# 融合层自己根据 shard_names 推断 shard_id
# Attention/MLP 等上层模块无需 override load_weights

关键改变:

  1. 1. 消除了模型文件中的 stacked_params_mapping 重复定义——映射关系由融合层自己声明
  2. 2. shard_id 不再是外部注入的——融合层根据权重名称自行推断
  3. 3. 路由问题由基类递归调度层自动解决——基类扫描子模块的 shard_names 构建融合路由表,自动将 checkpoint key 路由到正确的融合子模块,上层模块无需任何额外代码

5.3 消除 nn.Parameter 上的 weight_loader(对应缺陷 4.3)

5.3.1 问题回顾

如 4.3 节所述,nn.Parameter 本质上是纯粹的数据容器,但 vLLM 通过 setattr 将 weight_loader 等动态属性挂载到原生 nn.Parameter 上,绕过了类型系统。这导致了三个层面的问题:类型不安全(4.3.1)、v1/v2 版本分裂(4.3.2)、meta 物化依赖 __class__ hack(4.3.3)。

5.3.2 理想设计:定制逻辑由参数的拥有者实现

核心原则:nn.Parameter 不应有绕过类型系统的动态属性。如果某个参数需要定制的加载逻辑,由它的拥有者(持有该参数的 nn.Module 派生类)通过实现 load_weights 来完成。

这与 5.1 节引入的 vLLMModule 基类自然配合——基类提供默认的递归分发和简单 copy_ 加载,子类通过 override load_weights 来实现定制逻辑:

线性层的 load_weights(示意):

class ColumnParallelLinear(vLLMModule):
    def load_weights(self, weights):
        for name, loaded_weight in weights:
            param = params[name]
            if isinstance(param, BasevLLMParameter):
                param.load_column_parallel_weight(loaded_weight)  # 参数自服务分片
            else:
                # 原生 nn.Parameter,由模块处理 TP 分片(narrow + copy_)

其他需要定制加载的模块也同理,它们各自 override load_weights,在其中实现自己的加载逻辑,而不是把 weight_loader 挂到参数上。

5.3.3 消除动态属性的全景

消除动态属性后,原生 nn.Parameter 上通过 setattr 挂载的 weight_loader 等属性全部移除,这些职责转移到拥有者模块的 load_weights 中。原生 nn.Parameter 回归纯粹的数据容器(__dict__ 为空)。BasevLLMParameter 子类的 _output_dim_input_dimtp_rank 等分片元数据保留——这些是参数自身的固有属性,通过正式的构造函数和 @property 定义,与被消除的 weight_loader(外部调度逻辑被"挂载"到参数上)性质不同。

5.3.4 连锁收益:v1/v2 版本分裂自然消失

当所有模块都通过 load_weights 来调度权重加载后,v1(外部函数手动 narrow + copy_)和 v2(BasevLLMParameter 子类方法被挂载到参数上)都不再需要——模块的 load_weights 直接调用 param.load_column_parallel_weight() 等自服务方法,WEIGHT_LOADER_V2_SUPPORTED 白名单自然消失。

5.3.5 连锁收益:meta 物化简化

原生 nn.Parameter 上的动态属性被消除后,__class__ 赋值和 __dict__ 拷贝两个 hack 都不再需要。原生 nn.Parameter 的 __dict__ 为空,直接走 nn.Parameter(real_data) 构造器即可;BasevLLMParameter 子类通过新增的 materialize_on 方法(在真实设备上走 __new__ 创建实例,跳过 __init__ 的副作用)正规地构造并继承分片元数据。物化逻辑从 hack 变为正规的构造器调用。

5.3.6 连锁收益:物化逻辑内置于递归流程,统一所有加载场景

如 5.1.2 节基类骨架代码所示,load_weights 在递归分发之前调用 _maybe_materialize(),在分发完成后调用 _maybe_post_process()。每个模块在自己的 load_weights 开头物化自己的直接参数(检查是否在 meta device 上),子模块的物化由子模块自己负责,递归天然保证了"先物化、再加载"的顺序。这使得正常加载、Layerwise Reload、Transformers Backend 三个场景走同一个入口和递归流程,内部根据参数状态自动分支——不需要外部编排器,不需要 weight_loader 拦截机制,非 Layerwise 场景下 _maybe_materialize() 的 any() 检查立即返回 False,近乎零开销。

5.4 改造路径总结

整个理想态改造贴着三个缺陷逐一展开,形成一个有机整体:

缺陷 4.1:AutoWeightsLoader 的防递归与双向依赖
  └── 改造 5.1:引入 vLLMModule 基类,递归分发内化为模块体系自身的能力
        └── 消除 AutoWeightsLoader 外部工具类
        └── 消除防递归 hack

缺陷 4.2:融合 key 映射散落在模型层
  └── 改造 5.2:融合层 override load_weights,自行声明映射关系
        └── 消除模型文件中的 stacked_params_mapping 重复定义
        └── shard_id 由融合层自行推断
        └── 融合路由由基类递归调度层自动完成(扫描 shard_names 构建路由表)

缺陷 4.3:nn.Parameter 承载了不属于自己的职责
  └── 改造 5.3:定制逻辑由参数的拥有者(nn.Module 派生类)实现 load_weights,代替 weight_loader
        ├── 消除原生 nn.Parameter 上的动态属性(weight_loader、output_dim 等)
        ├── 连锁:v1/v2 版本分裂自然消失
        ├── 连锁:meta 物化简化,消除 __class__ hack
        └── 连锁:物化逻辑内置于递归流程,统一正常加载与 Layerwise Reload

核心原则:权重加载的所有逻辑都由 nn.Module 派生类承担——基类提供递归分发的默认实现,子类通过 override load_weights 实现定制逻辑(TP 分片、融合映射等)。nn.Parameter 回归纯粹的数据容器,不承载任何加载调度逻辑。BasevLLMParameter 体系作为类型安全的参数子类,其自服务的分片能力(load_column_parallel_weight 等)是合理的设计,不在消除之列。物化逻辑作为递归流程的有机组成部分,使正常加载、Layerwise Reload、Transformers Backend 三个场景走同一个代码路径,消除了对 weight_loader 拦截机制的依赖。


6. 附录:SGLang 权重加载体系对比分析

06-comparison-vllm-vs-sglang

 SGLang 的权重加载体系直接源自 vLLM,两者在架构设计上高度一致:四阶段加载流程相同,nn.Parameter 动态挂载 weight_loaderstacked_params_mapping 散落在模型层、v1/v2 版本分裂等核心缺陷均存在。因此,前述理想态改造方案对 SGLang 同样有价值——且由于 SGLang 几乎不使用 AutoWeightsLoader(仅 transformers.py 1 个文件使用),43+ 个模型文件全部采用手动遍历权重,引入基类 load_weights(改造 5.1)是收益最大的一项改造,这些模型文件中的 load_weights 方法高度相似(每个 30~130 行),可大量减少重复代码。

与 vLLM 的一个显著差异在于 meta 设备的使用:SGLang 的主流路径(DefaultModelLoader)直接在 GPU 设备上创建模型,不涉及 meta 设备;meta 设备仅出现在两个非主流路径中。因此 SGLang 不存在 vLLM 的 __class__ hack 问题。LayeredModelLoader 使用 PyTorch 原生的 to_empty() 逐模块物化,并将权重填充委托给模型自身的 load_weights_to_module 方法,但目前仅 torch_native_llama.py 一个模型实现了该接口,且其逻辑与 load_weights 有重复。如果采用理想态的基类方案,可以统一正常加载和逐层加载的代码路径,消除这一额外接口负担。

 

本文所在:https://www.cnblogs.com/cswuyg/p/19852392


文章来源:https://www.cnblogs.com/cswuyg/p/19852392
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云