CI(consolidated): route bare unsloth_zoo imports through pytest shim files

Run #6 progressed past install / collection but failed at step 10
(test_apply_fused_lm_head) inside unsloth_zoo/temporary_patches/gpt_oss.py:1141:

    device_memory = torch.cuda.memory.mem_get_info(0)[-1]
    AssertionError: Torch not compiled with CUDA enabled

The bare `python -c` heredoc spoofed torch.cuda.is_available but not the
deeper torch.cuda.memory.mem_get_info / cudart() lazy_init path. The
existing tests/conftest.py:84-141 already has the full spoof.

Switching three steps to write a one-shot shim test file under tests/ and
run it via pytest — pytest walks UP and applies tests/conftest.py before
the unsloth_zoo.* import, so the full GPU-spoof harness covers the deeper
mem_get_info / get_device_capability / is_bf16_supported probes:

- Step "test_apply_fused_lm_head": tests/_zoo_apply_fused_lm_head_shim.py
- Step "Runtime checks — invoke every zero-arg patch_*": tests/_runtime_patch_check_shim.py
- Step "Runtime checks — patch_tiled_mlp on a synthetic MLP module":
  tests/_tiled_mlp_check_shim.py

Each shim is rm-ed at the end of its step so it never lands in a commit.

Locally re-validated test_apply_fused_lm_head shim: 1 passed in 3.47 s.
This commit is contained in:
Daniel Han 2026-05-07 05:07:51 +00:00
commit 888355b24a

View file

