4. 模型加载与初始化

模型加载,说白了就是把训练好的权重从硬盘搬到内存里,再放到 GPU 上。这一步看似简单,但坑不少。我见过不少同学在加载时卡住,要么 OOM,要么设备不匹配,要么加载完发现参数对不上。今天咱们就把这块彻底捋清楚。

4.1 PyTorch 模型加载:state_dict 与 torch.load

PyTorch 保存模型有两种常见方式。我个人习惯只保存 state_dict,也就是参数的字典。为什么?因为这样更灵活,模型结构可以单独定义,权重只是“填充物”。

核心区别:torch.save(model.state_dict(), "model.pt") 只存参数;torch.save(model, "model.pt") 连结构一起存。后者容易出兼容性问题,我建议你别用。

import torch
import torch.nn as nn

# 定义一个简单的模型
class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Linear(768, 1024)

    def forward(self, x):
        return self.fc(x)

model = MyModel()

# 保存 state_dict
torch.save(model.state_dict(), "my_model.pt")

# 加载时先实例化,再 load
model_loaded = MyModel()
state_dict = torch.load("my_model.pt", map_location="cpu")
model_loaded.load_state_dict(state_dict)

这里有个关键点:map_location。我曾经在项目里忘了加这个参数,结果模型在 GPU 0 上保存,加载时却跑到了 GPU 1,导致后续张量设备不一致,报错找了好久。所以,加载时明确指定 map_location 是个好习惯。

避坑指南:load_state_dict 要求 key 完全匹配。如果你改了模型结构(比如层名变了),加载会报 missing keys 或 unexpected keys。我曾经因为改了个 fc 为 classifier,结果加载失败,排查了半天。解决办法是用 strict=False 加载,然后手动处理不匹配的 key。

4.2 Hugging Face Transformers:from_pretrained 的魔法

实际做大模型推理,我们很少手写 torch.load。更多是用 Hugging Face 的 from_pretrained。这个接口封装了下载、缓存、加载、设备映射等一系列操作。

from transformers import AutoModelForCausalLM, AutoTokenizer

model_name = "meta-llama/Llama-2-7b-hf"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.float16,      # 半精度加载,省显存
    device_map="auto",              # 自动分配设备
    low_cpu_mem_usage=True          # 减少 CPU 内存占用
)

你想想看,device_map="auto" 到底做了什么?它会根据你的 GPU 显存大小,自动把模型的各层分配到不同设备上。如果显存不够,它会把部分层放到 CPU。这就是模型并行的雏形。

我的经验:low_cpu_mem_usage=True 这个参数一定要加。不加的话,加载 7B 模型可能会吃掉 30GB 以上的 CPU 内存,因为 PyTorch 会先加载 float32 版本再转换。加了之后,直接加载 float16,CPU 内存占用能降一半。

4.3 模型并行基础:单卡放不下怎么办?

大模型动辄几十 GB,单张 GPU 根本放不下。这时候就需要模型并行。说白了,就是把模型切成几块,每块放在不同的 GPU 上。

我画了一张图,帮你理解模型并行的核心逻辑:

模型并行示意图 模型并行(Model Parallelism)核心流程 输入 GPU 0:Embedding + Layer 1-8 显存占用:~8GB GPU 1:Layer 9-16 显存占用:~7GB GPU 2:Layer 17-24 显存占用:~7GB GPU 3:Layer 25-32 + LM Head 显存占用:~8GB 输出 说明:每张 GPU 只存放模型的一部分,前向传播时依次计算,中间激活通过 PCIe 传输。 这种方式叫“流水线并行”,是模型并行的一种常见实现。

上图展示的是流水线并行(Pipeline Parallelism)。输入先经过 GPU 0,算完把中间结果传给 GPU 1,依次类推。每张卡只存一部分参数,这样就能跑更大的模型。

关键点:模型并行不是免费的。卡间通信有开销,尤其是当模型层数很多时,传输中间激活会占用大量带宽。所以实际部署时,我们会尽量让每张卡的计算量均衡,减少通信次数。

4.4 设备映射:CPU/GPU 的协同

设备映射,就是决定模型的哪部分放在哪个设备上。Hugging Face 的 device_map 参数帮我们做了这件事。但如果你想手动控制,可以用 PyTorch 的 to() 方法。

# 手动设备映射示例
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype=torch.float16)

# 把 embedding 层放到 GPU 0
model.model.embed_tokens = model.model.embed_tokens.to("cuda:0")

# 把前 16 层放到 GPU 0
for i in range(16):
    model.model.layers[i] = model.model.layers[i].to("cuda:0")

# 把后 16 层放到 GPU 1
for i in range(16, 32):
    model.model.layers[i] = model.model.layers[i].to("cuda:1")

# 把 lm_head 放到 GPU 1
model.lm_head = model.lm_head.to("cuda:1")

手动映射的好处是精细控制,但容易出错。我曾经手写映射时,漏掉了某个 layer norm,结果推理时设备不匹配,报错信息还特别隐晦。所以,除非你有特殊需求,否则我建议直接用 device_map="auto"。

注意:device_map="auto" 不是万能的。它基于显存大小做贪心分配,但不会考虑通信拓扑。比如,如果你有 4 张卡通过 NVLink 连接,而另外 4 张卡通过 PCIe 连接,自动分配可能把相邻层放到跨 PCIe 的卡上,导致通信变慢。这时候,手动映射反而更好。

4.5 加载后的初始化检查

模型加载完,别急着推理。先做几个检查,能省去后面很多调试时间。

检查项 方法 常见问题
参数设备 next(model.parameters()).device 参数还在 CPU 上,推理极慢
参数 dtype next(model.parameters()).dtype 误加载为 float32,显存翻倍
参数数量 sum(p.numel() for p in model.parameters()) 加载了错误的 checkpoint
前向测试 用 dummy input 跑一次 model() shape 不匹配、NaN 输出

我的习惯:加载完模型后,我会先打印模型结构的前 3 层和后 3 层,确认层名和参数 shape 跟预期一致。这一步虽然简单,但能避免很多“模型加载了但推理结果不对”的坑。

4.6 总结:加载流程一览

最后,我把整个模型加载与初始化的流程总结成一张图,方便你对照操作:

模型加载与初始化流程 模型加载与初始化完整流程 1. 选择模型 确定模型名称/路径 2. 设置 dtype float16 / bfloat16 3. 设备映射 device_map="auto" 4. 加载 from_pretrained 5. 初始化检查 设备 / dtype / 参数数量 / 前向测试 6. 准备推理 model.eval() + torch.no_grad()

嗯,到这里,模型加载与初始化的核心内容就讲完了。记住:加载不是简单的 torch.load,而是一个涉及设备、精度、并行策略的系统工程。每一步都踩过坑,才能做到心里有数。