千问大模型二次LoRA‑SFT指令微调指南

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

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

logo_qwen.jpg

在开展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_tokenpadding_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=rightapply_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.safetensorsadapter_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适配器,只加载少量权重,训练时只加载少量权重,训练速度更快,推理速度更快,适合微调大模型。
目录
相关文章
|
2天前
|
人工智能 自然语言处理 安全
阿里云AI数智鉴密:AI 生成内容如何拿到一张"防篡改的身份证"
隐形水印 + C2PA签名:让AI生成内容“持证上岗”。
1093 0
|
11天前
|
人工智能 自然语言处理 安全
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
本文聚焦阿里云2026年推出的三款自研AI办公产品,清晰拆解千问办公、Qoder Teams、Qoder CN的差异化定位与能力边界:千问办公主打职场全场景提效,支持自然语言指令一键完成PPT生成、数据分析等高频办公任务;Qoder Teams面向程序员团队,深度整合AI代码生成、团队协同与企业知识库能力;Qoder CN则专为金融、政务等强合规场景打造,实现数据不出境与VPC私有化部署。文章同步给出分场景选型指南与最新活动定价,帮助不同类型的企业按需组合产品,实现业务岗、研发岗与强合规场景的AI能力全覆盖。
3659 3
阿里云千问办公、Qoder Teams、Qoder CN区别与选择指南:模型能力、适用场景与最新活动参考
|
23天前
|
人工智能 缓存 前端开发
DeepSeek Harness 首发实测 + 入门教程,夯爆了!梁神我错了
DeepSeek Harness + DeepSeek V4 Pro 项目实战保姆级教程!手把手带你从零安装开源 AI 编程工具,开发架构图、知识讲解网站、3D 网页游戏、全栈 AI 应用 4 个项目,覆盖运行模式选择、插件安装与开发,看看能不能对标 Claude。
13401 93
DeepSeek Harness 首发实测 + 入门教程,夯爆了!梁神我错了
|
16天前
|
Web App开发 人工智能 API
16 个超火的 DeepSeek Harness 插件,大肥鱼已经落后 N 个版本了。。。
DeepSeek Harness 精选插件推荐合集,从图片识别、浏览器操控、多 Agent 协作到手机远程控制,一口气带你看完 DSH 社区热门的十几个插件,覆盖技能扩展、UI 界面增强、整活玩法三大类,让你的鲸鱼变得更强。
1913 5
|
9天前
|
人工智能 监控 测试技术
Qwen3.8-Flash 来了,100万上下文、Agent、Coding 都加强了
8月26日,通义千问发布Qwen3.8-Flash-Next:125B参数、每Token仅激活6B,原生支持26万Token、可扩展至100万上下文;Coding、Agent与工具调用能力显著增强,面向真实软件工程任务,推动大模型从“回答问题”迈向“完成工作”。
|
12天前
|
人工智能 Linux iOS开发
Ollama使用教程:Ollama官网下载、Ollama本地部署大模型(2026最新)
Ollama 是一款免费开源的本地大模型运行工具,支持在 Windows/macOS/Linux 上离线运行 Qwen、DeepSeek、Llama 等主流开源模型,数据不出本机、隐私安全。提供 OpenAI 兼容 API,命令行一键拉取/运行/管理模型,无需联网,无调用限制,是开发者与 AI 爱好者部署本地 AI 助手的理想选择。(239 字)
|
17天前
|
人工智能 Java BI
【AI】DeepSeek Harness 安装、运行、管理插件
本文介绍了如何运行DeepSeek开源的Agent框架DeepSeek Harness(dsh)。主要内容包括:使用nvm安装适配的Node版本;通过代理加速克隆GitHub源码;使用pnpm安装依赖并启动项目;配置DeepSeek API Token;安装扩展功能的插件。该框架自带Web界面,支持模型适配、文件编辑等插件化功能
2156 1

热门文章

最新文章