手把手教你用ms-swift微调GLM-4-9b-chat模型(含一机多卡配置避坑指南)
手把手教你用ms-swift微调GLM-4-9b-chat模型(含一机多卡配置避坑指南)
最近在尝试将GLM-4-9b-chat模型适配到我们内部的一个代码评审场景时,我发现直接使用基础模型的效果总差那么点意思。它要么对某些特定领域的术语理解不到位,要么生成的评审建议过于通用,缺乏针对性。这让我意识到,要想让大模型真正在垂直业务里落地,微调几乎是必经之路。但问题来了,GLM-4-9b这个体量的模型,单张消费级显卡根本跑不动,即便是专业卡,一张也常常捉襟见肘。于是,一机多卡的并行训练就成了我们这类技术实践者必须掌握的核心技能。
然而,把多张显卡塞进一台机器,然后指望框架能自动高效地利用起来,这种想法往往过于天真。我在配置过程中踩过的坑,从显存分配不均导致OOM(内存溢出),到多卡通信引发的诡异报错,再到训练效率远低于预期,每一个都足以让人折腾好几天。市面上关于大模型微调的教程不少,但真正聚焦于一机多卡实战细节,尤其是那些“坑”该怎么绕过去的深度分享,却并不多见。这篇文章,我就结合自己使用ms-swift框架微调GLM-4-9b-chat的真实经历,把从环境搭建、数据准备、命令配置到问题排查的全流程,以及那些至关重要的避坑点,毫无保留地梳理出来。无论你是希望将大模型应用于特定业务的数据科学家,还是负责搭建内部AI平台的基础架构工程师,相信这些实操细节都能为你省下不少时间。
1. 环境准备与核心工具栈解析
在开始敲命令之前,理清整个工具栈的构成和彼此间的依赖关系至关重要。这能帮助你在遇到问题时,快速定位是环境配置、框架版本还是硬件驱动层面的原因。
1.1 硬件与系统基础配置
一机多卡训练的首要前提,是确保硬件和操作系统层面为并行计算做好了准备。我使用的是一台配备了两张NVIDIA A100 40GB显存显卡的服务器,系统为Ubuntu 22.04 LTS。这里有几个必须检查的关键点:
-
NVIDIA驱动与CUDA版本:这是所有GPU计算的基石。务必通过
nvidia-smi命令确认驱动版本足够新(建议>=525),并且CUDA版本与你后续要安装的PyTorch等深度学习框架兼容。ms-swift底层依赖PyTorch,而PyTorch官网会明确列出其各版本所支持的CUDA版本。 -
NCCL库的安装与测试:NCCL是NVIDIA的集合通信库,是多卡训练(尤其是DDP数据并行)高效通信的核心。它通常随CUDA Toolkit一起安装,但最好单独验证其功能。一个简单的测试方法是使用NCCL自带的测试工具。
-
PCIe拓扑与NVLink:多卡之间的数据交换速度直接影响训练效率。如果主板支持且显卡具备NVLink接口,务必通过NVLink桥接器将卡连接起来,这能极大提升卡间带宽。通过
nvidia-smi topo -m命令可以查看GPU间的连接拓扑,理想状态是看到“NV*”字样,表示通过NVLink互联。
注意:即使没有NVLink,PCIe 4.0 x16也能满足大部分场景,但如果你计划进行大规模多卡训练,NVLink带来的性能提升会非常显著。
1.2 ms-swift框架的安装策略
ms-swift是ModelScope社区推出的一个“全家桶”式微调框架,它的优势在于对众多开源模型提供了开箱即用的支持,封装了训练、推理、评测等复杂流程。安装时,我强烈建议从源码安装,而非简单的 pip install。
# 创建并激活独立的Python虚拟环境,避免包冲突
python3 -m venv swift_env
source swift_env/bin/activate
# 从GitHub克隆最新源码并安装
git clone https://github.com/modelscope/ms-swift.git
cd ms-swift
pip install -e . # “-e”代表可编辑模式,方便后续查看或修改源码
从源码安装有两大好处:一是你能确保获取到最新的功能和Bug修复;二是当训练出错时,你可以直接查看框架内部的代码逻辑,这对于调试复杂问题(尤其是多卡相关的)至关重要。安装完成后,建议顺手安装 modelscope 库,它是下载和管理ModelScope上预训练模型的官方工具。
pip install modelscope
2. 数据准备与格式精讲
模型微调的效果,七分靠数据。对于GLM-4-9b-chat这类对话模型,数据的格式必须严格对齐其预训练时的对话结构。
2.1 构建高质量的指令微调数据集
我们的目标是让模型学会在“代码评审”这个特定场景下进行对话。因此,每一条训练样本都应该是一个完整的、多轮次的对话。ms-swift期望的数据格式是JSON Lines(.jsonl),即每一行都是一个独立的JSON对象。
一个符合要求的样本结构如下所示:
{
"messages": [
{
"role": "system",
"content": "你是一位经验丰富的软件架构师,擅长从代码可维护性、设计模式和性能角度进行评审。"
},
{
"role": "user",
"content": "请评审以下Python函数,它用于解析大型日志文件:\ndef parse_log(file_path):\n data = []\n with open(file_path, 'r') as f:\n for line in f:\n parts = line.strip().split('|')\n if len(parts) == 5:\n data.append({\n 'timestamp': parts[0],\n 'level': parts[1],\n 'service': parts[2],\n 'message': parts[3],\n 'trace_id': parts[4]\n })\n return data"
},
{
"role": "assistant",
"content": "该函数存在几个可优化点:\n1. **内存风险**:一次性将全部解析结果存入`data`列表,若日志文件极大,可能导致内存耗尽。建议改为生成器(`yield`)。\n2. **错误处理**:`split('|')`后未检查各部分是否为空或格式错误,可能引发`IndexError`或产生脏数据。\n3. **代码复用性**:解析逻辑硬编码,可考虑将分隔符和字段数量参数化。\n改进方向:采用流式读取、增加健壮性校验、提升配置灵活性。"
}
]
}
关键字段解析:
messages: 一个列表,包含按顺序排列的对话消息。role: 只能是system、user、assistant之一,分别代表系统指令、用户输入和模型回复。content: 对应角色的文本内容。system消息用于设定对话的背景、角色或行为准则,它对引导模型生成风格至关重要。
你需要准备两个这样的.jsonl文件:train.jsonl(训练集)和 dev.jsonl(验证集)。验证集用于在训练过程中定期评估模型性能,防止过拟合。
2.2 数据预处理与长度控制
GLM-4-9b-chat模型有其预设的上下文长度限制。如果我们的对话样本太长,超出部分会被截断,可能导致信息丢失,进而出现“模型回答不完整”的问题。这是微调中一个非常典型的坑。
解决方案是在构造数据时,就进行长度预估和裁剪。你可以编写一个简单的脚本,使用模型的Tokenizer对每条样本的 content 字段进行编码,统计token数量。一个实用的经验法则是,将单条样本(所有messages的content拼接起来)的token数控制在模型最大长度(如8192)的70%以内,为模型生成答案预留空间。
from transformers import AutoTokenizer
import json
tokenizer = AutoTokenizer.from_pretrained(“ZhipuAI/glm-4-9b-chat”, trust_remote_code=True)
max_length = 8192
reserved_for_generation = 1000 # 为模型生成预留的token数
effective_max_length = max_length - reserved_for_generation
def check_and_truncate(sample):
full_text = “”.join([msg[“content”] for msg in sample[“messages”]])
tokens = tokenizer.encode(full_text)
if len(tokens) > effective_max_length:
# 简易截断策略:从后往前保留有效长度
truncated_tokens = tokens[:effective_max_length]
# 注意:这里需要根据token反推出文本,并合理分配到各条message中,逻辑较复杂。
# 更佳实践是在构造数据时就控制好长度。
print(f“样本过长,需处理。原始长度: {len(tokens)}”)
return sample
与其事后在训练命令中通过 --max_length 参数粗暴截断,不如在数据准备阶段就做好精细化处理,这能从根本上保证训练数据的质量。
3. 一机多卡训练配置详解与避坑
这是本文的核心。多卡配置的每一个参数都牵一发而动全身,理解其背后的含义,是避开深坑的关键。
3.1 启动脚本的深层解析
下面是一个针对2张GPU的完整训练脚本 lora_run.sh,我逐行添加了详细注释:
#!/bin/bash
# 指定每张GPU上运行的进程数,通常等于使用的GPU数量
nproc_per_node=2
# 关键环境变量设置
CUDA_VISIBLE_DEVICES=0,1 \ # 指定使用哪几张物理GPU,这里用第0和第1张
MASTER_PORT=29501 \ # 多进程通信的主节点端口,如果多机训练或端口冲突需更改
NPROC_PER_NODE=$nproc_per_node \ # 将变量传入
# 启动swift sft(监督微调)命令
swift sft \
--model_type glm4-9b-chat \ # 指定模型类型,框架会根据此自动加载对应tokenizer和模板
--model_id_or_path /path/to/your/glm-4-9b-chat \ # 本地模型路径
--train_type lora \ # 微调方式,LoRA是当前最流行的高效微调方法
--dataset /path/to/data/train.jsonl /path/to/data/dev.jsonl \ # 训练和验证集路径
--torch_dtype bfloat16 \ # 模型计算精度,bf16在Ampere架构(如A100)上能兼顾速度和精度
--template default \ # 使用模型预设的对话模板
--num_train_epochs 3 \ # 训练轮数,根据数据集大小调整
--per_device_train_batch_size 1 \ # **核心参数**:每个GPU上的批次大小。由于模型大,通常设为1。
--per_device_eval_batch_size 2 \ # 评估批次大小可稍大,因不计算梯度
--learning_rate 5e-5 \ # 学习率,LoRA微调通常使用较小的学习率
--target_modules all-linear \ # LoRA注入的目标模块,'all-linear'对所有线性层生效
--gradient_accumulation_steps 8 \ # **核心参数**:梯度累积步数。全局批次大小 = per_device_train_batch_size * GPU数 * gradient_accumulation_steps。这里全局批次大小=1*2*8=16。
--eval_steps 200 \ # 每200步在验证集上评估一次
--save_steps 500 \ # 每500步保存一次检查点
--logging_steps 10 \ # 每10步打印一次日志
--output_dir ./output \ # 所有输出(模型、日志、配置)的保存目录
--system “You are a code review assistant.” \ # 覆盖默认系统提示词,与数据集中system消息配合
--warmup_ratio 0.05 \ # 学习率预热比例,前5%的训练步数线性增长学习率
--dataloader_num_workers 4 \ # 数据加载子进程数,提升数据读取效率
--ddp_find_unused_parameters false \ # **避坑关键**:多卡训练时设为false,避免特定错误
--bf16 true \ # 启用bf16混合精度训练,节省显存并加速
--gradient_checkpointing true \ # 启用梯度检查点,用计算时间换显存,能显著降低显存消耗
3.2 多卡配置的核心:显存与批次大小
在一机多卡训练中,数据并行是最常用的策略。它的原理是将一个大的批次(Global Batch)数据,平均分配到各个GPU上。每个GPU独立计算其分到的小批次(Per Device Batch)的梯度,然后所有GPU同步梯度,求平均后更新模型参数。
这里涉及三个关键参数的关系,我通过一个表格来厘清:
| 参数 | 含义 | 如何设置 | 对显存的影响 |
|---|---|---|---|
per_device_train_batch_size | 每张GPU每次前向传播处理的样本数。 | 受单卡显存限制。对于GLM-4-9b,在40G显存下,通常只能设为1。 | 直接影响最大。值越大,所需显存越多。 |
gradient_accumulation_steps | 梯度累积步数。 | 用于模拟更大的全局批次大小。当per_device_train_batch_size很小时,通过累积多次反向传播的梯度再更新参数。 | 几乎不影响峰值显存,但会轻微增加(因为需要保存中间激活值)。主要影响训练时间。 |
| 全局批次大小 | 一次参数更新所看到的样本总数。 | per_device_train_batch_size × GPU数量 × gradient_accumulation_steps | 是整体训练稳定性和效果的关键,通常需要找到一个稳定收敛的值(如16、32、64)。 |
避坑实践:假设你有2张40G的卡,跑GLM-4-9b-chat的LoRA微调。
- 你发现
per_device_train_batch_size设为2时显存溢出(OOM),设为1时则正常。那么就将它设为1。 - 根据经验,你想让全局批次大小达到16。那么
gradient_accumulation_steps就需要设置为16 / (1 * 2) = 8。 - 这样,每张卡处理1个样本,累积8次梯度后,所有卡同步一次梯度并更新参数,等效于用16个样本进行了一次更新。
3.3 必须绕开的常见报错与解决方案
在实际操作中,你几乎一定会遇到下面这两个错误。它们与多卡训练紧密相关。
报错一:RuntimeError: Expected to mark a variable ready only once...
这个错误的根源在于PyTorch的分布式数据并行(DDP)机制。在动态计算图中,如果某些模型参数在前向传播过程中没有被使用(unused parameters),DDP在反向传播时就会无法正确处理这些参数的梯度同步,从而引发上述错误。在使用LoRA等参数高效微调方法时,由于只更新部分参数,更容易触发此问题。
解决方案:在训练命令中明确添加
--ddp_find_unused_parameters false。这告诉DDP不要去寻找那些未使用的参数,从而避免同步错误。对于LoRA微调,这通常是安全的,因为冻结的参数本身就不需要梯度同步。
报错二:训练后模型推理时回答不完整或胡言乱语
这通常不是多卡特有的问题,但配置不当会加剧。可能的原因有:
- 数据被截断:如前所述,样本长度超过模型限制,关键信息丢失。
- 学习率过高:微调大模型需要使用很小的学习率(如1e-5到5e-5),过高的学习率会“冲坏”预训练好的知识。
- 训练轮数过多/过少:过拟合或欠拟合。
排查与解决步骤:
- 首先,检查训练日志中的
loss曲线。一个健康的训练过程,训练损失应平稳下降,验证损失在后期可能轻微上升(过拟合迹象)。如果损失剧烈波动或很早就降到接近0,说明有问题。 - 其次,在训练命令中尝试加入
--max_length 2048(或根据你的数据调整),确保数据以正确长度输入。但更推荐在数据预处理阶段解决。 - 最后,尝试调整
--learning_rate(调小)和--num_train_epochs(通常3-5个epoch对于指令微调足够)。
4. 模型合并、推理与效果评估
训练完成后,我们得到了一个LoRA适配器(adapter),它是一组小权重文件,需要与原始的基础模型合并,才能得到一个独立的、便于部署的完整模型。
4.1 合并LoRA权重
使用ms-swift提供的 export 命令可以轻松完成合并:
# 假设你的训练输出目录为 ./output/v1-checkpoint-1000
swift export \
--ckpt_dir ./output/v1-checkpoint-1000 \ # 训练保存的检查点目录
--merge_lora true \ # 关键参数:执行合并操作
--save_dir ./merged_model # 合并后模型的保存路径
这个命令会读取检查点目录中的 adapter_config.json 和 adapter_model.bin(LoRA权重),并将它们与 --model_id_or_path 指定的原始模型(如果命令中未指定,会从检查点记录中读取)进行合并,生成一个完整的、可直接用Transformers库加载的模型。
4.2 使用微调后的模型进行推理
合并后的模型可以像任何标准Hugging Face模型一样使用。这里给出一个简单的推理脚本示例:
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch
model_path = “./merged_model”
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.bfloat16, # 保持与训练一致的精度
device_map=“auto”, # 自动将模型层分配到可用GPU上
trust_remote_code=True
).eval()
# 构建对话
messages = [
{“role”: “system”, “content”: “你是一位经验丰富的软件架构师。”},
{“role”: “user”, “content”: “请评价使用全局变量存储配置的优缺点。”}
]
# 使用模型的聊天模板格式化输入
input_ids = tokenizer.apply_chat_template(messages, return_tensors=“pt”).to(model.device)
# 生成回复
with torch.no_grad():
outputs = model.generate(
input_ids,
max_new_tokens=512,
do_sample=True, # 启用采样,使生成结果更多样
temperature=0.8, # 采样温度,控制随机性
top_p=0.95 # 核采样,控制生成质量
)
response = tokenizer.decode(outputs[0][input_ids.shape[1]:], skip_special_tokens=True)
print(“模型回复:”, response)
4.3 效果评估与迭代
微调不是一劳永逸的。你需要系统评估模型在新任务上的表现。除了在验证集上计算困惑度(PPL)等自动指标外,人工评估至关重要。
我通常会构建一个包含50-100个典型场景的测试集,涵盖边界情况、复杂逻辑和易混淆点。然后让微调前后的模型分别回答,由领域专家进行盲评打分(例如,从“完全错误”、“部分相关”、“基本正确”、“优秀”四个维度)。只有经过这样严格的评估,才能判断微调是否真的提升了模型在目标场景下的能力。如果效果不理想,就需要回到数据制备环节,检查数据质量、数量,或者调整训练的超参数,开始新一轮的迭代。
整个流程走下来,最深的体会是,一机多卡微调大模型就像指挥一个小型交响乐团,硬件是乐器,框架是指挥棒,而配置参数就是乐谱上的强弱快慢记号。任何一个声部不协调,整场演出就会失败。其中,对梯度累积与全局批次大小的理解,是平衡显存限制与训练稳定性的杠杆;而 ddp_find_unused_parameters 这样的参数,则是解决多卡通信杂音的消音器。把这些关键点把控好,剩下的就是耐心地调试和迭代。当你看到微调后的模型,能精准地理解你的领域术语,并给出专业、流畅的回应时,之前所有的折腾都值了。
更多推荐


所有评论(0)