Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
pre-commit-ci[bot]
a1bd05d301 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2026-07-09 01:58:53 +00:00
Daniel Han
0fc33292c7 Reset torch.compile cache after a pre-train generate, not just a grad probe
PR #6511 drops the torch.compile graph cache that a stray forward/backward before
trainer.train() poisons, but its forward-hook detector only fires for a grad-enabled
manual forward. A model.generate() before training slips through: generation drives
the inner model (so the wrapped-forward hook never fires) and runs under no_grad
(which the detector ignores), yet it still specializes the fullgraph MoE kernels
(e.g. gpt-oss). The training recompile then hard-fails with FailOnRecompileLimitHit
under fullgraph=True.

for_inference now flags the generation on the pre-train marker via a new
_unsloth_flag_pretrain_generation helper, and _unsloth_reset_stray_compile_cache
resets the cache when either a grad probe or a generation preceded train(). The
grad-probe warning is unchanged; a pre-train generate is a normal pattern, so its
reset is silent.

Verified on gpt-oss-20b (bnb-4bit) LoRA SFT: generate-then-train hard-fails on main
and with the fix neutralized, and trains cleanly (loss 2.13 to 1.40) with the fix
active.
2026-07-09 01:57:10 +00:00
3 changed files with 64 additions and 17 deletions

View file

