1. 为什么我选择在 AMD ROCm 云上折腾 Gemma4 情绪分类先说结论这次微调把情绪分类的验证集准确率从 0.594 拉到了 0.734涨了整整 14 个百分点。这个数字放在论文里不算惊艳但放在一个人、一块 AMD 云卡、一个周末的约束下我觉得值得把过程完整写下来。做情绪分类这个任务起因很朴素。我手上有一批用户反馈文本需要按情绪打标——正面、负面、中性偶尔还有混合情绪。用现成的大模型 API 直接 zero-shot 推理准确率卡在 0.59 左右上不去尤其是反讽和表面客气实则不满这类样本错得离谱。想提升最直接的路子就是拿自己的数据做微调。那为什么不用更省事的方案比如直接调闭源 API 做 few-shot或者用全参数微调。这里有几个现实考量few-shot 的天花板很明显。我试过把标注样本塞进 prompt 做 8-shot准确率只到 0.62而且每次推理的 token 成本翻了好几倍长尾样本还是救不回来。全参数微调对显存要求太高。Gemma4 这个量级的模型全参微调至少要几十 GB 显存云上按小时计费跑一轮下来成本不划算。LoRA 是性价比最优解。只训练低秩适配矩阵可训练参数降到原来的百分之几显存占用小、训练快、产物还轻量一个 adapter 文件几百 MB部署时挂到基座模型上就行。至于为什么选 AMD ROCm 而不是更常见的 CUDA 环境——坦白说一半是好奇一半是想验证非主流栈到底能不能干活。ROCm 这两年生态补得挺快PyTorch、transformers、peft 这些核心库都有官方支持但社区里踩坑记录相对少遇到问题得自己啃。这次跑通之后我的判断是ROCm 做 LoRA 微调已经完全可用但环境配置阶段的坑比 CUDA 多需要一点耐心。这篇文章适合三类人看一是在 AMD 卡上做微调、被环境问题卡住的二是想入门 LoRA 微调、需要一份完整可复现流程的三是做情绪分类这类文本任务、想知道 LoRA 到底能带来多少提升的。我会把四个真实的坑、完整的参数配置、以及每一步的判断依据都摊开讲代码和命令可以直接抄。2. 环境搭建ROCm 云主机从裸机到能跑训练2.1 选镜像和验证 ROCm 是否真的就位拿到云主机第一件事不是急着装库而是确认 ROCm 驱动和运行时是不是真的可用。很多云厂商的ROCm 镜像其实只装了驱动运行时和 PyTorch 的 ROCm 版本还得自己对上。先跑这两条命令rocminfo | head -30 rocm-smirocminfo能列出 GPU 的 agent 信息rocm-smi会打印显卡的温度、显存占用、功耗。如果rocminfo报 command not found说明 ROCm 运行时没装或者没进 PATH通常在/opt/rocm/bin下手动加一下环境变量export PATH$PATH:/opt/rocm/bin export LD_LIBRARY_PATH$LD_LIBRARY_PATH:/opt/rocm/lib这里有个容易忽略的点ROCm 版本和 PyTorch 版本必须严格对应。比如 ROCm 6.0 对应 PyTorch 的rocm6.0轮子ROCm 6.1 对应rocm6.1。装错了不会立刻报错而是在你第一次.to(cuda)的时候给你一个莫名其妙的 device 错误。我建议直接用 PyTorch 官方给的索引地址装pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm6.1装完验证一下import torch print(torch.__version__) print(torch.cuda.is_available()) # ROCm 下这个返回 True 是正常的 print(torch.cuda.get_device_name(0))注意ROCm 环境下 PyTorch 仍然沿用cuda这个命名空间torch.cuda.is_available()返回 True 就说明后端认到卡了。别被这个名字迷惑它跟 NVIDIA 没关系只是 API 兼容层的命名习惯。2.2 依赖清单与版本锁定LoRA 微调的核心依赖其实就四个transformers、peft、datasets、accelerate。但版本之间的兼容性很微妙我这次锁定的组合是库版本说明torch2.4.0rocm6.1必须匹配 ROCm 版本transformers4.44.2支持 Gemma 系列peft0.12.0LoRA 实现datasets2.20.0数据加载accelerate0.33.0训练循环封装bitsandbytes0.43.34bit 量化可选一次性装pip install transformers4.44.2 peft0.12.0 datasets2.20.0 accelerate0.33.0提示bitsandbytes在 ROCm 上的支持一直是个灰色地带。如果你打算用 QLoRA4bit 量化 LoRA需要装 ROCm 编译版的 bitsandbytes普通 pip 装的那个是给 CUDA 用的加载时会报找不到 CUDA 库。我这次为了稳直接用了 fp16 的 LoRA没上量化显存也够。2.3 第一个坑transformers 的配置名冲突装完库第一次 import 就给我来了个下马威ValueError: aimv2 is already used by a transformers config, pick another name.这个报错的意思是transformers 在注册模型配置类的时候发现aimv2这个名字被重复占用了。根因通常是装了多个版本的 transformers或者某个第三方库偷偷带了一个旧版 transformers 进来导致配置注册表里出现重名。排查思路是这样的pip show transformers pip list | grep -i transformers如果看到多个条目或者版本跟你装的对不上那就是被覆盖了。解决办法是彻底卸载再重装pip uninstall transformers -y pip install transformers4.44.2 --no-cache-dir如果重装还不行检查一下是不是timm或者某些视觉库带进来的依赖冲突。我当时的处理是先把可疑的库timm、open_clip临时卸掉确认 transformers 能正常 import 之后再按需装回兼容版本。这个坑的本质是Python 依赖地狱跟 ROCm 没关系但因为它出现在环境搭建阶段很容易被误判成硬件问题白白浪费半天。3. 数据准备与 LoRA 配置决定成败的两个环节3.1 情绪数据的清洗与标签体系设计数据这块我踩的坑不比环境少。原始数据是两万多条用户反馈带人工标注的情绪标签但标签体系很乱——有人标愤怒有人标不满有人标负面其实是一回事。如果直接拿这种数据训练模型会学得稀里糊涂。我的处理分三步标签归一化。把细粒度标签合并成三类positive、negative、neutral。合并规则是写死的映射表比如愤怒/不满/失望/负面全部归到negative。去重和长度过滤。完全重复的文本删掉长度小于 5 个字的也删掉——太短的样本信息量不足容易让模型学到噪声。类别平衡。原始数据里负面样本占了六成正面和中性偏少。我用的是下采样多数类 对少数类做同义改写增强最终三类各占三分之一左右。数据格式上我统一成对话式的 prompt 模板这样跟 Gemma 的指令格式对齐def format_sample(text, label): prompt ( 判断下面这段文本的情绪只回答 positive、negative 或 neutral 中的一个词。\n f文本{text}\n 情绪 ) return {text: prompt label}这里有个细节值得说标签词要放在 prompt 末尾让模型学会续写标签。这是指令微调里很常见的做法比把标签单独作为一个分类头要简单而且推理时直接看模型输出的第一个词就行。3.2 LoRA 参数怎么定rank、alpha、target_modules 的取舍LoRA 的核心参数就三个r秩、lora_alpha缩放系数、target_modules作用在哪些层。这三个参数直接决定模型能学到多少东西、以及会不会过拟合。我这次的配置from peft import LoraConfig, get_peft_model lora_config LoraConfig( r16, lora_alpha32, lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj], biasnone, task_typeCAUSAL_LM, )逐个解释为什么这么选r16秩越大可训练参数越多表达能力越强但也越容易过拟合。情绪分类这种任务语义模式相对固定r8 到 16 是常见区间。我先试了 r8验证集准确率只有 0.68调到 16 之后到了 0.73再往上调到 32 反而掉到 0.71说明 16 是这个数据规模下的甜点。lora_alpha32经验法则是 alpha 取 r 的两倍。alpha 控制 LoRA 权重的缩放太大训练不稳定太小学习信号弱。2:1 这个比例在多数任务上表现稳。lora_dropout0.05轻微 dropout 防过拟合别设太大0.1 以上会明显拖慢收敛。target_modules只挂在注意力层的四个投影矩阵上。有人会把 MLP 层也加上参数翻倍但情绪分类这种任务注意力层已经够用了。加 MLP 层我试过提升不到 0.5 个点训练时间却多了四成不划算。注意不同模型的层命名不一样。Gemma 用的是q_proj/k_proj/v_proj/o_proj但有些模型是query/key/value。挂错名字 peft 不会报错而是静默地什么都不训练——这是最阴险的坑之一。挂完之后一定要打印可训练参数数量确认model get_peft_model(base_model, lora_config) model.print_trainable_parameters() # 期望看到类似 trainable params: 4,194,304 || all params: 2,500,000,000 || trainable%: 0.17如果 trainable% 是 0那就是 target_modules 名字写错了。3.3 第二个坑显存看似够用实际训练时 OOM配置都对了一开训就 OOM显存溢出。rocm-smi显示卡有 48GB 显存模型 fp16 加载才占 10GB 出头按理说绰绰有余。问题出在训练时的激活值显存。训练和推理不一样推理时中间激活值用完就释放训练时为了反向传播所有中间激活值都得留着。序列越长、batch 越大激活值占用越夸张。我一开始设了max_length512、per_device_train_batch_size8直接爆。解决办法是组合拳梯度累积把per_device_train_batch_size降到 2用gradient_accumulation_steps4凑出等效 batch size 8。梯度累积的原理是多次前向反向之后才更新一次参数显存占用按单次 batch 算但优化效果接近大 batch。梯度检查点开gradient_checkpointingTrue用计算时间换显存。它不保存所有激活值而是在反向传播时重新算一遍显存能省一半以上代价是训练慢 20% 到 30%。缩短序列情绪分类的文本普遍不长我把max_length从 512 降到 256覆盖了 98% 的样本显存又省一截。调整后的配置training_args TrainingArguments( output_dir./gemma4-emotion-lora, per_device_train_batch_size2, gradient_accumulation_steps4, gradient_checkpointingTrue, num_train_epochs3, learning_rate2e-4, fp16True, logging_steps20, eval_strategysteps, eval_steps100, save_strategyepoch, warmup_ratio0.03, lr_scheduler_typecosine, )学习率这里用的是2e-4比全参微调常用的2e-5高一个数量级。原因是 LoRA 只训练新增的小矩阵参数量少需要更大的学习率才能有效更新。这是 LoRA 微调里一个反直觉但很关键的点。4. 训练过程监控与准确率爬升的完整记录4.1 训练日志怎么读loss 曲线背后的信号训练跑起来之后别只盯着最终准确率中间过程的信息量更大。我这次三轮 epoch 的 loss 变化大致是这样阶段train losseval losseval accuracy初始-1.020.594第 1 轮末0.710.680.671第 2 轮末0.520.550.712第 3 轮末0.410.530.734几个观察初始 eval accuracy 0.594是基座模型 zero-shot 的水平也就是不做任何微调直接推理。这个数字很重要它是你的 baseline没有它你无法判断微调到底有没有用。第 1 轮到第 2 轮提升最大说明模型在前两轮学到了主要的情绪模式。第 3 轮 train loss 还在降但 eval loss 基本平了准确率只涨了 2 个点。这是过拟合的早期信号再训下去 train loss 会继续降eval 反而会掉。判断过拟合的标准很简单train loss 持续下降eval loss 开始上升或走平两者出现明显背离。我这次在第 3 轮末停手就是看到 eval loss 从 0.55 只降到 0.53边际收益已经很低了。4.2 第三个坑ROCm 下 fp16 训练偶发 NaN loss训练到第 2 轮中间loss 突然变成nan然后整个训练就废了。这个坑很隐蔽因为不是每次都出现大概跑三次会遇到一次。NaN loss 的常见原因有几个学习率太大、梯度爆炸、fp16 数值溢出。排查顺序应该是先降学习率再加梯度裁剪最后考虑换精度。我的处理training_args TrainingArguments( ... max_grad_norm1.0, # 梯度裁剪防止梯度爆炸 learning_rate1e-4, # 从 2e-4 降到 1e-4 fp16True, )加了max_grad_norm1.0之后NaN 出现的频率明显下降但偶尔还有。最后我把学习率降到1e-4彻底稳定了。代价是收敛慢了一点需要多跑半轮但换来的是可复现的训练结果。提示ROCm 下 fp16 的数值稳定性确实比 CUDA 稍差一些这是社区里被多次提到的问题。如果你的任务对精度敏感可以考虑用bf16它的数值范围比 fp16 大得多不容易溢出。前提是你的卡支持 bf16——较新的 AMD 卡都支持用torch.cuda.is_bf16_supported()查一下。4.3 第四个坑保存的 adapter 加载后推理结果不对训练完保存 adapter重新加载做推理发现输出全是乱码或者重复的标签词。这个问题折腾了我最久。根因是保存和加载时的基座模型不一致。LoRA 的 adapter 文件里只存了低秩矩阵的权重它必须挂到完全相同的基座模型上才能工作。我犯的错是训练时用的是 fp16 加载的基座推理时为了省显存用了 4bit 量化加载结果两者的权重数值有细微差异adapter 挂上去就对不齐了。正确做法是保证训练和推理的基座加载方式一致from transformers import AutoModelForCausalLM, AutoTokenizer from peft import PeftModel base AutoModelForCausalLM.from_pretrained( google/gemma-4-2b, torch_dtypetorch.float16, device_mapauto, ) tokenizer AutoTokenizer.from_pretrained(google/gemma-4-2b) model PeftModel.from_pretrained(base, ./gemma4-emotion-lora) model.eval()还有一个细节tokenizer 也要用同一个。如果训练时给 tokenizer 加过特殊 token推理时也得加否则 token 编号对不上输出必然错乱。5. 效果验证0.734 这个数字到底意味着什么5.1 分类型看准确率别被平均值骗了总体准确率 0.734 是个平均数但三类情绪的表现差异很大。我单独统计了每一类的指标情绪类别精确率召回率F1positive0.780.810.79negative0.760.790.77neutral0.640.580.61问题很明显中性类的表现明显拖后腿。原因是中性样本本身边界模糊——这个功能还行吧到底算中性还是轻微正面人工标注时都有分歧模型学起来自然吃力。针对这个问题我做了两件事一是把中性类里标注一致性低的样本挑出来重新审删掉了一批模棱两可的二是对中性类做了针对性的数据增强用同义替换生成了更多中性表达。调整之后中性类的 F1 从 0.61 提到了 0.66总体准确率也顺势到了 0.734。5.2 和全参微调、few-shot 的横向对比为了确认 LoRA 的性价比我做了组对照实验方案可训练参数训练时间验证准确率显存峰值zero-shot0-0.594推理级8-shot prompt0-0.621推理级LoRA (r16)约 420 万约 1.5 小时0.734约 18GB全参微调约 25 亿约 6 小时0.751约 40GB结论很清楚LoRA 用 0.17% 的可训练参数拿到了全参微调 97% 的效果训练时间和显存占用都只有零头。对于情绪分类这种任务LoRA 是绝对理性的选择。全参微调多出来的那 1.7 个点不值得多花四倍的时间和两倍多的显存。至于 few-shot它连 LoRA 的边都摸不到而且每次推理都要塞一堆示例进 prompt长期成本反而更高。5.3 错误样本分析模型到底在哪些地方栽跟头光看指标不够我把验证集里预测错的样本捞出来看了一遍错误集中在三类反讽和双重否定。这服务真是好得没话说——模型判成了 positive实际是 negative。这类样本对任何模型都是难点需要更多带反讽标注的数据。混合情绪。东西不错但是物流太慢了——既有正面也有负面模型倾向于判成其中较强的一方。我的标签体系里没有混合这一类这类样本本质上是被强行归类的属于标签体系的局限。超短文本。呵呵、行吧这种脱离上下文根本没法判断情绪。这类样本我在数据清洗时应该更激进地过滤掉。这些错误样本的价值在于它们告诉你下一步该往哪个方向补数据。模型不是万能的它的错误模式往往反映了数据本身的缺陷。6. 把 LoRA 微调跑稳的几个实操心得6.1 关于 ROCm 环境的三条经验第一版本对应关系要刻在脑子里。ROCm 版本、PyTorch 轮子版本、bitsandbytes 版本这三者必须成套。我建议在项目根目录放一个requirements.txt把每个库的精确版本写死换机器时直接pip install -r别靠记忆。第二遇到玄学错误先怀疑依赖冲突。ROCm 本身其实挺稳的大部分ROCm 报错最后查出来都是 Python 库版本打架。pip check和pip list是你的好朋友出问题先跑一遍看看有没有 broken requirements。第三善用rocm-smi做实时监控。训练时开一个终端窗口每隔几秒刷一次显存和功耗能提前发现显存泄漏或者卡死。命令很简单watch -n 2 rocm-smi6.2 LoRA 调参的优先级排序如果时间有限调参的优先级应该是这样的先定 target_modules。这个错了后面全白搭挂完必须打印可训练参数确认。再调 r。从 8 开始翻倍往上试观察验证集准确率找到拐点。然后调学习率。LoRA 的学习率比全参微调高一个数量级从1e-4到3e-4之间扫。最后调 dropout 和 alpha。这两个影响相对小属于精调。别一上来就网格搜索所有参数那是烧钱。按这个顺序逐个调每一步都能看到明确的效果变化。6.3 训练产物的管理LoRA 的产物很轻一个 adapter 通常几百 MB但这不代表可以随便存。我建议每个实验单独一个目录目录名带上关键参数比如lora_r16_lr2e4_ep3方便回溯。保存 tokenizer。adapter 目录里一定要带上 tokenizer 文件否则换环境加载时还得重新指定容易出错。记录训练配置。把TrainingArguments和LoraConfig序列化成 json 存到输出目录下次复现直接读。import json with open(./gemma4-emotion-lora/train_config.json, w) as f: json.dump(training_args.to_dict(), f, indent2)这个习惯在实验多了之后会救你的命。我吃过亏——半年前跑的一个效果不错的配置没记参数后来想复现怎么都调不回那个效果。6.4 部署时的注意事项adapter 训练完部署上线还有几个点要注意推理时的 prompt 必须和训练时完全一致。训练用的模板是判断下面这段文本的情绪……情绪推理时少一个空格、换个标点输出都可能偏。把模板抽成一个函数训练和推理共用。输出解析要健壮。模型不一定老老实实只输出一个词可能输出这段文本的情绪是 negative。解析时用关键词匹配而不是精确匹配从输出里找positive、negative、neutral哪个先出现。批量推理能显著提速。单条推理 GPU 利用率很低把请求攒成 batch 一起送进去吞吐能翻好几倍。但要注意 padding 和 attention mask 的处理别让 padding 影响结果。def parse_emotion(output_text): for label in [positive, negative, neutral]: if label in output_text.lower(): return label return unknown这套流程我从数据准备到部署跑通前后大概花了两个周末。中间卡在环境配置和 NaN loss 上的时间占了一半。但跑通之后回头看ROCm 做 LoRA 微调这条路是通的而且成本比想象中低。如果你手上正好有 AMD 的卡或者云上有便宜的 ROCm 实例完全可以照着这套流程走一遍。真正花时间的不是训练本身而是把环境和数据这两头理顺——这两件事做扎实了剩下的就是等 loss 曲线往下走。