@@ -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