简介QLoRA量化微调工具包面向希望在大规模语言模型上开展低资源微调的研究者与工程实践者解决显存受限条件下难以对LLM进行高效适配的问题。资源共274个文件压缩包约50.81MB以249个jsonl评测与生成数据为主辅以7个sh运行脚本、4个py源码、4个json配置、2个ipynb演示笔记及md说明文档覆盖数据、脚本与实验记录等环节。内容涉及MMLU少样本与零样本测试集、Vicuna基准人工标注、Guanaco与GPT-3.5生成质量对比以及7B模型在HH-RLHF数据上的采样生成结果便于复现量化微调流程并横向评估模型表现。目前已有643人学习下载适合具备一定深度学习基础、需要工具脚本与评测数据支撑的读者参考使用。1. 量化微调 LLM 到底在省什么从一张 24G 卡跑 7B 说起手头只有一张 24G 显存的卡却想微调一个 7B 甚至 13B 的大模型这是很多做 LLM 微调的工程师真实遇到的处境。全参数微调 7B 模型光优化器状态和梯度就要吃掉上百 G 显存普通设备根本跑不起来。QLoRA 这类量化微调工具解决的正是这个问题它把预训练权重压到 4-bit 存储再在关键层挂上低秩适配器LoRA做训练让显存占用降到原来的零头。这份资源围绕量化 LLM 微调展开包含基准测试数据、评测脚本、生成结果对比和 Colab 演示适合想在自己设备上跑通大模型微调、又不想被显存卡死的从业者。它不教你怎么从零训练模型而是给你一套能直接复现的量化微调与评测流程。2. QLoRA 的量化与低秩适配为什么 4-bit 还能训得动2.1 量化存储与反量化的分工要理解 QLoRA 为什么能在小显存上微调得先分清「存储」和「计算」两件事。QLoRA 的核心思路是预训练权重用 4-bit NormalFloatNF4格式存着前向传播时临时反量化成 16-bit 参与计算反向传播只更新挂在旁边的 LoRA 适配器原始权重全程冻结不动。这样一来显存里常驻的是压缩后的权重计算峰值虽然会短暂升高但整体占用远低于全参数微调。NF4 不是随便切一刀的均匀量化。它假设权重近似正态分布把量化区间按分位数切分让每个桶里落进去的权重数量尽量相等。相比 INT4 的均匀分桶NF4 在同样 4-bit 下保留的信息更多这也是 QLoRA 论文里强调的一个关键设计。另一个细节是双重量化Double Quantization量化本身会产生量化常数这些常数如果也用 32-bit 存累积起来不小QLoRA 把量化常数再量化一次每参数再省约 0.37 bit。LoRA 适配器的秩rank决定了可训练参数量。rank 越大表达能力越强但显存和训练时间也上去。常见做法是 rank 取 8 到 64 之间alpha 一般设成 rank 的两倍。目标模块通常选注意力层的 q_proj、v_proj有些实现也会加上 k_proj、o_proj 和 MLP 层具体看任务复杂度。2.2 用 bitsandbytes 加载 4-bit 模型下面这段是加载 4-bit 量化模型的标准写法基于 transformers bitsandbytes peft 这套组合。先装依赖pip install transformers accelerate bitsandbytes peft datasets然后加载模型和分词器import torch from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig # 4-bit 量化配置 bnb_config BitsAndBytesConfig( load_in_4bitTrue, # 开启 4-bit 加载 bnb_4bit_quant_typenf4, # 用 NF4 量化比 fp4 更适合正态权重 bnb_4bit_compute_dtypetorch.bfloat16,# 计算时用 bf16兼顾速度和精度 bnb_4bit_use_double_quantTrue, # 双重量化再省一点显存 ) model_name your-base-model-path # 替换成实际模型路径或名称 tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForCausalLM.from_pretrained( model_name, quantization_configbnb_config, device_mapauto, # 自动分配到可用设备 )这里几个参数值得说清楚。bnb_4bit_quant_type选 nf4 是 QLoRA 的默认推荐如果你的权重分布偏均匀可以试 fp4 对比。bnb_4bit_compute_dtype用 bfloat16 还是 float16取决于你的卡是否原生支持 bf16老卡比如 V100用 fp16 更稳。device_mapauto让 accelerate 自己决定层怎么放单卡场景基本就是全放上去多卡或显存不够时会自动 offload 到 CPU但 offload 会明显拖慢速度。加载完模型后还要做一步prepare_model_for_kbit_training它会把 LayerNorm 层转成 fp32、开启梯度检查点、冻结原始权重from peft import prepare_model_for_kbit_training model prepare_model_for_kbit_training(model) model.gradient_checkpointing_enable() # 用时间换显存长序列训练必开梯度检查点是个典型的显存换计算前向时不存中间激活反向时重新算一遍。序列长度上到 1024 以上时不开它很容易 OOM。2.3 配置 LoRA 并挂到目标层模型准备好之后用 peft 的 LoraConfig 定义适配器from peft import LoraConfig, get_peft_model lora_config LoraConfig( r16, # 秩8~64 之间调 lora_alpha32, # 一般设成 r 的 2 倍 target_modules[q_proj, v_proj], # 注意力层的投影矩阵 lora_dropout0.05, # 小数据集上防过拟合 biasnone, # 不训练 bias task_typeCAUSAL_LM, ) model get_peft_model(model, lora_config) model.print_trainable_parameters() # 确认可训练参数占比print_trainable_parameters()会打印出可训练参数占总参数的比例QLoRA 场景下通常不到 1%。如果这个比例异常高多半是 target_modules 写错了把整个模型都当成可训练的了。target_modules的名字要和模型实际层名对上不同模型命名不一样比如 LLaMA 系是 q_proj/v_proj有些模型是 query/value加载前最好print(model)看一眼层结构。训练循环可以用 transformers 的 Trainer也可以用 trl 的 SFTTrainer。数据格式上指令微调一般整理成 instruction/input/output 三段或者直接拼成一段带模板的文本。这份资源里的guanaco_7B_demo_colab.ipynb就是一个可跑的端到端示例从数据准备到训练再到推理都有适合拿来对照自己的流程。3. 评测与结果对比MMLU 脚本和生成质量怎么看3.1 MMLU 评测数据的组织方式微调完不能只看 loss 曲线得用标准评测集验证。这份资源里带了 MMLU 的测试和验证数据分 zero-shot 和 five-shot 两套文件用途说明zero_shot_mmlu_test.json零样本测试不給示例直接问zero_shot_mmlu_val.json零样本验证调参时用避免碰测试集five_shot_mmlu_test.json五样本测试每题给 5 个示例five_shot_mmlu_val.json五样本验证同上验证用zero-shot 和 five-shot 的差距能反映模型的上下文学习能力。量化微调之后如果 five-shot 相比 zero-shot 提升明显说明模型还能利用示例如果两者差不多甚至 five-shot 更差可能是微调把模型的 in-context learning 能力削弱了这在过度微调时会出现。评测脚本的核心逻辑是把题目和选项拼成 prompt让模型输出答案再和标准答案比对。常见做法是取模型在 A/B/C/D 四个 token 上的 logits选概率最高的那个而不是让模型自由生成再解析后者容易因为格式问题误判。3.2 生成质量对比Guanaco 65B vs GPT-3.5资源里的generations_qualitative_comparison_guanaco65b_vs_gpt35.ipynb做的是定性对比把同一个 prompt 分别喂给 Guanaco 65B 和 GPT-3.5人工看输出质量。这种对比不能替代定量评测但能暴露一些指标看不出的问题比如重复、跑题、格式崩坏。vicuna_benchmark_human_annotations.csv是人工标注的评测结果通常包含 prompt、两个模型的回答、标注者偏好。用这类数据可以算胜率但要注意标注者偏差和样本量小样本上的胜率波动很大别拿几十条就下结论。7b-hh-rlhf-oa-generations-topp0.9-temp0.7.jsonl是 7B 模型在 HH-RLHF 数据上的生成结果采样参数是 top_p0.9、temperature0.7。这两个参数是生成任务的常用起点temperature 0.7 让输出有一定多样性但不至于乱top_p 0.9 截掉长尾低概率 token。做对比实验时采样参数必须固定否则不同模型之间的差异可能只是随机性造成的。3.3 跑一次评测的完整流程以 MMLU five-shot 为例大致流程是import json import torch from transformers import AutoModelForCausalLM, AutoTokenizer # 加载微调后的模型可以是合并了 LoRA 的完整模型 model AutoModelForCausalLM.from_pretrained(your-finetuned-model, device_mapauto) tokenizer AutoTokenizer.from_pretrained(your-finetuned-model) with open(five_shot_mmlu_test.json) as f: test_data json.load(f) correct 0 for item in test_data: prompt build_prompt(item, shots5) # 拼接 5 个示例 当前题 inputs tokenizer(prompt, return_tensorspt).to(model.device) with torch.no_grad(): logits model(**inputs).logits[:, -1, :] # 取最后一个 token 的 logits # 只看 A/B/C/D 四个选项 token 的概率 option_ids [tokenizer.encode(opt)[0] for opt in [A, B, C, D]] pred [A, B, C, D][torch.argmax(logits[0, option_ids]).item()] if pred item[answer]: correct 1 print(fAccuracy: {correct / len(test_data):.4f})build_prompt负责把示例和题目按固定模板拼起来模板要和训练时一致否则分布对不上评测结果会偏低。取logits[:, -1, :]是拿模型对下一个 token 的预测前提是 prompt 结尾要让模型接着输出答案字母。有些实现会在 prompt 末尾加 Answer: 引导模型输出选项。评测时容易忽略的一点是 tokenizer 的 encode 行为。tokenizer.encode(A)可能返回带空格前缀的 token也可能不带取决于具体分词器。稳妥做法是分别试A和 A看哪个在词表里是单 token用那个。4. 避坑与排查量化微调里那些翻车现场4.1 显存没降反升现象配了 4-bit 加载结果显存占用和 fp16 差不多甚至更高。原因多半是device_map没设对模型被放到了 CPU 或者分散到多设备中间有拷贝开销也可能是 compute_dtype 设成了 float32反量化时把权重撑大了。解决确认bnb_4bit_compute_dtype是 bf16 或 fp16device_mapauto且只有一张卡时检查是否真的全在 cuda 上。用torch.cuda.memory_allocated()打印实际占用别只看 nvidia-smi 的缓存值。4.2 训练 loss 不降或变成 nan现象跑了几百步loss 一直在 2.0 以上晃或者突然变 nan。原因学习率太大是头号嫌疑QLoRA 场景常用 1e-4 到 2e-4但有些模型对学习率敏感得降到 1e-5 量级。另一个原因是数据里有空样本或超长样本padding 处理不当导致 attention mask 错位。解决先用小学习率跑 50 步看 loss 趋势确认在降再放大。数据侧过滤掉空文本和超过 max_seq_length 的样本或者做截断。bf16 比 fp16 更不容易出 nan卡支持的话优先用 bf16。4.3 微调后模型变「傻」现象微调完在目标任务上表现好了但通用能力明显下降问啥都往训练数据的格式上套。原因过拟合或者 LoRA rank 太大、训练轮数太多把原始权重带偏了。也可能是训练数据太单一模型只学会了那一种模式。解决减小 rank加 lora_dropout减少 epoch 数混入一些通用指令数据。评测时不能只看目标任务要跑一遍 MMLU 之类的通用基准确认没有灾难性遗忘。4.4 合并 LoRA 后推理结果不一致现象训练时推理正常把 LoRA 合并进基础模型后输出变了。原因合并时的精度处理有问题。LoRA 权重是 fp32基础模型是 4-bit合并时如果直接相加再量化会有精度损失。另外训练时用的 compute_dtype 和推理时不一致也会导致差异。解决合并时先把基础模型反量化到 fp16加上 LoRA 权重后再保存推理时用同样的 dtype。peft 的merge_and_unload()会处理大部分情况但量化模型合并要额外注意建议合并后在验证集上重新跑一遍确认。4.5 评测分数和论文对不上现象同样的模型和评测集自己跑出来的分数比论文低好几个点。原因prompt 模板不一致是最常见的。MMLU 的 few-shot 示例选择、顺序、格式都会影响结果。另外取答案的方式生成 vs logits不同分数也会有差异。解决对照原论文或官方脚本的 prompt 格式逐字比对。示例的选取如果是随机的固定随机种子。取答案优先用 logits 方式减少解析错误。5. 把量化微调跑稳的几个进阶习惯5.1 用验证集卡早停别盯着训练 loss训练 loss 降不代表模型变好。QLoRA 这种小参数量微调很容易在几百步内就过拟合。我一般会切出 5% 到 10% 的数据做验证集每 N 步跑一次验证 loss连续几次不降就停。资源里的zero_shot_mmlu_val.json和five_shot_mmlu_val.json就是干这个用的别拿测试集调参否则分数虚高上线就露馅。验证集上的指标要选对。生成任务看 loss 和人工抽检分类或选择题看准确率。如果验证 loss 在降但生成质量在变差多半是模型学会了训练数据的表面格式没学到内容这时候要检查数据质量和模板设计。5.2 采样参数固定对比才有意义做模型对比时temperature、top_p、max_new_tokens 这些必须锁死。我见过有人对比两个模型一个用 greedy 一个用采样然后得出「A 比 B 好」的结论这种对比没有意义。资源里7b-hh-rlhf-oa-generations-topp0.9-temp0.7.jsonl的文件名直接把采样参数写进去了这是个好习惯生成结果和参数绑定复现时不会搞混。如果要对比多个模型建议每个 prompt 生成多个样本比如 4 个人工或自动评估时看整体分布而不是单条输出。单条输出随机性太大容易误判。5.3 量化位数和任务难度的匹配不是所有任务都适合 4-bit。简单的指令跟随、格式转换4-bit 微调足够但涉及复杂推理、数学计算的任务4-bit 量化会损失精度微调后可能还不如不微调的 fp16 模型。这种场景可以考虑 8-bit 量化显存占用比 4-bit 高但比 fp16 低精度损失小很多。判断方法很简单先用 4-bit 跑一版在验证集上看指标如果明显低于预期换 8-bit 再跑一版对比。别一上来就追求最低显存效果不行省再多也没用。5.4 一个我常走的检查清单每次跑量化微调之前我会按这个顺序过一遍检查项确认内容显存预算模型大小 激活 优化器状态留 20% 余量量化配置nf4/fp4、compute_dtype、double quant 是否匹配硬件LoRA 目标层打印模型确认层名可训练参数占比是否合理数据格式模板一致、无空样本、长度分布合理评测方案验证集独立、prompt 模板固定、采样参数锁定这套流程走下来大部分低级错误能在开跑前拦住。量化微调本身不复杂坑多在细节里而这些细节往往要翻车一次才记得住。从那以后我每次动量化配置前都会先把这份清单过一遍省下的调试时间远比清单本身花的时间多。希望帮到你。本文还有配套的精品资源点击获取