千问大模型二次LoRA‑SFT指令微调指南
大模型微调是实现模型领域定制的核心方案。本文基于 Ubuntu 22.04 服务器环境,依托 NVIDIA RTX 家用高性能显卡,选用千问 Qwen3.5‑0.8B‑Instruct 指令模型,完整演示二次 LoRA‑SFT 微调全流程,包含环境搭建、模型选型、ChatML 数据集制作校验、LoRA 参数配置、SFT 训练、效果验证、权重合并,附带可直接运行的工程脚本,便于开发者快速实现千问模型轻量化定制与上线部署。

在开展Qwen模型微调实操前,需提前配置好服务器软硬件运行环境,本文所有实操流程均基于以下稳定运行的本机环境,读者可直接参考对齐配置,适配复现:
- 操作系统:Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-187-generic x86_64)
- 框架版本:torch 2.13.0 + CUDA 12.8.1 + Python 3.10.12
- 显卡驱动:NVIDIA ≥ 570.133.07
- 显卡类型:NVIDIA RTX 4090 24GB GPU
- CPU核心:INTEL(R) XEON(R) GOLD 6530
- 内存:64GB DDR5
- 存储:50GB(系统盘)+100GB(数据盘)
基础知识
两种训练
大模型行业落地场景中,通用预训练模型难以适配垂直领域专属知识、定制化对话风格与专属业务指令需求,模型微调成为轻量化、低成本实现模型能力定制的核心手段。
模型的微调训练包含两种主流模式:
-
第一种:基于Base预训练基座模型开展从零SFT微调,该类模型仅完成通用预训练,无官方指令对齐能力,无法原生理解对话指令,需人工构建对话格式、手动拼接对话文本及监督数据集,从零学习指令跟随与对话能力,多用于全新模型定制与二次预训练任务,该模式消耗算力大,一般不会使用此方式微调。
-
第二种:指令模型二次 LoRA‑SFT 微调,也是本文的核心内容,千问 Instruct 指令模型由 Base 基座经过官方 SFT 监督微调、DPO/RLHF 偏好对齐得到,原生具备指令理解与多轮对话能力。在此成熟对齐模型基础上开展二次 LoRA‑SFT 微调,可在保留模型通用知识与基础对话能力的前提下,低成本注入领域专属能力或知识,规避基座从零训练的数据门槛与对齐成本,是工程落地与轻量化模型定制的最优方案。
进行微调前需要判别模型底座,模型名称带有‑Base后缀的为预训练裸基座;模型目录中包含chat_template.jinja文件,则说明是 Instruct 指令模型,该类模型已经完成 SFT、DPO、RLHF 等指令对齐流程。现实中原始预训练基模较少对外开源,公开可获取的大多是经过完整后训练的成品权重。
LoRA/QLoRA
LoRA 与 QLoRA 是目前主流的参数高效微调方法。LoRA 通过冻结主干模型,仅训练注意力层的低秩适配器,在几乎不损失模型性能的前提下降低训练参数量,适合显存条件较好的硬件环境。QLoRA 在 LoRA 基础上引入 4/8 比特权重量化,将主干模型以量化形式加载,以此来压缩显存占用,使大模型微调可以在消费级显卡上完成,但会引入少量量化噪声,可能对最终效果带来轻微影响。
| 项目 | LoRA | QLoRA |
|---|---|---|
| 核心机制 | 低秩适配器 | 低秩适配器 + INT4/INT8 权重量化 |
| 模型主干精度 | FP16/BF16 | INT4/INT8 量化冻结 |
| 显存消耗 | 较低 | 更低 |
| 精度损失 | 几乎无 | 存在轻微量化损失 |
| 工程复杂度 | 低,稳定通用 | 较高,存在量化适配问题 |
| 适用场景 | 显存条件较好,8B‑14B,追求微调效果 | 显存受限,超大模型 (27B+),消费级显卡 |
LoRA 与 QLoRA 的选择主要依据硬件设备显存条件确定。对于 8B 规模模型,当显卡显存大于 24GB 时,优先采用 LoRA 微调;针对 14B、27B 等更大规模模型或显存资源受限场景,则选用 QLoRA 并开启 4 比特量化加载以降低显存压力。本文选用体量较小的 Qwen3.5‑0.8B‑Instruct 作为实验对象,模型参数量小,便于流程演示,因此直接采用 LoRA 开展二次 SFT 指令微调。
- 完整实验链路为:环境准备 → 数据集准备(train.json) → 加载基座模型(Qwen3.5‑0.8B‑Instruct) → LoRA 参数配置 → SFTTrainer 执行微调训练 → 本地推理效果验证 → LoRA 适配器与主模型权重合并导出
微调实验
环境安装
1、创建独立 Python 虚拟环境,隔离项目依赖,避免和系统 Python 包冲突。
root@localhost:~/# sudo apt install -y python3-full python3-venv tmux git tree
root@localhost:~/# sudo python3 -m venv ~/myvenv
root@localhost:~/# source ~/myvenv/bin/activate
2、激活进入虚拟环境,使用腾讯云镜像源加速深度学习、微调相关全套依赖包。
root@localhost:~/# pip3 install -i https://mirrors.cloud.tencent.com/pypi/simple/ torch torchvision trl datasets peft accelerate bitsandbytes wandb transformers sentencepiece huggingface_hub protobuf modelscope
root@localhost:~/# pip3 list
Package Version
---------------------- ------------
accelerate 1.14.0
aiohappyeyeballs 2.7.1
aiohttp 3.14.3
aiosignal 1.4.0
annotated-doc 0.0.5
annotated-types 0.8.0
anyio 4.15.1
async-timeout 5.0.1
attrs 26.1.0
bitsandbytes 0.50.2
certifi 2026.7.22
charset-normalizer 3.5.1
click 8.5.0
cuda-bindings 13.3.1
cuda-pathfinder 1.8.1
cuda-toolkit 13.0.3.0
datasets 5.0.1
dill 0.4.1
exceptiongroup 1.3.1
filelock 3.32.5
frozenlist 1.8.0
fsspec 2026.6.0
h11 0.16.0
hf-xet 1.6.0
httpcore 1.0.9
httpx 0.28.1
huggingface_hub 1.30.0
idna 3.19
Jinja2 3.1.6
markdown-it-py 4.2.0
MarkupSafe 3.0.3
mdurl 0.1.2
mpmath 1.3.0
multidict 6.7.1
multiprocess 0.70.19
networkx 3.4.2
numpy 2.2.6
nvidia-cublas 13.1.1.3
nvidia-cuda-cupti 13.0.85
nvidia-cuda-nvrtc 13.0.88
nvidia-cuda-runtime 13.0.96
nvidia-cudnn-cu13 9.24.0.43
nvidia-cufft 12.0.0.61
nvidia-cufile 1.15.1.6
nvidia-curand 10.4.0.35
nvidia-cusolver 12.0.4.66
nvidia-cusparse 12.6.3.3
nvidia-cusparselt-cu13 0.8.1
nvidia-nccl-cu13 2.30.7
nvidia-nvjitlink 13.3.33
nvidia-nvshmem-cu13 3.4.5
nvidia-nvtx 13.0.85
opentelemetry-api 1.44.0
packaging 26.3
pandas 2.3.3
peft 0.20.0
pillow 12.3.0
pip 22.0.2
platformdirs 4.11.7
propcache 0.5.2
protobuf 7.36.1
psutil 7.2.2
pyarrow 25.0.1
pydantic 2.13.5
pydantic_core 2.46.5
Pygments 2.21.0
python-dateutil 2.9.0.post0
pytz 2026.3.post1
PyYAML 6.0.3
regex 2026.9.3
requests 2.34.2
rich 15.0.0
safetensors 0.8.0
sentry-sdk 2.68.1
setuptools 84.0.0
shellingham 1.5.4
six 1.17.0
sympy 1.14.0
tokenizers 0.23.2
torch 2.14.0
torchaudio 2.11.0
torchvision 0.29.0
tqdm 4.70.0
transformers 5.16.1
triton 3.8.0
trl 1.12.0
typer 0.27.2
typing_extensions 4.16.0
typing-inspection 0.4.4
tzdata 2026.3
urllib3 2.7.0
wandb 0.29.0
xxhash 4.0.1
yarl 1.24.5
下载底座模型
使用 modelscope 下载 Qwen/Qwen3.5‑0.8B‑Instruct 已经预 SFT 完成的底座模型,该模型原生支持 ChatML 对话模板,不需要自己修改 tokenizer,适合直接二次 LoRA 微调;不要手动复制网页,使用 modelscope 下载保证文件完整,避免文件缺失导致训练报错。
https://www.modelscope.cn/models/icyfenix/Qwen3.5-0.8B-Instruct
root@localhost:~/# mkdir data
root@localhost:~/# cd data
root@localhost:~/# modelscope download --model icyfenix/Qwen3.5-0.8B-Instruct --local_dir ./qwen3.5‑0.8B‑Instruct
准备 SFT 微调数据集
采用 Qwen 标准 ChatML 格式,json 数组格式,每一条样本包含conversation数组,数组内是多轮对话,角色仅允许system/user/assistant,文件保存到/root/data/train.json中。
此处为小样本演示数据集,适合跑通流程;真实业务建议扩充到几百~几千条,太少容易过拟合。
[
{
"conversation": [
{"role":"user","content":"什么是大模型LoRA微调?"},
{"role":"assistant","content":"LoRA是一种轻量微调技术,冻结大模型主干权重,只训练少量低秩矩阵,不需要全量更新模型参数,显存消耗低,训练速度快,产出的LoRA适配器体积很小,可以挂载在基础模型上使用。"}
]
},
{
"conversation": [
{"role":"user","content":"LoRA和QLoRA有什么区别?"},
{"role":"assistant","content":"LoRA主干模型使用FP16/BF16精度加载,主干不量化;QLoRA把主干模型4bit量化,进一步降低显存开销,会带来微小的精度损失。"}
]
},
{
"conversation": [
{"role":"user","content":"SFT监督微调是什么?"},
{"role":"assistant","content":"SFT即监督微调,使用高质量问答对话数据集,让大模型学习期望的输出格式、回答风格、领域知识,是对齐大模型的基础步骤。"}
]
},
{
"conversation": [
{"role":"user","content":"微调为什么会出现灾难性遗忘?"},
{"role":"assistant","content":"当训练数据集单一,模型过度学习新数据,丢失原本基础能力,就会发生灾难性遗忘。解决办法是混入通用对话数据,控制训练步数,避免过拟合。"}
]
},
{
"conversation": [
{"role":"user","content":"PEFT是什么?"},
{"role":"assistant","content":"PEFT是HuggingFace的参数高效微调库,包含LoRA、IA3等多种算法,只训练一小部分参数,不用完整保存整个大模型。"}
]
},
{
"conversation": [
{"role":"user","content":"如何判断微调过拟合?"},
{"role":"assistant","content":"观察eval验证loss,loss不再下降甚至上升;模型在训练集问题回答很好,陌生问题表现很差;输出重复、背诵训练集内容,就是过拟合。可以减少训练步数,增大数据集多样性。"}
]
},
{
"conversation": [
{"role":"user","content":"vLLM可以直接加载LoRA适配器吗?"},
{"role":"assistant","content":"原生PEFT标准LoRA适配器,新版vLLM支持直接加载,不需要手动合并权重;Unsloth的bnb‑4bit的LoRA不能直接加载,必须合并导出完整权重。"}
]
},
{
"conversation": [
{"role":"system","content":"你是大模型微调助手,回答简洁专业"},
{"role":"user","content":"微调需要多少样本?"},
{"role":"assistant","content":"风格迁移:200‑1000条;垂直领域知识增强:2000‑10000条;样本过少极易过拟合。"}
]
},
{
"conversation": [
{"role":"user","content":"训练的时候max_steps和epoch选哪个?"},
{"role":"assistant","content":"小数据集优先max_steps,防止epoch重复训练造成过拟合;大数据集可以使用num_train_epochs,一般1‑3轮。"}
]
},
{
"conversation": [
{"role":"user","content":"LoRA的r参数代表什么?"},
{"role":"assistant","content":"r是LoRA秩,r越大可训练参数量越大;常用8、16、32;r太小表达能力不足,r太大容易过拟合。"}
]
},
{
"conversation": [
{"role":"user","content":"梯度累积的作用是什么?"},
{"role":"assistant","content":"显存不足调小batch_size,通过gradient_accumulation_steps做梯度累积,模拟更大的batch,不增加显存占用。"}
]
},
{
"conversation": [
{"role":"user","content":"bf16和fp16区别?"},
{"role":"assistant","content":"bf16指数范围更大,适合大模型训练;30系及以上显卡支持bf16;20系及更早显卡只能用fp16。"}
]
},
{
"conversation": [
{"role":"user","content":"微调数据集一定要用apply_chat_template吗?"},
{"role":"assistant","content":"强烈建议使用,不要手动拼接<|im_start|>标签,手动拼接极易格式错误,直接导致训练失效。"}
]
},
{
"conversation": [
{"role":"user","content":"训练完怎么推理?"},
{"role":"assistant","content":"两种方式:1、peft.PeftModel加载基础模型+LoRA适配器;2、合并LoRA权重得到完整模型;也可以vLLM直接加载LoRA做API服务。"}
]
},
{
"conversation": [
{"role":"user","content":"为什么要划分验证集?"},
{"role":"assistant","content":"验证集没有参与训练,用来监控过拟合,脚本中load_best_model_at_end会自动保存验证loss最优的LoRA权重。"}
]
},
{
"conversation": [
{"role":"user","content":"什么是packing在SFTTrainer?"},
{"role":"assistant","content":"packing=True会把多个短样本拼接在一起提升效率;样本长度参差不齐时建议关闭packing=False。"}
]
},
{
"conversation": [
{"role":"user","content":"学习率LoRA一般设置多大?"},
{"role":"assistant","content":"标准LoRA常用1e‑4 ~3e‑4,不要设置过大,容易震荡发散。"}
]
},
{
"conversation": [
{"role":"system","content":"你是大模型微调助手"},
{"role":"user","content":"训练loss下降就代表微调成功吗?"},
{"role":"assistant","content":"不是。loss下降只代表拟合训练数据,必须在真实问题上做人工评测,同时看验证集loss,防止过拟合。"}
]
},
{
"conversation": [
{"role":"user","content":"可以直接拿网上开源数据集直接训练吗?"},
{"role":"assistant","content":"可以,但要检查字段格式,转换为Qwen ChatML,过滤脏数据,最好混入通用对话数据,避免遗忘基础能力。"}
]
},
{
"conversation": [
{"role":"user","content":"微调之后模型会永久改变吗?"},
{"role":"assistant","content":"LoRA只是附加适配器,基础模型文件不会改动;推理时加载LoRA才生效,移除LoRA就恢复原始基础模型。"}
]
}
]
数据集校验
数据集校验脚本,提前拦截 JSON 语法错误、字段缺失、非法 role 角色;同时调用分词器apply_chat_template渲染对话,检查 ChatML 标签<|im_start|>/<|im_end|>输出是否正常。
很多训练失败根源是数据集格式错误,训练前必须运行该脚本,全部校验通过再进入训练。
import json
from transformers import AutoTokenizer
JSON_PATH = r"/root/data/train.json"
MODEL_NAME = r"/root/data/qwen3.5-0.8B-Instruct"
if __name__ == "__main__":
try:
with open(JSON_PATH, "r", encoding="utf‑8") as f:
data = json.load(f)
except Exception as e:
print(f"[-] JSON解析失败:{e}")
exit(1)
if not isinstance(data, list):
print("[-] 数据集最外层必须是数组")
exit(1)
print(f"[+] JSON读取成功,总样本数:{len(data)}")
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
print(f"[+] 成功加载分词器 {MODEL_NAME}")
valid_roles = {"user", "assistant", "system"}
error_count = 0
for idx, item in enumerate(data):
if "conversation" not in item:
print(f"\n[-] 第{idx}条:缺少 conversation 字段")
error_count += 1
continue
conv = item["conversation"]
if not isinstance(conv, list):
print(f"\n[-] 第{idx}条:conversation必须是数组")
error_count += 1
continue
for turn_idx, turn in enumerate(conv):
if "role" not in turn or "content" not in turn:
print(f"\n[-] 第{idx}条‑第{turn_idx}轮:缺少role或content")
error_count += 1
continue
r = turn["role"]
if r not in valid_roles:
print(f"\n[-] 第{idx}条‑第{turn_idx}轮:非法role={r},允许:{valid_roles}")
error_count += 1
print(f"\n---------------------------")
if error_count > 0:
print(f"[!] 检测到 {error_count} 处错误,请修改后再训练!")
exit(1)
else:
print("[+] 所有样本字段格式校验通过!")
# 渲染ChatML看标签是否正确
print("\n===== 抽样渲染前2条看ChatML输出 =====")
for i in range(min(2, len(data))):
conv = data[i]["conversation"]
text = tokenizer.apply_chat_template(
conv, tokenize=False, add_generation_prompt=False
)
print(f"\n--------样本{i}--------")
print(text[:600])
if "<|im_start|>" not in text:
print("[!] 警告:输出没有 <|im_start|>,模板异常!")
print("\n[+] 校验完成,可以送入训练脚本。")
检查通过后可看到如下所示输出内容:
root@localhost:~/data# python check.py
[+] JSON读取成功,总样本数:20
[+] 成功加载分词器 ./qwen3.5-0.8B-Instruct
---------------------------
[+] 所有样本字段格式校验通过!
===== 抽样渲染前2条看ChatML输出 =====
--------样本0--------
<|im_start|>user
什么是大模型LoRA微调?<|im_end|>
<|im_start|>assistant
<think>
</think>
LoRA是一种轻量微调技术,冻结大模型主干权重,只训练少量低秩矩阵,不需要全量更新模型参数,显存消耗低,训练速度快,产出的LoRA适配器体积很小,可以挂载在基础模型上使用。<|im_end|>
--------样本1--------
<|im_start|>user
LoRA和QLoRA有什么区别?<|im_end|>
<|im_start|>assistant
<think>
</think>
LoRA主干模型使用FP16/BF16精度加载,主干不量化;QLoRA把主干模型4bit量化,进一步降低显存开销,会带来微小的精度损失。<|im_end|>
[+] 校验完成,可以送入训练脚本。
分词器加载与验证脚本
验证底座模型分词器加载是否正常,查看特殊 token(<|im_start|>、<|im_end|>、eos/pad token)ID,验证编码解码、对话模板输出。很多训练 loss 爆炸、生成乱码来自分词器配置错误;Qwen 系列需要把pad_token设置等于eos_token,padding_side=right。
from datasets import load_dataset, concatenate_datasets
from transformers import AutoTokenizer,set_seed
SEED = 42
set_seed(SEED)
MODEL_NAME = r"/root/data/qwen3.5-0.8B-Instruct"
if __name__ == "__main__":
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
print("[+] 成功加载分词器", MODEL_NAME)
print(f"分词器类名: {type(tokenizer).__name__}")
print(f"vocab_size: {tokenizer.vocab_size}")
print(f"model_max_length: {tokenizer.model_max_length}")
print(f"padding_side: {tokenizer.padding_side}")
print(f"truncation_side: {tokenizer.truncation_side}")
print("\n---------- 全部特殊token及ID ----------")
special_tokens = [
("bos_token", tokenizer.bos_token, tokenizer.bos_token_id),
("eos_token", tokenizer.eos_token, tokenizer.eos_token_id),
("pad_token", tokenizer.pad_token, tokenizer.pad_token_id),
("unk_token", tokenizer.unk_token, tokenizer.unk_token_id),
("im_start token", "<|im_start|>", tokenizer.convert_tokens_to_ids("<|im_start|>")),
("im_end token", "<|im_end|>", tokenizer.convert_tokens_to_ids("<|im_end|>")),
]
for name, tok, tid in special_tokens:
print(f"{name:<15} token={repr(tok):<22} id={tid}")
print("\n---------- special_tokens_map ----------")
print(tokenizer.special_tokens_map)
#print("\n---------- init_kwargs 初始化参数 ----------")
#print(tokenizer.init_kwargs)
print("\n---------- 词表前30个样例 ----------")
vocab = list(tokenizer.get_vocab().items())[:30]
for token_str, token_id in vocab:
print(f"{token_id:6d} | {repr(token_str)}")
print("\n---------- 简单编码解码测试 ----------")
test_sentence = "你好,Qwen大模型!"
encode_out = tokenizer(test_sentence)
print(f"原文:{test_sentence}")
print(f"input_ids:{encode_out['input_ids']}")
print(f"还原:{tokenizer.decode(encode_out['input_ids'])}")
print("\n---------- apply_chat_template对话模板测试 ----------")
messages = [
{"role":"system","content":"你是助手"},
{"role":"user","content":"你好"}
]
chat_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
print("chat template输出文本:")
print(repr(chat_text))
检查通过后可看到如下所示输出内容:
重点检查:
pad_token不为 None,padding_side=right,apply_chat_template输出包含正确 im 标签。
root@localhost:~/data# python check.py
[+] 成功加载分词器
分词器类名: Qwen2Tokenizer
vocab_size: 248044
model_max_length: 262144
padding_side: right
truncation_side: right
---------- 全部特殊token及ID ----------
bos_token token=None id=None
eos_token token='<|im_end|>' id=248046
pad_token token='<|im_end|>' id=248046
unk_token token=None id=None
im_start token token='<|im_start|>' id=248045
im_end token token='<|im_end|>' id=248046
---------- special_tokens_map ----------
{'eos_token': '<|im_end|>', 'pad_token': '<|im_end|>', 'audio_bos_token': '<|audio_start|>', 'audio_eos_token': '<|audio_end|>', 'audio_token': '<|audio_pad|>', 'image_token': '<|image_pad|>', 'video_token': '<|video_pad|>', 'visi
on_bos_token': '<|vision_start|>', 'vision_eos_token': '<|vision_end|>'}
---------- 词表前30个样例 ----------
222609 | 'ã썿ĢĿãģ£ãģŁãĤī'
150945 | 'ĠдÑĥ'
101644 | 'ä¹Łå°±æĺ¯'
38671 | 'Ġcurse'
173593 | 'назна'
126060 | 'åīįä¸įä¹ħ'
164288 | 'hão'
136874 | 'éĢĢä¼ij人åijĺ'
125605 | 'ä¸ĢäºĽå°ı'
148886 | 'ள'
228510 | 'Ġentregue'
184351 | 'Ġinklus'
167525 | 'ĠاÙĦÙħصÙĨ'
176754 | 'Ġtratamento'
57152 | 'calar'
187517 | 'ÑģнÑĥ'
3978 | 'ĠOff'
42853 | 'Ġrecruited'
170936 | 'Ġbiá»ĥn'
6770 | 'ometry'
201897 | 'Ġmaintenir'
21855 | 'ĠSure'
144957 | 'åĩĨèĢĥè¯ģæīĵåį°'
66583 | '(seg'
81516 | 'ulado'
125493 | '空空'
832 | 'arg'
146557 | 'èı²èı²'
173079 | 'ĠTư'
83695 | 'ĉJson'
---------- 简单编码解码测试 ----------
原文:你好,Qwen大模型!
input_ids:[109266, 3709, 48, 16451, 95779, 103725, 6115]
还原:你好,Qwen大模型!
---------- apply_chat_template对话模板测试 ----------
chat template输出文本:
'<|im_start|>system\n你是助手<|im_end|>\n<|im_start|>user\n你好<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n'
LoRA‑SFT 训练脚本
加载本地 Qwen3.5‑0.8B‑Instruct 基座模型,冻结主干,使用 LoRA 做轻量化微调;读取本地 json 对话数据集,经过 chat 模板格式化、分词、过滤、划分训练验证集;使用 Trainer 执行 step 式训练,保存 LoRA 适配器,最后加载 LoRA 做简单推理验证。
import os
import torch
from datasets import load_dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
set_seed,
DataCollatorForLanguageModeling,
Trainer
)
from peft import PeftModel, LoraConfig, get_peft_model
import warnings
warnings.filterwarnings("ignore")
SEED = 42
set_seed(SEED)
# 配置路径
LOCAL_JSON_PATH = r"/root/data/train.json"
MODEL_NAME = r"/root/data/qwen3.5-0.8B-Instruct"
OUTPUT_DIR = r"/root/data/qwen_lora"
MAX_SEQ_LENGTH = 8192
# LoRA超参
LORA_R = 16
LORA_ALPHA = 16
LORA_DROPOUT = 0.0
# RTX4090‑24G 8192上下文 调参
BATCH_SIZE_PER_DEVICE = 2
GRADIENT_ACCUMULATION_STEPS = 4
MAX_STEPS = 40
LEARNING_RATE = 1e-4
WARMUP_STEPS = 10
# 显存优化
GRADIENT_CHECKPOINTING = True
# 4090+torch2.13优先bfloat16 比fp16更稳
USE_BF16 = True
def format_conversation(sample):
conv = sample["conversation"]
text = tokenizer.apply_chat_template(
conv,
tokenize=False,
add_generation_prompt=False
)
return {"text": text}
def tokenize_fn(sample):
out = tokenizer(
sample["text"],
truncation=True,
max_length=MAX_SEQ_LENGTH,
)
return out
def filter_long_sample(sample):
return len(sample["input_ids"]) < MAX_SEQ_LENGTH
if __name__ == "__main__":
print(f"PyTorch version: {torch.__version__}")
print(f"CUDA available: {torch.cuda.is_available()}")
print(f"CUDA version: {torch.version.cuda}")
print(f"GPU count: {torch.cuda.device_count()}")
if torch.cuda.is_available():
print(f"GPU Name: {torch.cuda.get_device_name(0)}")
print(f"Use bf16: {USE_BF16}")
# 路径校验
print(f"\n[CHECK] MODEL_NAME={MODEL_NAME}")
if not os.path.isdir(MODEL_NAME):
raise FileNotFoundError(f"模型文件夹不存在:{MODEL_NAME},检查真实路径!")
if not os.path.exists(os.path.join(MODEL_NAME, "config.json")):
raise FileNotFoundError(f"{MODEL_NAME} 缺少config.json,不是完整模型目录")
print("[CHECK] 模型目录校验通过")
if not os.path.exists(LOCAL_JSON_PATH):
raise FileNotFoundError(f"数据集文件不存在:{LOCAL_JSON_PATH}")
print("[CHECK] 数据集文件校验通过\n")
# 加载分词器
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
print("[+] 成功加载分词器", MODEL_NAME)
# 加载模型 单卡强制0号卡
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
dtype=torch.bfloat16 if USE_BF16 else torch.float16,
device_map={"": 0},
trust_remote_code=True,
)
for param in model.parameters():
param.requires_grad = False
print("[+] 主干模型加载完成 已冻结")
if GRADIENT_CHECKPOINTING:
model.gradient_checkpointing_enable()
model.enable_input_require_grads()
model.config.use_cache = False
print("[+] 开启梯度检查点 显存优化")
# LoRA配置
lora_config = LoraConfig(
r=LORA_R,
lora_alpha=LORA_ALPHA,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"
],
lora_dropout=LORA_DROPOUT,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 数据集加载
raw_ds = load_dataset("json", data_files=LOCAL_JSON_PATH, split="train")
print(f"原始数据集样本数量: {len(raw_ds)}")
raw_ds = raw_ds.map(format_conversation, num_proc=8)
raw_ds = raw_ds.map(tokenize_fn, num_proc=8)
raw_ds = raw_ds.filter(filter_long_sample)
raw_ds = raw_ds.select_columns(["input_ids", "attention_mask"])
raw_ds = raw_ds.shuffle(seed=SEED)
split_ds = raw_ds.train_test_split(test_size=0.05, seed=SEED)
train_ds = split_ds["train"]
eval_ds = split_ds["test"]
print(f"train:{len(train_ds)}, eval:{len(eval_ds)}")
print("数据集字段:", train_ds.column_names)
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=False
)
training_args = TrainingArguments(
output_dir=OUTPUT_DIR,
per_device_train_batch_size=BATCH_SIZE_PER_DEVICE,
per_device_eval_batch_size=1,
gradient_accumulation_steps=GRADIENT_ACCUMULATION_STEPS,
warmup_steps=WARMUP_STEPS,
max_steps=MAX_STEPS,
learning_rate=LEARNING_RATE,
bf16=USE_BF16,
fp16=not USE_BF16,
logging_steps=5,
optim="adamw_torch",
weight_decay=0.01,
lr_scheduler_type="linear",
seed=SEED,
report_to="none",
eval_strategy="steps",
eval_steps=20,
save_strategy="steps",
save_steps=20,
save_total_limit=3,
load_best_model_at_end=True,
prediction_loss_only=True,
push_to_hub=False,
save_only_model=True,
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_ds,
eval_dataset=eval_ds,
data_collator=data_collator,
)
print("[+] 开始训练......")
trainer.train()
trainer.save_model(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
print(f"训练完成,LoRA适配器输出路径:{OUTPUT_DIR}")
# 推理验证测试
base_model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
dtype=torch.bfloat16,
device_map={"": 0},
trust_remote_code=True
)
lora_model = PeftModel.from_pretrained(base_model, OUTPUT_DIR)
messages = [{"role":"user", "content":"你好"}]
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
outputs = lora_model.generate(**inputs, max_new_tokens=100)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))
训练结束输出产物:/root/data/qwen_lora,里面是 LoRA 适配器,体积很小(几十 MB)不是完整模型;包含adapter_model.safetensors、adapter_config.json以及 checkpoint 中间快照。
root@localhost:~/data# python qwen_lora.py
PyTorch version: 2.6.0+cu124
CUDA available: True
CUDA version: 12.4
GPU count: 1
GPU Name: NVIDIA GeForce RTX 4090
Use bf16: True
[CHECK] MODEL_NAME=/root/data/qwen3.5-0.8B-Instruct
[CHECK] 模型目录校验通过
[CHECK] 数据集文件校验通过
[+] 成功加载分词器 /root/data/qwen3.5-0.8B-Instruct
Loading weights: 100%|█████████████████████████████████████████| 320/320 [00:00<00:00, 1021.22it/s]
[+] 主干模型加载完成,已冻结
[+] 开启梯度检查点,显存优化
trainable params: 6,389,760 || all params: 758,782,784 || trainable%: 0.8421
原始数据集样本数量: 20
train:19, eval:1
数据集字段: ['input_ids', 'attention_mask']
[+] 开始训练......
{'loss': '3.747', 'grad_norm': '4.377', 'learning_rate': '4e-05', 'epoch': '1.8'}
{'loss': '3.275', 'grad_norm': '2.973', 'learning_rate': '9e-05', 'epoch': '3.4'}
{'loss': '2.823', 'grad_norm': '2.982', 'learning_rate': '8.667e-05', 'epoch': '5'}
{'loss': '2.292', 'grad_norm': '2.099', 'learning_rate': '7e-05', 'epoch': '6.8'}
{'eval_loss': '2.52', 'eval_runtime': '0.1862', 'eval_samples_per_second': '5.371', 'eval_steps_per_second': '5.371', 'epoch': '6.8'}
{'loss': '2.01', 'grad_norm': '2.022', 'learning_rate': '5.333e-05', 'epoch': '8.4'}
{'loss': '1.762', 'grad_norm': '3.386', 'learning_rate': '3.667e-05', 'epoch': '10'}
{'loss': '1.535', 'grad_norm': '2.145', 'learning_rate': '2e-05', 'epoch': '11.8'}
{'loss': '1.426', 'grad_norm': '2.224', 'learning_rate': '3.333e-06', 'epoch': '13.4'}
{'eval_loss': '2.717', 'eval_runtime': '0.1921', 'eval_samples_per_second': '5.204', 'eval_steps_per_second': '5.204', 'epoch': '13.4'}
{'train_runtime': '144.7', 'train_samples_per_second': '2.211', 'train_steps_per_second': '0.276', 'train_loss': '2.359', 'epoch': '13.4'}
100%|███████████████████████████████████████████████████| 40/40 [02:24<00:00, 3.62s/it]
训练完成,LoRA适配器输出路径:/root/data/qwen_lora
=====推理验证=====
Loading weights: 100%|██████████████████████████████████| 320/320 [00:00<00:00, 1011.21it/s]
user
你好
assistant
<think>
</think>
你好!有什么我可以帮你的吗?
root@localhost:~/data# cd qwen_lora/
root@localhost:~/data/qwen_lora# ll
total 44556
drwxr-xr-x 4 root root 4096 Sep 5 23:33 ./
drwxr-xr-x 5 root root 189 Sep 5 23:25 ../
-rw-r--r-- 1 root root 5218 Sep 5 23:33 README.md
-rw-r--r-- 1 root root 1164 Sep 5 23:33 adapter_config.json
-rw------- 1 root root 25584224 Sep 5 23:33 adapter_model.safetensors
-rw-r--r-- 1 root root 7755 Sep 5 23:33 chat_template.jinja
drwxr-xr-x 2 root root 4096 Sep 5 23:31 checkpoint-20/
drwxr-xr-x 2 root root 4096 Sep 5 23:33 checkpoint-40/
-rw-r--r-- 1 root root 19989325 Sep 5 23:33 tokenizer.json
-rw-r--r-- 1 root root 1124 Sep 5 23:33 tokenizer_config.json
-rw-r--r-- 1 root root 4792 Sep 5 23:33 training_args.bin
适配器合并完整权重
LoRA 适配器不能直接用于 llama.cpp、GGUF 导出;需要把 LoRA 权重合并进底座模型权重,输出完整 HF 格式模型。
脚本merge_lora.py将 LoRA 低秩矩阵和基础模型权重矩阵做矩阵相加,输出完整独立模型,不再依赖 peft 库;合并后可以直接 vLLM 推理,也可以转 GGUF。
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
BASE_MODEL_PATH = r"/root/data/qwen3.5-0.8B-Instruct"
LORA_PATH = r"/root/data/qwen_lora"
MERGED_OUT = r"/root/data/qwen3.5-0.8B-lora-merged"
print("加载基础模型...")
base = AutoModelForCausalLM.from_pretrained(
BASE_MODEL_PATH,
dtype=torch.bfloat16,
device_map={"": 0},
trust_remote_code=True
)
print("加载LoRA适配器并合并...")
model = PeftModel.from_pretrained(base, LORA_PATH)
merged_model = model.merge_and_unload()
print("保存合并后的完整模型")
merged_model.save_pretrained(MERGED_OUT, safe_serialization=True)
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_PATH, trust_remote_code=True)
tokenizer.save_pretrained(MERGED_OUT)
print(f"合并完成,输出目录:{MERGED_OUT}")
输出目录/root/data/qwen3.5‑0.8B‑lora‑merged
root@localhost:~/data# python lora‑merged.py
加载基础模型...
Loading weights: 100%|█████████████████████████████████████████████████| 320/320 [00:00<00:00, 918.63it/s]
加载LoRA适配器并合并...
保存合并后的完整模型
Writing model shards: 100%|██████████████████████████████████████████| 1/1 [00:01<00:00, 1.98s/it]
合并完成,输出目录:/root/data/qwen3.5-0.8B-lora-merged
root@localhost:~/data# cd qwen3.5-0.8B-Instruct/
root@localhost:~/data/qwen3.5-0.8B-Instruct# ls -lh
total 1.7G
-rw-r--r-- 1 root root 1.4K Sep 5 23:12 README.md
-rw-r--r-- 1 root root 2.9K Sep 5 23:12 config.json
-rw-r--r-- 1 root root 48 Sep 5 23:12 configuration.json
-rw-r--r-- 1 root root 1.7G Sep 5 23:17 model.safetensors
-rw-r--r-- 1 root root 336 Sep 5 23:12 preprocessor_config.json
-rw-r--r-- 1 root root 13M Sep 5 23:12 tokenizer.json
-rw-r--r-- 1 root root 17K Sep 5 23:12 tokenizer_config.json
root@localhost:~/data/qwen_lora# ls -lh
total 44M
-rw-r--r-- 1 root root 5.1K Sep 5 23:33 README.md
-rw-r--r-- 1 root root 1.2K Sep 5 23:33 adapter_config.json
-rw------- 1 root root 25M Sep 5 23:33 adapter_model.safetensors
-rw-r--r-- 1 root root 7.6K Sep 5 23:33 chat_template.jinja
drwxr-xr-x 2 root root 4.0K Sep 5 23:31 checkpoint-20
drwxr-xr-x 2 root root 4.0K Sep 5 23:33 checkpoint-40
-rw-r--r-- 1 root root 20M Sep 5 23:33 tokenizer.json
-rw-r--r-- 1 root root 1.1K Sep 5 23:33 tokenizer_config.json
-rw-r--r-- 1 root root 4.7K Sep 5 23:33 training_args.bin
root@localhost:~/data/qwen3.5-0.8B-lora-merged# ls -lh
total 1.5G
-rw-r--r-- 1 root root 7.6K Sep 5 23:37 chat_template.jinja
-rw-r--r-- 1 root root 1.8K Sep 5 23:37 config.json
-rw-r--r-- 1 root root 116 Sep 5 23:37 generation_config.json
-rw------- 1 root root 1.5G Sep 5 23:37 model.safetensors
-rw-r--r-- 1 root root 20M Sep 5 23:37 tokenizer.json
-rw-r--r-- 1 root root 1.1K Sep 5 23:37 tokenizer_config.json
完整模型推理测试
不依赖 peft 库,直接加载合并完成完整 HF 模型,验证对话生成效果;调用apply_chat_template构建 prompt,设置 temperature、top_p 采样参数做生成。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL_PATH = r"/root/data/qwen3.5-0.8B-lora-merged"
# 加载完整合并模型,不需要peft
model = AutoModelForCausalLM.from_pretrained(
MODEL_PATH,
dtype=torch.bfloat16,
device_map={"": 0},
trust_remote_code=True
)
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
def chat(prompt_text):
messages = [
{"role": "user", "content": prompt_text}
]
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
inputs = tokenizer(text, return_tensors="pt").to("cuda")
outputs = model.generate(
**inputs,
max_new_tokens=200,
temperature=0.7,
top_p=0.8,
do_sample=True
)
return tokenizer.decode(outputs[0], skip_special_tokens=True)
if __name__ == "__main__":
print("====测试1====")
res1 = chat("你好")
print(res1)
print("\n====测试2====")
res2 = chat("简单介绍一下人工智能")
print(res2)
运行后输出测试效果如下:
root@localhost:~/data# python test.py
Loading weights: 100%|██████████████████████████████████████████████| 320/320 [00:00<00:00, 1111.48it/s]
====测试1====
user
你好
assistant
<think>
</think>
你好!有什么可以帮你的吗?
====测试2====
user
简单介绍一下人工智能
assistant
<think>
</think>
人工智能是让计算机模仿人类智能,实现自主决策、学习和适应的能力。
底座和LoRA合并模型对比测试,同时加载原始底座模型、微调合并后的模型,输入同一个问题对比输出,直观确认微调是否生效。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
BASE = "/root/data/qwen3.5-0.8B-Instruct"
MERGED = "/root/data/qwen3.5-0.8B-lora-merged"
tokenizer = AutoTokenizer.from_pretrained(BASE, trust_remote_code=True)
def get_answer(model_path, question):
model = AutoModelForCausalLM.from_pretrained(
model_path,
dtype=torch.bfloat16,
device_map={"":0},
trust_remote_code=True
)
messages = [{"role":"user","content":question}]
prompt = tokenizer.apply_chat_template(messages,tokenize=False,add_generation_prompt=True)
inputs = tokenizer(prompt,return_tensors="pt").to("cuda")
out = model.generate(**inputs,max_new_tokens=150,temperature=0.7)
ans = tokenizer.decode(out[0],skip_special_tokens=True)
del model
torch.cuda.empty_cache()
return ans
q = "你好,什么是大模型LoRA微调?"
print("【原始底座回答】")
print(get_answer(BASE,q))
print("\n【LoRA合并后回答】")
print(get_answer(MERGED,q))
运行后输出测试效果如下:
现象:底座回答偏向通用百科;微调后的输出风格、话术会贴近训练集的回答内容。
root@localhost:~/data# python test.py
【原始底座回答】
Loading weights: 100%|████████████████████████████████████████████████| 320/320 [00:00<00:00, 867.20it/s]
[transformers] The following generation flags are not valid and may be ignored: ['temperature']. Set `TRANSFORMERS_VERBOSITY=info` for more details.
user
你好,什么是大模型LoRA微调?
assistant
<think>
</think>
你好!大模型(LLM)的 **LoRA(Low-Rank Adaptation)** 微调是一种高效、轻量级的模型微调技术,旨在在不显著影响模型整体性能的前提下,大幅降低计算成本。
简单来说,LoRA 的核心思想是:**只修改模型中“低秩”的部分,而保留“高秩”(即模型的核心能力)不变。**
以下是关于 LoRA 微调的详细解析:
### 1. 核心原理:低秩分解
LoRA 的精髓在于**低秩分解(Low-Rank Decomposition, LRD)**。
* **高秩矩阵**:代表模型中已经学习好的核心能力(如语言理解、逻辑推理、代码生成等
【LoRA合并后回答】
Loading weights: 100%|███████████████████████████████████████████████████| 320/320 [00:00<00:00, 1027.28it/s]
user
你好,什么是大模型LoRA微调?
assistant
<think>
</think>
LoRA是LoRA适配器,只加载少量权重,训练时只加载少量权重,训练速度更快,推理速度更快,适合微调大模型。
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

浙公网安备 33010602011771号