跳到主要内容

数据截至 (上游 commit 1fe27b1b53f3)

05 · 加载与导出:4bit 进来,GGUF/FP8 出去

这章走完权重的完整旅程:从 from_pretrained 的加载路由,到训练态的省显存开关(梯度检 查点启发式),再到训完怎么导出成能部署的格式。

1. 加载:FastLanguageModel 是个路由器

FastLanguageModel.from_pretrained(unsloth/models/loader.py:425-426)的参数面几乎照抄 HF,但它做的第一件事是分派:

  • 4bit QLoRA(默认) 走本仓库的老管线:FastLlamaModel.from_pretrained (unsloth/models/llama.py:2349)——先 pre_patch() 换类(见 01 章),再用 bitsandbytes 4bit 把权重装上卡;
  • 8bit / 全参数微调 / QAT 委托给新的 FastModel(unsloth/models/loader.py:512-549, 该类在 loader.py:1212)——另一条基于 unsloth_zoo 编译器补丁的管线;
  • 用户自带 BitsAndBytesConfig 时会被读取并覆盖 load_in_4bit/8bit 标志 (loader.py:462-476),尊重外部配置而不是顶掉它。

两个模型名层面的小机关:

  • 4bit 名 ↔ 16bit 名互查表:unsloth/models/mapper.py__INT_TO_FLOAT_MAPPER 记录每个 -bnb-4bit 仓库对应的 16bit 原版;没装 bitsandbytes(SUPPORTS_FOURBIT 为假)或用户要 16bit 时,加载器默默换成非量化仓库 (unsloth/models/loader_utils.py:463-474);导出合并时反查回来。
  • 分布式安全:多卡 torchrun 下量化模型每 rank 各自加载到自己设备,避免 Accelerate 搬移量化权重出错(loader.py:500-509)。

2. 训练态的省显存开关:梯度检查点启发式

use_gradient_checkpointing="unsloth" 是默认推荐值,但它不是总是更快: apply_unsloth_gradient_checkpointing(unsloth/models/_utils.py:372-396)按序列长度自动 降级:

# 示意,非源码 —— 启发式
if use_gradient_checkpointing == "unsloth":
if max_seq_length < 512:
return True # 短序列:卸载开销不值,用标准 checkpoint
else:
patch_unsloth_smart_gradient_checkpointing(...) # 长序列:激活卸载到 CPU
return "unsloth"

注释写明交叉点在 384–512 之间(_utils.py:384-386)。「unsloth 版」的本体 (Unsloth_Offloaded_Gradient_Checkpointer)在依赖包 unsloth_zoo,靠全局替换 torch.utils.checkpoint 生效——模型前向里 torch.utils.checkpoint.checkpoint(...) 的调用(unsloth/models/llama.py:1198-1206)因此自动变成「激活异步搬到 CPU、反向再取回」。

配套的状态管理:for_inference/for_training(llama.py:3977-4104)成对切换梯度检查 点、training 标志、tokenizer padding 方向(推理左 pad / 训练右 pad),并用 model._unsloth_gradient_checkpointing 记录生效值,防止 for_inference() 清掉标志后被 普通 TrainingArguments 静默关掉(注释见 llama.py:3753-3756,issue #4735)。

3. 出口一:合并 LoRA → 16bit

导出前先把 LoRA 合回主干:unsloth_save_model(unsloth/save.py:840)是总调度, _merge_lora(save.py:637)负责把 W + s·A@B 加回权重。mapper 表在这里反向使用: 4bit 训练出的模型合并时以 16bit 原版仓库为底,避免「在 4bit 残值上合并」带来的精度损失 (这张表的存在意义,见 mapper.py 头部 __all__)。

4. 出口二:GGUF 与 Unsloth 动态量化预设

save_to_gguf(unsloth/save.py:1885)编排整个 GGUF 流水线,两段式:

  1. 先转成高精度 GGUF:_choose_first_conversion(save.py:1862)选 f16/bf16 首转;
  2. 再逐个量化:llama.cpp 的 llama-quantize 把首转 GGUF 压成目标精度 (save.py:2298 起的第二步)。

关键设计是 Unsloth 自己的量化预设。以 _quantize_q2_k_l(save.py:377-458)为例:

  • Q2_K_L 不是 llama.cpp 的原生 ftype,而是 Unsloth 预设:整体 q2_k,但 --output-tensor-type q8_0 --token-embedding-type q8_0——输出头和词嵌入这两类对精度最 敏感的张量保 8bit(save.py:385-396 注释明确说了这一点);
  • 这就是「动态量化(dynamic quants)」的工程含义:不同张量不同位宽,按敏感度分配精度预 算,而不是一刀切。UD-Q4_K_XL 等命名都是这个思路的变体;
  • 支持 --imatrix(重要性矩阵校准),IQ 系低位量化必须有它(save.py:1898-1903)。

llama.cpp 本身由 install_llama_cpp_* 系列函数现场 clone + 编译(save.py:1474-1844), 非阻塞安装,导出路径对外仍是 model.save_pretrained_gguf(...) 一行。

5. 出口三:FP8/NVFP4 —— 子进程里跑 llm-compressor

FP8/NVFP4 这类需要校准(拿样本数据前向跑一遍统计 scale)的量化走 unsloth/_compressed_quantize.py,架构选择很讲究:

  • 单独子进程、按文件路径启动(模块 docstring,_compressed_quantize.py:11-17):因为 主进程里 unsloth 已经把 transformers 的 forward 全补丁了,而校准需要未打补丁的原版前 向;「按路径启动而非 python -m」正是为了让子进程不 import unsloth 包;
  • 校准数据由 _build_calibration_dataset 构造(_compressed_quantize.py:78);
  • compressed_ignore_patterns(:56)列出拒绝量化的模块(MoE /router、MTP 层等), 导出前的体积估算复用同一份清单,保证「计划」与「执行」一致。

6. 坑

  • 导出链路外部依赖重:GGUF 依赖现场编译 llama.cpp;FP8 依赖 llm-compressor 且对 transformers 版本有天花板检查(save.py:1566)。离线 / 异构环境下这些都可能断。
  • 合并的精度前提:4bit 训练 + 16bit 底合并是「尽量无损」,但 4bit 量化本身的误差在 训练前就已固化进主干权重——合并救不回那部分。
  • 梯度检查点启发式只看 max_seq_length:batch 大、层数深的模型在 <512 序列下也可能 更受益,启发式并不感知(_utils.py:372 的签名就两个参数)。