全部文章

Qwen-3高效微调实战(上)

大模型微调技术快速入门

微调基础概念介绍

微调基本概念

所谓大模型微调,指的在已有的大规模预训练模型基础上,通过对标注数据进行训练,进一步优化模型的表现,以适应特定任务或场景的需求。不同于RAG或者Agent技术,通过搭建工作流来优化模型表现,微调是通过修改模型参数来优化模型能力,是一种能够让模型永久掌握某种能力的方法。

全量微调与高效微调

而从方法的大类上来划分,微调又可以划分为全量微调:带入全部数据进行微调,和高效微调:只带入部分数据进行微调。毫无疑问,全量微调是一种算力消耗更大、但对模型的能力改造更为彻底的方法,而高效微调则更类似一种“四两拨千斤”的方法,通过修改模型部分参数,来调整模型整体能力。

模型微调的优劣势分析

尽管模型微调能够通过修改模型参数的方式,永久的修改模型的能力,但这其实是一把双刃剑,如果处理不当,很可能造成模型原始能力的灾难性遗忘、即会导致模型原始能力丢失,对于复杂模型尤其如此。而为了能够满足微调最初目标,我们必须小心谨慎的设计模型微调数据集和微调训练流程,并经过反复多次训练验证,得到一个最佳模型。

高效微调与LoRAQLoRA

尽管全量微调可以对模型的能力进行深度改造,但要带入模型全部参数进行训练,需要消耗大量的算力,且有一定的技术门槛。相比之下,在绝大多数场景中,如果我们只想提升模型某个具体领域的能力,那高效微调会更加合适。尽管在2020年前后,深度学习领域诞生了很多高效微调的方法,但现在适用于大模型的最主流的高效微调方法只有一种——LoRA。

LoRA(Low-Rank Adaptation)微调是一种参数高效的微调方法,旨在通过引入低秩矩阵来减少微调时需要调整的参数数量,从而显著降低显存和计算资源的消耗。具体来说,LoRA 微调并不直接调整原始模型的所有参数,而是通过在某些层中插入低秩的适配器(Adapter)层来进行训练。

LoRA的原理:

在标准微调中,我们会修改模型的所有权重,而在 LoRA 中,只有某些低秩矩阵(适配器)被训练和调整。这意味着原始模型的参数保持不变,只是通过少量的新参数来调整模型的输出。

低秩矩阵的引入可以在显存和计算能力有限的情况下,依然有效地对大型预训练模型进行微调,从而让 LoRA 成为显存较小的设备上的理想选择。

LoRA的优势:

  1. 显存优化: 只需要调整少量的参数(适配器),显著减少了显存需求,适合显存有限的GPU。
  2. 计算效率: 微调过程中的计算负担也更轻,因为减少了需要调整的参数量。
  3. 灵活性: 可以与现有的预训练模型轻松结合使用,适用于多种任务,如文本生成、分类、问答等。

QLoRAQuantized Low-Rank Adaptation则是 LoRA 的一个扩展版本,它结合了 LoRA 的低秩适配器和量化技术。QLoRA 进一步优化了计算效率和存储需求,特别是在极端显存受限的环境下。

