Chapter 05

加速原理:Triton kernel 与 fused LoRA

📌 commit 1cf145c070ea 本书最硬的一章 — 看清"快 2-5×"从哪来

Unsloth 的口号是"2-5× faster, 80% less memory"。这章把数字背后的工程拆开:哪几个算子被融合、 哪几个 kernel 用 Triton 重写、为什么数学等价但速度差几倍。读完你应该能给同事讲清楚 "为什么 Unsloth 比纯 HF + PEFT 快"。

5.1大局观:LoRA 训练的时间花在哪

一个 LoRA 训练 step 的时间分布(HF + PEFT 默认实现,Llama-3-8B / 4090):

阶段占比
Attention 计算 ~ 35%
MLP(含 SwiGLU) ~ 25%
LoRA 前向 + 反向 ~ 15%
LayerNorm / RoPE ~ 10%
Cross-Entropy + 反向 ~ 10%
其他(dataloader / opt step)~ 5%

Unsloth 对前 5 项都有 Triton 替代实现,每项加速 1.5-5×,总加速比 2-5×。

5.2Triton 是什么

Triton 是 OpenAI 开源的GPU kernel 编写 DSL,用 Python 写、JIT 编译成 PTX。优点:

Unsloth 几乎所有 kernel 都用 Triton 写。代码在 unsloth/kernels/

5.3fused LoRA:核心加速点

LoRA 的数学:

$$ y = Wx + \frac{\alpha}{r}\, BA\, x,\quad W \in \mathbb{R}^{d\times d},\ A \in \mathbb{R}^{r\times d},\ B \in \mathbb{R}^{d\times r},\ r \ll d $$

原版 PEFT 的实现(4 个 kernel launch)

def forward(x):
    main = self.base_layer(x)              # kernel 1: W @ x  (大矩阵乘)
    lora = self.lora_A(x)                  # kernel 2: A @ x  (小矩阵乘)
    lora = self.lora_B(lora)               # kernel 3: B @ lora
    lora = lora * (self.alpha / self.r)    # kernel 4: 标量乘
    return main + lora

每个 kernel 都要去 GPU 读写一遍 input/output 张量,memory bound。小矩阵乘上 GPU launch overhead 还压不住。

Unsloth fused LoRA(1 个 kernel launch)

unsloth/kernels/fast_lora.py,核心是把上面 4 步融合:

@triton.jit
def lora_forward_kernel(
    X_ptr, W_ptr, A_ptr, B_ptr, Y_ptr,
    M, N, K, R,
    scale,
    BLOCK_M, BLOCK_N, BLOCK_K, BLOCK_R,
):
    # 一个 program 算一个 (BLOCK_M, BLOCK_N) 输出 block:
    # 1) 累加 W @ x 的贡献
    # 2) 累加 (B @ A @ x) * scale 的贡献
    # 3) 一次性写回 Y
    # 中间张量从不写回 HBM

少 3 次 kernel launch + 中间张量不落地 = LoRA 部分快 2-3 倍。

反向也 fuse

LoRA 反向要算 4 个梯度:$\partial L/\partial X$、$\partial L/\partial W$(如果训)、$\partial L/\partial A$、$\partial L/\partial B$。 Unsloth 把整套反向也写在一个 Triton kernel 里,进一步省时间。

5.4fused RMSNorm

RMSNorm 公式:

$$ \mathrm{RMSNorm}(x) = \frac{x}{\sqrt{\frac{1}{d}\sum_i x_i^2 + \epsilon}} \cdot g $$

$g$ 是可学习缩放参数。原版 PyTorch 实现拆成 5 步:求平方、求均值、加 eps、开根、除 + 乘 g。每步一个 kernel。

Unsloth 的 unsloth/kernels/rms_layernorm.py 把所有步骤 fuse 在一个 Triton kernel 里,整层只读写一次 hidden state。 内存带宽利用率从 ~20% 提到 90%+,对 4090 这种内存带宽本来就是瓶颈的卡尤其明显。

5.5fused RoPE

RoPE 的本质是给 Q、K 乘一个旋转矩阵:

$$ \begin{pmatrix} q'_{2i} \\ q'_{2i+1} \end{pmatrix} = \begin{pmatrix} \cos\theta_i & -\sin\theta_i \\ \sin\theta_i & \cos\theta_i \end{pmatrix} \begin{pmatrix} q_{2i} \\ q_{2i+1} \end{pmatrix} $$

原版实现用 PyTorch 切片 + cat:

# 慢的版本(10+ 个 elementwise kernel)
q_rot, q_pass = q[..., :rot_dim], q[..., rot_dim:]
q_rot = (q_rot * cos) + (rotate_half(q_rot) * sin)
q = torch.cat([q_rot, q_pass], dim=-1)

Unsloth 的 unsloth/kernels/rope_embedding.py 把整个旋转 fuse 成单个 Triton kernel,就地修改 q、k(in-place),免去 cat 的内存分配。

5.6fused SwiGLU

SwiGLU MLP 的计算:

$$ \mathrm{SwiGLU}(x) = (W_g x \odot \sigma(W_g x)) \odot W_u x,\quad \text{output} = W_d \cdot \mathrm{SwiGLU}(x) $$

三次矩阵乘(gate / up / down)+ 中间一次 SiLU + elementwise 乘法。原版分 4 个 kernel;Unsloth 把 SiLU + 中间 elementwise 乘法 fuse 进上下游矩阵乘 epilogue,看 unsloth/kernels/swiglu.py

