AMD Hello-ROCm LoRA微调(二)
安装依赖
先执行pip uninstall torchvision
注意这边如果执行了之前环境配置操作,也要执行这一步操作,因为上一步操作中是这样安装的:
uv pip uninstall torchvision uv pip install vllm torchvision \ --no-cache \ --index-url https://mirrors.aliyun.com/pypi/simple/ \ --extra-index-url https://wheels.vllm.ai/rocm/ \ -U
而两次操作两个库不是从同一个源(一个阿里云一个腾讯云)安装的,而且 torchvision 的版本是预编译给另一个 PyTorch 构建的,否则按教程的操作运行Notebook示例文件时,会出现 torchvision::nms does not exist 和循环导入等错误。
官方教程给出的命令是下面这样:
uv pip install -U vllm modelscope transformers accelerate datasets trl peft scikit-learn pandas tqdm torchvision \
--no-cache \
-i https://mirrors.cloud.tencent.com/pypi/simple/ \
--extra-index-url https://wheels.vllm.ai/rocm/
|
字段 |
含义 |
|
! |
Jupyter Notebook 中执行系统命令的前缀(在终端中可直接去掉) |
|
uv |
一种用 Rust 编写的高性能 Python 包管理器,可替代 pip,安装速度更快 |
|
pip install |
uv 模拟的 pip 安装命令 |
|
-U (或 --upgrade) |
升级所有列出的包到最新可用版本(若已安装则升级,否则直接安装) |
|
待安装的包列表 |
一系列用于大模型推理、微调和数据处理的库:vllm(LLM 推理引擎)、modelscope(阿里模型库)、transformers/datasets/accelerate/trl/peft(Hugging Face 模型、数据、分布式、强化学习微调、参数高效微调)、scikit-learn(机器学习)、pandas(数据分析)、tqdm(进度条)、torchvision(PyTorch 视觉工具) |
|
--no-cache |
强制不使用本地缓存,从远程重新下载所有包以确保获取最新文件(下载会变慢) |
|
-i https://mirrors.cloud.tencent.com/pypi/simple/ |
指定主软件包索引源为腾讯云 PyPI 镜像,用于在国内加速下载大部分 Python 包 |
|
--extra-index-url https://wheels.vllm.ai/rocm/ |
额外添加一个索引源,专门提供 vLLM 针对 AMD ROCm 平台编译的 wheel 包,确保在 AMD GPU 上能正确安装和使用 vLLM |
前期准备
要微调模型要先准备好模型、数据集。


配置训练环境
接着要配置训练环境,通常包括:
GPU资源
这里是单卡环境。
数据集划分
1.比例划分:Train :Test(8:2);Train:Eval:Test(6:2:2)
2.设置集合样本数量如TRAIN_LIMIT、EVAL_LIMIT等参数,比如取Top 1000
精度
- 模型权重的存储数据类型MODEL_DTYPE。
|
类型 |
bit |
数值范围 |
特点 |
典型用途 |
|
FP32 |
32 |
±10^38 |
最高精度,训练基准 |
全精度训练与推理 |
|
FP16 |
16 |
±65504 |
显存减半,数值范围窄,可能溢出 |
混合精度训练(需梯度缩放) |
|
BF16 |
16 |
±10^38(同FP32) |
显存减半,动态范围大,几乎无需梯度缩放 |
混合精度训练 |
|
INT8 |
8 |
-128 ~ 127 |
再减半显存,需量化校准,略损精度 |
推理加速、边缘部署 |
|
INT4 |
4 |
-8 ~ 7 |
极致压缩,精度损失较大,需仔细量化 |
极低资源推理(如 llama.cpp Q4) |

2.训练参数精度:常见的有FP16、BF16、FP32,一般也受显卡影响。

- FP32精度最高,但训练需要显存也多。
- BF16牺牲了小数位的精度,但既能节省显存、加快训练,又能保持较大数值范围。
- FP16 的数值范围较小,训练时容易出现梯度溢出,通常需要配合梯度缩放

