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:
parent
9510bba7e9
commit
888355b24a
1 changed files with 106 additions and 108 deletions
220
.github/workflows/consolidated-tests-ci.yml
vendored
220
.github/workflows/consolidated-tests-ci.yml
vendored
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue