首页 > 基础资料 博客日记
vLLM 权重加载机制全解析:从挑战到理想架构
2026-04-11 18:00:02基础资料围观1次

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

在阅读 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.weight、k_proj.weight、v_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_proj、k_proj、v_proj。加载时如何做映射?
问题三:meta 设备初始化与延迟物化(materialize,即在真实设备上分配实际内存)
PyTorch 提供了一种特殊的 meta 设备(device="meta"):在 meta 设备上创建的张量只记录 shape、dtype、stride 等元信息,不分配任何实际内存。这对大模型非常有用——一个 500B 参数的模型如果直接在 GPU 上初始化空参数,仅参数本身就需要约 1000GB 显存(FP16),远超单卡容量。
vLLM 在在线量化和 Transformers Backend 等场景中使用 meta 设备来延迟分配内存。
问题:当参数位于 meta 设备时,不能直接 copy_ 数据到参数中(meta 张量没有实际存储)。权重加载如何处理这些"虚拟"参数?
带着这三个问题,我们来看 vLLM 的实际实现。
2. 权重加载体系概述

本节系统性地介绍 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.py(vllm/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.py(vllm/model_executor/models/qwen.py,Qwen-1)使用约 30 行手动遍历代码同时处理路由和融合,而后续的 qwen3.py(vllm/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. 匹配到
shard_name="q_proj",将 key 中的q_proj替换为qkv_proj,得到model.layers.0.self_attn.qkv_proj.weight - 2. 带上
shard_id="q"调用weight_loader(param, loaded_weight, shard_id="q") - 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.py(vllm/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. 默认
requires_grad=True:普通 Tensor 默认不参与梯度计算,而 Parameter 默认参与。这是它作为"可学习参数"的语义标识。 - 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. key 重命名:将 checkpoint key 映射为模型参数名
- 2. 融合映射:通过
stacked_params_mapping将分开的 checkpoint key(q_proj、k_proj、v_proj)映射到融合参数(qkv_proj),并注入shard_id - 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_loader、MergedColumnParallelLinear.weight_loader、QKVParallelLinear.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. 问题解答

本节回到第 1 章提出的三个问题,结合第 2 章介绍的体系,逐一给出 vLLM 的解决方案。
解答一:TP 下的权重切分与内存控制
切分机制:权重加载时,通过 narrow(截取)操作从完整权重中取出属于当前 rank 的切片,再 copy_ 到参数中。这个"截取"操作就是 weight_loader 的核心职责之一。
具体来说,ColumnParallelLinear.weight_loader(vllm/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, ...)
此时 ColumnParallelLinear、RowParallelLinear 等并行层会根据 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. 识别出
q_proj应该映射到qkv_proj的第 0 个分片(shard_id="q"); - 2. 将
q_proj的数据写入qkv_proj参数的对应区域; - 3. 对
k_proj、v_proj重复上述过程,分别写入各自的区域。
解答三:meta 设备初始化与延迟物化
vLLM 在两个场景中使用 meta 设备,并通过不同的物化策略来处理,下面以 Online Quantization(在线量化)为例:
当用户指定在线量化(如 FP8 per-tensor)时,模型加载的目标是:读取全精度 checkpoint → 在线量化为低精度 → 存储量化后的权重。如果先在 GPU 上分配全精度参数,再量化为 FP8,那么在量化完成前,GPU 上需要同时持有全精度权重和量化后权重,峰值内存翻倍。
为了解决这个问题,在线量化方法(如 Fp8OnlineLinearMethod,vllm/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. 权重加载时,先将 checkpoint 数据缓冲在 CPU 内存中,不立即写入参数(此时参数位于 meta 设备,无法写入)。具体来说,
online_process_loader拦截weight_loader调用,将调用参数(包含来自 checkpoint 迭代器的 CPU 张量引用)缓存到LayerReloadingInfo.loaded_weights列表中; - 2. 当一层的所有权重都缓冲完毕后,才物化(materialize)该层——在 GPU 上分配真实内存;
- 3. 将缓冲的权重加载到物化后的参数中;
- 4. 立即执行量化处理(
process_weights_after_loading),将全精度权重转为 FP8; - 5. 释放全精度权重,只保留量化后的结果。
这样,GPU 上同一时刻只需要持有一层的全精度权重,量化完成后立即释放,峰值显存大幅降低。
4. 设计缺陷分析

本节逐一分析 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 映射散落在模型层,而非内聚于融合算子
融合层(如 MergedColumnParallelLinear、QKVParallelLinear)将多个 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_loader、output_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_dim、tp_rank 等)是参数自身的固有属性,可以通过给基类添加 materialize_on 方法来正规处理(走 __new__ 而非 __init__,避免构造器副作用,同时继承分片元数据),同样不需要 __class__ hack。
5. 理想态设计

基于第 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. 消除了
AutoWeightsLoader外部工具类——递归分发是模块体系自身的能力 - 2. 消除了防递归 hack——基类的
load_weights只递归调用子模块,不会调用自身 - 3. 顶层模型类变得极简——大多数不需要 override
load_weights,直接继承基类即可;只有融合线性层、MoE 层等才 override
5.1.4 个性化定制能力
由于每个模块自己实现 load_weights,过滤等个性化逻辑自然有了归属:通用的 checkpoint key 过滤(如跳过 rotary_emb.inv_freq)可以在基类中统一处理,模型特有的过滤(如 skip_prefixes)由具体模块在自己的 load_weights 中处理。当前 AutoWeightsLoader 承担的 skip_prefixes、skip_substrs 等职责都可以按此方式自然分解。
5.2 融合映射内聚到融合算子(对应缺陷 4.2)
5.2.1 问题回顾
如 4.2 节所述,融合层(MergedColumnParallelLinear、QKVParallelLinear)将多个 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. 消除了模型文件中的
stacked_params_mapping重复定义——映射关系由融合层自己声明 - 2. shard_id 不再是外部注入的——融合层根据权重名称自行推断
- 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_dim、tp_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 权重加载体系对比分析

SGLang 的权重加载体系直接源自 vLLM,两者在架构设计上高度一致:四阶段加载流程相同,nn.Parameter 动态挂载 weight_loader、stacked_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 有重复。如果采用理想态的基类方案,可以统一正常加载和逐层加载的代码路径,消除这一额外接口负担。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签:

