大模型-大模型权重文件的加载2-63
这段代码的核心目标是高效、安全地将预训练好的大模型权重从硬盘加载到内存(或指定的计算设备)中,并正确地赋值给 nn.Module 模型实例的各个参数。vLLM 是一个为大语言模型(LLM)推理设计的、非常高效的库,因此它的权重加载逻辑也经过了特殊优化,以处理模型并行、量化等复杂情况。
下面我们将逐行、分块地进行详细分析。
整体逻辑概览
代码通过遍历指定路径下的所有 safetensors 文件,逐个读取文件中的权重张量(tensor),然后将其加载到模型对应的参数(parameter)中。它特殊处理了一种叫做 "packed modules" 的情况,这通常与量化或者其他模型优化技术有关。
Python
import glob
import os
from safetensors.torch import safe_open
from torch import nn
# 这是一个默认的权重加载器,以防参数没有自定义的加载器
def default_weight_loader(param, loaded_weight, shard_id=None):
# a simple default weight loader that copies the weight
param.data.copy_(loaded_weight)
def load_model(model: nn.Module, path: str):
# 1. 获取 packed_modules_mapping (如果存在)
packed_modules_mapping = getattr(model, "packed_modules_mapping", {})
# 2. 遍历所有 .safetensors 权重文件
for file in glob(os.path.join(path, "*.safetensors")):
# 3. 以安全模式打开权重文件
with safe_open(file, "pt", "cpu") as f:
# 4. 遍历文件中的每一个权重
for weight_name in f.keys():
is_packed = False
# 5. 检查是否为 "packed" 权重
for k in packed_modules_mapping:
if k in weight_name:
# 6. 处理 "packed" 权重
v, shard_id = packed_modules_mapping[k]
param_name = weight_name.replace(k, v)
param = model.get_parameter(param_name)
weight_loader = getattr(param, "weight_loader")
weight_loader(param, f.get_tensor(weight_name), shard_id)
is_packed = True
break
# 7. 处理普通权重
if not is_packed:
param = model.get_parameter(weight_name)
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, f.get_tensor(weight_name))
为了更清晰地解释,我在原始代码上做了一点小的重构(引入is_packed标志),使其逻辑更易于理解,但功能与原始的 for-else 循环完全等价。
详细代码解读
- safetensors 格式
首先,你需要了解 safetensors。这是一个由 Hugging Face 开发的安全、快速的张量存储格式。
安全 (Safe):它包含一个头部信息,描述了所有张量的元数据(名称、形状、数据类型)。在加载任何数据之前,会先校验这些信息,可以有效防止恶意代码执行(pickle 格式可能存在这个问题)。
快速 (Fast):它的设计允许直接内存映射(memory-mapping)张量,避免了额外的反序列化开销,加载速度非常快。
分片 (Sharding):大模型权重通常会被分割成多个较小的文件(例如,model-00001-of-00002.safetensors),以便于管理和加载。这段代码通过 glob 模式来处理所有这些分片。
2. load_model 函数签名
Python
def load_model(model: nn.Module, path: str):
model: nn.Module: 这是你要加载权重的目标模型对象。它是一个 PyTorch 的 nn.Module 或其子类的实例,并且已经被初始化好了(拥有正确的网络结构,但参数是随机的)。
path: str: 这是一个字符串,指向包含 .safetensors 权重文件的目录路径。
- packed_modules_mapping
Python
packed_modules_mapping = getattr(model, "packed_modules_mapping", {})
这是这段代码最关键、也最具 vLLM 特色的部分。
getattr(model, "packed_modules_mapping", {}): 这行代码尝试从 model 对象中获取一个名为 packed_modules_mapping 的属性。如果这个属性不存在,就返回一个空字典 {}。
什么是 "Packed Modules"? 在 vLLM 中,为了极致的推理性能,经常会对权重进行量化(例如,将 FP16 的权重转换为 INT8/INT4)。在量化过程中,多个原始的权重矩阵(如 Q、K、V 矩阵)可能会被合并(pack)成一个单一的、更大的量化后权重矩阵。这样做可以优化内存访问和计算效率。
packed_modules_mapping 的作用:这个字典就是用来描述这种“合并”关系的。
键 (key): 通常是合并后(packed)的权重在 safetensors 文件中的名称的一部分。例如,可能是 "qkv_proj"。
值 (value): 是一个元组 (v, shard_id)。
v: 对应模型中原始、未合并的参数名称的一部分。例如,可能是 "query_key_value"。
shard_id: 指示这个分片属于合并后大矩阵的哪个部分(例如,第0、1、2块分别对应Q、K、V)。
举个例子:
假设在文件中有一个权重叫 layers.0.attention.qkv_proj.weight。
packed_modules_mapping 可能包含这样的条目:{"qkv_proj": ("query_key_value", 0)}。
这段代码就会知道,qkv_proj 实际上是模型中 query_key_value 参数的一部分。
- 遍历与加载循环
Python
for file in glob(os.path.join(path, "*.safetensors")):
with safe_open(file, "pt", "cpu") as f:
for weight_name in f.keys():
...
glob(os.path.join(path, "*.safetensors")): 找到指定 path 目录下的所有以 .safetensors 结尾的文件。
safe_open(file, "pt", "cpu") as f: 打开一个权重文件。
"pt": 指定框架为 PyTorch。
"cpu": 指定先将权重加载到 CPU 内存中。这是一个稳妥的做法,可以避免直接加载到 GPU 导致显存溢出,之后再由 weight_loader 决定如何处理。
f.keys(): 获取当前 safetensors 文件中所有张量的名称列表。
for weight_name in f.keys(): 遍历每一个张量的名称。
5. 权重加载的两种路径
循环内部的 if-else 结构(在我的重构版本中是 if not is_packed)将权重加载分为两种情况:
情况一:处理 Packed 权重 (if-block)
Python
for k in packed_modules_mapping:
if k in weight_name:
v, shard_id = packed_modules_mapping[k]
param_name = weight_name.replace(k, v)
param = model.get_parameter(param_name)
weight_loader = getattr(param, "weight_loader")
weight_loader(param, f.get_tensor(weight_name), shard_id)
break
if k in weight_name: 检查 packed_modules_mapping 中的键(如 "qkv_proj")是否存在于当前从文件中读到的权重名称(如 layers.0.attention.qkv_proj.weight)中。
v, shard_id = packed_modules_mapping[k]: 如果匹配成功,就获取映射的目标名称和分片ID。
param_name = weight_name.replace(k, v): 将文件名中的 packed 名称替换为模型中的实际参数名称。例如,layers.0.attention.qkv_proj.weight 就被转换成了 layers.0.attention.query_key_value.weight。
param = model.get_parameter(param_name): 使用转换后的名称从 model 对象中获取对应的参数对象 (nn.Parameter)。
weight_loader = getattr(param, "weight_loader"): 这是另一个关键点。vLLM 为这些需要特殊处理的参数(如量化参数)附加了一个自定义的 weight_loader 方法。这个方法知道如何处理分片的、量化过的权重。它不会简单地进行数据复制。
weight_loader(param, f.get_tensor(weight_name), shard_id): 调用这个自定义加载器。
param: 目标参数。
f.get_tensor(weight_name): 从文件中实际读取张量数据。
shard_id: 告诉加载器,这个张量应该被加载到目标大参数的哪一个“分片”或区域。
情况二:处理普通权重 (else-block)
Python
else: # 在原始代码中,这是 for 循环正常结束后的 else 块
param = model.get_parameter(weight_name)
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, f.get_tensor(weight_name))
如果没有在 packed_modules_mapping 中找到任何匹配项,代码就认为这是一个普通的、未被合并的权重。
param = model.get_parameter(weight_name): 直接使用文件中的权重名获取模型参数。
weight_loader = getattr(param, "weight_loader", default_weight_loader): 尝试获取该参数的自定义 weight_loader。如果这个普通参数也没有自定义加载器,getattr 的第三个参数 default_weight_loader 就会作为默认值返回。
default_weight_loader 通常是一个简单的函数,其功能就是 param.data.copy_(loaded_weight),即将加载的权重数据直接复制到模型参数中。
weight_loader(param, f.get_tensor(weight_name)): 调用加载器(可能是自定义的,也可能是默认的)来加载权重。
总结
vLLM 的这段权重加载代码是一个高度工程化和优化的实现,其设计思想可以总结为以下几点:
安全性与速度:优先使用 safetensors 格式,保证了加载过程的安全和高效。
可扩展性与模块化:通过 weight_loader 属性,将权重加载的具体逻辑与加载流程本身解耦。这使得 vLLM 可以非常灵活地支持各种复杂的权重类型(如 FP16, BF16, INT8, INT4, GGUF 等),每种类型只需实现自己的 weight_loader 即可。
支持高级优化:通过 packed_modules_mapping 机制,优雅地解决了因量化等优化技术导致的权重“合并”问题,使得文件中的权重布局可以和模型在内存中的参数布局不一致。
鲁棒性:代码逻辑清晰地划分了“特殊权重”和“普通权重”的处理路径,并为普通权重提供了默认的加载方式,保证了代码的健壮性。
总而言之,这段代码不仅仅是简单的权重加载,它体现了 vLLM 作为一个高性能推理库在底层设计上的深思熟虑,旨在以统一、高效且可扩展的方式应对大模型时代的各种复杂场景。

浙公网安备 33010602011771号