Skip to content

Commit 90c3ffa

Browse files
committed
refactor: RULE #1 B4 - fused crash raises by default (was silent eager-degrade), opt-in loud bisect
B4 (eager-replaces-fused): _run_fused_value_and_grad_guarded swallowed any fused-runtime crash/timeout and silently ran eager autograd by default -- masking a broken fused Path C kernel with a fine-looking eager result (FORBIDDEN per RULE #1). Now: raise-by-default on fused crash ("Refusing to silently fall back" + where+what); the timeout watchdog also raises by default. The dual-use bisection net is preserved as an EXPLICIT opt-in: MLX_PATH_C_FUSED_ALLOW_EAGER_DEGRADE=1 -> LOUD degrade (RuntimeWarning + records path_c_fused_runtime_evidence/fallback_count/fallback_error), never the silent default. B5 (matmul2d/scalar markers) + force_path_c gated-off sites: classified ACCEPTABLE (status-report / structural legality predicate / explicit user-reference routing -- production force_path_c=True always raises), left untouched. Metal: 137 passed; synthetic crash -> raises, opt-in env -> warns+degrades, watchdog -> raises. On CUDA a real fused crash now surfaces instead of returning a fine-looking eager result.
1 parent be7299d commit 90c3ffa

1 file changed

Lines changed: 93 additions & 33 deletions

File tree

cppmega_mlx/training/compiled.py

Lines changed: 93 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1975,26 +1975,45 @@ def _run_fused_value_and_grad_guarded(
19751975
Returns the ``((loss, ntokens), grads)`` tuple on success, or ``None`` to
19761976
signal the caller should fall back to the eager loss_and_grad path.
19771977
1978-
The always-on guard is exception-based: if the fused runtime raises (e.g.
1979-
the MLX<->CUDA buffer handoff, incomplete fused gradient extraction, or a
1980-
CUDA launch/exec error surfaced by the bridge's explicit
1981-
``torch.cuda.synchronize()``), we catch it and degrade to the eager
1982-
baseline so the run never regresses to steps_completed=0.
1978+
RULE #1 (fail fast, fail loud): a fused-runtime crash is a *real bug* in
1979+
the selected Path C kernel / bridge. By DEFAULT this method therefore
1980+
RAISES with where+what instead of silently substituting an eager result
1981+
that looks fine but hides the broken fused path. Surfacing the bug is the
1982+
priority.
1983+
1984+
The eager degrade is retained ONLY as an explicit, opt-in bisection
1985+
escape, enabled by setting ``MLX_PATH_C_FUSED_ALLOW_EAGER_DEGRADE`` to a
1986+
truthy value (``1``/``true``/``yes``/``on``). When opted in, a fused
1987+
crash is *loudly* degraded to eager — a ``RuntimeWarning`` is emitted and
1988+
the receipt fields (``_path_c_runtime_fallback_count`` /
1989+
``_path_c_runtime_fallback_error``, surfaced as
1990+
``path_c_fused_runtime_evidence`` in the train-step report) record that
1991+
the run used a degraded path. This is for deliberate bisection only; it
1992+
is never the silent default.
19831993
19841994
An OPTIONAL wall-clock watchdog is enabled only when
19851995
``MLX_PATH_C_FUSED_STEP_TIMEOUT_S`` is set to a positive number. It runs
1986-
the fused call on a daemon worker thread and, on timeout, permanently
1987-
disables the fused runtime and falls back to eager. It is opt-in because
1988-
a CUDA kernel that is launched but does not return cannot be cancelled
1989-
in-process: the abandoned worker keeps the runaway kernel resident on the
1990-
GPU, starving the eager fallback. The clean default therefore runs the
1991-
fused call inline (no leaked kernel) and relies on the exception guard.
1992-
The first failure / timeout is recorded for reporting.
1996+
the fused call on a daemon worker thread and, on timeout, treats the
1997+
runaway fused kernel exactly like a crash: RAISE by default, or (with the
1998+
opt-in degrade env) permanently disable the fused runtime and fall back
1999+
to eager. The watchdog is opt-in because a CUDA kernel that is launched
2000+
but does not return cannot be cancelled in-process: the abandoned worker
2001+
keeps the runaway kernel resident on the GPU. The clean default therefore
2002+
runs the fused call inline (no leaked kernel) and relies on the exception
2003+
guard. The first failure / timeout is recorded for reporting.
19932004
"""
19942005

19952006
import os
19962007
import warnings
19972008

2009+
degrade_env = os.environ.get("MLX_PATH_C_FUSED_ALLOW_EAGER_DEGRADE", "")
2010+
allow_eager_degrade = degrade_env.strip().lower() in (
2011+
"1",
2012+
"true",
2013+
"yes",
2014+
"on",
2015+
)
2016+
19982017
timeout_env = os.environ.get("MLX_PATH_C_FUSED_STEP_TIMEOUT_S")
19992018
timeout_s = 0.0
20002019
if timeout_env:
@@ -2026,19 +2045,37 @@ def _worker() -> None:
20262045
worker.join(timeout=timeout_s)
20272046

20282047
if worker.is_alive():
2029-
# Timed out — a runaway / impractically slow fused kernel. Abandon
2030-
# the worker (daemon) and permanently disable the fused runtime so
2031-
# every subsequent step uses the eager baseline.
2032-
self._path_c_runtime_disabled = True
2048+
# Timed out — a runaway / impractically slow fused kernel. This is
2049+
# a real bug in the fused Path C kernel/bridge.
2050+
timeout_detail = (
2051+
f"fused value_and_grad exceeded {timeout_s:g}s budget"
2052+
)
20332053
if self._path_c_runtime_fallback_error is None:
2034-
self._path_c_runtime_fallback_error = (
2035-
f"fused value_and_grad exceeded {timeout_s:g}s budget"
2036-
)
2054+
self._path_c_runtime_fallback_error = timeout_detail
20372055
self._path_c_runtime_fallback_count += 1
2056+
if not allow_eager_degrade:
2057+
# RULE #1: surface the runaway fused kernel instead of
2058+
# silently degrading to eager.
2059+
raise RuntimeError(
2060+
"Path C fused direct-chain runtime.value_and_grad timed out "
2061+
f"in CompiledPretrainingStep._run_fused_value_and_grad_guarded "
2062+
f"({timeout_detail}). Refusing to silently fall back to the "
2063+
"eager loss_and_grad path (RULE #1) — this points at a runaway "
2064+
"or pathologically slow fused Path C kernel/bridge. Set "
2065+
"MLX_PATH_C_FUSED_ALLOW_EAGER_DEGRADE=1 to opt into a loud "
2066+
"eager degrade for bisection only."
2067+
)
2068+
# Opt-in bisection escape: abandon the worker (daemon) and
2069+
# permanently disable the fused runtime so every subsequent step
2070+
# uses the eager baseline. LOUD (warning + receipt fields).
2071+
self._path_c_runtime_disabled = True
20382072
warnings.warn(
20392073
"Path C fused direct-chain runtime.value_and_grad exceeded the "
2040-
f"{timeout_s:g}s budget; disabling the fused runtime and falling "
2041-
"back to the eager loss_and_grad path for the rest of the run.",
2074+
f"{timeout_s:g}s budget; MLX_PATH_C_FUSED_ALLOW_EAGER_DEGRADE is "
2075+
"set, so disabling the fused runtime and degrading to the eager "
2076+
"loss_and_grad path for the rest of the run (bisection mode). The "
2077+
"underlying fused-kernel bug is NOT fixed; the receipt field "
2078+
"path_c_fused_runtime_evidence records this degraded run.",
20422079
RuntimeWarning,
20432080
stacklevel=2,
20442081
)
@@ -2063,18 +2100,40 @@ def _worker() -> None:
20632100
f"{type(error).__name__}: {error}"
20642101
)
20652102
self._path_c_runtime_fallback_count += 1
2103+
if not allow_eager_degrade:
2104+
# RULE #1: a fused-runtime crash is a real bug in the selected
2105+
# Path C kernel/bridge. Surface it instead of silently producing
2106+
# an eager result that masks the broken fused path.
2107+
raise RuntimeError(
2108+
"Path C fused direct-chain runtime.value_and_grad crashed in "
2109+
"CompiledPretrainingStep._run_fused_value_and_grad_guarded "
2110+
f"({type(error).__name__}: {error}). Refusing to silently fall "
2111+
"back to the eager loss_and_grad path (RULE #1) — this points at "
2112+
"a real bug in the fused Path C kernel/bridge (e.g. the MLX<->CUDA "
2113+
"buffer handoff, incomplete fused gradient extraction, or a CUDA "
2114+
"launch/exec error). Set MLX_PATH_C_FUSED_ALLOW_EAGER_DEGRADE=1 to "
2115+
"opt into a loud eager degrade for bisection only."
2116+
) from error
2117+
# Opt-in bisection escape: LOUD degrade (warning + receipt fields).
20662118
warnings.warn(
20672119
"Path C fused direct-chain runtime.value_and_grad failed; "
2068-
"falling back to the eager loss_and_grad path: "
2069-
f"{type(error).__name__}: {error}",
2120+
"MLX_PATH_C_FUSED_ALLOW_EAGER_DEGRADE is set, so degrading to the "
2121+
"eager loss_and_grad path (bisection mode). The underlying fused "
2122+
"bug is NOT fixed; path_c_fused_runtime_evidence records this "
2123+
f"degraded run: {type(error).__name__}: {error}",
20702124
RuntimeWarning,
20712125
stacklevel=2,
20722126
)
20732127
return None
20742128

20752129
# Unreachable in practice: both the timeout and inline branches return on
2076-
# success above. Fall back to eager defensively if we ever get here.
2077-
return None
2130+
# success above. If we somehow reach here, fail loud rather than silently
2131+
# degrade to eager (RULE #1).
2132+
raise RuntimeError(
2133+
"CompiledPretrainingStep._run_fused_value_and_grad_guarded reached an "
2134+
"unreachable state: the fused runtime neither returned a result nor "
2135+
"recorded an error. This is an internal control-flow bug."
2136+
)
20782137

20792138
def _eager_step(
20802139
self,
@@ -2091,14 +2150,15 @@ def _eager_step(
20912150
if outcome is not None:
20922151
(loss, ntokens), grads = outcome
20932152
else:
2094-
# SAFETY FALLBACK: the fused Path C direct-chain runtime either
2095-
# raised or exceeded its wall-clock budget (e.g. the MLX<->CUDA
2096-
# buffer handoff, incomplete fused gradient extraction, or a
2097-
# pathologically slow / runaway fused kernel). Rather than
2098-
# aborting the whole run with steps_completed=0 we degrade to the
2099-
# working eager value_and_grad path (the same ops dispatched
2100-
# through the TileLang-CUDA EAGER kernels / pure-MLX reference),
2101-
# guaranteeing we never end up below the eager baseline.
2153+
# OPT-IN BISECTION DEGRADE ONLY: this branch is reachable solely
2154+
# when MLX_PATH_C_FUSED_ALLOW_EAGER_DEGRADE is set. By default
2155+
# _run_fused_value_and_grad_guarded RAISES on a fused crash/timeout
2156+
# (RULE #1 — surface the bug) instead of returning None. When the
2157+
# operator has explicitly opted into the loud bisection degrade,
2158+
# the fused Path C direct-chain runtime raised or exceeded its
2159+
# wall-clock budget; we degrade to the eager value_and_grad path
2160+
# (already warned + recorded in path_c_fused_runtime_evidence) so a
2161+
# deliberate bisection run completes rather than aborting.
21022162
(loss, ntokens), grads = self._loss_and_grad(
21032163
self.model, loss_batch
21042164
)

0 commit comments

Comments
 (0)