
简介面向AI研发提效与LoRA微调实践人群的资源包围绕LlamaAlpaca LoRA与ChatGLMChatGLM Tuning两条主流微调路线覆盖用户故事生成、测试代码生成、代码辅助生成、文本转SQL、文本生成代码五类典型研发任务既演示参数高效微调思路也提供从数据准备、脚本调试到结果查看的完整学习路径适合有深度学习基础、想通过低秩适配技术轻量微调大模型的开发者和研究者。资源包充分体现低秩适配与P-Tuning的差异提供开箱即用的Alpaca LoRA训练Notebook与ChatGLM Tuning脚本并配以JSONL格式训练数据、Python预处理工具和结果演示便于对比两种微调方案的实现差异在较小显存占用下实现针对性能力增强整体适合在消费级GPU上尝试。全包共93个文件约53.46MB核心包括18个ipynb训练调优Notebook、20个jsonl标注数据集、8个py工具脚本、11个md说明文档、10个pdf参考材料并附图片与配置示例目录按数据、代码、文档分层组织既有原始与合并数据集也有运行日志、调试输出等辅助内容方便按模块复用。目前已有442人学习/下载内容编排兼顾原理讲解、案例拆解与动手实践尤其适合刚接触LoRA的开发者按步骤复现。除可直接运行的训练Notebook与数据预处理脚本外还可获得依赖冻结清单、JSONL合并工具、输出日志、效果演示、单元测试项目骨架与金融科技领域示例支撑从环境搭建、数据准备、模型微调到结果验证的完整闭环显著降低复现门槛便于二次改造迁移到自身业务场景。1. 自己动手训练 LoRA一张消费卡就能把大模型调成懂业务的样子做私有化 Agent 和知识库问答的团队多半都卡在同一个环节模型能跑起来但回答太“通用”放到业务里总觉得差点意思。全参微调一张 A100 都未必够而 LoRA 用一张消费级显卡就能把大模型调成特定领域的形状。LoRA 想通以后并不玄学冻结原权重在旁路加两个低秩矩阵训练时只更新旁路。这篇笔记就按 LlamaAlpaca LoRA和 ChatGLM 两条主流路线的 LoRA 训练为主线讲清楚数据准备、最小可跑脚本、显存参数和那些容易翻车的坑。目标是让刚开始接触 LoRA 微调的工程师在 16G 或 24G 显存上跑通从数据到发布的完整闭环已经跑过一两次的人也能对着参数表和避坑清单调出自己的版本。2. LoRA 原理与选型低秩适配靠什么省下显存和时间2.1 冻结大模型权重旁路里塞两个小矩阵假设模型某一层权重是 W形状是 d×k。前向计算时输出是 h Wx。LoRA 不做任何结构改动只在旁边加了一条分支h Wx BAx。其中 A 是 d×r 的矩阵用高斯分布初始化B 是 r×k 的矩阵全零初始化。训练时 W 被冻结梯度只走向 A 和 B这就是“低秩适配”的全部含义。为什么这样设计能work因为微调本质上是对预训练权重的增量修正而这个增量在某个具体任务上的“有效自由度”并不高。预训练已经给了模型强大的通用能力领域适配只是把回答的分布往业务侧推一推低秩分解足够承载这个偏移。这也是 LoRA 和全参微调在效果上最核心的差别LoRA 不是换一个模型而是给原模型加一个方向明确的推力。参数量对比很直观。以 Llama 7B 的 q_proj 为例权重形状是 4096×4096单层就有 16M 参数r 取 8 时LoRA 分支只有 4096×8 8×4096 65536 个参数只占原来的 0.4%。全参微调需要把所有层的梯度、优化器状态都放进显存LoRA 只需要存两个小矩阵的梯度这就是为什么一张消费卡能跑 7B 甚至 13B 模型的微调。r 越大表达能力越强但显存和过拟合风险也同步上涨r8 是绝大多数场景的起步值。2.2 什么时候该用 LoRA什么时候该用全参微调把 LoRA 和常见的几种微调方案放在一起看选型会清晰很多。下面这个对比是我自己做选型时最常用的参考表。方案可训练参数量显存需求7B 级推理延迟适用场景全参微调全部70G 以上多卡才稳无变化数据量极大、任务与预训练分布差异大P-Tuning / Prefix Tuning极少embedding 前缀8G 左右有额外计算少量样本、只需调整输出风格LoRA0.1%1%12G16Gbf168G 内4bit可合并进原权重零额外延迟领域指令微调、对话风格调整、知识库问答适配QLoRA0.1%1%6G10G可合并显存有限时的 LoRA 微调效果略低于 LoRA这里有个容易误解的点QLoRA 不是一种新微调方法它是“4bit 量化基座 LoRA”。量化的是被冻结的基座权重LoRA 分支本身仍然用 bf16 训练所以最终合并回原权重时精度损失基本可控。如果你的显卡刚好差一口气优先试 QLoRA 而不是把 batch_size 压到 1 硬扛。什么时候别用 LoRA这个判断比学会用 LoRA 更重要。数据量到几十万条且能力缺口明显时低秩增量带不动该上全参就上全参需要模型学会全新知识体系或全新语言比如让英文模型直接学会中文对话LoRA 的效果会明显打折扣基座选错时 LoRA 也救不回来中文业务场景用中文基座做 LoRA比用英文基座加中文数据硬调靠谱得多。我的经验是LoRA 负责“把模型已经会的知识按你的格式和风格说出来”它不负责“教模型它本来不会的东西”。3. 指令数据准备从 Alpaca 格式到可训练的文本列3.1 三种常见数据形态和 Alpaca 归一化脚本Alpaca 数据是斯坦福那套 self-instruct 生成出来的格式一条样本三个字段instruction指令、input可选的背景输入、output期望回答。这套格式后来成了开源社区的事实标准也是标题里“Alpaca LoRA”的源头。实际清洗数据时我遇到的更多是这三种形态混在一起单轮问答instruction input outputAlpaca 原生格式。多轮对话conversations 列表user 和 assistant 交替出现。纯文本续写没有明确指令比如代码补全、文档改写就是一段文本接一段文本。这三种格式不能直接丢进同一个训练脚本因为它们拼接出来的文本结构不一致模型会学到混乱的对话规律。常见的做法是先归一化成一个 text 列指令和回答之间用固定分隔符隔开。下面是我常用的转换函数兼容三种形态。# data_normalize.py import json def build_text(item): if conversations in item: # 多轮对话只取最后一轮 user/assistant前面的轮次拼进指令 turns item[conversations] history .join( f用户{turn[content]}\n if turn[role] user else f助手{turn[content]}\n for turn in turns[:-1] ) last turns[-1] return {text: f历史{history}\n用户{last[content]}\n助手} if instruction in item: # 单轮问答输入为空时省略背景 if item.get(input): return {text: f指令{item[instruction]}\n背景{item[input]}\n回答} return {text: f指令{item[instruction]}\n回答} if text in item: # 纯文本续写直接当回答训练 return {text: item[text]} normalized [build_text(json.loads(line)) for line in open(raw.jsonl, encodingutf-8)] with open(train.jsonl, w, encodingutf-8) as f: for item in normalized: f.write(json.dumps(item, ensure_asciiFalse) \n)归一化脚本的逻辑说明多轮对话只保留到最后一轮是为了避免训练时上下文过长导致显存失控也避免了模型把历史轮次里用户的话当成自己要生成的内容。单轮问答把 input 作为背景拼在指令后面回答统一用“回答”开头模型会在训练中学会看到这个前缀就输出业务答案。如果你的场景需要模型真的记住多轮上下文就不要用这个脚本改用下面的标签遮蔽方式配合长 max_length 训练。这里要注意一个参数选择指令和回答之间的分隔符要全文统一不要有时用“回答”有时用“助手”。分隔符一旦混乱模型生成的头部就会带着各种奇怪的前缀这是我调整数据格式时踩得最频繁的坑。3.2 拼接与标签遮蔽让模型只对回答做学习数据归一化成 text 列之后下一步是把文本变成 input_ids并且决定哪些 token 参与 loss 计算。很多新手直接拿 tokenizer 把整条文本编码然后让模型把整条文本都当成预测目标这样模型会花大量精力去背你的指令和背景回答质量反而下降。正确的做法是对 label 做遮蔽问题部分的 label 全设成 -100只有回答部分的 token 参与 loss。# tokenize_with_mask.py from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(/data/models/llama-2-7b-hf) tokenizer.pad_token tokenizer.eos_token # Llama 没有 pad token必须显式指定 def tokenize_with_mask(examples, max_length512): texts examples[text] model_inputs tokenizer(texts, max_lengthmax_length, truncationTrue, return_tensorspt) labels [ids.copy() for ids in model_inputs[input_ids]] # 找到回答起始位置“回答”对应的 token之前的全部屏蔽 answer_token_ids tokenizer(回答, add_special_tokensFalse)[input_ids] for i, input_ids in enumerate(model_inputs[input_ids]): answer_pos None for j in range(len(input_ids) - len(answer_token_ids) 1): if input_ids[j:j len(answer_token_ids)].tolist() answer_token_ids: answer_pos j break if answer_pos is not None: labels[i][:answer_pos 1] [-100] * (answer_pos 1) model_inputs[labels] labels return model_inputs这段代码的逻辑说明先把整条文本编码再找到“回答”这几个 token 的位置把该位置之前的 label 全部替换成 -100。PyTorch 的 CrossEntropyLoss 会自动跳过 -100 的位置所以模型只会从“回答”之后开始学。max_length 这里设 512 是起步值初版流程跑通后再根据业务上下文长度往上调。数据量方面我的建议是走“小而精”的路线1k 条先验证全流程5k20k 条常见于垂直场景。超过 20k 条时先想想是不是数据噪声太多而不是无脑加量。数据质量上重点看三点指令多样性是否覆盖线上真实问题、输出是否稳定无格式噪声、有没有重复样本需要去重。有些团队从网上爬了一堆问答对不清理就直接训结果 LoRA 把爬虫里的 HTML 标签都学进去了生成回答带着“”这类尾巴这种数据问题靠调参数救不回来。4. Llama 与 ChatGLM 的 LoRA 训练实操最小可跑脚本和参数对照4.1 LlamaAlpaca LoRA最小训练脚本Llama 系的 LoRA 训练是生态里最成熟的transformers peft datasets 三件套就能跑。下面这个脚本接第 3 章归一化后的 train.jsonl是一个能直接跑通的最小实现。# train_llama_lora.py import torch from datasets import load_dataset from transformers import ( AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer, DataCollatorForSeq2Seq ) from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training model_path /data/models/llama-2-7b-hf tokenizer AutoTokenizer.from_pretrained(model_path, trust_remote_codeTrue) tokenizer.pad_token tokenizer.eos_token model AutoModelForCausalLM.from_pretrained( model_path, load_in_4bitTrue, # QLoRA 加载方式显存不够就开 torch_dtypetorch.bfloat16, device_mapauto, ) model prepare_model_for_kbit_training(model) model.config.use_cache False # 训练阶段关闭 KV cache省显存 lora_config LoraConfig( r8, # 低秩维度先 8 起步 lora_alpha16, # 缩放系数通常是 r 的两倍 lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj], biasnone, task_typeCAUSAL_LM, ) model get_peft_model(model, lora_config) model.print_trainable_parameters() # 确认可训练参数量远小于原模型 dataset load_dataset(json, data_filestrain.jsonl)[train] dataset dataset.map( # build_text 和 tokenize_with_mask 参考第 3 章 lambda x: tokenize_with_mask(build_text(x), max_length512) ) args TrainingArguments( output_dir./llama_lora_out, per_device_train_batch_size1, gradient_accumulation_steps8, # 等效 batch size 8 learning_rate2e-4, num_train_epochs3, logging_steps50, save_steps500, fp16True, # 消费卡建议开V100 及以下用 fp16 remove_unused_columnsFalse, ) trainer Trainer( modelmodel, argsargs, train_datasetdataset, data_collatorDataCollatorForSeq2Seq(tokenizer, paddingTrue, label_pad_token_id-100), ) trainer.train()脚本参数的逻辑说明load_in_4bit 打开后基座权重被量化到 4bit显存占用直接掉到 bf16 的四分之一这是 QLoRA 的核心。r8 是低秩维度控制旁路矩阵的大小lora_alpha 是缩放系数实际更新强度差不多是 alpha/r 倍所以 alpha 取 r 的两倍是社区最常见的做法。target_modules 里列的是 Llama 注意力层的四个投影矩阵如果用了某些 Llama 变体模块名要对应调整最稳妥的办法是打印 model.named_modules() 确认。fp16 和 bf16 二选一A100 上优先 bf16消费卡没有 bf16 优化就用 fp16。跑之前先在命令行执行 python train_llama_lora.py第一件事是看 print_trainable_parameters 的输出。正常情况下 trainable params 占总参数量的 0.5% 左右如果这个比例异常高说明冻结没生效检查 prepare_model_for_kbit_training 是否执行。训练过程中如果 loss 从 1.5 左右起步并在几个 epoch 内降到 0.8 以下基本算正常如果 loss 开局就是 0.1大概率标签遮蔽没生效模型在背答案。4.2 ChatGLM LoRA加载方式、target_modules 和数据格式差异ChatGLM 走的是另一套路子最大的差异在三处权重加载需要 trust_remote_codeTrue、注意力层结构不同导致 target_modules 不一样、对话格式里带“问/答”角色标记。下面是最小差异脚本主体结构和 Llama 版本一致只列关键差异。# train_chatglm_lora.py model AutoModelForCausalLM.from_pretrained( THUDM/chatglm2-6b, # 或本地路径 trust_remote_codeTrue, # ChatGLM 依赖 remote code必须开 load_in_4bitTrue, torch_dtypetorch.bfloat16, device_mapauto, ) tokenizer AutoTokenizer.from_pretrained( THUDM/chatglm2-6b, trust_remote_codeTrue ) lora_config LoraConfig( r8, lora_alpha32, # ChatGLM 场景稍大的 alpha 更稳 lora_dropout0.05, target_modules[query_key_value], # ChatGLM 把 QKV 合在一个线性层 biasnone, task_typeCAUSAL_LM, )参数说明ChatGLM 的自注意力把 query、key、value 三个投影合在同一个线性层里模块名叫 query_key_value这和 Llama 的 q_proj/k_proj/v_proj 是两套命名体系。如果按 Llama 的模块名直接套peft 会报找不到模块这是 ChatGLM 训练最常见的报错。lora_alpha 我习惯给到 32因为 ChatGLM 的输出分布和 Llama 不太一样需要稍微大一点的缩放系数才能看到明显变化但这要根据你自己的数据实验调整。ChatGLM 的数据格式还有一个特殊点它的官方对话模板是“[Round 0] 问… 答…”训练时如果不按这个模板拼接模型在推理时按模板生成就会对不上。我的处理方式是统一在 build_text 阶段就把文本拼成“问…\n答…”的形态这样生成阶段只需要沿用同样的前缀就能触发。多轮场景下按第 3 章的思路保留历史轮次拼接进“问”部分即可。4.3 显存预算与超参对照表显存是这个方向最现实的门槛下面是我在 16G 和 24G 显卡上实际跑过的预算参考。注意这里的数值会因为 max_length、batch_size、是否开 gradient_checkpointing 产生明显浮动。模型规模加载方式设备显存建议 max_length是否开 gradient_checkpointing7BLlama/ChatGLMQLoRA 4bit8G10G512否7BQLoRA 4bit10G14G1024是13BQLoRA 4bit16G20G5121024是7Bbf16 LoRA16G20G512否超参对照表是另一个容易让人纠结的地方直接给一组可用的起步值超参推荐起步值调整方向learning_rate1e-42e-4loss 震荡时降到 5e-5数据量小也降num_train_epochs23loss 降到平台期就停不是越多越好per_device_train_batch_size1显存允许时升到 2否则用梯度累积gradient_accumulation_steps8等效 batch size 保持在 832r8数据量大或任务复杂升到 16lora_alpha2×r效果弱时升到 3×r 试一版gradient_checkpointing 是显存不够时的后悔药打开后训练速度会慢 20%30%但显存占用能低三分之一。它和 gradient_accumulation_steps 不冲突两个可以同时开。如果 4bit 加载后显存还是爆优先砍 max_length把 2048 降到 1024 往往比把 batch_size 从 2 降到 1 更有效因为大部分输入 padding 到 max_length 时显存被无意义的空格吃掉了。5. LoRA 训练避坑5 个高频问题的现象、原因和处理方法5.1 训练 loss 一直在降回答却变成复读机现象loss 掉到 0.7 以下看着很漂亮但生成阶段模型反复输出同一句话或者把指令内容原样吐回来。这是 LoRA 新手最容易遇到也最容易误判成“模型坏了”的情况。原因学习率偏大导致模型在低秩空间里震荡到某个局部极值更常见的是标签遮蔽没生效模型把整条文本包括你的指令都背下来了生成时它只是在续写“指令部分”而不是回答。loss 下降只代表模型学会了预测下一个 token不代表它学会了你的业务逻辑。解决先验证标签遮蔽取一条训练样本打印 labels 数组确认指令部分全是 -100。然后学习率从 2e-4 降到 1e-4重训一个短的 epoch 对比。如果复读机现象出现在训练后期把 epoch 从 3 降到 2LoRA 过拟合的典型表现就是后期 loss 极低但生成退化。5.2 batch_size 稍微调大就爆显存现象per_device_train_batch_size 从 1 调到 2直接 CUDA out of memory但 nvidia-smi 看显存似乎还有剩余。原因transformers 的 DataCollator 默认会把 batch 内所有样本 padding 到当前 batch 的最大长度而不是模型的最大长度。如果两条样本一条 200 token 一条 1500 token显存按 1500 分配200 的那条也在空转。很多人的数据里混着超长文本截断设置又没生效。解决先确认 tokenize 时 max_length 有没有真正截断可以在 tokenize 函数里打印统计信息看最长长度。训练参数里开 gradient_checkpointing同时把 gradient_accumulation_steps 提到 16 来弥补 batch_size 下降带来的收敛变慢。如果单条样本本身就超过显存优先缩短 max_length这是比任何花式优化都直接的解法。5.3 加载旧 LoRA 权重时维度对不上现象训练完保存了 adapter_model.bin下次加载时报维度不匹配说某个 key 的 shape 对不上或者直接报找不到 query_key_value 这类模块名。原因LoRA 权重和 r、lora_alpha、target_modules 强绑定。你上次用 r8 训完这次想用 r4 加载维度当然对不上或者换了 ChatGLM 版本remote code 里的模块结构变了同名的 query_key_value 实际形状也变了。解决每个 LoRA 训练任务保存时把 lora_config.json 和 base model 的版本信息一起归档。加载前先对比当前 r 和 config 里的 r 是否一致不一致就重新训练不要妄图裁剪权重。ChatGLM 这类依赖 remote code 的模型切换版本前先跑一次 model.named_modules() 看模块名有没有变。我把这当作一个惯例LoRA 权重不跨 r 值复用不跨模型版本复用。5.4 训练完拿到新权重生成结果和原来一模一样现象loss 正常下降adapter 权重也保存了但生成时用 PeftModel 加载后输出和 base model 完全一样仿佛 LoRA 没训过。原因最常见的是推理时忘了加 adapter直接加载了 base model还有一种情况是训练脚本里 model.requires_grad_(False) 和 get_peft_model 的执行顺序错了LoRA 参数实际没被优化loss 下降只是巧合。另外有些推理框架需要显式传 adapter_name不传就默认走原始权重。解决训练阶段看 print_trainable_parameters 确认 trainable params 不为 0。推理阶段用 PeftModel.from_pretrained(base_model, lora_out) 加载并在 generate 前打印 model.active_adapter 确认 adapter 处于激活状态。合并权重时如果文件大小和 base model 几乎一样说明合并成功如果文件大小没变说明 merge 没有生效需要检查 peft 版本。5.5 ChatGLM 多轮数据把系统提示当成了对话内容现象用 ChatGLM 训练多轮对话数据后模型生成时会把“系统提示”里的内容当成上一轮用户的话复述出来或者角色混乱助手开始替用户提问。原因数据拼接时没有按 ChatGLM 的“[Round 0] 问… 答…”模板组织也没有区分角色。模型在训练里见到的规律是“所有文本都是它要预测的内容”推理时系统的角色标记它根本不认识自然当成普通文本处理。解决回到第 3 章的 build_text把多轮数据按“问用户输入\n答助手输出”的格式重组系统提示固定放在“问”之前并在标签遮蔽时把系统提示部分也设成 -100。如果你用的是 ChatGLM3它的官方 tokenizer 自带 apply_chat_template 方法直接用它来构造训练文本比自己拼模板更稳。这种角色混淆问题本质上是数据和生成模板没有对齐别在训练参数上浪费时间。6. 把 LoRA 合并回原模型发布前必须做的导出与回归验证6.1 合并权重与保存完整模型的一个技巧训练产物是 adapter 权重而部署环境往往只认完整模型文件。peft 提供 merge_and_unload 可以直接把 LoRA 分支合并进冻结权重得到一个脱离 peft 依赖的完整模型。一个关键习惯永远在副本上合并不要在训练机上直接覆盖原始权重。LoRA 微调本质是给原权重做增量修改合并操作不可逆没有备份就没有后悔药。# merge_and_save.py from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer base_model AutoModelForCausalLM.from_pretrained( /data/models/llama-2-7b-hf, torch_dtypeauto, device_mapauto ) lora_model PeftModel.from_pretrained(base_model, ./llama_lora_out) merged_model lora_model.merge_and_unload() merged_model.save_pretrained(/data/models/llama-2-7b-lora-merged) tokenizer AutoTokenizer.from_pretrained(/data/models/llama-2-7b-hf) tokenizer.save_pretrained(/data/models/llama-2-7b-lora-merged)这段代码里merge_and_unload 会把 BA 矩阵乘进原始权重并释放 LoRA 训练相关的额外结构保存出来的模型文件大小应该和原模型基本一致。文件大小明显变小说明合并没有完整执行检查 peft 和 transformers 版本是否匹配。合并后建议用 transformers 的原生接口做一次生成验证而不是直接依赖你训练时的 notebook 环境因为部署环境往往没有 peft。6.2 固定 case 清单做回归别只看 loss训练时盯着 loss 看是本能但 loss 下降不等于业务效果达标。我现在每训一版 LoRA 都会维护一份固定的 case 清单20 到 50 条覆盖三类样本——垂直场景的典型问题、通用能力的保留测试、容易越界的边界问题。每条 case 记录生成结果和 base model 的输出并排对比。具体做法是写一个批量评估脚本把 case 清单逐条输入合并后的模型输出到 markdown 文件里做人工打标。业务问题看回答是否准确且格式符合要求通用能力问题看模型有没有“变笨”比如原来会算的简单逻辑题现在还对不对边界问题看它会不会输出不该输出的内容。这三个维度缺一不可。我见过太多项目在垂直指标上刷得漂亮一上线发现通用对话能力崩了原因就是回归清单里没有保留通用测试样本。量化导出是合并后的下一步选项常见路径是转成 GGUF 格式给 llama.cpp 类推理框架用这块工具链比较成熟转换后模型体积能再压一半。我自己的发布习惯是先备份原始权重副本再合并新副本最后固定 case 清单跑完一遍回归才允许进部署。这套流程走了十几个项目翻车率显著下降。希望帮到你。本文还有配套的精品资源点击获取