Files
wehub-resource-sync e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:59:56 +08:00

393 lines
13 KiB
Python

# 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)
collator_args = {
"pad_token_id": 0,
"completion_only_loss": False,
"return_tensors": "pt",
}
optional_flags = [
{"padding_free": True, "return_position_ids": False},
{"padding_free": True},
{},
]
for extra in optional_flags:
try:
self.data_collator = DataCollatorForLanguageModeling(**collator_args, **extra)
break
except TypeError:
continue
# Ensure attributes exist even if the constructor rejected the flags.
if not hasattr(self.data_collator, "padding_free"):
self.data_collator.padding_free = True
if not hasattr(self.data_collator, "return_position_ids"):
self.data_collator.return_position_ids = False
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 now allows 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 aggregated into one 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()