拓冰建站拓冰建站
首页 / 资讯中心 / 正文

海光DCU上跑通ChatGLM3 LoRA微调:从环境搭建到训练实战

1. 方案先想明白为什么微调任务首选 LoRA拿到“在海光 DCU 上跑通第一个微调任务”这个目标我的第一反应不是急着敲命令而是先把方案定下来。因为很多人第一次上手就卡在两件事上一是连环境都没装起来二是装好了环境却不知道选什么微调方式结果训练了半天 loss 纹丝不动。抛开这些坑不谈先说结论——第一次跑微调强烈建议用 LoRA 而不是全量微调。海光 DCU 本质上是类 ROCm 生态的加速卡当前大多数开源模型在它上面跑全量微调很容易爆显存。全量微调意味着所有模型参数都要参与梯度更新一个 7B 模型光权重就得占 14GB 以上再加上优化器状态、梯度、中间激活值没有 64GB 显存根本玩不转。而 LoRA 的做法是冻结原始权重只训练额外插入的一小部分低秩矩阵参数量通常只有原来的 0.1% 到 1%7B 模型最低只需要 16GB 左右显存就能跑起来。从任务适配度来看如果你只想让模型学会某种风格、某个垂直领域的术语、或者对齐某种输出格式LoRA 完全够用。微调后的权重文件也小得多一个 rank8 的 LoRA 适配器往往只有几十 MB合并回原模型也方便部署成本低。我第一次在海光 DCU 上跑的就是 ChatGLM3-6B 的 LoRA 微调最后 loss 能稳定下降到 0.8 左右效果足够用于垂直问答场景。在动手之前我还特意对比了几种常见方案方案参数量/显存占用训练速度适用场景首次上手推荐度全量微调极高需多卡并行慢领域跨度大、数据量大不推荐LoRA低单卡可跑快垂直场景风格适配强烈推荐P-Tuning v2较低较快提示学习、少样本可以尝试冻结仅训练分类头最低最快分类任务不适合生成式模型选 LoRA 还有一个隐藏好处它能顺便帮你把 DCU 的 ROCm 环境、PyTorch 适配层、算子执行效率都摸一遍但出错的环节被控制在一个相对小的范围内。如果训练失败排查方向会清晰很多——无非是数据管线和 LoRA 层配置的问题而不是整个模型权重更新链路的锅。2. 装环境的完整步骤DCU 驱动到 PyTorch 一条龙2.1 先确认 DCU 驱动和 ROCm 状态买来一台带海光 DCU 的机器第一步不是装 PyTorch而是确认驱动和 ROCm 运行时是否就绪。很多新手一上来就 pip install torch等到torch.cuda.is_available()返回 False 或者根本找不到设备时才傻眼。在终端里先跑这样一条命令看加速卡设备列表rocm-smi这条命令类似 NVIDIA 的nvidia-smi会把 DCU 的温度、显存占用、设备编号列出来。能看到设备列表说明驱动层和 ROCm 内核模块基本没问题。如果命令不存在大概率是 ROCm 没装全或者需要先加载内核模块常见的模块名是amdgpu可以用lsmod | grep amdgpu检查。接着确认 ROCm 的版本号cat /opt/rocm/.info/version我当时机器上装的 ROCm 是 5.7.x这个信息后面选 PyTorch 版本时很关键。不同 ROCm 版本对 PyTorch 的兼容性有差异版本对不上后面运行算子时会报hipErrorNoBinaryForGpu之类的错代码写得再好也白搭。2.2 安装适配 DCU 的 PyTorch这一步是最容易踩坑的地方。海光 DCU 的软件生态走的是 ROCm 路线但它内部的实现又和 AMD GPU 不完全一样所以官方针对 DCU 发布了定制版 PyTorch。不要直接去 pytorch.org 装标准版 ROCm 包大概率装上后部分算子跑不动。推荐的做法是使用海光官方提供的 Docker 镜像或者离线 wheel 包。我当时用的是镜像方案docker pull pytorch/pytorch:2.0.1-rocm5.7-dcu如果公司内网不方便拉镜像也可以让管理员从海光官网下载对应的.whl文件然后 pip 安装。装完之后立刻验证python -c import torch; print(torch.__version__); print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))看到torch.__version__是 2.0.1torch.cuda.is_available()返回 True并且输出类似Huawei DCU或设备代号时环境就算通了。需要注意DCU 上的 PyTorch 虽然沿用了torch.cuda这个 API 命名空间但底层实际走的是 HIP。所以你在代码里继续写model.to(cuda)是没问题的这属于 ROCm 生态兼容 CUDA API 的设计。只是心里得有数torch.backends.cudnn这类设置可能部分失效卷积算子可能会走到 MIOpen 的路径去。2.3 安装配套依赖库PyTorch 装好后还需要装微调过程中会用到的一堆依赖。我列一份我实测过没问题的版本组合transformers: 4.42.4peft: 0.8.2datasets: 2.20.0accelerate: 0.29.3bitsandbytes: 如果有量化需求再考虑DCU 上的兼容性要看官方公告tensorboard: 2.15.0sentencepiece: 0.1.99protobuf: 3.20.3特别提醒一下protobuf版本在加载某些开源模型的分词器时特别容易出问题出现过版本不兼容导致IndexError或者TypeError的情况。直接用我列的这个版本组合能省去很多无谓报错。3. 数据准备格式对了训练才有的跑3.1 微调数据的标准化格式很多人以为微调就是把问答对扔给模型就行但实际上数据格式对 loss 曲线的影响比想象中大。我这次用的是 ChatGLM3 的标准指令微调格式每一条样本是一个字典包含conversations字段内部是一个角色消息列表{ conversations: [ { role: user, content: 什么是海光 DCU }, { role: assistant, content: 海光 DCU 是一款面向深度学习与科学计算的加速处理器兼容 ROCm 生态。 } ] }训练之前需要统计一下数据集的 token 长度分布确认最大长度设定是否合理。我当时用脚本遍历所有样本统计到最长样本约 1200 token于是把max_seq_length设成了 1280留一点余量。如果设得太短长样本会被截断模型学不到完整的语义设得太长显存占用会指数级上涨DCU 单卡会吃力。3.2 数据集切分不能偷懒训练集和验证集的切分比例我习惯用 9:1并且保证验证集和训练集的业务分布一致。这句话说起来简单实际操作中经常见到有人随机切分后验证集里全是一种类型的问题导致训练过程 loss 下降正常但验证集 loss 却在某个 epoch 后开始反弹反而误导了判断。我处理的办法是按样本来源做分层抽样如果数据是从不同业务线收集来的切分时保证每条业务线在两边各占相同的比例。这样评估结果才真正反映模型的泛化能力而不是某个子集的偶然表现。3.3 加载和 tokenize 的实现细节使用datasets库加载 JSON 文件后需要写一个tokenize_function把对话文本拼成模型输入。这里有个细节ChatGLM3 的 tokenizer 会识别特殊 token 来区分用户和助手角色所以拼接时不能只是简单地把文本接在一起要按模型的apply_chat_template方法来构造输入。我当时代码里是这样写的def tokenize_function(examples): texts [] for conv in examples[conversations]: text tokenizer.apply_chat_template( conv, tokenizeFalse, add_generation_promptFalse ) texts.append(text) model_inputs tokenizer( texts, max_length1280, paddingmax_length, truncationTrue, return_tensorspt ) labels model_inputs[input_ids].clone() labels[labels tokenizer.pad_token_id] -100 model_inputs[labels] labels return model_inputs这里的核心思路是input_ids是模型输入labels是对应的监督信号而 pad 位置要设置成 -100这样计算 loss 时会自动忽略这些位置。如果不做这一步模型会学着去预测 pad tokenloss 被莫名拉低但生成质量一塌糊涂。4. 微调脚本与关键参数让 DCU 把活干起来4.1 LoRA 配置怎么填LoRA 层的参数选择直接决定微调效果也决定显存占用。我用的配置如下from peft import LoraConfig, get_peft_model lora_config LoraConfig( r8, lora_alpha32, target_modules[query_key_value], lora_dropout0.05, biasnone, task_typeCAUSAL_LM )r是低秩矩阵的秩我第一次跑用 8不需要一上来就设 64那样显存和过拟合风险都会增加。target_modules这一项不同模型差别很大ChatGLM3 是query_key_value但如果是 LLaMA应该写成[q_proj, v_proj]。不知道具体模块名时可以用model.named_modules()打印所有层名再挑。lora_alpha和r的比值决定 LoRA 层权重在模型输出中的缩放强度常见设置是lora_alpha 2 * r也就是 32 对应 8。我第一次直接用这个比例训练曲线比较稳定。想更强地迁移风格可以调大但同时要加大正则防止过拟合。4.2 训练参数怎么设训练时我用了transformers的TrainingArguments几个关键参数如下from transformers import TrainingArguments training_args TrainingArguments( output_dir./chatglm3-lora-checkpoints, num_train_epochs3, per_device_train_batch_size1, gradient_accumulation_steps16, learning_rate2e-4, warmup_ratio0.03, logging_steps10, save_steps200, evaluation_strategysteps, eval_steps200, save_total_limit2, fp16True, dataloader_num_workers4, remove_unused_columnsFalse )为什么per_device_train_batch_size只设 1因为 DCU 单卡显存通常无法用满而 ChatGLM3-6B 全量前向加 LoRA 反向在 32GB 的 DCU 上batch 太大直接 OOM。batch_size1 配合梯度累积 16 步等效于 batch_size16既能压制单步显存峰值又能让梯度更新更稳定。这种“小 batch 梯度累积”的方案在国产加速卡上几乎是标配玩法。fp16True这项也需要解释一下。DCU 对 FP16 的支持属于可用状态开启后显存占用几乎减半训练速度也有提升。如果发现 loss 变成 NaN首查是不是 FP16 精度不足导致这时可改用 bf16 或降到 FP32 试试。remove_unused_columnsFalse这个很容易被忽略。数据集中有很多非模型输入字段例如原始问句、对应标签等如果设为 Truetransformers 会自动移除一些它认为不需要的列有时会误伤 labels 字段导致训练时找不到 label直接在第一个 step 就报错。4.3 损失计算和 trained model 组装数据集处理好、训练参数配置好后还需要用get_peft_model把 LoRA 层加到原模型上并冻结原模型model AutoModelForCausalLM.from_pretrained( model_path, trust_remote_codeTrue, torch_dtypetorch.float16 ) model get_peft_model(model, lora_config) model.print_trainable_parameters()运行这一步后会输出类似trainable params: 8,388,608 || all params: 6,244,573,184 || trainable%: 0.1343的信息。看到这个百分比很小说明 LoRA 生效了。如果输出的 total params 和原本一样大那大概率是某个地方出了问题或者 LoRA 层没有插入成功。代码整体写完后用torch.cuda.memory_summary()观察显存占用。正常运行后显存占用曲线会比较平滑如果某一步突然飙升基本可以判定是数据处理对内存消费不受控要去查dataloader_num_workers和数据集预处理逻辑。5. 训练实操从启动到第一次看到 loss 下降5.1 启动训练并盯住日志所有代码写好之后我直接用python train.py启动训练。日志输出前几行会显示进度条和指标例如{loss: 1.23, learning_rate: 1.68e-4, epoch: 0.001} {loss: 1.05, learning_rate: 1.78e-4, epoch: 0.01}第一次看到 loss 从 1.2 降到 1.0心里石头就落了地。再往后几百步内loss 降到 0.8 左右属于正常预期。注意观察 loss 曲线不要只看整体数值要看下降的斜率。训练初期最好能看到明显下降如果跑了 500 步 loss 还在原地打转那就不是精度问题而是配置问题需要马上排查。训练过程中还有一个容易忽略的点loss 下降是用验证集 loss 来评估的不是训练集 loss。看到训练集 loss 很低、验证集 loss 一直不降很可能过拟合要加早停或者增加 dropout。5.2 loss 不降或者突变的排查路径我最常被问的问题就是“我跑了半天 loss 不降”这里给一个排查优先级数据标签是否全被 -100 掩盖了。如果 labels 中绝大多数是 -100有效监督信号太少梯度趋近于零loss 自然不会动。检查方式打印一条样本的 labels 里非 -100 的数量是否大于 50%。是否真的加入 LoRA 层。有人忘记调用get_peft_model导致模型参数全部被冻结梯度为空loss 当然不动。在代码中打印model.print_trainable_parameters()就能看出来。学习率是否过小。LoRA 微调通常可以承受比全量微调更大的学习率如果 learning_rate 设置为1e-6训练速度会慢得不像话。建议从2e-4起步。tokenizer 与模型是否匹配。如果用了别的模型的 tokenizer生成出来的 input_ids 可能全是 unknown token模型输入基本是噪声loss 很难降下去。这些坑我全踩过尤其是第一种。当时是写了一个通用数据处理函数padding 后忘记对 labels 做 -100 掩码导致模型在学预测 pad tokenloss 看起来很低但生成效果一塌糊涂。后来打印了一条样本的 labels 才发现问题。5.3 保存 checkpoint 和最终合并训练过程中我设置了每 200 步保存一次 checkpoint。DCU 训练过程中生成的 checkpoint 会自动保存在checkpoint-200、checkpoint-400这种目录里。训练中断时可以用以下命令恢复python train.py --resume_from_checkpoint ./checkpoint-400这个操作极其重要因为我第一次训练跑到第 600 步的时候机器突然断电训练进程直接没了。好在我设置了 checkpoint重启机器后拉起来接着训没浪费之前十几个小时的计算量。如果你不想用命令行参数也可以在TrainingArguments里加一个resume_from_checkpoint参数。训练结束后LoRA 权重是独立于基础模型的。要想部署或者继续评测需要把 LoRA 权重合并回原模型merged_model model.merge_and_unload() merged_model.save_pretrained(./chatglm3-lora-merged) tokenizer.save_pretrained(./chatglm3-lora-merged)合并后的模型可以直接用AutoModelForCausalLM加载推理和正常的 ChatGLM3 没有区别。这个过程在 DCU 上能正常完成因为合并操作只是权重矩阵加和不涉及复杂算子。6. 海光 DCU 训练过程中的底噪与意外情况6.1 power loss 自动关闭训练中断最大元凶这里的“底噪”指的是训练环境本身的波动。DCU 机器通常放在机房但不少小型团队是扎在办公室或实验室里供电环境远没有机房稳定。从一开始就要有这个心理预期如果整个训练跑到一半电源跳闸、机器直接关机、所有进度清零那感觉就像做了一桌菜厨师端上来之前盘子碎了。“power loss 自动关闭”这件事我遇到过不止一次所以现在所有 DCU 相关训练任务都会强制设置 save 策略和恢复机制宁可多存几个 checkpoint也不能赌机器不关机。具体建议是save_steps调小一点比如 100 或 200save_total_limit也不用设太大3 个就够。这样即便断电最多损失最近几百步的训练结果。另外如果能接 UPS强烈建议给训练服务器配上尤其是负载大的时候DCU 峰值功耗很高对市电波动也更敏感。6.2 Windows 系统装深度学习环境的弯路很多人第一次拿到 DCU 开发机默认在 Windows 上直接配环境因为桌面操作方便。但我实测下来Windows 下这块加速卡的驱动和 ROCm 工具链适配度远不如 Linux很多算子跑着跑着会报错或者直接崩掉。如果你是想快速在 DCU 上跑通微调强烈建议直接用 Linux 系统或者用 WSL2 的 Ubuntu 容器。我见过有人在 Windows 上折腾了整整两天最后换到 Ubuntu 系统半天就把环境装好了。如果团队里只有 Windows 开发机、没有单独的 Linux 服务器建议在 WSL2 里装 Docker 版 PyTorch 镜像做成隔离环境。这样好处是 Windows 里也能用 GPU 加速同时避免了直接在 Windows 主机上装驱动所带来的各种兼容性问题。代价是 Docker 和 WSL2 本身需要一些学习成本但对于用 DCU 跑微调这种任务来说这个成本是值得的。6.3 OOM 和算子兼容性这类小问题也别忽略6.3.1 显存 OOM 怎么办在 DCU 上最容易出现的就是 OOMout of memory。除了把 batch_size 调小之外还可以做两件事减少max_seq_length。数据里最长样本若只有 512 token就没必要设 1280。开启梯度检查点。model.gradient_checkpointing_enable()可以用更长的计算时间去换显存空间在 DCU 上也能正常使用。如果还是 OOM就要看 PyTorch 是否存在显存碎片。这种情况常见于中途训练中断、重启后又重新加载模型多次。解决方式是重启进程或者使用torch.cuda.empty_cache()释放缓存。6.3.2 个别算子报错怎么办用 DCU 跑微调的另一个不确定性是某些算子可能没有完整优化比如某些 Attention 的高阶写法可能走的是自定义 CUDA kernel在 ROCm 下不兼容。我的建议是如果遇到算子报错导致 loss 计算失败优先尝试关闭模型的trust_remote_code中那些自定义实现改用 transformers 官方原生的模型实现再不行就把torch_dtype从 float16 换成 float32 试试。经验法则能用 PyTorch 原生算子的就用原生的能不用自定义 CUDA 扩展的就不用。调过一次之后你会明白稳定大于炫技跑通是第一优先级性能优化是后面的事。7. 个人实际操作后的几点体会第一次在海光 DCU 上跑微调整个过程比想象中要曲折但走通之后后续再换其他模型、调参、部署就顺手多了。环境装好后训练本身的代码逻辑其实和 CUDA 环境差别不大因为 PyTorch 做了大量 API 兼容这在很大程度上降低了国产加速卡的上手门槛。有一点我特别想说不要遇到报错就急着找人问或者放弃很多问题是环境版本不匹配导致的打印完整的错误堆栈去搜关键词比重新毁掉环境从头再来更高效。比如常见的一个报错是unsupported image type其实只是数据加载器读取了损坏的图片文件定位到具体样本后删掉干净训练马上就恢复了。最后再分享一个小技巧在 DCU 上跑微调前先用一个极小数据集比如 500 条完整跑一遍训练和评估确认 loss 确实能下降再切换到全量数据。这相当于给整个管线做一次“冒烟测试”能帮你提前发现 90% 的问题。我后来所有任务都这么干几乎没再出现过跑到第三天发现 prob reward 恒等于 0 这种灾难级事故。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门