微调模型的各种参数怎么设置?微调的显存消耗如何估算和优化?
大家好,欢迎来到code秘密花园,我是花园老师(ConardLi)。
今天我们来到 LLaMA Factory 微调教程的第三期,一起来学习微调过程中的各种参数设置,以及显存消耗的估算和优化。
调整微调参数
在模型微调中,各类参数就像是你在给模型 “补课” 之前制定的教学计划和策略。它们决定了你如何教学、教学的强度以及教学的方向。如果你选择的教学计划不合适(比如补课时间太短、讲解速度太快或复习策略不合理),可能会导致学生学习效果不好。同样,如果你选择的超参数不合适,模型的性能也可能不理想。
过去经常收到很多同学的问题:"在微调过程中这些参数到底要怎么设置效果才最好?" ,这肯定不是一个标准答案,参数的设置和你选择的模型、用于微调的数据集,以及你微调机器的硬件配置都有关系,并不存在 “最佳参数” 这个说法,下面我会结合我个人的经验,尽可能的告诉大家一些关键参数的作用和影响,以及在实际微调任务中调整的思路是什么。没有在下面讲到的参数,大家都直接保持默认值即可。
学习率(Learning Rate)
| 分类 | 说明 |
|---|---|
| 核心概念 | 学习率(Learning Rate) |
| 通俗理解 | + 学习率大(比如0.1):每次做完一道题后,你会对解题方法进行很大的调整。比如,你可能会完全改变解题思路。优点是进步可能很快,因为你每次都在进行较大的调整。缺点就是可能会因为调整幅度过大而“走偏”,比如突然改变了一个已经掌握得很好的方法,导致之前学的东西都忘了。 + 学习率小(比如0.0001):每次做完一道题后,你只对解题方法进行非常细微的调整。比如,你发现某个步骤有点小错误,就只调整那个小错误。优点是非常稳定,不会因为一次错误而“走偏”,适合需要精细调整的场景。缺点就是进步会很慢,因为你每次只调整一点点。 |
| 个人经验 | |
| 显存影响 | |
| 本次取值 |
训练轮数(Number of Epochs)
| 分类 | 说明 |
|---|---|
| 核心概念 | 训练轮数(Number of Epochs) |
| 通俗理解 | + 轮数少:比如你只复习一遍,可能对书里的内容还不是很熟悉,考试成绩可能不会太理想。 + 轮数多:比如你复习了 10 遍,对书里的内容就很熟悉了,但可能会出现一个问题——你对书里的内容背得很熟,但遇到新的、类似的问题就不会解答了,简单讲就是 “学傻了“,只记住这本书里的内容了,稍微变一变就不会了(过拟合)。 |
| 个人经验 | |
| 显存影响 | |
| 本次取值 |
批量大小(Batch Size)
| 分类 | 说明 |
|---|---|
| 核心概念 | |
| 通俗理解 | + 批量大(比如100):每次复习时,你集中精力做100道题。优点是复习速度很快,因为你每次处理很多题目,能快速了解整体情况(训练更稳定,易收敛到全局最优)。缺点是可能会因为一次处理太多题目而感到压力过大,甚至错过一些细节(耗显存,泛化能力差,易过拟合)。 + 批量小(比如1):每次复习时,你只做一道题,做完后再做下一道。优点是可以非常专注,能仔细分析每道题的细节,适合需要深入理解的场景(省显存,易捕捉数据细节,泛化能力强)。缺点就是复习速度很慢,因为每次只处理一道题(训练不稳定,易陷入局部最优)。** |
| 显存影响 | |
| 批量计算 | per_device_train_batch_size * gradient_accumulation_steps) |
| 梯度累计步数(Gradient Accumulation Steps) + 1. 先算 2 个样本的梯度,不更新参数; + 2. 再算 2 个样本的梯度,不更新参数; + 3. 再算 2 个样本的梯度,与前两次累积后一起更新参数。 这样就能实现用小显存实现大 batch_size 的效果,类似于 “分期付款” 的效果。 | |
| 个人经验 | |
| 本次取值 |
截断长度(Cutoff length)
| 分类 | 说明 |
|---|---|
| 核心概念 | |
| 显存影响 | + 截断长度 1024 时,1 块 32GB 显存可能支持批量大小 16; + 截断长度增至 2048,同显存下批量大小可能需减半至 8,否则会因显存不足报错。 |
| 个人经验 | 在实际训练中,不能为了省显存而把截断长度设定的过小,比如你的训练数据平均大小为 2048 个 Token,那你把截断长度也设定为了 2048 个 Token,那有接近一半的训练数据都将是被截断、不完整的,这会大大影响训练效果。在显存条件允许的情况下,最好的截断长度当然是设定为数据集中单条数据的最大 Token 数,但现实可能不允许,你可以去计算你的数据集 Token 长度的 P99、P95 的值,来根据你的显存大小逐步调整,当然这也就意味着有 1%、5% 的训练数据是不完整的,你也可以在训练前把这部分较长的数据集剔除出去,这样可以减小对训练效果的影响。 |
| 本次取值 |
目前市面上有比较多成熟的工具来计算文本的 Token数,比如 (https://tiktokenizer.vercel.app/):
如果使用的是 Easy Dataset 构造的数据集,我们也可以看到每条数据集的 Token 数(默认以 GPT 4 计算):
注意,不同模型在计算 Token 时的算法是不一样的,例如 Qwen、ChatGLM、Doubao 等,会针对中文语义特点优化分词逻辑,采用类似 BPE(字节对编码)的动态分词,但针对中文语境优化了 “语义块” 分割逻辑。例如,“人工智能” 会被识别为一个整体 Token(而非拆成 “人工”+“智能” 或单个字符),减少冗余 Token 数量。传统英文模型(如 GPT-3.5):字符级或粗粒度分词,对中文缺乏优化,可能将汉字拆分为单个字符(如 “今”“天”“天”“气”...),所以在中文数据集的分词中,不同模型的差距还是挺大的。
在 LLaMA-Factory 中也提供了一个帮助批量计算数据集 Token 长度分布的脚本(scripts/stat_utils/length_cdf.py ),我们可以通过如下命令运行,注意把 model_name_or_path 、dataset 替换你自己的信息:
torchrun --nproc_per_node=1
scripts/stat_utils/length_cdf.py
--model_name_or_path /root/autodl-tmp/Qwen2.5-7B-Instruct
--dataset security
--dataset_dir data
--template qwen
对于本地微调的数据集运行结果如下,可以发现 99.83% 的数据都是 < 4000 Token 的,所以我们本次设定的截断长度为 4096 ,如果你的设备性能足够电话,也可以直接设置为 5000:
LoRA 秩(LoRA rank)
| 分类 | 说明 |
|---|---|
| 核心概念 | |
| 通俗理解 | + 秩高(如 64):相当于掌握了 100 种解题思路,遇到新题可以灵活组合方法。优点是能处理更复杂的任务(比如生成风格更细腻的内容),模型微调的 “潜力” 更大;缺点是容易学 “乱”—— 可能把无关的知识强行关联(过拟合),而且太费 “脑子”(显存消耗显著增加)。 |
| 个人经验 | |
| 显存影响 | |
| 本次取值 |
验证集比例(Val size)
| 分类 | 说明 |
|---|---|
| 核心概念 | |
| 通俗理解 | |
| 个人经验 | + 大数据(>10000 样本):0.05-0.1,建议验证集样本数 ≥ 1000 即可(如 10 万样本用 5000 做验证)。 比较复杂的任务可适当提高比例,因需要更严格监控各类别的拟合情况;比较简单单任务可适当降低比例,避免浪费训练数据。 |
| 本次取值 |
启动微调任务
预览命令
在以上配置完成后,我们配置一个最终微调后 Lora 适配器的输出目录,然后点击预览命令:
点击后可以输出拼接好的 llamafactory-cli train 的各项参数:
llamafactory-cli train \
--stage sft \
--do_train True \
--model_name_or_path /root/autodl-tmp/Qwen/Qwen2.5-7B-Instruct \
--preprocessing_num_workers 16 \
--finetuning_type lora \
--template qwen \
--flash_attn auto \
--dataset_dir data \
--dataset security \
--cutoff_len 4096 \
--learning_rate 5e-05 \
--num_train_epochs 3.0 \
--max_samples 100000 \
--per_device_train_batch_size 1 \
--gradient_accumulation_steps 8 \
--lr_scheduler_type cosine \
--max_grad_norm 1.0 \
--logging_steps 5 \
--save_steps 100 \
--warmup_steps 0 \
--packing False \
--report_to none \
--use_swanlab True \
--output_dir /root/autodl-tmp/models/security007 \
--bf16 True \
--plot_loss True \
--trust_remote_code True \
--ddp_timeout 180000000 \
--include_num_input_tokens_seen True \
--optim adamw_torch \
--lora_rank 8 \
--lora_alpha 16 \
--lora_dropout 0 \
--lora_target all \
--swanlab_project security007 \
--swanlab_mode cloud \
--val_size 0.15 \
--eval_strategy steps \
--eval_steps 100 \
--per_device_eval_batch_size 1
本次使用的微调参数的总结:
| 字段名 | 含义 | 具体值 |
|---|---|---|
训练过程
然后我们点击开始训练:
回到终端可以看到整个训练过程:
回到 LLaMA Board 页面,我们也可以看到具体的进度和 LOSS 曲线:
这里我们看到总的进度为 456 步,这个可以根据我们之前的微调参数计算出来:
总步数由“每轮处理次数”和“轮数”决定,而前者取决于“数据量”“批次大小”“梯度累积”的组合。 向下取整意味着最后一次若凑不满完整的更新批次,就跳过不处理。
显存消耗估算
本次微调使用的硬件配置如下:
在微调过程中,我们可以通过 nvidia-smi 命令查看 GPU 和显存的占用:
这里我用了两张 48G 显存的卡,我们看到在微调过程中,两张卡分别使用了 G 的显存。如果你不知道一个微调任务需要什么样的硬件配置,我们可以通过当前选择的微调参数来对可能的显存占用做个简单的估算(估算方法与实际严谨的计算方法可能存在偏差,但大体接近)。在模型微调(Lora)过程中,显存占用主要由基础模型权重、激活值、框架开销、LoRA 适配器等几部分构成:
基础模型权重(Base Model Weights)
预训练模型的参数矩阵,可以简单理解为就是选择的预训练模型占用的显存大小。
计算方法:显存占用 = 模型参数数量 × 单个参数的字节数 FP32:32 个二进制位(4 字节,1字节 = 8位) FP16:16 个二进制位(2 字节) BF16:16 个二进制位(2 字节,指数位同 FP32 ) INT8:8 个二进制位(1 字节) INT4:4 个二进制位(0.5 字节) INT2:2 个二进制位(0.25 字节) 上面我们提到过常见的模型精度下,单个参数的显存占用: 在本次微调中,我们选择的模型是 Qwen2.5-7B-Instruct ,参数量为 70 亿,计算精度为 BF16 ,所以预估的显存占用为 70 亿 × 2byte ≈ 140 亿字节(14GB)。
框架开销(Framework Overhead)
LLaMA Factory 底层使用的深度学习框架(如 PyTorch)本身的显存占用,包括:张量缓存、线程资源、内核调度开销、自动微分图结构等等。
计算方法:比较难精确计算; 估算方法:一般占用不会太大,我们可以先默认估算消耗 1G
Lora 适配器(LoRA Adapters)
在 Lora 微调中,不会直接修改原始模型的庞大权重,而是通过插入轻量级的 “Lora适配器模块” 来学习模型微调所需的变化,这部分可以理解为就是这个小模块的显存占用。
计算方法:显存占用 = LoRA 层数 × 秩(Rank)×(输入维度 + 输出维度)× 2B 估算方法:和我们上面提到的 LoRA 秩的大小呈正相关,一般占用不会太大,在常规配置下不会超过 0.5G,保守估计 0.5G
激活值(Activations)
前向传播过程中各层的输出张量(如隐藏层状态、注意力矩阵等)。简单理解就是模型 “处理数据时产生的所有中间结果”,这些结果需要临时存在显存里,主要受训练时同时处理的数据量大小的影响。
计算方法:显存占用 = 批量大小 × 序列长度 × 隐藏层维度 × 模型层数 × 单个元素字节数 估算方法:单次处理的 Token 量每增加 1K,显存约增加 2.5G
激活值的显存占用和我们上面提到的单 GPU 批量大小和数据集的截断长度成正相关,可以说除去以上三个值,剩下的就是激活值的占用,为了找到估算规律,我们做个简单的实验,在以上微调配置不变的情况下,我们设置一下不同的阶段长度和批量大小(单 GPU):
可以发现,当截断长度、批量大小分别设定为 1024 * 2 和 2048 * 1 时显存消耗是是完全相同的,激活值当显存占用就是和单个 GPU 单次处理的 Token 量是正相关的,单次处理的 Token 量每增加 1K,显存约增加 2.5G。
根据我们前面的微调配置,显存占用估算如下:
| 显存消耗分布 | 对应设置 | 估算方式 |
|---|---|---|
| 基础模型权重 | + 计算:70亿 × 2 Byte = 140亿字节 = 14 GB | |
| 框架开销 | ||
| LoRA 适配器 | ||
| 激活值 | 截断长度 = 4096 Token | + 估算:每1K Token约增加2.5 GB显存 + 计算:4 × 2.5 GB = 10 GB |
基础模型权重:14 GB
框架开销:1 GB
LoRA适配器:0.5 GB
激活值:10 GB
---------------------
总计:14 + 1 + 0.5 + 10 = **25.5 GB**
这里可以使用我开发的这个智能体进行简单估算(后续 LLaMA Factory 也会支持在 webui 中显示预估显存消耗):
https://www.coze.cn/s/3d3maKRCIlk/
注意这里的消耗建立在我们没有主动启用其他额外的优化手段的情况下,如果你的硬件配置有限,可以尝试开启以下两个优化手段。
显存优化技巧:liger_kernel
以上显存计算方式中,模型权重、Lora 适配器、框架消耗这些已经无法再进行优化了(不考虑量化的情况下),只有激活值的消耗还有一定的优化空间,这里我们可以启用 liger_kernel,在前面的加速方式章节,我们简单提到过,这里就派上用场了。
Liger Kernel 将 Transformer 中的关键操作(如 RMSNorm、RoPE、SwiGLU、CrossEntropy)重写为 Triton 内核,并通过内核融合将多个操作合并为一个计算步骤,避免了传统方法中多次内存读写带来的冗余存储,在模型微调过程中,可以显著降低数据处理相关的内存占用。
下面是我针对先前的几组配置,在开启 Liger Kernel 后的显存消耗实验:
可以发现,开启 liger_kernel 后,在 2K Token 和 4K Token 下的消耗分别为 16.6G 和 17.9G,每增加 1K Token,显存约增长 0.6G,相比先前的 1K Token 2.5G ,下降了 76% 。
注意:在默认情况下,LLaMA Factory 会启用 flash attention 进行加速,而开启 liger_kernel 后 flash attention 依然是会开启的,两者并不是互斥的关系。
分布式显存优化:DeepSpeed
以上的显存消耗我们都是基于单卡(单 GPU)的维度计算的,在实际任务中,单张消费级显卡是很难支撑大参数或者大批量的微调任务的,所以一般我们都会采用多卡分布式训练。
假定在以上的微调任务中,我们将截断长度改为 2048、批量大小改为3,那么预估的显存消耗( liger_kernel 未开启)为 14+1.5+15=30.5G,假定我们现在有两张 RTX 4090D(24GB) 的卡,总显存是 48G,理论上是完全可以 Cover 这个任务的,但如果你还是使用以上的配置,一定会爆显存:
这里可能会涉及到大家的一个常见物误解:我们虽然使用了多张卡进行训练,但却并不是真正意义上的内存平摊,模型参数和优化器状态仍需完整存储在单张卡上,无法突破单卡内存限制,可以理解为每张卡上的任务还是互相独立的,互不干扰,这样可以有效提升微调的速度,但每张卡都需要承担最大的显存消耗。想要真正的实现多卡的显存平摊,可以启用 DeepSpeed Stage3。
DeepSpeed 是由微软研发的一款深度学习优化库,旨在简化分布式训练与推理过程,DeepSpeed 通过ZeRO 技术将模型状态分片到多卡,消除显存冗余,并结合混合精度训练和并行策略(张量/流水线并行),使多卡训练时单卡显存占用随 GPU 数量显著降低,支持更大规模模型训练。
在 LLaMA Factory 中需要配置 DeepSpeed Stage:
我们发现 DeepSpeed Stage 配置里有四个选项:0(默认)、1、2、3,这里我们用一个通俗的例子来让大家更容易理解:如果把多卡训练比作「多人包饺子」,那 DeepSpeed Stage 就是「分工方案」,假设用 4 张卡训练大模型,对比不同 Stage的「分工模式」:
Stage 0(不开启 Stage):每张卡独立全包
做法:每张卡都存完整的模型参数、算前向、算反向、更新参数。 类比:4 个人各自包 4 盘饺子,每人都要准备 1 份馅、1 份面皮。
Stage 1:参数共享,计算独立
做法: 参数只存 1 份,放在所有卡上(共享馅),但每张卡各自算前向、反向、更新参数。 类比:4 个人共用 1 份馅,但各自擀面皮、包饺子。
Stage 2:参数和优化器分离
做法: 参数存1份(共享馅),优化器状态(如Adam的动量、方差)单独放在部分卡上(比如2张卡存优化器,2张卡算前向)。 类比:2个人专门管馅和调料(优化器),2个人负责擀面皮、包饺子。
Stage 3:极致分工,参数「流动」
做法: 把模型按层拆成多段(比如4张卡各管1 / 4层),参数不再固定在卡上,而是按计算流程「流动」: 卡 1 算前 1-4 层,用完参数就传给卡 2; 卡 2 算 5-8 层,用完再传给卡 3,以此类推。 优化器状态和参数都只存1份,放在「参数服务器」卡上,计算卡用完就还回去。 类比: 包饺子流水线:卡 1 负责剁馅,卡 2 负责擀面皮,卡 3 负责包饺子,卡 4 负责煮饺子,材料(馅、面皮)在流水线上传递。
不开启 Stage(Stage 0): 优点:简单,卡之间不用传数据,速度快。 缺点:显存占用大。 开启 Stage 1/2/3: 优点:用「分工」换显存,让更大的模型能在有限显卡上训练。 缺点:Stage 越高,卡之间通信越多,可能降低训练速度。
下面是我们在开启 DeepSpeed Stage 3 的情况下两张卡的显存消耗:分别是 16.3 G,总共消耗 32.6 G,比我们预估的 30.5 G 要多出 2G,这些就是多卡通信的额外开销:
今天这一期,我们就讲到这里,下一期,我会带大家一起学习微调过程中的 LOSS 要怎么观察,以及微调后模型的导出及部署。
如果本期对你有所帮助,希望得到一个免费的三连,感谢大家支持!