LoRA、QLoRA 还是全参数微调:需要多少显存?
在 GPU 服务器上用 LoRA 或 QLoRA 微调 LLM,就是借助 Hugging Face 的 TRL 和 PEFT,在冻结的模型之上训练一个小型适配器。LoRA 让基础模型保持 BF16 精度,只训练附加在各层上的低秩矩阵;QLoRA 的做法相同,只是基础模型改为用 bitsandbytes 以 4-bit 加载。全参数微调则会更新每一个权重,并为所有权重保存梯度和优化器状态。CryptGPU 的选型规则把 GPU 显存的 92% 计为可用,并为激活值留出余量:LoRA 约为每 10 亿参数 3 GB,QLoRA 约为 0.75 GB,全参数微调约为 18 GB。
下表给出所需显存,以及能放下它的最便宜服务器的月价:
| 模型 | QLoRA | LoRA(BF16) | 全参数微调 |
|---|---|---|---|
| 8B | 6 GB:1× RTX 5080,$199 | 24 GB:1× RTX 5090,$329 | 144 GB:1× MI355X,$1,409 |
| 32B | 24 GB:1× RTX 5090,$329 | 96 GB:4× RTX 5090,$1,316 | 576 GB:4× MI355X,$5,636 |
| 70B | 53 GB:2× RTX 5090,$658 | 210 GB:1× MI355X,$1,409 | 1,260 GB:8× MI355X,$11,272 |
- 先从 QLoRA 开始,在最小的服务器上检验您的数据和参数设置,如果质量有要求,再转向 BF16 下的 LoRA。
- 单块 GPU 更简单。使用多块 GPU 时,模型需要拆分或分片(见下文),而且 GeForce 显卡之间要通过 PCIe 交换数据。70B 模型的 QLoRA 训练(53 GB)也可以放进一块 H100(80 GB,每月 $1,779)或一块 RTX PRO 6000(96 GB,$959),32B 模型的 LoRA 训练(96 GB)则可以放进一块 H200(141 GB,$2,279)。
- MI355X 使用的是 ROCm。PyTorch、bitsandbytes、TRL 和 PEFT 都能在它上面运行(见下文)。如果只考虑 NVIDIA GPU,能放下 70B LoRA 训练的最便宜配置是 4× RTX PRO 6000,$3,836。
其他模型规模可以用选型助手查看,您的模型需要多少 GPU 显存一文解释了这条规则。按月租用的服务器在两次训练之间会保留您的数据和环境;如果只是一次短时间的训练,按小时计费的云平台可能更便宜。参见面向微调的 GPU 服务器。
配置环境
如果您下单时在“软件”步骤中选择了“微调工具包”(Axolotl、TRL 和 PEFT),它已经装好;下面介绍手动安装的方法。把整套软件安装在独立的 Python 环境中,并把下载内容保存在 NVMe 数据盘上(参见存储与数据)。加上 --torch-backend=auto 后,uv 会检测 GPU 驱动,并安装相匹配的 PyTorch 构建:NVIDIA 上为 CUDA 版,AMD 上为 ROCm 版(参见 uv 的 PyTorch 指南)。
curl -LsSf https://astral.sh/uv/install.sh | sh # 然后开一个新终端,让 uv 出现在 PATH 中
uv venv --python 3.12 --seed --managed-python ~/ft-env
source ~/ft-env/bin/activate
uv pip install torch "trl[peft]" bitsandbytes --torch-backend=auto
export HF_HOME=/data/hf # 模型、数据集和您的令牌都放在 NVMe 上
export HF_HUB_DISABLE_TELEMETRY=1 # 关闭 Hugging Face 库的使用情况遥测
mkdir -p /data/ft # 存放数据集、脚本和输出
hf auth login # 仅需要授权的模型(例如 Llama)才需要登录
trl env # 显示各库的版本和检测到的 GPU
把这两行 export 加入 ~/.bashrc,让设置长期生效。在 MI355X 上,请确认 python -c "import torch; print(torch.cuda.get_arch_list())" 的输出中包含 gfx950,或者在 rocm/pytorch 容器中工作(参见容器)。bitsandbytes 发布了包含 gfx950 的 ROCm wheel 包,AMD 的微调指南也是在 Instinct GPU 上使用 TRL 和 PEFT。
准备 JSONL 格式的对话数据集
TRL 的 SFTTrainer 会把每段对话读取为一个 messages 列表,列表中的每一项都包含 role 和 content,并且会自行套用模型的聊天模板(参见 TRL 数据集格式)。在 /data/ft/train.jsonl 中每行写一段对话:
{"messages": [{"role": "system", "content": "You answer customer emails briefly."}, {"role": "user", "content": "My order arrived damaged."}, {"role": "assistant", "content": "Sorry about that. Send a photo and we will ship a replacement."}]}
{"messages": [{"role": "user", "content": "Can I change the delivery address?"}, {"role": "assistant", "content": "Yes, until the order ships. Reply with the new address."}]}
请使用与模型实际会收到的请求相似的样本,去除重复项,并剔除您无权用于训练的数据。下文的训练脚本会留出 5% 的数据行用于评估。先通过聊天模板打印一条样本,检查角色和特殊 token 是否正确:
python - <<'EOF'
from datasets import load_dataset
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
row = load_dataset("json", data_files="/data/ft/train.jsonl", split="train")[0]
print(tok.apply_chat_template(row["messages"], tokenize=False))
EOF方案 A:用 TRL SFTTrainer 进行 QLoRA 或 LoRA 微调
这个脚本使用当前的 TRL API(TRL 1.16、PEFT 0.21),在单块 GPU 上微调 Qwen3-8B。QLORA = True 会用 bitsandbytes 以 4-bit NF4 格式加载基础模型。如果要改用 Llama 3.1,请先在 Hugging Face 上接受它的许可协议,运行 hf auth login,再修改 MODEL。
# /data/ft/train_sft.py:用 TRL 进行 LoRA 或 QLoRA 微调
import torch
from datasets import load_dataset
from peft import LoraConfig
from transformers import BitsAndBytesConfig
from trl import SFTConfig, SFTTrainer
MODEL = "Qwen/Qwen3-8B"
QLORA = True # False:在 BF16 模型上做 LoRA
OUT = "/data/ft/qwen3-8b-lora"
data = load_dataset("json", data_files="/data/ft/train.jsonl", split="train")
data = data.train_test_split(test_size=0.05, seed=42)
bnb = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
) if QLORA else None
lora = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05,
target_modules="all-linear", task_type="CAUSAL_LM")
args = SFTConfig(
output_dir=OUT,
model_init_kwargs={"dtype": torch.bfloat16},
max_length=2048,
assistant_only_loss=True,
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
per_device_eval_batch_size=2,
num_train_epochs=2,
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_steps=0.03, # 小于 1 的值表示占整个训练过程的比例
logging_steps=10,
eval_strategy="steps",
eval_steps=0.1, # 每完成 10% 的训练就评估并保存一次
save_steps=0.1,
save_total_limit=3,
)
trainer = SFTTrainer(
model=MODEL,
args=args,
train_dataset=data["train"],
eval_dataset=data["test"],
quantization_config=bnb,
peft_config=lora,
)
trainer.train()
trainer.save_model(OUT) # 保存 LoRA 适配器
tmux new -s ft # SSH 会话断开后,训练仍会继续
source ~/ft-env/bin/activate
cd /data/ft
CUDA_VISIBLE_DEVICES=0 python train_sft.py 2>&1 | tee train.log
这些数值只是起点:
target_modules="all-linear"会在除输出层以外的所有线性层上添加适配器,这正是 TRL 的 LoRA Without Regret 指南推荐的做法;r=16和lora_alpha=32是 TRL 命令行参数的默认值。learning_rate=2e-4是 TRL 针对 LoRA 监督微调(SFT)建议的学习率,为其全参数微调默认值的十倍(参见 PEFT 集成)。有效批量为 2 × 8 = 16 条序列,低于 TRL 针对 LoRA 建议的上限 32。Unsloth 的指南建议训练一到三个 epoch。model_init_kwargs让权重以 BF16 加载,否则 TRL 会以 float32 加载。梯度检查点和 BF16 训练默认都已开启。assistant_only_loss=True只在助手的回复上计算损失。这要求聊天模板中带有生成标记(generation markers),对于 Qwen3、Llama 3 等 TRL 已知的模型系列,TRL 会自动添加(参见 SFT Trainer)。max_length会截断更长的对话;TRL 的默认值是 1,024 个 token。
在多 GPU 服务器上,CUDA_VISIBLE_DEVICES=0 能让训练只在一块 GPU 上进行;如果直接用 python 运行,模型会被分散到所有 GPU 上。
方案 B:在单块 GPU 上使用 Unsloth
Unsloth 用自己的 Triton 内核替换模型的部分组件,从而在单块 GPU 上以更少的显存、更短的时间完成训练,而且可以直接接入同一个 SFTTrainer。请把它安装在独立的环境中:
uv venv ~/unsloth_env --python 3.13
source ~/unsloth_env/bin/activate
uv pip install unsloth --torch-backend=auto
# /data/ft/train_unsloth.py
from unsloth import FastLanguageModel
from datasets import load_dataset
from trl import SFTConfig, SFTTrainer
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="Qwen/Qwen3-8B",
max_seq_length=2048,
load_in_4bit=True, # False:在 16-bit 基础模型上做 LoRA
)
model = FastLanguageModel.get_peft_model(
model, r=16, lora_alpha=16, lora_dropout=0,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
use_gradient_checkpointing="unsloth",
)
data = load_dataset("json", data_files="/data/ft/train.jsonl", split="train")
data = data.map(lambda x: {"text": tokenizer.apply_chat_template(x["messages"], tokenize=False)},
remove_columns=["messages"])
trainer = SFTTrainer(
model=model,
processing_class=tokenizer,
train_dataset=data,
args=SFTConfig(output_dir="/data/ft/unsloth", max_length=2048,
per_device_train_batch_size=2, gradient_accumulation_steps=8,
learning_rate=2e-4, num_train_epochs=1),
)
trainer.train()
model.save_pretrained("/data/ft/unsloth-lora")
tokenizer.save_pretrained("/data/ft/unsloth-lora")
这些数值遵循 Unsloth 的超参数指南(秩取 16 或 32,alpha 等于秩或秩的两倍,学习率 2e-4,有效批量 16)。model.save_pretrained_merged("/data/ft/unsloth-merged", tokenizer, save_method="merged_16bit") 会写出一个合并后的 16-bit 模型,供 vLLM 使用。Unsloth 的 AMD 指南列出了 Linux 上的 MI350 系列(gfx950),并单独给出了安装步骤。
在 2、4 或 8 块 GPU 上训练
这个 TRL 脚本也能在多块 GPU 上运行(请按照多 GPU 一文准备服务器)。当模型能放进单块 GPU 时,数据并行(DDP)会让每块 GPU 都持有一份完整的模型副本,并处理各自的批次;为每块 GPU 启动一个进程:
accelerate launch --num_processes 4 train_sft.py
torchrun --standalone --nproc-per-node=4 train_sft.py # 或者使用 torchrun
此时有效批量等于单卡批量 × GPU 数 × 梯度累积步数(参见 TRL 分布式训练):在四块 GPU 上,设置 gradient_accumulation_steps=2 即可保持 16。
如果模型放不进单块 GPU(例如在 BF16 下对 70B 模型做 LoRA,或者全参数微调),就需要用 FSDP2 或 DeepSpeed ZeRO-3 进行分片;FSDP2 内置于 PyTorch,开箱即可处理 LoRA。TRL 为两者都提供了 Accelerate 配置模板。请先设置 QLORA = False:QLoRA 搭配 FSDP 需要 PEFT 的 FSDP 指南中所述的额外设置。如果做全参数微调,还要去掉 peft_config,并把学习率改回 TRL 的默认值 2e-5。
curl -LO https://raw.githubusercontent.com/huggingface/trl/main/examples/accelerate_configs/fsdp2.yaml
accelerate launch --config_file fsdp2.yaml --num_processes 8 train_sft.py
uv pip install deepspeed # 部分算子会在首次使用时编译
curl -LO https://raw.githubusercontent.com/huggingface/trl/main/examples/accelerate_configs/deepspeed_zero3.yaml
accelerate launch --config_file deepspeed_zero3.yaml --num_processes 8 train_sft.py
需要两块 GPU 的 QLoRA 训练(例如在 2× RTX 5090 上训练 70B 模型)也可以直接用 python train_sft.py 启动:Transformers 会把各层分布到两块 GPU 上,两块 GPU 随后依次进行计算。
监控训练并断点续训
tmux attach -t ft # 回到训练会话
nvidia-smi --query-gpu=index,utilization.gpu,memory.used,memory.total --format=csv -l 5
watch -n 2 amd-smi monitor # AMD MI355X
tail -f /data/ft/train.log # 每 10 步输出一次 loss,每完成 10% 的训练输出一次 eval_loss
训练损失应该逐步下降,评估损失也应随之下降。如果训练损失持续下降而评估损失却在上升,说明模型正在死记训练数据(过拟合):请保留更早的检查点,或减少训练的 epoch 数。如果训练因显存不足(out of memory)错误而中断,可以把 per_device_train_batch_size 减半并把 gradient_accumulation_steps 加倍、缩短 max_length,或者改用 QLoRA。
每次保存时,训练器都会在输出目录中写入一个 checkpoint-N 文件夹(N 为步数),save_total_limit=3 会只保留最近的三个。训练中断后,把调用改为 trainer.train(resume_from_checkpoint=True),即可从最新的检查点继续训练。
合并适配器,或用 vLLM 部署
您可以把适配器与训练时所用的基础模型一起部署,也可以把它合并成一个独立的检查点。请在单独的环境中运行 vLLM,并像推理服务一文那样配置 API 密钥和 SSH 隧道。
部署适配器
vllm serve Qwen/Qwen3-8B --host 127.0.0.1 --port 8000 \
--enable-lora --lora-modules support=/data/ft/qwen3-8b-lora --max-lora-rank 16
客户端通过 "model": "support" 选择该适配器。--max-lora-rank 必须不小于适配器的 r,其默认值为 16(参见 vLLM LoRA 适配器)。
合并到基础模型
python - <<'EOF'
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-8B", dtype=torch.bfloat16)
model = PeftModel.from_pretrained(base, "/data/ft/qwen3-8b-lora").merge_and_unload()
model.save_pretrained("/data/ft/qwen3-8b-merged")
AutoTokenizer.from_pretrained("Qwen/Qwen3-8B").save_pretrained("/data/ft/qwen3-8b-merged")
EOF
vllm serve /data/ft/qwen3-8b-merged --host 127.0.0.1 --port 8000
merge_and_unload() 会把适配器融合进权重,因此推理时没有额外的适配器开销(参见 PEFT LoRA 指南)。合并时会以 BF16 加载基础模型,每个参数约占 2 字节内存;合并结果是一个普通的检查点,vLLM 也可以用 FP8 来部署它。
备份检查点并核对许可证
如果租期没有续费,服务器会在付费月份结束时停止运行,磁盘也会被清空。每次训练结束后,以及在租期结束前尽早,请把适配器、检查点、数据集和脚本复制到服务器之外,并按照存储与数据中的方法校验副本:
rclone copy /data/ft s3remote:my-bucket/ft --progress
rclone check /data/ft s3remote:my-bucket/ft
微调后的模型仍受其基础模型许可证的约束。Qwen3-8B 采用 Apache 2.0 许可证发布。Llama 3.1 系列模型需要申请授权才能下载,其社区许可证除其他条款外,还要求对外分发的微调模型在名称开头加上“Llama”,并显示“Built with Llama”。在发布或出售微调成果之前,请先阅读基础模型的许可证以及您所用数据的使用条款。
对这篇指南有疑问?
请通过联系表单告诉我们您使用的 GPU、运行的命令以及得到的输出结果。
