1. 从零搭PyTorch环境:版本漂移比模型本身还难调
上周帮一位做机器狗控制的朋友调LoRA训练脚本,他刚装完PyTorch,兴致勃勃准备微调一个7B模型,结果第一步就卡住了——import torch没问题,但torch.cuda.is_available()一直返回False。折腾了一整晚,最后发现是 pip 默认给他装了CPU版本的 torch,白高兴一场。这类事情在PyTorch生态里太常见了。
很多人对PyTorch这条技术栈的认知是:装个库、下个模型、跑个训练,三步搞定。但实际上,pytorch、torch这两个词在不同语境下指代的东西完全不同——有时是框架本身,有时是底层张量库,有时特指某一版本的CUDA构建。而 Hugging Face、PEFT、LoRA 这三个工具,又全部依赖PyTorch这个地基。地基没打稳,上面全是空中楼阁。
1.1 Python与PyTorch版本的对应关系:别让解释器成为第一道坎
先说最基础的版本对应。PyTorch每个正式版本发布时,都会明确声明支持的Python版本范围。选错Python版本,轻则装不上,重则装上了但某些算子行为异常。以我用过的几个版本为例:
| PyTorch版本 | 支持Python版本 | 对应CUDA版本建议 |
|---|---|---|
| 1.8.x | 3.6 - 3.9 | CUDA 10.2 / 11.1 |
| 1.10.x | 3.6 - 3.9 | CUDA 10.2 / 11.3 |
| 1.13.x | 3.7 - 3.10 | CUDA 11.6 / 11.7 |
| 2.0.x | 3.8 - 3.11 | CUDA 11.7 / 11.8 |
| 2.1.x | 3.8 - 3.11 | CUDA 11.8 / 12.1 |
| 2.2.x | 3.8 - 3.12 | CUDA 11.8 / 12.1 |
| 2.3.x | 3.8 - 3.12 | CUDA 11.8 / 12.1 |
| 2.4.x | 3.8 - 3.12 | CUDA 12.1 / 12.4 |
这里有个有意思的细节:PyTorch 2.x 系列其实对Python版本已经很宽容了,3.8到3.12都能跑。但很多国内教程还在推荐Python 3.8,因为早期一些依赖库(比如bitsandbytes)在旧版本上兼容性最好。我的建议是:如果你要跑Hugging Face全家桶,选Python 3.10是最稳的,既不会有老库不兼容的问题,也不会有新库强制要求更高版本的风险。
torch和pytorch在 pip 世界里其实是同一个包。有些人会遇到pip install pytorch装了个莫名其妙的东西——因为PyPI上确实存在一个叫pytorch的废弃包,那是2016年左右的遗留物。正确写法永远是pip install torch。如果你在环境里同时看到了torch和pytorch两个包,赶紧把后者卸了,它只会给你的import制造混乱。
1.2 镜像源与安装命令:一条命令写清楚,少走三小时弯路
安装PyTorch最标准的姿势是去官网生成安装命令,但在国内网络环境下,官网源偶尔抽风。我常用的做法是使用清华源或阿里源,加上--extra-index-url指定CUDA版本的下载地址。
比如装CUDA 12.1版本的PyTorch 2.3.x:
pip install torch==2.3.0 torchvision==0.18.0 torchaudio==2.3.0 \ --index-url https://mirrors.aliyun.com/pytorch-wheels/cu121/ \ --extra-index-url https://pypi.tuna.tsinghua.edu.cn/simple有几个容易踩的坑需要单独说明:
- 如果使用
--index-url指向PyTorch官方wheel源,那这个源里不包含numpy、transformers这类普通PyPI包,所以必须加--extra-index-url补一个通用源。 - 网上很多教程写的
pip3 install torch==1.8.2 torchvision==0.9.2 torchaudio==0.8.2 --extra-index-url https://download.pytorch.org/whl/cu111,这条命令本身没问题,但它默认走的是官方下载地址,国内直连容易超时。加了镜像源之后,同样的版本能快十几倍。 - 检查是否装成了CPU版本,用
pip list | grep torch看版本号后面的构建标识,比如2.3.0+cu121带CUDA标识,2.3.0+cpu就是CPU版。
1.3 Conda环境与WSL:Windows用户的两个常用选择
凡是在Windows上跑PyTorch的人,迟早要面对一个问题:是直接用Windows原生Python,还是用WSL。
我的经验是:如果你只是做推理、写脚本,Windows原生环境完全够用。但如果你要跑训练,尤其需要高显存、长时间运行的任务,WSL表现得更稳定。原因其实不复杂——wsl里没有Windows那套图形栈的额外开销,CUDA上下文管理更干净,而且很多Hugging Face的扩展库在Linux下的兼容问题少得多。
用Anaconda创建独立环境是防止环境错乱最有效的手段:
conda create -n lora python=3.10 -y conda activate lora # 安装PyTorch(同上) conda install cudatoolkit=11.8 -c conda-forge # 如果走conda安装CUDA组件有AMD显卡(比如 7900XTX)的朋友注意了,你的情况特殊:PyTorch在Windows原生环境下的ROCm支持不如Linux。我实测下来,7900XTX 在 WSL 里装 ROCm 版本的 PyTorch 能跑通大部分训练任务,但在原生Windows下经常碰到算子不兼容。所以如果你是用A卡做深度学习,WSL是性价比最高的方案。
1.4 装完怎么确认环境真的能用
安装完成后的第一件事,打开Python终端验证:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "CPU mode")如果is_available()返回False,顺序排查三件事:
nvidia-smi能看到显卡但返回异常——检查驱动版本,PyTorch的CUDA版本不能比驱动支持的CUDA版本新太多。torch.__version__带+cpu后缀——装错包了,重装。- 还有种隐蔽情况:conda环境里存在多个
torch,系统加载了旧版本。用python -c "import torch; print(torch.__file__)"看看实际加载的是哪个路径。
网上搜“绘世启动器显示pytorch不支持设备”这类问题,十有八九就是上述某个情况。这个报错其实是个笼统的提示,真正的原因只有在逐个排查后才能定位到。
2. Hugging Face:模型仓库与模型加载的正确打开方式
把PyTorch环境装好之后,下一个要打通的环节是Hugging Face。在我刚接触这条技术栈时,一直没想明白一个问题:为什么大家都围绕Hugging Face转?后来用多了才明白,它不是一个简单的模型下载站,而是一整套模型加载、数据预处理、训练评估的标准协议。
2.1 Transformer技术栈里,各个库的分工
transformers是整个Hugging Face生态的入口,统一了绝大多数预训练模型的加载与使用方式。比如你把AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct")这个API跑通,之后换任何模型都只需改一个名字。这种统一抽象在工程上的价值怎么强调都不为过。
在这套生态里,几个核心库的分工是:
transformers:模型架构、预训练权重加载、pipeline推理封装。datasets:数据集的下载、缓存、批处理;支持内存映射,超大数据集也不会撑爆内存。tokenizers:高性能分词器,BPE和WordPiece的底层实现。accelerate:分布式训练的调度层,单机多卡、多机多卡都靠它。peft:参数高效微调工具集,LoRA、Prefix Tuning这些都在这里。
实际使用中,这五者几乎总是同时出现。形象点说:transformers是发动机,datasets是油箱,tokenizers是进气系统,accelerate是变速箱,peft是外挂的涡轮增压。
2.2 国内加载模型的四种方案对比
直接访问Hugging Face官网在国内不太稳定,所以需要适配方案。我用了很久之后总结出四种可行路线:
| 方案 | 实现方式 | 适用场景 | 速度 |
|---|---|---|---|
| 镜像站 | 设置环境变量HF_ENDPOINT=https://hf-mirror.com | 日常开发、反复下载模型 | 较快 |
| 命令行下载 | huggingface-cli download 模型名配合镜像 | 一次性拉取大模型 | 稳定 |
| ModelScope下载 | 从魔搭下载后用本地路径加载 | 官方直连受限且需完整权重 | 取决于网络 |
| 手动搬运 | git clone + lfs 拉取,再指向本地目录 | 离线环境 / 内网部署 | 不适用 |
最省心的当属配置环境变量的方式:
export HF_ENDPOINT=https://hf-mirror.com设好之后,所有from_pretrained调用都会自动走镜像,无感切换。不过要提醒一下,镜像站同步有时差,最新发布的小众模型可能暂时拉不到,这时候就得去ModelScope试试。
huggingface-cli download适合那种需要断点续传的场景,因为它是HTTP多线程下载,比from_pretrained不知道稳到哪里去了。下载完成后,它会返回一个本地缓存路径,把这个路径交给后续的from_pretrained就行。
2.3 模型缓存机制与硬盘清理
Hugging Face的缓存机制在解决重复下载问题的同时,也制造了新的隐患。默认情况下,模型会缓存在~/.cache/huggingface/hub,每个模型拆成多个文件存成blob格式。问题在于:同一个模型在不同时间下载的不同版本,会存成不同的blob,但这些blob之间可能存在大量重复数据。跑几次实验后,你可能会惊讶地发现磁盘被几十GB的缓存占满。
两个实用的处理手段:
# 查看缓存占用量 du -sh ~/.cache/huggingface/hub/* # 清理未引用的缓存 huggingface-cli scan-cache huggingface-cli delete-cache另外,在离线机器上加载模型时,记得设置这两个环境变量,否则程序会反复尝试连接远端超时:
export HF_HUB_OFFLINE=1 export TRANSFORMERS_OFFLINE=13. LoRA不是玄学:低秩适配的完整拆解
在开始跑微调之前,必须先搞清楚LoRA到底在做什么。很多人一上来就pip install peft、写几行LoraConfig,跑通了也不知道为什么能跑通,遇到rank该设多少、alpha该设多少就懵了。这部分我尽量用最通俗的方式讲透。
3.1 微调的本质与全参微调的成本
模型微调的本质是:在预训练权重的基础上,用特定领域的数据继续更新参数,让模型学会新技能。全参微调(Full Fine-tuning)的做法是,把所有层的所有参数都放进梯度计算图里更新。这个做法有两个致命问题:
- 存储成本:一个7B参数的模型,用FP16存权重就要14GB,而优化器状态(AdamW要存一阶矩和二阶矩)又要翻几倍。训练时显存里要同时放下模型权重、梯度、优化器状态、激活值,合计轻松超过50GB。
- 灾难性遗忘:全部参数都动,模型很容易在适配新任务的同时丢失原有的通用能力。
用一个不太精确但很好懂的类比:全参微调相当于把整家餐厅的厨师全部换掉重新培训,LoRA则是在原有主厨不动的情况下,给每个关键岗位配一个“辅助助手”,这个助手只有几个人的小团队,但在特定菜品上能帮主厨做出正确的调味决策。
3.2 LoRA的核心数学直觉
LoRA(Low-Rank Adaptation)的理论基础来自一个观察:模型在微调过程中产生的权重变化矩阵ΔW,其内在秩(intrinsic rank)通常很低。也就是说,ΔW虽然没有必要,但可以被两个更小的矩阵近似表达。
具体做法是:冻结原始权重W,引入两个可训练的小矩阵A和B。记输入为x,原本的前向计算是Wx,LoRA将其修改为Wx + BAx。A把输入投影到低秩空间,B把低秩空间投影回原始维度。训练时只更新A和B。
参数量对比一目了然:一个4096 × 4096的权重矩阵,全量更新要训练约1670万个参数;用rank=16的LoRA,只需要训练4096×16 + 16×4096 ≈ 13万个参数,直接少了两个数量级。
3.3 PEFT库的LoraConfig参数逐个解读
peft库把上述思路封装成了几行代码,但每个参数都值得细抠:
from peft import LoraConfig, get_peft_model, TaskType config = LoraConfig( r=8, # 低秩矩阵的秩,越大表达能力越强,显存占用越高 lora_alpha=32, # 缩放系数,实际缩放比例是 alpha / r target_modules=["q_proj", "v_proj", "k_proj", "o_proj"], # 要注入LoRA的模块 lora_dropout=0.05, # 防止小矩阵过拟合 bias="none", # 是否训练偏置项,一般保持"none" task_type=TaskType.CAUSAL_LM # 任务类型,告诉PEFT模型结构 )对新手最容易困惑的是r和lora_alpha的关系。实际生效的缩放因子是lora_alpha / r,不是lora_alpha本身。比如r=8, lora_alpha=32,缩放因子是4。这个缩放因子存在的意义是:增大r时,如果不相应调整alpha,会改变前向传播的数值尺度,影响训练的稳定性。
选择target_modules也有一些经验:如果资源充裕,四个注意力投影矩阵都加上;如果显存紧张,只加q_proj和v_proj也能看到效果,但收敛速度会慢一些。MLP层一般不加,收益不明显。对于Qwen系列,使用from_pretrained加载后可以用model.named_modules()查看所有模块名,再决定注入哪些。
3.4 LoRA的边界:什么时候不该用
LoRA并非万能。如果数据量极大(比如超过几十万条),或者任务模态极其丰富,那么低秩表达的假设可能不成立,这时全参微调或更大的r才是正解。
另一个陷阱是:r设得太大,LoRA省显存的意义就消失了。r=256的LoRA参数量已经接近全量微调的五分之一,不如直接全量微调。
4. 跑一次真正能用的LoRA微调:以Qwen为例
理论讲了一堆,最后总要落地。这里以 Qwen2.5-7B 为例,走一遍完整的LoRA微调流程。这个模型有中文能力,且通过与AutoModelForCausalLM标准接口兼容,拿来做演示再合适不过。
4.1 任务选择与数据准备:找一个性价比最高的入口
如果你想用最小的代价体验一套流程,我建议做“通用指令跟随”微调而不是“垂直领域问答”。通用指令数据容易获取,效果验证也更直接——训练完随便问两句就能感觉到变化。
数据格式按Hugging Face的标准对话结构来:
[ { "instruction": "请你介绍一下量子计算的基本原理", "output": "量子计算基于量子力学原理...(省略具体内容)" }, { "instruction": "用一句话解释什么是梯度下降", "output": "梯度下降是一种迭代优化算法,通过计算损失函数对参数的梯度并沿负梯度方向更新参数来最小化目标函数。" } ]加载并转成训练格式的代码:
from datasets import Dataset data = [ {"instruction": inst, "output": out} for inst, out in zip(train_data["instruction"], train_data["output"]) ] dataset = Dataset.from_list(data) def format_func(example): text = f"<|im_start|>user\n{example['instruction']}<|im_end|>\n<|im_start|>assistant\n{example['output']}<|im_end|>" return {"text": text} dataset = dataset.map(format_func, remove_columns=dataset.column_names)这里用到的<|im_start|>是Qwen系列的Chat格式标记,必须跟模型tokenizer统一。如果你不按这个格式写,模型能训练但推理时会以意想不到的方式生成回复。
4.2 tokenizer细节:最容易忽略的翻车点
加载模型和分词器时,有几个细节决定训练成败:
from transformers import AutoModelForCausalLM, AutoTokenizer import torch model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen2.5-7B-Instruct", torch_dtype=torch.bfloat16, device_map="auto", use_cache=False, # 训练时必须关闭KV缓存 ) tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct", trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token # Qwen的pad_token默认是None,必须手动指定pad_token必须设,否则collator在批处理时会报错。另一个坑是padding_side:对Causal LM来说,左填充(padding_side="left")通常更好,因为右填充会让pad token出现在序列末尾,影响生成的连续性。
4.3 训练配置与实际训练
from transformers import TrainingArguments, Trainer from peft import LoraConfig, get_peft_model lora_config = LoraConfig( r=8, lora_alpha=32, target_modules=["q_proj", "v_proj"], lora_dropout=0.05, ) model = get_peft_model(model, lora_config) training_args = TrainingArguments( output_dir="./qwen_lora", # 模型保存路径 per_device_train_batch_size=2, gradient_accumulation_steps=8, num_train_epochs=3, learning_rate=2e-4, fp16=False, bf16=True, # 有条件用bf16,数值更稳定 logging_steps=10, save_steps=500, save_total_limit=2, remove_unused_columns=False, ) trainer = Trainer( model=model, args=training_args, train_dataset=dataset, tokenizer=tokenizer, data_collator=lambda x: { "input_ids": torch.stack([torch.tensor(v["input_ids"]) for v in x]), "attention_mask": torch.stack([torch.tensor(v["attention_mask"]) for v in x]), "labels": torch.stack([torch.tensor(v["input_ids"]) for v in x]), # labels与input_ids一致 }, ) trainer.train()几个参数说明一下:
per_device_train_batch_size=2在7B模型上已经比较激进了,显存紧张就降到1。gradient_accumulation_steps=8的意思是:攒8个小批次再更新一次参数,效果相当于batch_size=16,但显存只需要单批量的量。remove_unused_columns=False必须设,因为自定义的数据列text在Trainer眼里是“未使用列”,默认会移除,把数据格式破坏掉。- 如果你用
4bit量化加载,把bitsandbytes的BnbConfig配置好,显存还能再少一半。
4.4 推理、保存与导出
训练完成后,PEFT生成的目录里只有Adapter权重(也就是A和B两个小矩阵),不是完整模型。加载和合并的流程:
from peft import PeftModel # 先加载基础模型 base_model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen2.5-7B-Instruct", torch_dtype=torch.bfloat16, device_map="auto" ) # 再把LoRA adapter叠加进去 model = PeftModel.from_pretrained(base_model, "./qwen_lora/checkpoint-1000") # 合并权重,导出完整模型 merged_model = model.merge_and_unload() merged_model.save_pretrained("./qwen_merged") tokenizer.save_pretrained("./qwen_merged")合并完的模型在后续推理、部署、导出ONNX时行为与普通模型完全一致。提到pytorch转onnx,这个操作简单但很考验输入格式,需要准备一个假的input_ids让模型跑一遍前向图。
另外提一个很多人问过的问题:“pytorch返回实例的类对象名称”。在推理脚本里想确认当前加载模型的类型,用type(model).__name__就好。如果想看更动态的类名,比如PeftModel觉得不够精确,可以深入model.base_model.model拿底层的LlamaForCausalLM。调试时多打印这类信息,能帮你快速判断是不是在正确的模型结构上做了操作。
5. 爆显存自救手册:从OOM日志到逐项优化
“LoRA训练爆显存”是热搜区的高频词。尤其是minimaxh3这类偏大模型的项目,加速框架甚至比微调本身更吃显存。这节把显存问题掰开揉碎讲清楚。
5.1 显存的四个去向
训练时显存占用有四块:模型权重、梯度、优化器状态、激活值。以7B模型为例,全参微调的账单大致如下:
| 项目 | FP16模型 | 说明 |
|---|---|---|
| 权重 | 14GB | 7B × 2字节 |
| 梯度 | 14GB | 权重同尺寸 |
| AdamW状态 | 28GB | 一阶矩+二阶矩,每项跟权重同尺寸 |
| 激活值 | 因batch而异 | 单样本forward可能也要5-10GB |
合计轻松超过60GB。这就是为什么24GB的显卡几乎无法全参微调7B模型。
LoRA的省钱逻辑是:冻结原权重,不但权重本身不动,梯度也不需要为它计算和存储;优化器状态只更新A、B两个小矩阵。权重14GB、双卡各半,适配器部分可能不到200MB。所以LoRA理论上是“穷人”的福音。
5.2 按性价比排序的优化手段
按实施难度和收益排序,我的建议顺序是:
- 缩小batch size + 梯度累积。把batch从4降到1,显存直接省下一大块,再用
gradient_accumulation_steps补回来。零成本,是最霸道的优化。 - gradient checkpointing。设置
model.gradient_checkpointing_enable(),用“计算换显存”,前向传播时丢弃部分激活值,反向传播时重新计算。通常能省掉40-60%的激活值占用,代价是训练时间增加约20%。 - 混合精度。FP16一般够用,BF16更好,显存立减一半。如果模型有数值敏感层,加
torch.cuda.amp.GradScaler处理。 - LoRA只注入必要模块。模块越少,训练参数越少,优化器状态占用的显存越少。先用
q_proj+v_proj起步,不够再扩。 - 4bit量化加载。配置
bitsandbytes把基础模型量化为4bit,7B模型权重降到约3.5GB,这是极限省法。
5.3 一次暴显存问题的完整排查链路
朋友那台机器,24GB A5000,加载Qwen2.5-7B直接OOM。完整排查过程如下。
第一步看日志。报错集中在:
torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 512.00 MiB (GPU 0; 23.7 GiB total capacity; 22.1 GiB already allocated; ...)注意最后一行的“already allocated”高达22.1GB,说明显存基本上满了。模型用FP16加载需要14GB,不应该直接满。怀疑tokenizer在数据侧制造了过长的序列。一查,训练数据里有条样本被拼到了3000多个token,一个batch塞进去,激活值瞬间爆表。
处理办法:把所有data_collator里的输入做截断,max_length=1024。同时在加载阶段完成两件事:开启gradient_checkpointing,把基础模型换成了4bit加载。结果:峰值占用从23.7GB降到13GB,训练时间增加了约15%,但能跑了。
5.4 一个关键误解:显存不够≠模型跑不起来
很多人以为显存只有16GB就不能跑7B模型,这是一个典型的误区。4bit加载 + LoRA + 梯度累积 + gradient checkpointing,这套组合拳在16GB显卡上是完全可行的。步骤是:
pip install bitsandbytesfrom transformers import BitsAndBytesConfig bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", ) model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen2.5-7B-Instruct", quantization_config=bnb_config, device_map="auto", )注意:量化加载之后,LoRA注入时target_modules依然能正常工作,因为PEFT会自动适配量化层。
6. 那些反复出现的报错,值得单独记一笔
最后集中梳理几个出现频率极高的报错场景,如果你是从零开始搭这套环境,这些大概率会碰到。
6.1RuntimeError: CUDA error: no kernel image is available for execution on the device
这个报错的字面意思是:当前PyTorch编译时用的CUDA版本,和你的显卡驱动支持的CUDA版本不兼容。最常见于新显卡 + 旧PyTorch的组合。
例如 RTX 4090 用 PyTorch 1.8 时候,就是经典的“no kernel image”。解法只有一条:升级到支持对应显卡架构的PyTorch版本。RTX 30系要CUDA 11.1+,RTX 40系要CUDA 11.8+,这个信息在你的显卡规格文档里都有。
6.2FileNotFoundError: Couldn't find 'tokenizer.json'
这是把Hugging Face模型下到一半或者只下了单个文件导致的。很多人在from_pretrained之前会单独下载某个文件放到本地目录,结果后续加载时还是找不到配套文件。正确做法是:用工具把整个模型仓库拉全。本地路径文件夹里至少要包含config.json、tokenizer.json、tokenizer_config.json、model.safetensors(或pytorch_model.bin),缺一不可。
6.3torch.load的map_location:单卡模型加载到多卡环境的坑
如果你保存模型时用了model.state_dict(),然后加载时写成:
torch.load("model.pt")在多卡环境会报Expected all tensors to be on the same device。因为保存时tensor在cuda:0或cuda:1,加载时去了别的卡。标准解法:
torch.load("model.pt", map_location="cuda:0") # 或 map_location="cpu"这条经验看似琐碎,但在生产环境里真的能把人逼疯。我见过同事花了半天排查“为什么训练好好的,一加载就报设备错误”,最后就是map_location没写。
6.4RuntimeError: CUDA error: device-side assert triggered
这是最隐蔽的一类报错,因为出错的代码往往不在报错点附近。常见原因是标签(labels)里出现了超出vocab_size范围的索引,比如使用自定义tokenizer时,把pad_token_id设成了很大但模型词汇表却没覆盖那个ID。
排查这个问题的通用做法:在数据加载后手动检查labels.max()与model.config.vocab_size的大小关系,一旦labels.max() >= vocab_size,立刻收敛数据构造逻辑。
6.5 小工具:快速检查模型结构与当前设备
调试时一个好用的小脚本:
from torchinfo import summary summary(model, input_size=(1, 1024), dtype=torch.long) # 检查每层所在设备 for name, module in model.named_modules(): if hasattr(module, "weight") and module.weight is not None: print(name, module.weight.device)第一行看参数总量和计算图结构,第二行检查模型有没有被device_map="auto"正确切分布到多张卡上。这两个组合,基本能解半天的环境排查。
写到这里,这套PyTorch + Hugging Face + PEFT + LoRA的链路就算是理顺了。我个人的体会是,这条技术栈的每一环都有各自的门道:环境问题归根结底是版本对应,模型加载的本质是路径和文件管理,LoRA的精髓在于理解低秩假设和参数配置,爆显存的排查本质是对训练过程的定量观察。把这些概念都过一遍之后,再回头跑lora微调实战教程那类文章,就发现它们都只是这条主干线上的具体节点。最后再分享一个小技巧:在项目最开始就写好一个requirements.txt并锁定所有主版本号,换机器时用pip install -r requirements.txt一键恢复,能省下你未来每一次环境迁移的半天时间。