@ -194,27 +194,28 @@ jobs:
--deselect tests/test_unsloth_zoo_lora_merge.py::test_merge_lora_moves_cpu_inputs_to_active_device
- name: unsloth_zoo — test_apply_fused_lm_head (lives in compiler.py)
# Not under tests/, so pytest's default discovery does not pick it
# up. Pure Python regex over transformers source strings; no GPU,
# no model download. Wall ~5-15 s, dominated by transformers import.
#
# The CUDA-spoof prelude mirrors tests/conftest.py:84-141: GH-hosted
# ubuntu-latest runners are GPU-less, and unsloth_zoo's __init__.py
# calls device_type.get_device_type() at module load, which raises
# NotImplementedError without an accelerator. We patch
# torch.cuda.is_available before the unsloth_zoo import so the
# cached @functools.cache-decorated get_device_type() captures
# "cuda" and the import chain finishes. The function under test
# is pure regex; spoofed CUDA presence has no effect on it.
# Env inherited from job block.
# `test_apply_fused_lm_head` lives at unsloth_zoo/compiler.py:1983,
# not under tests/, so pytest's default discovery does not pick it up.
# We route it through pytest by writing a one-shot shim test file
# inside the unsloth checkout's tests/ — pytest then walks UP and
# picks up tests/conftest.py, whose GPU-spoof harness (lines 84-141)
# patches torch.cuda.is_available, torch.cuda.memory.mem_get_info,
# torch.cuda.get_device_capability, and is_bf16_supported. That full
# spoof is required because unsloth_zoo/temporary_patches/gpt_oss.py
# at module load reads torch.cuda.memory.mem_get_info(0), which
# bare `is_available = True` doesn't cover. Env inherited.
run: |
python <<'PY'
import torch
torch.cuda.is_available = lambda: True
from unsloth_zoo.compiler import test_apply_fused_lm_head
test_apply_fused_lm_head()
print("OK: test_apply_fused_lm_head")
set -euxo pipefail
cat > tests/_zoo_apply_fused_lm_head_shim.py <<'PY'
# Auto-generated by .github/workflows/consolidated-tests-ci.yml.
# Wraps unsloth_zoo.compiler.test_apply_fused_lm_head so that
# tests/conftest.py's GPU-spoof harness applies before the import.
from unsloth_zoo.compiler import test_apply_fused_lm_head as _zoo_test
def test_zoo_apply_fused_lm_head_runs():
_zoo_test()
PY
python -m pytest -q --tb=short tests/_zoo_apply_fused_lm_head_shim.py
rm -f tests/_zoo_apply_fused_lm_head_shim.py
- name: Static checks — unsloth/trainer.py + unsloth/models/rl.py against latest pip TRL
# AST-only sanity: confirm both files parse and that every TRL symbol
@ -297,28 +298,24 @@ jobs:
print(f"hf_utils.py public surface ({len(public)}): " + ", ".join(public))
PY
- name: Runtime checks — invoke every zero-arg patch_* across both repos
# The user asked to confirm patch_* functions actually work, not just
# exist. Two layers:
# 1. Symbol presence: hasattr(module, name) — covers both direct
# definitions AND re-exports (patch_unsloth_smart_gradient_checkpointing
# is re-exported from unsloth/models/_utils.py:138 from
# unsloth_zoo/gradient_checkpointing.py:906).
# 2. Runtime invocation: every patch_* whose required parameters are
# all defaulted is called and the result type is reported. Most
# "patch_*" hooks return None on success and raise on failure; a
# handful return bool. A failure here means transformers/torch/trl
# drift broke a hook. Locally validated 50/51 succeed against the
# workspace's pinned deps (the one failure is a real bug surfaced
# by this check: unsloth.models._utils.patch_fast_lora raises
# NameError because fast_lora_forward is referenced unbound).
# The check explicitly does NOT fail on per-patch errors -- the whole
# job runs continue-on-error: true for the first pass anyway, and we
# want the full failure ledger in the log, not just the first one.
- name: Runtime checks — invoke every zero-arg patch_* across both repos (via pytest shim)
# Routed through pytest so tests/conftest.py's GPU-spoof harness
# applies before any unsloth_zoo.temporary_patches.* import.
# Locally validated 50/51 zero-arg patches succeed; the lone failure
# surfaces a real bug (unsloth.models._utils.patch_fast_lora raises
# NameError: name 'fast_lora_forward' is not defined). The shim
# reports the full ledger but only fails when one of the two
# `required` helpers is absent.
run: |
set -euxo pipefail
python <<'PY'
import importlib, inspect, sys
cat > tests/_runtime_patch_check_shim.py <<'PY'
# Auto-generated by .github/workflows/consolidated-tests-ci.yml.
# Wraps the runtime patch_* validation into a pytest test so the
# tests/conftest.py GPU-spoof harness applies. continue-on-error
# at the workflow level catches per-patch failures; this shim only
# asserts that the two `required` helpers are reachable.
import importlib, inspect
MODULES = [
"unsloth.models._utils", "unsloth.models.rl", "unsloth.import_fixes",
"unsloth.kernels.cross_entropy_loss", "unsloth.kernels.rms_layernorm",
@ -336,67 +333,70 @@ jobs:
"unsloth_zoo.temporary_patches.bitsandbytes",
"unsloth_zoo.temporary_patches.flex_attention_bwd",
]
required = {
REQUIRED = {
"patch_unsloth_smart_gradient_checkpointing",
"patch_gradient_accumulation_fix",
}
ok, fail, args, miss_imports = 0, [], [], {}
seen_required = set()
for mod_name in MODULES:
try:
mod = importlib.import_module(mod_name)
except Exception as e:
miss_imports[mod_name] = f"{type(e).__name__}: {e}"
continue
for name in sorted(dir(mod)):
if not name.startswith("patch_"): continue
fn = getattr(mod, name, None)
if not callable(fn): continue
if name in required: seen_required.add(name)
try:
sig = inspect.signature(fn)
need = [p.name for p in sig.parameters.values()
if p.default is inspect.Parameter.empty
and p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.POSITIONAL_ONLY)]
except (TypeError, ValueError):
need = []
if need:
args.append((mod_name, name, need)); continue
try:
fn()
ok += 1
print(f" OK {mod_name}.{name}")
except Exception as e:
fail.append((mod_name, name, type(e).__name__, str(e)[:200]))
print(f" FAIL {mod_name}.{name} -> {type(e).__name__}: {str(e)[:200]}")
print(f"\nzero-arg patch_*: ok={ok} fail={len(fail)}")
print(f"arg-required patch_*: {len(args)}")
for m, n, r in args:
print(f" needs={r}: {m}.{n}")
if miss_imports:
print("\nmodules failed to import (skipped):")
for k, v in miss_imports.items():
print(f" {k}: {v}")
missing_required = required - seen_required
if missing_required:
print(f"\n::error::required patch_* helpers MISSING: {sorted(missing_required)}")
sys.exit(1)
print(f"\nrequired patch_* helpers present: {sorted(seen_required)}")
PY
- name: Runtime checks — patch_tiled_mlp on a synthetic MLP module
# Build a minimal nn.Module with the gate_proj/up_proj/down_proj
# surface the tiled MLP patcher expects, then apply patch_tiled_mlp
# and confirm the forward still produces the same output (within
# numerical tolerance). No real model download.
def test_zero_arg_patch_invocations():
ok, fail, args, miss_imports = 0, [], [], {}
seen_required = set()
for mod_name in MODULES:
try:
mod = importlib.import_module(mod_name)
except Exception as e:
miss_imports[mod_name] = f"{type(e).__name__}: {e}"
continue
for name in sorted(dir(mod)):
if not name.startswith("patch_"): continue
fn = getattr(mod, name, None)
if not callable(fn): continue
if name in REQUIRED: seen_required.add(name)
try:
sig = inspect.signature(fn)
need = [p.name for p in sig.parameters.values()
if p.default is inspect.Parameter.empty
and p.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.POSITIONAL_ONLY)]
except (TypeError, ValueError):
need = []
if need:
args.append((mod_name, name, need)); continue
try:
fn()
ok += 1
print(f" OK {mod_name}.{name}")
except Exception as e:
fail.append((mod_name, name, type(e).__name__, str(e)[:200]))
print(f" FAIL {mod_name}.{name} -> {type(e).__name__}: {str(e)[:200]}")
print(f"\nzero-arg patch_*: ok={ok} fail={len(fail)}")
print(f"arg-required patch_* (skipped, listed for review): {len(args)}")
for m, n, r in args:
print(f" needs={r}: {m}.{n}")
if miss_imports:
print("\nmodules failed to import (skipped):")
for k, v in miss_imports.items():
print(f" {k}: {v}")
print(f"required patch_* helpers seen: {sorted(seen_required)}")
missing = REQUIRED - seen_required
assert not missing, f"required patch_* helpers MISSING: {sorted(missing)}"
PY
python -m pytest -q --tb=short tests/_runtime_patch_check_shim.py -s
rm -f tests/_runtime_patch_check_shim.py
- name: Runtime checks — patch_tiled_mlp on a synthetic MLP module (via pytest shim)
# Same shim pattern: pytest picks up tests/conftest.py before importing
# unsloth_zoo.tiled_mlp, so the GPU-spoof harness covers
# unsloth_zoo.temporary_patches.gpt_oss's mem_get_info call.
run: |
set -euxo pipefail
python <<'PY'
import torch, torch.nn as nn
cat > tests/_tiled_mlp_check_shim.py <<'PY'
# Auto-generated by .github/workflows/consolidated-tests-ci.yml.
import torch
import torch.nn as nn
from unsloth_zoo.tiled_mlp import patch_tiled_mlp, patch_mlp
class MLP(nn.Module):
class _MLP(nn.Module):
def __init__(self, hidden=64, intermediate=128):
super().__init__()
self.gate_proj = nn.Linear(hidden, intermediate, bias=False)
@ -406,33 +406,31 @@ jobs:
def forward(self, x):
return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
class FakeModel(nn.Module):
class _FakeModel(nn.Module):
def __init__(self):
super().__init__()
self.layers = nn.ModuleList([nn.ModuleDict({"mlp": MLP()})
for _ in range(2)])
self.layers = nn.ModuleList([nn.ModuleDict({"mlp": _MLP()}) for _ in range(2)])
def forward(self, x):
for layer in self.layers:
x = x + layer["mlp"](x)
return x
torch.manual_seed(0)
m = FakeModel().eval()
x = torch.randn(2, 4, 64)
with torch.no_grad():
y_before = m(x).clone()
# patch_mlp on a single MLP module is the unit-level entry point.
patch_mlp(m.layers[0]["mlp"])
# patch_tiled_mlp walks a model and replaces matching MLPs.
patch_tiled_mlp(m)
with torch.no_grad():
y_after = m(x).clone()
# tiled MLP should be numerically equivalent to the eager path.
err = (y_before - y_after).abs().max().item()
print(f"patch_tiled_mlp output diff = {err:.3e}")
assert err < 1e-3, f"tiled MLP output drifted: {err}"
print("OK: patch_tiled_mlp + patch_mlp")
def test_patch_tiled_mlp_numerical_equivalence():
torch.manual_seed(0)
m = _FakeModel().eval()
x = torch.randn(2, 4, 64)
with torch.no_grad():
y_before = m(x).clone()
patch_mlp(m.layers[0]["mlp"])
patch_tiled_mlp(m)
with torch.no_grad():
y_after = m(x).clone()
err = (y_before - y_after).abs().max().item()
print(f"patch_tiled_mlp output diff = {err:.3e}")
assert err < 1e-3, f"tiled MLP output drifted: {err}"
PY
python -m pytest -q --tb=short tests/_tiled_mlp_check_shim.py -s
rm -f tests/_tiled_mlp_check_shim.py
- name: llama.cpp install + `llama-cli --help` smoke
# The user asked to confirm llama.cpp installs and the CLI runs.