简介面向希望掌握大模型微调实战的开发者提供基于LLama3.1的全参微调、LoRA微调与QLoRA微调完整项目源码及流程教程。资源覆盖数据准备、环境配置、微调脚本编写、模型评估与优化等关键环节适合具备一定Python与深度学习基础、正在开展大模型应用落地的学习者参考。压缩包共45个文件以Python脚本24个、Shell脚本13个为主辅以JSON配置、README说明与依赖清单包含多种微调与推理预测脚本并配有Zero2/Zero3等分布式训练配置可按需选择不同规模模型的微调方案。已有454人学习下载。通过源码与教程配合读者可深入理解不同微调方式的计算开销、过拟合控制与收敛策略并结合实际任务调整参数、评估效果从而更高效地将LLama3.1等预训练模型应用到具体业务场景中。1. 大模型微调三选一从 Llama-3.1 的全参、LoRA 到 QLoRA 该怎么下手如果你手里刚好有一批垂直领域数据想让 Llama-3.1、Qwen 这类开源大模型在具体任务上真正好用那你大概率绕不开全参微调、LoRA 和 QLoRA 这三条路。很多人以为三者是一个比另一个高级其实它们只是在显存、数据量和效果之间做三种不同取舍。这个项目正好把这三条路都拆成了能直接跑的源码全参微调脚本、LoRA 脚本、QLoRA 脚本加上 Zero2/Zero3 的 DeepSpeed 配置以及覆盖 Qwen、GLM-4、InternLM2.5、Yi-1.5 多个基座的 finetune 和 predict 脚本。适合没完整跑过微调的人照着复现也适合熟手对比不同微调策略的实际差距。下面按「先理解底层开销再动手调参」的顺序展开。2. 全参微调与 DeepSpeed 配置先把 8B 模型的显存账算清楚2.1 全参微调为什么值得先看显存构成与过拟合边界全参微调的概念不复杂预训练权重全部参与梯度更新。它的上限最高因为模型所有表达能力都能被任务数据重新塑形但代价也很直接——显存开销不是按模型体积线性涨而是按「参数 梯度 优化器状态 激活值」四块叠加。以一个 8B 参数模型为例常用 AdamW 优化器时参数本身 fp16 约 16GB梯度再 16GB优化器状态还要维护 fp32 主权重、一阶动量 m 和二阶动量 v摊下来接近每参数 16 字节。也就是说仅权重、梯度和优化器状态三项就要 8B × 12~16 字节约 96~128GB这还不是全部。激活值在长序列下再翻一份单卡 24GB 根本装不下这也是全参微调必须上 DeepSpeed 的根本原因。很多人在这里有个误区以为全参微调只是「显存需求大」这一个缺点。实际上它还有一个更容易翻车的面——数据量不够时过拟合非常快。全参把模型的每一层都往任务上压数据若只有几千条模型会把训练集的格式特征当成通用规则损失函数看着降得很好换一批新样本生成质量立刻打回原形。所以全参微调的适用场景通常是有较干净的万级指令数据、且你希望模型整体能力向任务方向偏移而不是只学一个输出模板。2.2 ZeRO2 与 ZeRO3两个配置文件的取舍项目里给了三个 DeepSpeed 配置分别是 ds_config_zero2.json、ds_config_zero3.json、ds_config_zero3_72B.json正好对应两种典型场景。ZeRO-2 的核心是把优化器状态和梯度做分片每张卡只维护自己那份参数仍是全局复制。对 8B 模型来说用 ZeRO-2 跑多卡全参微调是性价比最高的组合通信开销小、训练速度快显存压力由多卡分担。ZeRO-3 则更进一步连参数本身都分片每张卡只在用到某层时才把它物化出来这意味着单卡也能勉强跑 8B 全参代价是频繁的 all-gather 通信显存换速度跑起来明显变慢。从项目文件看finetune_fullparameter.sh 指向的就是这一类全参训练入口配合 finetune_llama3.py 或 finetune_qwen1.5.py 这类训练脚本使用。常见的 ZeRO-3 配置文件骨架大致如下项目里那份也是同一个套路只是 stage 和 offload 字段按需调整{ zero_optimization: { stage: 3, offload_optimizer: { device: cpu, pin_memory: true }, offload_param: { device: cpu, pin_memory: true }, overlap_comm: true, contiguous_gradients: true, reduce_bucket_size: 500000000, stage3_prefetch_bucket_size: 50000000, stage3_param_persistence_threshold: 1000000 }, train_batch_size: auto, train_micro_batch_size_per_gpu: auto, gradient_accumulation_steps: auto }参数说明stage 决定分片级别80G 显存以上的多机多卡可以锁 stage 2 换取吞吐offload_optimizer 和 offload_param 都指到 cpu 后显存占用显著下降但 CPU 内存和 PCIe 带宽会成为新瓶颈训练速度可能掉一半以上。overlap_comm 打开后通信与计算部分重叠对 ZeRO-3 尤其重要。batch 相关字段保持 auto由 Trainer 从训练参数里自动推算避免两边手改不一致。我一般会这样选单卡跑 7B/8B 全参、且能接受慢速训练时用 ZeRO-3有多卡、节奏要求高时用 ZeRO-2那个 ds_config_zero3_72B.json 我把它当作「72B 级别参数分片 极致 offload」的模板用而不是真的指望单卡跑 72B。2.3 什么情况下该放弃全参微调全参不是万金油项目里同时给出 LoRA 和 QLoRA 的原因也在这里。以我的实践经验判断标准不是「哪个效果好」而是「你的资源边界和数据边界在哪」。下面这张表是三种方式在同一任务上的典型差异数值是常见经验值具体以你的显卡和批次为准微调方式显存需求8B 模型训练速度对数据量的要求过拟合倾向全参微调多卡或 80G 单卡最快万级起步数据不足时明显LoRA约 18~24GB较快数千条可启动较低QLoRA 4bit约 12GB 以内较慢数千条可启动最低判断是否放弃全参的标准我建议看两个信号一是单卡显存低于 80G、又拿不到集群资源时全参复现周期会拖到不可接受二是训练 loss 降下去了但测试集生成明显背诵训练集格式这时全参的容量已经超出了任务复杂度换 LoRA 反而更稳。项目实战里 LoRA 和 QLoRA 是多数人的落点下一章重点拆这两种方式的参数套路。3. LoRA 与 QLoRA在有限显存里逼近全参效果的参数套路3.1 LoRA 低秩分解为什么只更新两个小矩阵LoRA 的直觉在于一个前提假设预训练模型本身的权重已经足够强微调其实只需要在原始权重旁边叠一个「小规模的增量」。这个增量不直接加到每层权重里而是拆成低秩矩阵 A 和 B 的乘积前向时输出从 h W0x 变成 h W0x BAx。初始时 B 为零矩阵整个增量不起作用模型保持原输出训练过程中 A 和 B 学习出适合当前任务的偏移。r 控制这个增量的秩r 越大表达空间越大所需显存也越高lora_alpha 是缩放系数决定增量在输出中的影响力习惯上取 lora_alpha 为 r 的 2~4 倍。项目里多个 finetune 脚本都走同一套 PEFT 流程核心片段长这样from peft import LoraConfig, get_peft_model from transformers import AutoModelForCausalLM base_model AutoModelForCausalLM.from_pretrained( /path/to/Llama-3.1-8B-Instruct, torch_dtypeauto, device_mapauto ) lora_config LoraConfig( r8, lora_alpha32, target_modules[q_proj, v_proj, k_proj, o_proj], lora_dropout0.05, biasnone, task_typeCAUSAL_LM ) model get_peft_model(base_model, lora_config) model.print_trainable_parameters()逻辑说明先加载完整基座再通过 LoraConfig 声明要注入低秩矩阵的模块get_peft_model 把原模型冻结并替换目标模块的前向路径。print_trainable_parameters 会把可训练参数量打印出来8B 模型 r8、只注入 attention 四件套时可训练参数通常只占全部参数的 0.5%~1% 左右。这里最常见的调参点是 target_modules——它必须和基座的内部模块命名完全一致。不同系列模型的注意力模块命名差异很大这也是很多人第一次加载 adapter 后模型乱答的直接原因基座模型注意力模块常见命名LoRA 常见 target_modulesLlama-3 / 3.1、Yi-1.5q_proj, k_proj, v_proj, o_proj上述四个Qwen、Qwen1.5、Qwen2q_proj, k_proj, v_proj, o_proj上述四个也可加 gate_proj/up_proj/down_projGLM-4-9Bquery_key_value, densequery_key_value, dense3.2 从 shell 脚本看 LoRA 超参学习率与 target_modules项目里的 script 目录是很好的「参数手册」fintune_lora_llama3.1_8B_Instruct.sh 和 finetune_lora_qwen2.sh 之间的差异暴露了不同基座在超参上的真实偏好。拿一份典型的 LoRA 启动脚本来看结构如下torchrun --nproc_per_node1 finetune_llama3.py \ --model_name_or_path /path/to/Llama-3.1-8B-Instruct \ --data_path data/Belle_sampled_qwen.json \ --output_dir output/llama3.1_lora \ --num_train_epochs 3 \ --per_device_train_batch_size 2 \ --gradient_accumulation_steps 8 \ --learning_rate 2e-4 \ --lr_scheduler_type cosine \ --warmup_ratio 0.03 \ --lora_r 8 \ --lora_alpha 32逻辑说明torchrun 是 PyTorch 官方多进程启动器nproc_per_node1 表示单卡per_device_train_batch_size 是单卡单步真实样本数gradient_accumulation_steps 把多个微批累积后再更新两者相乘才是等效 batch size上面就是 2 × 8 16。LoRA 微调最值得记住的一个血泪经验是学习率不要沿用全参的 1e-5LoRA 增量矩阵需要更大的步长2e-4 附近是常见起点。lr_scheduler_type 用 cosine 并配一个小的 warmup_ratio训练前期更稳避免增量矩阵在初期被大步长冲偏。如果你打算喂更多数据一般先加 gradient_accumulation_steps 而不是直接加 per_device_train_batch_size这样显存曲线更平缓。多卡场景把 nproc_per_node 改成卡数但要注意不同模型脚本里 target_modules 未必一样照抄脚本前先确认基座模块名这一点在预测脚本里体现得更明显。3.3 QLoRA 的 4bit/8bit 底座量化基座 LoRA 增量QLoRA 不是 LoRA 之外的新方法它是在 LoRA 前面加了一步把基座模型权重量化到 4bit 或 8bit再在量化后的底座上训练 LoRA 增量。项目里 predict 目录同时出现了 llama3_qlora_4bit_predict.py、qwen_qlora_4bit_predict.py以及 qwen_qlora_8bit_predict.py说明这一套代码把 4bit 和 8bit 两条路径都覆盖了。4bit 路径的关键代码是用 BitsAndBytesConfig 控制加载方式from transformers import BitsAndBytesConfig bnb_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_quant_typenf4, bnb_4bit_use_double_quantTrue, bnb_4bit_compute_dtypetorch.float16 ) base_model AutoModelForCausalLM.from_pretrained( model_path, quantization_configbnb_config, device_mapauto )参数说明load_in_4bitTrue 让基座以 4bit 常驻显存这是显存骤降的主要原因quant_type 选 nf4 而非 fp4nf4 是 QLoRA 论文里验证更适合激活值分布的量化方式use_double_quant 开启双重量化把量化缩放系数再量化一次省下的显存虽小但积少成多compute_dtype 保持 fp16 则是因为 4bit 权重在反向传播时要临时反量化成高精度计算太低会损害梯度质量。QLoRA 的梯度只流经 LoRA 增量矩阵基座权重虽然被量化成 4bit但前向时会被反量化回 fp16 参与计算只参与推理不更新。项目中 QLoRA 脚本一般会把优化器换成 paged_adamw_32bit这是基于分页内存的优化器变体显存不足时能把优化器状态换出到 CPU是 QLoRA 能在 12GB 卡上跑 8B 模型的最后一块拼图。选 8bit 还是 4bit我的习惯是单卡 24GB 用 4bit 最稳追求生成质量且显存仍有富余时上 8bit推理阶段同样要保持量化方式一致不然加载 adapter 时权重分布对不上输出会莫名其妙。4. 跑通一条完整链路数据、训练入口与断点续训的落地细节4.1 指令数据格式与 prompt 模板拼接项目 data 目录下有 Belle_sampled_qwen.json 和西西嘛呦.json 两个文件这俩都属于中文指令微调常见格式。前者的字段结构与 Alpaca 风格接近最后喂进模型时需要把 instruction、input、output 拼成一个完整文本而且这个「拼法」直接决定模型学的应答格式。微调这事的玄学成分没想象中高大部分翻车都发生在模板不一致上。典型的数据样例如下{ instruction: 请把下面的句子翻译成英文, input: 今天天气很好, output: The weather is nice today. }数据预处理时常见做法是实现一个统一的格式化函数把所有样本都转成「指令 输入 应答」的结构def format_example(ex): if ex.get(input) and ex[input].strip(): prompt ### Instruction:\n{instruction}\n### Input:\n{input}\n### Response:\n.format( instructionex[instruction], inputex[input] ) else: prompt ### Instruction:\n{instruction}\n### Response:\n.format( instructionex[instruction] ) return prompt ex[output]这里最关键的一步是把 prompt 部分在 labels 里掩盖掉。模型只需要对 output 部分计算损失instruction 和 input 只是条件输入。如果 labels 不掩盖 promptloss 就会把「复述题目」也算作任务训练 loss 降得很好生成却全是乱码。项目多个 finetune 脚本里都会有一段 tokenize 逻辑做这件事差别只在 prompt 模板字符串不同换模型时必须连模板一起换。4.2 训练入口torchrun 与 DeepSpeed 的搭配方式项目的 script 目录提供了很多启动脚本它们的共同特征是通过命令行参数把模型路径、数据路径、输出路径一次传入不硬编码。常见组合是 torchrun 启动训练脚本再配合 --deepspeed 指向配置torchrun --nproc_per_node2 finetune_llama3.py \ --model_name_or_path /path/to/Llama-3.1-8B-Instruct \ --data_path data/西西嘛呦.json \ --deepspeed config/ds_config_zero2.json \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 16 \ --output_dir output/llama3.1_full逻辑说明nproc_per_node2 表示单机两卡并行每卡处理 batch_size1那么等效 batch size 是 1 × 2 卡 × 16 累积 32。把 batch 做大对收敛稳定有好处代价是优化器更新频率降低。项目里多个 shell 脚本的差异主要在模型路径、数据路径和 LoRA 相关参数训练脚本本身是同一套 Trainer 流程。环境变量方面多机时还需要设置 MASTER_ADDR 和 MASTER_PORT单机多卡 torchrun 会自动生成不需要额外处理。72B 级别配置那个脚本则通常配 ZeRO-3理由很简单参数总量太大ZeRO-2 只分片优化器和梯度显存依然不够。4.3 输出目录、adapter 保存与断点续训训练结束后output_dir 下生成的目录结构是个容易被忽略的重点。LoRA/QLoRA 训练使用 PEFT 的 save_pretrained 后只会保存一个很小的 adapter 目录里面是 adapter_config.json 和 adapter_model.safetensors不包含完整基座权重。这个特性意味着你可以放心把多个任务的 adapter 各存一份磁盘开销几乎可忽略。断点续训上Trainer 默认会在 output_dir 下生成 checkpoint-xxx 目录里面包含 optimizer 状态、调度器状态和模型权重中途中断后可以这样恢复trainer.train(resume_from_checkpointoutput/llama3.1_lora/checkpoint-200)我一般会同时设置 save_total_limit2只保留最近两个 checkpoint避免训练几天后磁盘被 checkpoint 撑爆。加载时切记PEFT adapter 的 adapter_config.json 里写死了训练时的 base_model_name_or_path换机器跑预测前一定要核对这个路径它指向的基座不存在时加载过程会报错或尝试从头下载很多「模型乱答」的现场其实是从这里开始的。5. 微调避坑与常见问题五个让我反复排查的经验点5.1 现象loss 快速下降到 0.2 但生成结果是乱码这几乎是 LoRA 微调最经典的一个坑。原因基本出在模板拼接和 label 掩盖上如果把 instruction 和 input 也放进 labels 计算损失模型只需要学会「背题」就能把 loss 压到很低根本没去学应答。解决方法是先打印一条 tokenized 样本人工确认 labels 中 prompt 部分是否为 -100再跑一次生成如果输出里出现 Prompt 原文重复基本就是模板没有正确掩码。项目里各 finetune 脚本的 preprocess 函数就是做这件事的换数据集时不要轻易删减。5.2 现象DeepSpeed stage3 下直接 OOM全参微调开启 ZeRO-3 后仍然爆显存通常是两个原因一是没开 gradient_checkpointing激活值占掉大头二是 offload 配置不完整只 offload 了优化器参数仍然留在显存。解决在 TrainingArguments 中开启 gradient_checkpointingTrue并确认 ds_config_zero3.json 里 offload_optimizer 和 offload_param 都指向 cpu如果还爆把 per_device_train_batch_size 降到 1改用 gradient_accumulation_steps 补 batch。注意 gradient checkpointing 会牺牲约 20%~30% 训练速度这是用时间换显存。5.3 现象加载 adapter 后模型回答与目标任务完全无关adapter_config.json 里的 base_model_name_or_path 指向的是训练机上的绝对路径换机器或换路径加载时PEFT 可能尝试去原路径找基座找不到就用当前加载的模型导致结构不一致。另一个常见原因是 target_modules 与基座模块名不匹配比如对 GLM-4 使用 q_proj/v_proj 而非 query_key_value。解决加载时显式指定正确的基座路径并在加载后打印 model.peft_config 核对 target_modules不同系列模型间的脚本不要互抄。5.4 现象多卡训练时显存不均衡或进程卡死torchrun 多卡启动后如果各卡 batch size 一致但显存占用仍不平衡多半是 dataset sampler 触发了数据长度扎堆长样本全分到同一张卡上。另一个隐藏点是没有统一设置 gradient_accumulation_steps脚本间不一致会导致各 rank 的更新节奏错位。解决多卡环境只从一份启动脚本拉起全部进程保证参数完全一致实在不行关闭 dataset 中的按长度排序逻辑让样本随机分布到各 rank。5.5 现象LoRA 训练却把整个基座 checkpoint 写进磁盘训练结束后发现 output_dir 里躺着一份好几个 GB 的完整权重说明保存时没有走 PEFT 的 save_pretrainedTrainer 默认的 save_model 会把模型全量写进去。解决训练脚本里显式调用 model.save_pretrained(output_dir) 只保存 adapter并设置 save_only_modelTrue新版 transformers 支持再配 save_total_limit2 控制数量。这样每个任务只占几百 MB换模型、换任务、回滚实验都方便得多。6. 预测脚本与效果验证把 adapter 用对才算微调完成6.1 预测脚本的加载方式项目 predict 目录下脚本命名很规律llama3.1_peft_lora_predict.py、llama3_peft_qlora_predict.py 这类文件核心都在做同一件事加载基座、挂 adapter、统一生成参数。QLoRA 的预测脚本和训练脚本必须保持量化方式一致否则 4bit 量化后的权重分布和 adapter 期望的输入对不上生成质量会明显劣化。推荐用 AutoModelForCausalLM 与 PeftModel 组合加载from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer tokenizer AutoTokenizer.from_pretrained(base_model_path, trust_remote_codeTrue) base_model AutoModelForCausalLM.from_pretrained( base_model_path, device_mapauto, torch_dtypeauto ) model PeftModel.from_pretrained(base_model, adapter_path) inputs tokenizer(### Instruction:\n请写一段产品介绍\n### Response:\n, return_tensorspt) outputs model.generate(**inputs, max_new_tokens256, do_sampleFalse) print(tokenizer.decode(outputs[0], skip_special_tokensTrue))注意生成时 do_sampleFalse 保证结果可复现验证多样性时再调 temperature0.7 跑多组。加载后第一件事是打印 model.peft_config 与模型的 dtype确认没有混合精度错位。6.2 三个验证习惯第一个习惯固定同一批 prompt在微调前后各生成一次肉眼对比格式和内容变化。第二个习惯从测试集抽 30~50 条样本算格式正确率和关键信息命中率而不是只看 loss。第三个习惯分别用 do_sampleFalse 和 temperature0.7 跑同一批样本观察稳定性和多样性是否都在可接受范围。这样能快速暴露「过拟合到训练集格式」和「adapter 没生效」两类问题。6.3 一次路径没对齐的教训我第一次跑 Llama-3.1 的 LoRA 微调时训练和预测分开在两台机器上进行adapter 加载后生成内容完全不可用。排查半天才发现 adapter_config.json 里的 base_model_name_or_path 还指向训练机的旧路径PeftModel 加载时被带偏没有用我当前指定的基座。从那以后我每次换机器、换数据、换 adapter都强制先跑一遍「加载并生成一句固定 prompt」的冒烟脚本把模型路径、adapter 路径、tokenizer 路径三个值逐一打印确认再进入正式预测。这套流程后来帮我省掉了大量无效排查时间希望帮到你。本文还有配套的精品资源点击获取