简介面向需要落地大模型微调项目的开发者这套资料围绕LLama3.1给出全参数微调、Lora微调与QLora微调的完整方案包含可直接运行的源码和分步流程教程。资源包为zip压缩格式共45个文件整体仅144KB主要包含24个Python脚本覆盖训练、预测、数据下载与依赖生成、13个Shell启动脚本内置不同微调策略的命令、5个JSON配置文件含DeepSpeed Zero2/Zero3分布式训练设置另有requirements.txt和README说明。教程从数据集准备、运行环境搭建、微调脚本执行到模型效果评估逐步讲解并附带Qwen、GLM4、InternLM2.5、Yi等主流模型的微调与预测示例便于横向对比不同参数高效微调方法。通过实际动手操作可深入理解全参微调的内存开销、Lora的低资源适配以及QLora在受限环境下的加速收敛思路。目前已有454人学习下载适合具备一定Python和深度学习基础、希望快速上手大模型微调的开发者。1. 从全参到QLoRALLama3.1微调的三条路线与显存真相一枚8B量级的LLama3.1微调项目九成团队第一反应都是全参微调这也是最容易翻车的决定。8B模型在全参模式下仅权重、梯度、AdamW优化器状态三份账就要96GB以上单张80G显卡都未必放得下换成QLoRA重跑同一份数据显存可能只要十几GB效果差距却远没有想象中大。这个标题里的“附项目源码流程教程”本质是把全参、LoRA、QLoRA三条路线整理成可直接改参数的脚本和文档。为什么值得投入时间因为大多数真实业务只需要对齐输出格式、修正领域语义并不需要重训一个基座而这三条路线从16GB单卡到多卡集群都有对应解。我会把显存账单、最小训练脚本、必须避开的坑、以及微调后如何验证逐条拆开。新手能跟着命令跑通熟手能直接对照参数边界。2. 先算显存账再选路线全参、LoRA、QLoRA的原理与边界2.1 全参微调到底在“全”什么“全参”意味着模型每一层线性层的权重和偏置都会进入反向传播并更新。训练一条样本时显存里有四类东西在同时占空间权重本身、反向传播产生的梯度、优化器状态、以及前向计算留下的激活值。前三个是固定开销最后一个随序列长度和batch size暴涨。以LLama3.1常见的8B量级模型为例bf16精度下权重占2字节/参数梯度也是2字节/参数而AdamW优化器要维护一阶矩和二阶矩两个fp32副本共8字节/参数。三者加起来就是12字节×80亿96GB这还没算激活值。显存组成部分单参数字节数8B量级估算模型权重bf162字节约16GB梯度bf162字节约16GBAdamW优化器状态fp32×28字节约64GB权重梯度优化器合计12字节约96GB激活值随batch和seq长度变化数GB到数十GB这就是为什么全参微调几乎总是跑在多卡上还要配合ZeRO把优化器状态切分到多张卡。项目源码里有多少个batch_size都是浮云显存账在上一步已经锁死了上限。我一般这样判断是否选全参数据量在十万条以上且任务需要模型学到全新的知识结构而不是单纯改变回答格式。全参微调的收敛上限确实最高但跑不动就毫无意义。2.2 LoRA的低秩假设LoRA的核心不是“少训一点”而是假设微调过程只会改动预训练权重的极少数方向。做法是把原基座权重冻结在旁边挂一组低秩矩阵A和B前向变成Wx BAx。训练时只有A、B在更新基座原权重全程不动。可训练参数量的公式大约是2 × 隐藏维度 × r × 模块数。在8B量级模型上r16时通常对应几千万到一亿个可训练参数只占全模型参数的0.1%~1%。这也是LoRA能被广泛接受的根本原因预训练模型已经有很强的通用能力微调只需要在很小的子空间里校正方向。显存账也乐观得多基座权重和梯度仍然按bf16占用但优化器状态只对LoRA部分生效。一个8B模型用LoRA训练总显存需求大致在20~28GB单张24G显卡能跑这和全参动辄上百GB完全不是一个量级。LoRA最适合的场景是数据量不大任务和基座能力接近主目标是约束输出格式和领域术语。它的缺点也很明确更新空间受限如果任务和预训练分布差异太大效果会明显低于全参。2.3 QLoRA的三个关键让步QLoRA是在LoRA基础上的进一步压缩主要是三件事基座权重降成4bit的NF4类型把量化常数再做一次量化以及引入分页优化器把不常访问的优化器状态临时挪到CPU。基座权重从2字节/参数压到0.5字节/参数8B基座大约只需要4GB。微调路线8B量级典型显存可训练参数适用显卡全参微调110GB以上80亿多卡集群80G×4以上LoRA20~28GB约0.1%~1%单张24G可跑QLoRA10~16GB约0.1%~1%单张16G/24G均可QLoRA让单卡训练大模型从“不可能”变成了“日常操作”但它也有代价量化引入的信息损失在极端任务上会让模型学不到足够精细的语义且训练速度通常比LoRA慢一些。我的习惯是QLoRA作为默认路线先跑通数据流程和效果验证如果发现效果明显不够再考虑LoRA或全参升级。这个顺序能在资源有限时保住底线。3. 全参微调LLama3.1环境搭建与最小训练脚本3.1 先把环境固定住LLama3.1微调的依赖安装全参微调依赖Transformers、accelerate、DeepSpeed、datasets这几个核心库。拿到标题里这类项目zip包第一件事不是跑train而是解压后检查有没有requirements文件没有就直接按下面的方式装齐。conda create -n llama_full python3.11 -y conda activate llama_full pip install transformers accelerate deepspeed datasets装完先跑一句python检查版本和CUDA是否匹配。Transformers和accelerate、DeepSpeed三者版本不匹配最典型的表现是报找不到某个类或者启动时提示deepspeed config字段未知。我习惯先全部升级到当前稳定版再各自锁版本而不是让pip自己猜。python -c import torch, transformers, deepspeed; print(torch.cuda.is_available(), transformers.__version__, deepspeed.__version__)只有torch.cuda.is_available()为True后面的显存账才成立。这块最容易翻车的是装了CPU版torch训练启动才发现CUDA不可用浪费半天。3.2 最小训练脚本从数据到Trainer全参微调的核心训练脚本用Transformers的Trainer封装最省事。DeepSpeed的接入通过training_args里的deepspeed字段指向配置文件不需要手写分布式逻辑。import torch from transformers import ( AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments, DataCollatorForSeq2Seq, ) model AutoModelForCausalLM.from_pretrained( 你的本地模型路径, torch_dtypetorch.bfloat16, device_mapauto, ) tokenizer AutoTokenizer.from_pretrained(你的本地模型路径) tokenizer.pad_token tokenizer.eos_token # 基座模型通常没有pad token training_args TrainingArguments( output_dircheckpoints/full, per_device_train_batch_size1, gradient_accumulation_steps16, learning_rate2e-5, num_train_epochs3, bf16True, gradient_checkpointingTrue, logging_steps10, save_strategysteps, save_steps500, deepspeedds_config.json, ) trainer Trainer( modelmodel, argstraining_args, train_datasetdataset, data_collatorDataCollatorForSeq2Seq(tokenizer), ) trainer.train()这里有几个参数值得展开说。per_device_train_batch_size1gradient_accumulation_steps16等效batch size是16多卡场景下这个值要跟着卡数调整避免有效batch过大导致收敛异常。bf16True要求显卡支持bf16计算不支持就退到fp16但fp16在8B量级模型上容易出现loss溢出排除问题更费劲。gradient_checkpointingTrue是用约30%的训练速度换显存减半全参微调必须开。数据侧要提前把训练集处理成文本。常见做法是给每条样本拼一个带“指令、输入、输出”三段标签的模板然后tokenize。from datasets import Dataset import json def encode(example, max_len2048): text ( f### 指令\n{example[instruction]}\n f### 输入\n{example[input]}\n f### 输出\n{example[output]} ) return tokenizer(text, truncationTrue, max_lengthmax_len, return_tensorspt) dataset Dataset.from_list(json.load(open(train.json))) dataset dataset.map(encode)注意标签格式要统一推理时也要用同样的模板拼prompt不能训练一种格式、推理另一种格式。这个看似不起眼的点是后面“微调后变笨”最常见的元凶。3.3 DeepSpeed配置多卡全参的启动命令全参微调在多卡环境下绕不开ZeRO。8B模型用ZeRO Stage 2就够了Stage 2会把优化器状态和梯度切到各卡Stage 3更进一步把模型权重也切分通常给更大模型用。{ train_batch_size: auto, gradient_accumulation_steps: auto, zero_optimization: { stage: 2, offload_optimizer: { device: cpu } }, bf16: { enabled: true } }offload_optimizer把优化器状态换到CPU内存能显著降低每卡显存占用但训练会变慢。如果显存够用建议先不开offload跑得慢有时候比爆显存更折磨人。deepspeed --num_gpus4 train_full.py --deepspeed ds_config.json启动之后重点观察每张卡的显存峰值。不要只看总显存要看单卡峰值是否接近上限。全参微调跑通后checkpoint可以直接用AutoModelForCausalLM加载也可以继续在此基础上挂LoRA做后续实验。4. LoRA与QLoRA微调LLama3.1配置、训练与显存账4.1 LoRA冻结基座只训练低秩旁路LoRA落地依赖PEFT库。流程是先加载原始bf16基座再包一层LoraConfig最后用get_peft_model把可训练参数锁定到LoRA上。from peft import LoraConfig, get_peft_model lora_config LoraConfig( r16, lora_alpha32, target_modules[q_proj, k_proj, v_proj, o_proj], lora_dropout0.05, biasnone, ) model get_peft_model(model, lora_config) model.print_trainable_parameters()r是低秩矩阵的秩越小可训练参数越少、拟合能力越弱lora_alpha是缩放因子实际生效的缩放比例是alpha/r这里32/162。lora_dropout0.05是常规值如果训练数据噪声较大可以调到0.1。target_modules必须跟模型实际模块名完全一致LLama3.1的注意力层常见模块名就是q_proj、k_proj、v_proj、o_proj如果名字对不上PEFT不会报错但trainable_parameters数量会明显不对那说明LoRA根本没挂上目标层。trainable_parameters打印出来后对比总参数量确认占比在0.1%~1%之间才算正常。4.2 QLoRA4bit基座加载与分页优化器QLoRA和LoRA的差别全在基座加载这一步。需要先用BitsAndBytesConfig构造4bit量化参数再传给from_pretrained。量化好之后还要调用prepare_model_for_kbit_training这一步会冻结基座并处理好反向传播时的dtype问题漏掉它会在第一轮训练直接炸显存或报梯度错误。from transformers import BitsAndBytesConfig from peft import prepare_model_for_kbit_training bnb_config BitsAndBytesConfig( load_in_4bitTrue, bnb_4bit_quant_typenf4, bnb_4bit_compute_dtypetorch.bfloat16, bnb_4bit_use_double_quantTrue, ) model AutoModelForCausalLM.from_pretrained( 你的本地模型路径, quantization_configbnb_config, device_mapauto, ) model prepare_model_for_kbit_training(model) lora_config LoraConfig( r16, lora_alpha32, target_modules[q_proj, k_proj, v_proj, o_proj], lora_dropout0.05, ) model get_peft_model(model, lora_config)nf4是QLoRA论文里验证过的4bit数据类型比int4保留更多信息compute_dtype用bf16表示反量化后计算仍然走bf16精度损失主要发生在存权重时。双重量化打开后基座体积还能再小一点。QLoRA训练时建议把优化器换成paged_adamw_8bit这是bitsandbytes提供的分页优化器显存吃紧时会把部分状态挪到CPU临时续命很好用。training_args TrainingArguments( output_dircheckpoints/qlora, per_device_train_batch_size2, gradient_accumulation_steps8, learning_rate2e-4, num_train_epochs3, bf16True, gradient_checkpointingTrue, logging_steps10, save_strategysteps, save_steps500, optimizerpaged_adamw_8bit, )LoRA和QLoRA的学习率比全参高一个量级常见范围是1e-4到2e-4。如果loss下降太猛先降一半再看如果完全不降优先检查数据模板而不是调学习率。4.3 数据模板统一用apply_chat_templateLLama3.1这类对话模型的训练数据最稳的做法是用tokenizer自带的chat template来拼文本。自己手拼special token很容易漏漏一个bos或角色标签模型在推理时就会把标签当正文输出。def format_chat(example): messages [ {role: system, content: example[system]}, {role: user, content: example[instruction]}, {role: assistant, content: example[output]}, ] return tokenizer.apply_chat_template(messages, tokenizeFalse) dataset dataset.map(lambda x: {text: format_chat(x)})训练时用这个text列做tokenize推理时同样用apply_chat_template构造输入。这样能保证两边看到的是完全一致的token序列不会出现格式错乱问题。提示如果pre-trained模型的tokenizer没有chat_template属性需要用一段带角色标签的模板手动指定而不是直接拼接纯文本。5. 避坑OOM、lossnan与微调后变笨的真实原因5.1 显存OOM你的显存账漏了激活值现象按12字节公式算好96GB配了4张80G显卡结果一启动训练就OOM甚至batch_size降到1还是崩。原因只算了权重、梯度、优化器状态没算激活值LLama3.1在seq_len4096时激活值峰值可能再吃数十GB。解决先开gradient_checkpointing再用CUDA_LAUNCH_BLOCKING1启动让报错精确到层然后观察nvidia-smi峰值。CUDA_LAUNCH_BLOCKING1 deepspeed --num_gpus4 train_full.py --deepspeed ds_config.json这个环境变量让GPU报错立即同步返回而不是异步卡死在某个晦涩的cuda error里。如果还爆就把max_seq_len从4096降到2048这是全参微调里最管用的两个杠杆。5.2 lossnan先降学习率再查数据现象前几步loss还在2.x第20步突然变成nan之后一直nan。原因最常见是learning_rate过大其次是bf16/fp16精度处理不当还有一个隐藏原因是数据里存在极长样本tokenizer截断后target部分为空。解决先把learning_rate降一个量级比如全参从2e-5降到1e-5QLoRA从2e-4降到1e-4然后过滤空输出样本。def filter_empty(example): enc tokenizer(example[output], add_special_tokensFalse) return len(enc[input_ids]) 0 dataset dataset.filter(filter_empty)如果使用了fp16且显卡支持bf16直接切回bf16更省心。nan之后不要硬跑加载最近一个save checkpoint降lr重启。5.3 微调后变笨训练集loss正常但输出格式错乱现象训练loss稳步下降推理时模型却回答“好的我了解了”或者把角色标签原样输出。原因训练数据用了纯文本拼接推理用了apply_chat_template两边的special token不一致模型学到的输出分布和推理输入完全对不上。解决训练侧和推理侧统一走apply_chat_templatesystem/user/assistant结构保持一致。这个坑在项目实战里出现频率极高尤其当教程把“数据预处理”和“推理”分成两个独立脚本时。两边各写各的最容易出问题。5.4 验证集loss比训练集高很多数据切分有问题现象训练集3轮后loss到1.2验证集一直停在1.8不降。原因最常见是数据切分没按语义去重同一条指令的不同回答同时出现在训练集和验证集模型在背答案而不是学泛化也可能是验证集和训练集领域分布不同。解决按指令文本做hash去重确保验证集里的instruction没有在训练集出现过。train_instructions set(ds_train[instruction]) ds_val ds_val.filter(lambda x: x[instruction] not in train_instructions)验证集要的是“没见过的任务”上的表现而不是记忆力的测试。如果数据量太少可以按时间或来源领域切分保证分布有差异。5.5 QLoRA合并后模型还是4bit现象用merge_and_unload合并LoRA后保存的模型文件很小加载后回答质量明显不对。原因QLoRA的基座本身就是4bit量化权重合并结果默认仍然是nf4格式直接把adapter合并进量化基座等于把LoRA信号加在了已经损失精度的权重上。解决不追求单文件合版推理时用原始bf16基座adapter加载如果一定要合并先把基座转回bf16再做merge。base AutoModelForCausalLM.from_pretrained( 你的本地模型路径, torch_dtypetorch.bfloat16, device_mapauto, ) model PeftModel.from_pretrained(base, ./checkpoints/qlora/step-1500)这个方式的推理质量和训练时最接近。想交付单文件模型再单独跑一遍反量化合并脚本不要直接拿量化基座merge。6. 验证微调效果训练loss只是热身固定提示词才是尺子6.1 用固定提示词做回归测试训练完先别急着调参准备一组固定提示词覆盖通用任务和垂直任务分别跑微调前后的输出。关键在于do_sampleFalse和固定seed保证结果可复现。from peft import PeftModel base AutoModelForCausalLM.from_pretrained( 你的本地模型路径, torch_dtypetorch.bfloat16, device_mapauto, ) model PeftModel.from_pretrained(base, ./checkpoints/lora/step-1500) questions [ 用一句话说明什么是过拟合。, 把这句话翻译成英文这家店的招牌菜是酸菜鱼。, 列出三种常见的激活函数并说明用途。, ] for q in questions: inputs tokenizer.apply_chat_template( [{role: user, content: q}], return_tensorspt, ).to(cuda) out model.generate(inputs, max_new_tokens256, do_sampleFalse) print(tokenizer.decode(out[0][inputs.shape[1]:], skip_special_tokensTrue))对比时既看内容正确性也看格式是否稳定。通用任务集负责检查模型有没有“变笨”垂直任务集负责确认业务目标是否达成。这个回归集合一旦固定下来就不要再频繁改动否则没法横向比较。6.2 用困惑度做定量对照困惑度能给出一个客观数字用于同一个模型在微调前后、以及不同超参版本之间的比较。import math def compute_ppl(model, tokenizer, texts, max_len512): total_nll 0.0 total_tokens 0 for text in texts: enc tokenizer(text, truncationTrue, max_lengthmax_len, return_tensorspt) with torch.no_grad(): out model(**enc, labelsenc[input_ids]) total_nll out.loss.item() * enc[input_ids].numel() total_tokens enc[input_ids].numel() return math.exp(total_nll / total_tokens)困惑度下降说明模型对验证文本的拟合变好了但它不代表输出格式正确。早期我做微调只盯loss训到怀疑人生也没发现模型输出格式烂后来学乖先立回归提示词集合每次训练结束先跑一遍再谈优化超参。这个习惯帮我避开了一半以上的无效调参。希望帮到你。本文还有配套的精品资源点击获取