随机种子
因为计算机生成的是伪随机数,其生成函数可以看作:下一个数 = f(当前状态, 种子),相当于给定种子调用random()拿到的数一定相同,让数据 shuffle、LoRA 初始化和训练过程尽量可复现(因为复现性还受计算差异如硬件一致性、确定性算法等影响)。
def setup_seed(seed: int = 42):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
set_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
setup_seed(SEED)
指令微调(复用原有架构以及knowledge)
因为我们想要想要微调后的模型实现情绪分类任务(classification task),但对于LLM来说本质上仍然是生成任务(generation task)而不仅仅是特征到标签的映射,所以要把分类任务重新包装成生成任务,即变成一个prompt-completion 格式数据:
- prompt:system + user
- completion:assistant 只输出对应的情绪标签

def to_prompt_completion(example):
text = example["text"]
label = label_names[example["label"]]
user_content = f"Classify the emotion of this text:\n\n{text}"
return {
"prompt": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_content},
],
"completion": [
{"role": "assistant", "content": label},
],
}
sft_dataset = dataset.map(
to_prompt_completion,
remove_columns=dataset["train"].column_names,
)
batch_size
每次训练传多少个数据进去batch。
比如有 1000 条对话训练数据,设定 batch_size = 8:
模型一次拿 8 条数据,算它们的平均损失;
反向传播时,基于这 8 条数据产生的平均梯度来更新一次权重;
跑完整个数据集需要 1000 / 8 = 125 次参数更新,这 125 步称为 1 个 epoch。

而模型训练时的显存占用可以拆成三块:
- 模型参数本身(权重 + 优化器状态):模型结构不变就固定下来了。
- 中间激活值:在多层神经网络中,每一层的梯度都依赖于该层的输入激活值。所以前向传播时产生的临时数据,必须存到反向传播用。其体积 ≈ 单条样本激活值 × batch size。
神经网络的训练本质是求梯度(参数对损失的影响),用的是链式法则。
举例类比:函数 y = f(g(x)),要算 dy/dx,需要:
dy/dx = f'(g(x)) * g'(x)
其中 g(x) 就是中间激活值,g'(x) 计算时需要用到 x 的值。

- 数据本身:输入的 token 张量等,也跟 batch size 成正比。

所以遇到GPU OOM可以考虑减小batch_size或降低前文所提的精度等。
加载 tokenizer 和基础模型
Tokenizer(分词器)
组成:词汇表 + 特殊标记 + 格式化模板,把自然语言转成模型需要的数字序列。其实推理的时候也是需要的,不过模型会内置

- tokenizer.json(词汇表与分词规则)
- tokenizer_config.json(特殊标记、chat_template 等)
- special_tokens_map.json

