Files
unslothai--unsloth/tests/version_compat/test_trl_fake_train_cpu.py
T
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

286 lines
9.7 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team.
"""Fake CPU training runs for the Unsloth-patched SFT / GRPO / DPO trainers.
The patch-run canary (test_trl_grpo_fake_run.py) only compiles + inspects the
generated trainer source. This goes one layer deeper: it actually runs
`trainer.train()` for a couple of steps on a CPU-only runner, under the CUDA
spoof, wrapping a plain (tiny, random-weight) HF model in the Unsloth-patched
trainer. That exercises the real train() loop at runtime -- data collation,
generation (GRPO), the injected `_get_per_token_logps_and_entropies`, loss,
backward, optimizer -- so a TRL or transformers change that breaks the loop
(not just the source structure) surfaces here. No GPU, no meaningful numerics.
What it does NOT cover: Unsloth's Triton/GPU-optimized model kernels (the
FastLanguageModel fast path) cannot run on CPU, so this validates the
trainer-transform + orchestration layer with a standard forward, not the
optimized kernels.
"""
from __future__ import annotations
import os
# CPU-only: no torch.compile / dynamo (it reaches into the CUDA accelerator), no
# Unsloth kernel compile, no mixed precision. Must be set before torch/unsloth.
os.environ.setdefault("UNSLOTH_COMPILE_DISABLE", "1")
os.environ.setdefault("TORCHDYNAMO_DISABLE", "1")
os.environ.setdefault("TORCH_COMPILE_DISABLE", "1")
os.environ.setdefault("ACCELERATE_MIXED_PRECISION", "no")
import importlib
import importlib.util
import sys
from pathlib import Path
import pytest
# torch is needed for everything below (daily-fresh-fetch collects this dir with
# only pytest installed); skip the whole module cleanly when it is absent.
if importlib.util.find_spec("torch") is None:
pytest.skip(
"torch not installed; fake CPU train needs the real runtime", allow_module_level = True
)
# Apply the CUDA spoof before any unsloth-touching import.
_SPOOF_DIR = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(_SPOOF_DIR))
import _zoo_aggressive_cuda_spoof as _spoof # noqa: E402
_spoof.apply()
import torch # noqa: E402
# The generated GRPO trainer hard-decorates hot functions with @torch.compile,
# which dynamo processes even under the disable env vars, reaching into
# torch.accelerator (real CUDA) on a GPU-less box. Make torch.compile an eager
# passthrough before unsloth generates/imports the trainer -- same logic, no
# dynamo. (An eager CPU run is exactly what we want here.)
def _eager_compile(
model = None,
*args,
**kwargs,
):
if callable(model):
return model
return lambda fn: fn
torch.compile = _eager_compile
# Belt-and-suspenders: if any @torch.compile still routes through dynamo, let it
# fall back to eager instead of crashing, and stop its stream-capture probe from
# reaching torch.accelerator -> real CUDA on a GPU-less box.
try:
import torch._dynamo # noqa: E402
torch._dynamo.config.suppress_errors = True
except Exception:
pass
if hasattr(torch, "accelerator"):
torch.accelerator.is_available = lambda *a, **k: False
# Redirect any `device="cuda"` tensor allocation / `.to("cuda")` / `.cuda()` to
# CPU. The aggressive spoof deliberately keeps real allocators, but a fake CPU
# train needs cuda-targeted ops (e.g. inductor's init_gpu_context does
# `torch.empty(1, device="cuda")`) to land on CPU instead of erroring.
def _is_cuda_dev(d):
try:
return d is not None and torch.device(d).type == "cuda"
except Exception:
return False
for _name in (
"empty",
"zeros",
"ones",
"full",
"tensor",
"arange",
"randn",
"rand",
"randint",
"empty_like",
"zeros_like",
"ones_like",
):
_orig = getattr(torch, _name, None)
if _orig is None:
continue
def _redir(
*args,
_orig = _orig,
**kwargs,
):
if _is_cuda_dev(kwargs.get("device")):
kwargs["device"] = "cpu"
return _orig(*args, **kwargs)
setattr(torch, _name, _redir)
_orig_to = torch.Tensor.to
def _to_cpu(self, *args, **kwargs):
args = tuple("cpu" if _is_cuda_dev(a) else a for a in args)
if _is_cuda_dev(kwargs.get("device")):
kwargs["device"] = "cpu"
return _orig_to(self, *args, **kwargs)
torch.Tensor.to = _to_cpu
torch.Tensor.cuda = lambda self, *a, **k: self
# Extra CUDA stubs the aggressive spoof lacks, needed to walk a real train():
# Adam's _cuda_graph_capture_health_check() probes stream capture.
torch.cuda.is_current_stream_capturing = lambda *a, **k: False
try:
import torch.cuda.graphs as _cg # noqa: E402
_cg._cuda_isCurrentStreamCapturing = lambda *a, **k: False
except Exception:
pass
# A broken libmlx.so in the shared site-packages crashes transformers' Mac-only
# is_mlx_array probe on Linux; disable it.
try:
import transformers.utils.generic as _g # noqa: E402
_g._is_mlx_available = False
except Exception:
pass
# Dense (non-MoE) tiny model on purpose: MoE models route through Unsloth's
# grouped_gemm Triton kernel, which is CUDA-only and cannot run on a CPU runner.
_MODEL = "hf-internal-testing/tiny-random-LlamaForCausalLM"
def _load_plain():
"""Tiny plain HF model + tokenizer on CPU. Skips (not fails) if the model
cannot be fetched -- that is a network/hub issue, not an unsloth regression."""
from transformers import AutoModelForCausalLM, AutoTokenizer
try:
tok = AutoTokenizer.from_pretrained(_MODEL)
model = AutoModelForCausalLM.from_pretrained(_MODEL, dtype = torch.float32)
except OSError as e: # hub unreachable / model missing
pytest.skip(f"could not fetch {_MODEL} (network/hub): {str(e)[:150]}")
if tok.pad_token is None:
tok.pad_token = tok.eos_token
# Unsloth's GRPO path calls model.for_training()/for_inference() (added by
# FastLanguageModel). A plain HF model lacks them; supply minimal train/eval
# equivalents so the loop proceeds without the optimized wrapper.
if not hasattr(model, "for_training"):
model.for_training = lambda *a, **k: model.train()
if not hasattr(model, "for_inference"):
model.for_inference = lambda *a, **k: model.eval()
return model.to("cpu"), tok
@pytest.fixture(autouse = True)
def _require_stack():
global torch # the `import torch._dynamo` below would otherwise shadow it as local
if importlib.util.find_spec("unsloth") is None or importlib.util.find_spec("trl") is None:
pytest.skip("unsloth or trl not installed")
# A real import failure is a regression we want to surface, so do not guard it.
import unsloth # noqa: F401 -- patches TRL trainers to the Unsloth variants
# `import unsloth` reinstalls the real torch.compile (overwriting the eager
# passthrough set at module load), so the GRPO hot path (chunked_selective_
# log_softmax) would really compile -- and inductor picks the spoofed CUDA
# device, crashing on device props (`gcnArchName`). Re-apply the eager
# passthrough and flip dynamo's call-time kill switch so every @torch.compile
# runs eager regardless of when it was decorated. CPU eager is what we want.
torch.compile = _eager_compile
try:
import torch._dynamo # noqa: E402
torch._dynamo.config.disable = True
except Exception:
pass
def test_sft_trains_on_cpu(tmp_path):
from datasets import Dataset
from trl import SFTConfig, SFTTrainer
assert SFTTrainer.__name__ == "UnslothSFTTrainer", "SFT patch did not apply"
model, tok = _load_plain()
ds = Dataset.from_list([{"text": "The quick brown fox jumps over the lazy dog."}] * 8)
cfg = SFTConfig(
output_dir = str(tmp_path / "ci_sft"),
per_device_train_batch_size = 2,
max_steps = 2,
logging_steps = 1,
report_to = "none",
save_strategy = "no",
use_cpu = True,
max_length = None,
padding_free = False,
dataset_text_field = "text",
fp16 = False,
bf16 = False,
optim = "adamw_torch",
)
SFTTrainer(model = model, processing_class = tok, args = cfg, train_dataset = ds).train()
def test_grpo_trains_on_cpu(tmp_path):
from datasets import Dataset
from trl import GRPOConfig, GRPOTrainer
assert GRPOTrainer.__name__ == "UnslothGRPOTrainer", "GRPO patch did not apply"
model, tok = _load_plain()
ds = Dataset.from_list([{"prompt": "hi there"}] * 4)
cfg = GRPOConfig(
output_dir = str(tmp_path / "ci_grpo"),
per_device_train_batch_size = 2,
num_generations = 2,
max_steps = 2,
max_completion_length = 8,
logging_steps = 1,
report_to = "none",
temperature = 1.0,
beta = 0.0,
save_strategy = "no",
use_cpu = True,
use_vllm = False,
fp16 = False,
bf16 = False,
optim = "adamw_torch",
)
GRPOTrainer(
model = model,
processing_class = tok,
reward_funcs = [lambda completions, **k: [float(len(c)) for c in completions]],
args = cfg,
train_dataset = ds,
).train()
def test_dpo_trains_on_cpu(tmp_path):
from datasets import Dataset
from trl import DPOConfig, DPOTrainer
assert DPOTrainer.__name__ == "UnslothDPOTrainer", "DPO patch did not apply"
model, tok = _load_plain()
ds = Dataset.from_list(
[{"prompt": "Hi", "chosen": " hello friend", "rejected": " go away"}] * 8
)
cfg = DPOConfig(
output_dir = str(tmp_path / "ci_dpo"),
per_device_train_batch_size = 2,
max_steps = 2,
logging_steps = 1,
report_to = "none",
save_strategy = "no",
use_cpu = True,
beta = 0.1,
fp16 = False,
bf16 = False,
optim = "adamw_torch",
)
DPOTrainer(model = model, processing_class = tok, args = cfg, train_dataset = ds).train()