基于 Self-LLM 的 DeepSeek-7B-Chat 4bits 量化 QLoRA 微调实战:6G 显存训练 7B 模型
【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/datawhalechina/self-llm
本篇技术指南围绕 models/DeepSeek/05-DeepSeek-7B-chat 4bits量化 Qlora 微调.md 展开,讲解如何在 Linux 环境下基于 transformers、peft、bitsandbytes 框架,对 DeepSeek-7B-Chat 模型执行 4bits 量化的 QLoRA 高效微调,并用 6G 显存级别的消费级显卡训练一个完整的 7B 参数模型。读完本文,你将掌握 4bit 量化模型加载、QLoRA 配置、低显存训练参数调优以及微调后的模型推理验证这一整套实战技能。
概述:为什么选择 4bits 量化 QLoRA
传统全量微调 7B 级模型对显存的要求极高,普通消费级显卡难以承受。LoRA(Low-Rank Adaptation)通过冻结原模型权重、只训练注入的低秩矩阵,大幅降低了可训练参数量与显存开销;而 QLoRA 更进一步,将基础模型以 4bits 量化精度加载进显存,使得「6G 显存训练 7B 模型」成为可能。
本教程使用的方案要点如下:
- 基于
transformers、peft、bitsandbytes三个核心框架完成全流程; - 以 4bits(NF4 量化格式)加载 DeepSeek-7B-Chat 基础模型,加载完成后用
nvidia-smi查看显存占用约5.7G; - 只对注入的 LoRA 低秩矩阵做反向传播与参数更新,原模型权重保持冻结;
- 优化器采用 QLoRA 配套的
paged_adamw_32bit分页加载方案,进一步压低显存峰值。
配套的可运行实现见 05-DeepSeek-7B-chat 4bits量化 Qlora 微调.ipynb,可与本文对照学习。若需对比不加量化的普通 LoRA 微调流程,可参考同目录下的 04-DeepSeek-7B-chat Lora 微调.md。
环境配置:安装量化微调依赖
在完成基础环境配置(Python、CUDA 驱动、PyTorch)与本地模型部署之后,还需要安装一组经过本教程验证的第三方库,版本组合如下:
pip install transformers==4.35.2 pip install peft==0.4.0 pip install datasets==2.10.1 pip install accelerate==0.20.3 pip install tiktoken pip install transformers_stream_generator pip install bitsandbytes==0.41.1其中,bitsandbytes是 QLoRA 的量化基石,负责在 GPU 上执行 4bits 线性层量化与反量化计算;peft提供LoraConfig、get_peft_model等低秩适配 API;accelerate支撑device_map="auto"的自动设备分配;datasets用于将 JSON 指令集转换为 Dataset 对象。
本教程使用的微调数据集为仓库根目录下的 dataset/huanhuan.json,该数据集由约 3700 条甄嬛风格对话样本组成(notebook 中处理结果显示num_rows: 3729),是后续指令微调的原料。若安装依赖时网络受限,可先参考 General-Setting/01-pip、conda换源.md 配置国内镜像源。
指令集构建:让模型学会「扮演甄嬛」
LLM 的微调通常指指令微调(Instruction Tuning),即使用形如下方的三元组数据:
{ "instrution":"回答以下用户问题,仅输出答案。", "input":"1+1等于几?", "output":"2" }字段语义如下:
instruction:用户指令,告知模型需要完成的任务;input:用户输入,是完成指令所必需的输入内容;output:模型应当给出的标准输出。
核心训练目标是让模型具备理解并遵循用户指令的能力,因此指令集必须针对目标任务专门构建。本教程以「模拟甄嬛对话风格」的个性化 LLM 为目标,构造的指令样本形如:
{ "instruction": "现在你要扮演皇帝身边的女人--甄嬛", "input":"你是谁?", "output":"家父是大理寺少卿甄远道。" }全部构造好的指令数据存放在 dataset/huanhuan.json 中。从实际样本看(dataset/huanhuan.json),多数样本的input字段为空字符串,instruction直接承载完整语境,output为甄嬛口吻的回答,例如:
{ "instruction": "皇上驾到!", "input": "", "output": "皇上万福金安。" }仓库中 examples/Chat-嬛嬛 目录同样围绕该对话风格数据集展开,可看到同一数据集在不同模型上的复用方式。
数据格式化:将指令集编码为模型可读的序列
LoRA 训练数据需要先经过格式化、编码再送入模型。熟悉 PyTorch 训练流程的同学知道,通常需要把输入文本编码为input_ids,把输出文本编码为labels,编码结果都是多维向量。为此定义一个预处理函数process_func,对每个样本编码输入、输出文本并返回编码后的字典:
def process_func(example): MAX_LENGTH = 384 # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性 input_ids, attention_mask, labels = [], [], [] instruction = tokenizer(f"User: {example['instruction']+example['input']}\n\n", add_special_tokens=False) # add_special_tokens 不在开头加 special_tokens response = tokenizer(f"Assistant: {example['output']}<|end▁of▁sentence|>", add_special_tokens=False) input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id] attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1] # 因为eos token咱们也是要关注的所以 补充为1 labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id] if len(input_ids) > MAX_LENGTH: # 做一个截断 input_ids = input_ids[:MAX_LENGTH] attention_mask = attention_mask[:MAX_LENGTH] labels = labels[:MAX_LENGTH] return { "input_ids": input_ids, "attention_mask": attention_mask, "labels": labels }该格式化逻辑参考了 DeepSeek 官方仓库 README 中给出的对话模板:
User: {messages[0]['content']} Assistant: {messages[1]['content']}<|end▁of▁sentence|>User: {messages[2]['content']} Assistant:几个关键细节:
- 输入侧拼接为
User: ...前缀,输出侧拼接为Assistant: ...前缀并以 DeepSeek 的结束符<|end▁of▁sentence|>收尾,保持与官方对话格式一致; add_special_tokens=False表示不在开头额外添加 special token;labels中,输入部分(User:段)全部置为-100(PyTorch 交叉熵损失会忽略该值),只有Assistant:输出段参与损失计算;- 序列末尾补充
pad_token_id,并将对应attention_mask位置补1,保证长度对齐; MAX_LENGTH = 384用于截断超长样本,因为 DeepSeek 使用的 Llama 系分词器会把一个中文字切分为多个 token,需要放宽最大长度以保留数据完整性。
在 notebook 中,加载数据集并应用该函数的过程为:
from datasets import Dataset import pandas as pd df = pd.read_json('../../data/huanhuan.json') ds = Dataset.from_pandas(df) tokenized_id = ds.map(process_func, remove_columns=ds.column_names)处理完成后tokenized_id的特征列为['input_ids', 'attention_mask', 'labels'],共 3729 行。用tokenizer.decode可以验证格式化结果,例如解码某样本得到:
'User: 小姐,别的秀女都在求中选,唯有咱们小姐想被撂牌子,菩萨一定记得真真儿的——\n\nAssistant: 嘘——都说许愿说破是不灵的。<|end▁of▁sentence|><|end▁of▁sentence|>'过滤掉-100后单独解码labels,则能还原出仅含 Assistant 回复的训练目标:
'Assistant: 你们俩话太多了,我该和温太医要一剂药,好好治治你们。<|end▁of▁sentence|><|end▁of▁sentence|>'加载 Tokenizer 与 4bit 量化模型
模型以 4bits 精度加载,可大幅压缩显存占用。如果你的显卡较新,也可以改用torch.bfloat16作为计算精度。对于 DeepSeek 这类自定义代码模型,加载时必须指定trust_remote_code=True。
tokenizer = AutoTokenizer.from_pretrained('./deepseek-ai/deepseek-llm-7b-chat/', use_fast=False, trust_remote_code=True) tokenizer.padding_side = 'right' # padding在右边 model = AutoModelForCausalLM.from_pretrained( '/root/model/deepseek-ai/deepseek-llm-7b-chat/', trust_remote_code=True, torch_dtype=torch.half, device_map="auto", low_cpu_mem_usage=True, # 是否使用低CPU内存 load_in_4bit=True, # 是否在4位精度下加载模型。如果设置为True,则在4位精度下加载模型。 bnb_4bit_compute_dtype=torch.half, # 4位精度计算的数据类型。这里设置为torch.half,表示使用半精度浮点数。 bnb_4bit_quant_type="nf4", # 4位精度量化的类型。这里设置为"nf4",表示使用nf4量化类型。 bnb_4bit_use_double_quant=True # 是否使用双精度量化。如果设置为True,则使用双精度量化。 ) model.generation_config = GenerationConfig.from_pretrained('/root/model/deepseek-ai/deepseek-llm-7b-chat/') model.generation_config.pad_token_id = model.generation_config.eos_token_id4bit 加载相关参数的作用如下:
| 参数 | 取值 | 作用 |
|---|---|---|
load_in_4bit | True | 以 4bits 精度加载模型,是 QLoRA 的核心开关 |
bnb_4bit_compute_dtype | torch.half | 4bits 权重参与计算时反量化到的计算精度,通常设为半精度 |
bnb_4bit_quant_type | nf4 | 4bits 量化格式,nf4(Normal Float 4)是 bitsandbytes 针对正态分布权重设计的 4bit 量化格式,精度优于传统的 int4 |
bnb_4bit_use_double_quant | True | 双重量化,即对量化缩放因子再做一次量化,进一步压缩显存占用 |
device_map | "auto" | 由 accelerate 自动将各层分配到可用设备 |
low_cpu_mem_usage | True | 降低加载过程中的 CPU 内存峰值 |
加载完成后,可以在终端用nvidia-smi确认显存占用,本教程实践观察约5.7G左右。从 notebook 输出的模型结构也可以印证量化确实生效:模型各线性层均显示为Linear4bit,例如:
(self_attn): LlamaAttention( (q_proj): Linear4bit(in_features=4096, out_features=4096, bias=False) (k_proj): Linear4bit(in_features=4096, out_features=4096, bias=False) (v_proj): Linear4bit(in_features=4096, out_features=4096, bias=False) (o_proj): Linear4bit(in_features=4096, out_features=4096, bias=False) ) (mlp): LlamaMLP( (gate_proj): Linear4bit(in_features=4096, out_features=11008, bias=False) (up_proj): Linear4bit(in_features=4096, out_features=11008, bias=False) (down_proj): Linear4bit(in_features=11008, out_features=4096, bias=False) ... )可见 DeepSeek-7B-Chat 的注意力四投影(q/k/v/o)与 MLP 三个投影(gate/up/down)全部被替换为Linear4bit,这正是显存被压到 6G 量级的直接原因。
定义 LoraConfig:注入低秩适配器
LoraConfig是 peft 中配置低秩适配的核心类,可设置参数较多,但核心参数并不多:
task_type:模型任务类型,因果语言模型填TaskType.CAUSAL_LM;target_modules:需要注入 LoRA 的层名,主要是 attention 部分,不同模型层名不同,可传数组、字符串或正则表达式;r:LoRA 的秩(rank);lora_alpha:LoRA 的缩放系数;lora_dropout:LoRA 层 dropout 比例。
需要特别澄清的是,LoRA 的实际缩放比例是lora_alpha / r,而不是r本身。下面配置中lora_alpha=32、r=8,缩放即为 4 倍。
config = LoraConfig( task_type=TaskType.CAUSAL_LM, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], inference_mode=False, # 训练模式 r=8, # Lora 秩 lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理 lora_dropout=0.1# Dropout 比例 )需要说明的是,配套 notebook 中target_modules实际使用的是["q_proj", "k_proj", "v_proj", "o_proj"],即只对 attention 四个投影注入 LoRA;而本教程正文给出的是同时覆盖 MLP 三个投影(gate_proj、up_proj、down_proj)的扩展版本。从训练成本角度推断,覆盖 MLP 层会增加可训练参数量与计算开销,但对模型表达能力可能有一定增益,读者可根据显存余量自行取舍。对应地,notebook 中 4 模块配置下调用get_peft_model后的可训练参数统计为:
trainable params: 7,864,320 || all params: 3,882,602,496 || trainable%: 0.20255279823525874即在约 38.8 亿参数(含 4bit 量化基座)的模型中,仅约 786 万参数参与训练,训练比例仅约0.20%,这正是 LoRA 高效微调的直接量化证据。
在正式训练前还需注意:若开启梯度检查点(gradient_checkpointing=True),必须执行model.enable_input_require_grads():
model = get_peft_model(model, config) model.enable_input_require_grads() # 开启梯度检查点时,要执行该方法 model.print_trainable_parameters()自定义 TrainingArguments:为低显存场景调参
TrainingArguments的源码对每个参数都有说明,这里介绍几个训练中最常用的:
output_dir:模型输出目录;per_device_train_batch_size:单卡 batch size;gradient_accumulation_steps:梯度累加步数,显存较小时可调小 batch size、增大梯度累加;logging_steps:每隔多少步输出一次日志;num_train_epochs:训练轮数(epoch);save_steps:每隔多少步保存一次 checkpoint;learning_rate:学习率;gradient_checkpointing:梯度检查点,开启后以计算换显存,可显著降低激活值占用,但必须配合model.enable_input_require_grads()使用;optim="paged_adamw_32bit":使用 QLoRA 的分页优化器,当显存不足时将优化器状态换出到 CPU 内存,从而避免 OOM。
args = TrainingArguments( output_dir="./output/DeepSeek", per_device_train_batch_size=8, gradient_accumulation_steps=2, logging_steps=10, num_train_epochs=3, save_steps=100, learning_rate=1e-4, save_on_each_node=True, gradient_checkpointing=True, optim="paged_adamw_32bit" # 优化器类型 )显存适配说明:上方正文配置(batch_size=8、梯度累加 2)适合显存较充裕的场景;而 notebook 中为了演示低显存训练,实际采用了per_device_train_batch_size=1、gradient_accumulation_steps=1的更保守组合,配合 4bit 量化与梯度检查点,即可在约 6G 显存条件下完成 7B 模型的训练。两种配置的其余超参数保持一致,读者可根据自身显卡显存灵活调整。
使用 Trainer 训练
数据、模型与训练参数就绪后,使用 transformers 的Trainer启动训练,并用DataCollatorForSeq2Seq对批次内序列做动态 padding:
trainer = Trainer( model=model, args=args, train_dataset=tokenized_id, data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True), ) trainer.train()几点补充说明:
DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True)会在每个 batch 内部按最长序列动态补齐pad_token_id,相比固定长度 padding 更省显存;- 由于
TrainingArguments中设置了save_steps自动保存策略,训练结束后并不需要手动保存模型,checkpoint 会自动写入output_dir; - 整个训练集为 3729 条样本,3 个 epoch 下迭代步数有限,适合在消费级显卡上一次跑通全流程。
模型推理与效果验证
训练完成后,用经典方式做一次推理验证,输入一句甄嬛风格的台词观察模型回复:
text = "小姐,别的秀女都在求中选,唯有咱们小姐想被撂牌子,菩萨一定记得真真儿的——" inputs = tokenizer(f"User: {text}\n\n", return_tensors="pt") outputs = model.generate(**inputs.to(model.device), max_new_tokens=100) result = tokenizer.decode(outputs[0], skip_special_tokens=True) print(result)本教程的推理结果如下,可见即使是 4bits 量化的微调模型,对话风格还原效果依然不错:
User: 小姐,别的秀女都在求中选,唯有咱们小姐想被撂牌子,菩萨一定记得真真儿的—— Assistant: 姐姐,你别说了,我自有打算。推理时需注意保持与训练一致的对话模板前缀(User: ...),并设置max_new_tokens控制生成长度;skip_special_tokens=True用于在解码时剔除<|end▁of▁sentence|>等特殊 token。
小结
本文完整复现了基于 models/DeepSeek/05-DeepSeek-7B-chat 4bits量化 Qlora 微调.md 的 DeepSeek-7B-Chat QLoRA 微调全流程:从bitsandbytes等依赖安装、甄嬛指令集构建与格式化,到 4bits NF4 量化模型加载、LoraConfig注入低秩适配器、TrainingArguments低显存调参,再到Trainer训练与推理验证。
核心要点回顾:
- 4bits 量化(
load_in_4bit+bnb_4bit_quant_type="nf4"+ 双重量化)将 7B 模型的显存占用压到约 5.7G,使消费级显卡训练成为可能; - LoRA 只训练约 0.20% 的参数,配合
paged_adamw_32bit分页优化器与梯度检查点,进一步控制显存峰值; - 数据格式必须与 DeepSeek 官方
User:/Assistant:对话模板保持一致,labels中仅 Assistant 段参与损失计算; - 配套完整实现可直接参考 05-DeepSeek-7B-chat 4bits量化 Qlora 微调.ipynb,数据与更多示例见 dataset/huanhuan.json 与 examples/Chat-嬛嬛。
【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/datawhalechina/self-llm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考