Skip to content

Commit dfb4d03

Browse files
committed
fix(path-c): hard-gate fp8 Path C off at long seq (OOM) + prefer non-deprecated mx.set_memory_limit
FIX 1 (OOM): at seq>=2048 fp8 Path C peak explodes to 159-171 GB (~5x bf16 Path C's 43-54 GB) and is slower (reports/cppmega_1b_path_matrix_20260526_checkpoint_replay_60g) due to row-phased replay arenas + fp32-mandatory scale/lse/state banks. Add an AUTO-only long-seq gate (CPPMEGA_FP8_PATH_C_MAX_SEQ_LEN, default 2048, 0=disable): an AUTO fp8 Path C request at seq>=threshold fails closed to the fp8 Path B baseline (same bf16 carrier dtype, no re-quant), recorded as fp8_path_c_auto_long_seq_gated_to_path_b. Explicit --use-path-c-*-runtime bypasses the gate; bf16 Path C and Path B untouched. FIX 2: cppmega_mlx/runtime/memory.py preferred the deprecated mx.metal.set_memory_limit, which failed all Path B cells in the seq2048 matrix. Prefer mx.set_memory_limit, fall back to mx.metal.set_memory_limit only for old MLX. Verified no deprecation error (MLX 0.32.dev). Metal: dry-trace confirms seq512 selectable / seq>=2048 gated-to-B / override bypass / bf16+PathB untouched / threshold configurable. Tests 153 passed +10 gate tests; memory 68 passed; pre-existing failures confirmed on baseline.
1 parent 1cf41c2 commit dfb4d03

4 files changed

Lines changed: 359 additions & 23 deletions

File tree

cppmega_mlx/runtime/memory.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,14 @@ def memory_limit_api_status(mx_module: Any | None = None) -> MemoryLimitApiStatu
273273
root_memory_limit_available = callable(getattr(mx, "set_memory_limit", None))
274274
metal = getattr(mx, "metal", None)
275275
metal_memory_limit_available = callable(getattr(metal, "set_memory_limit", None))
276+
# List the non-deprecated root setter first so ``preferred_*`` reflects what
277+
# ``apply_memory_limit_plan`` actually calls. ``mx.metal.set_memory_limit`` is
278+
# only retained as a fallback for older MLX without ``mx.set_memory_limit``.
276279
supported_paths: list[str] = []
277-
if metal_memory_limit_available:
278-
supported_paths.append("mx.metal.set_memory_limit")
279280
if root_memory_limit_available:
280281
supported_paths.append("mx.set_memory_limit")
282+
if metal_memory_limit_available:
283+
supported_paths.append("mx.metal.set_memory_limit")
281284
return MemoryLimitApiStatus(
282285
wired_limit_available=callable(getattr(mx, "set_wired_limit", None)),
283286
root_memory_limit_available=root_memory_limit_available,
@@ -362,12 +365,16 @@ def apply_memory_limit_plan(
362365
metal_set_memory_limit = getattr(metal, "set_memory_limit", None)
363366
if not callable(set_wired_limit):
364367
raise RuntimeError("mlx.core.set_wired_limit is unavailable")
365-
if callable(metal_set_memory_limit):
366-
set_memory_limit = metal_set_memory_limit
367-
memory_limit_api_path = "mx.metal.set_memory_limit"
368-
elif callable(root_set_memory_limit):
368+
# Prefer the non-deprecated root setter. ``mx.metal.set_memory_limit`` is
369+
# deprecated in current MLX releases and emits a DeprecationWarning that, under
370+
# ``-W error``, fails every cell that applies a memory limit. Fall back to the
371+
# metal namespace only on older MLX that lacks ``mx.set_memory_limit``.
372+
if callable(root_set_memory_limit):
369373
set_memory_limit = root_set_memory_limit
370374
memory_limit_api_path = "mx.set_memory_limit"
375+
elif callable(metal_set_memory_limit):
376+
set_memory_limit = metal_set_memory_limit
377+
memory_limit_api_path = "mx.metal.set_memory_limit"
371378
else:
372379
raise RuntimeError("no supported MLX memory limit setter is available")
373380

scripts/m04_train_step.py

Lines changed: 173 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,23 @@
289289
SPARSE_MLA_FP8_ROUTE_ENV = "CPPMEGA_SPARSE_MLA_FP8_ROUTE"
290290
SPARSE_MLA_FP8_BWD_ENV = "CPPMEGA_SPARSE_MLA_FP8_BWD"
291291
MAMBA3_PATH_C_BWD_ENV = "CPPMEGA_MAMBA3_PATH_C_BWD"
292+
# Long-sequence safety gate for the FP8 Path C AUTO route. At seq>=2048 the
293+
# row-phased replay/recompute arenas plus the fp32-mandatory q_scale/kv_scale/
294+
# lse/state banks make FP8 Path C peak memory explode to ~159-171 GB (~5x the
295+
# bf16 Path C peak) and run slower than FP8 Path B. The AUTO route therefore
296+
# fails closed to the FP8 Path B reference baseline at long sequence lengths.
297+
# Override the threshold with CPPMEGA_FP8_PATH_C_MAX_SEQ_LEN (set 0 to disable
298+
# the gate entirely). The gate is AUTO-only: an explicit Path C runtime opt-in
299+
# (--use-path-c-direct-chain-runtime / --use-path-c-fused-train-block-runtime)
300+
# always bypasses it.
301+
FP8_PATH_C_MAX_SEQ_LEN_ENV = "CPPMEGA_FP8_PATH_C_MAX_SEQ_LEN"
302+
# Set by run_receipt() from the --use-path-c-*-runtime args so that downstream
303+
# gate evaluations driven by the (flag-less) TrainHybridTinyConfig still observe
304+
# the explicit operator override and bypass the AUTO long-seq gate.
305+
FP8_PATH_C_EXPLICIT_OVERRIDE_ENV = "CPPMEGA_FP8_PATH_C_EXPLICIT_RUNTIME_OVERRIDE"
306+
FP8_PATH_C_LONG_SEQ_GATE_DEFAULT = 2048
307+
FP8_PATH_C_LONG_SEQ_GATE_FALLBACK_DTYPE = FP8_PATH_B_DTYPE
308+
FP8_PATH_C_LONG_SEQ_GATE_STATUS = "fp8_path_c_auto_long_seq_gated_to_path_b"
292309
FP8_PATH_C_RUNTIME_ENV: dict[str, str] = {
293310
SPARSE_MLA_FP8_ROUTE_ENV: "path_c",
294311
SPARSE_MLA_FP8_BWD_ENV: "path_c",
@@ -742,16 +759,147 @@ def optimizer_variant_payload(args: argparse.Namespace) -> dict[str, Any]:
742759
}
743760

744761

745-
def fp8_path_c_route_requested(
762+
def fp8_path_c_dtype_requested(
746763
args: argparse.Namespace | TrainHybridTinyConfig,
747764
) -> bool:
765+
"""Return whether the CLI/config dtype names the FP8 Path C route.
766+
767+
This is the raw dtype check before the long-sequence AUTO safety gate. Use
768+
:func:`fp8_path_c_route_requested` for the effective decision that honors the
769+
gate.
770+
"""
771+
748772
return str(getattr(args, "dtype", "")).strip().lower() == FP8_PATH_C_DTYPE
749773

750774

775+
def fp8_path_c_explicit_runtime_override(
776+
args: argparse.Namespace | TrainHybridTinyConfig,
777+
) -> bool:
778+
"""Return whether an explicit Path C runtime opt-in is present.
779+
780+
The two ``--use-path-c-*-runtime`` flags install the Path C value_and_grad
781+
runtime on the actual training critical path. They are explicit operator
782+
overrides, so they bypass the AUTO long-sequence safety gate.
783+
784+
The flags live only on the argparse namespace; ``run_receipt`` mirrors them
785+
into :data:`FP8_PATH_C_EXPLICIT_OVERRIDE_ENV` so the same decision holds when
786+
the gate is evaluated against the flag-less ``TrainHybridTinyConfig``.
787+
"""
788+
789+
if (
790+
getattr(args, "use_path_c_direct_chain_runtime", False)
791+
or getattr(args, "use_path_c_fused_train_block_runtime", False)
792+
):
793+
return True
794+
return os.environ.get(FP8_PATH_C_EXPLICIT_OVERRIDE_ENV, "").strip().lower() in {
795+
"1",
796+
"true",
797+
"yes",
798+
"on",
799+
}
800+
801+
802+
def fp8_path_c_max_seq_len() -> int:
803+
"""Return the FP8 Path C AUTO long-sequence gate threshold.
804+
805+
Defaults to :data:`FP8_PATH_C_LONG_SEQ_GATE_DEFAULT` (2048). Override with the
806+
``CPPMEGA_FP8_PATH_C_MAX_SEQ_LEN`` env var. A value of ``0`` (or any
807+
non-positive integer) disables the gate.
808+
"""
809+
810+
raw = os.environ.get(FP8_PATH_C_MAX_SEQ_LEN_ENV)
811+
if raw is None or raw.strip() == "":
812+
return FP8_PATH_C_LONG_SEQ_GATE_DEFAULT
813+
try:
814+
value = int(raw)
815+
except ValueError as exc:
816+
raise ValueError(
817+
f"{FP8_PATH_C_MAX_SEQ_LEN_ENV} must be an integer sequence length"
818+
) from exc
819+
return value
820+
821+
822+
def fp8_path_c_long_seq_gate(
823+
args: argparse.Namespace | TrainHybridTinyConfig,
824+
) -> dict[str, Any]:
825+
"""Evaluate the FP8 Path C AUTO long-sequence safety gate.
826+
827+
The gate only applies to the AUTO FP8 Path C dtype route. It fails closed to
828+
the FP8 Path B reference baseline when the configured sequence length reaches
829+
the threshold, because FP8 Path C peak memory explodes (~5x bf16 Path C) and
830+
runs slower than Path B at long sequence lengths. An explicit Path C runtime
831+
opt-in bypasses the gate, and a non-positive threshold disables it.
832+
833+
Returns a receipt-shaped payload. ``gated`` is True only when the route is
834+
actively being refused.
835+
"""
836+
837+
dtype_requested = fp8_path_c_dtype_requested(args)
838+
explicit_override = fp8_path_c_explicit_runtime_override(args)
839+
threshold = fp8_path_c_max_seq_len()
840+
seq_len = int(getattr(args, "seq_len", 0) or 0)
841+
gate_enabled = threshold > 0
842+
gated = (
843+
dtype_requested
844+
and gate_enabled
845+
and not explicit_override
846+
and seq_len >= threshold
847+
)
848+
reason: str | None = None
849+
if gated:
850+
reason = (
851+
f"FP8 Path C AUTO route refused: seq_len={seq_len} >= "
852+
f"{FP8_PATH_C_MAX_SEQ_LEN_ENV}={threshold}. FP8 Path C peak memory "
853+
f"explodes (~5x bf16 Path C, 159-171 GB observed at seq=2048) and is "
854+
f"slower than FP8 Path B at long sequence lengths; falling back to the "
855+
f"FP8 Path B reference baseline. Pass an explicit Path C runtime "
856+
f"opt-in (--use-path-c-direct-chain-runtime / "
857+
f"--use-path-c-fused-train-block-runtime) or set "
858+
f"{FP8_PATH_C_MAX_SEQ_LEN_ENV}=0 to override."
859+
)
860+
return {
861+
"gated": gated,
862+
"dtype_requested": dtype_requested,
863+
"explicit_override": explicit_override,
864+
"gate_enabled": gate_enabled,
865+
"threshold": threshold,
866+
"seq_len": seq_len,
867+
"fallback_dtype": FP8_PATH_C_LONG_SEQ_GATE_FALLBACK_DTYPE if gated else None,
868+
"status": FP8_PATH_C_LONG_SEQ_GATE_STATUS if gated else "not_gated",
869+
"env": FP8_PATH_C_MAX_SEQ_LEN_ENV,
870+
"reason": reason,
871+
}
872+
873+
874+
def fp8_path_c_route_requested(
875+
args: argparse.Namespace | TrainHybridTinyConfig,
876+
) -> bool:
877+
"""Return the effective FP8 Path C route decision (AUTO gate applied).
878+
879+
Equals the raw dtype request unless the AUTO long-sequence safety gate fires,
880+
in which case the route fails closed to the FP8 Path B baseline.
881+
"""
882+
883+
if not fp8_path_c_dtype_requested(args):
884+
return False
885+
return not fp8_path_c_long_seq_gate(args)["gated"]
886+
887+
888+
def fp8_path_c_route_gated_to_path_b(
889+
args: argparse.Namespace | TrainHybridTinyConfig,
890+
) -> bool:
891+
"""Return whether an FP8 Path C request was demoted to Path B by the gate."""
892+
893+
return fp8_path_c_dtype_requested(args) and fp8_path_c_long_seq_gate(args)["gated"]
894+
895+
751896
def fp8_path_b_route_requested(
752897
args: argparse.Namespace | TrainHybridTinyConfig,
753898
) -> bool:
754-
return str(getattr(args, "dtype", "")).strip().lower() == FP8_PATH_B_DTYPE
899+
if str(getattr(args, "dtype", "")).strip().lower() == FP8_PATH_B_DTYPE:
900+
return True
901+
# A long-seq-gated FP8 Path C request fails closed onto the Path B baseline.
902+
return fp8_path_c_route_gated_to_path_b(args)
755903

756904

757905
def fp8_training_route_requested(
@@ -1232,7 +1380,8 @@ def precision_route_payload(
12321380
args: argparse.Namespace | TrainHybridTinyConfig,
12331381
) -> dict[str, Any]:
12341382
if fp8_path_b_route_requested(args):
1235-
return {
1383+
long_seq_gate = fp8_path_c_long_seq_gate(args)
1384+
payload = {
12361385
"requested": FP8_PATH_B_DTYPE,
12371386
"kind": "fp8_path_b_reference_baseline",
12381387
"status": FP8_PATH_B_E2E_TRAINING_STATUS,
@@ -1246,6 +1395,14 @@ def precision_route_payload(
12461395
"hidden_wrapper_quantization_allowed": False,
12471396
"kernel_boundary_quantization_allowed": False,
12481397
}
1398+
if long_seq_gate["gated"]:
1399+
# The user asked for FP8 Path C but the AUTO long-seq gate demoted the
1400+
# route to the Path B reference baseline; record why for the receipt.
1401+
payload["requested"] = FP8_PATH_C_DTYPE
1402+
payload["kind"] = "fp8_path_c_auto_long_seq_gated_to_path_b"
1403+
payload["status"] = long_seq_gate["status"]
1404+
payload["fp8_path_c_long_seq_gate"] = long_seq_gate
1405+
return payload
12491406
if fp8_path_c_route_requested(args):
12501407
producer = sparse_mla_fp8_producer_payload(args)
12511408
producer_configured = bool(producer["configured"])
@@ -10861,6 +11018,13 @@ def run_receipt(
1086111018
local_gb10_route_fn: Callable[..., tuple[dict[str, Any], int]] | None = None,
1086211019
allocation_probe_fn: Callable[[], dict[str, Any]] | None = None,
1086311020
) -> tuple[dict[str, Any], int]:
11021+
# Mirror the explicit Path C runtime opt-in onto the environment so the AUTO
11022+
# long-seq gate makes the same decision whether it is evaluated against the
11023+
# argparse namespace or the flag-less TrainHybridTinyConfig.
11024+
if fp8_path_c_explicit_runtime_override(args):
11025+
os.environ[FP8_PATH_C_EXPLICIT_OVERRIDE_ENV] = "1"
11026+
else:
11027+
os.environ.pop(FP8_PATH_C_EXPLICIT_OVERRIDE_ENV, None)
1086411028
if args.steps < 1:
1086511029
return (
1086611030
blocked_receipt(
@@ -10989,9 +11153,9 @@ def _run_existing_training(
1098911153
)
1099011154
return enforce_loss_decrease_requirement(args, receipt)
1099111155
with (
10992-
fp8_path_b_kernel_policy(config),
10993-
fp8_path_c_kernel_policy(config),
10994-
fp8_path_c_stdio_suppressed(config),
11156+
fp8_path_b_kernel_policy(args),
11157+
fp8_path_c_kernel_policy(args),
11158+
fp8_path_c_stdio_suppressed(args),
1099511159
):
1099611160
return local_gb10_route_fn(
1099711161
args,
@@ -11013,9 +11177,9 @@ def _run_existing_training(
1101311177
memory_before = metal_memory_payload()
1101411178
try:
1101511179
with (
11016-
fp8_path_b_kernel_policy(config),
11017-
fp8_path_c_kernel_policy(config),
11018-
fp8_path_c_stdio_suppressed(config),
11180+
fp8_path_b_kernel_policy(args),
11181+
fp8_path_c_kernel_policy(args),
11182+
fp8_path_c_stdio_suppressed(args),
1101911183
):
1102011184
if args.dry_run_json:
1102111185
train_payload = dry_run_payload_fn(

0 commit comments

Comments
 (0)