加载模型
由于本实验是在单卡上跑的,所以训练加载模型时要配置如下参数:
1.不用 device_map="auto":device_map="auto" 是 Hugging Face transformers 为多卡、CPU offload 设计的自动分配器。单卡场景下如果用了,有时会把部分层放到 CPU,或者引入额外的通信开销,反而增加显存占用甚至变慢。
不加这一项,模型就只会加载到单张 GPU 上(或先加载到 CPU,后续由 Trainer 统一移动到 GPU),简单可控。
2.不用 bitsandbytes 4bit
4bit 量化(load_in_4bit=True)能把模型权重大幅压缩,显存省很多,但会带来两个问题:
精度损失:微调时可能影响最终性能。
与某些 Trainer 特性不兼容:比如梯度检查点、优化器状态等有时会出小问题。
3 加载后交给 SFTTrainer 管理设备:不需要自己手动调用 .cuda() 或 .to(device)。把模型直接传给 SFTTrainer,它会在内部自动把模型移动到训练所用的设备(比如 cuda:0),并且管理好数据、优化器、模型之间的设备一致性。避免手动管理设备时常见的错误(比如模型和数据不在同一设备)。不过通常还是要.cuda().to(device)自己配置。
4 开启 gradient_checkpointing=True 来节省显存(计算时间换显存空间)
正常情况下,前向传播每一层产生的中间激活值(隐藏状态、注意力矩阵等)都要保存在显存里,等着反向传播时用。开启梯度检查点后,只保留少量层的激活值作为“检查点”,其余层的前向结果直接丢弃。等到反向传播需要时,再从最近的检查点重新计算出来。显存占用显著下降,代价是大约 20~30% 的额外计算量。
推理辅助函数
微调前后都用同一套推理函数做评估,方便对比模型效果。extract_label() 用于从模型生成文本里提取对应的label(数据集的情绪标签集合,固定映射)。如果提取不到,会被记为 INVALID(相当于模型没有输出对应的情绪标签)。
def extract_label(raw_text: str) -> str:
raw_text = raw_text.strip().lower()
match = LABEL_PATTERN.search(raw_text)
if match:
return match.group(1)
tokens = raw_text.split()
if not tokens:
return "INVALID"
return tokens[0].strip(".,!?:;\"'()[]{}")
def generate_label(model, tokenizer, user_text: str, system_prompt: str = SYSTEM_PROMPT, max_new_tokens: int = 4) -> str:
user_content = f"Classify the emotion of this text:\n\n{user_text}"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content},
]
device = next(model.parameters()).device
inputs = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_dict=True,
return_tensors="pt",
)
inputs = {k: v.to(device) for k, v in inputs.items()}
input_len = inputs["input_ids"].shape[-1]
model.eval()
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
)
raw_pred = tokenizer.decode(outputs[0][input_len:], skip_special_tokens=True).strip()
return extract_label(raw_pred)
评估函数
评估指标包括:
|
指标 |
含义 |
|
accuracy |
预测正确的样本占总样本的比例,说明整体有多准(但类别不均衡时会被带偏) |
|
macro_f1 |
对每个类别分别计算 F1,然后取平均(不加权),说明小类(样本少的情绪)是否也被照顾到了 |
|
invalid_predictions |
模型没有输出合法标签的样本数,说明模型是否遵循了指令,输出格式是否稳定利用推理函数来计算得出。 |
|
classification_report |
包含每个类别的 precision、recall、f1-score,说明每一个具体情绪的判断质量 |
|
confusion_matrix |
以热力图或矩阵展示真实 vs 预测的分布,说明最容易混淆的情绪对(如“愤怒” vs “悲伤”) |
微调前评估

配置 LoRA
LoRA(Low-Rank Adaptation)是目前大语言模型微调最主流的方法。它不修改原始模型权重,而是在特定层旁路插入小的、可训练的秩分解矩阵,只更新这些小矩阵。可以形象的类比为:
- 全量微调:把原书每一页都擦掉重写,成本极高,而且一旦写坏书就废了。
- LoRA:在每一页上贴一张极薄的便利贴,只写要补充或修改的内容。原书正文完全不动,但阅读时会把便利贴的内容和原文叠加,最终效果相当于一本新书。
数学上,原有权重矩阵 W 保持不变,LoRA 添加两个小矩阵 A 和 B,使得:
新权重=W+A×B
其中A和B都是低秩矩阵(参数量远小于 W)。训练时只更新 A 和 B,原模型W冻结。

教程中使用target_modules="all-linear",表示尽量给模型中的线性层加 LoRA,可以改成指定模块名控制显存和训练速度,也会影响训练效果。

定义训练参数

