千问大模型二次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 包冲突。

bash 复制代码
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、激活进入虚拟环境,使用腾讯云镜像源加速深度学习、微调相关全套依赖包。

bash 复制代码
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

bash 复制代码
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中。

此处为小样本演示数据集,适合跑通流程;真实业务建议扩充到几百~几千条,太少容易过拟合。

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|>输出是否正常。

很多训练失败根源是数据集格式错误,训练前必须运行该脚本,全部校验通过再进入训练。

python 复制代码
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[+] 校验完成,可以送入训练脚本。")

检查通过后可看到如下所示输出内容:

bash 复制代码
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_tokenpadding_side=right

python 复制代码
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=rightapply_chat_template输出包含正确 im 标签。

bash 复制代码
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 做简单推理验证。

python 复制代码
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.safetensorsadapter_config.json以及 checkpoint 中间快照。

bash 复制代码
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。

python 复制代码
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

bash 复制代码
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 采样参数做生成。

python 复制代码
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)

运行后输出测试效果如下:

bash 复制代码
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合并模型对比测试,同时加载原始底座模型、微调合并后的模型,输入同一个问题对比输出,直观确认微调是否生效。

python 复制代码
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))

运行后输出测试效果如下:

现象:底座回答偏向通用百科;微调后的输出风格、话术会贴近训练集的回答内容。

bash 复制代码
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适配器,只加载少量权重,训练时只加载少量权重,训练速度更快,推理速度更快,适合微调大模型。
相关推荐
lyshark6 天前
轻量化小模型MiniMind从训练到落地指南
大模型应用技术实践
lyshark10 天前
Ubuntu 大模型HF转GGUF全流程实践指南
大模型应用技术实践·linux 系统运维技术实践
lyshark11 天前
LangChain 消息流输出与结构化处理
大模型应用技术实践
lyshark12 天前
Python 原生封装 Llama.cpp 大模型推理接口
大模型应用技术实践
lyshark13 天前
LangGraph Server Agent 框架本地部署指南
大模型应用技术实践
lyshark14 天前
LangChain 实现AdvancedRAG增强向量检索生成
大模型应用技术实践
lyshark15 天前
LangChain 实现NaiveRAG朴素向量检索生成
大模型应用技术实践
lyshark17 天前
LangGraph+PostgreSQL 会话记忆持久化存储
大模型应用技术实践
lyshark18 天前
LangChain+FastMCP 搭建大模型工具调用服务
大模型应用技术实践