From 500599832eba694991cae2765002d1d343823619 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 8 Jul 2026 08:22:50 +0000 Subject: [PATCH 1/6] version-compat CI: fake CPU training runs for SFT/GRPO/DPO Adds a runtime layer on top of the patch-run canary: actually runs trainer.train() for a couple of steps on a CPU-only runner under the CUDA spoof, wrapping a plain tiny HF model in the Unsloth-patched trainer. Exercises the real train() loop (collation, generation, the injected _get_per_token_logps_and_entropies, loss, backward, optimizer) so a TRL or transformers change that breaks the loop at runtime -- not just the source structure -- surfaces here. No GPU, no meaningful numerics. Needs a chain of small CPU shims (eager torch.compile, dynamo suppress, cuda tensor-alloc redirect to CPU, model.for_training/for_inference equivalents) documented inline. Does not exercise Unsloth's Triton/GPU kernels (CPU can't). --- .github/workflows/version-compat-ci.yml | 2 + .../version_compat/test_trl_fake_train_cpu.py | 221 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 tests/version_compat/test_trl_fake_train_cpu.py diff --git a/.github/workflows/version-compat-ci.yml b/.github/workflows/version-compat-ci.yml index b15d5bfa254..266614aa1ea 100644 --- a/.github/workflows/version-compat-ci.yml +++ b/.github/workflows/version-compat-ci.yml @@ -345,6 +345,7 @@ jobs: python -c "import trl; print('Resolved TRL', trl.__version__)" PYTHONPATH=. python -m pytest \ tests/version_compat/test_trl_grpo_fake_run.py \ + tests/version_compat/test_trl_fake_train_cpu.py \ -v --tb=short # `main` is scheduled/dispatch-only so PR jobs stay fast and a bleeding-edge # TRL break does not red every PR. github.event_name is valid in a step if. @@ -360,6 +361,7 @@ jobs: python -c "import trl; print('Resolved TRL', trl.__version__)" PYTHONPATH=. python -m pytest \ tests/version_compat/test_trl_grpo_fake_run.py \ + tests/version_compat/test_trl_fake_train_cpu.py \ -v --tb=short # Daily-only: same suites but with --strict on importable upstream diff --git a/tests/version_compat/test_trl_fake_train_cpu.py b/tests/version_compat/test_trl_fake_train_cpu.py new file mode 100644 index 00000000000..4a7fb5b4554 --- /dev/null +++ b/tests/version_compat/test_trl_fake_train_cpu.py @@ -0,0 +1,221 @@ +# 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(): + 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 + + +def test_sft_trains_on_cpu(): + 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 = "temp/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, + ) + SFTTrainer(model = model, processing_class = tok, args = cfg, train_dataset = ds).train() + + +def test_grpo_trains_on_cpu(): + 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 = "temp/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, + ) + 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(): + 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 = "temp/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, + ) + DPOTrainer(model = model, processing_class = tok, args = cfg, train_dataset = ds).train() From 6ed663ab3425541463085d3fa57a0d93395b2c01 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:28:45 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../version_compat/test_trl_fake_train_cpu.py | 91 ++++++++++++++----- 1 file changed, 69 insertions(+), 22 deletions(-) diff --git a/tests/version_compat/test_trl_fake_train_cpu.py b/tests/version_compat/test_trl_fake_train_cpu.py index 4a7fb5b4554..30db7cf62e7 100644 --- a/tests/version_compat/test_trl_fake_train_cpu.py +++ b/tests/version_compat/test_trl_fake_train_cpu.py @@ -39,7 +39,9 @@ # 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) + 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] @@ -50,12 +52,17 @@ 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): +def _eager_compile( + model = None, + *args, + **kwargs, +): if callable(model): return model return lambda fn: fn @@ -68,7 +75,6 @@ def _eager_compile(model = None, *args, **kwargs): # 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 @@ -88,14 +94,28 @@ def _is_cuda_dev(d): for _name in ( - "empty", "zeros", "ones", "full", "tensor", "arange", "randn", "rand", "randint", - "empty_like", "zeros_like", "ones_like", + "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): + def _redir( + *args, + _orig = _orig, + **kwargs, + ): if _is_cuda_dev(kwargs.get("device")): kwargs["device"] = "cpu" return _orig(*args, **kwargs) @@ -120,7 +140,6 @@ def _to_cpu(self, *args, **kwargs): 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 @@ -129,7 +148,6 @@ def _to_cpu(self, *args, **kwargs): # 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 @@ -178,10 +196,18 @@ def test_sft_trains_on_cpu(): model, tok = _load_plain() ds = Dataset.from_list([{"text": "The quick brown fox jumps over the lazy dog."}] * 8) cfg = SFTConfig( - output_dir = "temp/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, + output_dir = "temp/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, ) SFTTrainer(model = model, processing_class = tok, args = cfg, train_dataset = ds).train() @@ -194,15 +220,27 @@ def test_grpo_trains_on_cpu(): model, tok = _load_plain() ds = Dataset.from_list([{"prompt": "hi there"}] * 4) cfg = GRPOConfig( - output_dir = "temp/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, + output_dir = "temp/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, ) GRPOTrainer( - model = model, processing_class = tok, + model = model, + processing_class = tok, reward_funcs = [lambda completions, **k: [float(len(c)) for c in completions]], - args = cfg, train_dataset = ds, + args = cfg, + train_dataset = ds, ).train() @@ -212,10 +250,19 @@ def test_dpo_trains_on_cpu(): 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) + ds = Dataset.from_list( + [{"prompt": "Hi", "chosen": " hello friend", "rejected": " go away"}] * 8 + ) cfg = DPOConfig( - output_dir = "temp/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, + output_dir = "temp/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, ) DPOTrainer(model = model, processing_class = tok, args = cfg, train_dataset = ds).train() From dd2986b5fcb1ce7622ef03f5d90da2421c8256ce Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 8 Jul 2026 08:55:49 +0000 Subject: [PATCH 3/6] cpu fake-train: force adamw_torch + disable dynamo for CPU runner On a real CPU-build torch runner (GitHub CI) two things bit that a CUDA-build torch with GPUs hidden masked locally: - The default optimizer is adamw_8bit (bitsandbytes), whose is_on_gpu() check dies on CPU tensors. Force optim=adamw_torch in all three configs. - import unsloth reinstalls the real torch.compile over the eager passthrough, so the GRPO hot path (chunked_selective_log_softmax) actually compiles and inductor picks the spoofed CUDA device, crashing on device props (gcnArchName). Re-apply the eager passthrough after import and flip torch._dynamo.config.disable so every @torch.compile runs eager at call time. --- .../version_compat/test_trl_fake_train_cpu.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/version_compat/test_trl_fake_train_cpu.py b/tests/version_compat/test_trl_fake_train_cpu.py index 30db7cf62e7..fc8fbb1ffea 100644 --- a/tests/version_compat/test_trl_fake_train_cpu.py +++ b/tests/version_compat/test_trl_fake_train_cpu.py @@ -182,11 +182,26 @@ def _load_plain(): @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(): from datasets import Dataset @@ -208,6 +223,7 @@ def test_sft_trains_on_cpu(): dataset_text_field = "text", fp16 = False, bf16 = False, + optim = "adamw_torch", ) SFTTrainer(model = model, processing_class = tok, args = cfg, train_dataset = ds).train() @@ -234,6 +250,7 @@ def test_grpo_trains_on_cpu(): use_vllm = False, fp16 = False, bf16 = False, + optim = "adamw_torch", ) GRPOTrainer( model = model, @@ -264,5 +281,6 @@ def test_dpo_trains_on_cpu(): beta = 0.1, fp16 = False, bf16 = False, + optim = "adamw_torch", ) DPOTrainer(model = model, processing_class = tok, args = cfg, train_dataset = ds).train() From 1ac8376a0a70822cac6c9181f31f6d715ad94071 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 08:57:19 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/version_compat/test_trl_fake_train_cpu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/version_compat/test_trl_fake_train_cpu.py b/tests/version_compat/test_trl_fake_train_cpu.py index fc8fbb1ffea..2da6585be06 100644 --- a/tests/version_compat/test_trl_fake_train_cpu.py +++ b/tests/version_compat/test_trl_fake_train_cpu.py @@ -197,7 +197,6 @@ def _require_stack(): torch.compile = _eager_compile try: import torch._dynamo # noqa: E402 - torch._dynamo.config.disable = True except Exception: pass From fab5a2b2b3ab5fbf9a8267f764454c9453bdf99a Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 8 Jul 2026 09:36:23 +0000 Subject: [PATCH 5/6] cpu fake-train: write checkpoints under pytest tmp_path Use pytest's tmp_path for each trainer's output_dir instead of a hardcoded relative temp/ci_* path, so a local pytest run does not leave untracked dirs in the repo tree and the tests are CWD-independent. --- tests/version_compat/test_trl_fake_train_cpu.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/version_compat/test_trl_fake_train_cpu.py b/tests/version_compat/test_trl_fake_train_cpu.py index 2da6585be06..4dae6962829 100644 --- a/tests/version_compat/test_trl_fake_train_cpu.py +++ b/tests/version_compat/test_trl_fake_train_cpu.py @@ -202,7 +202,7 @@ def _require_stack(): pass -def test_sft_trains_on_cpu(): +def test_sft_trains_on_cpu(tmp_path): from datasets import Dataset from trl import SFTConfig, SFTTrainer @@ -210,7 +210,7 @@ def test_sft_trains_on_cpu(): model, tok = _load_plain() ds = Dataset.from_list([{"text": "The quick brown fox jumps over the lazy dog."}] * 8) cfg = SFTConfig( - output_dir = "temp/ci_sft", + output_dir = str(tmp_path / "ci_sft"), per_device_train_batch_size = 2, max_steps = 2, logging_steps = 1, @@ -227,7 +227,7 @@ def test_sft_trains_on_cpu(): SFTTrainer(model = model, processing_class = tok, args = cfg, train_dataset = ds).train() -def test_grpo_trains_on_cpu(): +def test_grpo_trains_on_cpu(tmp_path): from datasets import Dataset from trl import GRPOConfig, GRPOTrainer @@ -235,7 +235,7 @@ def test_grpo_trains_on_cpu(): model, tok = _load_plain() ds = Dataset.from_list([{"prompt": "hi there"}] * 4) cfg = GRPOConfig( - output_dir = "temp/ci_grpo", + output_dir = str(tmp_path / "ci_grpo"), per_device_train_batch_size = 2, num_generations = 2, max_steps = 2, @@ -260,7 +260,7 @@ def test_grpo_trains_on_cpu(): ).train() -def test_dpo_trains_on_cpu(): +def test_dpo_trains_on_cpu(tmp_path): from datasets import Dataset from trl import DPOConfig, DPOTrainer @@ -270,7 +270,7 @@ def test_dpo_trains_on_cpu(): [{"prompt": "Hi", "chosen": " hello friend", "rejected": " go away"}] * 8 ) cfg = DPOConfig( - output_dir = "temp/ci_dpo", + output_dir = str(tmp_path / "ci_dpo"), per_device_train_batch_size = 2, max_steps = 2, logging_steps = 1, From d901e5d79b217e4c65d11ab18ec26cfd6670a3d9 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 8 Jul 2026 10:40:27 +0000 Subject: [PATCH 6/6] version-compat CI: disable dynamo at process level for the fake-run job Set TORCHDYNAMO_DISABLE / TORCH_COMPILE_DISABLE in the fake-run step env so dynamo/inductor is off before conftest.py's early import unsloth, not only via the per-test runtime shim. Defense in depth on the GPU-less runner: the GRPO hot path never compiles regardless of when its functions were decorated. --- .github/workflows/version-compat-ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/version-compat-ci.yml b/.github/workflows/version-compat-ci.yml index 266614aa1ea..6becccc90ae 100644 --- a/.github/workflows/version-compat-ci.yml +++ b/.github/workflows/version-compat-ci.yml @@ -339,6 +339,11 @@ jobs: env: UNSLOTH_IS_PRESENT: '1' UNSLOTH_COMPILE_DISABLE: '1' + # Disable dynamo/inductor at the process level, before conftest.py's early + # `import unsloth`, so the GRPO hot path never compiles on the GPU-less runner + # (defense in depth; the CPU fake-train also flips this at runtime). + TORCHDYNAMO_DISABLE: '1' + TORCH_COMPILE_DISABLE: '1' PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION: python run: | cd unsloth @@ -354,6 +359,8 @@ jobs: env: UNSLOTH_IS_PRESENT: '1' UNSLOTH_COMPILE_DISABLE: '1' + TORCHDYNAMO_DISABLE: '1' + TORCH_COMPILE_DISABLE: '1' PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION: python run: | pip install --upgrade "git+https://github.com/huggingface/trl"