|
类别 |
参数 |
作用 |
典型值 / 说明 |
|
精度与显存 |
MODEL_DTYPE |
模型权重的存储类型 |
bfloat16(支持 BF16 的卡)或 float16 |
|
bf16 / fp16 |
开启混合精度训练 |
二选一,BF16 更稳定 |
|
|
load_in_4bit |
4-bit 量化加载原模型(QLoRA) |
True 时显存大幅降低 |
|
|
批次与序列 |
per_device_train_batch_size |
单张 GPU 一次前向的样本数 |
1–4(显存受限) |
|
gradient_accumulation_steps |
梯度累积步数,等效 batch = batch_size × 累积步数 |
8–32,用于撑大有效 batch |
|
|
max_seq_length / cutoff_len |
单条样本最大 token 数,超出截断 |
512–2048(按任务需要) |
|
|
优化器与学习率 |
learning_rate |
更新步长 |
LoRA 推荐 1e-4 ~ 5e-4,全量 1e-5 |
|
optimizer |
优化算法 |
adamw_torch、adamw_8bit(省显存) |
|
|
lr_scheduler_type |
学习率调度策略 |
cosine 或 linear,配合 warmup |
|
|
warmup_ratio |
前期缓慢增加学习率的比例 |
0.03–0.1,避免初期震荡 |
|
|
max_grad_norm |
梯度裁剪阈值 |
1.0,防梯度爆炸 |
|
|
训练轮次 |
num_train_epochs |
全量数据重复次数 |
1–3,指令微调不宜过多 |
|
max_steps |
最大更新步数(与 epoch 二选一) |
按需设置 |
|
|
LoRA 结构 |
lora_rank (r) |
低秩矩阵的秩,决定适配器容量 |
8–64,越高表示可学习参数越多 |
|
lora_alpha |
缩放系数,实际学习率 ∝ alpha/r |
通常设为 rank 的 2 倍 |
|
|
lora_dropout |
适配器 dropout,正则化 |
0.05–0.1 |
|
|
lora_target |
要添加适配器的模块 |
q_proj,v_proj 等 |
|
|
评估与保存 |
eval_steps |
每隔多少步在验证集上评估 |
100–500 |
|
save_steps |
每隔多少步保存一次 checkpoint |
与 eval_steps 一致 |
|
|
save_total_limit |
最多保留多少个 checkpoint |
2–3,避免占满磁盘 |
|
|
metric_for_best_model |
选择最佳模型的指标 |
eval_loss 或 macro_f1 |
training_args = SFTConfig(
output_dir=OUTPUT_DIR,
per_device_train_batch_size=4,
per_device_eval_batch_size=1,
gradient_accumulation_steps=4,
learning_rate=1e-4,
weight_decay=0.01,
lr_scheduler_type="linear",
warmup_steps=50,
num_train_epochs=1,
logging_steps=5,
eval_strategy="steps",
eval_steps=25,
save_strategy="steps",
save_steps=25,
save_total_limit=2,
metric_for_best_model="eval_loss",
greater_is_better=False,
gradient_checkpointing=True,
bf16=BF16,
fp16=FP16,
tf32=False,
max_length=256,
packing=False,
completion_only_loss=True,
remove_unused_columns=False,
dataloader_num_workers=2,
optim="adamw_torch", #避免 AMD ROCm 下 `bitsandbytes` 优化器兼容问题
report_to="none",
seed=SEED,
data_seed=SEED,
)
然后就可以开始 LoRA 微调了!

保存 LoRA adapter 和 tokenizer
保存的是 LoRA adapter,不是完整大模型权重。
目录中通常包括:
- adapter_model.safetensors
- adapter_config.json
- tokenizer 相关文件
- training checkpoints
微调后评估

加载本地 LoRA adapter 推理
以后不想重新训练,可以用下面代码重新加载:
- 本地基础模型目录加载 base model。
- 从 OUTPUT_DIR 加载 LoRA adapter,PeftModel.from_pretrained 是 PEFT 库提供的接口,它会把基础模型和 LoRA 适配器捆绑到一起。
- 使用同一个 generate_label() 做推理

其他一些自己的实验截图碎片:




参考文献
更多推荐


所有评论(0)