@ -63,6 +63,7 @@ __all__ = [
"unpatch_unsloth_smart_gradient_checkpointing",
"apply_unsloth_gradient_checkpointing",
"_unsloth_install_pretrain_detector",
"_unsloth_flag_pretrain_generation",
"_unsloth_reset_stray_compile_cache",
"patch_compiled_autograd",
"process_vision_info",
@ -314,23 +315,58 @@ def _unsloth_install_pretrain_detector(model):
return model
def _unsloth_flag_pretrain_generation(model):
"""Record that a torch.compile generation ran before trainer.train().
model.generate() does not trip the pre-train forward hook: generation drives the inner
model, not the wrapped forward the hook sits on, so the hook never fires (and generation
is no-grad, which _mark ignores anyway). But a no-grad generate still specializes the
fullgraph MoE kernels (e.g. gpt-oss); reusing those inference graphs hard-fails the
training recompile (FailOnRecompileLimitHit under fullgraph=True). Set a persistent marker
so the next prepare_for_training_mode drops the cache. Walk the wrapper chain since the
marker can sit on a different node than the one for_inference is called on. No-op if no
detector was installed (e.g. an untrained model)."""
_curr = model
_visited = set()
while _curr is not None and id(_curr) not in _visited:
_visited.add(id(_curr))
_m = getattr(_curr, "_unsloth_pretrain_marker", None)
if isinstance(_m, dict):
_m["generation"] = True
# Follow the wrapper chain: Unsloth/HF (.model), PEFT (.base_model), DDP/FSDP (.module).
_nxt = getattr(_curr, "model", None)
if _nxt is None:
_nxt = getattr(_curr, "base_model", None)
if _nxt is None:
_nxt = getattr(_curr, "module", None)
_curr = _nxt
return model
def _unsloth_reset_stray_compile_cache(self):
# A manual forward/backward under torch.compile BEFORE trainer.train() (e.g. a grad-norm
# probe) caches a forward + AOTAutograd backward graph in a one-off context; reusing it
# poisons training with NaN/zero gradients. If such a forward was seen and compile is on,
# drop the compiled-graph cache so training recompiles cleanly. No-op on the normal path.
# Module-level (not just inside the RL trainer template) so the SFT auto-packing wrapper and
# the plain-Trainer loop can import and run it too.
# A forward under torch.compile BEFORE trainer.train() specializes graphs that poison
# training when reused. Two flavours, both reset here:
# * "seen" - a grad-enabled manual forward/backward (e.g. a grad-norm probe) caches
# a forward + AOTAutograd backward graph, giving NaN/zero gradients.
# * "generation" - a model.generate() (even no-grad) specializes the fullgraph MoE kernels
# (e.g. gpt-oss); the training recompile then hard-fails with
# FailOnRecompileLimitHit. The forward hook cannot catch this (generate
# drives the inner model), so for_inference flags it via
# _unsloth_flag_pretrain_generation.
# If either was seen and compile is on, drop the compiled-graph cache so training recompiles
# cleanly. No-op on the normal path. Module-level (not just inside the RL trainer template)
# so the SFT auto-packing wrapper and the plain-Trainer loop can import and run it too.
import os
model = getattr(self, "model", None)
if model is None:
return
# The detector hook can sit on any wrapper in the chain, and the probe may have run on a
# different one than self.model, so walk the chain: detect a "seen" marker anywhere and
# collect every marker to tear down below.
# The detector marker can sit on any wrapper in the chain, and the probe/generation may have
# run on a different node than self.model, so walk the chain: detect a flagged marker
# anywhere and collect every marker to tear down below.
markers = []
seen = False
generation = False
_curr = model
_visited = set()
while _curr is not None and id(_curr) not in _visited:
@ -340,6 +376,8 @@ def _unsloth_reset_stray_compile_cache(self):
markers.append(_m)
if _m.get("seen"):
seen = True
if _m.get("generation"):
generation = True
# Follow the wrapper chain: Unsloth/HF (.model), PEFT (.base_model), DDP/FSDP (.module).
_nxt = getattr(_curr, "model", None)
if _nxt is None:
@ -347,7 +385,7 @@ def _unsloth_reset_stray_compile_cache(self):
if _nxt is None:
_nxt = getattr(_curr, "module", None)
_curr = _nxt
if seen and os.environ.get("UNSLOTH_COMPILE_DISABLE", "0") != "1":
if (seen or generation) and os.environ.get("UNSLOTH_COMPILE_DISABLE", "0") != "1":
try:
import torch._dynamo as _dynamo
_dynamo.reset()
@ -364,13 +402,15 @@ def _unsloth_reset_stray_compile_cache(self):
model.zero_grad(set_to_none = True)
except Exception:
pass
import warnings
warnings.warn(
"Unsloth: detected a manual forward/backward run before trainer.train(); "
"reset the torch.compile graph cache it poisoned so training starts clean. "
"To avoid this, run any pre-train probe under `with torch.no_grad():`."
)
# Only the grad-enabled probe is a likely mistake worth flagging; a pre-train generate is
# a normal pattern, so reset its cache silently.
if seen:
import warnings
warnings.warn(
"Unsloth: detected a manual forward/backward run before trainer.train(); "
"reset the torch.compile graph cache it poisoned so training starts clean. "
"To avoid this, run any pre-train probe under `with torch.no_grad():`."
)
# Tear down every one-shot detector hook in the chain so none adds per-step cost.
for _m in markers:
hook = _m.pop("hook", None)
@ -380,6 +420,7 @@ def _unsloth_reset_stray_compile_cache(self):
except Exception:
pass
_m["seen"] = False
_m["generation"] = False
def apply_unsloth_gradient_checkpointing(use_gradient_checkpointing, max_seq_length, dtype):

View file

@ -3770,6 +3770,9 @@ class FastLlamaModel:
restore_use_cache(model)
except ImportError:
pass
# Flag that a compiled generation ran, so the next trainer.train() drops the
# torch.compile cache it specialized before it hard-fails the training recompile.
_unsloth_flag_pretrain_generation(model)
return model
@staticmethod

View file

@ -2113,6 +2113,9 @@ class FastBaseModel:
# Turn off skip guards and set stance to default
if torch_compiler_set_stance is not None:
torch_compiler_set_stance(stance = "default", skip_guard_eval_unsafe = False)
# Flag that a compiled generation ran, so the next trainer.train() drops the
# torch.compile cache it specialized before it hard-fails the training recompile.
_unsloth_flag_pretrain_generation(model)
return model
@staticmethod