Merge branch 'unslothai:main' into main

This commit is contained in:
electron271 2025-12-12 15:48:03 -06:00 committed by GitHub
commit 3ac0abf7d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 2928 additions and 612 deletions

View file

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.14.6
rev: v0.14.8
hooks:
- id: ruff
args:

View file

@ -23,14 +23,15 @@ Notebooks are beginner friendly. Read our [guide](https://docs.unsloth.ai/get-st
| Model | Free Notebooks | Performance | Memory use |
|-----------|---------|--------|----------|
| **gpt-oss (20B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/gpt-oss-(20B)-Fine-tuning.ipynb) | 1.5x faster | 70% less |
| **Qwen3 (14B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_(14B)-Reasoning-Conversational.ipynb) | 2x faster | 70% less |
| **Mistral Ministral 3 (3B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Ministral_3_VL_(3B)_Vision.ipynb) | 1.5x faster | 60% less |
| **gpt-oss (20B): GRPO** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/gpt-oss-(20B)-GRPO.ipynb) | 2x faster | 80% less |
| **Qwen3: Advanced GRPO** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_(4B)-GRPO.ipynb) | 2x faster | 50% less |
| **Qwen3-VL (8B): GSPO** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_VL_(8B)-Vision-GRPO.ipynb) | 1.5x faster | 80% less |
| **Qwen3-VL (8B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_VL_(8B)-Vision.ipynb) | 2x faster | 50% less |
| **Gemma 3 (270M)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Gemma3_(270M).ipynb) | 1.7x faster | 60% less |
| **Gemma 3n (4B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Gemma3N_(4B)-Conversational.ipynb) | 1.5x faster | 50% less |
| **DeepSeek-OCR (3B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Deepseek_OCR_(3B).ipynb) | 1.5x faster | 30% less |
| **Llama 3.1 (8B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Llama3.1_(8B)-Alpaca.ipynb) | 2x faster | 70% less |
| **Llama 3.1 (8B) Alpaca** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Llama3.1_(8B)-Alpaca.ipynb) | 2x faster | 70% less |
| **Llama 3.2 Conversational** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Llama3.2_(1B_and_3B)-Conversational.ipynb) | 2x faster | 70% less |
| **Orpheus-TTS (3B)** | [▶️ Start for free](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Orpheus_(3B)-TTS.ipynb) | 1.5x faster | 50% less |
- See all our notebooks for: [Kaggle](https://github.com/unslothai/notebooks?tab=readme-ov-file#-kaggle-notebooks), [GRPO](https://docs.unsloth.ai/get-started/unsloth-notebooks#grpo-reasoning-rl-notebooks), **[TTS](https://docs.unsloth.ai/get-started/unsloth-notebooks#text-to-speech-tts-notebooks)** & [Vision](https://docs.unsloth.ai/get-started/unsloth-notebooks#vision-multimodal-notebooks)
@ -50,17 +51,20 @@ Use our official [Unsloth Docker image](https://hub.docker.com/r/unsloth/unsloth
For RTX 50x, B200, 6000 GPUs: `pip install unsloth`. Read our [Blackwell Guide](https://docs.unsloth.ai/basics/training-llms-with-blackwell-rtx-50-series-and-unsloth) and [DGX Spark Guide](https://docs.unsloth.ai/new/fine-tuning-llms-with-nvidia-dgx-spark-and-unsloth) for more details.
## 🦥 Unsloth News
- New RoPE & MLP **Triton Kernels** & **Auto Packing**: 3x faster training & 30% less VRAM. [Blog](https://docs.unsloth.ai/new/3x-faster-training-packing)
- **Ministral 3** by Mistral: Run Ministral 3 or fine-tune with our vision or RL sodoku notebook. [Guide](https://docs.unsloth.ai/new/ministral-3) • [Notebooks](https://docs.unsloth.ai/new/ministral-3#fine-tuningb)
- **500K Context**: Training a 20B model with >500K context is now possible on an 80GB GPU. [Blog](https://docs.unsloth.ai/new/500k-context-length-fine-tuning)
- **FP8 Reinforcement Learning**: You can now do FP8 GRPO on consumer GPUs. [Blog](https://docs.unsloth.ai/new/fp8-reinforcement-learning) • [Notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Qwen3_8B_FP8_GRPO.ipynb)
- **DeepSeek-OCR**: Fine-tune to improve language understanding by 89%. [Guide](https://docs.unsloth.ai/new/deepseek-ocr-run-and-fine-tune) • [Notebook](https://colab.research.google.com/github/unslothai/notebooks/blob/main/nb/Deepseek_OCR_(3B).ipynb)
- **Docker**: Use Unsloth with no setup & environment issues with our new image. [Guide](https://docs.unsloth.ai/new/how-to-train-llms-with-unsloth-and-docker) • [Docker image](https://hub.docker.com/r/unsloth/unsloth)
- **gpt-oss RL**: Introducing the fastest possible inference for gpt-oss RL! [Read blog](https://docs.unsloth.ai/new/gpt-oss-reinforcement-learning)
- **Vision RL**: You can now train VLMs with GRPO or GSPO in Unsloth! [Read guide](https://docs.unsloth.ai/new/vision-reinforcement-learning-vlm-rl)
- **Quantization-Aware Training**: We collabed with Pytorch, recovering ~70% accuracy. [Read blog](https://docs.unsloth.ai/new/quantization-aware-training-qat)
- **gpt-oss** by OpenAI: Read our [Unsloth Flex Attention](https://docs.unsloth.ai/new/long-context-gpt-oss-training) blog and [gpt-oss Guide](https://docs.unsloth.ai/basics/gpt-oss). 20B works on 14GB VRAM. 120B on 65GB.
<details>
<summary>Click for more news</summary>
- **Quantization-Aware Training**: We collabed with Pytorch, recovering ~70% accuracy. [Read blog](https://docs.unsloth.ai/new/quantization-aware-training-qat)
- **Memory-efficient RL**: We're introducing even better RL. Our new kernels & algos allows faster RL with 50% less VRAM & 10× more context. [Read blog](https://docs.unsloth.ai/new/memory-efficient-rl)
- **Gemma 3n** by Google: [Read Blog](https://docs.unsloth.ai/basics/gemma-3n-how-to-run-and-fine-tune). We [uploaded GGUFs, 4-bit models](https://huggingface.co/collections/unsloth/gemma-3n-685d3874830e49e1c93f9339).
- **[Text-to-Speech (TTS)](https://docs.unsloth.ai/basics/text-to-speech-tts-fine-tuning)** is now supported, including `sesame/csm-1b` and STT `openai/whisper-large-v3`.

View file

@ -55,12 +55,12 @@ huggingfacenotorch = [
"huggingface_hub>=0.34.0",
"hf_transfer",
"diffusers",
"transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.2",
"transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.3",
"trl>=0.18.2,!=0.19.0,<=0.24.0",
]
huggingface = [
"unsloth[huggingfacenotorch]",
"unsloth_zoo>=2025.11.5",
"unsloth_zoo>=2025.12.4",
"torchvision",
"unsloth[triton]",
]
@ -285,10 +285,16 @@ cu130onlytorch290 = [
"xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.33.post1-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
]
cu126onlytorch291 = [
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.33.post2-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu126/xformers-0.0.33.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
]
cu128onlytorch291 = [
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.33.post2-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu128/xformers-0.0.33.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
]
cu130onlytorch291 = [
"xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.33.post2-cp39-abi3-manylinux_2_28_x86_64.whl ; ('linux' in sys_platform)",
"xformers @ https://download.pytorch.org/whl/cu130/xformers-0.0.33.post2-cp39-abi3-win_amd64.whl ; (sys_platform == 'win32')",
]
cu118 = [
"unsloth[huggingface]",
@ -517,10 +523,10 @@ colab-ampere-torch220 = [
"flash-attn>=2.6.3 ; ('linux' in sys_platform)",
]
colab-new = [
"unsloth_zoo>=2025.11.5",
"unsloth_zoo>=2025.12.4",
"packaging",
"tyro",
"transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.2",
"transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,<=4.57.3",
"datasets>=3.4.1,!=4.0.*,!=4.1.0,<4.4.0",
"sentencepiece>=0.2.0",
"tqdm",

View file

@ -0,0 +1,272 @@
# Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Unit tests for packed-attention mask helpers with sliding-window logic."""
import math
import torch
from unsloth.utils import attention_dispatch
from unsloth.utils import packing as packing_utils
def _make_seq_info(lengths):
lengths = torch.tensor(lengths, dtype = torch.int32)
cu = torch.cat(
[
torch.zeros(1, dtype = torch.int32),
torch.cumsum(lengths, dim = 0, dtype = torch.int32),
]
)
max_len = int(lengths.max().item())
return lengths, cu, max_len
def test_sdpa_packed_attention_mask_sliding_window():
seq_info = _make_seq_info([5, 3])
mask = packing_utils.build_sdpa_packed_attention_mask(
seq_info,
dtype = torch.float32,
device = torch.device("cpu"),
sliding_window = 3,
)
assert mask.shape == (1, 1, 8, 8)
block_first = mask[0, 0, :5, :5]
upper = torch.triu(torch.ones_like(block_first), diagonal = 1).bool()
assert torch.all(block_first[upper] == float("-inf"))
assert block_first[3, 0].item() == float("-inf")
assert block_first[4, 1].item() == float("-inf")
assert block_first[4, 2].item() > -math.inf
assert mask[0, 0, 0, 6].item() == float("-inf")
def test_xformers_block_mask_sliding_window(monkeypatch):
class _FakeMask:
def __init__(self, lengths, window = None):
self.lengths = lengths
self.window = window
@classmethod
def from_seqlens(cls, lengths):
return cls(tuple(lengths))
def make_local_attention(self, window_size):
return _FakeMask(self.lengths, window = window_size)
monkeypatch.setattr(packing_utils, "_XFormersBlockMask", _FakeMask, raising = False)
seq_info = _make_seq_info([4, 4])
mask = packing_utils.build_xformers_block_causal_mask(
seq_info,
sliding_window = 2,
)
assert isinstance(mask, _FakeMask)
assert mask.window == 2
def test_run_attention_sdpa_passes_sliding_window(monkeypatch):
seq_info = _make_seq_info([3, 2])
sliding_window = 2
original_builder = attention_dispatch.build_sdpa_packed_attention_mask
captured = {}
def _capture_builder(seq_info_arg, *, dtype, device, sliding_window = None):
captured["window"] = sliding_window
return original_builder(
seq_info_arg,
dtype = dtype,
device = device,
sliding_window = sliding_window,
)
monkeypatch.setattr(
attention_dispatch,
"build_sdpa_packed_attention_mask",
_capture_builder,
)
def _fake_sdpa(Q, K, V, **kwargs):
captured["mask"] = kwargs.get("attn_mask")
return torch.zeros_like(Q)
monkeypatch.setattr(attention_dispatch, "scaled_dot_product_attention", _fake_sdpa)
config = attention_dispatch.AttentionConfig(
backend = attention_dispatch.SDPA,
n_kv_heads = 1,
n_groups = 1,
)
context = attention_dispatch.AttentionContext(
bsz = 1,
q_len = 5,
kv_seq_len = 5,
n_heads = 1,
head_dim = 1,
requires_grad = False,
seq_info = seq_info,
attention_mask = None,
causal_mask = None,
sliding_window = sliding_window,
)
Q = torch.zeros(1, 1, 5, 1)
K = torch.zeros_like(Q)
V = torch.zeros_like(Q)
attention_dispatch.run_attention(
config = config,
context = context,
Q = Q,
K = K,
V = V,
)
assert captured["window"] == sliding_window
mask = captured["mask"]
assert mask is not None and mask.shape == (1, 1, 5, 5)
assert mask[0, 0, 4, 1].item() == float("-inf")
def test_run_attention_xformers_passes_sliding_window(monkeypatch):
seq_info = _make_seq_info([4])
sliding_window = 3
class _FakeBias:
pass
captured = {}
def _fake_builder(seq_info_arg, *, sliding_window = None, base_mask = None):
captured["window"] = sliding_window
captured["base"] = base_mask
return _FakeBias()
def _fake_attention(Q, K, V, attn_bias = None, **_):
captured["bias"] = attn_bias
return torch.zeros_like(Q)
monkeypatch.setattr(
attention_dispatch, "build_xformers_block_causal_mask", _fake_builder
)
monkeypatch.setattr(
attention_dispatch, "xformers_attention", _fake_attention, raising = False
)
monkeypatch.setattr(
attention_dispatch, "XFORMERS_BLOCK_DIAG_CLS", _FakeBias, raising = False
)
config = attention_dispatch.AttentionConfig(
backend = attention_dispatch.XFORMERS,
n_kv_heads = 1,
n_groups = 1,
)
context = attention_dispatch.AttentionContext(
bsz = 1,
q_len = 4,
kv_seq_len = 4,
n_heads = 1,
head_dim = 1,
requires_grad = False,
seq_info = seq_info,
attention_mask = None,
causal_mask = None,
sliding_window = sliding_window,
)
Q = torch.zeros(1, 1, 4, 1)
K = torch.zeros_like(Q)
V = torch.zeros_like(Q)
attention_dispatch.run_attention(
config = config,
context = context,
Q = Q,
K = K,
V = V,
)
assert captured["window"] == sliding_window
assert isinstance(captured["bias"], _FakeBias)
def test_run_attention_flash_varlen_receives_window_and_softcap(monkeypatch):
seq_info = _make_seq_info([4])
sliding_window = 3
softcap = 0.5
window_tuple = (sliding_window, sliding_window)
captured = {}
def _fake_flash_varlen(Q, K, V, cu_q, cu_k, max_q, max_k, **kwargs):
captured["kwargs"] = kwargs
return torch.zeros_like(Q)
monkeypatch.setattr(
attention_dispatch,
"flash_attn_varlen_func",
_fake_flash_varlen,
)
monkeypatch.setattr(attention_dispatch, "HAS_FLASH_ATTENTION", True)
config = attention_dispatch.AttentionConfig(
backend = attention_dispatch.FLASH_VARLEN,
n_kv_heads = 1,
n_groups = 1,
flash_varlen_kwargs = {
"dropout_p": 0.0,
"softmax_scale": 1.0,
"causal": True,
"softcap": softcap,
"window_size": window_tuple,
},
)
context = attention_dispatch.AttentionContext(
bsz = 1,
q_len = 4,
kv_seq_len = 4,
n_heads = 1,
head_dim = 2,
requires_grad = False,
seq_info = seq_info,
attention_mask = None,
causal_mask = None,
sliding_window = sliding_window,
)
Q = torch.zeros(1, 1, 4, 2)
K = torch.zeros_like(Q)
V = torch.zeros_like(Q)
attention_dispatch.run_attention(
config = config,
context = context,
Q = Q,
K = K,
V = V,
)
assert captured["kwargs"]["softcap"] == softcap
assert captured["kwargs"]["window_size"] == window_tuple
"""Unit tests for packed-attention mask helpers with sliding-window logic."""

391
tests/utils/test_packing.py Normal file
View file

@ -0,0 +1,391 @@
# Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from unsloth import FastLanguageModel
from unsloth.utils import attention_dispatch as attention_dispatch_utils
from unsloth.utils.packing import (
configure_padding_free,
configure_sample_packing,
enable_padding_free_metadata,
enable_sample_packing,
mask_packed_sequence_boundaries,
)
from contextlib import ExitStack
from types import SimpleNamespace
from unittest.mock import patch
import pytest
import torch
from datasets import Dataset
from trl import SFTConfig, SFTTrainer
from trl.trainer.sft_trainer import DataCollatorForLanguageModeling
def _build_packed_training_setup(tmp_path, device):
dtype = None
if device.type == "cuda":
if torch.cuda.is_bf16_supported():
dtype = torch.bfloat16
else:
dtype = torch.float16
try:
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "hf-internal-testing/tiny-random-LlamaForCausalLM",
max_seq_length = 64,
load_in_4bit = False,
dtype = dtype,
)
except OSError as exc: # pragma: no cover - offline CI
pytest.skip(f"Requires access to tiny llama checkpoint: {exc}")
model.to(device)
dataset = Dataset.from_dict(
{
"text": [
"Hello world!",
"Short sample.",
"This is a slightly longer packed example to test batching.",
"Another response to include in the batch.",
]
}
)
training_args = SFTConfig(
per_device_train_batch_size = 1,
per_device_eval_batch_size = 1,
gradient_accumulation_steps = 1,
dataset_text_field = "text",
max_length = 64,
logging_steps = 1,
max_steps = 1,
fp16 = device.type == "cuda" and not torch.cuda.is_bf16_supported(),
bf16 = device.type == "cuda" and torch.cuda.is_bf16_supported(),
dataset_num_proc = 1,
output_dir = str(tmp_path),
packing = True,
)
trainer = SFTTrainer(
model = model,
processing_class = tokenizer,
train_dataset = dataset,
args = training_args,
)
enable_sample_packing(model, trainer)
dataloader = trainer.get_train_dataloader()
batch = next(iter(dataloader))
model_device = next(model.parameters()).device
for key, value in list(batch.items()):
if torch.is_tensor(value):
batch[key] = value.to(model_device)
from unsloth.models import llama as llama_mod
return model, batch, trainer, llama_mod
def _trim_batch_to_total_tokens(data, total_tokens):
def _trim_tensor(t: torch.Tensor):
if t.ndim >= 2 and t.size(1) > total_tokens:
return t[:, :total_tokens].contiguous()
return t
trimmed = {}
for key, value in data.items():
if torch.is_tensor(value):
trimmed[key] = _trim_tensor(value)
else:
trimmed[key] = value
return trimmed
def test_mask_packed_sequence_boundaries_marks_single_row():
shift_labels = torch.arange(6, dtype = torch.long).view(1, 6)
changed = mask_packed_sequence_boundaries(
shift_labels,
torch.tensor([2, 1, 3], dtype = torch.int32),
)
assert changed is True
flat = shift_labels.view(-1)
assert flat[1].item() == -100
assert flat[2].item() == -100
assert flat[5].item() == -100
assert flat[0].item() != -100
def test_mask_packed_sequence_boundaries_across_multiple_rows():
shift_labels = torch.arange(10, dtype = torch.long).view(2, 5)
lengths = torch.tensor([3, 2, 4, 1], dtype = torch.int32)
changed = mask_packed_sequence_boundaries(shift_labels, lengths)
assert changed is True
flat = shift_labels.view(-1)
for idx in (2, 4, 8, 9):
assert flat[idx].item() == -100
assert torch.any(flat != -100)
def test_configure_sample_packing():
config = SimpleNamespace()
configure_sample_packing(config)
assert config.packing is True
assert config.padding_free is True
assert config.remove_unused_columns is False
def test_configure_padding_free():
config = SimpleNamespace(remove_unused_columns = True)
configure_padding_free(config)
assert config.padding_free is True
assert config.remove_unused_columns is False
class _DummyChild(torch.nn.Module):
def __init__(self):
super().__init__()
self.max_seq_length = 8
class _DummyModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.max_seq_length = 16
self.child = _DummyChild()
self.config = SimpleNamespace(_attn_implementation = "sdpa")
self.generation_config = SimpleNamespace(attn_implementation = "sdpa")
class _DummyTrainer:
def __init__(self):
self.args = SimpleNamespace(remove_unused_columns = True)
self.data_collator = DataCollatorForLanguageModeling(
pad_token_id = 0,
completion_only_loss = False,
padding_free = True,
return_position_ids = False,
return_tensors = "pt",
)
class _PaddingFreeCollator:
def __init__(self):
self.padding_free = True
self.return_position_ids = False
self.calls = 0
def torch_call(self, examples):
self.calls += 1
return {
"input_ids": torch.tensor([[0]], dtype = torch.long),
"examples_seen": self.calls,
}
def test_enable_sample_packing():
model = _DummyModel()
trainer = _DummyTrainer()
enable_sample_packing(model, trainer)
# model hierarchy should now allow packed overlength inputs
assert getattr(model, "_unsloth_allow_packed_overlength") is True
assert getattr(model.child, "_unsloth_allow_packed_overlength") is True
collator = trainer.data_collator
assert collator.return_position_ids is True
assert getattr(collator, "_unsloth_packing_wrapped") is True
examples = [
{
"input_ids": [0, 1, 2],
"labels": [0, 1, 2],
"seq_lengths": [2, 1],
},
{
"input_ids": [3, 4, 5],
"labels": [3, 4, 5],
"seq_lengths": [3],
},
]
batch = collator.torch_call(examples)
# packed lengths are aggregated into a single tensor
assert "packed_seq_lengths" in batch
assert torch.equal(
batch["packed_seq_lengths"],
torch.tensor([2, 1, 3], dtype = torch.int32),
)
assert batch["input_ids"].shape == (1, 6)
expected_positions = torch.tensor([0, 1, 0, 0, 1, 2], dtype = torch.long)
assert torch.equal(batch["position_ids"].view(-1)[:6], expected_positions)
def test_enable_sample_packing_trl_collator(tmp_path):
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model, _, trainer, _ = _build_packed_training_setup(tmp_path, device)
enable_sample_packing(model, trainer)
examples = [
{
"input_ids": [0, 1, 2],
"labels": [0, 1, 2],
"seq_lengths": [2, 1],
},
{
"input_ids": [3, 4, 5],
"labels": [3, 4, 5],
"seq_lengths": [3],
},
]
batch = trainer.data_collator.torch_call(examples)
assert batch["input_ids"].shape == (1, 6)
assert torch.equal(
batch["packed_seq_lengths"],
torch.tensor([2, 1, 3], dtype = torch.int32),
)
expected_positions = torch.tensor([0, 1, 0, 0, 1, 2], dtype = torch.long)
assert torch.equal(batch["position_ids"].view(-1)[:6], expected_positions)
if hasattr(trainer, "accelerator"):
trainer.accelerator.free_memory()
def test_enable_padding_free_metadata():
model = _DummyModel()
trainer = SimpleNamespace(
args = SimpleNamespace(remove_unused_columns = True),
data_collator = _PaddingFreeCollator(),
)
enable_padding_free_metadata(model, trainer)
assert getattr(model, "_unsloth_allow_packed_overlength") is True
assert getattr(model.child, "_unsloth_allow_packed_overlength") is True
collator = trainer.data_collator
assert collator.return_position_ids is True
assert getattr(collator, "_unsloth_padding_free_lengths_wrapped") is True
examples = [
{"input_ids": [0, 1, 2]},
{"input_ids": [3, 4]},
]
batch = collator.torch_call(examples)
assert torch.equal(
batch["packed_seq_lengths"],
torch.tensor([3, 2], dtype = torch.int32),
)
assert trainer.args.remove_unused_columns is False
def test_packing_sdpa(tmp_path):
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
model, batch, trainer, llama_mod = _build_packed_training_setup(tmp_path, device)
assert "packed_seq_lengths" in batch
assert "attention_mask" not in batch
assert batch["packed_seq_lengths"].dtype == torch.int32
total_tokens = batch["input_ids"].size(-1)
assert int(batch["packed_seq_lengths"].sum().item()) == total_tokens
packed_tokens = int(batch["packed_seq_lengths"].sum().item())
assert "position_ids" in batch
flat_positions = batch["position_ids"].reshape(-1)[:packed_tokens]
expected_positions = torch.cat(
[
torch.arange(length, dtype = torch.long)
for length in batch["packed_seq_lengths"].tolist()
]
)
assert torch.equal(flat_positions.cpu(), expected_positions)
inputs = _trim_batch_to_total_tokens(batch, packed_tokens)
seq_info = llama_mod.get_packed_info_from_kwargs(
{"packed_seq_lengths": batch["packed_seq_lengths"]},
inputs["input_ids"].device,
)
assert seq_info is not None
original_mask = attention_dispatch_utils.build_sdpa_packed_attention_mask
mask_calls = []
captured_loss_labels = {}
def _capture_mask(seq_info, dtype, device, *, sliding_window = None):
mask_calls.append(tuple(seq_info[0].tolist()))
return original_mask(
seq_info,
dtype = dtype,
device = device,
sliding_window = sliding_window,
)
def _capture_loss(*, logits, labels, **loss_kwargs):
captured_loss_labels["labels"] = labels.detach().to("cpu")
return torch.zeros((), device = logits.device, dtype = logits.dtype)
with ExitStack() as stack:
stack.enter_context(
patch.object(attention_dispatch_utils, "HAS_FLASH_ATTENTION", False)
)
stack.enter_context(
patch.object(attention_dispatch_utils, "HAS_XFORMERS", False)
)
stack.enter_context(
patch.object(
attention_dispatch_utils,
"build_sdpa_packed_attention_mask",
side_effect = _capture_mask,
)
)
stack.enter_context(
patch.object(
llama_mod,
"fast_cross_entropy_loss",
side_effect = _capture_loss,
)
)
with torch.no_grad():
outputs = model(**inputs)
assert mask_calls, "SDPA packed mask was not constructed"
assert outputs.loss is not None
assert "labels" in captured_loss_labels
flat_loss_labels = captured_loss_labels["labels"].reshape(-1)
boundaries = (
torch.cumsum(
batch["packed_seq_lengths"].to(device = "cpu", dtype = torch.long), dim = 0
)
- 1
)
for idx in boundaries.tolist():
assert flat_loss_labels[idx].item() == -100
assert torch.any(flat_loss_labels != -100)
if hasattr(trainer, "accelerator"):
trainer.accelerator.free_memory()

View file

@ -34,23 +34,24 @@ import os
def run(args):
import torch
from unsloth import FastLanguageModel
from datasets import load_dataset
from transformers.utils import strtobool
from trl import SFTTrainer, SFTConfig
from transformers import TrainingArguments
from unsloth import is_bfloat16_supported
from unsloth.models.loader_utils import prepare_device_map
import logging
logging.getLogger("hf-to-gguf").setLevel(logging.WARNING)
# Load model and tokenizer
device_map, distributed = prepare_device_map()
model, tokenizer = FastLanguageModel.from_pretrained(
model_name = args.model_name,
max_seq_length = args.max_seq_length,
dtype = args.dtype,
load_in_4bit = args.load_in_4bit,
device_map = device_map,
)
# Configure PEFT model
@ -112,6 +113,7 @@ def run(args):
# Configure training arguments
training_args = SFTConfig(
per_device_train_batch_size = args.per_device_train_batch_size,
per_device_eval_batch_size = args.per_device_eval_batch_size,
gradient_accumulation_steps = args.gradient_accumulation_steps,
warmup_steps = args.warmup_steps,
max_steps = args.max_steps,
@ -127,7 +129,8 @@ def run(args):
report_to = args.report_to,
max_length = args.max_seq_length,
dataset_num_proc = 2,
packing = False,
ddp_find_unused_parameters = False if distributed else None,
packing = args.packing,
)
# Initialize trainer
@ -138,8 +141,7 @@ def run(args):
args = training_args,
)
# Train model
trainer_stats = trainer.train()
trainer.train()
# Save model
if args.save_model:
@ -164,13 +166,15 @@ def run(args):
else:
print(f"Saving model with quantization method: {args.quantization}")
model.save_pretrained_gguf(
args.save_path, tokenizer, quantization_method = args.quantization
args.save_path,
tokenizer,
quantization_method = args.quantization,
)
if args.push_model:
model.push_to_hub_gguf(
hub_path = args.hub_path,
hub_token = args.hub_token,
quantization_method = quantization_method,
quantization_method = args.quantization,
)
else:
model.save_pretrained_merged(args.save_path, tokenizer, args.save_method)
@ -181,7 +185,6 @@ def run(args):
if __name__ == "__main__":
# Define argument parser
parser = argparse.ArgumentParser(
description = "🦥 Fine-tune your llm faster using unsloth!"
)
@ -218,7 +221,8 @@ if __name__ == "__main__":
)
lora_group = parser.add_argument_group(
"🧠 LoRA Options", "These options are used to configure the LoRA model."
"🧠 LoRA Options",
"These options are used to configure the LoRA model.",
)
lora_group.add_argument(
"--r",
@ -239,7 +243,10 @@ if __name__ == "__main__":
help = "LoRA dropout rate, default is 0.0 which is optimized.",
)
lora_group.add_argument(
"--bias", type = str, default = "none", help = "Bias setting for LoRA"
"--bias",
type = str,
default = "none",
help = "Bias setting for LoRA",
)
lora_group.add_argument(
"--use_gradient_checkpointing",
@ -254,10 +261,15 @@ if __name__ == "__main__":
help = "Random state for reproducibility, default is 3407.",
)
lora_group.add_argument(
"--use_rslora", action = "store_true", help = "Use rank stabilized LoRA"
"--use_rslora",
action = "store_true",
help = "Use rank stabilized LoRA",
)
lora_group.add_argument(
"--loftq_config", type = str, default = None, help = "Configuration for LoftQ"
"--loftq_config",
type = str,
default = None,
help = "Configuration for LoftQ",
)
training_group = parser.add_argument_group("🎓 Training Options")
@ -267,6 +279,12 @@ if __name__ == "__main__":
default = 2,
help = "Batch size per device during training, default is 2.",
)
training_group.add_argument(
"--per_device_eval_batch_size",
type = int,
default = 4,
help = "Batch size per device during evaluation, default is 4.",
)
training_group.add_argument(
"--gradient_accumulation_steps",
type = int,
@ -280,7 +298,10 @@ if __name__ == "__main__":
help = "Number of warmup steps, default is 5.",
)
training_group.add_argument(
"--max_steps", type = int, default = 400, help = "Maximum number of training steps."
"--max_steps",
type = int,
default = 400,
help = "Maximum number of training steps.",
)
training_group.add_argument(
"--learning_rate",
@ -289,7 +310,10 @@ if __name__ == "__main__":
help = "Learning rate, default is 2e-4.",
)
training_group.add_argument(
"--optim", type = str, default = "adamw_8bit", help = "Optimizer type."
"--optim",
type = str,
default = "adamw_8bit",
help = "Optimizer type.",
)
training_group.add_argument(
"--weight_decay",
@ -309,8 +333,12 @@ if __name__ == "__main__":
default = 3407,
help = "Seed for reproducibility, default is 3407.",
)
training_group.add_argument(
"--packing",
action = "store_true",
help = "Enable padding-free sample packing via TRL's bin packer.",
)
# Report/Logging arguments
report_group = parser.add_argument_group("📊 Report Options")
report_group.add_argument(
"--report_to",
@ -331,19 +359,31 @@ if __name__ == "__main__":
"all",
"none",
],
help = "The list of integrations to report the results and logs to. Supported platforms are: \n\t\t 'azure_ml', 'clearml', 'codecarbon', 'comet_ml', 'dagshub', 'dvclive', 'flyte', 'mlflow', 'neptune', 'tensorboard', and 'wandb'. Use 'all' to report to all integrations installed, 'none' for no integrations.",
help = (
"The list of integrations to report the results and logs to. Supported platforms are:\n\t\t "
"'azure_ml', 'clearml', 'codecarbon', 'comet_ml', 'dagshub', 'dvclive', 'flyte', "
"'mlflow', 'neptune', 'tensorboard', and 'wandb'. Use 'all' to report to all integrations "
"installed, 'none' for no integrations."
),
)
report_group.add_argument(
"--logging_steps", type = int, default = 1, help = "Logging steps, default is 1"
"--logging_steps",
type = int,
default = 1,
help = "Logging steps, default is 1",
)
# Saving and pushing arguments
save_group = parser.add_argument_group("💾 Save Model Options")
save_group.add_argument(
"--output_dir", type = str, default = "outputs", help = "Output directory"
"--output_dir",
type = str,
default = "outputs",
help = "Output directory",
)
save_group.add_argument(
"--save_model", action = "store_true", help = "Save the model after training"
"--save_model",
action = "store_true",
help = "Save the model after training",
)
save_group.add_argument(
"--save_method",
@ -358,14 +398,20 @@ if __name__ == "__main__":
help = "Convert the model to GGUF after training",
)
save_group.add_argument(
"--save_path", type = str, default = "model", help = "Path to save the model"
"--save_path",
type = str,
default = "model",
help = "Path to save the model",
)
save_group.add_argument(
"--quantization",
type = str,
default = "q8_0",
nargs = "+",
help = "Quantization method for saving the model. common values ('f16', 'q4_k_m', 'q8_0'), Check our wiki for all quantization methods https://github.com/unslothai/unsloth/wiki#saving-to-gguf ",
help = (
"Quantization method for saving the model. common values ('f16', 'q4_k_m', 'q8_0'), "
"Check our wiki for all quantization methods https://github.com/unslothai/unsloth/wiki#saving-to-gguf"
),
)
push_group = parser.add_argument_group("🚀 Push Model Options")
@ -386,7 +432,9 @@ if __name__ == "__main__":
help = "Path on Hugging Face hub to push the model",
)
push_group.add_argument(
"--hub_token", type = str, help = "Token for pushing the model to Hugging Face hub"
"--hub_token",
type = str,
help = "Token for pushing the model to Hugging Face hub",
)
args = parser.parse_args()

View file

@ -17,16 +17,27 @@ from packaging.version import Version
import os, re, subprocess, inspect, functools
import numpy as np
# Fix some issues before importing other packages
from .import_fixes import fix_message_factory_issue
fix_message_factory_issue()
del fix_message_factory_issue
# Log Unsloth is being used
os.environ["UNSLOTH_IS_PRESENT"] = "1"
# Check if modules that need patching are already imported
critical_modules = ["trl", "transformers", "peft"]
already_imported = [mod for mod in critical_modules if mod in sys.modules]
# Fix some issues before importing other packages
from .import_fixes import (
fix_message_factory_issue,
check_fbgemm_gpu_version,
torchvision_compatibility_check,
)
fix_message_factory_issue()
check_fbgemm_gpu_version()
torchvision_compatibility_check()
del fix_message_factory_issue
del check_fbgemm_gpu_version
del torchvision_compatibility_check
# This check is critical because Unsloth optimizes these libraries by modifying
# their code at import time. If they're imported first, the original (slower,
# more memory-intensive) implementations will be used instead of Unsloth's
@ -35,7 +46,7 @@ if already_imported:
# stacklevel=2 makes warning point to user's import line rather than this library code,
# showing them exactly where to fix the import order in their script
warnings.warn(
f"WARNING: Unsloth should be imported before {', '.join(already_imported)} "
f"WARNING: Unsloth should be imported before [{', '.join(already_imported)}] "
f"to ensure all optimizations are applied. Your code may run slower or encounter "
f"memory issues without these optimizations.\n\n"
f"Please restructure your imports with 'import unsloth' at the top of your file.",
@ -55,8 +66,6 @@ os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python"
# "pinned_use_cuda_host_register:True,"\
# "pinned_num_register_threads:8"
# Log Unsloth is being used
os.environ["UNSLOTH_IS_PRESENT"] = "1"
from importlib.metadata import version as importlib_version
from importlib.metadata import PackageNotFoundError
@ -64,7 +73,7 @@ from importlib.metadata import PackageNotFoundError
# Check for unsloth_zoo
try:
unsloth_zoo_version = importlib_version("unsloth_zoo")
if Version(unsloth_zoo_version) < Version("2025.11.2"):
if Version(unsloth_zoo_version) < Version("2025.12.4"):
print(
"Unsloth: Please update Unsloth and Unsloth-Zoo to the latest version!\n"
"Do this via `pip install --upgrade --force-reinstall --no-cache-dir --no-deps unsloth unsloth_zoo`"
@ -110,25 +119,34 @@ from unsloth_zoo.device_type import (
from .import_fixes import (
fix_xformers_performance_issue,
fix_vllm_aimv2_issue,
fix_vllm_guided_decoding_params,
ignore_logger_messages,
patch_ipykernel_hf_xet,
patch_trackio,
patch_datasets,
patch_enable_input_require_grads,
fix_openenv_no_vllm,
)
fix_xformers_performance_issue()
fix_vllm_aimv2_issue()
fix_vllm_guided_decoding_params()
ignore_logger_messages()
patch_ipykernel_hf_xet()
patch_trackio()
patch_datasets()
patch_enable_input_require_grads()
fix_openenv_no_vllm()
del fix_xformers_performance_issue
del fix_vllm_aimv2_issue
del fix_vllm_guided_decoding_params
del ignore_logger_messages
del patch_ipykernel_hf_xet
del patch_trackio
del patch_datasets
del patch_enable_input_require_grads
del fix_openenv_no_vllm
# Torch 2.4 has including_emulation
if DEVICE_TYPE == "cuda":

View file

@ -16,10 +16,31 @@ import os
import importlib.util
from pathlib import Path
from importlib.metadata import version as importlib_version
from packaging.version import Version
from packaging.version import Version as TrueVersion
import re
import logging
# Cannot import logger here since it'll import transformers
# from unsloth_zoo.log import logger
UNSLOTH_ENABLE_LOGGING = os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") == "1"
def Version(version):
try:
new_version = str(version)
new_version = re.match(r"[0-9\.]{1,}", new_version)
if new_version is None:
raise Exception(str(e))
new_version = new_version.group(0).rstrip(".")
if new_version != version:
new_version += ".1" # Add .1 for dev / alpha / beta / rc
return TrueVersion(new_version)
except:
from inspect import getframeinfo, stack
caller = getframeinfo(stack()[1][0])
raise RuntimeError(
f"Unsloth: Could not get version for `{version}`\n"
f"File name = [{caller.filename}] Line number = [{caller.lineno}]"
)
# Ignore logging messages
@ -49,9 +70,10 @@ def fix_message_factory_issue():
def GetPrototype(self, *args, **kwargs):
return
from unsloth_zoo.log import logger
if not hasattr(google.protobuf.message_factory, "MessageFactory"):
if UNSLOTH_ENABLE_LOGGING:
print("Unsloth: Patching protobuf.MessageFactory as it doesn't exist")
logger.info("Unsloth: Patching protobuf.MessageFactory as it doesn't exist")
google.protobuf.message_factory.MessageFactory = MessageFactory
elif (
hasattr(google.protobuf.message_factory, "MessageFactory")
@ -61,8 +83,7 @@ def fix_message_factory_issue():
and not hasattr(google.protobuf.message_factory, "GetMessageClass")
):
google.protobuf.message_factory.MessageFactory = MessageFactory
if UNSLOTH_ENABLE_LOGGING:
print("Unsloth: Patching protobuf.MessageFactory as it doesn't exist")
logger.info("Unsloth: Patching protobuf.MessageFactory as it doesn't exist")
elif (
hasattr(google.protobuf.message_factory, "MessageFactory")
and not hasattr(
@ -76,8 +97,7 @@ def fix_message_factory_issue():
return GetMessageClass(descriptor)
google.protobuf.message_factory.MessageFactory.GetPrototype = GetPrototype
if UNSLOTH_ENABLE_LOGGING:
print("Unsloth: Patching protobuf.MessageFactory.GetPrototype")
logger.info("Unsloth: Patching protobuf.MessageFactory.GetPrototype")
pass
except:
pass
@ -89,6 +109,8 @@ def fix_xformers_performance_issue():
return
xformers_version = importlib_version("xformers")
if Version(xformers_version) < Version("0.0.29"):
from unsloth_zoo.log import logger
xformers_location = importlib.util.find_spec("xformers").origin
xformers_location = os.path.split(xformers_location)[0]
cutlass = Path(xformers_location) / "ops" / "fmha" / "cutlass.py"
@ -105,13 +127,11 @@ def fix_xformers_performance_issue():
f.seek(0)
f.write(text)
f.truncate()
if UNSLOTH_ENABLE_LOGGING:
print(
"Unsloth: Patching Xformers to fix some performance issues."
)
logger.info(
"Unsloth: Patching Xformers to fix some performance issues."
)
except Exception as e:
if UNSLOTH_ENABLE_LOGGING:
print(f"Unsloth: Failed patching Xformers with error = {str(e)}")
logger.info(f"Unsloth: Failed patching Xformers with error = {str(e)}")
# ValueError: 'aimv2' is already used by a Transformers config, pick another name.
@ -120,6 +140,8 @@ def fix_vllm_aimv2_issue():
return
vllm_version = importlib_version("vllm")
if Version(vllm_version) < Version("0.10.1"):
from unsloth_zoo.log import logger
vllm_version = importlib.util.find_spec("vllm").origin
vllm_version = os.path.split(vllm_version)[0]
ovis_config = Path(vllm_version) / "transformers_utils" / "configs" / "ovis.py"
@ -146,13 +168,27 @@ def fix_vllm_aimv2_issue():
f.seek(0)
f.write(text)
f.truncate()
if UNSLOTH_ENABLE_LOGGING:
print(
"Unsloth: Patching vLLM to fix `'aimv2' is already used by a Transformers config, pick another name.`"
)
logger.info(
"Unsloth: Patching vLLM to fix `'aimv2' is already used by a Transformers config, pick another name.`"
)
except Exception as e:
if UNSLOTH_ENABLE_LOGGING:
print(f"Unsloth: Failed patching vLLM with error = {str(e)}")
logger.info(f"Unsloth: Failed patching vLLM with error = {str(e)}")
def fix_vllm_guided_decoding_params():
if importlib.util.find_spec("vllm") is None:
return
# GuidedDecodingParmas is renamed to StructuredOutputsParams in vLLM
# https://github.com/vllm-project/vllm/pull/22772/files
# trl still wants to use GuidedDecodingParams. This is a temporary patch till trl updates
import vllm
try:
from vllm.sampling_params import GuidedDecodingParams
except ImportError:
vllm.sampling_params.GuidedDecodingParams = (
vllm.sampling_params.StructuredOutputsParams
)
def ignore_logger_messages():
@ -223,3 +259,168 @@ def patch_datasets():
f"#### Unsloth: Using `datasets = {str(datasets_version)}` will cause recursion errors.\n"
"Please downgrade datasets to `datasets==4.3.0"
)
def check_fbgemm_gpu_version():
if importlib.util.find_spec("fbgemm_gpu") is None:
return
try:
fbgemm_gpu_version = importlib_version("fbgemm_gpu_genai")
except:
return
# We noticed some SegFault or bad alloc errors on lower versions of fbgemm_gpu.
if Version(fbgemm_gpu_version) < Version("1.4.0"):
raise ImportError(
f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected. It might cause unexpected issues like segmentation faults. Please uninstall the current one by doing `pip uninstall fbgemm-gpu` && `pip install fbgemm-gpu` to install fbgemm-gpu 1.4.0 or newer!"
)
from unsloth_zoo.log import logger
logger.info(f"Unsloth: fbgemm_gpu_genai=={fbgemm_gpu_version} detected.")
def patch_enable_input_require_grads():
"""
Patch transformers PreTrainedModel.enable_input_require_grads to handle vision models
that raise NotImplementedError from get_input_embeddings().
"""
import inspect
from transformers import PreTrainedModel
# Check if the original function iterates over self.modules() instead of just returning the enable_input_require_grads
# Ref: https://github.com/huggingface/transformers/pull/41993/files#diff-6b72b98c4c2dcfc6cc606843917733f5d858374fbc22a735ff483bbc0c1e63eaL1979-R1996
try:
original_source = inspect.getsource(PreTrainedModel.enable_input_require_grads)
except:
return
# Only patch if the new pattern exists (iterating over self.modules())
if "for module in self.modules()" not in original_source:
return
def _patched_enable_input_require_grads(self):
def make_inputs_require_grads(module, input, output):
output.requires_grad_(True)
hooks = []
seen_modules = set()
for module in self.modules():
if not (
isinstance(module, PreTrainedModel)
and hasattr(module, "get_input_embeddings")
):
continue
try:
input_embeddings = module.get_input_embeddings()
except NotImplementedError:
# Vision models may not implement get_input_embeddings - skip them
# For GLM V4.6 for example, this skips only `self.visual`
continue
if input_embeddings is None:
continue
embedding_id = id(input_embeddings)
if embedding_id in seen_modules:
continue
seen_modules.add(embedding_id)
hooks.append(
input_embeddings.register_forward_hook(make_inputs_require_grads)
)
self._require_grads_hooks = hooks
if hooks:
self._require_grads_hook = hooks[0]
PreTrainedModel.enable_input_require_grads = _patched_enable_input_require_grads
from unsloth_zoo.log import logger
logger.info(
"Unsloth: Patched enable_input_require_grads for vision model compatibility"
)
def torchvision_compatibility_check():
if importlib.util.find_spec("torch") is None:
raise ImportError("Unsloth: torch not found. Please install torch first.")
if importlib.util.find_spec("torchvision") is None:
return
torch_version = importlib_version("torch")
torchvision_version = importlib_version("torchvision")
# Torch version -> minimum required torchvision version
# See https://pytorch.org/get-started/previous-versions/
TORCH_TORCHVISION_COMPAT = [
("2.9.0", "0.24.0"),
("2.8.0", "0.23.0"),
("2.7.0", "0.22.0"),
("2.6.0", "0.21.0"),
("2.5.0", "0.20.0"),
("2.4.0", "0.19.0"),
]
required_torchvision = None
for min_torch, min_torchvision in TORCH_TORCHVISION_COMPAT:
if Version(torch_version) >= Version(min_torch):
required_torchvision = min_torchvision
break
if required_torchvision is None:
# Torch version not in compatibility table, skip check
return
if Version(torchvision_version) < Version(required_torchvision):
raise ImportError(
f"Unsloth: torch=={torch_version} requires torchvision>={required_torchvision}, "
f"but found torchvision=={torchvision_version}. "
f"Please refer to https://pytorch.org/get-started/previous-versions/ for more information."
)
from unsloth_zoo.log import logger
logger.info(
f"Unsloth: torch=={torch_version} and torchvision=={torchvision_version} are compatible."
)
# Fix TRL OpenEnv 0.26 NameError: name 'SamplingParams' is not defined
def fix_openenv_no_vllm():
if importlib.util.find_spec("trl") is None:
return
trl_location = importlib.util.find_spec("trl").origin
trl_location = os.path.split(trl_location)[0]
openenv = Path(trl_location) / "experimental" / "openenv" / "utils.py"
if not openenv.exists():
return
from unsloth_zoo.log import logger
try:
with open(openenv, "r+", encoding = "utf-8") as f:
text = f.read()
bad = (
"if is_vllm_available():\n"
" from vllm import SamplingParams\n"
" from vllm.sampling_params import GuidedDecodingParams\n"
)
if bad + "\n" + "\n" in text:
text = text.replace(
bad + "\n" + "\n",
bad
+ (
"else:\n"
" from typing import Any\n"
" SamplingParams = Any\n"
" GuidedDecodingParams = Any\n"
"\n"
),
)
f.seek(0)
f.write(text)
f.truncate()
logger.info(
"Unsloth: Patching TRL OpenEnv to fix SamplingParams not defined"
)
except Exception as e:
logger.info(f"Unsloth: Failed patching TRL OpenEnv with error = {str(e)}")

View file

@ -160,7 +160,7 @@ class Fast_RMS_Layernorm(torch.autograd.Function):
def forward(ctx, X: torch.Tensor, W: torch.Tensor, eps: float, gemma: bool = False):
shape = X.shape
dim: int = shape[-1]
X = X.view(-1, dim)
X = X.reshape(-1, dim)
n_rows: int
n_cols: int
n_rows, n_cols = X.shape
@ -199,7 +199,7 @@ class Fast_RMS_Layernorm(torch.autograd.Function):
def backward(ctx, dY: torch.Tensor):
shape = dY.shape
dim: int = shape[-1]
dY = dY.view(-1, dim)
dY = dY.reshape(-1, dim)
X, W, r = ctx.saved_tensors
n_rows: int
n_cols: int

View file

@ -1,22 +1,111 @@
# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved.
# Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# http://www.apache.org/licenses/LICENSE-2.0
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import triton
import triton.language as tl
import torch
from ..device_type import DEVICE_COUNT
from .utils import calculate_settings, torch_gpu_device, torch_device_stream
def _rope_embedding_QK(
Q,
Q_batch_stride,
Q_head_stride,
Q_seq_stride,
K,
K_batch_stride,
K_head_stride,
K_seq_stride,
cos,
cos_row_stride,
sin,
sin_row_stride,
rope_embedding_indices,
seqlen,
head_dim: tl.constexpr,
n_heads_K: tl.constexpr,
BACKWARD_PASS: tl.constexpr,
HAS_ROPE_INDICES: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
):
row_position = tl.program_id(0)
head_position = tl.program_id(1)
col_offsets = tl.arange(0, BLOCK_SIZE)
half_head_dim = head_dim // 2
mask = col_offsets < half_head_dim
if HAS_ROPE_INDICES:
rot_position = tl.load(
rope_embedding_indices + row_position,
eviction_policy = "evict_first",
).to(tl.int32)
else:
rot_position = row_position % seqlen
cos_ptr = cos + rot_position * cos_row_stride
sin_ptr = sin + rot_position * sin_row_stride
sin1 = tl.load(
sin_ptr + col_offsets,
mask = mask,
other = 0,
)
cos1 = tl.load(
cos_ptr + col_offsets,
mask = mask,
other = 0,
)
if BACKWARD_PASS:
sin1 = -sin1
batch_id = row_position // seqlen
seq_index = row_position - batch_id * seqlen
q_ptr = (
Q
+ batch_id * Q_batch_stride
+ head_position * Q_head_stride
+ seq_index * Q_seq_stride
)
q0 = tl.load(q_ptr + col_offsets, mask = mask, other = 0)
q1 = tl.load(q_ptr + half_head_dim + col_offsets, mask = mask, other = 0)
tl.store(q_ptr + col_offsets, q0 * cos1 - q1 * sin1, mask = mask)
tl.store(q_ptr + half_head_dim + col_offsets, q1 * cos1 + q0 * sin1, mask = mask)
if head_position < n_heads_K:
k_ptr = (
K
+ batch_id * K_batch_stride
+ head_position * K_head_stride
+ seq_index * K_seq_stride
)
k0 = tl.load(k_ptr + col_offsets, mask = mask, other = 0)
k1 = tl.load(k_ptr + half_head_dim + col_offsets, mask = mask, other = 0)
tl.store(k_ptr + col_offsets, k0 * cos1 - k1 * sin1, mask = mask)
tl.store(k_ptr + half_head_dim + col_offsets, k1 * cos1 + k0 * sin1, mask = mask)
_rope_embedding_QK = triton.jit(_rope_embedding_QK)
_rope_embedding_QK = triton.heuristics(
{
"BACKWARD_PASS": lambda args: bool(args["BACKWARD_PASS"]),
"HAS_ROPE_INDICES": lambda args: bool(args["HAS_ROPE_INDICES"]),
}
)(_rope_embedding_QK)
ROPE_GROUP_SIZE: int = 4
@ -102,7 +191,7 @@ class Fast_RoPE_Embedding(torch.autograd.Function):
n_heads: int
head_dim: int
batch, seq_len, n_heads, head_dim = Q.shape
Q = Q.view(batch * seq_len, n_heads * head_dim)
Q = Q.reshape(batch * seq_len, n_heads * head_dim)
n_rows: int
n_cols: int
n_rows, n_cols = Q.shape
@ -143,7 +232,7 @@ class Fast_RoPE_Embedding(torch.autograd.Function):
ctx.n_groups = n_groups
ctx.cos = cos
ctx.sin = sin
return Q.view(batch, seq_len, n_heads, head_dim)
return Q.reshape(batch, seq_len, n_heads, head_dim)
@staticmethod
def backward(ctx, dY):
@ -153,7 +242,6 @@ class Fast_RoPE_Embedding(torch.autograd.Function):
head_dim: int
batch, seq_len, n_heads, head_dim = dY.shape
dY = dY.reshape(batch * seq_len, n_heads * head_dim)
# Must be reshape not view
n_rows: int
n_cols: int
n_rows, n_cols = dY.shape
@ -181,7 +269,7 @@ class Fast_RoPE_Embedding(torch.autograd.Function):
BLOCK_SIZE = ctx.BLOCK_SIZE,
num_warps = ctx.num_warps,
)
dY = dY.view(batch, seq_len, n_heads, head_dim)
dY = dY.reshape(batch, seq_len, n_heads, head_dim)
return (
dY,
None,
@ -191,12 +279,150 @@ class Fast_RoPE_Embedding(torch.autograd.Function):
# [TODO] Unsure why RoPE Embedding is not torch.compiling properly
@torch.compiler.disable
def fast_rope_embedding(Q, K, cos, sin):
Q = Fast_RoPE_Embedding.apply(Q.transpose(1, 2), cos, sin).transpose(1, 2)
K = Fast_RoPE_Embedding.apply(K.transpose(1, 2), cos, sin).transpose(1, 2)
# synchronize before cat to avoid race condition
torch_device_stream(Q.device).synchronize()
return Q, K
def fast_rope_embedding(
Q,
K,
cos,
sin,
rope_embedding_indices = None,
):
if rope_embedding_indices is not None:
Q_out, K_out = Fast_RoPE_Embedding_QK.apply(
Q, K, cos, sin, rope_embedding_indices
)
else:
Q_out = Fast_RoPE_Embedding.apply(
Q.transpose(1, 2).contiguous(), cos, sin
).transpose(1, 2)
K_out = Fast_RoPE_Embedding.apply(
K.transpose(1, 2).contiguous(), cos, sin
).transpose(1, 2)
if DEVICE_COUNT > 1:
torch_device_stream(Q.device).synchronize()
return Q_out, K_out
class Fast_RoPE_Embedding_QK(torch.autograd.Function):
@staticmethod
def forward(ctx, Q, K, cos, sin, rope_indices):
has_indices = rope_indices is not None
cos, sin = cos.squeeze(), sin.squeeze()
batch, n_heads_Q, seq_len, head_dim = Q.shape
_, n_heads_K, _, _ = K.shape
# Inplace rotary embedding is generally fine
Q_out = Q.clone() if not Q.is_contiguous else Q
K_out = K.clone() if not K.is_contiguous else K
if has_indices:
# TRL's rotary indices are always in int32, so casting is just for safety
rope_ptr = rope_indices.reshape(-1).to(dtype = torch.int32, device = Q.device)
else:
rope_ptr = cos.new_empty(1, dtype = torch.int32)
BLOCK_SIZE, num_warps = calculate_settings(head_dim)
Q_batch_stride, Q_head_stride, Q_seq_stride = (
Q_out.stride(0),
Q_out.stride(1),
Q_out.stride(2),
)
K_batch_stride, K_head_stride, K_seq_stride = (
K_out.stride(0),
K_out.stride(1),
K_out.stride(2),
)
with torch_gpu_device(Q.device):
_rope_embedding_QK[(batch * seq_len, n_heads_Q)](
Q_out,
Q_batch_stride,
Q_head_stride,
Q_seq_stride,
K_out,
K_batch_stride,
K_head_stride,
K_seq_stride,
cos,
cos.stride(0),
sin,
sin.stride(0),
rope_ptr,
seq_len,
head_dim = head_dim,
n_heads_K = n_heads_K,
BACKWARD_PASS = False,
HAS_ROPE_INDICES = has_indices,
BLOCK_SIZE = BLOCK_SIZE,
num_warps = num_warps,
)
ctx.block_size = BLOCK_SIZE
ctx.num_warps = num_warps
ctx.has_indices = has_indices
ctx.cos = cos
ctx.sin = sin
ctx.rope_indices = rope_ptr if has_indices else None
ctx.seq_len = seq_len
ctx.n_heads_Q = n_heads_Q
ctx.n_heads_K = n_heads_K
return (
Q_out,
K_out,
)
@staticmethod
def backward(ctx, dQ, dK):
batch, _, _, head_dim = dQ.shape
rope_ptr = (
ctx.rope_indices
if ctx.has_indices
else ctx.cos.new_empty(1, dtype = torch.int32)
)
Q_batch_stride, Q_head_stride, Q_seq_stride = (
dQ.stride(0),
dQ.stride(1),
dQ.stride(2),
)
K_batch_stride, K_head_stride, K_seq_stride = (
dK.stride(0),
dK.stride(1),
dK.stride(2),
)
# Inplace rotary embedding is generally fine
dQ_out = dQ.clone() if not dQ.is_contiguous else dQ
dK_out = dK.clone() if not dK.is_contiguous else dK
with torch_gpu_device(dQ.device):
_rope_embedding_QK[(batch * ctx.seq_len, ctx.n_heads_Q)](
dQ_out,
Q_batch_stride,
Q_head_stride,
Q_seq_stride,
dK_out,
K_batch_stride,
K_head_stride,
K_seq_stride,
ctx.cos,
ctx.cos.stride(0),
ctx.sin,
ctx.sin.stride(0),
rope_ptr,
ctx.seq_len,
head_dim = head_dim,
n_heads_K = ctx.n_heads_K,
BACKWARD_PASS = True,
HAS_ROPE_INDICES = ctx.has_indices,
BLOCK_SIZE = ctx.block_size,
num_warps = ctx.num_warps,
)
return (dQ_out, dK_out, None, None, None)
class Slow_RoPE_Embedding(torch.autograd.Function):
@ -206,8 +432,8 @@ class Slow_RoPE_Embedding(torch.autograd.Function):
# The first two dimensions of cos and sin are always 1, so we can `squeeze` them.
cos = cos.squeeze(1).squeeze(0) # [seq_len, dim]
sin = sin.squeeze(1).squeeze(0) # [seq_len, dim]
cos = cos[position_ids].unsqueeze(1) # [bs, 1, seq_len, dim]
sin = sin[position_ids].unsqueeze(1) # [bs, 1, seq_len, dim]
cos = cos[position_ids].unsqueeze(2) # [bs, seq_len, 1, dim]
sin = sin[position_ids].unsqueeze(2) # [bs, seq_len, 1, dim]
# Q * cos + rotate_half(Q) * sin
half = Q.shape[-1] // 2

View file

@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
__version__ = "2025.11.4"
__version__ = "2025.12.5"
__all__ = [
"SUPPORTS_BFLOAT16",
@ -85,6 +85,7 @@ import re
from dataclasses import dataclass, field
import functools
import textwrap
import logging
import warnings, subprocess, inspect, psutil, os, math
from unsloth_zoo.utils import Version, get_quant_type
from importlib.metadata import version as importlib_version
@ -167,9 +168,9 @@ warnings.filterwarnings(
)
warnings.filterwarnings(action = "ignore", category = RuntimeWarning, module = "multiprocess")
warnings.filterwarnings(action = "ignore", category = UserWarning, module = "triton")
# Stop "Special tokens have been added in the vocabulary, ..."
import logging
warnings.filterwarnings(action = "ignore", category = UserWarning, module = "bitsandbytes")
# Stop "Special tokens have been added in the vocabulary, ..."
logging.getLogger("transformers.tokenization_utils_base").setLevel(logging.CRITICAL + 1)
@ -412,6 +413,16 @@ try:
except:
pass
# Flax classes are deprecated and will be removed in Diffusers v1.0.0.
try:
from diffusers.utils import logger as diffusers_logger
diffusers_logger.addFilter(HideLoggingMessage("are deprecated"))
del diffusers_logger
except:
pass
# Errors out on
# Some weights of Gemma3nForConditionalGeneration were not initialized from the model checkpoint
from transformers.modeling_utils import logger as transformers_logger
@ -761,6 +772,13 @@ elif DEVICE_TYPE == "xpu":
# =============================================
# Get Xformers
# Silence xformers CUDA mismatch warnings before import
try:
_xformers_logger = logging.getLogger("xformers")
_xformers_logger.setLevel(logging.ERROR)
del _xformers_logger
except:
pass
try:
from xformers import __version__ as xformers_version
@ -835,10 +853,11 @@ except ModuleNotFoundError:
xformers_attention = None
xformers_version = None
except Exception as e:
print(
"========\nSwitching to PyTorch attention since your Xformers is broken.\n========\n"
)
print(str(e))
if os.environ.get("UNSLOTH_ENABLE_LOGGING", "0") != "0":
print(
"========\nSwitching to PyTorch attention since your Xformers is broken.\n========\n"
)
print(str(e))
xformers = None
xformers_attention = None
xformers_version = None
@ -2302,17 +2321,20 @@ def verify_fp8_support_if_applicable(model_config):
raise ValueError(
f"Unsloth: FP8 quantization is only supported on CUDA GPUs. You are using {DEVICE_TYPE}."
)
major_version, minor_version = torch.cuda.get_device_capability()
if quant_method == "fbgemm_fp8" and major_version < 9:
# While L4 does support FP8 as data type, it doesn't have fbgemm (package) support yet. So we restrict it.
raise ValueError(
f"Unsloth: FBGEMM FP8 quantization is only supported on H100 and higher GPUs. L4 is not supported. You are using {torch.cuda.get_device_name()}. Refer to https://developer.nvidia.com/cuda-gpus for more details."
)
if quant_method == "fp8" and major_version * 10 + minor_version < 89:
# In case of block quantized, we allow L4 because we fall back to torchao kernels.
raise ValueError(
f"Unsloth: FP8 quantization is only supported on L4 and higher GPUs with compute capability 8.9 or higher. You are using {torch.cuda.get_device_name()}. Refer to https://developer.nvidia.com/cuda-gpus for more details."
)
# [TODO] Need to add FP8 support for Intel XPUs
if DEVICE_TYPE == "cuda":
major_version, minor_version = torch.cuda.get_device_capability()
if quant_method == "fbgemm_fp8" and major_version < 9:
# While L4 does support FP8 as data type, it doesn't have fbgemm (package) support yet. So we restrict it.
raise ValueError(
f"Unsloth: FBGEMM FP8 quantization is only supported on H100 and higher GPUs. L4 is not supported. You are using {torch.cuda.get_device_name()}. Refer to https://developer.nvidia.com/cuda-gpus for more details."
)
if quant_method == "fp8" and major_version * 10 + minor_version < 89:
# In case of block quantized, we allow L4 because we fall back to torchao kernels.
raise ValueError(
f"Unsloth: FP8 quantization is only supported on L4 and higher GPUs with compute capability 8.9 or higher. You are using {torch.cuda.get_device_name()}. Refer to https://developer.nvidia.com/cuda-gpus for more details."
)
def _get_inference_mode_context_manager(model: torch.nn.Module):

View file

@ -16,6 +16,13 @@ from .llama import *
from ._utils import __version__
from unsloth_zoo.hf_utils import dtype_from_config
from unsloth_zoo.utils import _get_dtype
from ..utils.packing import get_packed_info_from_kwargs
from ..utils.attention_dispatch import (
AttentionConfig,
AttentionContext,
run_attention,
select_attention_backend,
)
try:
from transformers.models.cohere.modeling_cohere import (
@ -104,6 +111,7 @@ def CohereAttention_fast_forward(
Q = Q.view(bsz, q_len, n_heads, head_dim).transpose(1, 2)
K = K.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
V = V.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
seq_info = get_packed_info_from_kwargs(kwargs, Q.device)
if self.use_qk_norm:
Q = fast_layernorm_compiled(self.q_norm, Q)
K = fast_layernorm_compiled(self.k_norm, K)
@ -112,12 +120,17 @@ def CohereAttention_fast_forward(
if past_key_value is not None:
kv_seq_len += past_key_value[0].shape[-2]
cos, sin = position_embeddings
if position_ids is None:
Q, K = fast_rope_embedding(Q, K, cos, sin)
# Extend RoPE dynamically to fit in VRAM
if position_embeddings:
cos, sin = position_embeddings
else:
cos, sin = cos[position_ids], sin[position_ids]
Q, K = inplace_rope_embedding(Q, K, cos, sin, position_ids)
cos, sin = self.rotary_emb.get_cached(kv_seq_len, Q.device.index)
rope_position_ids = (
position_ids if position_ids is not None else kwargs.get("position_ids")
)
# Useful for LongRoPE
Q, K = fast_rope_embedding(Q, K, cos, sin, rope_position_ids)
if past_key_value is not None:
K = torch.cat([past_key_value[0], K], dim = 2)
@ -125,54 +138,33 @@ def CohereAttention_fast_forward(
past_key_value = (K, V) if use_cache else None
# Attention module
if not HAS_FLASH_ATTENTION and HAS_XFORMERS and attention_mask is None:
# Xformers memory efficient attention
# Also has Flash Attention v2 dispatching
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
use_varlen = seq_info is not None and past_key_value is None
backend = select_attention_backend(use_varlen)
attention_config = AttentionConfig(
backend = backend,
n_kv_heads = n_kv_heads,
n_groups = n_groups,
flash_dense_kwargs = {"causal": True},
flash_varlen_kwargs = {
"dropout_p": 0.0,
"causal": True,
"softmax_scale": getattr(self, "softmax_scale", None),
},
)
context = AttentionContext(
bsz = bsz,
q_len = q_len,
kv_seq_len = kv_seq_len,
n_heads = n_heads,
head_dim = head_dim,
requires_grad = hidden_states.requires_grad,
seq_info = seq_info,
attention_mask = attention_mask,
causal_mask = causal_mask,
)
# Group query attention
if n_groups != 1:
K = K.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
V = V.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
K = K.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
V = V.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
if hidden_states.requires_grad:
K = K.reshape(bsz, kv_seq_len, n_heads, head_dim)
V = V.reshape(bsz, kv_seq_len, n_heads, head_dim)
else:
Q = Q.view(bsz, q_len, n_kv_heads, n_groups, head_dim)
A = xformers_attention(Q, K, V, attn_bias = causal_mask)
A = A.view(bsz, q_len, n_heads, head_dim)
A = run_attention(config = attention_config, context = context, Q = Q, K = K, V = V)
elif HAS_FLASH_ATTENTION and attention_mask is None:
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
A = flash_attn_func(Q, K, V, causal = True)
else:
# Grouped query attention
if n_groups != 1:
K = K[:, :, None, :, :].expand(
bsz, n_kv_heads, n_groups, kv_seq_len, head_dim
)
V = V[:, :, None, :, :].expand(
bsz, n_kv_heads, n_groups, kv_seq_len, head_dim
)
K = K.reshape(bsz, n_heads, kv_seq_len, head_dim)
V = V.reshape(bsz, n_heads, kv_seq_len, head_dim)
pass
# Must be contiguous or else results are False!
# https://github.com/pytorch/pytorch/issues/112577
Q, K, V = Q.contiguous(), K.contiguous(), V.contiguous()
# Needs (batch_size, n_heads, seq_len, head_dim)
# is_casual and attention_mask must not be both set!
A = scaled_dot_product_attention(
Q, K, V, attn_mask = attention_mask, is_causal = False
)
# Go back to (batch_size, seq_len, n_heads, head_dim)
A = A.transpose(1, 2).contiguous()
attn_output = A.reshape(bsz, q_len, n_heads * head_dim)
attn_output = self.apply_o(self, attn_output)
attn_weights = None
@ -215,6 +207,7 @@ def CohereDecoderLayer_fast_forward(
output_attentions = output_attentions,
use_cache = use_cache,
padding_mask = padding_mask,
**kwargs,
)
# Fully Connected
@ -234,6 +227,7 @@ def CohereDecoderLayer_fast_forward(
output_attentions = output_attentions,
use_cache = use_cache,
padding_mask = padding_mask,
**kwargs,
)
# Fully Connected

View file

@ -17,6 +17,14 @@ import os
from ._utils import __version__
from unsloth_zoo.utils import Version, _get_dtype
from unsloth_zoo.hf_utils import dtype_from_config
from ..utils.packing import get_packed_info_from_kwargs
from ..utils.attention_dispatch import (
AttentionConfig,
AttentionContext,
run_attention,
select_attention_backend,
SDPA,
)
from .llama import (
LlamaRotaryEmbedding,
LlamaLinearScalingRotaryEmbedding,
@ -98,13 +106,10 @@ def FalconH1Attention_fast_forward(
assert n_kv_heads * n_groups == n_heads
Q, K, V = self.apply_qkv(self, hidden_states)
Q = Q.view(
bsz, q_len, n_heads, head_dim
) # .transpose(1, 2) # we will transpose after normalisation
K = K.view(
bsz, q_len, n_kv_heads, head_dim
) # .transpose(1, 2) # we will transpose after normalisation
Q = Q.view(bsz, q_len, n_heads, head_dim)
K = K.view(bsz, q_len, n_kv_heads, head_dim)
V = V.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
seq_info = get_packed_info_from_kwargs(kwargs, hidden_states.device)
# Falcon H1 multiplies key states by a multiplier
K = K * self.config.key_multiplier
@ -116,20 +121,19 @@ def FalconH1Attention_fast_forward(
if past_key_value is not None:
kv_seq_len += past_key_value[0].shape[-2]
# Extend RoPE dynamically to fit in VRAM
if position_embeddings and kv_seq_len <= position_embeddings[0].shape[0]:
cos, sin = position_embeddings
else:
# Extend RoPE dynamically to fit in VRA
rotary_emb = self.rotary_emb
rotary_emb.extend_rope_embedding(V, seq_len = kv_seq_len)
device_index = Q.device.index
cos, sin = rotary_emb.get_cached(kv_seq_len, Q.device.index)
if position_ids is None:
# Useful for LongRoPE
cos, sin = rotary_emb.get_cached(kv_seq_len, device_index)
else:
cos, sin = rotary_emb.get_cached(kv_seq_len, device_index)
Q, K = fast_rope_embedding(Q, K, cos, sin)
rope_position_ids = (
position_ids if position_ids is not None else kwargs.get("position_ids")
)
# Useful for LongRoPE
Q, K = fast_rope_embedding(Q, K, cos, sin, rope_position_ids)
if past_key_value is not None:
K = torch.cat([past_key_value[0], K], dim = 2)
@ -137,54 +141,45 @@ def FalconH1Attention_fast_forward(
past_key_value = (K, V) if use_cache else None
# Attention module
if not HAS_FLASH_ATTENTION and attention_mask is None:
# Xformers memory efficient attention
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
K_M = V_M = bsz * kv_seq_len
Q_M = bsz * q_len
window = (-1, -1)
use_varlen = (
attention_mask is None
and seq_info is not None
and past_key_value is None
and window == (-1, -1)
)
# Group query attention
K = K.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
V = V.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
K = K.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
V = V.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
if hidden_states.requires_grad:
K = K.reshape(bsz, kv_seq_len, n_heads, head_dim)
V = V.reshape(bsz, kv_seq_len, n_heads, head_dim)
else:
# Xformers does support the forward pass though
Q = Q.view(bsz, q_len, n_kv_heads, n_groups, head_dim)
backend = (
SDPA if attention_mask is not None else select_attention_backend(use_varlen)
)
attention_config = AttentionConfig(
backend = backend,
n_kv_heads = n_kv_heads,
n_groups = n_groups,
flash_dense_kwargs = {
"causal": True,
"window_size": (kv_seq_len, kv_seq_len),
},
flash_varlen_kwargs = {
"dropout_p": 0.0,
"softmax_scale": None,
"causal": True,
},
sdpa_kwargs = {} if attention_mask is None else {"attn_mask": attention_mask},
)
context = AttentionContext(
bsz = bsz,
q_len = q_len,
kv_seq_len = kv_seq_len,
n_heads = n_heads,
head_dim = head_dim,
requires_grad = hidden_states.requires_grad,
seq_info = seq_info,
attention_mask = attention_mask,
causal_mask = causal_mask,
)
A = xformers_attention(Q, K, V, attn_bias = causal_mask)
A = A.view(bsz, q_len, n_heads, head_dim)
elif HAS_FLASH_ATTENTION and attention_mask is None:
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
sw = kv_seq_len
window = (-1, -1) if (kv_seq_len <= sw) else (sw, sw)
A = flash_attn_func(Q, K, V, causal = True, window_size = window)
else:
# Grouped query attention
# if n_groups != 1:
K = K[:, :, None, :, :].expand(bsz, n_kv_heads, n_groups, kv_seq_len, head_dim)
V = V[:, :, None, :, :].expand(bsz, n_kv_heads, n_groups, kv_seq_len, head_dim)
K = K.reshape(bsz, n_heads, kv_seq_len, head_dim)
V = V.reshape(bsz, n_heads, kv_seq_len, head_dim)
# pass
# Must be contiguous or else results are False!
# https://github.com/pytorch/pytorch/issues/112577
Q, K, V = Q.contiguous(), K.contiguous(), V.contiguous()
# Needs (batch_size, n_heads, seq_len, head_dim)
# is_casual and attention_mask must not be both set!
A = scaled_dot_product_attention(
Q, K, V, attn_mask = attention_mask, is_causal = False
)
# Go back to (batch_size, seq_len, n_heads, head_dim)
A = A.transpose(1, 2).contiguous()
A = run_attention(config = attention_config, context = context, Q = Q, K = K, V = V)
attn_output = A.reshape(bsz, q_len, n_heads * head_dim)
attn_output = self.apply_o(self, attn_output)
@ -442,6 +437,7 @@ def FalconH1DecoderLayer_fast_forward(
use_cache = use_cache,
padding_mask = padding_mask,
position_embeddings = position_embeddings,
**kwargs,
)
attention_hidden_states = attention_hidden_states * self.attn_out_multiplier
@ -486,6 +482,7 @@ def FalconH1DecoderLayer_fast_forward(
use_cache = use_cache,
padding_mask = padding_mask,
position_embeddings = position_embeddings,
**kwargs,
)
attention_hidden_states = attention_hidden_states * self.attn_out_multiplier
@ -726,7 +723,7 @@ class FastFalconH1Model(FastLlamaModel):
# Solves https://github.com/unslothai/unsloth/issues/168
# Static KV Cache was introduced in 4.38.0, causing training to be much slower.
# Inferene can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# Inference can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# https://github.com/huggingface/transformers/pull/27931
# https://github.com/huggingface/transformers/blob/v4.37.2/src/transformers/models/llama/modeling_llama.py
import transformers.models.falcon_h1.modeling_falcon_h1

View file

@ -16,6 +16,11 @@ from .llama import *
from ._utils import __version__
from unsloth_zoo.utils import _get_dtype
from unsloth_zoo.hf_utils import dtype_from_config
from ..utils.packing import (
build_sdpa_packed_attention_mask,
build_xformers_block_causal_mask,
get_packed_info_from_kwargs,
)
import math
try:
@ -110,6 +115,7 @@ def GemmaDecoderLayer_fast_forward(
output_attentions = output_attentions,
use_cache = use_cache,
padding_mask = padding_mask,
**kwargs,
)
hidden_states += residual
@ -134,6 +140,7 @@ def GemmaDecoderLayer_fast_forward(
output_attentions = output_attentions,
use_cache = use_cache,
padding_mask = padding_mask,
**kwargs,
)
hidden_states = residual + hidden_states
@ -164,6 +171,7 @@ def GemmaModel_fast_forward_inference(
past_key_values,
position_ids,
attention_mask = None,
**kwargs,
):
out_weights = tuple(
torch.empty_like(
@ -416,7 +424,7 @@ class FastGemmaModel(FastLlamaModel):
# Solves https://github.com/unslothai/unsloth/issues/168
# Static KV Cache was introduced in 4.38.0, causing training to be much slower.
# Inferene can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# Inference can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# https://github.com/huggingface/transformers/pull/27931
# https://github.com/huggingface/transformers/blob/v4.37.2/src/transformers/models/llama/modeling_llama.py
import transformers.models.gemma.modeling_gemma

View file

@ -16,6 +16,14 @@ from .llama import *
from ._utils import __version__
from unsloth_zoo.utils import _get_dtype
from unsloth_zoo.hf_utils import dtype_from_config
from ..utils.packing import get_packed_info_from_kwargs
from ..utils.attention_dispatch import (
AttentionConfig,
AttentionContext,
run_attention,
select_attention_backend,
SDPA,
)
from .gemma import (
GemmaFixedRotaryEmbedding,
GemmaFixedLinearScalingRotaryEmbedding,
@ -98,19 +106,25 @@ def Gemma2Attention_fast_forward(
Q = Q.view(bsz, q_len, n_heads, head_dim).transpose(1, 2)
K = K.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
V = V.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
seq_info = get_packed_info_from_kwargs(kwargs, Q.device)
kv_seq_len = K.shape[-2]
if past_key_value is not None:
kv_seq_len += past_key_value[0].shape[-2]
device_index = Q.device.index
if position_ids is None:
cos = self.rotary_emb.multi_gpu_cos_cached[device_index]
sin = self.rotary_emb.multi_gpu_sin_cached[device_index]
Q, K = fast_rope_embedding(Q, K, cos, sin)
cos = self.rotary_emb.multi_gpu_cos_cached[device_index]
sin = self.rotary_emb.multi_gpu_sin_cached[device_index]
rope_position_ids = (
position_ids if position_ids is not None else kwargs.get("position_ids")
)
if rope_position_ids is not None:
# Useful for LongRoPE
cos_var, sin_var = self.rotary_emb.get_cached(kv_seq_len, device_index)
Q, K = fast_rope_embedding(Q, K, cos_var, sin_var, rope_position_ids)
else:
cos, sin = self.rotary_emb.get_cached(kv_seq_len, device_index)
Q, K = inplace_rope_embedding(Q, K, cos, sin, position_ids)
Q, K = fast_rope_embedding(Q, K, cos, sin)
if past_key_value is not None:
K = torch.cat([past_key_value[0], K], dim = 2)
@ -118,32 +132,68 @@ def Gemma2Attention_fast_forward(
past_key_value = (K, V) if use_cache else None
# Only enable if the attention_mask is True
has_sliding_window = type(causal_mask) is bool and causal_mask is True
if HAS_FLASH_ATTENTION_SOFTCAPPING and attention_mask is None:
window = (-1, -1)
if has_sliding_window:
sw = getattr(self.config, "sliding_window", None)
sw = kv_seq_len if (sw is None or sw == "null") else sw
window = (-1, -1) if (kv_seq_len <= sw) else (sw, sw)
use_sliding_window = kwargs.get("use_sliding_window")
has_sliding_window = (
use_sliding_window
if use_sliding_window is not None
else isinstance(causal_mask, bool) and causal_mask is True
)
use_flash = HAS_FLASH_ATTENTION_SOFTCAPPING and attention_mask is None
if use_flash:
window = (-1, -1)
sliding_window = getattr(self.config, "sliding_window", None)
if has_sliding_window:
sliding_window = (
sliding_window if sliding_window is not None else kv_seq_len
)
window = (
(-1, -1)
if kv_seq_len <= sliding_window
else (sliding_window, sliding_window)
)
# FA uses 1 / sqrt for softmax_scale!
if not hasattr(self, "_flash_attention_softmax_scale"):
self._flash_attention_softmax_scale = 1.0 / (
self.config.query_pre_attn_scalar**0.5
)
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
A = flash_attn_func(
Q,
K,
V,
causal = True,
softcap = self.config.attn_logit_softcapping,
softmax_scale = self._flash_attention_softmax_scale,
window_size = window,
use_varlen = seq_info is not None and past_key_value is None
attention_config = AttentionConfig(
backend = select_attention_backend(use_varlen),
n_kv_heads = n_kv_heads,
n_groups = n_groups,
flash_dense_kwargs = {
"causal": True,
"softcap": self.config.attn_logit_softcapping,
"softmax_scale": self._flash_attention_softmax_scale,
"window_size": window,
},
flash_varlen_kwargs = {
"dropout_p": 0.0,
"softmax_scale": self._flash_attention_softmax_scale,
"causal": True,
"softcap": self.config.attn_logit_softcapping,
"window_size": window,
},
)
context = AttentionContext(
bsz = bsz,
q_len = q_len,
kv_seq_len = kv_seq_len,
n_heads = n_heads,
head_dim = head_dim,
requires_grad = hidden_states.requires_grad,
seq_info = seq_info,
attention_mask = attention_mask,
causal_mask = causal_mask,
sliding_window = sliding_window,
)
A = run_attention(config = attention_config, context = context, Q = Q, K = K, V = V)
A = A.reshape(bsz, q_len, n_heads * head_dim)
else:
fx = (
@ -192,6 +242,7 @@ def Gemma2DecoderLayer_fast_forward(
use_cache = use_cache,
padding_mask = padding_mask,
_flag_for_generation = self._flag_for_generation,
**kwargs,
)
hidden_states = fast_rms_layernorm_inference_gemma(
self.post_attention_layernorm, hidden_states, out_weight
@ -222,6 +273,7 @@ def Gemma2DecoderLayer_fast_forward(
output_attentions = output_attentions,
use_cache = use_cache,
padding_mask = padding_mask,
**kwargs,
)
hidden_states = fast_rms_layernorm(
self.post_attention_layernorm, hidden_states, gemma = True
@ -263,6 +315,7 @@ def Gemma2Attention_fast_forward_inference(
do_prefill = False,
attention_mask = None,
use_sliding_window = False,
**kwargs,
):
Xn = hidden_states
bsz, _, hd = hidden_states.size()
@ -426,6 +479,7 @@ def Gemma2Model_fast_forward_inference(
past_key_values,
position_ids,
attention_mask = None,
**kwargs,
):
out_weights = tuple(
torch.empty_like(
@ -550,7 +604,7 @@ class FastGemma2Model(FastLlamaModel):
# Solves https://github.com/unslothai/unsloth/issues/168
# Static KV Cache was introduced in 4.38.0, causing training to be much slower.
# Inferene can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# Inference can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# https://github.com/huggingface/transformers/pull/27931
# https://github.com/huggingface/transformers/blob/v4.37.2/src/transformers/models/llama/modeling_llama.py
import transformers.models.gemma2.modeling_gemma2

View file

@ -17,6 +17,14 @@ import os
from ._utils import __version__
from unsloth_zoo.utils import _get_dtype
from unsloth_zoo.hf_utils import dtype_from_config
from ..utils.packing import get_packed_info_from_kwargs
from ..utils.attention_dispatch import (
AttentionConfig,
AttentionContext,
run_attention,
select_attention_backend,
SDPA,
)
from .llama import (
LlamaRotaryEmbedding,
LlamaLinearScalingRotaryEmbedding,
@ -96,6 +104,7 @@ def GraniteAttention_fast_forward(
Q = Q.view(bsz, q_len, n_heads, head_dim).transpose(1, 2)
K = K.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
V = V.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
seq_info = get_packed_info_from_kwargs(kwargs, Q.device)
kv_seq_len = K.shape[-2]
if past_key_value is not None:
@ -103,10 +112,14 @@ def GraniteAttention_fast_forward(
assert position_embeddings is not None
cos, sin = position_embeddings
if position_ids is None:
Q, K = fast_rope_embedding(Q, K, cos, sin)
rope_position_ids = (
position_ids if position_ids is not None else kwargs.get("position_ids")
)
if rope_position_ids is not None:
# Useful for LongRoPE
Q, K = fast_rope_embedding(Q, K, cos, sin, rope_position_ids)
else:
Q, K = inplace_rope_embedding(Q, K, cos, sin, position_ids)
Q, K = fast_rope_embedding(Q, K, cos, sin)
if past_key_value is not None:
K = torch.cat([past_key_value[0], K], dim = 2)
@ -114,69 +127,59 @@ def GraniteAttention_fast_forward(
past_key_value = (K, V) if use_cache else None
# Attention module
if not HAS_FLASH_ATTENTION and HAS_XFORMERS and attention_mask is None:
# Xformers memory efficient attention
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
K_M = V_M = bsz * kv_seq_len
Q_M = bsz * q_len
use_varlen = (
attention_mask is None and seq_info is not None and past_key_value is None
)
# Group query attention
K = K.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
V = V.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
K = K.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
V = V.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
if hidden_states.requires_grad:
K = K.reshape(bsz, kv_seq_len, n_heads, head_dim)
V = V.reshape(bsz, kv_seq_len, n_heads, head_dim)
else:
# Xformers does support the forward pass though
Q = Q.view(bsz, q_len, n_kv_heads, n_groups, head_dim)
backend = (
SDPA if attention_mask is not None else select_attention_backend(use_varlen)
)
A = xformers_attention(
Q, K, V, attn_bias = causal_mask, scale = self.scaling, p = dropout_p
)
A = A.view(bsz, q_len, n_heads, head_dim)
window = (kv_seq_len, kv_seq_len)
softmax_scale = getattr(self, "scaling", None)
attention_config = AttentionConfig(
backend = backend,
n_kv_heads = n_kv_heads,
n_groups = n_groups,
flash_dense_kwargs = {
"causal": True,
"softmax_scale": softmax_scale,
"dropout_p": dropout_p,
"window_size": window,
},
flash_varlen_kwargs = {
"dropout_p": 0.0,
"softmax_scale": softmax_scale,
"causal": True,
},
sdpa_kwargs = {
k: v
for k, v in {
"attn_mask": attention_mask,
"scale": softmax_scale,
"dropout_p": dropout_p,
}.items()
if v is not None
},
xformers_kwargs = {
"scale": softmax_scale,
"p": dropout_p,
},
)
elif HAS_FLASH_ATTENTION and attention_mask is None:
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
window = (kv_seq_len, kv_seq_len)
A = flash_attn_func(
Q,
K,
V,
causal = True,
window_size = window,
softmax_scale = self.scaling,
dropout_p = dropout_p,
)
else:
# Grouped query attention
# if n_groups != 1:
K = K[:, :, None, :, :].expand(bsz, n_kv_heads, n_groups, kv_seq_len, head_dim)
V = V[:, :, None, :, :].expand(bsz, n_kv_heads, n_groups, kv_seq_len, head_dim)
K = K.reshape(bsz, n_heads, kv_seq_len, head_dim)
V = V.reshape(bsz, n_heads, kv_seq_len, head_dim)
# pass
# Must be contiguous or else results are False!
# https://github.com/pytorch/pytorch/issues/112577
Q, K, V = Q.contiguous(), K.contiguous(), V.contiguous()
# Needs (batch_size, n_heads, seq_len, head_dim)
# is_casual and attention_mask must not be both set!
A = scaled_dot_product_attention(
Q,
K,
V,
attn_mask = attention_mask,
scale = self.scaling,
is_causal = False,
dropout_p = dropout_p,
)
# Go back to (batch_size, seq_len, n_heads, head_dim)
A = A.transpose(1, 2).contiguous()
context = AttentionContext(
bsz = bsz,
q_len = q_len,
kv_seq_len = kv_seq_len,
n_heads = n_heads,
head_dim = head_dim,
requires_grad = hidden_states.requires_grad,
seq_info = seq_info,
attention_mask = attention_mask,
causal_mask = causal_mask,
)
A = run_attention(config = attention_config, context = context, Q = Q, K = K, V = V)
attn_output = A.reshape(bsz, q_len, n_heads * head_dim)
attn_output = self.apply_o(self, attn_output)
@ -222,6 +225,7 @@ def GraniteDecoderLayer_fast_forward(
padding_mask = padding_mask,
position_embeddings = position_embeddings,
_flag_for_generation = self._flag_for_generation,
**kwargs,
)
hidden_states = torch.add(residual, hidden_states, alpha = residual_multiplier)
@ -245,6 +249,7 @@ def GraniteDecoderLayer_fast_forward(
use_cache = use_cache,
padding_mask = padding_mask,
position_embeddings = position_embeddings,
**kwargs,
)
hidden_states = torch.add(residual, hidden_states, alpha = residual_multiplier)

View file

@ -16,7 +16,8 @@ import torch
import gc
import math
import functools
from typing import Any, Dict, Optional, Tuple, List, Union
from typing import Optional, Tuple, List, Union
from ._utils import *
from ._utils import patch_unsloth_smart_gradient_checkpointing
from ._utils import __version__, importlib_version
@ -25,6 +26,16 @@ from ._utils import (
_get_inference_mode_context_manager,
_prepare_model_for_qat,
)
from ..utils.packing import (
get_packed_info_from_kwargs,
mask_packed_sequence_boundaries,
)
from ..utils.attention_dispatch import (
AttentionConfig,
AttentionContext,
run_attention,
select_attention_backend,
)
from torch.nn.functional import scaled_dot_product_attention
from transformers import __version__ as transformers_version
from unsloth_zoo.utils import Version, _get_dtype
@ -61,9 +72,6 @@ from transformers.modeling_attn_mask_utils import (
)
from ..kernels import *
from ..tokenizer_utils import *
if HAS_FLASH_ATTENTION:
from flash_attn import flash_attn_func
from .vision import FastBaseModel
# Final patching code
@ -551,7 +559,6 @@ def LlamaAttention_fast_forward(
del self.temp_KV
del self.RH_Q
del self.attention
bsz, q_len, _ = hidden_states.size()
n_heads = self.config.num_attention_heads
@ -564,6 +571,7 @@ def LlamaAttention_fast_forward(
Q = Q.view(bsz, q_len, n_heads, head_dim).transpose(1, 2)
K = K.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
V = V.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
seq_info = get_packed_info_from_kwargs(kwargs, Q.device)
kv_seq_len = K.shape[-2]
if past_key_value is not None:
@ -572,23 +580,20 @@ def LlamaAttention_fast_forward(
if position_embeddings and kv_seq_len <= position_embeddings[0].shape[0]:
cos, sin = position_embeddings
else:
# Extend RoPE dynamically to fit in VRA
rotary_emb = self.rotary_emb
rotary_emb.extend_rope_embedding(V, seq_len = kv_seq_len)
# if position_ids is None:
# # Useful for LongRoPE
# cos, sin = rotary_emb.get_cached(kv_seq_len, device = Q.device)
# else:
# cos, sin = rotary_emb.get_cached(seq_len = kv_seq_len, device = Q.device)
cos, sin = rotary_emb.get_cached(kv_seq_len, Q.device.index)
rope_position_ids = position_ids
if rope_position_ids is None and seq_info is not None:
rope_position_ids = kwargs.get("position_ids")
# Q, K = (
# fast_rope_embedding(Q, K, cos, sin)
# if position_ids is None
# else inplace_rope_embedding(Q, K, cos, sin, position_ids)
# if rope_position_ids is None
# else inplace_rope_embedding(Q, K, cos, sin, rope_position_ids)
# )
Q, K = fast_rope_embedding(Q, K, cos, sin)
Q, K = fast_rope_embedding(Q, K, cos, sin, rope_position_ids)
if past_key_value is not None:
K = torch.cat([past_key_value[0], K], dim = 2)
@ -596,76 +601,28 @@ def LlamaAttention_fast_forward(
past_key_value = (K, V) if use_cache else None
# Attention module
if not HAS_FLASH_ATTENTION and HAS_XFORMERS and attention_mask is None:
# Xformers memory efficient attention
# Also has Flash Attention v2 dispatching
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
use_varlen = seq_info is not None and past_key_value is None
backend = select_attention_backend(use_varlen)
config = AttentionConfig(
backend = backend,
n_kv_heads = n_kv_heads,
n_groups = n_groups,
flash_dense_kwargs = {"causal": True},
flash_varlen_kwargs = {"dropout_p": 0.0, "causal": True},
)
context = AttentionContext(
bsz = bsz,
q_len = q_len,
kv_seq_len = kv_seq_len,
n_heads = n_heads,
head_dim = head_dim,
requires_grad = hidden_states.requires_grad,
seq_info = seq_info,
attention_mask = attention_mask,
causal_mask = causal_mask,
)
# Group query attention
if n_groups != 1:
K = K.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
V = V.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
K = K.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
V = V.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
if hidden_states.requires_grad:
K = K.reshape(bsz, kv_seq_len, n_heads, head_dim)
V = V.reshape(bsz, kv_seq_len, n_heads, head_dim)
else:
Q = Q.view(bsz, q_len, n_kv_heads, n_groups, head_dim)
A = xformers_attention(Q, K, V, attn_bias = causal_mask)
A = A.view(bsz, q_len, n_heads, head_dim)
elif HAS_FLASH_ATTENTION and attention_mask is None:
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
A = flash_attn_func(Q, K, V, causal = True)
else:
# when qlen==vlen and attn_mask is None, we should use causal attention
Q_len = Q.shape[-2]
K_len = K.shape[-2]
if attention_mask is None and Q_len == K_len:
is_causal = True
else:
is_causal = False
# Grouped query attention
if SDPA_HAS_GQA:
# Needs (batch_size, n_heads, seq_len, head_dim)
# is_casual and attention_mask must not be both set!
A = scaled_dot_product_attention(
Q,
K,
V,
attn_mask = attention_mask,
is_causal = is_causal,
enable_gqa = n_groups != 1,
)
# Go back to (batch_size, seq_len, n_heads, head_dim)
A = A.transpose(1, 2) # .contiguous()
else:
if n_groups != 1:
K = K[:, :, None, :, :].expand(
bsz, n_kv_heads, n_groups, kv_seq_len, head_dim
)
V = V[:, :, None, :, :].expand(
bsz, n_kv_heads, n_groups, kv_seq_len, head_dim
)
K = K.reshape(bsz, n_heads, kv_seq_len, head_dim)
V = V.reshape(bsz, n_heads, kv_seq_len, head_dim)
pass
# Must be contiguous or else results are False!
# https://github.com/pytorch/pytorch/issues/112577
Q, K, V = Q.contiguous(), K.contiguous(), V.contiguous()
# Needs (batch_size, n_heads, seq_len, head_dim)
# is_casual and attention_mask must not be both set!
A = scaled_dot_product_attention(
Q, K, V, attn_mask = attention_mask, is_causal = is_causal
)
# Go back to (batch_size, seq_len, n_heads, head_dim)
A = A.transpose(1, 2).contiguous()
pass
A = run_attention(config = config, context = context, Q = Q, K = K, V = V)
attn_output = A.reshape(bsz, q_len, n_heads * head_dim)
attn_output = self.apply_o(self, attn_output)
attn_weights = None
@ -715,6 +672,7 @@ def LlamaDecoderLayer_fast_forward(
use_cache = use_cache,
padding_mask = padding_mask,
position_embeddings = position_embeddings,
**kwargs,
)
hidden_states += residual
@ -738,6 +696,7 @@ def LlamaDecoderLayer_fast_forward(
use_cache = use_cache,
padding_mask = padding_mask,
position_embeddings = position_embeddings,
**kwargs,
)
hidden_states = residual + hidden_states
@ -815,8 +774,11 @@ def LlamaModel_fast_forward(
seq_length_with_past = seq_length
# Fix out of bounds tokenization
if hasattr(self, "max_seq_length"):
# Fix out of bounds tokenization unless we were given packed metadata
allow_overlength = getattr(self, "_unsloth_allow_packed_overlength", False) or (
"packed_seq_lengths" in kwargs
)
if hasattr(self, "max_seq_length") and not allow_overlength:
if seq_length > self.max_seq_length:
shape = input_ids.shape if input_ids is not None else inputs_embeds.shape
logger.warning_once(
@ -1072,10 +1034,12 @@ def LlamaModel_fast_forward(
mask = causal_mask
if IS_GEMMA2:
if idx % 2 == 0:
use_sliding_window = idx % 2 == 0
if use_sliding_window:
mask = self.SWA_mask if use_static_mask else dynamic_SWA_mask
else:
mask = self.GA_mask if use_static_mask else dynamic_GA_mask
kwargs["use_sliding_window"] = use_sliding_window
if gradient_checkpointing and not isinstance(
decoder_layer, GradientCheckpointingLayer
@ -1089,6 +1053,7 @@ def LlamaModel_fast_forward(
output_attentions,
padding_mask = padding_mask,
position_embeddings = position_embeddings,
**kwargs,
)
return custom_forward
@ -1115,6 +1080,7 @@ def LlamaModel_fast_forward(
use_cache = use_cache,
padding_mask = padding_mask,
position_embeddings = position_embeddings,
**kwargs,
)
hidden_states = layer_outputs[0]
@ -1175,6 +1141,7 @@ def _LlamaModel_fast_forward_inference(
past_key_values,
position_ids,
attention_mask = None,
**kwargs,
):
input_ids = input_ids[:, : self.max_seq_length]
bsz, q_len = input_ids.shape
@ -1306,6 +1273,7 @@ def CausalLM_fast_forward(fast_forward_inference):
past_key_values,
position_ids = position_ids,
attention_mask = attention_mask,
**kwargs,
)
else:
causal_mask = (
@ -1338,6 +1306,7 @@ def CausalLM_fast_forward(fast_forward_inference):
output_attentions = output_attentions,
output_hidden_states = output_hidden_states,
return_dict = return_dict,
**kwargs,
)
hidden_states = outputs[0]
@ -1446,6 +1415,10 @@ def CausalLM_fast_forward(fast_forward_inference):
shift_labels = torch.empty_like(labels)
shift_labels[..., :-1] = labels[..., 1:]
shift_labels[..., -1] = -100
mask_packed_sequence_boundaries(
shift_labels,
kwargs.get("packed_seq_lengths"),
)
# shift_labels = torch.hstack((labels[..., 1:], self.extra_ignored_labels[:labels.shape[0]]))
n_items = kwargs.get("num_items_in_batch", None)
if n_items is None:
@ -1533,7 +1506,7 @@ def PeftModel_fast_forward(
# Solves https://github.com/unslothai/unsloth/issues/168
# Static KV Cache was introduced in 4.38.0, causing training to be much slower.
# Inferene can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# Inference can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# https://github.com/huggingface/transformers/pull/27931
# https://github.com/huggingface/transformers/blob/v4.37.2/src/transformers/models/llama/modeling_llama.py
class LlamaRotaryEmbedding(torch.nn.Module):
@ -2010,8 +1983,8 @@ def unsloth_fast_generate(
> self.config.max_position_embeddings
):
raise ValueError(
f'Unsloth: input length {kwargs["input_ids"].shape[-1]} + max_new_tokens {kwargs["max_new_tokens"]} exceeds the maximum sequence length of {self.config.max_position_embeddings}!\n'
'You will need to do long context extension by increasing the `max_seq_length` in `FastLanguageModel.from_pretrained`.'
f"Unsloth: input length {kwargs['input_ids'].shape[-1]} + max_new_tokens {kwargs['max_new_tokens']} exceeds the maximum sequence length of {self.config.max_position_embeddings}!\n"
"You will need to do long context extension by increasing the `max_seq_length` in `FastLanguageModel.from_pretrained`."
)
# Must patch accelerate for Xformers

View file

@ -12,8 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from ..device_type import DEVICE_TYPE_TORCH
import importlib
import os
import torch
import re
import tempfile
from typing import Union
@ -30,12 +32,14 @@ from packaging.version import Version
from transformers import __version__ as transformers_version
from unsloth.models._utils import TorchAOConfig
from unsloth_zoo.utils import Version
import torch
import gc
transformers_version = Version(transformers_version)
SUPPORTS_FOURBIT = transformers_version >= Version("4.37")
LOCAL_RANK_KEYS = ("LOCAL_RANK", "RANK")
WORLD_SIZE_KEYS = ("WORLD_SIZE",)
BAD_MAPPINGS = {
"unsloth/Qwen3-32B-unsloth-bnb-4bit".lower(): "unsloth/Qwen3-32B-bnb-4bit".lower(), # 32B dynamic quant is way too big
"unsloth/Qwen3-30B-A3B-unsloth-bnb-4bit".lower(): "unsloth/Qwen3-30B-A3B".lower(), # HF loads MoEs too slowly
@ -45,6 +49,50 @@ BAD_MAPPINGS = {
}
def _get_env_int(keys):
for key in keys:
value = os.environ.get(key)
if value is None:
continue
try:
return int(value)
except ValueError:
continue
return None
def _infer_distributed_ranks():
if torch.distributed.is_available() and torch.distributed.is_initialized():
try:
return torch.distributed.get_rank(), torch.distributed.get_world_size()
except Exception:
pass
return _get_env_int(LOCAL_RANK_KEYS), _get_env_int(WORLD_SIZE_KEYS)
def is_distributed():
rank, world_size = _infer_distributed_ranks()
return (world_size or 1) > 1 or (rank is not None and rank > 0)
def prepare_device_map():
rank, world_size = _infer_distributed_ranks()
distributed = (world_size or 1) > 1 or (rank is not None and rank > 0)
if not distributed:
return None, False
local_rank = 0 if rank is None else rank
device_map = {"": f"{DEVICE_TYPE_TORCH}:{local_rank}"}
try:
if DEVICE_TYPE_TORCH == "cuda":
torch.cuda.set_device(local_rank)
elif DEVICE_TYPE_TORCH == "xpu" and hasattr(torch, "xpu"):
torch.xpu.set_device(local_rank)
except Exception:
pass
return device_map, True
def __get_model_name(
model_name,
load_in_4bit = True,

View file

@ -17,6 +17,16 @@ import os
from ._utils import __version__
from unsloth_zoo.utils import _get_dtype
from unsloth_zoo.hf_utils import dtype_from_config
from ..utils.packing import (
get_packed_info_from_kwargs,
mask_packed_sequence_boundaries,
)
from ..utils.attention_dispatch import (
AttentionConfig,
AttentionContext,
run_attention,
select_attention_backend,
)
from .llama import (
LlamaRotaryEmbedding,
LlamaLinearScalingRotaryEmbedding,
@ -76,6 +86,7 @@ def MistralAttention_fast_forward(
Q = Q.view(bsz, q_len, n_heads, head_dim).transpose(1, 2)
K = K.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
V = V.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
seq_info = get_packed_info_from_kwargs(kwargs, Q.device)
kv_seq_len = K.shape[-2]
if past_key_value is not None:
@ -83,12 +94,13 @@ def MistralAttention_fast_forward(
# Extend RoPE dynamically to fit in VRAM
self.rotary_emb.extend_rope_embedding(V, seq_len = kv_seq_len)
cos, sin = self.rotary_emb.get_cached(kv_seq_len, Q.device.index)
if position_ids is None:
Q, K = fast_rope_embedding(Q, K, cos, sin)
else:
Q, K = inplace_rope_embedding(Q, K, cos, sin, position_ids)
rope_position_ids = (
position_ids if position_ids is not None else kwargs.get("position_ids")
)
# Useful for LongRoPE
Q, K = fast_rope_embedding(Q, K, cos, sin, rope_position_ids)
if past_key_value is not None:
K = torch.cat([past_key_value[0], K], dim = 2)
@ -96,68 +108,38 @@ def MistralAttention_fast_forward(
past_key_value = (K, V) if use_cache else None
# Attention module
if not HAS_FLASH_ATTENTION and HAS_XFORMERS and attention_mask is None:
# Xformers memory efficient attention
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
K_M = V_M = bsz * kv_seq_len
Q_M = bsz * q_len
sw_cfg = getattr(self.config, "sliding_window", None)
sw = kv_seq_len if (sw_cfg is None or sw_cfg == "null") else sw_cfg
window_size = (-1, -1) if (kv_seq_len <= sw) else (sw, sw)
has_swa = isinstance(causal_mask, xformers.attn_bias.BlockDiagonalCausalMask)
# Group query attention
K = K.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
V = V.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
K = K.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
V = V.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
if hidden_states.requires_grad:
K = K.reshape(bsz, kv_seq_len, n_heads, head_dim)
V = V.reshape(bsz, kv_seq_len, n_heads, head_dim)
if has_swa:
Q = Q.view(1, Q_M, n_heads, head_dim)
K = K.view(1, K_M, n_heads, head_dim)
V = V.view(1, V_M, n_heads, head_dim)
else:
# Xformers does support the forward pass though
Q = Q.view(bsz, q_len, n_kv_heads, n_groups, head_dim)
if has_swa:
Q = Q.view(1, Q_M, n_kv_heads, n_groups, head_dim)
K = K.view(1, K_M, n_kv_heads, n_groups, head_dim)
V = V.view(1, V_M, n_kv_heads, n_groups, head_dim)
A = xformers_attention(Q, K, V, attn_bias = causal_mask)
A = A.view(bsz, q_len, n_heads, head_dim)
elif HAS_FLASH_ATTENTION and attention_mask is None:
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
sw = getattr(self.config, "sliding_window", None)
sw = kv_seq_len if (sw is None or sw == "null") else sw
window = (-1, -1) if (kv_seq_len <= sw) else (sw, sw)
A = flash_attn_func(Q, K, V, causal = True, window_size = window)
else:
# Grouped query attention
# if n_groups != 1:
K = K[:, :, None, :, :].expand(bsz, n_kv_heads, n_groups, kv_seq_len, head_dim)
V = V[:, :, None, :, :].expand(bsz, n_kv_heads, n_groups, kv_seq_len, head_dim)
K = K.reshape(bsz, n_heads, kv_seq_len, head_dim)
V = V.reshape(bsz, n_heads, kv_seq_len, head_dim)
# pass
# Must be contiguous or else results are False!
# https://github.com/pytorch/pytorch/issues/112577
Q, K, V = Q.contiguous(), K.contiguous(), V.contiguous()
# Needs (batch_size, n_heads, seq_len, head_dim)
# is_casual and attention_mask must not be both set!
A = scaled_dot_product_attention(
Q, K, V, attn_mask = attention_mask, is_causal = False
)
# Go back to (batch_size, seq_len, n_heads, head_dim)
A = A.transpose(1, 2).contiguous()
use_varlen = (
seq_info is not None and past_key_value is None and window_size == (-1, -1)
)
backend = select_attention_backend(use_varlen)
attention_config = AttentionConfig(
backend = backend,
n_kv_heads = n_kv_heads,
n_groups = n_groups,
flash_dense_kwargs = {"causal": True, "window_size": window_size},
flash_varlen_kwargs = {
"dropout_p": 0.0,
"causal": True,
"softmax_scale": getattr(self, "softmax_scale", None),
},
)
context = AttentionContext(
bsz = bsz,
q_len = q_len,
kv_seq_len = kv_seq_len,
n_heads = n_heads,
head_dim = head_dim,
requires_grad = hidden_states.requires_grad,
seq_info = seq_info,
attention_mask = attention_mask,
causal_mask = causal_mask,
)
A = run_attention(config = attention_config, context = context, Q = Q, K = K, V = V)
attn_output = A.reshape(bsz, q_len, n_heads * head_dim)
attn_output = self.apply_o(self, attn_output)
attn_weights = None
@ -283,6 +265,7 @@ def MistralForCausalLM_fast_forward(
output_attentions = output_attentions,
output_hidden_states = output_hidden_states,
return_dict = return_dict,
**kwargs,
)
hidden_states = outputs[0]
@ -376,6 +359,10 @@ def MistralForCausalLM_fast_forward(
shift_labels = torch.empty_like(labels)
shift_labels[..., :-1] = labels[..., 1:]
shift_labels[..., -1] = -100
mask_packed_sequence_boundaries(
shift_labels,
kwargs.get("packed_seq_lengths"),
)
loss = fast_cross_entropy_loss(
logits = shift_logits,
labels = shift_labels,
@ -439,7 +426,7 @@ class FastMistralModel(FastLlamaModel):
# Solves https://github.com/unslothai/unsloth/issues/168
# Static KV Cache was introduced in 4.38.0, causing training to be much slower.
# Inferene can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# Inference can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# https://github.com/huggingface/transformers/pull/27931
# https://github.com/huggingface/transformers/blob/v4.37.2/src/transformers/models/llama/modeling_llama.py
import transformers.models.mistral.modeling_mistral

View file

@ -60,7 +60,7 @@ class FastQwen2Model(FastLlamaModel):
# Solves https://github.com/unslothai/unsloth/issues/168
# Static KV Cache was introduced in 4.38.0, causing training to be much slower.
# Inferene can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# Inference can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# https://github.com/huggingface/transformers/pull/27931
# https://github.com/huggingface/transformers/blob/v4.37.2/src/transformers/models/llama/modeling_llama.py
import transformers.models.qwen2.modeling_qwen2

View file

@ -16,6 +16,13 @@ from .llama import *
import os
from ._utils import __version__
from unsloth_zoo.utils import Version, _get_dtype
from ..utils.packing import get_packed_info_from_kwargs
from ..utils.attention_dispatch import (
AttentionConfig,
AttentionContext,
run_attention,
select_attention_backend,
)
from .llama import (
LlamaRotaryEmbedding,
LlamaLinearScalingRotaryEmbedding,
@ -95,6 +102,7 @@ def Qwen3Attention_fast_forward(
bsz, q_len, n_kv_heads, head_dim
) # .transpose(1, 2) # we will transpose after normalisation
V = V.view(bsz, q_len, n_kv_heads, head_dim).transpose(1, 2)
seq_info = get_packed_info_from_kwargs(kwargs, hidden_states.device)
# Qwen3 has QKNorm. This seems to be the only difference from Qwen2.
# Note that using fast_layernorm_compiled causes issues as the dimensions don't match up.
@ -110,20 +118,19 @@ def Qwen3Attention_fast_forward(
if past_key_value is not None:
kv_seq_len += past_key_value[0].shape[-2]
# Extend RoPE dynamically to fit in VRAM
if position_embeddings and kv_seq_len <= position_embeddings[0].shape[0]:
cos, sin = position_embeddings
else:
# Extend RoPE dynamically to fit in VRA
rotary_emb = self.rotary_emb
rotary_emb.extend_rope_embedding(V, seq_len = kv_seq_len)
device_index = Q.device.index
cos, sin = rotary_emb.get_cached(kv_seq_len, Q.device.index)
if position_ids is None:
# Useful for LongRoPE
cos, sin = rotary_emb.get_cached(kv_seq_len, device_index)
else:
cos, sin = rotary_emb.get_cached(kv_seq_len, device_index)
Q, K = fast_rope_embedding(Q, K, cos, sin)
rope_position_ids = (
position_ids if position_ids is not None else kwargs.get("position_ids")
)
# Useful for LongRoPE
Q, K = fast_rope_embedding(Q, K, cos, sin, rope_position_ids)
if past_key_value is not None:
K = torch.cat([past_key_value[0], K], dim = 2)
@ -131,74 +138,32 @@ def Qwen3Attention_fast_forward(
past_key_value = (K, V) if use_cache else None
# Attention module
if not HAS_FLASH_ATTENTION and HAS_XFORMERS and attention_mask is None:
# Xformers memory efficient attention
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
K_M = V_M = bsz * kv_seq_len
Q_M = bsz * q_len
use_varlen = seq_info is not None and past_key_value is None
backend = select_attention_backend(use_varlen)
attention_config = AttentionConfig(
backend = backend,
n_kv_heads = n_kv_heads,
n_groups = n_groups,
flash_dense_kwargs = {"causal": True},
flash_varlen_kwargs = {
"dropout_p": 0.0,
"causal": True,
"softmax_scale": getattr(self, "softmax_scale", None),
},
)
context = AttentionContext(
bsz = bsz,
q_len = q_len,
kv_seq_len = kv_seq_len,
n_heads = n_heads,
head_dim = head_dim,
requires_grad = hidden_states.requires_grad,
seq_info = seq_info,
attention_mask = attention_mask,
causal_mask = causal_mask,
)
has_swa = isinstance(causal_mask, xformers.attn_bias.BlockDiagonalCausalMask)
# Group query attention
K = K.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
V = V.view(bsz, kv_seq_len, n_kv_heads, 1, head_dim)
K = K.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
V = V.expand(bsz, kv_seq_len, n_kv_heads, n_groups, head_dim)
if hidden_states.requires_grad:
K = K.reshape(bsz, kv_seq_len, n_heads, head_dim)
V = V.reshape(bsz, kv_seq_len, n_heads, head_dim)
if has_swa:
Q = Q.view(1, Q_M, n_heads, head_dim)
K = K.view(1, K_M, n_heads, head_dim)
V = V.view(1, V_M, n_heads, head_dim)
else:
# Xformers does support the forward pass though
Q = Q.view(bsz, q_len, n_kv_heads, n_groups, head_dim)
if has_swa:
Q = Q.view(1, Q_M, n_kv_heads, n_groups, head_dim)
K = K.view(1, K_M, n_kv_heads, n_groups, head_dim)
V = V.view(1, V_M, n_kv_heads, n_groups, head_dim)
A = xformers_attention(Q, K, V, attn_bias = causal_mask)
A = A.view(bsz, q_len, n_heads, head_dim)
elif HAS_FLASH_ATTENTION and attention_mask is None:
Q = Q.transpose(1, 2)
K = K.transpose(1, 2)
V = V.transpose(1, 2)
sw = kv_seq_len
window = (-1, -1) if (kv_seq_len <= sw) else (sw, sw)
A = flash_attn_func(Q, K, V, causal = True, window_size = window)
else:
# Grouped query attention
# if n_groups != 1:
K = K[:, :, None, :, :].expand(bsz, n_kv_heads, n_groups, kv_seq_len, head_dim)
V = V[:, :, None, :, :].expand(bsz, n_kv_heads, n_groups, kv_seq_len, head_dim)
K = K.reshape(bsz, n_heads, kv_seq_len, head_dim)
V = V.reshape(bsz, n_heads, kv_seq_len, head_dim)
# pass
# Must be contiguous or else results are False!
# https://github.com/pytorch/pytorch/issues/112577
Q, K, V = Q.contiguous(), K.contiguous(), V.contiguous()
# Needs (batch_size, n_heads, seq_len, head_dim)
# is_casual and attention_mask must not be both set!
# when qlen==vlen and attn_mask is None, we should use causal attention
Q_len = Q.shape[-2]
K_len = K.shape[-2]
if attention_mask is None and Q_len == K_len:
is_causal = True
else:
is_causal = False
A = scaled_dot_product_attention(
Q, K, V, attn_mask = attention_mask, is_causal = is_causal
)
# Go back to (batch_size, seq_len, n_heads, head_dim)
A = A.transpose(1, 2).contiguous()
A = run_attention(config = attention_config, context = context, Q = Q, K = K, V = V)
attn_output = A.reshape(bsz, q_len, n_heads * head_dim)
attn_output = self.apply_o(self, attn_output)
@ -451,7 +416,7 @@ class FastQwen3Model(FastLlamaModel):
# Solves https://github.com/unslothai/unsloth/issues/168
# Static KV Cache was introduced in 4.38.0, causing training to be much slower.
# Inferene can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# Inference can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# https://github.com/huggingface/transformers/pull/27931
# https://github.com/huggingface/transformers/blob/v4.37.2/src/transformers/models/llama/modeling_llama.py
import transformers.models.qwen3.modeling_qwen3

View file

@ -59,12 +59,14 @@ def Qwen3MoeSparseMoeBlock_fast_forward(self, X, temp_gate = None, temp_up = Non
self.gate_proj, X, out = temp_gate
) # pretty much the only change from transformers implementation.
routing_weights = torch_nn_functional_softmax(router_logits, dim = -1)
routing_weights = torch_nn_functional_softmax(
router_logits, dim = -1, dtype = torch.float32
)
routing_weights, selected_experts = torch.topk(routing_weights, self.top_k, dim = -1)
routing_weights /= routing_weights.sum(dim = -1, keepdim = True)
# we cast back to the input dtype
routing_weights = routing_weights.to(X.dtype)
final_X = torch.zeros((bsz * seq_len, hd), dtype = X.dtype, device = X.device)
final_X = torch.zeros((bsz * seq_len, hd), dtype = torch.float32, device = X.device)
# One hot encode the selected experts to create an expert mask
# this will be used to easily index which expert is going to be sollicitated
@ -128,7 +130,7 @@ def Qwen3MoeDecoderLayer_fast_forward(
position_embeddings = position_embeddings,
_flag_for_generation = self._flag_for_generation,
)
hidden_states = residual + hidden_states
hidden_states += residual
# MoE Router MLP
residual = hidden_states
@ -138,7 +140,7 @@ def Qwen3MoeDecoderLayer_fast_forward(
hidden_states, router_logits = Qwen3MoeSparseMoeBlock_fast_forward(
self.mlp, hidden_states
)
hidden_states = residual + hidden_states
hidden_states += residual
else:
residual = hidden_states
hidden_states = fast_rms_layernorm(self.input_layernorm, hidden_states)
@ -200,7 +202,7 @@ class FastQwen3MoeModel(FastQwen3Model):
# Solves https://github.com/unslothai/unsloth/issues/168
# Static KV Cache was introduced in 4.38.0, causing training to be much slower.
# Inferene can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# Inference can now be CUDAGraphed, but we shall retain the old rotary embeddings.
# https://github.com/huggingface/transformers/pull/27931
# https://github.com/huggingface/transformers/blob/v4.37.2/src/transformers/models/llama/modeling_llama.py\
import transformers.models.qwen3_moe.modeling_qwen3_moe

View file

@ -24,6 +24,7 @@ import os
import re
import torch
from unsloth_zoo.compiler import create_new_function
from unsloth_zoo.log import logger
from unsloth_zoo.logging_utils import PatchRLStatistics
from unsloth_zoo.rl_replacements import RL_REPLACEMENTS
from .rl_replacements import (
@ -32,6 +33,7 @@ from .rl_replacements import (
RL_PRE_ITEMS,
RL_CONFIG_CHANGES,
RL_METRICS_CHANGES,
RL_ADDITIONAL_FUNCTIONS,
)
torch_compile_options = {
@ -312,7 +314,7 @@ class Unsloth{RLTrainer_name}(_Unsloth{RLTrainer_name}):
if getattr(args, "_n_gpu", 1) != 1:
args._n_gpu = 1
if "model" in locals() and hasattr(model, "for_training"):
model.for_training()
model.for_training(use_gradient_checkpointing=getattr(args, 'gradient_checkpointing', True))
super().__init__({RLTrainer_call_args}{RLTrainer_kwargs})
if "model" in locals() and hasattr(model, "for_inference"):
model.for_inference()
@ -329,6 +331,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"):
try:
trainer = eval(f"trl.trainer.{trainer_file}")
except Exception as error:
print(f"Unsloth: Could not import trl.trainer.{trainer_file}: {error}")
return
# Get SFTTrainer and SFTConfig names
@ -347,8 +350,14 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"):
and trainer_file.split("_")[0] in x.lower()
]
if len(name) != 1:
logger.info(
f"Unsloth: Could not find Trainer class in trl.trainer.{trainer_file}. Found: {name}"
)
return
if len(config) != 1:
logger.info(
f"Unsloth: Could not find Config class in trl.trainer.{trainer_file}. Found: {config}"
)
return
# Get SFTTrainer, SFTConfig
@ -356,17 +365,25 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"):
RLConfig_name = config[0]
try:
RLTrainer = eval(f"trl.trainer.{trainer_file}.{RLTrainer_name}")
except:
except Exception as e:
logger.info(
f"Unsloth: Could not load {RLTrainer_name} from trl.trainer.{trainer_file}: {e}"
)
return
try:
RLConfig = eval(f"trl.trainer.{trainer_file}.{RLConfig_name}")
except:
except Exception as e:
logger.info(
f"Unsloth: Could not load {RLConfig_name} from trl.trainer.{trainer_file}: {e}"
)
return
# Check name
if RLTrainer.__name__.startswith("Unsloth"):
print(f"Unsloth: {RLTrainer.__name__} is already patched.")
return
if RLConfig.__name__.startswith("Unsloth"):
print(f"Unsloth: {RLConfig.__name__} is already patched.")
return
# Get old source
@ -442,7 +459,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"):
" force_float32 = True\n"
"mixed_precision_dtype = os.environ.get('UNSLOTH_MIXED_PRECISION', 'float32')\n"
"dtype = getattr(model.config, 'dtype', None) or getattr(model.config, 'torch_dtype', None)\n"
"if dtype is None: dtype = model.get_input_embeddings().dtype\n"
"if dtype is None: dtype = model.get_input_embeddings().weight.dtype\n"
"from unsloth_zoo.utils import _get_dtype\n"
"dtype = _get_dtype(dtype)\n"
"float16 = dtype == torch.float16\n"
@ -453,13 +470,24 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"):
" args.fp16 = False\n"
" args.bf16 = False\n"
" os.environ['ACCELERATE_MIXED_PRECISION'] = 'no'\n"
" if hasattr(args, 'mixed_precision'): args.mixed_precision = 'no'\n"
" # args.mixed_precision is a new argument which needs to be set now\n"
"elif (not use_bf16 and not use_fp16) and mixed_precision_dtype == 'float32':\n"
" # Mixed precision training\n"
" args.fp16 = float16\n"
" args.bf16 = not float16\n"
" os.environ['ACCELERATE_MIXED_PRECISION'] = 'fp16' if float16 else 'bf16'\n"
" if hasattr(args, 'mixed_precision'): args.mixed_precision = 'fp16' if float16 else 'bf16'\n"
" # args.mixed_precision is a new argument which needs to be set now\n"
"elif mixed_precision_dtype == 'bfloat16':\n"
" # Both False since bfloat16 full finetuning doesn't do any autocasting.\n"
" args.fp16 = False\n"
" args.bf16 = False\n"
" os.environ['ACCELERATE_MIXED_PRECISION'] = 'no'\n"
" if hasattr(args, 'mixed_precision'): args.mixed_precision = 'no'\n"
" # args.mixed_precision is a new argument which needs to be set now\n"
"\n"
)
"elif mixed_precision_dtype == 'bfloat16':\n" " # Both False since bfloat16 full finetuning doesn't do any autocasting.\n" " args.fp16 = False\n" " args.bf16 = False\n" " os.environ['ACCELERATE_MIXED_PRECISION'] = 'no'\n"
extra_args += mixed_precision
# Check if per_device_eval_batch_size (default 8) bigger than bsz
@ -565,7 +593,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"):
if "model" in call_args:
training_check = (
"if model is not None and hasattr(model, 'for_training'):\n"
" model.for_training()\n"
" model.for_training(use_gradient_checkpointing=getattr(args, 'gradient_checkpointing', True))\n"
"if 'tokenizer' in locals() and hasattr(tokenizer, 'padding_side'): tokenizer.padding_side = 'right'\n"
"if 'processing_class' in locals():\n"
" if hasattr(processing_class, 'padding_side'): processing_class.padding_side = 'right'\n"
@ -977,7 +1005,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"):
RLTrainer_source,
f"trl.trainer.{trainer_file}",
imports,
overwrite = True,
overwrite = False,
)
# Patch Trainer
@ -1242,6 +1270,11 @@ def patch_functions(RLTrainer, trainer_file, RLTrainer_name, all_imports, import
+ r", load_tensors = True))",
source,
)
# All these are to fix multiple commas before lora_request (in case the original code ends with something like ",)")
# https://github.com/huggingface/trl/blob/main/trl/trainer/grpo_trainer.py#L1388 for eg has such an ending
source = re.sub(r"\,[\s]{1,}\,[\s]{0,}lora_request", ", lora_request", source)
source = re.sub(r"[\s]{1,}\,[\s]{0,}lora_request", ", lora_request", source)
source = re.sub(r"[\,]{1,}[\s]{0,}lora_request", ", lora_request", source)
# Prefer using unsloth's sampling params and fallback to trl's if not found
# We'll enable this later separately when combining both this and GRPOConfig params
# source = re.sub(
@ -1291,15 +1324,27 @@ def patch_trl_rl_trainers():
import trl.trainer
all_trainers = dir(trl.trainer)
all_trainers = [x for x in all_trainers if x.islower() and x.endswith("_trainer")]
all_trainers = [
x
for x in all_trainers
if x.islower() and x.endswith("_trainer") and x != "base_trainer"
]
for trainer in all_trainers:
_patch_trl_rl_trainers(trainer)
return
def patch_trl_openenv():
for function in RL_ADDITIONAL_FUNCTIONS["openenv"]:
logger.info(f"Unsloth: Patching trl openenv with function: {function.__name__}")
function() # Call the function to apply the patch
return
def PatchFastRL(algorithm = None, FastLanguageModel = None):
if FastLanguageModel is not None:
PatchRL(FastLanguageModel)
patch_trl_rl_trainers()
patch_trl_openenv()
if type(algorithm) is str and algorithm.islower():
PatchRLStatistics(algorithm)

View file

@ -26,6 +26,10 @@ import torch
import inspect
from collections import defaultdict
from unsloth_zoo.rl_replacements import RL_REPLACEMENTS, left_pack_padding
from unsloth_zoo.utils import Version
from importlib.metadata import version as importlib_version
from unsloth_zoo.log import logger
import importlib.util
from ..device_type import (
is_hip,
get_device_type,
@ -42,6 +46,7 @@ RL_FUNCTIONS = defaultdict(list)
RL_PRE_ITEMS = defaultdict(list)
RL_CONFIG_CHANGES = defaultdict(list)
RL_METRICS_CHANGES = defaultdict(list)
RL_ADDITIONAL_FUNCTIONS = defaultdict(list)
torch_compile_options = {
"epilogue_fusion": True,
@ -216,6 +221,27 @@ def grpo_trainer__prepare_inputs(function_name, function):
RL_FUNCTIONS["grpo_trainer"].append(grpo_trainer__prepare_inputs)
# Remove collective RPC of reload weights from generate
# trl added reload weights (potentially for quantized models), we don't need it for our use case (LoRA primarily)
# https://github.com/huggingface/trl/commit/7856d3b1f6518601732f489883b341bb6dd36434#diff-964e6fd373aa93037604064cb2b822d7f8e2735e33f791065acf2c4c3552d393R1168-R1169
def grpo_trainer__generate_single_turn(function_name, function):
if function_name != "_generate_single_turn":
return function
# Remove the reload_weights collective RPC call from the generate function's source
# function = function.replace('self.llm.collective_rpc("reload_weights")', "")
# The regex below does the same thing but is more flexible and can handle single or double quotes
function = re.sub(
r"self\.llm\.collective_rpc\(\s*(['\"])reload_weights\1\s*\)",
"",
function,
)
return function
RL_FUNCTIONS["grpo_trainer"].append(grpo_trainer__generate_single_turn)
# Fix incorrect special tokens handling and truncation in older TRL versions
def grpo_trainer__generate_and_score_completions(function_name, function):
if function_name != "_generate_and_score_completions":
@ -906,3 +932,54 @@ def grpo_trainer_metrics(RLTrainer_source, RLConfig_source):
RL_METRICS_CHANGES["grpo_trainer"].append(grpo_trainer_metrics)
def openenv_vllm_reload_weights():
# This function patches the trl openenv generate_rollout_completions function to:
# 1. Remove the reload_weights call (unsloth handles weight reloading)
# 2. Fix wake_up call to be compatible with unsloth (remove tags to wake everything)
#
# The issue: TRL's wake_up(tags=["kv_cache"]) only wakes kv_cache, leaving is_sleeping=True
# at the executor level. This causes unsloth's patched generate to try waking up again,
# resulting in double create_and_map on already-mapped handles.
#
# The fix: Use wake_up() with no tags, which wakes everything. Unsloth's patched
# CuMemAllocator.wake_up skips weights anyway, so this is safe.
if importlib.util.find_spec("trl") is None:
return
if Version(importlib_version("trl")) < Version("0.26.0"):
return
try:
import trl.experimental.openenv.utils as openenv_utils
import trl.experimental.openenv as openenv
except ImportError as e:
logger.info(f"Unsloth: Failed to import trl openenv: {e}")
return
src = inspect.getsource(openenv_utils.generate_rollout_completions)
src = textwrap.dedent(src)
original_src = src
# Remove the reload_weights call - unsloth handles this differently
src = re.sub(r'.*\.collective_rpc\("reload_weights"\).*\n?', "", src)
# Change wake_up(tags=["kv_cache"]) to wake_up() - wake everything to set is_sleeping=False
# This prevents double wake_up issues. Unsloth's allocator skips weights anyway.
src = re.sub(r"\.wake_up\(tags=\[.*?\]\)", ".wake_up()", src)
if original_src == src:
logger.warning("Unsloth: Warning - regex did not match, patch may have failed")
return
# Execute and explicitly assign to module
local_ns = {}
exec(compile(src, "<unsloth>", "exec"), openenv_utils.__dict__, local_ns)
patched_func = local_ns["generate_rollout_completions"]
# Patch both the utils module and the parent openenv module
openenv_utils.generate_rollout_completions = patched_func
openenv.generate_rollout_completions = patched_func
logger.info("Unsloth: Patched trl openenv generate_rollout_completions")
RL_ADDITIONAL_FUNCTIONS["openenv"].append(openenv_vllm_reload_weights)

View file

@ -430,8 +430,10 @@ class FastBaseModel:
max_memory = round(gpu_stats.total_memory / 1024 / 1024 / 1024, 3)
arch_name = model_type_arch.title()
arch_name = arch_name.replace("_Vl_", "_VL_").replace("_Moe", "_MoE")
statistics = (
f"==((====))== Unsloth {__version__}: Fast {model_type_arch.title()} patching. Transformers: {transformers_version}.{vllm_version}\n"
f"==((====))== Unsloth {__version__}: Fast {arch_name} patching. Transformers: {transformers_version}.{vllm_version}\n"
f" {chr(92)}{chr(92)} /| {gpu_stats_name}Num GPUs = {DEVICE_COUNT}. Max memory: {max_memory} GB. Platform: {platform_system}.\n"
f"O^O/ {chr(92)}_/ {chr(92)} Torch: {torch.__version__}. {gpu_stats_snippet} Triton: {triton_version}\n"
f"{chr(92)} / Bfloat16 = {str(SUPPORTS_BFLOAT16).upper()}. FA [Xformers = {xformers_version}. FA2 = {HAS_FLASH_ATTENTION}]\n"
@ -670,25 +672,34 @@ class FastBaseModel:
model.fast_generate = model.generate
model.fast_generate_batches = error_out_no_vllm
if offload_embedding:
embed_tokens = model.get_input_embeddings()
nbytes = embed_tokens.weight.numel() * embed_tokens.weight.itemsize
ngb = round(nbytes / 1024 / 1024 / 1024, 2)
print(f"Unsloth: Offloading embeddings to RAM to save {ngb} GB.")
embed_tokens.to("cpu")
if bool(
os.environ.get("WSL_DISTRO_NAME") or os.environ.get("WSL_INTEROP")
):
# WSL doesn't work with offloaded embeddings
pass
elif os.name == "nt":
# Windows doesn't work with offloaded embeddings
pass
else:
embed_tokens = model.get_input_embeddings()
nbytes = embed_tokens.weight.numel() * embed_tokens.weight.itemsize
ngb = round(nbytes / 1024 / 1024 / 1024, 2)
print(f"Unsloth: Offloading embeddings to RAM to save {ngb} GB.")
embed_tokens.to("cpu")
# Add hooks to move inputs to CPU and back to CUDA
# [TODO] Doesn't seem to work!
# def pre_hook(module, args):
# args[0]._old_device = args[0].device
# return (args[0].to("cpu", non_blocking = True))
# def post_hook(module, args, output):
# old_device = getattr(args[0], "_old_device", "cuda")
# return output.to(old_device, non_blocking = True)
# embed_tokens.register_forward_pre_hook(pre_hook, prepend = True)
# embed_tokens.register_forward_hook (post_hook, prepend = True)
# Must free GPU memory otherwise will not free!
torch.cuda.empty_cache()
gc.collect()
# Add hooks to move inputs to CPU and back to CUDA
# [TODO] Doesn't seem to work!
# def pre_hook(module, args):
# args[0]._old_device = args[0].device
# return (args[0].to("cpu", non_blocking = True))
# def post_hook(module, args, output):
# old_device = getattr(args[0], "_old_device", "cuda")
# return output.to(old_device, non_blocking = True)
# embed_tokens.register_forward_pre_hook(pre_hook, prepend = True)
# embed_tokens.register_forward_hook (post_hook, prepend = True)
# Must free GPU memory otherwise will not free!
torch.cuda.empty_cache()
gc.collect()
else:
from unsloth_zoo.vllm_utils import (
load_vllm,

View file

@ -42,6 +42,7 @@ import re
from transformers.models.llama.modeling_llama import logger
from .tokenizer_utils import fix_sentencepiece_gguf
from .models.loader_utils import get_model_name
from .models._utils import _convert_torchao_model
from .ollama_template_mappers import OLLAMA_TEMPLATES, MODEL_TO_OLLAMA_TEMPLATE_MAPPER
from transformers import ProcessorMixin
from huggingface_hub import HfApi
@ -2734,11 +2735,35 @@ def unsloth_generic_push_to_hub_merged(
gc.collect()
def unsloth_save_pretrained_torchao(
self,
def _unsloth_save_torchao_with_attached_config(
model,
save_directory: Union[str, os.PathLike],
tokenizer = None,
torchao_config = None,
tokenizer,
push_to_hub: bool = False,
token: Optional[Union[str, bool]] = None,
):
"""Save a QAT-trained model by converting fake-quantized weights to real quantized weights."""
# Convert QAT fake-quantized weights to real quantized weights
_convert_torchao_model(model)
# TorchAO does not support safe_serialization reliably
safe_serialization = False
if push_to_hub:
model.push_to_hub(
save_directory, safe_serialization = safe_serialization, token = token
)
tokenizer.push_to_hub(save_directory, token = token)
else:
model.save_pretrained(save_directory, safe_serialization = safe_serialization)
tokenizer.save_pretrained(save_directory)
def _unsloth_save_torchao_with_given_config(
model,
save_directory: Union[str, os.PathLike],
tokenizer,
torchao_config,
push_to_hub: bool = False,
token: Optional[Union[str, bool]] = None,
):
@ -2749,23 +2774,26 @@ def unsloth_save_pretrained_torchao(
`torchao_config` (TorchAOBaseConfig): configuration for torchao quantization, full list: https://docs.pytorch.org/ao/main/api_ref_quantization.html#inference-apis-for-quantize
`push_to_hub` (bool): whether to push the checkpoint to huggingface hub or save locally
"""
if push_to_hub:
assert token is not None, "Unsloth: Please specify a token for uploading!"
assert (
torchao_config is not None
), "Unsloth: Please specify a torchao_config for post-training quantization!"
# first merge the lora weights
arguments = dict(locals())
arguments["model"] = self
arguments["tokenizer"] = tokenizer
arguments["push_to_hub"] = False # We save ourselves
arguments["save_method"] = "merged_16bit" # Must be 16bit
del arguments["self"]
del arguments["torchao_config"]
if token is None and push_to_hub:
token = get_token()
if not isinstance(self, PeftModelForCausalLM) and not isinstance(self, PeftModel):
self.save_pretrained(save_directory)
if not isinstance(model, PeftModelForCausalLM) and not isinstance(model, PeftModel):
model.save_pretrained(save_directory)
tokenizer.save_pretrained(save_directory)
else:
unsloth_generic_save(**arguments)
for _ in range(3):
gc.collect()
@ -2778,26 +2806,20 @@ def unsloth_save_pretrained_torchao(
)
from torchao import quantize_
if torchao_config is None:
from torchao.quantization import Int8DynamicActivationInt8WeightConfig
print(
"Unsloth: You did not specify a `torchao_config`, so defaulting to `Int8DynamicActivationInt8WeightConfig`"
)
torchao_config = Int8DynamicActivationInt8WeightConfig()
quantization_config = TorchAoConfig(quant_type = torchao_config)
# Determine if this is a VLM
is_vlm = False
if hasattr(self, "config") and hasattr(self.config, "architectures"):
if hasattr(model, "config") and hasattr(model.config, "architectures"):
is_vlm = any(
x.endswith(("ForConditionalGeneration", "ForVisionText2Text"))
for x in self.config.architectures
for x in model.config.architectures
)
is_vlm = is_vlm or hasattr(self.config, "vision_config")
is_vlm = is_vlm or hasattr(model.config, "vision_config")
auto_model = AutoModelForImageTextToText if is_vlm else AutoModelForCausalLM
auto_processor = AutoProcessor if is_vlm else AutoTokenizer
tokenizer = auto_processor.from_pretrained(arguments["save_directory"])
tokenizer = auto_processor.from_pretrained(save_directory)
# TorchAO must only use bfloat16 for loading (float16 fails)
if HAS_TORCH_DTYPE:
@ -2805,8 +2827,9 @@ def unsloth_save_pretrained_torchao(
else:
kwargs = {"dtype": torch.bfloat16}
model = auto_model.from_pretrained(
arguments["save_directory"],
# Reload with quantization applied
quantized_model = auto_model.from_pretrained(
save_directory,
device_map = "auto",
quantization_config = quantization_config,
**kwargs,
@ -2817,25 +2840,92 @@ def unsloth_save_pretrained_torchao(
# TorchAO does not support safe_serialization right now 0.14.0 seems broken!
safe_serialization = Version(importlib_version("torchao")) > Version("0.14.0")
safe_serialization = False
if push_to_hub:
if token is None and push_to_hub:
token = get_token()
model.push_to_hub(
quantized_model.push_to_hub(
torchao_save_directory, safe_serialization = safe_serialization, token = token
)
tokenizer.push_to_hub(torchao_save_directory, token = token)
else:
model.save_pretrained(
quantized_model.save_pretrained(
torchao_save_directory, safe_serialization = safe_serialization
)
tokenizer.save_pretrained(torchao_save_directory)
# Clean up the intermediate unquantized model
if os.path.exists(save_directory):
try:
import shutil
shutil.rmtree(save_directory)
except:
pass
def unsloth_save_pretrained_torchao(
self,
save_directory: Union[str, os.PathLike],
tokenizer = None,
torchao_config = None,
push_to_hub: bool = False,
token: Optional[Union[str, bool]] = None,
):
"""Saves a torchao quantized model checkpoint.
This function handles two mutually exclusive workflows:
1. **QAT (Quantization-Aware Training)**: If the model was trained with `qat_scheme`
parameter, do NOT pass `torchao_config`. The function will convert the QAT
fake-quantized weights to real quantized weights and save directly.
2. **PTQ (Post-Training Quantization)**: If you want to apply quantization to a
regular model, pass a `torchao_config`. The model must NOT have been trained
with `qat_scheme`.
Args:
`save_directory`: local folder path or huggingface hub ID when `push_to_hub` is True
`tokenizer`: the tokenizer to save alongside the model
`torchao_config` (TorchAOBaseConfig): configuration for torchao quantization.
Required for PTQ, must be None for QAT models.
Options: https://docs.pytorch.org/ao/main/api_ref_quantization.html#inference-apis-for-quantize
`push_to_hub` (bool): whether to push to huggingface hub or save locally
`token`: HuggingFace token for pushing to hub
"""
if token is None and push_to_hub:
token = get_token()
has_qat_config = (
hasattr(self, "_torchao_config") and self._torchao_config is not None
)
if torchao_config is not None:
# PTQ path: user provided a config, model must NOT have QAT config
assert not has_qat_config, (
"Unsloth: You passed `torchao_config` but this model was trained with `qat_scheme`. "
"For QAT models, do not pass `torchao_config` - the quantization config is already "
"attached to the model from training."
)
_unsloth_save_torchao_with_given_config(
model = self,
save_directory = save_directory,
tokenizer = tokenizer,
torchao_config = torchao_config,
push_to_hub = push_to_hub,
token = token,
)
else:
# QAT path: no config provided, model must have QAT config
assert has_qat_config, (
"Unsloth: No `torchao_config` provided and model was not trained with `qat_scheme`. "
"Either train with `qat_scheme` parameter, or provide a `torchao_config` for "
"post-training quantization."
)
_unsloth_save_torchao_with_attached_config(
model = self,
save_directory = save_directory,
tokenizer = tokenizer,
push_to_hub = push_to_hub,
token = token,
)
for _ in range(3):
gc.collect()

View file

@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import os
import warnings
from dataclasses import dataclass, field
from typing import Optional
@ -21,13 +23,20 @@ import trl
import inspect
from trl import SFTTrainer
from . import is_bfloat16_supported
from unsloth.utils import (
configure_padding_free,
configure_sample_packing,
enable_padding_free_metadata,
enable_sample_packing,
)
from unsloth_zoo.training_utils import (
unsloth_train as _unsloth_train,
)
from unsloth_zoo.vision_utils import (
UnslothVisionDataCollator,
)
from packaging.version import Version
from unsloth_zoo.hf_utils import get_transformers_model_type
from unsloth_zoo.utils import Version
import dataclasses
__all__ = [
@ -38,8 +47,59 @@ __all__ = [
"UnslothVisionDataCollator",
]
logger = logging.getLogger(__name__)
_AUTO_PADDING_FREE_ENV_DISABLED = os.environ.get(
"UNSLOTH_DISABLE_AUTO_PADDING_FREE", ""
).strip().lower() in {"1", "true", "yes", "on"}
PADDING_FREE_BLOCKLIST = {
"gemma2", # - gemma2: Uses slow_attention_softcapping which has torch.compile issues
"gpt_oss", # - gpt_oss: Uses Flex Attention which doesn't handle padding_free correctly
}
def _should_pack(config) -> bool:
if config is None or not getattr(config, "packing", False):
return False
return not getattr(config, "_unsloth_disable_auto_packing", False)
def _should_auto_padding_free(config) -> bool:
if (
config is None
or _AUTO_PADDING_FREE_ENV_DISABLED
or getattr(config, "packing", False)
):
return False
return not getattr(config, "padding_free", False)
def _disable_sample_packing(config):
if config is None:
return
for attr, value in (("packing", False), ("padding_free", False)):
if hasattr(config, attr):
setattr(config, attr, value)
if hasattr(config, "remove_unused_columns"):
setattr(config, "remove_unused_columns", True)
setattr(config, "_unsloth_disable_auto_packing", True)
_AUTO_PACK_SKIP_MESSAGES = (
"packing is not supported",
"padding-free training",
"passing a custom data collator",
)
def _should_skip_auto_packing_error(exc: Exception) -> bool:
message = str(exc).lower()
return any(msg in message for msg in _AUTO_PACK_SKIP_MESSAGES)
# Unsloth gradient accumulation fix:
from transformers import __version__ as transformers_version
from transformers import __version__ as transformers_version, ProcessorMixin
if Version(transformers_version) > Version("4.45.2"):
@ -211,6 +271,141 @@ def _backwards_compatible_trainer(trainer_class, config_class):
return new_init
def _patch_sft_trainer_auto_packing(trl_module):
sft_trainer = getattr(trl_module, "SFTTrainer", None)
if sft_trainer is None:
return
if getattr(sft_trainer, "_unsloth_auto_packing_wrapped", False):
return
original_init = sft_trainer.__init__
@wraps(original_init)
def new_init(self, *args, **kwargs):
config_arg = None
if len(args) >= 2:
config_arg = args[1]
else:
config_arg = kwargs.get("args")
# Check if model type is unsupported for padding_free
model = kwargs.get("model")
is_unsupported_model = False
is_vlm = False
if model is not None:
model_config = getattr(model, "config", None)
if model_config is not None:
model_types = get_transformers_model_type(model_config)
# Blocklist: models that don't work correctly with padding_free
is_unsupported_model = any(
x in PADDING_FREE_BLOCKLIST for x in model_types
)
# Check if VLM
architectures = getattr(model_config, "architectures", None)
if architectures is None:
architectures = []
is_vlm = any(
x.endswith("ForConditionalGeneration") for x in architectures
)
is_vlm = is_vlm or hasattr(model_config, "vision_config")
processing_class = kwargs.get("processing_class") or kwargs.get("tokenizer")
data_collator = kwargs.get("data_collator")
# We also disable vision language models for padding free collators
blocked = (
(data_collator is not None)
or isinstance(processing_class, ProcessorMixin)
or is_vlm
or is_unsupported_model
or (
os.environ.get("UNSLOTH_RETURN_LOGITS", "0") == "1"
) # Disable padding free on forced logits
)
requested_pack = bool(getattr(config_arg, "packing", False))
if blocked:
if hasattr(config_arg, "packing"):
setattr(config_arg, "packing", False)
if hasattr(config_arg, "padding_free"):
setattr(config_arg, "padding_free", False)
if blocked and requested_pack:
reason = "custom data collator"
if data_collator is None and isinstance(processing_class, ProcessorMixin):
reason = "processor-based model"
elif is_vlm:
reason = "vision-language model"
elif is_unsupported_model:
reason = f"unsupported model type(s): {', '.join(model_types)}"
message = "Unsloth: Sample packing skipped " f"({reason} detected)."
print(message)
packing_active = False
if _should_pack(config_arg) and not blocked:
configure_sample_packing(config_arg)
packing_active = True
logger.info("Unsloth: Sample packing enabled for SFTTrainer instance.")
auto_padding_free_active = False
padding_free_requested = getattr(config_arg, "padding_free", None) is True
if not blocked:
if padding_free_requested:
configure_padding_free(config_arg)
elif _should_auto_padding_free(config_arg):
configure_padding_free(config_arg)
auto_padding_free_active = True
logger.info(
"Unsloth: Padding-free batching auto-enabled for SFTTrainer instance."
)
try:
original_init(self, *args, **kwargs)
except ValueError as exc:
if packing_active and _should_skip_auto_packing_error(exc):
logger.info(
"Unsloth: Auto sample packing failed because trainer reported an incompatible setup (%s).",
exc,
)
_disable_sample_packing(config_arg)
packing_active = False
original_init(self, *args, **kwargs)
else:
raise
trainer_args = getattr(self, "args", None)
trainer_packing = bool(trainer_args and getattr(trainer_args, "packing", False))
trainer_padding_free = bool(
trainer_args and getattr(trainer_args, "padding_free", False)
)
if blocked and trainer_args is not None:
# Mirror the block on the trainer args to avoid re-enabling later
setattr(trainer_args, "packing", False)
setattr(trainer_args, "padding_free", False)
if (
not blocked
and trainer_packing
and (packing_active or _should_pack(trainer_args))
):
enable_sample_packing(self.model, self)
print(
"🦥 Unsloth: Packing enabled - training is >2x faster and uses less VRAM!"
)
elif not blocked and trainer_padding_free:
enable_padding_free_metadata(self.model, self)
message = (
"🦥 Unsloth: Padding-free auto-enabled, enabling faster training."
if auto_padding_free_active
else "🦥 Unsloth: Padding-free enabled, enabling faster training."
)
print(message)
sft_trainer.__init__ = new_init
sft_trainer._unsloth_auto_packing_wrapped = True
def _patch_trl_trainer():
import trl
@ -237,4 +432,6 @@ def _patch_trl_trainer():
except:
continue
_patch_sft_trainer_auto_packing(trl)
trl.__UNSLOTH_BACKWARDS_COMPATIBLE__ = True

View file

@ -0,0 +1,48 @@
# Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from .packing import (
configure_padding_free,
configure_sample_packing,
enable_padding_free_metadata,
enable_sample_packing,
mark_allow_overlength,
)
from .attention_dispatch import (
AttentionConfig,
AttentionContext,
FLASH_DENSE,
FLASH_VARLEN,
SDPA,
XFORMERS,
run_attention,
select_attention_backend,
)
__all__ = [
"configure_sample_packing",
"configure_padding_free",
"enable_sample_packing",
"enable_padding_free_metadata",
"mark_allow_overlength",
"AttentionConfig",
"AttentionContext",
"FLASH_VARLEN",
"FLASH_DENSE",
"XFORMERS",
"SDPA",
"run_attention",
"select_attention_backend",
]

View file

@ -0,0 +1,283 @@
# Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Shared helpers for attention backend selection and execution."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Optional, Tuple
from torch import Tensor
from torch.nn.functional import scaled_dot_product_attention
from ..models._utils import *
from ..utils.packing import (
build_sdpa_packed_attention_mask,
build_xformers_block_causal_mask,
)
if HAS_FLASH_ATTENTION:
from flash_attn import flash_attn_func, flash_attn_varlen_func
HAS_XFORMERS = xformers is not None
BlockDiagonalCausalMask = None
if HAS_XFORMERS:
BlockDiagonalCausalMask = xformers.attn_bias.BlockDiagonalCausalMask
SDPA_HAS_GQA = "enable_gqa" in (scaled_dot_product_attention.__doc__ or "")
FLASH_VARLEN = "flash_varlen"
FLASH_DENSE = "flash_dense"
XFORMERS = "xformers"
SDPA = "sdpa"
XFORMERS_BLOCK_DIAG_CLS = (
xformers.attn_bias.BlockDiagonalCausalMask if HAS_XFORMERS else None
)
@dataclass
class AttentionConfig:
"""
Per-layer attention metadata.
NOTE(djsaunde): I had originally intended this to be populated once per layer, but
we're currently constructing it on every forward pass since it can possibly be
invalid from one forward pass to the next (e.g., switching from training to
inference). For now, I'm keeping separate from AttentionContext for the sake of
better grouping of params.
"""
backend: str
n_kv_heads: int
n_groups: int
flash_dense_kwargs: Optional[dict[str, Any]] = None
flash_varlen_kwargs: Optional[dict[str, Any]] = None
sdpa_kwargs: Optional[dict[str, Any]] = None
xformers_kwargs: Optional[dict[str, Any]] = None
@dataclass
class AttentionContext:
"""Per-call info required to run attention."""
bsz: int
q_len: int
kv_seq_len: int
n_heads: int
head_dim: int
requires_grad: bool
seq_info: Optional[Tuple[Tensor, Tensor, int]]
attention_mask: Optional[Tensor]
causal_mask: Optional[Any]
sliding_window: Optional[int] = None
def select_attention_backend(use_varlen: bool = False) -> str:
"""Return attention backend based on availability / priority order."""
if HAS_FLASH_ATTENTION:
if use_varlen:
return FLASH_VARLEN
else:
return FLASH_DENSE
if HAS_XFORMERS:
return XFORMERS
return SDPA
def run_attention(
*,
config: AttentionConfig,
context: AttentionContext,
Q: Tensor,
K: Tensor,
V: Tensor,
) -> Tensor:
"""
Run attention using config / context info.
Backend choice is prioritized for speed: FlashAttention when installed
(`flash_varlen` for packed/variable-length inputs with `seq_info`, otherwise dense
flash), then xFormers if flash is unavailable, with PyTorch SDPA as the final
fallback (e.g., CPU or no fused kernels).
Varlen flash is preferred when packing metadata is present because it avoids padding
and keeps peak memory low. xFormers and SDPA can also handle packed batches (we
pass a block-diagonal mask into each).
"""
backend = config.backend
if backend == FLASH_VARLEN and context.seq_info is None:
backend = FLASH_DENSE if HAS_FLASH_ATTENTION else SDPA
flash_dense_kwargs = config.flash_dense_kwargs or {}
flash_varlen_kwargs = config.flash_varlen_kwargs or {}
sdpa_kwargs = config.sdpa_kwargs or {}
xformers_kwargs = config.xformers_kwargs or {}
bsz = context.bsz
n_heads = context.n_heads
q_len = context.q_len
head_dim = context.head_dim
kv_seq_len = context.kv_seq_len
requires_grad = context.requires_grad
sliding_window = context.sliding_window
if backend == FLASH_VARLEN:
Q_f = Q.transpose(1, 2).reshape(bsz * q_len, n_heads, head_dim)
K_f = K.transpose(1, 2).reshape(bsz * q_len, config.n_kv_heads, head_dim)
V_f = V.transpose(1, 2).reshape(bsz * q_len, config.n_kv_heads, head_dim)
_, cu_seqlens, max_seqlen = context.seq_info
return flash_attn_varlen_func(
Q_f,
K_f,
V_f,
cu_seqlens,
cu_seqlens,
max_seqlen,
max_seqlen,
**flash_varlen_kwargs,
).view(bsz, q_len, n_heads, head_dim)
elif backend == FLASH_DENSE:
Q_t = Q.transpose(1, 2)
K_t = K.transpose(1, 2)
V_t = V.transpose(1, 2)
return flash_attn_func(Q_t, K_t, V_t, **flash_dense_kwargs).reshape(
bsz, q_len, n_heads, head_dim
)
elif backend == XFORMERS:
attn_bias = build_xformers_block_causal_mask(
context.seq_info,
sliding_window = sliding_window,
base_mask = context.causal_mask,
)
Q_t = Q.transpose(1, 2)
K_t = K.transpose(1, 2)
V_t = V.transpose(1, 2)
K_mod = K_t
V_mod = V_t
Q_mod = Q_t
if config.n_groups != 1:
K_mod = K_t.view(bsz, kv_seq_len, config.n_kv_heads, 1, head_dim)
V_mod = V_t.view(bsz, kv_seq_len, config.n_kv_heads, 1, head_dim)
K_mod = K_mod.expand(
bsz, kv_seq_len, config.n_kv_heads, config.n_groups, head_dim
)
V_mod = V_mod.expand(
bsz, kv_seq_len, config.n_kv_heads, config.n_groups, head_dim
)
if requires_grad:
K_mod = K_mod.reshape(bsz, kv_seq_len, n_heads, head_dim)
V_mod = V_mod.reshape(bsz, kv_seq_len, n_heads, head_dim)
else:
Q_mod = Q_t.view(
bsz, q_len, config.n_kv_heads, config.n_groups, head_dim
)
has_block = XFORMERS_BLOCK_DIAG_CLS is not None and isinstance(
attn_bias, XFORMERS_BLOCK_DIAG_CLS
)
if config.n_groups != 1 and has_block:
if not requires_grad:
Q_mod = Q_mod.view(
1, bsz * q_len, config.n_kv_heads, config.n_groups, head_dim
)
K_mod = K_mod.view(
1, bsz * kv_seq_len, config.n_kv_heads, config.n_groups, head_dim
)
V_mod = V_mod.view(
1, bsz * kv_seq_len, config.n_kv_heads, config.n_groups, head_dim
)
else:
Q_mod = Q_mod.view(1, bsz * q_len, n_heads, head_dim)
K_mod = K_mod.view(1, bsz * kv_seq_len, n_heads, head_dim)
V_mod = V_mod.view(1, bsz * kv_seq_len, n_heads, head_dim)
out = xformers_attention(
Q_mod,
K_mod,
V_mod,
attn_bias = attn_bias,
**xformers_kwargs,
)
if config.n_groups != 1 and not requires_grad:
if has_block:
out = out.view(bsz, q_len, config.n_kv_heads, config.n_groups, head_dim)
else:
out = out.view(bsz, q_len, config.n_kv_heads, config.n_groups, head_dim)
out = out.reshape(bsz, q_len, n_heads, head_dim)
else:
if has_block:
out = out.view(bsz, q_len, n_heads, head_dim)
else:
out = out.view(bsz, q_len, n_heads, head_dim)
return out
else:
local_mask = context.attention_mask
is_causal_local = False
if context.seq_info is not None and local_mask is None:
local_mask = build_sdpa_packed_attention_mask(
context.seq_info,
dtype = Q.dtype,
device = Q.device,
sliding_window = sliding_window,
)
else:
q_len_local = Q.shape[-2]
k_len_local = K.shape[-2]
is_causal_local = local_mask is None and q_len_local == k_len_local
kwargs = dict(sdpa_kwargs)
kwargs.setdefault("attn_mask", local_mask)
kwargs.setdefault("is_causal", is_causal_local)
if SDPA_HAS_GQA:
kwargs.setdefault("enable_gqa", config.n_groups != 1)
out = scaled_dot_product_attention(Q, K, V, **kwargs)
return out.transpose(1, 2)
K_mod = K
V_mod = V
if config.n_groups != 1:
K_mod = K[:, :, None, :, :].expand(
bsz, config.n_kv_heads, config.n_groups, kv_seq_len, head_dim
)
V_mod = V[:, :, None, :, :].expand(
bsz, config.n_kv_heads, config.n_groups, kv_seq_len, head_dim
)
K_mod = K_mod.reshape(bsz, n_heads, kv_seq_len, head_dim)
V_mod = V_mod.reshape(bsz, n_heads, kv_seq_len, head_dim)
out = scaled_dot_product_attention(
Q.contiguous(),
K_mod.contiguous(),
V_mod.contiguous(),
**kwargs,
)
return out.transpose(1, 2).contiguous()
__all__ = [
"AttentionConfig",
"AttentionContext",
"select_attention_backend",
"run_attention",
]

344
unsloth/utils/packing.py Normal file
View file

@ -0,0 +1,344 @@
# Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Utilities for enabling packed (padding-free) batches across Unsloth."""
from __future__ import annotations
import logging
from collections import OrderedDict
from typing import Any, Iterable, Optional, Sequence, Tuple
import torch
try:
from xformers.ops.fmha.attn_bias import (
BlockDiagonalCausalMask as _XFormersBlockMask,
)
except Exception:
try:
from xformers.attn_bias import BlockDiagonalCausalMask as _XFormersBlockMask
except Exception:
_XFormersBlockMask = None
_XFORMERS_MASK_CACHE_MAXSIZE = 32
_XFORMERS_MASK_CACHE: OrderedDict[Tuple[Tuple[int, ...], int], Any] = OrderedDict()
def _window_cache_key(sliding_window: Optional[int]) -> int:
if sliding_window is None or sliding_window <= 0:
return 0
return int(sliding_window)
def _get_cached_block_mask(
lengths: Tuple[int, ...],
sliding_window: Optional[int],
):
if _XFormersBlockMask is None:
return None
window_key = _window_cache_key(sliding_window)
cache_key = (lengths, window_key)
cached = _XFORMERS_MASK_CACHE.get(cache_key)
if cached is not None:
_XFORMERS_MASK_CACHE.move_to_end(cache_key)
return cached
mask = _XFormersBlockMask.from_seqlens(list(lengths))
if window_key and mask is not None and hasattr(mask, "make_local_attention"):
mask = mask.make_local_attention(window_size = window_key)
_XFORMERS_MASK_CACHE[cache_key] = mask
if len(_XFORMERS_MASK_CACHE) > _XFORMERS_MASK_CACHE_MAXSIZE:
_XFORMERS_MASK_CACHE.popitem(last = False)
return mask
class _TrlPackingWarningFilter(logging.Filter):
to_filter = (
"attention implementation is not",
"kernels-community",
)
def filter(self, record: logging.LogRecord) -> bool:
message = record.getMessage()
return not any(substring in message for substring in self.to_filter)
_TRL_FILTER_INSTALLED = False
def _ensure_trl_warning_filter():
global _TRL_FILTER_INSTALLED
if _TRL_FILTER_INSTALLED:
return
logging.getLogger("trl.trainer.sft_trainer").addFilter(_TrlPackingWarningFilter())
_TRL_FILTER_INSTALLED = True
def mark_allow_overlength(module):
"""Mark a module hierarchy so padding-free batches can exceed max_seq_length."""
if module is None:
return
if hasattr(module, "max_seq_length"):
setattr(module, "_unsloth_allow_packed_overlength", True)
children = getattr(module, "children", None)
if children is None:
return
for child in children():
mark_allow_overlength(child)
def configure_sample_packing(config):
"""Mutate an ``SFTConfig`` so TRL prepares packed batches."""
_ensure_trl_warning_filter()
setattr(config, "packing", True)
setattr(config, "padding_free", True)
def configure_padding_free(config):
"""Mutate an ``SFTConfig`` so TRL enables padding-free batching without packing."""
_ensure_trl_warning_filter()
setattr(config, "padding_free", True)
def enable_sample_packing(
model,
trainer,
*,
sequence_lengths_key: str = "seq_lengths",
) -> None:
"""Enable runtime support for packed batches on an existing trainer."""
if model is None or trainer is None:
raise ValueError("model and trainer must not be None")
mark_allow_overlength(model)
if hasattr(trainer, "args") and hasattr(trainer.args, "remove_unused_columns"):
trainer.args.remove_unused_columns = False
collator = getattr(trainer, "data_collator", None)
if collator is None or not hasattr(collator, "torch_call"):
return
if getattr(collator, "_unsloth_packing_wrapped", False):
return
if hasattr(collator, "padding_free"):
collator.padding_free = True
if hasattr(collator, "return_position_ids"):
collator.return_position_ids = True
original_torch_call = collator.torch_call
def torch_call_with_lengths(examples: Sequence[dict]):
batch = original_torch_call(examples)
if examples and isinstance(examples[0], dict):
seq_lengths: list[int] = []
for example in examples:
lengths = example.get(sequence_lengths_key)
if isinstance(lengths, Iterable):
seq_lengths.extend(int(length) for length in lengths)
if seq_lengths:
batch["packed_seq_lengths"] = torch.tensor(
seq_lengths, dtype = torch.int32
)
if "attention_mask" in batch:
batch.pop("attention_mask")
return batch
collator.torch_call = torch_call_with_lengths
collator._unsloth_packing_wrapped = True
def enable_padding_free_metadata(model, trainer):
"""Inject seq-length metadata when padding-free batching is enabled without packing."""
collator = getattr(trainer, "data_collator", None)
if (
collator is None
or getattr(collator, "_unsloth_padding_free_lengths_wrapped", False)
or not getattr(collator, "padding_free", False)
):
return
mark_allow_overlength(model)
if hasattr(collator, "return_position_ids"):
collator.return_position_ids = True
original_torch_call = collator.torch_call
def torch_call_with_padding_free_metadata(examples: Sequence[dict]):
seq_lengths: list[int] = []
if examples and isinstance(examples[0], dict):
for example in examples:
lengths = example.get("seq_lengths")
if lengths is None:
ids = example.get("input_ids")
if ids is None:
continue
lengths = [len(ids)]
example["seq_lengths"] = lengths
seq_lengths.extend(lengths)
batch = original_torch_call(examples)
if seq_lengths:
batch["packed_seq_lengths"] = torch.tensor(
seq_lengths,
dtype = torch.int32,
)
return batch
collator.torch_call = torch_call_with_padding_free_metadata
collator._unsloth_padding_free_lengths_wrapped = True
def get_packed_info_from_kwargs(
kwargs: dict,
device: torch.device,
) -> Optional[Tuple[torch.Tensor, torch.Tensor, int]]:
"""Return packed sequence metadata expected by the attention kernels."""
seq_lengths = kwargs.get("packed_seq_lengths")
if seq_lengths is None:
return None
lengths = seq_lengths.to(device = device, dtype = torch.int32, non_blocking = True)
cu_seqlens = torch.empty(lengths.numel() + 1, dtype = torch.int32, device = device)
cu_seqlens[0] = 0
torch.cumsum(lengths, dim = 0, dtype = torch.int32, out = cu_seqlens[1:])
max_seqlen = int(lengths.max().item())
return lengths, cu_seqlens, max_seqlen
def build_xformers_block_causal_mask(
seq_info: Optional[Tuple[torch.Tensor, torch.Tensor, int]],
*,
sliding_window: Optional[int] = None,
base_mask: Optional[Any] = None,
):
if _XFormersBlockMask is None:
return None
if seq_info is not None:
seq_lengths, _, _ = seq_info
lengths_tensor = seq_lengths.to("cpu", torch.int32)
if lengths_tensor.numel() == 0:
return None
lengths = tuple(int(x) for x in lengths_tensor.tolist())
mask = _get_cached_block_mask(lengths, sliding_window)
else:
mask = base_mask
if (
sliding_window is not None
and sliding_window > 0
and mask is not None
and hasattr(mask, "make_local_attention")
):
mask = mask.make_local_attention(window_size = sliding_window)
return mask
def build_sdpa_packed_attention_mask(
seq_info: Tuple[torch.Tensor, torch.Tensor, int],
*,
dtype: torch.dtype,
device: torch.device,
sliding_window: Optional[int] = None,
) -> torch.Tensor:
seq_lengths, _, _ = seq_info
total_tokens = int(seq_lengths.sum().item())
mask = torch.full(
(total_tokens, total_tokens),
float("-inf"),
dtype = dtype,
device = device,
)
offset = 0
for length in seq_lengths.tolist():
length = int(length)
if length <= 0:
continue
block = torch.zeros((length, length), dtype = dtype, device = device)
upper = torch.triu(
torch.ones((length, length), device = device), diagonal = 1
).bool()
block = block.masked_fill(upper, float("-inf"))
if (
sliding_window is not None
and sliding_window > 0
and length > sliding_window
):
idx = torch.arange(length, device = device)
dist = idx.unsqueeze(1) - idx.unsqueeze(0)
window_mask = dist >= sliding_window
block = block.masked_fill(window_mask, float("-inf"))
mask[offset : offset + length, offset : offset + length] = block
offset += length
return mask.unsqueeze(0).unsqueeze(0)
def _normalize_packed_lengths(
seq_lengths: Any,
*,
device: torch.device,
) -> Optional[torch.Tensor]:
if seq_lengths is None:
return None
if isinstance(seq_lengths, torch.Tensor):
lengths = seq_lengths.to(device = device, dtype = torch.int64)
else:
lengths = torch.tensor(seq_lengths, device = device, dtype = torch.int64)
if lengths.ndim != 1:
lengths = lengths.reshape(-1)
if lengths.numel() == 0:
return None
return lengths
def mask_packed_sequence_boundaries(
shift_labels: torch.Tensor,
seq_lengths: Any,
*,
ignore_index: int = -100,
) -> bool:
"""Mark final token of every packed sample so CE ignores boundary predictions."""
lengths = _normalize_packed_lengths(seq_lengths, device = shift_labels.device)
if lengths is None:
return False
flat = shift_labels.reshape(-1)
total_tokens = flat.shape[0]
boundary_positions = torch.cumsum(lengths, dim = 0) - 1
valid = boundary_positions < total_tokens
if not torch.all(valid):
boundary_positions = boundary_positions[valid]
if boundary_positions.numel() == 0:
return False
flat[boundary_positions] = ignore_index
return True
__all__ = [
"configure_sample_packing",
"configure_padding_free",
"enable_sample_packing",
"enable_padding_free_metadata",
"mark_allow_overlength",
"get_packed_info_from_kwargs",
"build_xformers_block_causal_mask",
"build_sdpa_packed_attention_mask",
"mask_packed_sequence_boundaries",
]