5.7chunked cross-entropy(大词表的杀手锏)

Llama-3 词表 128K,输出层 logits 矩阵在 batch_size=8、seq=2048 时占用:

$$ 8 \times 2048 \times 128000 \times 4\ \text{bytes} \approx 8.4\ \text{GB}\ (\text{fp32}) $$

原版 cross-entropy 一次性算所有 logits,这一个张量就把 24 GB 4090 压爆。Unsloth 的招:

  1. 不实例化完整 logits 矩阵;
  2. 把序列切成 chunks(默认 256 个 token 一块);
  3. 每个 chunk 算自己的 logits + loss + 梯度,当场释放 logits
  4. 累计 chunk loss 到最终 loss。

显存峰值从 8.4 GB 降到约 100 MB,损失 0 精度unsloth/kernels/cross_entropy_loss.py 实现这件事。

为什么这一项最重要 对大词表模型(Llama-3 128K / Gemma 256K),chunked CE 就是"Unsloth 能在 24G 卡上训 8B 模型"的核心原因。 不开它,多大显存都不够。

5.8"unsloth" gradient checkpointing

原版 PyTorch torch.utils.checkpoint.checkpoint 的逻辑:

Unsloth 的 "unsloth" 模式更激进:

使用:

model = FastLanguageModel.get_peft_model(
    model,
    ...,
    use_gradient_checkpointing = "unsloth",   # ★ "unsloth" 而不是 True
)

5.9fp8 训练(v2026 新增)

unsloth/kernels/fp8.py,v2026 加入了 fp8 mixed precision 训练支持。条件:

fp8 训练相比 bf16 再快 30-50%,但对消费卡(4090)无用,Ada / Ampere 架构没有 fp8 tensor cores。

5.10flex attention

unsloth/kernels/flex_attention.py 集成了 PyTorch FlexAttention(2024+ 引入),让用户能写自定义 attention mask (如 sliding window、causal + sliding 等),同时还能享受 FlashAttention 级别的性能。

对长上下文训练 / 滑窗 attention 尤其有用,第 7 章会展开。

5.11"数学等价"是怎么保证的

所有 Unsloth kernel 都做严格的数值等价测试:

tests/python/ 下有大量这类测试。这就是 Unsloth 敢说"快 + 数学等价"的依据 —— 任何 Unsloth 训出的模型, 理论上能用纯 HF + PEFT 复现到小数点后几位。

5.12对比 FlashAttention 怎么共存

组件flash-attnUnsloth
Attention ✅ FA-2/3 调 flash-attn 的 API
RMSNorm ✅ Triton fused
RoPE ✅ Triton fused
Cross-Entropy ✅ chunked CE
LoRA ✅ fused LoRA
SwiGLU ✅ fused

Unsloth + flash-attn = 互补,不是替代。都装上效果最好

5.13能不能不用 LoRA 也享受这些加速

可以。把 LoRA 部分关掉,剩下的 fused RMSNorm / RoPE / Cross-Entropy / SwiGLU / gradient ckpt 依然生效:

model, tokenizer = FastLanguageModel.from_pretrained(...)
# 不调 get_peft_model → 全参微调
trainer = SFTTrainer(model=model, ...)
trainer.train()

但全参微调显存往往装不下,所以实际场景几乎都配 LoRA / QLoRA用。

5.14性能数字

官方 README 公布的 benchmark(Llama-3-8B QLoRA on 1× A100 40G):

实现训练速度峰值显存
HF + PEFT + flash-attn 1.00× 27 GB
+ Unsloth 2.1× 16 GB
+ Unsloth + chunked CE 2.4× 10 GB
+ Unsloth Pro(多卡) 3.5× 单卡基准

4090 单卡上加速比通常更高(因为显存带宽相对计算更紧):3-5×

5.15kernel 源码定位(Triton 实现)

kernel位置关键符号
Fused LoRA(MLP / QKV / W) unsloth/kernels/fast_lora.py apply_lora_mlp_swiglu() line 235、apply_lora_qkv() line 535
Fast RoPE(含 QK fusion) unsloth/kernels/rope_embedding.py Fast_RoPE_Embedding line 185、fast_rope_embedding() line 282
Fast RMSNorm unsloth/kernels/rms_layernorm.py Fast_RMS_Layernorm line 162、patch_rms_layernorm() line 270
Chunked CrossEntropy unsloth/kernels/cross_entropy_loss.py fwd / bwd 分块
SwiGLU 融合 unsloth/kernels/swiglu.py FFN 中间维 fuse
GEGLU unsloth/kernels/geglu.py Gemma 系激活
非 RMS LayerNorm unsloth/kernels/layernorm.py 给非 Llama 系模型用
FlexAttention 接入 unsloth/kernels/flex_attention.py PyTorch 2.6+ 路径
FP8 unsloth/kernels/fp8.py 2026 新增
MoE unsloth/kernels/moe/ Qwen3-MoE 等

5.16patch 注入入口

patch 函数位置
patch_fast_lora() models/_utils.py:2544
patch_rms_layernorm() kernels/rms_layernorm.py:270
patch_llama_rope_scaling() models/_utils.py:2060
patch_mistral_nemo_attention() models/mistral.py:416
patch_model_and_tokenizer() models/_utils.py(通用入口)

5.17这章带走的