海光DCU上跑通大模型微调:从环境配置到LoRA实战
我一直觉得在大模型微调这件事上最难的不是算法也不是数据而是“环境跑不起来”。尤其是当你的机器上不是 NVIDIA 显卡而是一块海光 DCU 时网上铺天盖地的 CUDA 教程几乎全部失效。我第一次在海光 DCU 上跑微调任务光装环境就折腾了整整两天期间无数次怀疑是卡坏了还是驱动没装好。这篇博文就记录我从零开始在海光 DCU 上把第一个微调任务跑通的全过程从环境配置到 loss 曲线稳定下降。如果你也是第一次在 DCU 上做微调希望这篇能帮你少走点弯路。1. 海光 DCU 的软件栈到底是什么先搞清几个要命的概念很多人拿到 DCU 机器后的第一个操作就是打开终端pip install torch然后发现装完的 PyTorch 根本识别不了 DCU甚至torch.cuda.is_available()返回 False。其实这不怪 DCU而是因为我们下意识地把 DCU 当成了 CUDA GPU 来用。1.1 DCU 和 CUDA 的关系不是替代是另一套生态海光 DCU 是一块 GPGPU 架构的加速卡底层的指令集和编程模型跟 NVIDIA 的 CUDA 并不通用。但它做了 ROCm 兼容层所以在软件栈上更接近 AMD 的 ROCm 生态。换句话说你不能直接在 DCU 上跑 CUDA 写出来的代码但你可以跑通过 HIP/ROCm 移植过的代码。这个概念很关键因为后面所有的环境配置都是围绕这个兼容层展开的。如果你的同事给你一份“在 NVIDIA 上跑通”的微调脚本拿过来直接python train.py大概率会报算子不支持的错千万不要慌这是正常的。1.2 DTK 到底是什么海光官方提供的开发工具包叫 DTKDeveloper Toolkit它相当于海光版的 CUDA Toolkit里面包含了编译器、数学库、通信库这些底层依赖。PyTorch 要跑在 DCU 上必须依赖 DTK 提供的 runtime。所以一个完整的 DCU 微调环境从上到下大概是这样的层级组件作用应用层微调训练脚本基于 PyTorch定义模型、数据、训练逻辑框架层PyTorch-DCU海光定制版让 PyTorch 能调用 DCU 算子软件栈DTK含 HIP、MIOpen、RCCL 等提供编译器、数学库、通信库驱动层DCU 内核驱动让系统识别并调度 DCU 硬件硬件层海光 DCU 加速卡执行计算任务从这张表就能看出来每一层都得对上型号和版本。DTK 版本不对PyTorch-DCU 起不来驱动版本太老DTK 的新功能又用不了。这也是 DCU 环境配置最大的痛点。1.3 为什么环境配置 90% 的问题都出在版本匹配上我第一次装环境时在网上找了一份“海光 DCU 安装 PyTorch”的帖子照着敲完了所有命令结果import torch能过torch.cuda.is_available()也返回 True但一跑模型就报算子错误Not implemented或者HIP error。后来问了海光的工程师才知道是 DTK 版本和 PyTorch-DCU 的编译版本不匹配。这个经验可以总结成一句话在 DCU 上版本匹配的优先级高于一切。官方文档里给出的 DTK 版本、PyTorch-DCU 版本、Python 版本往往是一个“锁死”的组合。你用 conda 建环境的时候指定了 Python 3.9 还是 3.11也直接决定了你能装哪个版本的 PyTorch-DCU。我的建议是先选定一个官方验证过的组合再动手建环境别自己发挥。2. 从零装环境反直觉的版本匹配是最大的坑确定了大方向之后就可以开始实际装环境了。这一节我把完整的步骤写出来每步都附上我踩过的坑。2.1 第一步确认驱动和 DCU 状态在装任何软件之前先确认系统能不能看到 DCU。海光 DCU 上查看卡状态的命令是hy-smi作用类似于 NVIDIA 的nvidia-smi。hy-smi如果命令能正常输出卡的数量、显存、驱动版本说明驱动层面是 OK 的。如果没有这个命令大概率是 DTK 或者驱动没装好这时候直接装 PyTorch 是白费功夫。你还需要确认一个关键信息驱动版本对应的 DTK 版本。海光官方通常会提供一张驱动与 DTK 的兼容表。驱动太旧的话需要先升级驱动否则后面装新版 DTK 也可能出现编译错误或运行崩溃。2.2 第二步创建一个干净的 conda 环境我强烈建议任何微调任务都用一个全新的 conda 环境不要动系统自带的 Python。因为 DCU 的各种库对 Python 版本很敏感系统 Python 一旦被搞乱运维成本极高。conda create -n dcu-finetune python3.10 -y conda activate dcu-finetunePython 版本怎么选看 DTK 和 PyTorch-DCU 的支持列表一般 3.8、3.10、3.11 是常见选项。我这次用的是 Python 3.10原因是 3.8 太老部分新版代码有兼容问题3.12 又太新很多库还没有适配。3.10 是中间态稳妥很多。2.3 第三步安装 DTKDTK 的安装方式通常是下载一个 deb/rpm 包或者解压一个 tar 包然后设置环境变量。以常见的方式为例# 假设 DTK 包已经解压到 /opt/dtk export PATH/opt/dtk/bin:$PATH export LD_LIBRARY_PATH/opt/dtk/lib:$LD_LIBRARY_PATH这一步有个容易忽略的细节LD_LIBRARY_PATH必须把 DTK 的 lib 目录放在前面否则系统可能找不到 DTK 的动态库。而且这个环境变量需要在每次启动训练前都export一遍或者写进~/.bashrc。我个人的习惯是写进训练脚本的同级.env文件里这样每次source一下就行避免污染全局配置。2.4 第四步安装海光定制版 PyTorch这是最核心的一步。千万不能用官方 PyTorch 直接装因为那是基于 CUDA 编译的在 DCU 上根本跑不了。海光官方会发布一个定制版 PyTorch通常托管在自己的源上。安装命令类似这样pip install torch版本号dcu -f 海光官方提供的源地址如果你是在内网环境没有外网访问条件那就得让管理员把 wheel 包下载好再离线安装pip install torch-xxx.whl装完之后验证一下import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))torch.cuda.is_available()返回 True 还不够要看torch.cuda.get_device_name(0)能不能返回 DCU 的设备名比如Hygon DCU之类的。如果这一步正常说明 PyTorch 能识别 DCU 了。我遇到过一种情况torch.cuda.is_available()是 True但一执行张量运算就报hipErrorNoDevice。后来发现是HIP_VISIBLE_DEVICES环境变量没设置系统没把 DCU 设备暴露给进程。解决方法是export HIP_VISIBLE_DEVICES0如果你有多张 DCU可以通过export HIP_VISIBLE_DEVICES0,1来指定用哪几张开跑。2.5 第五步安装配套的深度学习库做微调通常还需要transformers、peft、datasets、accelerate这几个库。这些库本身不依赖 CUDA可以直接用 pip 装pip install transformers peft datasets accelerate但要注意版本兼容性特别是transformers的大版本升级有时会改 API。建议按照 PyTorch-DCU 对应的教程里的版本来装别一味追求最新版本。我这次用的是 transformers 4.44 左右的版本比较稳定。3. 第一个微调任务跑什么LoRA 是新手的正确打开方式环境装好了接下来要选一个微调方案。新手在这一步很容易纠结是全量微调full fine-tuning还是冻结部分层的 freeze 微调还是用 LoRA3.1 三种微调方式的对比方式显存占用训练速度效果代码复杂度全量微调极高慢最优低Freeze 微调中中较好中LoRA低快接近全量低全量微调是效果最好的但显存需求也最夸张。即使是 7B 模型全量微调通常也需要多张 32G 以上的卡才能跑起来这对第一次上手的人来说门槛太高。Freeze 微调虽然折中但需要开发者对模型结构有不错的理解知道该冻结哪些层、不冻结哪些层否则容易“冻错地方”效果反而不如 LoRA。3.2 为什么在 DCU 上我更推荐 LoRALoRA 的核心思想很朴素原模型的权重在训练期间完全冻结不做任何更新。我们只是在模型的某些线性层旁边额外挂上一对小矩阵 A 和 B。训练时只更新这两张小矩阵最终把训练好的 A、B 合并回原模型或者单独保存成一个小文件。用生活化的比喻来说全量微调像是把整台发动机拆开重新锻造每一个零件而 LoRA 像是在发动机旁边加装了一个可插拔的“调节阀门”。前者工程量巨大后者只需要调小阀门就行。在 DCU 上LoRA 的优势尤其明显显存压力小训练 7B 模型时LoRA 一般只需要 16G 到 32G 显存单张 DCU 就有机会跑起来。适配难度低LoRA 本身不修改原始模型结构只在原有层上做包装所以对 DCU 的算子兼容性要求更低。全量微调中容易遇到的某些算子不支持问题在 LoRA 下更容易绕过。训练稳定性好因为大部分参数被冻结模型的输出分布不会剧烈变化loss 相对容易收敛。3.3 选什么基座模型新手第一次在 DCU 上跑微调我建议选 7B 参数级别的开源模型比如 Qwen2.5-7B 或 Llama-3.1-8B。这类模型在 HuggingFace 上可以直接下载transformers 库原生支持而且 LoRA 的参考资料特别多。如果你的 DCU 显存只有 16G 甚至 8G那就考虑 3B 或者更小的模型或者把量化如 4-bit 量化也考虑进来。不过量化在 DCU 上的兼容性要额外验证新手先跑通没量化的 LoRA再考虑量化。4. 把训练脚本拆开看数据、模型、训练循环我尽量把一个可以跑通的 LoRA 训练脚本一步步拆开说明每一段代码在做什么以及为什么这么写。完整的训练脚本很长我按功能模块来讲解。4.1 模型加载模块加载基座模型并包装 LoRAimport torch from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig, get_peft_model, TaskType model AutoModelForCausalLM.from_pretrained( /path/to/model, torch_dtypetorch.bfloat16, device_mapauto ) tokenizer AutoTokenizer.from_pretrained(/path/to/model, trust_remote_codeTrue) if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token lora_config LoraConfig( task_typeTaskType.CAUSAL_LM, r8, lora_alpha16, lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj], ) peft_model get_peft_model(model, lora_config) peft_model.print_trainable_parameters()target_modules指定要把 LoRA 挂在哪些层上。不同模型可选的模块名不一样Qwen 系一般是q_proj、k_proj、v_proj、o_proj这些注意力层的投影矩阵。最稳妥的办法是先用print(model)看一下实际的模块命名再决定写什么。print_trainable_parameters()会打印可训练参数的数量和占比。一般来说7B 模型 LoRA 的可训练参数大概在 0.1%-1% 之间。如果你看到 100% 的可训练占比说明 LoRA 包装失败了模型变成了全量微调要回头检查。4.2 数据准备模块决定 loss 能否下降的第一道关口数据格式我用最常见的 Alpaca 格式一条数据包含 instruction指令、input输入、output期望输出。from datasets import load_dataset from transformers import DataCollatorForSeq2Seq dataset load_dataset(json, data_filestrain.json) def preprocess_function(examples): prompts [] for instruction, inp, output in zip(examples[instruction], examples[input], examples[output]): if inp: prompt f指令{instruction}\n输入{inp}\n回答{output} else: prompt f指令{instruction}\n回答{output} prompts.append(prompt) model_inputs tokenizer(prompts, max_length512, truncationTrue, paddingmax_length) labels tokenizer(prompts, max_length512, truncationTrue, paddingmax_length) model_inputs[labels] labels[input_ids] return model_inputs tokenized_dataset dataset.map(preprocess_function, batchedTrue, remove_columnsdataset[train].column_names)一个小细节loss 的计算通常是拿模型预测的 token 和 labels 里的 token 做交叉熵。如果你不想让模型去学习 prompt 部分的 loss也就是指令和输入部分需要把那部分设置成 -100。这种做法叫“Mask 掉 prompt loss”是很多微调任务里提升稳定性的关键。新手可以先不处理直接让模型学习整段文本的 loss这样实现简单也能跑通但如果你发现生成结果总是在复读指令就要考虑加上 label mask 了。4.3 训练参数学习率怎么定、batch size 怎么调训练参数是整个微调里最需要“手感”的部分直接决定了 loss 能不能降下来。我先给一组能跑的配置from transformers import TrainingArguments, Trainer training_args TrainingArguments( output_dir./output, num_train_epochs3, per_device_train_batch_size4, gradient_accumulation_steps8, learning_rate2e-4, lr_scheduler_typecosine, warmup_ratio0.03, logging_steps10, save_steps100, bf16True, remove_unused_columnsFalse, ) trainer Trainer( modelpeft_model, argstraining_args, train_datasettokenized_dataset[train], data_collatorDataCollatorForSeq2Seq(tokenizer, paddingTrue), ) trainer.train()这里有几个参数值得展开说per_device_train_batch_size4单张卡上的 batch size。如果 OOM就减到 2 或者 1。新手最容易犯的错是一上来就想用大 batch结果直接爆显存。gradient_accumulation_steps8梯度累积。相当于每跑 8 个小 batch 才做一次参数更新等效于 batch size 4×832。这能缓解显存不够的问题但代价是训练时间变长。learning_rate2e-4LoRA 常用学习率在 1e-4 到 5e-4 之间。如果你用的是全量微调学习率要降到 1e-5 级别。我第一次跑的时候直接把之前全量微调的 1e-5 拿过来用结果 loss 降得跟蜗牛爬一样慢。bf16True用 bfloat16 混合精度训练能显著减少显存占用。但前提是 DCU 支持 bf16我用的 DTK 版本支持如果你的 DTK 版本较老可能需要用fp16True或者干脆全用 fp32。4.4 启动训练先跑一个 smoke test正式训练之前我强烈建议先跑一个几步的“冒烟测试”。具体做法不改训练脚本但是用一个只有几十条数据的 toy 数据集启动训练。python train_lora.py --model_path /path/to/model --data_path toy_train.json --max_steps 20跑 20 步重点看两个东西能不能正常更新loss 是否在逐步下降哪怕只是从 3.5 降到 3.2 也算正常。每步耗时在 DCU 上7B 模型 LoRA 训练每步耗时一般在几秒到十几秒之间。如果每步卡到几十秒甚至几分钟多半是数据加载或者算子调用出了问题要趁早排查。等 smoke test 通过了再换全量数据集把max_steps删掉用num_train_epochs来正式跑。5. loss 不下降的排查链路我在 DCU 上踩过的四个典型坑loss 不下降是微调新手最容易崩溃的时刻。这里我把自己在 DCU 上遇到过的四种“loss 不降”的情况完整捋一遍每一个都是真实踩过坑、最终找到根因的。5.1 第一个坑loss 打印出来的是初始化 loss模型根本没训练现象是loss 数值非常“稳定”从第一步到最后一步几乎不变而且稳定得像一条直线。最典型的原因是在脚本里打错了 loss逻辑上打的是还没经过 backward 的初始输出而不是训练循环里的 loss。比如有人会在forward之后立刻打一次 loss然后才调用backward一看曲线“一动不动”就慌了其实模型早就正常训练了。排查思路把第一次loggings间隔设成 1也就是每一步都打印 loss看第一步到第二步有没有变化。如果没有再检查是不是忘了loss.backward()或者optimizer.step()。在 Trainer 或 peft 封装好的脚本里这个坑相对较少。但如果自己手写训练循环就很容易犯。5.2 第二个坑tokenizer 和模型不匹配导致 loss 从一开始就巨大现象loss 初始值非常高比如 10 以上而且训练了很久也降不到正常水平。有一次我用一个中文指令数据集去微调一个英文基座模型tokenizer 对中文文本的编码效率特别低很多中文字符被拆成了多个 token序列长度变大模型学习效率极差loss 一直在高位震荡。排查方法先看 tokenizer 能不能正常编码训练数据。手动tokenizer.tokenize(你好世界)如果输出的 token 粒度非常碎或者在 tokenizer 的 vocab 里查不到这些字就要考虑换 tokenizer 或者换基座模型。对于中文微调任务建议直接用中文语料预训练过的基座模型比如 Qwen 系列、Baichuan 系不要拿一个纯英文模型强行做中文任务。5.3 第三个坑学习率过大或过小loss 出现了“假平”现象loss 在某个值附近反复震荡既不上升也不下降看起来像是卡住了。这个“卡住”可能是学习率太大导致的震荡也可能是学习率太小导致的几乎不更新。区别方法很简单看 loss 曲线的振幅。如果每一步的 loss 波动在 0.1 以上说明学习率偏大模型参数在最优值附近来回横跳如果波动小于 0.001说明学习率可能偏小模型更新得太慢。针对 LoRA我的经验是优先从3e-4开始调如果 loss 震荡就降到1e-4如果降得太慢就升到5e-4。但不要超过1e-3否则很容易把 LoRA 的低秩矩阵训崩。5.4 第四个坑数据集 shuffle 没开模型在“死记硬背”现象训练 loss 在下降但下降得特别慢而且 eval loss 完全跟不上出现过拟合迹象。很多时候是数据加载的问题。如果数据集没有做 shuffle模型每轮看到的训练样本顺序完全一致它就会逐渐“背”下这个顺序而不是学习真正的语义模式。在 Trainer 里默认是开了 shuffle 的但如果你自己手写训练循环、或者数据读取时用了load_dataset并直接迭代就要检查是否显式设置了shuffleTrue。另外数据集的构建也很重要——如果你的数据里大量样本都是相似的模板比如“请回答问题A”“请回答问题B”模型可能会偷懒直接学会“看到‘请回答’就复制后面内容”这种捷径导致 loss 下降到一定程度就停住。5.5 DCU 特有的算子报错loss 曲线突然断了这个不是 loss 不下降的问题而是 loss 曲线“断掉”的问题。训练到某一步程序突然崩溃报错信息里出现HIP error或Not implemented。这种情况在 DCU 上比较多见原因是某些算子在该版本的 DTK 里没有实现或者有 bug。我的建议排序是先升级 DTK 和 PyTorch-DCU 到较新版本很多算子问题在新版本里已经解决了。绕开算子如果某个 loss 计算函数或模型层导致报错可以用等价实现替代。比如某些模型的flash_attention在 DCU 上还不支持就换成普通的eagerattention。实在绕不开再把模型换小一号以跑通为优先。6. 验证与扩展loss 降下来了不代表完事训练 loss 降到 1.0 以下甚至 0.5 以下很多人就觉得“完事了”。但训练 loss 只是训练过程中的一个信号真正要看的是模型在实际输入上的生成效果。6.1 加载 LoRA 权重做推理验证LoRA 微调完成后模型参数分为两部分基座模型的原始权重以及 LoRA 的低秩矩阵。推理时有两种方式合并权重把 LoRA 低秩矩阵合并回原模型导出一个完整模型后续推理就是普通的大模型加载效率最高。不合并权重保持基座模型不变推理时单独加载 LoRA adapter。我建议在验证阶段用第二种方式因为试错成本低随时可以换不同的 adapter 来对比效果。from peft import PeftModel base_model AutoModelForCausalLM.from_pretrained(/path/to/model, torch_dtypetorch.bfloat16, device_mapauto) model PeftModel.from_pretrained(base_model, ./output/checkpoint-300) inputs tokenizer(指令介绍一下北京的秋天\n回答, return_tensorspt).to(cuda) outputs model.generate(**inputs, max_new_tokens200, do_sampleTrue, temperature0.7) print(tokenizer.decode(outputs[0], skip_special_tokensTrue))这里有个细节temperature不要设太高0.7左右比较适中。温度太高会导致生成结果随机性过强不好判断模型到底学没学到东西。6.2 用验证集 loss 选 checkpoint而不是用训练 loss训练过程中模型会定期保存 checkpoint比如每 100 步保存一次。到底选哪一个我的建议是看验证集 loss而不是训练集 loss。训练 loss 低只能说明模型记住了训练数据验证集 loss 低才说明模型学到了泛化能力。我自己的习惯是把数据集按 9:1 切分训练和验证训练结束后在验证集上跑一遍生成拿几个典型问题做人工评测。人工评测这一步非常重要因为 loss 指标再好看也比不上实际看一眼生成结果来得直观。6.3 后续扩展方向freeze 微调和多卡训练跑通了第一个 LoRA 任务之后你就有了一块“敲门砖”。接下来的扩展方向我个人的建议顺序是freeze 微调对比 LoRA看效果是否有明显提升。freeze 微调对显存的要求介于 LoRA 和全量微调之间代码也不复杂本质上就是在加载模型后把某些层设成requires_gradFalse。多卡微调用accelerate库做数据并行把更大的模型或者更大的 batch size 跑起来。海光 DCU 多卡通信走的是 RCCL跟 NVIDIA 的 NCCL 类似如果遇到通信层面的问题先确认 DTK 的 RCCL 版本是否和 PyTorch-DCU 匹配。更长序列和更多数据等前两步都稳定了再逐步增加max_length、增加训练数据量、尝试不同的 LoRA rank 值。从我实际的体验来说DCU 上微调并没有想象中那么特殊。它跟 CUDA 生态最大的差别在于软件栈的“成熟度”——很多在 NVIDIA 上开箱即用的东西在 DCU 上需要手动对齐版本、手动验证算子支持。但这个对齐过程走完一遍之后后面再跑任务就顺了。第一个任务跑通永远是最难的一步也是最值的一步。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →