AMD ROCm上Gemma4情绪LoRA微调:从环境避坑到准确率提升实战
发布时间:2026/10/1 14:49:00来源:尧图网络
上个月我在一台 AMD ROCm 云实例上跑 Gemma4 情绪 LoRA 微调折腾了整整一周多。中间一度怀疑是不是这块 AMD 卡本身有问题lspci 连设备都查不到差点直接放弃。最后倒是顺利跑通了准确率从 0.594 提到 0.734不过这数字背后真正值钱的其实是过程中踩出来的四个坑——每一个都足以让刚接触 ROCm 的人心态崩掉。如果你现在正打算在 AMD 显卡上做 LoRA 微调或者已经在考虑“要不要为了省成本去用 ROCm 环境”这篇文章就是给你准备的。我会把整个链路拆开讲清楚基线怎么来的、环境怎么装、数据和参数怎么定、训练结果怎么验证以及四个坑从现象到根因的完整排查过程。结合最近社区里大家常搜的那些关键词——AMD ROCm、Gemma4、LoRA 微调、大模型微调实战、微调工具框架选型——这篇文章基本能把这条路上的主要雷区都覆盖到。1. 选用 ROCm 跑 LLM 微调为什么这么折腾还值得先说结论AMD ROCm 这套栈在 Linux 上其实比很多人想象的干净得多。真正劝退人的不是流程复杂而是安装匹配这一步容易被忽略。Windows 下 AMD 驱动的体验大家都懂右键菜单里莫名其妙冒出 AMD SoftwareAdrenalin Edition 的入口还有人遇到过错误码 2147942659这类小毛病在 Linux 下的 ROCm 工具链里反而不太常见。ROCm 本质上是 AMD 对标 CUDA 的开源计算平台它提供 HIP 运行时、编译器以及一整套深度学习支持PyTorch、TensorFlow、Unsloth、PEFT 这些主流框架都有对应的 ROCm 版本。选择 AMD 的最大理由就是显存成本低同样 24GB 或 48GB 的卡AMD 的云实例价格通常比 NVIDIA 同级别便宜不少对个人做实验或者中小企业做垂直场景微调来说这笔账很划算。不过也要提前打预防针ROCm 不是 CUDA 的一比一替代。你没法直接把 NVIDIA 镜像里的 PyTorch 包搬过来用必须装带 ROCm 后缀的 wheel。LoRA 微调涉及到的 transformers、peft、bitsandbytes 这层生态对 ROCm 的支持是分阶梯的核心路径模型加载、训练、推理没问题部分辅助组件偶尔会有点“偏科”比如某些量化算子在没有针对性适配时不生效。我这次用的方案是 Unsloth LoRA它在 ROCm 上做了专门的算子优化比我一开始用原生 PEFT 的体验顺畅不少。Unsloth 会在安装时自动检测你的 GPU 环境如果是 AMD 显卡就拉取对应的 ROCm 版 PyTorch 和 triton 预编译包省掉不少手工对版本的时间。另外有个观点我先放在这跑 LLM 微调这件事环境折腾占到的比重有时候能到 50%。很多教程默认你已经有能用的 CUDA 环境直接跳去讲 LoRA 原理和训练参数但在 AMD 平台上环境这关不过后面全是空谈。所以这篇文章我会把环境验证写得很细每一处都给出可以复现的命令而不是只丢一句“安装 ROCm 即可”。2. 基线 0.594 是怎么来的任务定义与直接预测2.1 情绪分类的原始形态5 类标签我要做的任务是从文本里判断情绪类别。真实业务场景里通常不是简单的“正面/负面”二分类而是更细的多分类。我这次用的是 5 类愤怒、快乐、悲伤、恐惧、中性。中性这一类必须保留因为大量真实文本其实没有明显情绪把它们强行塞进四类里只会让模型学出一堆错误分布。数据集用的是公开的中文情绪语料原始量在 4.2 万条左右。清洗规则比较朴素但有效去掉短于 5 个字的文本去掉明显重复的样本去掉包含超长数字和乱码的异常条目。清洗完之后剩 3.1 万条做训练验证集 4000 条测试集 4000 条。这里有个我自己的习惯测试集一旦定下来在整个调参过程里只许看不许碰。人在调参的时候很难抵抗“看测试集效果”的诱惑但一旦看了你对测试集的判断就开始失真了。我这次所有中间结果都只用验证集评估最后才在测试集上跑了完整报告。2.2 直接让 Gemma4 预测的效果0.594之所以先测基线是因为没有基线你根本不知道 LoRA 到底带来了多大提升。很多新手上来就微调跑完看到准确率 0.7 觉得挺高但如果原本模型 zero-shot 就有 0.68那你的 LoRA 其实只涨了 2 个点价值大打折扣。我的基线做法很简单把测试集文本套进一个固定模板让 Gemma4 生成情绪词然后做严格字符串匹配。核心代码大概是这样import torch from transformers import AutoModelForCausalLM, AutoTokenizer model_name /models/gemma4-2b-base model AutoModelForCausalLM.from_pretrained( model_name, torch_dtypetorch.bfloat16, device_mapcuda, ) tokenizer AutoTokenizer.from_pretrained(model_name) label_map {愤怒: 0, 快乐: 1, 悲伤: 2, 恐惧: 3, 中性: 4} def predict(text): prompt f下面这段文本表达的情绪是什么只能回答愤怒、快乐、悲伤、恐惧、中性。\n文本{text}\n情绪 inputs tokenizer(prompt, return_tensorspt).to(cuda) out model.generate(**inputs, max_new_tokens4, do_sampleFalse) answer tokenizer.decode(out[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue).strip() return label_map.get(answer, 4)跑完 4000 条测试集准确率恰好 0.594macro F1 只有 0.49 左右。准确率比随机猜测0.2高很多说明模型本身有情绪理解能力F1 偏低则反映出另一个问题模型在“恐惧”和“中性”上分得稀烂多半是生成式输出偏好导致的比如它老是倾向于输出“愤怒”和“快乐”这类情绪色彩强的词。0.594 这个数字看起来挺平庸但它其实是后续所有工作的参照系。LoRA 微调是不是真的有效不是看训练 loss 降了多少而是看测试集准确率能不能稳定超过这个数。2.3 评估方式的严谨性严格匹配这里必须强调评估的硬性标准。模型生成“愤怒”和生成“愤怒。”在我这里都算正确但生成“有点愤怒”或者“愤怒且悲伤”就算错误。情绪分类任务的输出空间只有 5 个词模板里也明确约束了“只能回答”所以这种严格匹配是合理的。如果你也想先测基线我给个建议别用小样本数据集至少 3000 条以上否则 0.594 这个数字的置信区间会宽到没意义。按二项分布粗算4000 条样本下准确率的标准误大约在 0.008 左右所以 0.594 到 0.734 这 14 个百分点的差距是远远超出随机波动的结论可靠。3. ROCm 云环境从驱动到 PyTorch 的安装路线3.1 先确认 GPU 真的在系统里很多人拿到云实例的第一反应是装 PyTorch其实第一步应该是确认 PCIe 层面的设备枚举。这个环节恰恰是坑 1 发生的地方我后面会展开讲。这里只说正确的操作在 Ubuntu 22.04 里执行lspci -nn | grep -i amd rocminfolspci能看到 AMD 显卡的 PCI 设备描述rocminfo能看到 ROCm 运行时识别出的 GPU agent。如果你看到Agent 1: AMD Instinct MI210或者类似的 Radeon 设备说明硬件和驱动层面已经通了一半。rocminfo里还会显示该 GPU 是否支持 bf16 和 fp16这对后续 LoRA 训练的精度选择很关键。如果lspci就查不到设备那就别急着装软件了先解决设备可见性问题。云实例的话先查规格说明确认你的实例类型是不是真的带 GPU 直通物理机的话大概率需要去 BIOS 里开 Above 4G Decoding 和 Resizable BAR。这两个选项不开Linux 内核可能无法正确映射一大段显存地址。3.2 安装 PyTorch 的 ROCm wheel驱动和 ROCm Toolkit 装好之后最核心的一步是装对 PyTorch。这里有个极其容易踩的坑直接用pip install torch会装到 CPU 版或者 CUDA 版。CPU 版的问题不用多说CUDA 版在 AMD 卡上虽然能 import但一跑就报hipErrorNoDevice非常迷惑。正确的安装命令是pip install torch2.4.1rocm6.1 torchvision torchaudio \ --index-url https://download.pytorch.org/whl/rocm6.1版本号要和你的 ROCm 版本对得上。比如你装了 ROCm 6.2就用rocm6.2后缀的 wheelROCm 6.1 就对应rocm6.1。我这次用的是 ROCm 6.1 PyTorch 2.4.1 的组合实测很稳。这里必须啰嗦一句一定要用torch.version.hip这个属性来做验证。python -c import torch; print(torch.__version__); print(torch.version.hip); print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))ROCm 环境下torch.cuda.is_available()返回True是正常的PyTorch 内部把 HIP 设备抽象成了 CUDA 接口所以很多代码层面你依然在写.cuda()。torch.version.hip则应该返回类似6.1的字符串。如果是None说明你装成了 CPU 版趁早重装。3.3 训练前先跑一次纯推理环境装好之后强烈建议别急着进训练流程先跑一次小推理。加载模型随便输入几条中文文本看生成是否正常。这一步能提前暴露大量问题分词器是否加载正确、显存是否不足、bf16 是否被硬件正确支持。我在坑 3 之前其实就是在这一步发现了一个更基础的问题后面会细讲。总之把“能推理”作为训练前的验收标准能省下你后面 debug 的几个小时。4. 情绪 LoRA 微调的数据准备、模型加载与完整参数4.1 为什么用 LoRA 而不是全参微调全参微调 20 亿参数的模型不是不行但要存的优化器状态、梯度、参数副本会把显存和训练时间都推高一大截。LoRA 的思路是保持原始模型权重冻结在 attention 层的线性投影旁插入低秩矩阵。训练的时候只更新这几个低秩矩阵显存占用降到全参的零头训练速度也更快。我的实际感受是对于情绪分类这种单任务垂直场景LoRA 的效果和全参微调差别很小但工程成本差别巨大。为什么要用 Unsloth 而不是纯 PEFT我做个简单对比维度Unsloth LoRAPEFT TransformersROCm 适配有预编译算子优化开箱即用需要自己确认算子兼容性训练速度在 AMD 卡上实测快 40% 左右常规速度显存占用有优化支持更长的 max_seq_length常规灵活性封装好适合快速迭代控制更精细适合高级用户如果你是要做严谨的对比实验或者需要自定义 Trainer 内部逻辑那就用 PEFT如果是快速验证业务效果Unsloth 几乎是我现在唯一推荐的选择。4.2 模型加载与 LoRA 参数配置我用的是 Gemma4 2B base 版权重放在本地路径/models/gemma4-2b-base。注意base 版没有经过指令微调直接做文本生成时经常答非所问这正好用来验证 LoRA 微调能把它的行为拉回“分类”这个任务空间。加载和挂 LoRA 的代码如下import torch from unsloth import FastLanguageModel model, tokenizer FastLanguageModel.from_pretrained( model_name/models/gemma4-2b-base, max_seq_length256, dtypetorch.bfloat16, load_in_4bitFalse, ) model FastLanguageModel.get_peft_model( model, r16, target_modules[q_proj, k_proj, v_proj, o_proj], lora_alpha32, lora_dropout0.05, )target_modules这里只选了 attention 里的四个线性层。为什么不加 feed-forward 层因为任务只需要模型做句子级的语义压缩attention 层是信息混合的核心把 LoRA 加到 FFN 上也能提升一些效果但显存和训练时间都会涨。对于 2B 规模的模型我的经验是先把 attention 四件套跑通看效果不够再往上加。几个参数的取值逻辑也和大家说一下r16低秩矩阵的秩。秩越高可学习参数越多但并非越高越好小模型上 r16 到 r32 之间差别不大再往上就明显过拟合。alpha32缩放比例。一般取 r 的 2 倍作用是让训练初期 LoRA 对原模型的影响保持在一个温和的尺度。dropout0.05LoRA 层的 dropout。分类任务样本够多dropout 太低容易过拟合太高则欠拟合。max_seq_length256中文学短语料绝大多数在 256 token 以内。这里有个和坑 4 相关的伏笔训练和推理的截断长度必须保持一致后面细说。4.3 数据格式与训练超参数数据格式很简单每条样本就是一段文本加一个整数标签{ text: 看到这个结果我真的无语了等了半天就等来一句敷衍回复, label: 0, # 0愤怒 }数据加载的时候用 HuggingFace 的Dataset.map做分词把text编码成input_ids、attention_mask和labels。注意这里的labels是给因果语言模型用的标签和分类的label不是同一个东西。我这次训练依然走 next-token prediction 的方式把 “文本 情绪标签词” 拼成一个序列让模型去预测那个情绪标签词。例如对于愤怒样本训练序列就是 “文本内容\n【情绪】愤怒”模型学习预测“愤怒”这个 token。这种方式的好处是复用因果语言模型的训练流程不需要为分类任务重新设计损失函数。完整训练参数如下参数取值说明训练集大小31000 条清洗后per_device_train_batch_size8单卡每步 8 条gradient_accumulation_steps4有效 batch 32learning_rate2e-4LoRA 常用学习率lr_scheduler_typecosine收敛更平滑warmup_ratio0.05先热身再加速num_train_epochs3大约 2900 步bf16TrueMI210 支持良好gradient_checkpointingTrue省显存速度略降训练命令如果走的是transformers.Trainer直接把这些参数传给TrainingArguments就行。Unsloth 也给了对应的UnslothTrainingArguments但本质上没有太大区别。5. 训练过程实录与 0.734 的验证结果5.1 loss 曲线和训练中的观察训练大概持续了 50 多分钟跑完 3 个 epoch。loss 曲线的形态比较健康一开始从 0.45 左右快速下降到第 400 步左右降到 0.3 附近之后减速到第 1500 步之后降到 0.21 上下最后基本平了。这里有个值得警惕的现象loss 降到 0.2 附近后验证集准确率是在 0.72 和 0.73 之间来回小幅波动的没有继续上涨。这说明模型已经接近这个数据规模下的表征上限。我中间试过把lr降到 1e-4 再续训 500 步验证集准确率只涨了不到 0.01果断放弃还是保留最初的权重最干净。训练时的显存表现是峰值约 22GB 出头我这次云实例给了 48GB很宽裕。如果换成 24GB 的卡同样参数也能跑但要关掉gradient_checkpointing以外的其他冗余缓存或者把per_device_train_batch_size降到 4。LoRA 的优势在这里体现得淋漓尽致冻结底模之后激活值才是显存大头而激活值可以通过梯度检查点技巧大幅压缩。5.2 测试集上的完整结果训练结束后我在那 4000 条没碰过的测试集上做了完整评估。准确率 2936 / 4000 0.734正好对应标题里的数字。相比基线 0.594提升了 14 个百分点的绝对准确率这个提升幅度对于单任务 LoRA 来说是实打实可用的。分类别看macro F1 从 0.49 提升到了 0.66。具体各类别的 F1 如下类别基线 F1LoRA 微调后 F1愤怒0.580.83快乐0.530.78悲伤0.470.73恐惧0.310.63中性0.520.70提升最大的两个类别是“恐惧”和“中性”。恐惧类在基线上几乎是一塌糊涂因为生成式模型很难主动输出“恐惧”这个低频词LoRA 用大量样本把输出分布拉向了这个方向让它不再畏惧输出“恐惧”。中性类则更多是决策边界的问题基线模型太容易把中性的文本误判成某种情绪微调之后模型学会了“没有明显情绪就是中性”这个判别逻辑。5.3 错误样本的复盘我抽样看了 200 条错误样本发现一个规律大约 80% 的错误集中在“愤怒 vs 中性”和“悲伤 vs 中性”这两组边界上。这其实不难理解因为文本情绪往往是渐变的。比如“项目搞得一团糟但我已经不想说什么了”这句话人看是“悲伤”多一点“中性”也不算错模型判错是有迷惑性的。这类问题靠 LoRA 继续压已经压不出多少效果了下一步可以考虑把类别体系改成层级结构或者引入“强度”回归作为辅助任务。不过那就是另一个项目了。6. 四个坑的完整排查链路这一章是标题里的重头戏我从现象、排查过程、根因、解决办法四步走完整还原每一个坑。6.1 坑 1lspci 查不到 AMD GPU驱动装了和没装一样现象非常恐怖新开的云实例SSH 上去执行lspci | grep -i amd无反应一个字符都不带显示的。执行rocminfo也直接报No ROCm agents found。我当时心想这卡是不是坏了或者云厂商给的东西不对。排查过程分三步走。第一步执行完整版lspci -nn | grep -i display\|VGA看有没有其他显示设备被占用。第二步查内核日志dmesg | grep -i amdgpu | tail -30如果日志里只有一行amdgpu: Initialized amdgpu而没有后续的设备初始化信息那基本可以断定内核在为设备分配资源时卡住了。第三步查一下 PCIe 枚举用的是不是正常的 BAR 地址范围。根因有两条路径。云实例的话很多时候是因为你买的实例规格并不包含 GPU 直通PCIe 设备根本没有暴露给操作系统lspci 当然查不到。物理机或者裸机服务器则很可能需要在 BIOS 里开启 Above 4G Decoding 和 Resizable BAR。AMD GPU 的显存映射范围超过 4GB 之后如果这两个选项没开Linux 内核会拒绝映射设备的 BAR 空间于是设备就从 PCI 枚举列表里消失了。解决与验证如果是云实例换成“GPU 加速型”或“GPU 直通型”规格如果是物理机进 BIOS 开启上述两个选项。重启之后再次执行lspci -nn | grep -i amd看到 AMD 显卡设备后再跑rocminfo就能看到 GPU agent 了。这个坑的迷惑性在于驱动明明装好了modinfo amdgpu也正常但你就是找不到设备。6.2 坑 2PyTorch 检测不到设备报 hipErrorNoDevice当rocminfo能正常看到 GPU 之后我本来以为高枕无忧了。结果跑了一段测试代码报错HIP error: hipErrorNoDevice: no device。当时我第一反应是 ROCm Toolkit 版本不对折腾了半天版本对齐毫无进展。排查过程先确认torch.__version__输出是2.4.1cpu。我瞬间明白了PyTorch 是 CPU 版。再验证一下python -c import torch; print(torch.version.hip)输出是None。这基本实锤。为什么 CPU 版会报 HIP 错误而不是老老实实说不支持 GPU因为代码里用了.cuda()CPU 版的 CUDA/HIP 本身就是缺失的于是底层抛出一个无关痛痒的 HIP 错误极其误导人。根因安装 PyTorch 时没有指定rocm版本的 index-url。很多人包括当时的我习惯了pip install torch但这条命令默认装的是 CPU 版或者通用版在 AMD 卡上没有任何意义。解决卸载之后重装 ROCm 版pip uninstall torch torchvision torchaudio pip install torch2.4.1rocm6.1 torchvision torchaudio \ --index-url https://download.pytorch.org/whl/rocm6.1装完再跑python -c import torch; print(torch.version.hip)看到输出6.1的那一刻心里才踏实。这个坑提醒我每次新建环境装 PyTorch第一件事永远是看torch.version.hip而不是torch.cuda.is_available()。6.3 坑 3分类头池化位置错误LoRA 几乎白训训练一切正常loss 下降得很平滑我以为稳了。结果我把模型接到验证集上做分类评估准确率只有 0.62比基线 0.594 也强不到哪去。这显然不正常loss 都降了一个档次准确率不可能只涨这么点。排查过程我抽了十几条验证集样本把模型输出的 logits 打印出来看分布。发现预测几乎是平均分布的而且个别样本的 logits 非常接近全零。我意识到问题出在把 hidden_states 聚合成句子向量的方式上。我最初写的分类头是从 BERT 迁移过来的习惯直接取hidden_states[:, 0]也就是第一个 tokenBOS对应的向量。根因Gemma 是 decoder-only 架构第一个 token 的 hidden state 并不像 BERT 的 CLS token 那样专门聚合了全局句子语义。在因果注意力机制下第一个 token 的位置几乎只代表“序列开始”没有太多有意义的信息。正确做法是取最后一个有效 token 对应的 hidden state因为它能通过注意力看到序列里所有前面的 token。我把分类头的聚合逻辑改成按attention_mask找到最后一个非 padding 位置def forward(self, hidden_states, attention_mask): last_idx attention_mask.sum(dim1) - 1 pooled hidden_states[torch.arange(hidden_states.size(0)), last_idx] return self.fc(pooled)改完之后再评估验证集准确率直接跳到 0.72 附近。这个坑最值得说的点是训练 loss 完全正常的情况下下游任务效果差问题不一定在 LoRA 参数或者数据质量上很可能出在任务头和模型结构的匹配上。下次你换任何 decoder-only 模型做分类任务先检查你取的是哪个位置的 hidden state。6.4 坑 4DataLoader 并发太高GPU 利用率上不去训练跑得很稳定但速度一直不对。60 个 step 跑了将近 6 分钟我预期里应该一半时间就够了。我用rocm-smi看了一眼 GPU 利用率只有 40% 左右而且经常掉到 30% 以下。GPU 干活的时间没多少CPU 反而忙得不行。排查过程先用rocm-smi --showuse确认 GPU 利用率波动再用htop看 CPU 进程。发现 12 个 DataLoader worker 全部处于忙乱状态CPU 占用极高。我一开始以为数据预处理太复杂后来意识到是 DataLoader 的并发和 ROCm 的上下文切换在打架。根因ROCm 环境下num_workers 开太多并不一定带来数据加载加速。尤其是机器只有有限的内存带宽和 PCIe 通道时12 个 worker 同时做分词、pad、batch 拼接反而把内存拷贝和 GPU 传输的带宽给抢占了。我实际试验下来num_workers4比num_workers12速度快不少。解决方式training_args.dataloader_num_workers 4 training_args.dataloader_pin_memory False这里要提醒一下pin_memoryTrue在 CUDA 环境下通常是有益的但在 ROCm 上并不一定至少我这次实测关闭之后 GPU 利用率稳定到了 85% 以上整轮训练时间从 1 小时 20 分钟缩短到了 52 分钟。如果你的训练速度异常慢不要先怪模型太大先看一眼数据加载链路在干什么。7. 从 Gemma4 到 Qwen、Llama这套流程能复用的部分训练跑通之后我把这套流程在 Qwen 2.5 1.5B 上又跑了一遍确认它不是 Gemma4 专属的巧合。模型的架构大同小异都是标准 decoder-only所以 LoRA 的核心配置基本可以直接搬。唯一要注意的是 target modules 的名字各家模型对 attention 层的命名略有不同。Gemma 是q_proj/k_proj/v_proj/o_projQwen 是q_proj/k_proj/v_proj/o_proj加gate_proj/up_proj/down_projLlama 大体也是后者。实际做的时候可以先打印模型结构确认一下免得 LoRA 挂了个寂寞。tokenizer 的差异也要注意。Qwen 没有句首的 BOS token 概念Gemma 则有这直接影响了坑 3 里“last token”位置的判断。用attention_mask.sum(dim1) - 1这种通用计算方法不管哪家 tokenizer 都能正确取到最后一个有效 token比硬编码hidden_states[:, -1]更稳妥。给想复现这套流程的朋友一张最小检查清单lspci -nn | grep -i amd能看到 GPU 设备。rocminfo能看到至少一个 GPU agent。torch.version.hip返回类似6.1的字符串不是None。先用纯推理加载模型跑通一条中文样本再进训练。分类任务确认你取的 hidden state 是最后一个有效 token不是[:, 0]。训练和推理用同一个max_seq_length避免截断不一致影响评估。先用 500 条数据的小样本跑 100 步确认 loss 在降、GPU 利用率正常再全量训练。这套流程里我最大的体会是ROCm 环境并不可怕真正可怕的是“假成功”——训练 loss 正常下降但下游效果不涨GPU 看起来在跑但利用率上不去报错信息跟实际原因完全对不上。遇到这种情况最有效的办法就是把链路切成小段逐段验证。情绪分类这个场景跑通之后我现在已经把同样的管线用到另一个垂直文本分类任务上从环境准备到拿到第一个有效结果只花了一个下午。LoRA 微调这件事只要过了环境门槛和数据规范这两关剩下的就是迭代而已。
网站建设高端定制企业官网