ROCm云端Gemma4 LoRA微调实战:情绪分类准确率提升至0.734
发布时间:2026/10/1 17:13:28 作者:尧图编辑部 阅读量:1,286

上个月我在某云平台开了一台带 AMD MI210 的 ROCm 实例主要想验证一个想法在完全不用 NVIDIA 卡的条件下能不能把 Gemma4 这种轻量级生成模型用 LoRA 拉起来做情绪分类。跑通之后准确率从 0.594 涨到 0.734提升不算惊艳但整个过程踩出来的坑足够写一篇完整总结。这篇文章没有多余的废话从环境安装、数据准备、微调参数到我遇上的 4 个天坑全部按我实际操作的顺序写给你。适合那些手里只有 ROCm 云资源、又想把大模型落地到具体任务的开发者也适合准备入坑 LoRA 微调但还没系统性跑过的朋友。1. 项目设想与方案选型1.1 为什么选 AMD ROCm 云而不是租 NVIDIA 卡先说背景。我当时手上没有本地 A100临时任务也不值当按小时包一块 80G 的 NVIDIA 卡。正好那阵子云厂商推 AMD 实例价格比同规格 NVIDIA 便宜不少而且 ROCm 从 5.6 开始对 PyTorch 的兼容已经比前两年好了很多所以我决定赌一把。ROCm 的全称是 Radeon Open ComputeAMD 自己的一套 GPU 计算栈。很多人一听到“不用 CUDA”就头皮发麻实际上今天 PyTorch 官方直接提供torchrocm的预编译包安装方式和 CUDA 版几乎一样。我的目标很简单让 Gemma4 在 ROCm 上完成 forward、backward、LoRA 参数更新评测指标达到可接受范围。至于要不要榨干每一帧显存那是下一步的事。如果你是第一次接触 ROCm先记住一个概念ROCm 里的显卡驱动、运行时库和 PyTorch 的轮子必须对齐版本。没有对齐后面大概率会出现“模型加载正常一 forward 就 kernel crash”的玄学问题。这也是我后面 4 个坑里最折腾的一个。1.2 为什么选 Gemma4 做情绪分类Gemma4 是当前开源社区里“小而能打”的一类模型代表参数量比动辄几十 B 的模型友好太多但基础语义理解、指令跟随能力都还在线。我需要的是一个能完成“给定一句话输出正/负情绪标签”的底座而不是一个需要从头训练的词向量模型。选它的另一个原因是 HuggingFace 的 transformers 已经从代码层面做了兼容AutoModelForSequenceClassification可以直接把这类 decoder-only 模型包成一个分类器。这样一来我只需要操心“怎么在分类头上做好 LoRA”而不需要手写 transformer block。当然情绪分类本身是个入门级任务杀鸡用牛刀也不丢人。真正的目的不是刷 SOTA而是把整套 ROCm Gemma4 LoRA 的链路打通。后面如果有更难的任务比如舆情细粒度分类、客服对话意图识别直接复用这套流程就很快。1.3 LoRA 微调的基础逻辑LoRA 的玩法其实不复杂冻结预训练模型的全部权重在部分线性层旁边插入两个低秩矩阵 A 和 B。训练时只更新这两个矩阵推理时再把 AB 合并回原权重。这样可以极大减少可训练参数量一个小模型微调任务通常只需要训练几百万到几千万参数。用生活类比来说基座模型就像一个知识量很大但不会“说人话”的转述机器LoRA 则是给它加一个轻量校正扳手只调“这个任务下该出的调子”不让它把已经学会的知识洗掉。情绪分类场景下我不需要 LoRA 覆盖所有层一般选择注意力层的 q_proj、v_proj 或者 gate_proj 就行。具体目标模块取决于任务难度太深的模型层或者 MLP 层也可以微调但参数量会成倍上涨收益未必成正比。这个平衡我后面会详细讲。2. 环境准备与数据准备2.1 ROCm 环境安装与验证我先列一下我的云实例规格AMD EPYC 7003 系列 CPU一张 64GB 显存的 MI210系统是 Ubuntu 22.04。首先确保内核版本、驱动、ROCm 库三者匹配。云厂商给的镜像一般已经装好了 amdgpu 驱动所以我不需要重新编译内核模块只需要安装 user-space 工具和 PyTorch。实际操作是预先安装好 ROCm 运行库和 rocm-smi 工具检查 GPU 是否被系统识别rocm-smi --showallinfo这里会列出 GPU 类型、显存总量、驱动版本、风扇转速等。如果确实看到 MI210说明驱动层没问题。接着安装 PyTorch 的 ROCm 版本。我的做法是创建干净的 conda 环境conda create -n gemma-lora python3.10 -y conda activate gemma-lora pip install torch2.3.0rocm5.7 --index-url https://download.pytorch.org/whl/rocm5.7 pip install transformers4.40.0 datasets peft accelerate版本对应关系很关键。当时 PyTorch 那边维护了多个 ROCm 分支2.3.0rocm5.7是官方测试比较充分的一组。如果你拿到的是 MI250 这种卡可能还要额外设置一个环境变量这个我放在后面的坑里讲。验证 torch 是否真正可用 GPU不要只看torch.cuda.is_available()ROCm 下这个函数会返回 True但实际调用的是 HIP。最可靠的办法是直接跑一次矩阵乘法然后把张量打印出来。我当时写了一段import torch print(torch.version.hip) x torch.randn(10, 10, devicecuda) y torch.matmul(x, x) print(y.sum().item())如果看到一串数字输出说明计算链路已经通了。注意不要被“cuda”字样吓到PyTorch 为了兼容老代码把 ROCm 也暴露为cuda接口实际底层是 HIP。2.2 模型加载与 tokenizer 配置Gemma4 的权重我在 HuggingFace Hub 上找了一个带分类头的 checkpoint也或者说只有原始基座权重我用 transformers 加上分类头。前者更省事后者更灵活。加载模型时需要注意一个细节如果 repo 里有自定义代码需要trust_remote_codeTrue。不过更规范的路径是直接加载普通模型再换分类头。我最终用的方案是from transformers import AutoTokenizer, AutoModelForSequenceClassification model_path your-registry/gemma4-base tokenizer AutoTokenizer.from_pretrained(model_path, use_fastFalse) model AutoModelForSequenceClassification.from_pretrained( model_path, num_labels2, trust_remote_codeTrue )为什么use_fastFalse因为 Gemma4 的分词器在某些版本下tokenizer.json格式不完整用 fast tokenizer 会直接抛异常换成 slow 反而稳。这个坑我详细写在第 4 节。再强调一点加载完模型后立刻打印model.config.hidden_size和参数量确认模型没有读成一个奇怪的随机初始化版本。如果你发现参数量少了一个量级大概率是 repo 选错了。2.3 情绪数据集构建与标签设计数据我用的是影评情绪二分类训练集 5000 条验证集 800 条测试集 1200 条。长文本统一截断到 256 个 token。标签设计很简单1 表示正向0 表示负向。但有一条经验不要隐含地把“中性”也塞进去三分类会明显增加任务难度而 LoRA 在小样本情况下最容易把这几个类混淆。数据加载直接用 HuggingFace 的 datasetsfrom datasets import load_dataset dataset load_dataset(your_usecase/imdb-style-sentiment, splittrain) dataset dataset.train_test_split(test_size0.15, seed42) def tokenize(batch): return tokenizer(batch[text], truncationTrue, max_length256, paddingmax_length) dataset dataset.map(tokenize, batchedTrue, remove_columns[text])这里有个细节paddingmax_length会让每个样本定长batch 内部没有 padding 差异可以省去注意力掩码的动态计算但对显存不友好。如果你用 64G 显存问题不大如果只有 24G建议改成paddinglongest。我这次的训练数据分布还算平衡正负各 2500 左右。说实话0.594 这个基线水平主要不是数据问题而是直接拿“零样本提示词”去让 Gemma4 输出标签时模型总是倾向于输出格式里的第二个 token导致准确率不算高。等用 LoRA 真正微调后分类头才学到稳定边界。3. LoRA 微调实操全流程3.1 冻结基座模型并插入 LoRA 层拿到分类模型后我直接交给 PEFT 处理。首先确认哪些模块要被注入 LoRA。对于 Gemma4 这种架构注意力层的投影矩阵通常叫q_proj、k_proj、v_proj、o_proj。我只选择了q_proj和v_proj这是任务复杂度比较低时的保守选择参数量少过拟合风险低。from peft import LoraConfig, get_peft_model lora_config LoraConfig( r8, lora_alpha16, lora_dropout0.05, target_modules[q_proj, v_proj], biasnone, task_typeSEQ_CLS ) model get_peft_model(model, lora_config) model.print_trainable_parameters()输出大概是Trainable params: 4,718,592 || all params: 2,164,682,752 || trainers%: 0.2178不到千万的 trainable 参数对 ROCm 来说没有任何压力。之所以不把k_proj、o_proj也加进去是因为情绪分类任务是一种粗粒度语义判断相比于细粒度 NER 或推理任务并不需要那么强的表示自由度。加了反而可能损失稳定性。task_type必须设为SEQ_CLS这是一个很容易被忽视的点。如果把task_type留空PEFT 不会自动为分类头创建可训练参数而你恰恰需要分类头有一部分可训练权重去拟合标签映射。3.2 训练参数设置与保存策略训练器的选择上我没有用 Trainer而是手动写了一个torch训练循环。Trainer 很方便但对 ROCm 上出现的一些底层异常不够透明不利于排查 kernel crash。手动循环能看到每一步的显存占用和报错现场。超参配置如下learning_rate: 2e-4 batch_size: 16 gradient_accumulation_steps: 2 num_epochs: 3 optimizer: AdamW lr_scheduler: cosine weight_decay: 0.01 max_grad_norm: 1.0这里学习率 2e-4 是 LoRA 微调的常见起点。普通全参数微调一般用 1e-5 到 3e-5但 LoRA 可训练参数少可以用更大的学习率去快速适应新任务。如果学习率设成 5e-5loss 虽然有下降但收敛速度会慢不少。优化器我用了标准的 AdamW没有用 8bit 版。ROCm 上 bitsandbytes 的支持还不够完善我在这个卡的 ROCm 环境里试过一次直接报“未检测到 CUDA”。虽然可以强行设环境变量去骗过检查但风险太大不如直接用全精度优化器状态。因为一共才 470 万可训练参数优化器态占用的显存可以忽略不计。训练循环核心部分from transformers import get_cosine_schedule_with_warmup from torch.optim import AdamW optimizer AdamW(model.parameters(), lr2e-4, weight_decay0.01) scheduler get_cosine_schedule_with_warmup(optimizer, num_warmup_steps100, num_training_stepstotal_steps) for epoch in range(3): model.train() for step, batch in enumerate(dataloader): batch {k: v.to(cuda) for k, v in batch.items()} outputs model(**batch) loss outputs.loss / gradient_accumulation_steps loss.backward() if (step 1) % gradient_accumulation_steps 0: torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() optimizer.zero_grad()每 10 步打印一次当前 loss 和 GPU 利用率step 10 | loss 0.6832 | lr 1.98e-04 | mem 38.2G step 20 | loss 0.5817 | lr 1.92e-04 | mem 38.4G ...打印torch.cuda.memory_summary()可以看到 ROCm 下的显存分配情况。实际上 ROCm 显存管理和 CUDA 差不多都会随着第一次 forward 逐步分配后面趋于平稳。3.3 评估与准确率跃迁记录训练完三个 epoch 后我用验证集挑选最佳 checkpoint再加载到测试集评估。评估时绝不能继续用 training mode要把 LoRA 权重合并进基座模型或者至少用带 adapter 的推理模式。合并权重的方式merged_model model.merge_and_unload()合并之后推理性价比更高也方便导出成一个独立的单文件模型。如果不合并而是直接使用 PeftModel 推理首次加载 adapter 权重会多耗一点时间。评估代码如下from sklearn.metrics import accuracy_score, classification_report model.eval() preds, labels [], [] with torch.no_grad(): for batch in test_dataloader: batch {k: v.to(cuda) for k, v in batch.items()} logits model(**batch).logits preds.extend(torch.argmax(logits, dim-1).cpu().tolist()) labels.extend(batch[labels].cpu().tolist()) print(accuracy:, accuracy_score(labels, preds)) print(classification_report(labels, preds))最终结果accuracy: 0.7342 precision recall f1-score support 0 0.73 0.72 0.72 600 1 0.74 0.75 0.73 600对比零样本基线 0.594提升约 14 个百分点。这个提升幅度对我很有意义因为它说明即使是轻量级 LoRA在 ROCm 上也能完成一次有效果的适配。真要说哪里贡献最大我的体会是分类头那层可训练参数和 v_proj 的 low-rank 更新共同起了作用只靠 q_proj 时准确率卡在 0.68 左右。4. 我踩过的 4 个坑及处理实录4.1 坑 1PyTorch ROCm 版本与驱动不匹配kernel 一执行就崩现象环境检查全部正常rocm-smi能看到 GPUtorch.cuda.is_available()返回 True。但第一次调用模型model(input_ids)时程序直接崩溃报错末尾往往跟着Memory access fault或者Kernel launch failed也可能直接让整个 python 进程退出连 traceback 都不给。原因PyTorch 编译时绑定了某个 MI 系列架构的指令集而我这个实例的实际 GPU 架构不在默认支持列表里。云厂商提供的是 MI210GCN 架构代号是gfx90a但 PyTorch 的 hipify 层可能默认匹配成gfx908导致 kernel 编译出来跑不动。解决在启动 python 前设置环境变量告诉 ROCm 后端强制使用兼容架构export HSA_OVERRIDE_GFX_VERSION9.0.8 python train.py这里9.0.8对应 gfx908是一个相对通用的兼容选项。如果你不确定自己的卡是哪一代可以先运行rocminfo | grep gfx看 Compute Unit 信息再决定填9.0.8还是9.0.a。经验遇到 kernel 崩溃不要先怀疑代码逻辑先检查环境变量。ROCm 这种问题十有八九是架构不对齐。另一个办法是直接安装云厂商提供的 docker 镜像那里面的 PyTorch 已经调好了能省掉很多痛苦。4.2 坑 2Gemma4 的分词器加载出现“special tokens lost”现象用AutoTokenizer.from_pretrained(model_path)加载时提示找不到tokenizer_config.json或者把unk_token设置成了None。起初没在意等训练时发现 loss 忽高忽低而且生成出来的标签经常是同一个 token id。原因Gemma4 的分词器有些文件是从原始训练框架里导出的并不是标准的tokenizer.json加tokenizer_config.json组合。直接用 fast tokenizer 去推断特殊 token 时会失败。transformers 4.40 对此处理得还不够智能。解决把use_fastTrue改为use_fastFalse并且显式把特殊 token 重新塞进去tokenizer AutoTokenizer.from_pretrained(model_path, use_fastFalse) if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token if tokenizer.unk_token is None: tokenizer.unk_token tokenizer.eos_token另外推荐一个习惯每次把 tokenizer 保存到训练输出目录时主动保存一份tokenizer_config.json这样后续指定from_pretrained(model_dir)时不会出兼容问题。经验不要全信from_pretrained的“自动模式”。加载任何非主流结构模型前先打开 config 文件看一眼tokenizer_class是不是GemmaTokenizer如果是但没有匹配的 fast 类型那就是 slow 的命。4.3 坑 3LoRA 只挂到了 embedding 上训练半天指标纹丝不动现象loss 从 0.7 降到 0.62看起来在收敛但测试集准确率还是 0.59 附近。我第一反应是数据有问题后来打印了 trainable 参数位置才发现model.print_trainable_parameters()显示可训练参数只有 20 万明显偏少。原因模型仓库里的层名和我预想的不同。Gemma4 的分类头不在常规的score层而 LoRA 配置的target_modules用通配符匹配时只匹配到了embed_tokens。embedding 的更新对分类边界影响非常有限自然指标不动。解决先用model.named_modules()把所有模块名字列出来筛选出真正包含q_proj、v_proj、dense的路径。我最终的 target 是这样写的target_modules[.*q_proj, .*v_proj, score]然后再次print_trainable_parameters()确认可训练参数回到了 470 万。经验任何微调任务开始之前都值得打印一次可训练参数数量。如果莫名其妙特别少不用急着调学习率先查目标模块匹配对不对。4.4 坑 4显存不足想用 CPU offload结果每个 step 慢到怀疑人生现象训练过程中 batch size 从 16 加到 32直接 OOM。我快速加了accelerate的 CPU offload 配置把优化器状态和部分参数放到 CPU。结果每 step 时间从 0.8 秒涨到了 5 秒三天都跑不完。原因ROCm 上 PCIe 传输带宽和 CUDA 环境下的 NVLink 完全不是一回事频繁把梯度状态在 GPU/CPU 之间搬移传输开销远超省下来的一点显存收益。解决这条路放弃。我改回 batch size 16开启梯度累积到 2相当于“伪 batch size 32”。梯度累积不会增加瞬时显存只会让更新频率变慢一点但对 LoRA 这种参数小的任务完全够用。经验优先缩小 batch size 梯度累积而不是无脑 offload。只有当输入序列特别长、且 batch size 已经小到 1 时才值得考虑 offload。5. 效果复盘与后续扩展5.1 从 0.594 到 0.734准确率提升拆解0.594 的基线并不是随机猜测。二分类随机猜是 0.50.594 比随机高了不少这是因为直接使用模型在预训练阶段形成的语言先验某些积极词出现时模型会偏向输出正类。但整体偏保守对否定句、反讽句式基本失效。LoRA 微调后分类头学到了一个更贴合数据分布的决策边界。同样一句话模型不再依赖“词表面情感”而是从注意力层抽取更抽象的上下文表示。提升最大的样本集中在否定句上比如“我本来觉得无聊结果越看越上头”原先会判成负现在判成正。另外一个数据细节验证集上最优 epoch 出现在第 2 轮而不是第 3 轮。后续继续训练会有轻微的过拟合训练继续收敛验证准确率却开始下降。如果你复现时发现第 2 轮最高建议把early_stopping或直接按验证集保存最佳权重。5.2 LoRA 超参数与数据量对结果的影响我在固定其他参数不变的情况下稍微测了两组对照。第一组把lora_alpha调到 32验证集精度没有继续上升反而在最后 epoch 掉了 0.02说明alpha16已经足够表达这个任务的低秩空间。第二组把训练数据削减到 1200 条准确率仍然能到 0.70说明小样本场景下 LoRA 的泛化能力还可以。如果后续要进一步提高最值得尝试的改动是增加目标模块把o_proj也纳入训练同时把r从 8 提到 16。代价是训练参数翻倍还多但对更复杂的情绪类别可能有利。我没有做特别夸张的 grid search原因是在 ROCm 云上按小时计费每次全量训练大概 40 分钟我想把预算留给更有价值的任务。如果你有耐心建议至少跑三组 r8、16、32和两组 target_modules来找到你所在数据域的最佳平衡。5.3 后续还能怎么扩展这套链路里真正值得复用的是“ROCm 云 Gemma4 LoRA 分类头”的最小工程闭环。我后续会把它扩展成两个方向一是多标签情绪分类比如惊讶、愤怒、悲伤、喜悦四分类这时需要把num_labels改成 4同时增加训练样本二是把 LoRA 与指令微调结合让模型不仅输出标签还要生成一句简短的评论这会变成一个序列到序列的任务。另外值得提一句用 ROCm 做 LoRA 微调的生态已经很成熟但要注意 bitsandbytes 的 8bit/4bit 量化在 ROCm 上仍不原生可用。如果你为了省显存想上 QLoRA最好先去查一下当前版本的兼容情况否则可能浪费很多时间在“骗过 CUDA 检测”上。最终个人经验补充分享最后分享一个我从这次实践中沉淀下来的小技巧每次训练前把环境版本信息固定下来至少记录torch.__version__、torch.version.hip、transformers.__version__这三个。虽然听起来很啰嗦但 ROCm 环境下版本错配是最大的隐形杀手把这三个版本写进训练日志里再遇到问题翻日志会省掉一半的排查时间。另外训练过程中每 50 步强制做一次验证集推理把预测结果写进 JSONL方便后续看错误样本这比只看准确率数字有用得多。这次项目能稳定跑通到 0.734很大程度上也归功于这种“每个中间结果都留痕”的习惯。