
第二次尝试微调 qwen3.5-4b 模型时最重要的一步不是打开训练脚本而是先复盘第一次到底失败在哪个环节。很多项目卡在原地不是因为代码不会写而是因为同一套错误方案反复执行换了一个数据集又来一轮。本文记录的是以 qwen3.5-4b 为模型代号的第二次微调尝试核心路线从全参微调切到 LoRA用一份对话式训练数据跑通数据准备、参数配置、训练监控、LoRA 合并和推理对比的完整闭环。读完你可以照着复现也可以把里面的检查清单直接带到下一次微调里。如果你本机没有同名权重用 4B 量级的实际可下载模型替换即可本文重点是流程而不是某一次具体权重。1. 先复盘第一次失败再确定第二次的微调路线1.1 第一次尝试最容易踩的四个坑第一次微调 4B 级别大模型时常见的失败现象其实很集中。显存不足是最先暴露的问题直接加载全参模型做反向传播4B 参数在 bf16 下权重就要约 8GB加上梯度和优化器状态单卡 16GB 几乎不可能跑起来。很多人一开始就把per_device_train_batch_size设为 4结果还没到第一个 log 就报 CUDA OOM。第二个坑是学习率设置不当。全参微调常参考对比学习里的 1e-5 附近但对数据量很小的领域任务来说学习率偏大容易让模型快速遗忘原有能力表现为训练 loss 下降很快生成时却重复、乱码或完全丢失对话格式。第三个坑是数据格式没有统一。直接把领域文本当作text字段做 next token prediction模型学到的是“续写”不是“问答”。推理时又手动拼接 prompt和训练时的格式不一致效果自然差。第四个坑是训练和推理的 tokenizer 处理不一致。比如训练时自动加 padding推理时没有使用相同的 chat template导致 special token 错位。第二次尝试时这些根因都要逐一确认而不是简单把学习率调低再跑一次。1.2 为什么第二次改用 LoRA 而不是继续全参训练LoRA 的核心思路是冻结原始模型权重在部分线性层旁边添加低秩矩阵只训练这些低秩参数。这样可训练参数通常只有全参的 0.1% 到 0.5%显存峰值明显下降训练时长也短得多。对 4B 模型做领域微调LoRA 是性价比非常高的中间方案。它不改变模型结构推理时可以合并回原权重也可以用 adapter 方式单独保存不会污染基座模型。第一次尝试如果因为全参训练导致显存或收敛问题第二次优先改用 LoRA 是合理的。如果显存仍然紧张还可以进一步退到 QLoRA把基座模型量化到 4bit再加载 LoRA 层训练。QLoRA 的显存占用比普通 LoRA 更低但训练速度更慢量化过程也可能带来轻微精度损失。方案可训练参数显存需求4B 模型示例训练速度适用场景全参微调全部参数很高通常需要 24GB 以上慢数据量大、算力充足、需要整体适配LoRA极少约 0.1% - 0.5%中等常见 16GB - 24GB较快领域问答、格式适配、小规模数据QLoRA极少约 0.1% - 0.5%较低常见 12GB - 16GB较慢单卡显存小、低成本验证1.3 4B 模型微调前先算清显存账显存占用并不只由模型大小决定还取决于 batch size、序列长度、是否开启 gradient checkpointing、优化器状态以及是否量化。粗略估算时可以先看模型权重4B 参数在 bf16 下约 8GBfp16 同样约 8GBfp32 则到 16GB。全参训练还要保存梯度、Adam 优化器的一阶动量和二阶动量这部分开销通常是模型权重的数倍。LoRA 训练时优化器状态只针对低秩矩阵所以整体占用低很多。一个常见的组合是4B LoRA bf16 per_device_train_batch_size1max_seq_length1024 gradient checkpointing实测显存需求大约在 12GB 到 18GB 之间。如果换成全参同样配置往往需要 24GB 以上。这里给的是经验区间不是精确值落地前要结合自己的 GPU 型号实操确认。注意不能只看“模型多少 G”决定显存还要把激活值、padding 长度和中间变量都算进去。先把 batch size 调到 1跑通 50 步再逐步放大才是稳妥路径。2. 环境、依赖和模型权重先打地基2.1 推荐运行环境第二次尝试不要浪费时间在环境反复报错上。推荐使用 Linux 环境Python 3.10 以上PyTorch 2.1 以上CUDA 11.8 或 12.1 以上。Windows 也可以跑但部分扩展编译和显存行为更容易出问题建议优先使用容器或远程 GPU 环境。学习实验阶段单张 16GB 显存的 GPU 就能跑 QLoRA如果只用 LoRA 且数据长度较长建议 24GB 以上。生产环境还要考虑多卡并行、断点续训、日志收集和模型版本管理不是简单的“能跑通”就够。如果你完全不确定自己的环境是否满足要求可以先运行一个小冒烟测试加载模型输入一段普通文本让模型正常生成一次。如果这一步都失败后面的微调没有必要开始。2.2 安装依赖和版本确认训练脚本中常见的库是transformers、peft、datasets、accelerate、bitsandbytes如果使用trl的SFTTrainer还需要额外安装trl。以 pip 方式安装pip install transformers accelerate peft datasets bitsandbytes trl版本没有放到 requirements 锁定时建议先确认本机实际安装版本再决定使用哪个 API。比如transformers的Trainer在较新版本中使用eval_strategy旧版本则可能是evaluation_strategy。如果教程里的参数名和本地版本不一致优先以官方文档为准。下载模型权重可以使用modelscope也可以使用transformers直接加载 Hugging Face 路径from modelscope import snapshot_download model_path snapshot_download(Qwen/Qwen2.5-3B-Instruct) print(model_path)如果你的代码中已经配置了本地模型目录直接传路径即可。标题中的qwen3.5-4b只是本次尝试使用的模型代号实际落地时一定要替换成本机真实存在、且经过基座验证的模型目录。2.3 确认 tokenizer 和模型是否匹配加载模型前先检查 tokenizer 是否存在chat_template是否可用pad_token是否为 None。很多训练脚本在打 padding 时崩溃就是因为没有处理 pad token。可以用下面这段代码做前置检查from transformers import AutoModelForCausalLM, AutoTokenizer model_path /path/to/qwen-4b-instruct tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained( model_path, torch_dtypeauto, device_mapauto, trust_remote_codeTrue, ) print(pad_token:, tokenizer.pad_token if tokenizer.pad_token is not None else tokenizer.eos_token) print(chat_template:, tokenizer.chat_template if tokenizer.chat_template is not None else None) print(model dtype:, model.dtype)如果pad_token为 None一般会手动设置成tokenizer.eos_token。如果chat_template为 None说明该 tokenizer 没有内置对话模板需要自行构造 prompt这在 4B 指令模型的微调中非常少见但遇到时要先处理。3. 训练数据准备数据格式决定 loss 能降到什么程度3.1 对话格式优先不要直接塞纯文本目标领域微调通常希望模型学会“用户提问 - 模型回答”的交互方式。如果数据是纯文本模型学到的是语言概率分布而不是指令遵循能力。第二次尝试时推荐把训练样本整理成对话结构。一种简单格式是 JSONL每行一个样本{instruction: 根据产品描述生成一段客服回答, input: 客户问订单三天没有发货。, output: 您好您的订单已确认预计今天内完成发货请您耐心等待。}也可以直接使用messages字段{messages: [{role: user, content: 客户问订单三天没有发货。}, {role: assistant, content: 您好您的订单已确认预计今天内完成发货请您耐心等待。}]}在数据预处理时使用tokenizer.apply_chat_template把 messages 转成模型要求的输入格式。这样能保证训练和推理看到的特殊 token 完全一致。3.2 数据量、去重和数据集切分数据量不是越多越好。第一次尝试如果只有几百条模型很可能直接过拟合训练 loss 降到很低但外部验证效果极差。第二次建议分成三档考虑验证冒烟实验500 到 1000 条主要看流程能不能跑通。有效领域微调2000 到 10000 条能观察到明显的领域能力提升。高覆盖微调10000 条以上要求数据本身有足够多样性和质量。数据清洗至少要做三步去掉空 output去掉重复样本检查字段类型是否一致。重复数据会导致训练集中某些样本权重被放大即使不加num_train_epochs模型也容易过度拟合这些重复内容。还需要切分验证集。建议至少保留 5% 到 10% 的数据作为 eval 集并且训练集和验证集不能有共享文本否则 loss 会虚低。3.3 写一个数据校验脚本在训练之前先对数据做一次批量校验统计字段缺失、长度分布和重复率。这个脚本不需要复杂关键是提前暴露问题。import json from collections import Counter path train.jsonl rows [] with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if not line: continue rows.append(json.loads(line)) print(总样本数:, len(rows)) missing [i for i, row in enumerate(rows) if not row.get(output, ).strip()] print(缺失 output 的样本:, len(missing)) outputs [row.get(output, ).strip() for row in rows] counter Counter(outputs) duplicated sum(1 for v in counter.values() if v 1) print(重复 output 数量:, duplicated) lengths [len(row.get(output, )) for row in rows] print(output 长度中位数:, sorted(lengths)[len(lengths) // 2] if lengths else 0)如果发现有大量缺失 output 或重复数据先清洗再开始训练不要等到 loss 不降时才回头排查。4. 用 LoRA 搭建微调训练脚本4.1 加载基座模型时的精度选择第二次尝试时我建议先不要用 4bit 量化先用 bf16 加载普通 LoRA。原因很简单少一层量化排查问题更容易。如果显存不够再降级到 QLoRA。bf16 适合 Ampere 及以上架构的 GPU。如果你的 GPU 不支持 bf16会收到相关报错此时改用 fp16并让Trainer自己处理梯度缩放。示例import torch from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig, get_peft_model model_path /path/to/qwen-4b-instruct tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token model AutoModelForCausalLM.from_pretrained( model_path, torch_dtypetorch.bfloat16, device_mapauto, trust_remote_codeTrue, )使用device_mapauto可以自动分配模型到可用 GPU。单卡环境下也能正常运行。注意如果后续要做merge_and_unload()需要保留原始模型对象。4.2 LoRA 参数配置LoraConfig中最重要的参数是r、lora_alpha、target_modules和task_type。参数常见值作用调整建议r8 / 16 / 32低秩矩阵的秩决定可训练参数量小数据集用 8 起步过拟合不明显可以提到 16lora_alpha16 / 32控制 LoRA 权重缩放比例一般设为r的 1 到 2 倍lora_dropout0.05 / 0.1防止过拟合的 dropout小数据可以设 0.05数据量大时可提高target_modules模型线性层列表指定哪些层插入 LoRA要和模型实际层名匹配biasnone是否训练偏置通常保持 nonetask_typeCAUSAL_LM任务类型生成模型固定为 CAUSAL_LM对于常见的 Qwen2 系列模型target_modules通常包含q_proj、k_proj、v_proj、o_proj、gate_proj、up_proj、down_proj。确认方式可以打印模型的层名也可以在加载后直接使用这些常见命名。lora_config LoraConfig( r8, lora_alpha16, lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj], biasnone, task_typeCAUSAL_LM, ) model get_peft_model(model, lora_config) model.print_trainable_parameters()如果print_trainable_parameters()输出的可训练参数占比为 0%说明target_modules没有匹配到任何层。此时要检查模型的实际层名。4.3 TrainingArguments 中容易被调崩的参数训练参数是第二次尝试中改动风险最高的部分。下面几个参数需要特别关注per_device_train_batch_size单卡 batch。显存不足时先降到 1。gradient_accumulation_steps不改变单步显存只影响多少步更新一次参数。batch size 不够时用这个补。learning_rateLoRA 常用 1e-4 到 3e-4不要照搬全参微调的 1e-5。num_train_epochs小数据可以先从 2 到 3 轮开始eval loss 上升时及时停止。bf16/fp16根据 GPU 支持情况选择。gradient_checkpointing开启后显存明显下降但训练会慢一些。save_total_limit控制保存的 checkpoint 数量避免磁盘写满。eval_strategy新版 transformers 使用这个名字老版本是evaluation_strategy。示例from transformers import TrainingArguments training_args TrainingArguments( output_dir./qwen-4b-lora-out, overwrite_output_dirTrue, num_train_epochs2, per_device_train_batch_size2, gradient_accumulation_steps4, learning_rate2e-4, lr_scheduler_typeconstant, warmup_ratio0.03, logging_steps10, eval_strategysteps, eval_steps100, save_steps200, save_total_limit2, bf16True, gradient_checkpointingTrue, report_tonone, )4.4 完整训练脚本示例使用Trainer和DataCollatorForSeq2Seq的组合不依赖额外封装便于理解每一步在做什么。import torch from transformers import ( AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, DataCollatorForSeq2Seq, ) from peft import LoraConfig, get_peft_model from datasets import load_dataset model_path /path/to/qwen-4b-instruct train_data_path train.jsonl tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) if tokenizer.pad_token is None: tokenizer.pad_token tokenizer.eos_token dataset load_dataset(json, data_filestrain_data_path, splittrain) def format_example(example): user_content example[instruction] if example.get(input): user_content \n example[input] messages [ {role: user, content: user_content}, {role: assistant, content: example[output]}, ] tokens tokenizer.apply_chat_template( messages, tokenizeTrue, add_generation_promptFalse, return_dictTrue, ) input_ids tokens[input_ids] return { input_ids: input_ids, attention_mask: tokens[attention_mask], labels: input_ids, } dataset dataset.map(format_example, remove_columnsdataset.column_names) split dataset.train_test_split(test_size0.05) model AutoModelForCausalLM.from_pretrained( model_path, torch_dtypetorch.bfloat16, device_mapauto, trust_remote_codeTrue, ) lora_config LoraConfig( r8, lora_alpha16, lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj], biasnone, task_typeCAUSAL_LM, ) model get_peft_model(model, lora_config) model.print_trainable_parameters() training_args TrainingArguments( output_dir./qwen-4b-lora-out, num_train_epochs2, per_device_train_batch_size2, gradient_accumulation_steps4, learning_rate2e-4, lr_scheduler_typeconstant, warmup_ratio0.03, logging_steps10, eval_strategysteps, eval_steps100, save_steps200, save_total_limit2, bf16True, gradient_checkpointingTrue, report_tonone, ) trainer Trainer( modelmodel, argstraining_args, train_datasetsplit[train], eval_datasetsplit[test], tokenizertokenizer, data_collatorDataCollatorForSeq2Seq(tokenizertokenizer, paddinglongest), ) trainer.train()上面代码把整段对话文本都作为labels也就是模型同时学习用户输入和助手输出。对大多数流程验证来说足够但如果你想严格只让模型学习回答部分就需要在labels里把用户 token 对应位置替换为-100。否则模型会花一部分能力在“模仿用户提问”上。5. 开始训练、观察指标和保存结果5.1 启动训练命令把脚本保存为train_lora.py运行python train_lora.py首次启动建议先用很小的数据量比如 100 条只跑 20 到 50 步确认训练脚本没有报错、loss 在逐步下降再进入完整训练。这样可以避免数据或配置问题浪费大量时间。5.2 loss 曲线怎么读训练日志中会输出loss和eval_loss。关键不是第一步的数字而是趋势。训练 loss 持续下降但 eval loss 快速上升说明过拟合两个 loss 都震荡可能是学习率过大或数据噪声太多loss 完全没有下降趋势则需要检查数据格式、pad token 和冻结状态。Trainer输出示例{loss: 2.3456, learning_rate: 0.0002, epoch: 0.02} {loss: 1.2345, learning_rate: 0.0002, epoch: 0.03}第二次尝试时建议记录第一次结果的关键指标比如训练集样本数、epoch、loss 下降到多少、验证集表现如何。后续调参才有对比基础。5.3 检查点保存、加载与 LoRA 权重合并训练结束后output_dir下会保存多个 checkpoint。如果想单独加载 adapter 做推理可以直接使用PeftModel.from_pretrainedfrom transformers import AutoModelForCausalLM, AutoTokenizer from peft import PeftModel base_model_path /path/to/qwen-4b-instruct adapter_path ./qwen-4b-lora-out/checkpoint-500 tokenizer AutoTokenizer.from_pretrained(base_model_path, trust_remote_codeTrue) base_model AutoModelForCausalLM.from_pretrained( base_model_path, torch_dtypetorch.bfloat16, device_mapauto, trust_remote_codeTrue, ) model PeftModel.from_pretrained(base_model, adapter_path) merged_model model.merge_and_unload() merged_model.save_pretrained(./qwen-4b-merged) tokenizer.save_pretrained(./qwen-4b-merged)merge_and_unload()会把低秩矩阵合并回原始权重得到一个独立的模型目录方便后续用普通加载方式跑推理。如果不合并也可以保留 adapter 路径推理时动态加载但要求基座模型路径一致。5.4 显存和训练速度监控训练过程中建议周期性查看显存使用情况nvidia-smi -l 2也可以用 PyTorch 打印更详细的内存摘要import torch print(torch.cuda.memory_summary())如果显存占用接近上限但还没 OOM可以适当调小max_seq_length或单卡 batch size。训练速度主要受 GPU 型号、序列长度和gradient_checkpointing影响开启梯度检查点后速度会下降但显存占用会明显缓解。6. 推理验证和第一次结果做对比6.1 加载训练好的模型进行推理训练完成并合并权重后用和训练时一致的apply_chat_template生成结果import torch from transformers import AutoModelForCausalLM, AutoTokenizer model_path ./qwen-4b-merged tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) model AutoModelForCausalLM.from_pretrained( model_path, torch_dtypetorch.bfloat16, device_mapauto, trust_remote_codeTrue, ) prompt 订单三天没有发货怎么办 messages [{role: user, content: prompt}] inputs tokenizer.apply_chat_template( messages, tokenizeTrue, add_generation_promptTrue, return_dictTrue, return_tensorspt, ).to(model.device) outputs model.generate( **inputs, max_new_tokens256, do_sampleTrue, temperature0.7, ) response tokenizer.decode(outputs[0][inputs[input_ids].shape[1]:], skip_special_tokensTrue) print(response)这里的关键是add_generation_promptTrue它告诉 tokenizer 在对话末尾追加模型开始作答的提示符。如果只做推理不加这个参数生成的文本会直接跟在用户内容之后格式会乱掉。6.2 对比维度不要只看 loss第二次尝试是否算成功至少要比较以下维度对比项第一次结果描述第二次结果描述是否遵循指令格式是否完整输出回答是否按训练模板输出内容准确性是否答非所问是否包含领域关键信息文本流畅度是否重复、乱码是否自然连贯对用户输入的容忍度是否只能在固定模板下工作换一种问法是否仍能回答泛化到未见过问题是否存在严重遗忘基础能力是否保留通用能力建议准备 20 到 50 条训练时没见过的真实问题手工逐条判断。不要因为训练集 loss 低就认为微调成功外部验证才是最终标准。6.3 结果不理想时的下一步判断如果第二次结果仍然不理想不要急着调参先判断问题出在哪个环节。数据质量差会导致回答内容空洞学习率过大会导致基础能力遗忘LoRA rank 太小会限制模型适配能力epoch 过多会过拟合领域样例。一个可用的排查顺序是先跑基座模型确认同一句 prompt 下基座是否已经具备部分能力。再用微调后模型跑对比差异。如果基座本身也无法回答说明是数据覆盖或知识注入问题不是 LoRA 参数问题。如果基座能回答但微调后变差优先检查学习率、epoch 和验证集是否过拟合。7. 常见问题排查链路7.1 loss 不降或震荡现象表现为训练了 500 步loss 仍然在初始值附近抖动或者一开始下降很快然后发散。可能的检查顺序确认print_trainable_parameters()输出的可训练参数占比不是 0%。确认数据集字段名和format_example里取字段名一致。确认 tokenizer 的pad_token已设置。确认labels中没有大量-100把主要内容屏蔽掉。确认学习率没有过大尝试降到 1e-4。7.2 CUDA OOM 和显存不足现象是训练刚开始就报CUDA out of memory。优先处理方案per_device_train_batch_size降到 1。开启gradient_checkpointingTrue。缩短最大序列长度过滤掉过长样本。使用 QLoRA把基座模型以 4bit 加载。关闭report_towandb或降低 logging 频率减少额外显存消耗。7.3 生成乱码或重复现象是模型输出大量重复片段或者夹杂特殊符号。可能原因训练样本本身包含乱码或噪声数据。labels没有正确屏蔽用户输入模型把用户话术也学会了。推理时temperature设置过高或do_sampleTrue配合很小top_p导致不稳定。训练和推理的 chat template 不一致。处理时先检查数据再检查推理参数。可以尝试do_sampleFalse或temperature0.3做一次确定性生成用排除法定位问题。7.4 合并 LoRA 后没有变化现象是加载./qwen-4b-merged后生成的回答和基座模型完全相同。检查点确认训练后的 adapter 路径里存在adapter_config.json。确认PeftModel.from_pretrained加载的是训练输出目录而不是基座模型目录。确认target_modules匹配了实际层结构。如果没有匹配训练日志中可训练参数占比会极低。确认训练完成后确实保存了 checkpoint而不是只保存了基础模型。问题现象常见原因检查方式处理建议loss 不降数据格式错误、pad token 未设置、学习率过大打印单条 tokenize 结果修正数据格式降低学习率CUDA OOMbatch size 太大、序列太长查看 nvidia-smi 显存调小 batch开启 grad checkpoint生成乱码数据噪声、模板不一致检查训练样本和推理模板清洗数据统一 chat template合并后无变化adapter 没加载或 target_modules 不匹配检查 adapter_config.json重新确认训练输出目录8. 可复用清单与生产化方向8.1 第二次微调前检查清单这条清单可以作为所有类似模型微调前的固定动作避免从零开始踩坑。明确任务类型是通用对话指令遵循还是特定领域知识注入。选择训练方案全参、LoRA、QLoRA根据显存和数据量决定。确认模型路径基座模型能否正常加载和推理tokenizer 是否有 chat_template。检查数据字段instruction、input、output 是否有空值是否去重。切分验证集至少留 5% 到 10%且不与训练集重复。预估显存先用 batch size 1 冒烟测试。设置训练参数LoRA 学习率建议 1e-4 到 3e-4epoch 从 2 到 3 开始。启动训练并监控 loss同时记录第一次结果的基线指标。合并 adapter 并做外部验证不要只看训练集 loss。8.2 学习环境与生产环境的差异学习环境下数据量小、单卡运行、不追求训练速度只要能把流程跑通就可以。生产环境还需要更多保障维度学习实验环境生产微调环境显存单卡 16GB 起步多卡或更大显存留足余量数据几百到几千条经过清洗、去重、脱敏和版本管理训练恢复重新跑也不心疼必须支持断点续训和 checkpoint 管理日志监控print 日志即可接入训练指标监控、告警和审计模型部署本地推理验证需要服务化、量化、并发和回滚方案权限与安全本地文件模型仓库、权重加密、访问控制8.3 下一步扩展方向第二次尝试跑通之后可以沿着几个方向继续深入。如果目标是领域知识库微调可以把领域文档切成问答对再结合检索增强生成让模型在回答时引用外部知识。微调负责“学会输出格式和表达方式”检索负责“提供知识内容”两者并不冲突。如果数据量充足也可以从 LoRA 升级到全参微调但要准备好更大的显存和更长的训练时间。如果数据量有限还可以用模型蒸馏思路用更大规模模型生成高质量训练样本再微调 4B 模型。部署侧训练完的模型目录可以用 vLLM 做服务化或使用 Ollama 导入本地模型做离线测试。部署前必须重新评估推理延迟、显存占用和量化方式训练时用的 bf16 权重不一定适合直接放在生产环境里。第二次尝试的关键不在于用多复杂的训练技巧而是先缩小实验半径用 LoRA 跑通一条可复现的链路再根据外部验证结果逐步优化数据、参数和部署方式。把这一轮的结果、坑和检查清单记录下来下一次面对新的领域模型微调时就会快很多。