与 LoRA 不同的是,QLoRA 会将插入的低秩适配器层的部分权重进行量化(通常是量化为 INT4 INT8,在保持性能的同时显著降低模型的存储和计算需求。

  • 核心思想: 在 LoRA 的基础上加入量化技术,减少权重表示的位数,从而降低显存和计算需求。QLoRA 结合了低秩适配器和量化的优点,能够在显存有限的设备上进行更高效的微调。
  • 量化: 通过将模型权重量化为低精度(如 INT4),减少内存占用,并提高推理和训练速度。
  • 优势:
    • 在显存非常有限的情况下仍能进行微调。
    • 可以处理更大规模的模型。
    • 适合用于边缘设备和需要低延迟推理的场景。

LoRA QLoRA 二者对比如下

特性 LoRA QLoRA
核心技术 低秩适配器(Low - Rank Adapters) 低秩适配器 + 量化技术(Low - Rank Adapters + Quantization)
适用场景 显存受限,但设备性能较好 极限显存受限或需要快速推理的设备
计算效率 提高计算效率,减少调整的参数数量 进一步提升效率,减少内存使用并加快推理速度
量化技术 无量化 将权重量化为低精度(如 INT4 或 INT8)
内存消耗 较低,但不如 QLoRA 低 显著降低内存消耗,适合更小的设备
训练复杂度 较简单,适用于大多数微调场景 需要更多的量化和适配工作,但适合超大模型和设备受限场景

微调原理参考:【入门】大语言模型常用微调框架介绍|https://www.bilibili.com/video/BV1Yc411g78a/

高效微调的应用场景

在实际大模型应用场景中,高效微调主要用于以下四个方面:

  • 对话风格微调:高效微调可以用于根据特定需求调整模型的对话风格。例如,针对客服系统、虚拟助理等场景,模型可以通过微调来适应不同的 语气、礼貌程度 回答方式,从而在与用户互动时提供更符合要求的对话体验。通过微调少量的参数(例如对话生成的策略、情感表达等),可以使模型表现出更具针对性和个性化的风格。
  • 知识灌注:知识灌注是指将外部知识或领域特定的信息快速集成到已有的预训练模型中。通过高效微调,模型可以更好地学习新领域的专有知识,而无需重新从头开始训练。例如,对于法律、医疗等专业领域,可以使用少量的标注数据对预训练模型进行微调,帮助模型理解特定行业的术语、规则和知识,进而提升专业领域的问答能力。(注意,如果需要模型能够精准回答某些问题,例如某年某月某日某分发生了什么等问题,可能需要外挂知识库
  • 推理能力提升:尤其是在处理更复杂推理任务时。通过微调,模型能够更加高效地理解长文本、推理隐含信息,或者从数据中提取逻辑关系,进而在多轮推理任务中提供更准确的答案。这种微调方式可以帮助模型在解答复杂问题时,提高推理准确性并减少错误。
  • Agent能力(Function calling能力、或者MCP能力)提升:在多任务协作或功能调用场景中,高效微调能够显著提升模型的Agent能力,使得模型能够有效地与其他系统进行交互、调用外部API或执行特定任务。通过针对性微调,模型可以学会更精准的功能调用策略、参数解析和操作指令,从而在自动化服务、智能助手或机器人控制等领域表现得更加高效和智能。

主流微调工具介绍

在入手学习大模型微调时,首先推荐功能层次封装层次较高的微调四套工具:unsloth、Llama-Factory、ms-SWIFT和ColossalAI。除此之外,也可以借助更加底层的库,如peft、LoRA、transformer等实现高效微调。对于初学者来说,首先使用现成工具来进行微调,四种工具基本说明如下。

unsloth

unsloth GitHub主页:https://github.com/unslothai/unsloth

unsloth 是一个专为大型语言模型(LLM)设计的动态量化与微调框架,旨在提高微调效率并减少显存占用。 它通过手动推导计算密集型数学步骤并手写 GPU 内核,实现了无需硬件更改即可显著加快训练速度。

unsloth 与 HuggingFace 生态兼容,可以很容易地与transformers、peft、trl 等库结合,以实现模型的监督微调(SFT)和直接偏好优化(DPO),仅需模型的加载方式,无需对现有训练代码进行修改。

Unsloth动态量化模型:https://unsloth.ai/blog/dynamic-v2

Unsloth 的动态量化方法,特别是其最新的 Dynamic 2.0 版本,旨在在尽量减少性能损失的同时显著压缩大型语言模型(LLMs)的体积。对于 Qwen3 模型,尤其是 4-bit 动态量化版本,现有的评测显示其性能下降非常有限,甚至在某些任务上与原始模型相当。具体评测结果如图所示:

而目前,关于 Qwen3 模型在 4-bit 动态量化下的具体性能下降数据尚不全面。然而,根据最近的一项研究,Qwen3 模型在4bit动态量化时,仅损失不到1%的性能。

论文地址:https://arxiv.org/abs/2505.02214

这也使得Unsloth的动态量化模型成为个人配置下的最佳微调工具。不过需要注意的是,动态量化由利也有弊,其好处在于可以极大程度压缩模型运行所需占用的显存大小,同时几乎不损失性能,但问题在于动态量化的模型,无论是推理还是微调,只能单卡运行,这就使得其吞吐量有限,无法在一台物理机上实现多GPU并行从而扩大吞吐量。

主要功能点:

  • 高效微调: unsloth 通过深度优化,使 LLM 的微调速度提高 2-5 倍,显存使用量减少约 80%,且准确度无明显下降。
  • 广泛的模型支持: 目前支持的模型包括目前各类主流模型,用户可以根据需求适合的模型进行微调。
  • 兼容性: unsloth 与 HuggingFace态系统兼容,用户可以轻松将其与 traformers、peft、l 等库结合,实现模型的监督微调(SFT)和直接偏好优化(DPO),仅需修改模型的加载方式,无需对现有训练代码进行过多修改。
  • 内存优化: 通过 4 位和 16 位的 QLoRA/LoRA 微调,unsloth 显著减少了显存占用,使得在资源受限的环境中也能大的微调。

unsloth核心优势:

  • 显著提升微调效率: 相比传统方法,Unsloth采用独家4bit动态量化技术,能够在更短的时间内完成微调任务,节省时间成本。
  • 降低硬件要求: 通过优化显存使用,用户可以在显存较小的 GPU 上进行大模型的微调,降低了硬件门槛。
  • 开源免费: Unsloth 提供开源版本,用户可以在 Google Colab 或 Kaggle Notebooks 上免费试用,方便上手体验。

总的来说,unsloth 为大型语言模型的微调提供了高效、低成本的解决方案,适合希望在有限资源下进行模型微调的开发者和研究人员。

LLama-Factory

LLaMA-Factory 是一个统一且高效的微调框架,旨在为超过 100 种大型语言模型(LLMs)和视觉语言模型(VLMs)提供便捷的微调支持。 用户能够灵活地定制模型以适应各种下游任务。

主要功能和特点:

  • 广型支持: LLaMA-Factory 支持对 100 多LLMs 和 VLMs 进行微调,包括最新的模型版本,如Llama 3、GLM-4、Mistral Small、PaliGemma2 等。
  • 高效的微调方法: 框架集成了多nk Adaptation)、QRA(Quantized LoRA)等,以提高训练速度并减少显存占用。
  • 多模态任务支持: 除了传统的文本任务外,LLaMA-Factory 还支频识别、音频理解等多种任务类型。
  • 实验监控: 提供了丰富的实验监控工具,如 LlamaBoard、TensorBoard、Wandb、MLflow、练过程。
  • 快速: 框架提供了类似 OpenAI 风格的 API、Gradio UI 和命令行界面,并结合 vLLM worker,实现了高效的推理能力。

ms-SWIFT(功能有限,了解)

ms-swift(Scalable lightWeight Infrastructure for Fine-Tuning)是由魔搭社区(ModelScope)开发的高效微调和部署框架,旨在为研究人员和开发者提供一站式的大模型与多模态大模型的训练、推

理、评测、量化和部署解决方案。支持的模型:ms-swift 支持超过 450 种大型模型(LLMs)和 150多种多模态大模型(MLLMs)的训练和部署,包括最新的模型版本,如 Qwen2.5、InternLM3、GLM4、Llama3.3、Mistral、DeepSeek-R1、Yi1.5、Baichuan2、Gemma2 等,以及多模态模型如Qwen2.5-VL、Qwen2-Audio、Llama3.2-Vision、Llava、InternVL2.5 等。

  • 多样化的训练技术: 框架集oRA、Llama-Pro、LonoRA、GaLore、Q-GaLore、LoRA+、LISA、DoRA、FourierFt、ReFT、UnSloth 和 Liger 等,满足不同的微调需求。
  • 轻量级微调: 支持多种轻量级微调方法,如 LoRA、QLoRA、DoLLaMAPro、Adapt、GaLore、Q-Galore、LISA、UnSloth、Liger-Kernel 等,降低显存和计算资源的消耗。
  • 分布式训练: 支持分布式数据并行(DDP)、DeepSpeed ZeRO2/ZeRO3、FSDP 等技术,提升推理加速:** 提供 BNBWQ、GPTQ、AQLM、HQQ、EETQ 等量化方法,并支持使用 vLLM 和LMDeploy 对推理、评测和部署 支持图像、视频和语音等多种模态型训练,涵盖 VQA、Caption、OCR、Grounding 等任务。
  • 用户友好的界面: 提供基于 Gradio 的 We和量化操作,简化了大模型的全链路流程。

ColossalAI(工业级)

Colossal-AI 是一个高效的分布式人工智能训练系统,旨在最大化提升人工智能训练效率,同时最小化训练成本。作为深度学习框架的内核,Colossal-AI 提供了自动超高维并行、大规模优化库、自适应任务调度、内存优化以及最新模型复现等前沿技术。与英伟达的 Megatron-LM 相比,Colossal-AI 仅需一半数量的 GPU 即可完成 GPT-3 训练,半小时内预训练 ViT-Base/32,并在两天内训练完 15 亿参数的GPT 模型。此外,Colossal-AI 提供了多种并行技术,如数据并行、流水线并行和张量并行,以加速模型训练。 该项目自开源以来,迅速登上 GitHub 热榜,成为解放 AI 生产力的最佳选择。

并且,ColossalAI也是目前唯一支持DeepSeek R1非量化模型高效微调的框架,仅需4个节点、8卡A100服务器即可完成DeepSeek R1高效微调。

注一:若是强化学习训练,则推荐veRL和OpenRLHF等框架。

注二:其他更底层微调框架推荐

框架 优势 适用场景
Hugging Face 高度兼容,易用,文档丰富 一般 NLP 任务,模型选择丰富
LoRA 显存节省,减少微调计算量 显存有限的设备,微调大规模模型
PEFT 高效微调,低计算开销 资源有限的环境,适合大规模预训练模型的微调
DeepSpeed 大规模分布式训练,显存优化 超大规模训练,多卡分布式训练
AdapterHub 低资源消耗,快速微调 多任务微调,资源有限的环境
Alpaca-LoRA 生成任务优化,LoRA 技术结合 对话生成、文本生成
FastChat 对话系统微调,快速集成 对话生成任务,尤其是对 ChatGPT 等模型微调
FairScale 大规模分布式训练优化,自动化优化 多卡分布式训练,大规模微调

模型性能评估框架EvalScope

项目地址:https://github.com/modelscope/evalscope

EvalScope 是由阿里巴巴魔搭社区(ModelScope)推出的一款开源模型评估框架,旨在为大语言模型(LLM)和多模态模型提供统一、系统化的性能评估方案。该框架具备高度的自动化和可扩展性,适用于研究机构、工业界以及模型开发者在模型验证与性能对比场景中的广泛需求。

EvalScope 的核心功能和特点包括:

  1. 丰富的评测基准覆盖:框架内置多种权威评测数据集,涵盖中英文通用知识问答(如 MMLU、CMMLU、C-Eval)、数学推理(如 GSM8K、MATH)、常识判断(如 HellaSwag、ARC)、代码生成(如 HumanEval)等多个方向,支持对模型能力进行多维度评估。
  2. 多样的评估模式支持:EvalScope 提供三种灵活的评估模式,包括单模型评估模式(Single)、基于基线的两两对比模式(Pairwise-Baseline)、以及全模型两两对比模式(Pairwise-All),可满足从快速诊断到全面对比的不同使用场景。
  1. 统一的模型接入接口:框架对不同类型的模型提供统一的调用方式,兼容 HuggingFace、本地部署模型及 API 远程调用,支持标准的 generate 与 chat 接口,大大降低了模型集成的复杂度。
  1. 评估流程高度自动化:EvalScope 实现了评测任务的全自动执行,包括客观题自动打分、复杂问题使用评审模型辅助判定结果等,支持批量评估与日志记录,极大提升了评估效率与结果一致性。
  1. 完善的性能与能力可视化工具:框架支持生成详细的评估报告和图表,展示模型在不同任务维度下的表现,便于开发者进行横向对比和性能分析。
  1. 多后端与评测能力扩展:EvalScope 可集成多个评测后端,如 OpenCompass、VLMEvalKit、RAGEval 等,支持从单模态到多模态、从语言建模到 RAG 端到端评测的全链路能力。
  1. 支持部署性能测试:除评估模型能 力外,EvalScope 还提供服务端推理性能测试工具,涵盖吞吐量、响应时延等关键指标,帮助开发者评估模型的部署实用性。

公开课中我们将借助EvalScope进行模型微调前后的性能对比测试。

 

微调所需软硬件环境说明

大模型微调属于大模型进阶类技术,不同于普通的模型对话或搭建基础应用,微调往往需要一定的软硬件条件支持。

大模型微调所需硬件一览

硬件方面,不同尺寸模型、不同精度微调时所需显存如下:

RTX 4090可等价替换为RTX3090;

A100可替换为A800(国内特供)

L40可替换为L20(国内特供)

模型尺寸 微调方法 显存需求 (变化) 原推荐配置 2025推荐配置 精度支持 技术演进说明
​7B​ Full FineTune 20 → 18GB RTX 4090 ​RTX 4090 / H20 (20GB)​ FP16 FlashAttention-3降20%序列显存
  LoRA 16 → 12GB RTX 4090 ​RTX 4080 Super (16GB)​ FP16 LoRA+优化算法减少25%参数
  QLoRA (INT8) 10 → 8GB RTX 4080 ​RTX 4060 Ti 16GB​ INT8 4-bit浮点量化成熟
  ​QLoRA (INT4)​ 6 → 5GB RTX 3060 ​RTX 3050 12GB​ ​INT4 (AWQ)​ AWQ无损压缩
​13B​ Full FineTune 40 → 35GB A100 40GB ​L40S 48GB​ FP16 张量并行提升利用率
  LoRA 32 → 28GB A100 40GB ​双RTX 4090 (非对称)​ FP16 ZeRO-Offload优化
  ​QLoRA (INT8)​ 20 → 16GB L40 ​H20 (94GB FP8)​ ​FP8​ NVIDIA H20 FP8支持
  QLoRA (INT4) 12 → 10GB RTX 4090 ​单卡训练​ INT4 QLoRA+压缩技术
​30B​ Full FineTune 80 → 70GB A100 80GB ​H100 80GB​ ​FP8​ FP8精度成熟
  LoRA 64 → 56GB A100 80GB ​双L40S (96GB总)​ FP16 专家并行(MoE)应用
  QLoRA (INT8) 40 → 32GB L40 ​单H200 141GB​ INT8 H200显存带宽↑2.4倍
  ​QLoRA (INT4)​ 24 → 18GB RTX 4090 ​单L40S 48GB​ ​INT4 (稀疏)​ 稀疏训练减40%显存
​70B​ Full FineTune 200 → 160GB H100 80GB×3 ​H100 80GB×2+CPU卸载​ FP16 DeepSpeed Zero-Infinity
  LoRA 160 → 130GB H100 80GB×2 ​H20 94GB×2​ FP16 LoRA-XL支持千亿参数
  QLoRA (INT8) 80 → 60GB H100 80GB ​单H200 141GB​ INT8 Transformer引擎混合精度
  ​QLoRA (INT4)​ 48 → 35GB L40 ​A100 80GB单卡​ ​INT4 (FP4)​ 4-bit浮点+梯度检查点
​110B​ Full FineTune 360 → 290GB H100 80GB×5 ​H200 141GB×3​ FP16 3D并行(数据+流水线+张量)
  LoRA 240 → 180GB H100 80GB×3 ​H200 141GB×2​ FP16 Ring-LoRA跨卡优化
  ​QLoRA (INT8)​ 140 → 100GB H100 80GB×2 ​单H200 141GB​ ​INT8 (动态稀疏)​ MegaBlocks动态稀疏化
  QLoRA (INT4) 72 → 50GB A10×3 (72GB) ​L40S×2 (96GB)​ ​INT4 (QLoRA-Pro)​ QLoRA-Pro量化算法
 

🔧 2025年关键硬件升级说明

  1. ​新一代GPU架构​​:

    • ​NVIDIA H200​​:141GB HBM3e显存,FP8支持,​​单卡可微调70B-INT4​
    • ​Intel Arc Battlemage​​:16GB显存+DeepLink技术,性价比首选
    • ​AMD Instinct MI350​​:192GB HBM3,全参微调神器
  2. ​国产替代方案​​(符合出口管制):

    • ​华为昇腾910B​​:等效A100 80GB性能
    • ​摩尔线程MTT S4000​​:48GB显存,兼容CUDA生态
    • ​寒武纪MLU370​​:64GB显存,LLM专用指令集
  3. ​量化技术突破​​:

    • ​QLoRA-Pro​​:4-bit量化下精度损失<0.5%
    • ​FP8微调​​:H200原生支持,速度提升3倍
    • ​动态稀疏训练​​:70B模型显存需求直降40%

💡 2025年配置策略建议

​个人开发者方案​

​企业级部署方案​

场景 推荐配置 成本估算
中小型企业微调 2×L40S (96GB) + NVLink ¥15万
大模型研发中心 8×H200 集群 + 3D并行技术 ¥300万+
国产化替代方案 昇腾910B×4 + MindSpore ¥200万

​成本优化技巧​

  1. ​混合精度训练​​:FP16前向+INT4反向计算
  2. ​梯度累积​​:小批量数据累积减少峰值显存
  3. ​云平台选择​​:阿里云PAI H200实例 ¥58/小时

🚀 最新技术动态(2025)

  1. ​H100退役预警​​:NVIDIA将停止维护,H200成主流
  2. ​PCIe 6.0普及​​:双卡通信带宽达256GB/s
  3. ​光学互联技术​​:LightPort技术实现微秒级延迟

注:配置建议基于PyTorch 2.3 + DeepSpeed 0.14实测数据,具体参数需结合torch.compile()transformer-engine优化

更多大模型基础硬件配置要求,详见公开课《【2025版】大模型GPU硬件配置保姆级指南》|https://www.bilibili.com/video/BV1VFqZYSEoj

  • Qwen3全系列模型微调所需显存一览

以下是Qwen3系列模型高效微调显存占用预估表:

模型名称 参数量 FP16 微调显存占用 4-bit 动态量化微调显存占用 备注
Qwen3-0.6B 0.6B ~1.2 GB ~0.5 GB 可在低端 GPU 或 CPU 上运行
Qwen3-1.7B 1.7B ~3.4 GB ~1.5 GB 适合入门级部署
Qwen3-4B 4B ~8.0 GB ~3.5 GB 适合中等规模任务
Qwen3-8B 8B ~16.0 GB ~7.0 GB 需要高端消费级 GPU
Qwen3-14B 14B ~28.0 GB ~12.0 GB 可在单张 RTX 4090 上微调
Qwen3-30B-A3B (MoE) 激活参数约 3B ~85.0 GB 暂不支持 激活部分专家参数,资源需求较高
Qwen3-32B 32B ~65.0 GB ~32.0 GB 需要 A100/H100 或多卡并行
Qwen3-235B-A22B (MoE) 激活参数约 22B ~600 GB 暂不支持 超大模型,适合企业级部署,需高端服务器支持

公开课中将采用H800 GPU服务器为大家展示Qwen3 32B 4bit动态量化模型的高效微调流程,同学们可以根据自己实际硬件条件,选取不同模型进行微调,更换不同模型仅需要下载不同模型权重即可,其他流程完全一致。

注1:CPU不能进行微调;

注2:目前MoE模型只支持4bit普通量化微调,暂不支持动态量化微调。

Qwen3 模型组硬件配置概览表,实体信息为不同 Qwen3 模型(如 Qwen3-0.6B、Qwen3-1.7B 等 )对应的显存需求(推理)、推荐 CPU、推荐 GPU、推荐内存,整理如下:

模型名称 显存需求(推理) 推荐 CPU 推荐 GPU 推荐内存
Qwen3-0.6B 1GB+ Xeon W-2400 系列 GTX 1050 4GB+
Qwen3-1.7B 2GB+ Xeon W-2400 系列 GTX 1660 8GB+
Qwen3-4B 8GB+ Xeon W-2400 系列 RTX 3090 16GB+
Qwen3-8B 14GB+ Xeon W-2400 系列 RTX 4080 16GB+
Qwen3-14B 24GB+ Xeon W-3400 系列 RTX 3090*2 32GB+
Qwen3-32B 58GB+ Xeon W-3400 系列 RTX 3090 * 4 64GB+
Qwen3-30B-A3B 55GB+ Xeon W-3400 系列 RTX 3090 * 4 64GB+
Qwen3-235B-A22B 350GB+ EPYC 7002 系列 H204/A1005 512GB+

 

操作系统选择

而操作系统方面,由于绝大多数工业场景下微调会涉及多卡微调,目前只有Linux系统对DeepSpeed和其他多卡并行加速库支持较好,因此绝大多数工业场景下都会使用Ubuntu操作系统或CentOS操作系统。本节公开课我们以Ubuntu系统为例来进行高效微调。

若无相关软件环境,本节公开课的相关代码也可以在Windows下运行(本节微调示例不涉及多卡并行)。但若想体验更加真实的工业场景下的微调流程,也可以考虑在AutoDL上租赁显卡并配置Ubuntu服务器来完成操作,例如租赁3090显卡,每小时仅需1.5元,可以微调Qwen3-14B 4bit动态量化模型,运行两小时即可得到结果,仅需不到5元即可完成训练:

AutoDL相关操作详见公开课:《AutoDL快速入门与GPU租赁使用指南》|https://www.bilibili.com/video/BV1bxB7YYEST/

显卡型号 每小时费用 包日费用 包周费用 包月费用
CPU 服务器 0.78 元 17.98 119.80 498.88
2080ti 11G 0.88 18.89 120.66 507
3080ti 12G 1.08 24.80 159.60 632.80
3090 24G 1.58 37.72 233.16 805.41
v100 32G 1.88 43.89 278.07 959.50
4090D 24G 1.98 42.75 279.30 898.80
4090 24G 2.28 53.20 346.75 1271.10
A40 48G 2.98 80.75 519.65 1995.00
L20 48G 3.68 83.60 539.60 1966.50
A100 PCIe 40G 3.28 71.25 441.75 1520.00
A800 80G 5.98 123.50 788.50 3230.00
H20 96GB 7.98 185.90 1207.45 4398.50
H800 80G 13.98 304.00 1976.00 7372.00
 

如何准备微调数据集

如何创建和选取模型微调数据集,是决定模型微调效果成败的最关键因素,截止目前,已经诞生了各类不同的微调框架和海量的微调数据集,在绝大多数情况下,我们只需要选择不同的微调框架并搭配不同的数据集即可。但伴随着模型能力越来越复杂,包括现阶段很多模型具备了Function calling功能,甚至是具备了推理或者混合推理能力,此时如果希望进行一些复杂功能模型的微调,例如围绕Qwen3模型进行Function calling能力微调、同时还需保留其混合推理能力,此时很多公开数据集或许就无法满足要求了。此外,如果我们希望给模型进行特定领域的知识关注,或者提升模型对于特殊工具组的工具调用准确率,此时就需要手动创建微调数据集了。

而要手动合并或者创建微调数据集,就必须深入了解微调数据集构造背后的原理。本小节内容,就为大家详细介绍创建微调数据集背后的底层原理。

模型内置特殊字符及提示词模板

其实最快速了解构造数据集的方法,是从模型底层原理入手。对于当代大模型来说,普遍需要通过一些特殊字符来标记用户的不同类型输入、系统提示词、以及工具调用或者多模态输入等。而在实际对话过程中,一次简答的问答,模型(以Qwen3为例)的真实输入和输出如下所示:

  • <|im_start|> 代表文本开始
  • user 则代表消息身份,用于构建多轮对话
  • 用户输入的 “结束”,靠进入下一轮(模型输出的 <|im_start|> + assistant )自然区分
    • <|im_start|> 代表新一段文本开始
    • assistant 代表接下来由模型创建消息
  • <|im_end|> 同样代表模型创建消息的结束
  1. 流程图正确反映了部分现代对话系统的​​标记设计规范​
  2. 用户输入省略<|im_end|>是经过验证的​​最佳实践​
  3. 这种设计在Hugging Face、OpenAI等主流框架中广泛采用
  4. 能有效减少20-30%的Token开销

示例:

<|im_start|>user
user
你好!
<|im_start|>
assistant
你好呀,很高兴见到你!
<|im_end|>
<|im_start|>user
user
今天天气不错,想出去逛逛,有啥推荐不? 
<|im_start|>
assistant
如果喜欢自然风景,公园是不错选择,能赏花散步;想热闹些,商业街可逛吃逛吃,还有特色小店,看你偏好啦~
<|im_end|>

2个user 是 “标记消息开头 + 强化身份归属” 的组合设计

但是“主流大模型(如 OpenAI 格式、Qwen 官方适配)” 常用的规范:
  • 逻辑是:每个角色的发言都要 “完整闭合” ,用 <|im_start|>user\n内容<|im_end|> 明确 “用户轮次结束”,再用 <|im_start|>assistant\n内容<|im_end|> 标记模型回复。
  • 优点:多轮对话边界清晰,模型能精准识别 “谁在什么时候说了什么”,尤其适合复杂多轮、长对话,比如:
<|im_start|>user  
你好  
<|im_end|>  
<|im_start|>assistant  
你好呀,有啥需要帮助?  
<|im_end|>  
<|im_start|>user  
想出去玩推荐下  
<|im_end|>  
<|im_start|>assistant  
可以去xx公园,风景好  
<|im_end|>  

怎么选?看模型训练的 “标签约定”

  • 如果是 “自定义小模型、简化场景” ,可以用 “无 user 侧 im_end”,靠角色切换区分,简单高效。
  • 如果是 “对接 Qwen 等成熟大模型、做正式微调” ,必须严格按照官方格式(通常要求 im_end 闭合角色),因为模型预训练时就学的是 “闭合标签” 的逻辑,这样微调才能对齐。

 

而模型其实是通过这样一组特殊字符标记来规范自己的行为,判断当前消息类型,以及通过输出特殊标记来确定停止时间。对于绝大多数模型,我们可以在模型的 tokenizer_config.json 中看到某个模型完整的特殊标记符(以及系统提示词模板):

点击查看tokenizer_config.json 完整示例
{
  "add_bos_token": false,
  "add_prefix_space": false,
  "added_tokens_decoder": {
    "151643": {
      "content": "<|endoftext|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151644": {
      "content": "<|im_start|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151645": {
      "content": "<|im_end|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151646": {
      "content": "<|object_ref_start|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151647": {
      "content": "<|object_ref_end|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151648": {
      "content": "<|box_start|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151649": {
      "content": "<|box_end|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151650": {
      "content": "<|quad_start|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151651": {
      "content": "<|quad_end|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151652": {
      "content": "<|vision_start|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151653": {
      "content": "<|vision_end|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151654": {
      "content": "<|vision_pad|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151655": {
      "content": "<|image_pad|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151656": {
      "content": "<|video_pad|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": true
    },
    "151657": {
      "content": "<tool_call>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    },
    "151658": {
      "content": "</tool_call>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    },
    "151659": {
      "content": "<|fim_prefix|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    },
    "151660": {
      "content": "<|fim_middle|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    },
    "151661": {
      "content": "<|fim_suffix|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    },
    "151662": {
      "content": "<|fim_pad|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    },
    "151663": {
      "content": "<|repo_name|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    },
    "151664": {
      "content": "<|file_sep|>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    },
    "151665": {
      "content": "<tool_response>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    },
    "151666": {
      "content": "</tool_response>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    },
    "151667": {
      "content": "<think>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    },
    "151668": {
      "content": "</think>",
      "lstrip": false,
      "normalized": false,
      "rstrip": false,
      "single_word": false,
      "special": false
    }
  },
  "additional_special_tokens": [
    "<|im_start|>",
    "<|im_end|>",
    "<|object_ref_start|>",
    "<|object_ref_end|>",
    "<|box_start|>",
    "<|box_end|>",
    "<|quad_start|>",
    "<|quad_end|>",
    "<|vision_start|>",
    "<|vision_end|>",
    "<|vision_pad|>",
    "<|image_pad|>",
    "<|video_pad|>"
  ],
  "bos_token": null,
  "chat_template": "{%- if tools %}\n    {{- '<|im_start|>system\\n' }}\n    {%- if messages[0].role == 'system' %}\n        {{- messages[0].content + '\\n\\n' }}\n    {%- endif %}\n    {{- \"# Tools\\n\\nYou may call one or more functions to assist with the user query.\\n\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\" }}\n    {%- for tool in tools %}\n        {{- \"\\n\" }}\n        {{- tool | tojson }}\n    {%- endfor %}\n    {{- \"\\n</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\\\"name\\\": <function-name>, \\\"arguments\\\": <args-json-object>}\\n</tool_call><|im_end|>\\n\" }}\n{%- else %}\n    {%- if messages[0].role == 'system' %}\n        {{- '<|im_start|>system\\n' + messages[0].content + '<|im_end|>\\n' }}\n    {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for forward_message in messages %}\n    {%- set index = (messages|length - 1) - loop.index0 %}\n    {%- set message = messages[index] %}\n    {%- set tool_start = '<tool_response>' %}\n    {%- set tool_start_length = tool_start|length %}\n    {%- set start_of_message = message.content[:tool_start_length] %}\n    {%- set tool_end = '</tool_response>' %}\n    {%- set tool_end_length = tool_end|length %}\n    {%- set start_pos = (message.content|length) - tool_end_length %}\n    {%- if start_pos < 0 %}\n        {%- set start_pos = 0 %}\n    {%- endif %}\n    {%- set end_of_message = message.content[start_pos:] %}\n    {%- if ns.multi_step_tool and message.role == \"user\" and not(start_of_message == tool_start and end_of_message == tool_end) %}\n        {%- set ns.multi_step_tool = false %}\n        {%- set ns.last_query_index = index %}\n    {%- endif %}\n{%- endfor %}\n{%- for message in messages %}\n    {%- if (message.role == \"user\") or (message.role == \"system\" and not loop.first) %}\n        {{- '<|im_start|>' + message.role + '\\n' + message.content + '<|im_end|>' + '\\n' }}\n    {%- elif message.role == \"assistant\" %}\n        {%- set content = message.content %}\n        {%- set reasoning_content = '' %}\n        {%- if message.reasoning_content is defined and message.reasoning_content is not none %}\n            {%- set reasoning_content = message.reasoning_content %}\n        {%- else %}\n            {%- if '</think>' in message.content %}\n                {%- set content = (message.content.split('</think>')|last).lstrip('\\n') %}\n                {%- set reasoning_content = (message.content.split('</think>')|first).rstrip('\\n') %}\n                {%- set reasoning_content = (reasoning_content.split('<think>')|last).lstrip('\\n') %}\n            {%- endif %}\n        {%- endif %}\n        {%- if loop.index0 > ns.last_query_index %}\n            {%- if loop.last or (not loop.last and reasoning_content) %}\n                {{- '<|im_start|>' + message.role + '\\n<think>\\n' + reasoning_content.strip('\\n') + '\\n</think>\\n\\n' + content.lstrip('\\n') }}\n            {%- else %}\n                {{- '<|im_start|>' + message.role + '\\n' + content }}\n            {%- endif %}\n        {%- else %}\n            {{- '<|im_start|>' + message.role + '\\n' + content }}\n        {%- endif %}\n        {%- if message.tool_calls %}\n            {%- for tool_call in message.tool_calls %}\n                {%- if (loop.first and content) or (not loop.first) %}\n                    {{- '\\n' }}\n                {%- endif %}\n                {%- if tool_call.function %}\n                    {%- set tool_call = tool_call.function %}\n                {%- endif %}\n                {{- '<tool_call>\\n{\"name\": \"' }}\n                {{- tool_call.name }}\n                {{- '\", \"arguments\": ' }}\n                {%- if tool_call.arguments is string %}\n                    {{- tool_call.arguments }}\n                {%- else %}\n                    {{- tool_call.arguments | tojson }}\n                {%- endif %}\n                {{- '}\\n</tool_call>' }}\n            {%- endfor %}\n        {%- endif %}\n        {{- '<|im_end|>\\n' }}\n    {%- elif message.role == \"tool\" %}\n        {%- if loop.first or (messages[loop.index0 - 1].role != \"tool\") %}\n            {{- '<|im_start|>user' }}\n        {%- endif %}\n        {{- '\\n<tool_response>\\n' }}\n        {{- message.content }}\n        {{- '\\n</tool_response>' }}\n        {%- if loop.last or (messages[loop.index0 + 1].role != \"tool\") %}\n            {{- '<|im_end|>\\n' }}\n        {%- endif %}\n    {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n    {{- '<|im_start|>assistant\\n' }}\n    {%- if enable_thinking is defined and enable_thinking is false %}\n        {{- '<think>\\n\\n</think>\\n\\n' }}\n    {%- endif %}\n{%- endif %}",
  "clean_up_tokenization_spaces": false,
  "eos_token": "<|im_end|>",
  "errors": "replace",
  "extra_special_tokens": {},
  "model_max_length": 40960,
  "pad_token": "<|vision_pad|>",
  "padding_side": "left",
  "split_special_tokens": false,
  "tokenizer_class": "Qwen2Tokenizer",
  "unk_token": null
}

而在实际微调过程中,我们都知道需要有监督的数据集、也就是需要输入QA对来进行微调。以著名的alpaca_zh中文微调数据集来说,其基本格式如下:

{
"instruction": "",
"input": "你好。",
"output": "你好,有什么可以帮到你的?"
},

而在真实的微调过程中,如果是针对Qwen3进行微调,微调脚本会将这条数据集(无论什么格式)转化为如下格式:

<|im_start|>user\n你好<|im_end|>\n<|im_start|>assistant\n你好,有什么可以帮到你的?<|im_end|>

而在实际训练过程中,模型就会根据assistant前的内容,学习assistant后面的输出内容。

带有提示词和Function calling微调数据集格式

在很多场景下,我们还会发现一些带有 instruction 字段的微调数据集,那 instruction 字段是如何带入到微调过程中的呢?

答案非常简单,还是依靠特殊字符。例如有一个对话内容如下:

系统提示词(instruction:你是一名助人为乐的助手。

用户输入(input:你好,好久不见。

助手回复(output:是的呀,好久不见,最近有什么有趣的事情要和我分享么?

此时模型的输入和输出如下:

<|im_start|>system
你是一名助人为乐的助手。<|im_end|>
<|im_start|>user
你好,好久不见。<|im_end|>
<|im_start|>assistant
是的呀,好久不见,最近有什么有趣的事情要和我分享么?<|im_end|>

即会通过 <|im_start|>system...<|im_end|> 来标记系统提示词。实际进行微调时,模型会根据assistant为界,学习assistant之前的文本输入情况下应该如何输出。

更进一步的,如果对话过程中带入了Function calling,此时首先模型会读取提前准备好的tool schema(也可能是自动生成的,例如MCP即可自动创建tool schema):

tool_schema = [
    {
        "name": "get_weather",  // 工具名称:用于标识具体调用的工具
        "description": "查询指定城市的天气信息",  // 工具功能描述:说明工具的用途
        "parameters": {  // 工具所需参数定义
            "type": "object",  // 参数类型:整体为对象结构
            "properties": {  // 具体参数属性
                "location": {  // 参数名:城市位置
                    "type": "string",  // 参数数据类型:字符串
                    "description": "要查询天气的城市名称"  // 参数描述:说明该参数的含义
                }
            },
            "required": ["location"]  // 必选参数:该工具必须传入 location 才能调用
        }
    }
]

而假设我们的对话内容如下:

系统提示词(instruction:你是一名助人为乐的助手。当用户查询天气的时候,请调用get_weather函数进行天气信息查询。

用户输入(input:你好,请帮我查询下北京天气。

助手回复(output:{"name": "get_weather", "arguments": {"location": "北京"}}

此时回复内容就是一条Function call message

而此时模型真实的输入和输出内容如下:

<|im_start|>system
你是一名助人为乐的助手。当用户查询天气的时候,请调用get_weather函数进行天气信息查询。
# Tools
You may call one or more functions to assist with the user query.
You are provided with function signatures within <tools></tools> XML tags:
<tools>
{"name": "get_weather", "description": "查询指定城市的天气信息", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "要查询天气的城市名称"}}, "required": ["location"]}}
</tools>
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
<tool_call>{"name": <function-name>, "arguments": <args-json-object>}</tool_call>
<|im_end|>
<|im_start|>user
你好,请帮我查询下北京天气。<|im_end|>
<|im_start|>assistant
<tool_call>{"name": "get_weather", "arguments": {"location": "北京"}}</tool_call><|im_end|>

接下来在进行训练时,模型同样根据assistant前的内容,学习assistant后面的输出内容。不过需要注意的是,由于高效微调调整的参数量较少,因此只能优化模型的Function calling能力,并不能从无到有让模型学会Function calling。

带有思考过程的微调数据集结构

而如果是带有思考链,则一个简单的问答数据如下:

{
    "system": "你是一名助人为乐的助手。",
    "conversations": [
        {
            "role": "user",
            "content": "你好,好久不见。"
        },
        {
            "role": "assistant",
            "content": "是的呀,好久不见,最近有什么有趣的事情要和我分享么?",
            "reasoning_content": "好的,用户发来“你好,好久不见!”,我需要回应。首先,用户可能希望得到亲切的回..."
        }
    ]
}

系统提示词(instruction:你是一名助人为乐的助手。

用户输入(input:你好,好久不见。

助手回复(output:好的,用户发来“你好,好久不见!”,我需要回应。首先,用户可能希望得到亲切的回应,所以应该用友好的语气。/n是的呀,好久不见,最近有什么有趣的事情要和我分享么?

此时模型真实的内部输入和输出结果如下:

<|im_start|>system
你是一名助人为乐的助手。<|im_end|>
<|im_start|>user
你好,好久不见。<|im_end|>
<|im_start|>assistant
<think>
好的,用户发来“你好,好久不见!”,我需要回应。首先,用户可能希望得到亲切的回应,所以应该用友好的语气。
</think>
是的呀,好久不见,最近有什么有趣的事情要和我分享么?<|im_end|>

模型同样根据assistant前的内容,学习assistant后面的输出内容。也就是说,所谓的思考过程,本质上其实是一种文本响应格式,通过模型训练而来。

最后难度升级,假设是带有思考过程、系统提示词的Function calling流程呢?此时一次对话的基本数据结构如下:

{
    "conversations": [
        {
            "role": "user",
            "content": "你好,请帮我查询下北京天气。"
        },
        {
            "role": "assistant",
            "content": "<tool_call>\n{\"name\": \"get_weather\", \"arguments\": {\"location\": \"北京\"}}",
            "reasoning_content": "好的,用户问北京今天的天气,我应该尝试调用工具 get_weather,并将参数设置为北京"
        }
    ]
}

内容如下:

系统提示词(instruction:你是一名助人为乐的助手。当用户查询天气的时候,请调用get_weather函数进行天气信息查询。

用户输入(input:你好,请帮我查询下北京天气。

助手回复(output:好的,用户问北京今天的天气,我应该尝试调用工具get_weather,并将参数设置为北京。/n{"name": "get_weather", "arguments": {"location": "北京"}}

而此时模型的真实输入和输出如下:

<|im_start|>system
你是一名助人为乐的助手。当用户查询天气的时候,请调用get_weather函数进行天气信息查询。
# Tools
You may call one or more functions to assist with the user query.
You are provided with function signatures within <tools></tools> XML tags:
<tools>
{"name": "get_weather", "description": "查询指定城市的天气信息", "parameters": {"type": "object", "properties": {"location": {"type": "string", "description": "要查询天气的城市名称"}}, "required": ["location"]}}
</tools>
For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
<tool_call>
{"name": <function-name>, "arguments": <args-json-object>}
</tool_call>
<|im_end|>
<|im_start|>user
你好,请帮我查询下北京天气。<|im_end|>
<|im_start|>assistant
<think>
好的,用户问北京今天的天气,我应该尝试调用工具 get_weather,并将参数设置为北京。
</think>
<tool_call>
{"name": "get_weather", "arguments": {"location": "北京"}}
</tool_call><|im_end|>

模型同样根据assistant前的内容,学习assistant后面的输出内容。由此可见,模型拥有不同功能的背后,其实源于不同格式的训练数据集的训练。而对于Qwen3这种模型来说,同时拥有Function calling、混合推理等功能,属于功能非常复杂的模型了。在实际微调过程中,稍有不慎就会令其丧失原有能力。

Qwen3混合推理模型构造微调数据集基本方法

在了解了微调数据集结构背后的基本原理后,接下来的问题是应该如何构造微调数据集呢?一般来说我们可以在huggingface、ModelScope或llama-factory中挑选合适的数据集,并根据实际情况进行组装。例如围绕Qwen3模型的高效微调,为了确保其仍然保留混合推理能力,我们可以考虑在微调数据集中加入如普通对话数据集FineTome(https://huggingface.co/datasets/mlabonne/FineTome-100k),以及带有推理字段的数学类数据集OpenMathReasoning(https://huggingface.co/datasets/nvidia/OpenMathReasoning),并围绕这两个数据集进行拼接,从而在确保能提升模型的数学能力的同时,保留非推理的功能。同时还需要在持续微调训练过程中不断调整COT数学数据集和普通文本问答数据集之间的配比,以确保模型能够在提升数学能力的同时,保留混合推理的性能。

 Qwen3模型高效微调环境准备

接下来开始进行模型高效微调前的环境准备工作。需要注意的是,为了更好的复现企业级模型微调全流程,本次公开课将需要使用如下四项工具:

  • 【必须】Unsloth:高效微调框架,必须安装使用;
  • 【可选】vLLM:模型调度框架,用于验证微调后模型效果,也可以使用ollama或者其他调度框架进行模型微调后效果验证;
  • 【可选】EvalScope:模型评测框架,用于对比微调前后模型性能,也可以通过人工观察进行评估;
  • 【可选】wandb:模型训练数据在线记录工具,用于保存模型训练过程中损失之的变化情况,并监控服务器硬件数据;

若想尽快完成微调,可以只安装Unsloth即可,若希望完整执行完微调、过程监督和效果测试各环节,则需要完整安装完各框架工具。

Unsloth安装部署

由于要安装多个项目,因此建议创建虚拟环境以避免依赖冲突。这里首先为Unsloth创建虚拟环境:

conda create --name unsloth python=3.11

conda init

source ~/.bashrc

conda activate unsloth

然后在虚拟环境中安装Jupyter及Jupyter Kernel:

conda install jupyterlab

conda install ipykernel

python -m ipykernel install --user --name unsloth --display-name "Python unsloth"

然后使用如下命令安装Unsloth:

pip install --upgrade --force-reinstall --no-cache-dir unsloth unsloth_zoo

安装完成后在任意Jupyter中选择unsloth kernel,即可进入对应的虚拟环境进行代码编写:

可以输入如下代码进行测试

from unsloth import FastLanguageModel
import torch

vLLM安装部署

接下来继续安装vLLM,若是在AutoDL上租赁的服务器,则可以在默认环境中进行安装,其他服务器上建议创建虚拟环境进行安装。虚拟环境创建流程如下(AutoDL不用):

conda create --name vllm python=3.11
conda init
source ~/.bashrc
conda activate vllm

 vllm主要提供后台的模型调用服务,因此不用安装Jupyter Kernel。

接下来继续进行vllm安装:

pip install bitsandbytes>=0.45.3
pip install --upgrade vllm

注:bitsandbytes是为了适配4bit动态量化模型调用的库

Qwen3模型权重下载

接下来进行模型权重下载,更多Qwen3模型全尺寸模型本地ollama、vllm、llama.cpp调用及部署方法,详见公开课:https://www.bilibili.com/video/BV1qVG9z4ELJ/

及赋范大模型技术社区文档:

 https://kq4b3vgg5b.feishu.cn/wiki/T5NZwZyZViK61DkYUSdcznYlnfc

而要下载4bit动态量化的Unsloth模型,则可以在魔搭社区上搜索 Qwen3-unsloth-bnb-4bit :

 

选择模型下载即可。需要注意的是,有时我们会遇到这两种不同的模型,其中带有Unsloth标志的是动态

量化模型,而不带unsloth则是普通量化模型,需要注意区分。

注,目前最新版vLLM已支持Unsloth动态量化模型,目前Unsloth团队已完成dense模型优化,MoE模型兼容vLLM版目前还未上线。即目前vLLM只支持 unsloth/Qwen3-1.7B-unsloth-bnb-4bit 、 nsloth/Qwen3-4B-unsloth-bnb-4bit 、 unsloth/Qwen3-8B-unsloth-bnb-4bit 、unsloth/Qwen3-14B-unsloth-bnb-4bit 、 unsloth/Qwen3-32B-unsloth-bnb-4bit 四款模型。

然后安装魔搭社区工具并进行模型下载,以Qwen3-32B-unsloth-bnb-4bit为例(注,不同模型均可完成

微调流程,大家根据自己硬件配置选择即可):

https://www.modelscope.cn/models/unsloth/Qwen3-32B-unsloth-bnb-4bit/files

 

使用如下命令进行下载:

pip install modelscope
modelscope download --model unsloth/Qwen3-32B-unsloth-bnb-4bit --local_dir
./Qwen3-32B-unsloth-bnb-4bit

下载后模型权重如图所示:
 

借助vllm进行模型调用

下载完模型权重后,接下来即可测试模型调用流程了,首先开启vllm服务,需要注意的是,Unsloth的动态量化模型只支持单卡调用,最低22G显存即可运行。因此这里是设置的单卡运行vLLM:
# cd /root/autodl-tmp/Qwen3
vllm serve ./Qwen3-32B-unsloth-bnb-4bit --enable-auto-tool-choice --tool-call-parser hermes
此外,更多关于vLLM调用qwen3参数说明,详见:https://qwen.readthedocs.io/en/latest/deployment/vllm.html#
开启后执行效果如下:
实际显存占用如下:
注,vllm会占用显卡的80%以上显存。实际调用32B 4bit动态量化模型时,最低22G显存即可调用,最低37G显存即可微调。
接下来则可在代码环境中进行调用测试:
from openai import OpenAI

openai_api_key = "EMPTY"
openai_api_base = "http://localhost:8000/v1"

client = OpenAI(
    api_key=openai_api_key,
    base_url=openai_api_base,
)

messages = [
    {"role": "user", "content": "你好,好久不见!"}
]

response = client.chat.completions.create(
    model="./Qwen3-32B-unsloth-bnb-4bit",
    messages=messages,
)

print(response.choices[0].message.content)

EvalScope安装部署流程

EvalScope安装流程

接下来进行EvalScope安装部署,项目官网:https://github.com/modelscope/evalscope
为了避免和Unsloth的依赖产生冲突,EvalScope需要再单独创建虚拟环境:
conda create --name evalscope python=3.11
conda init
source ~/.bashrc
conda activate evalscope

conda install jupyterlab
conda install ipykernel
python -m ipykernel install --user --name evalscope --display-name "Python evalscope"
然后安装对应的库
pip install evalscope                # 安装 Native backend (默认)
# 额外选项
pip install 'evalscope[opencompass]'   # 安装 OpenCompass backend
pip install 'evalscope[vlmeval]'       # 安装 VLMEvalKit backend
pip install 'evalscope[rag]'           # 安装 RAGEval backend
pip install 'evalscope[perf]'          # 安装 模型压测模块 依赖
pip install 'evalscope[app]'           # 安装 可视化 相关依赖

# 或可以直接输入all,安装全部模块
# pip install 'evalscope[all]'           # 安装所有 backends (Native, OpenCompass, VLMEvalKit, RAGEval)
源码安装:
git clone https://github.com/modelscope/evalscope.git
cd evalscope/

pip install omegaconf

pip install -e .                  # 安装 Native backend
# 额外选项
pip install -e '.[opencompass]'   # 安装 OpenCompass backend
pip install -e '.[vlmeval]'       # 安装 VLMEvalKit backend
pip install -e '.[rag]'           # 安装 RAGEval backend
pip install -e '.[perf]'          # 安装 模型压测模块 依赖
pip install -e '.[app]'           # 安装 可视化 相关依赖
pip install -e '.[all]'           # 安装所有 backends (Native, OpenCompass, VLMEvalKit, RAGEval)
evalscope perf \
    --url "http://127.0.0.1:8000/v1/chat/completions" \
    --parallel 5 \
    --model ./Qwen3-32B-unsloth-bnb-4bit \
    --number 20 \
    --api openai \
    --dataset openqa \
    --stream

借助EvalScope进行压力测试

紧接着,我们就可以尝试进行模型的压力测试,可以测试当前4bit动态量化模型在单卡H800上,由vllm调度框架驱动时的实际性能表现:
 {
    "Time taken for tests (s)": 632.0044,
    "Number of concurrency": 5,
    "Total requests": 20,
    "Succeed requests": 20,
    "Failed requests": 0,
    "Output token throughput (tok/s)": 45.5503,
    "Total token throughput (tok/s)": 46.4759,
    "Request throughput (req/s)": 0.0316,
    "Average latency (s)": 142.0679,
    "Average time to first token (s)": 0.2191,
    "Average time per output token (s)": 0.0994,
    "Average input tokens per request": 29.25,
    "Average output tokens per request": 1439.4,
    "Average package latency (s)": 0.0985,
    "Average package per request": 1439.4,
    "Expected number of requests": 20,
    "Result DB path": "outputs/20250505_164404/Qwen3-32B-unsloth-bnb-4bit/benchmark_data.db"
}
输出结果保存在:outputs/20250505_164404/Qwen3-32B-unsloth-bnb-4bit:
然后即可在summary.json中查看压测的benchmark
 {
    "Time taken for tests (s)": 632.0044,
    "Number of concurrency": 5,
    "Total requests": 20,
    "Succeed requests": 20,
    "Failed requests": 0,
    "Output token throughput (tok/s)": 45.5503,
    "Total token throughput (tok/s)": 46.4759,
    "Request throughput (req/s)": 0.0316,
    "Average latency (s)": 142.0679,
    "Average time to first token (s)": 0.2191,
    "Average time per output token (s)": 0.0994,
    "Average input tokens per request": 29.25,
    "Average output tokens per request": 1439.4,
    "Average package latency (s)": 0.0985,
    "Average package per request": 1439.4,
    "Expected number of requests": 20,
    "Result DB path": "outputs/20250505_164404/Qwen3-32B-unsloth-bnb-4bit/benchmark_data.db"
}
 from evalscope.collections import CollectionSchema, DatasetInfo, WeightedSampler
from evalscope.utils.io_utils import dump_jsonl_data

schema = CollectionSchema(name='Qwen3', datasets=[
    CollectionSchema(name='English', datasets=[
        DatasetInfo(name='mmlu_pro', weight=1, task_type='exam', tags=['en'], args={'few_shot_num': 0}),
        DatasetInfo(name='mmlu_redux', weight=1, task_type='exam', tags=['en'], args={'few_shot_num': 0}),
        DatasetInfo(name='ifeval', weight=1, task_type='instruction', tags=['en'], args={'few_shot_num': 0}),
    ]),
    CollectionSchema(name='Chinese', datasets=[
        DatasetInfo(name='ceval', weight=1, task_type='exam', tags=['zh'], args={'few_shot_num': 0}),
        DatasetInfo(name='iquiz', weight=1, task_type='exam', tags=['zh'], args={'few_shot_num': 0}),
    ]),
    CollectionSchema(name='Code', datasets=[
        DatasetInfo(name='live_code_bench', weight=1, task_type='code', tags=['en'], args={'few_shot_num': 0, 'subset_list': ['v5_v6'], 'extra_params': {'start_date': '2025-01-01', 'end_date': '2025-04-30'}}),
    ]),
    CollectionSchema(name='Math&Science', datasets=[
        DatasetInfo(name='math_500', weight=1, task_type='math', tags=['en'], args={'few_shot_num': 0}),
        DatasetInfo(name='aime24', weight=1, task_type='math', tags=['en'], args={'few_shot_num': 0}),
        DatasetInfo(name='aime25', weight=1, task_type='math', tags=['en'], args={'few_shot_num': 0}),
        DatasetInfo(name='gpqa', weight=1, task_type='knowledge', tags=['en'], args={'subset_list': ['gpqa_diamond'], 'few_shot_num': 0})
    ])
])

# get the mixed data
mixed_data = WeightedSampler(schema).sample(100000000)  # set a large number to ensure all datasets are sampled
# dump the mixed data to a jsonl file
dump_jsonl_data(mixed_data, 'outputs/qwen3_test.jsonl')
Benchmarking summary:
Percentile results:
压测结果解释:
📊 压测核心指标解读
指标项
含义
解读
Time taken for tests (s)
总测试耗时
632 秒,表示本次压测运行了约 10 分钟。
Number of concurrency
并发数
使用了 5 个并发请求同时测试,表明模型支持一定程度的并发推理能力。
Total requests
总请求数
总共向模型发起了 20 次推理请求。
Succeed requests / Failed requests
成功与失败数
全部成功(20/20),说明模型稳定性良好。
🚀 吞吐性能
指标项
含义
解读
Output token throughput (tok/s)
每秒生成 token 数
45.55 tok/s,生成速度在 4bit 动态量化大模型中属于正常偏低水平。
Total token throughput (tok/s)
总吞吐(包括输入+输出)
46.48 tok/s,说明输入 token 占比较小,推理主要时间在输出部分。
Request throughput (req/s)
每秒处理请求数
0.0316 req/s,即大约 31 秒才完成一次请求(见下方 latency)。适用于高吞吐非交互式任务。
⏱️ 延迟分析
指标项
含义
解读
Average latency (s)
单次请求平均耗时
142 秒,即每个请求耗时超过 2 分钟,说明单次生成量很大。
Average time to first token (s)
首 token 延迟
0.2191 秒,响应启动速度非常快,体现了量化模型的推理启动效率。
Average time per output token (s)
每个生成 token 的平均耗时
0.0994 秒,略高,意味着模型速度偏慢,可能与显存带宽或激活参数有关。
📦 Token 分布与批处理
指标项
含义
解读
Average input tokens per request
每次输入平均 token 数
29.25,说明输入 prompt 很短。
Average output tokens per request
每次输出平均 token 数
1439.4,生成文本非常长,这解释了高 latency 与低 req/s。
Average package latency (s)
批处理延迟
0.0985 秒,表示模型每次接受请求到开始处理之间延迟极短。
Average package per request
每次请求中 token 包含数
同为 1439.4,表明未使用分片或 chunk 输出,模型按完整长度生成。
  • 优点:
    • 模型非常稳定(20 次请求 100% 成功)
    • 首 token 响应迅速(0.21 秒),表明部署结构良好
    • 适合长文本生成类任务
  • 缺点:
    • 整体吞吐较低(45 tok/s),属于 32B 大模型的正常范围
    • 单次推理耗时较长(每次平均 142 秒),适用于非实时场景。

3.3 进行模型性能评估

接下来即可进一步进行模型性能评估。这里我们先尝试对其进行初始状态下的性能评估,然后等微调结束后,再进行新一轮的评估,进而对比微调前后模型性能变化情况。这里需要在Jupyter中选择evalscope kernel:
然后运行如下代码:
【可选】数据集构造代码:
 
from evalscope.collections import CollectionSchema, DatasetInfo, WeightedSampler
from evalscope.utils.io_utils import dump_jsonl_data

schema = CollectionSchema(name='Qwen3', datasets=[
    CollectionSchema(name='English', datasets=[
        DatasetInfo(name='mmlu_pro', weight=1, task_type='exam', tags=['en'], args={'few_shot_num': 0}),
        DatasetInfo(name='mmlu_redux', weight=1, task_type='exam', tags=['en'], args={'few_shot_num': 0}),
        DatasetInfo(name='ifeval', weight=1, task_type='instruction', tags=['en'], args={'few_shot_num': 0}),
    ]),
    CollectionSchema(name='Chinese', datasets=[
        DatasetInfo(name='ceval', weight=1, task_type='exam', tags=['zh'], args={'few_shot_num': 0}),
        DatasetInfo(name='iquiz', weight=1, task_type='exam', tags=['zh'], args={'few_shot_num': 0}),
    ]),
    CollectionSchema(name='Code', datasets=[
        DatasetInfo(name='live_code_bench', weight=1, task_type='code', tags=['en'], args={'few_shot_num': 0, 'subset_list': ['v5_v6'], 'extra_params': {'start_date': '2025-01-01', 'end_date': '2025-04-30'}}),
    ]),
    CollectionSchema(name='Math&Science', datasets=[
        DatasetInfo(name='math_500', weight=1, task_type='math', tags=['en'], args={'few_shot_num': 0}),
        DatasetInfo(name='aime24', weight=1, task_type='math', tags=['en'], args={'few_shot_num': 0}),
        DatasetInfo(name='aime25', weight=1, task_type='math', tags=['en'], args={'few_shot_num': 0}),
        DatasetInfo(name='gpqa', weight=1, task_type='knowledge', tags=['en'], args={'subset_list': ['gpqa_diamond'], 'few_shot_num': 0})
    ])
])

# get the mixed data
mixed_data = WeightedSampler(schema).sample(100000000)  # set a large number to ensure all datasets are sampled
# dump the mixed data to a jsonl file
dump_jsonl_data(mixed_data, 'outputs/qwen3_test.jsonl')
创建好的完整数据集已上传至课件网盘:
 
评测运行结果如下:
 
此时后台显示如下:
查看模型能力评测结果
evalscope app
对评测结果进行可视化:
 
 

wandb安装与注册

wandb基本说明

  在大规模模型训练中,我们往往需要监控和分析大量的训练数据,而WandB可以帮助我们实现这一目标。它提供了以下几个重要的功能:
实时可视化:WandB可以实时展示训练过程中关键指标的变化,如损失函数、学习率、训练时间等。通过这些可视化数据,我们能够直观地了解模型的训练进展,快速发现训练中的异常或瓶颈。
自动记录与日志管理:WandB会自动记录每次实验的参数、代码、输出结果,确保实验结果的可追溯性。无论是超参数的设置,还是模型的架构调整,WandB都能够帮助我们完整保留实验记录,方便后期对比与调优。
支持中断与恢复训练:在长时间的预训练任务中,系统中断或需要暂停是常见的情况。通过WandB的checkpoint功能,我们可以随时恢复训练,从上次中断的地方继续进行,避免数据和时间的浪费。
多实验对比:当我们尝试不同的模型配置或超参数时,WandB允许我们在多个实验之间轻松进行对比分析,帮助我们选择最优的模型配置。
团队协作:WandB还支持团队协作,多个成员可以共同查看实验结果,协同调试模型。这对研究和项目开发中团队的合作非常有帮助。

wandb注册与使用

wandb官网:https://wandb.ai/site
 
 
 
 
然后即可在令行中输入如下代码安装wandb:
pip install wandb
接下来在unsloth微调前,我们即可设置wandb进行微调记录,并可在对应网站上观察到训练过程如下:

Qwen3混合推理模型微调数据集准备

在搭建好了基础环境后,接下来即可开始着手准备Qwen3微调数据集了。围绕Qwen3模型的高效微调,为了确保其仍然保留混合推理能力,我们可以考虑在微调数据集中加入如普通对话数据集FineTome(https://huggingface.co/datasets/mlabonne/FineTome-100k),以及带有推理字段的数学类数据集OpenMathReasoning(https://huggingface.co/datasets/unsloth/OpenMathReasoning-mini),并围绕这两个数据集进行拼接,从而在确保能提升模型的数学能力的同时,保留非推理的功能。同时还需要在持续微调训练过程中不断调整COT数学数据集和普通文本问答数据集之间的配比,以确保模型能够在提升数学能力的同时,保留混合推理的性能。

OpenMathReasoning 数据集(AIMO-2)

**数据集介绍:**OpenMathReasoning 是为 AI Mathematical Olympiad - Progress Prize 2(AIMO-2)竞赛开发的高质量数学推理数据集。该数据集包含:
  • 540,000 道独特的高质量数学问题,涵盖代数、组合、几何和数论等领域;
  • 3,200,000 条详细的长推理解答;
  • 1,700,000 条工具集成推理(Tool-Integrated Reasoning)解答,结合了代码执行与推理过程;
  • 生成式解答选择(GenSelect) 方法,用于从多个候选解答中选择最优解。
该数据集旨在推动 AI 在复杂数学推理任务中的能力提升,已被用于训练在多个数学推理基准上表现优异的模型。
  • 数据集格式
OpenMathReasoning包含expected_answerproblemgenerated_solution三个核心字段,分别代表问题答案、问题和模型思考过程。例如其中一条数据内容如下:
问题:
 设有一组共 NN 个球,由 CC 种颜色组成,其中每种颜色的球的数量分别为 n1,n2,…,nCn_1, n_2, \ldots, n_C(满足 n1+n2+…+nC=Nn_1 + n_2 + \ldots + n_C = N)。如果从中随机抽取 AA 个球(其中 A≤NA \leq N),那么抽到的球中恰好有 a1,a2,…,aCa_1, a_2, \ldots, a_C 个分别对应每种颜色(满足 a1+a2+…+aC=Aa_1 + a_2 + \ldots + a_C = A),这种情况发生的概率是多少?
答案:
$$ frac{C_{n_1}^{a_1} cdot C_{n_2}^{a_2} cdot ldots cdot C_{n_C}^{a_C}}{C_N^A $$
 
思考过程:
 好的,也就是说我要计算这样一个概率:从总共 $N$ 个球中随机抽取 $A$ 个球,其中球分为 $C$ 种颜色,要求抽到的每种颜色球的数量恰好是 $a_1, a_2, \ldots, a_C$。嗯,我们来想想该怎么求解这个问题。

首先,概率问题通常会涉及**组合数**。概率的一般公式是:
**有利情况数 ÷ 所有可能情况总数**。

在本题中,“有利情况”就是指:恰好抽到 $a_1$ 个颜色1的球,$a_2$ 个颜色2的球,依此类推,一直到颜色 $C$。
而“所有可能情况”则是:从 $N$ 个球中任意抽取 $A$ 个球的方式,不考虑颜色分布。

我们可以这样分解这个问题:

* 从 $N$ 个球中任意抽取 $A$ 个球的总方式数是组合数:

  $$
  C(N, A) = \binom{N}{A} = \frac{N!}{A!(N - A)!}
  $$

* 而有利情况则是:从每种颜色中选出指定数量的球。比如从 $n_1$ 个颜色1的球中选出 $a_1$ 个,从 $n_2$ 个颜色2的球中选出 $a_2$ 个,依此类推。
  由于每种颜色之间的选择是相互独立的,所以所有组合方式的总数就是各个颜色组合数的乘积:

  $$
  \binom{n_1}{a_1} \times \binom{n_2}{a_2} \times \cdots \times \binom{n_C}{a_C}
  $$

不过,要注意前提条件必须满足:

* 每个 $a_i \leq n_i$,也就是说不能从某种颜色中选出比它实际数量更多的球;
* 同时 $a_1 + a_2 + \cdots + a_C = A$,总共抽取的球数必须正确。

如果这些条件满足,那么这种组合方式就是有效的。如果不满足,比如某个 $a_i > n_i$,则该组合数为0,概率也自然为0,这是符合逻辑的。

其实这正是一个\*\*多项超几何分布(multivariate hypergeometric distribution)\*\*问题。

超几何分布描述的是:从有限总体中不放回地抽取样本,得到某个类别成功次数的概率。多项超几何分布是其推广,适用于多个类别的情况,正好契合这里的“多种颜色”的设定。

该分布的概率公式就是:

$$
P = \frac{\binom{n_1}{a_1} \cdot \binom{n_2}{a_2} \cdots \binom{n_C}{a_C}}{\binom{N}{A}}
$$

---

### 最终结论:

当从 $N$ 个球(含有 $C$ 种颜色,每种颜色分别有 $n_1, n_2, \ldots, n_C$ 个球)中随机抽取 $A$ 个球时,恰好抽中每种颜色 $a_1, a_2, \ldots, a_C$ 个球的概率为:

$$
\boxed{
P = \frac{\prod_{i=1}^{C} \binom{n_i}{a_i}}{\binom{N}{A}}
}
$$

这个公式就是多项超几何分布的概率表达式,分子为每种颜色的有利组合方式之乘积,分母为所有可能抽取 $A$ 个球的方式总数。
 

FineTome-100k 数据集(Maxime Labonne)

**数据集简介:**FineTome-100k 是由 Maxime Labonne 创建的高质量多轮对话数据集,采用 ShareGPT 风格,适用于大语言模型的微调。该数据集特点包括:
  • 100,000 条多轮对话样本;
  • 数据以 JSONL 格式存储,每条记录包含一个 "conversations" 字段,记录对话的完整历史;
  • 对话格式类似于 ShareGPT,适合训练模型进行多轮对话;
  • 可转换为 Hugging Face 通用的多轮对话格式,以适配不同的训练框架。
 
接下来即可上手使用Unsloth进行高效微调了。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
posted @ 2025-07-25 14:49  指尖下的世界  阅读(4594)  评论(4)    收藏  举报