之前用 Qwen 3.5 4B 模型做全参微调第一次尝试跑了不到 2000 步直接爆显存后续又遇到 loss 震荡、权重合并失败一堆问题。中间折腾了一周查看各种资料最后在第二次尝试中通过切换到 LoRA 微调方案成功在一张 24GB 显存的消费级显卡上跑通了完整流程。这篇文章把第二次尝试的完整过程记录下来包含数据集准备、LoRA 配置、训练参数设置、权重合并和推理验证以及各个阶段踩到的坑。无论你是第一次接触大模型微调还是已经尝试过但失败了这篇实战教程都能给你一套可以照着复现的闭环方案。1. 背景与核心概念1.1 为什么选择 Qwen 3.5 4B 模型大模型微调这个词在很多开发者眼里似乎默认是对几十B、上百B参数的大模型进行的操作需要一张甚至多张 A100/H100 才跑得动。但实际上业务场景更常见的诉求是在有限算力下得到一个符合特定场景行为模式的模型这时候小参数模型反而是性价比最高的选择。Qwen 3.5 4B 模型从定位上说属于轻量级的通用大语言模型参数量在 4B 级别既能保持不错的对话和指令遵循能力又不像 7B、14B 那样对显存要求那么苛刻。在实际开发中4B 模型更适合以下场景垂直领域客服机器人通过微调让模型学会特定行业话术。私有化部署场景一台 GPU 服务器需要同时服务多个业务线模型体积太大无法满足并发需求。移动端或边缘设备原型验证先在小模型上验证产品逻辑再考虑是否需要升级到更大模型。很多开发者听到“微调”两个字第一反应是“我本地 GPU 显存不够”于是直接把这条路否掉了。但实际上微调和训练对显存的要求差异非常大4B 模型完全有办法在消费级显卡上完成微调。这也是本文的主题。1.2 全参微调与 LoRA 微调的区别要理解为什么能在一张 24GB 显存的显卡上微调 4B 模型必须先理解全参微调和 LoRA 微调在原理上的核心区别。全参微调Full Parameter Fine-Tuning指的是对模型的所有参数进行梯度更新。这听起来没什么问题但实际运行时显存里要同时存放的内容包括模型权重本身。梯度。优化器状态比如 AdamW 需要保存一阶动量和二阶动量参数量的 2 倍。前向过程计算出来的中间激活值。算一笔简单的账一个 4B 参数的模型假设用 FP16 半精度加载权重本身需要约 8GB 显存。全参微调时梯度还需要 8GBAdamW 优化器状态需要 16GB这些加起来已经 32GB 了。这还没算激活值、KV Cache 和训练框架自身的开销。所以用 24GB 显存做 4B 模型的全参微调理论上就非常紧张实际跑起来大概率 OOM。LoRALow-Rank Adaptation低秩适配的思路完全不同。它不更新原始模型权重而是在模型的线性层旁边额外插入一小部分低秩矩阵训练时只优化这些新增的小矩阵。原始权重保持冻结状态不需要计算梯度也基本不需要维护优化器状态。此时显存占用变成模型权重冻结约 8GB。LoRA 低秩矩阵权重、梯度和优化器状态只需要几百万或几千万参数规模显存开销非常小。激活值仍有但显存压力相比全参微调已经大大缓解。这样计算下来4B 模型启用 LoRA 微调24GB 显存不仅跑得动还可以把 batch size 适当调大一些。1.3 LoRA 之外的另一种选择QLoRA除了标准 LoRA还有一个改进方案叫 QLoRA它的关键变化是先把基础模型量化到 4bit比如用 bitsandbytes 做 NF4 量化再在量化后的模型上应用 LoRA。QLoRA 的好处是进一步降低显存占用。同样是 4B 模型4bit 量化后权重只需要大约 2GB这样就给激活值和 LoRA 参数留出了更多空间。缺点是在量化模型上注入 LoRA 时默认情况下 LoRA 权重本身是 FP16 或 BF16 精度训练时会涉及反量化和再量化训练速度会略慢一些。如果显卡显存是 16GB 或更小QLoRA 会是更稳妥的选择。这次第二次尝试先使用了标准 LoRA如果你的显卡是 RTX 4080、4090 这种 16GB 及以上显存参考本文配置即可如果是 8GB 显存建议把本文中的加载方式换成 4bit 量化加载。2. 环境准备与版本说明微调大模型的环境配置是整个流程中最容易出问题的一步很多失败案例并不是模型配置写错了而是 PyTorch 版本和 CUDA 版本不匹配导致的速度异常或直接报错。2.1 硬件环境本次实测使用的硬件配置如下硬件配置GPUNVIDIA RTX 4090 24GB单卡CPU16 核以上用于数据加载与 Tokenizer 并行处理内存32GB磁盘预留至少 50GB 空闲空间如果显存小于 24GB比如 16GB可以尝试把 batch size 调整为 1并启用梯度累积如果显存是 8GB建议启用 4bit 量化加载模型这是后文会提到的降级方案。2.2 软件环境本次实测的软件版本如下软件版本操作系统Ubuntu 22.04Python3.10CUDA12.1PyTorch2.1.2Transformers4.40.2PEFT0.10.0TRL0.9.4Datasets2.19.1bitsandbytes0.43.1需要说明的是这里列出的版本只代表本次实测环境你在自己的环境中不必完全一致但建议保持主要依赖之间兼容。最容易出现兼容性问题的是 PyTorch 和 CUDA建议先装 PyTorch 官方对应的 CUDA 版本再安装 Transformers 和 PEFT 等 Python 包。版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。2.3 安装依赖建议使用虚拟环境避免污染系统环境。python3.10 -m venv venv source venv/bin/activate pip install --upgrade pip pip install torch2.1.2 torchvision0.16.2 torchaudio2.1.2 --index-url https://download.pytorch.org/whl/cu121 pip install transformers4.40.2 peft0.10.0 trl0.9.4 datasets2.19.1 bitsandbytes0.43.1 accelerate0.29.3安装完成后可以先验证 PyTorch 是否能正确识别 GPUpython -c import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))如果输出True和显卡名称说明环境就绪。3. LoRA 微调原理拆解在进入完整代码之前有必要先把 LoRA 的核心参数和原理拆开讲清楚。很多教程直接贴代码读者抄完能跑通但遇到需要调参的场景就完全不知道怎么改。掌握原理后调参才有方向。3.1 LoRA 做了什么一个标准 Transformer 层里包含多个线性层例如q_proj、k_proj、v_proj、o_proj等。全参微调时这些层的权重都会更新更新量等于权重矩阵本身的大小计算量很大。LoRA 的做法是冻结这些原始权重W然后为每个目标线性层增加一个低秩分支h Wx BAx其中A和B是新增的小矩阵。A的维度是(r, d)B的维度是(d, r)这里r远小于d比如d是 4096r取 16那么新增参数量只有原来的很小一部分。训练时W保持不变只有A和B接受梯度更新。这样需要保存的状态就从整个模型的参数规模缩小到了低秩矩阵的规模。推理时有两种处理方式把训练好的BA加回到原始权重W上得到W W BA然后正常加载合并后的权重。也可以不合并继续用原始权重配合外部保存的 adapter 权重一起加载。推荐训练完成后合并权重这样部署时模型结构和普通模型没有区别不需要额外依赖 PEFT 库。3.2 LoRA 关键参数说明参数作用本文取值r低秩矩阵的秩越大表示可学习的参数越多表达能力越强但显存占用也增大8lora_alphaLoRA 缩放因子实际缩放比为lora_alpha / r16lora_dropout低秩分支的 Dropout 概率防止过拟合0.05target_modules指定要对哪些模块注入 LoRAq_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_projbias是否训练偏置项通常设为 nonenonetask_type任务类型例如因果语言模型是 CAUSAL_LMCAUSAL_LMr值的选择需要结合数据量和任务复杂度。数据量小r过大会导致过拟合数据量大r过小则模型学习不到足够特征。对 4B 模型来说r取 8 或 16 是常见的起点后续可以根据验证集效果调到 32 或 64。3.3 为什么要选 target_modules在注入 LoRA 时target_modules参数决定哪些线性层被注入低秩分支。如果只注入q_proj、v_proj参数量会很小训练速度快但效果可能不够如果把所有线性层都注入参数量会增大效果也更好但对显存和训练时间的影响较小因为低秩分支本身很小。对于 Qwen 这类基于 Transformer 结构的大模型比较推荐的做法是把注意力模块的四个投影层和 MLP 模块的三个线性层都作为目标模块。这样模型能学习到的语义特征更丰富但代价是 adapter 文件会稍微大一些。4. 第二次尝试完整实战流程4.1 创建项目结构本次实战的项目目录结构如下qwen-finetune/ ├── data/ │ └── train.jsonl ├── output/ │ ├── adapter/ │ └── merged_model/ ├── scripts/ │ ├── train_lora.py │ ├── merge_weights.py │ └── inference.py └── requirements.txt其中data存放训练数据。output/adapter保存训练得到的 LoRA 权重。output/merged_model保存合并后的完整模型权重。scripts存放三个 Python 脚本。先创建目录mkdir -p qwen-finetune/{data,output/{adapter,merged_model},scripts} cd qwen-finetune4.2 准备训练数据集微调效果好不好数据质量占了很大比重。这里使用一个最小示例数据格式为jsonl每一行是一个完整的指令对话样本。{instruction: 请介绍一下微积分的基本概念。, output: 微积分是数学中研究函数变化和积累问题的一个分支主要包括极限、微分和积分三部分。} {instruction: Python 中如何判断一个数是否为偶数, output: 可以使用取余运算符%若该数对 2 取余等于 0则为偶数。例如8 % 2 的结果是 0。}把以上内容保存到data/train.jsonl。实际项目中建议准备至少 500 条高质量数据起步数据太少模型学不到稳定模式。4.3 编写训练脚本训练脚本是这次微调的核心完整代码如下。# 文件路径scripts/train_lora.py import json import torch from datasets import Dataset from transformers import ( AutoTokenizer, AutoModelForCausalLM, TrainingArguments, BitsAndBytesConfig, ) from trl import SFTTrainer from peft import LoraConfig, get_peft_model def build_dataset(data_path, tokenizer, max_length1024): samples [] with open(data_path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue sample json.loads(line) text ( fuser{sample[instruction]}/user\n fassistant{sample[output]}/assistant ) samples.append({text: text}) dataset Dataset.from_list(samples) def tokenize_function(examples): return tokenizer(examples[text], truncationTrue, max_lengthmax_length, paddingFalse) tokenized_dataset dataset.map(tokenize_function, batchedFalse) return tokenized_dataset def main(): model_path Qwen/Qwen-3.5-4B tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token # 使用 4bit 量化加载模型能显著降低显存占用 bnb_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_quant_typenf4, bnb_4bit_compute_dtypetorch.bfloat16, bnb_4bit_use_double_quantTrue, ) model AutoModelForCausalLM.from_pretrained( model_path, trust_remote_codeTrue, quantization_configbnb_config, torch_dtypetorch.bfloat16, device_mapauto, ) lora_config LoraConfig( r8, lora_alpha16, lora_dropout0.05, biasnone, task_typeCAUSAL_LM, target_modules[ q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj, ], ) training_args TrainingArguments( output_dir./output/adapter, per_device_train_batch_size1, gradient_accumulation_steps4, learning_rate2e-4, num_train_epochs3, logging_steps50, save_steps500, save_total_limit2, fp16True, report_tonone, optimadamw_torch, ) trainer SFTTrainer( modelmodel, argstraining_args, train_datasetbuild_dataset(data/train.jsonl, tokenizer), tokenizertokenizer, peft_configlora_config, dataset_text_fieldtext, max_seq_length1024, ) trainer.train() # 训练结束后保存 adapter 权重 trainer.model.save_pretrained(./output/adapter) tokenizer.save_pretrained(./output/adapter) if __name__ __main__: main()运行训练cd qwen-finetune python scripts/train_lora.py如果环境正确训练过程中会输出类似以下日志{loss: 1.2345, learning_rate: 0.00012, epoch: 0.05} {loss: 0.9876, learning_rate: 0.00011, epoch: 0.10}这里涉及几个关键配置逐一说明load_in_4bit将基础模型以 4bit 精度加载这是显存占用的关键优化。per_device_train_batch_size1每个 GPU 上的 batch size。24GB 显存可以尝试 1 或 2显存不足时优先使用gradient_accumulation_steps。gradient_accumulation_steps4等价于把 batch size 累积到 4让梯度更新更稳定。learning_rate2e-4LoRA 微调通常会比全参微调使用更大的学习率因为训练参数少。fp16使用混合精度训练减少显存占用并在一定程度加速计算。4.4 合并 LoRA 权重训练得到的 LoRA adapter 权重需要合并回原始模型才能得到完整可部署的模型权重。编写合并脚本# 文件路径scripts/merge_weights.py from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer import torch # 原始模型路径 base_model_path Qwen/Qwen-3.5-4B # LoRA adapter 路径 adapter_path ./output/adapter # 合并后的输出路径 save_path ./output/merged_model tokenizer AutoTokenizer.from_pretrained(base_model_path, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained( base_model_path, trust_remote_codeTrue, torch_dtypetorch.bfloat16, device_mapauto, ) model PeftModel.from_pretrained(model, adapter_path) merged_model model.merge_and_unload() merged_model.save_pretrained(save_path) tokenizer.save_pretrained(save_path)运行合并命令python scripts/merge_weights.py合并后的模型保存在output/merged_model目录下包含完整的模型权重文件和分词器文件可以直接作为普通模型加载使用。4.5 推理验证合并完权重之后需要验证微调效果。编写推理脚本# 文件路径scripts/inference.py from transformers import AutoModelForCausalLM, AutoTokenizer model_path ./output/merged_model tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained( model_path, trust_remote_codeTrue, torch_dtypetorch.bfloat16, device_mapauto, ) prompt userPython 中如何判断一个数是否为偶数/user\nassistant inputs tokenizer(prompt, return_tensorspt).to(cuda) outputs model.generate( **inputs, max_new_tokens128, temperature0.7, do_sampleTrue, top_p0.9, ) response tokenizer.decode(outputs[0], skip_special_tokensTrue) print(response)预期输出示例userPython 中如何判断一个数是否为偶数/user assistant可以使用取余运算符%若该数对 2 取余等于 0则为偶数。/assistant 如果回答内容符合预期说明这次微调流程已经完整跑通。5. 常见问题与排查思路即使流程跑通了在微调过程中依然会遇到各种问题。下面按出现问题概率从高到低排列。问题现象常见原因解决思路启动训练时报 CUDA out of memorybatch size 过大、激活值占满显存调小 batch size 为 1开启梯度累积或改用 4bit 量化加载训练过程中 loss 不下降或震荡学习率过大、数据噪声大降低学习率到 1e-4 或 5e-5检查数据是否有标签错误训练结束后模型回答和 base 模型没什么区别训练步数不足、数据量太少增加 epoch 或数据量适当把 LoRA 的 r 调大loss 下降但下游评测分数没有提升模型过拟合训练集增加验证集和早停机制提高 lora_dropout合并权重时报 shape mismatchLoRA 的 target_modules 与模型结构不匹配打印模型结构确认线性层名称是否正确推理时生成内容重复或乱码temperature 过高、model 上下文长度不足调整采样参数增加 max_new_tokens检查数据格式单独的排查清单当显存不足时先不要急着买新显卡优先检查per_device_train_batch_size是否过大然后看是否启用了gradient_accumulation_steps。训练速度过慢时检查是否启用了fp16以及 PyTorch 是否能正确使用 GPU。如果训练中途 crash可以在TrainingArguments中设置resume_from_checkpointTrue从最近的 checkpoint 恢复训练。如果数据集是自定义格式先写一个独立的脚本打印 tokenize 后的结果确认数据没有异常。另外一个容易被忽略的是 tokenizer 的 pad token。如果 pad token 为 None在 batch 填充时可能报错。建议在训练前if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token6. 最佳实践与工程建议6.1 数据质量优先于数据数量微调不是数据越多越好而是数据质量越高越好。收集数据时需要关注指令的多样性、输出格式的一致性和答案的准确性。哪怕只有几百条高质量数据也可能比几万条充满噪声的数据效果更好。另外建议在训练前对数据进行去重和清洗如果数据中有很长且重复的模板文本可能会严重浪费模型的上下文窗口。6.2 小步迭代快速验证第一次跑微调时不需要一上来就用完整的几万条数据。建议先用 100~200 条数据跑通训练和推理闭环验证代码没有问题再增加数据规模。这样即使配置有误几分钟内就能发现而不是等几个小时后才报错。6.3 训练过程监控在TrainingArguments中设置logging_steps50可以每 50 步打印一次 loss。推荐结合tensorboard或wandb进行更细致的监控。当观察到 loss 在某个区间持续震荡建议立刻停止训练检查学习率或数据而不是等到 3 个 epoch 全部跑完。6.4 checkpoint 定期保存save_steps500表示每 500 步保存一个 checkpointsave_total_limit2表示只保留最近 2 个 checkpoint避免磁盘被占满。如果训练意外中断可以通过resume_from_checkpointTrue恢复不需要从头开始。6.5 生产环境部署注意事项合并后的模型可以按普通模型处理部署时建议使用torch_dtypetorch.bfloat16加载模型减少显存占用。如果请求量较大可以尝试使用 vLLM 或 TGI 等推理框架提升吞吐量。量化部署时要在评估集上验证量化后的效果避免精度损失影响业务质量。不宜把微调模型直接暴露在公网环境中API 服务需要做好鉴权和限流。6.6 微调并不是垂直领域落地的唯一方案很多场景下不微调模型也能拓展垂类应用。检索增强生成RAG把领域知识放入向量数据库对话时先检索再让大模型生成。提示词工程通过精心设计的 few-shot 提示词引导模型遵循特定输出格式。功能路由由一个小模型判断用户意图再决定调用哪个大模型或接口。微调适合的是模型行为模式的改变比如对话风格、输出格式、工具调用习惯等。如果只是想引入大量知识RAG 一般会更稳定、更新更及时。7. 总结与学习路线本次用 LoRA 方式微调 Qwen 3.5 4B 模型完整跑通了数据准备、训练、权重合并和推理验证的流程。相比第一次全参微调失败的经历第二次采用 LoRA 方案后在 24GB 显存显卡上顺利运行。核心收获可以总结为三点LoRA 微调通过冻结原始权重、只训练低秩矩阵把训练所需显存降低到普通消费级 GPU 可接受的范围。数据格式、tokenizer 的 pad token 设置、target_modules 的选择是影响训练是否顺利的几个关键细节。训练完的 LoRA 权重需要合并回原始模型才能得到完整可部署的模型文件。下一步可以继续学习了解 QLoRA 与 LoRA 在效果和显存占用上的差异。学习如何制作更高质量的训练数据集比如用 Self-Instruct 方法生成指令数据。探索 PEFT 库中的其他参数高效微调方法如 Prefix Tuning、P-Tuning v2。使用trl库的DPOTrainer做偏好对齐微调。最后提醒一句微调过程中如果遇到问题不要急着换方案先看显存占用再看数据质量最后看训练超参数。这三步排查顺序可以帮你解决绝大多数训练异常。如果本文对你有帮助可以收藏备用后续有新的实战经验也会继续更新。