Skip to content

Commit 1cf41c2

Browse files
committed
feat(v4-path-e): harden vendored mlx-lm Path E - fail-close guards, eligibility, provenance, bench
Path E (vendored mlx-lm GDN/KDA gated_delta + FP8 dequant) was runnable but silently traded correctness for speed. Hardened: - Amplifying-gate fail-close: the GDN adapter silently clamped (g_clamped=min(g,-1e-6)) on amplifying gates (g>0/decay>1, which upstream cannot represent) -> now raises PathEUnavailable so the dispatcher falls back to B/A. New _path_e_eligibility.py = single source of truth (gate-sign + shape). - Eligibility in status: GDN/KDA Path E status reflects Dk%32==0 & Dv%4==0 (+ gate sign) so auto_pick SKIPS E instead of dropping into the slow upstream-ops path. KDA adapter silent slow-path fallback removed (fails closed; KDA has no gate-sign constraint). - Relabel: lightning_indexer_fp8 no longer self-labels "path E" (pure-MLX + vendored dequant). - Provenance: VENDORED_MANIFEST.json pins PR #1217/#1224 + per-file sha256; check_vendored_drift wired into path_sanity_guard (vendored_snapshot_drift); test + reports/VENDORED_PROVENANCE.md. Metal verified: eligible->E, g>0->fail-close to A (output==A exactly), ineligible shape->skip, drift detected. Bench: fused Metal E-VJP 4.1-5.7x faster than Path B fused bwd -> recommend E-VJP as GDN bwd default when eligible. Tests 83 passed/2 skip on surface; tests/v4 3053 passed (18 pre-existing fails); v3 untouched.
1 parent 77c154c commit 1cf41c2

13 files changed

Lines changed: 832 additions & 44 deletions

cppmega_v4/_tilelang/kda_paths.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,13 +204,42 @@ def _path_e_status() -> PathStatus:
204204
path="path_e", available=True,
205205
reason=(
206206
"vendored mlx-lm gated_delta vectorised-gate Metal kernel "
207-
"(Dk%32==0 & Dv%4==0; smaller dims fall back to the pure-ops "
208-
"reference). The same kernel as GDN Path E — KDA hits the "
209-
"g.ndim==4 branch in gated_delta_kernel."
207+
"(fast kernel for Dk%32==0 & Dv%4==0; fails closed for smaller "
208+
"dims so the dispatcher falls back to Path B/A instead of the slow "
209+
"pure-ops reference). The same kernel as GDN Path E — KDA hits the "
210+
"g.ndim==4 branch in gated_delta_kernel. No gate-sign constraint "
211+
"(KDA passes g_decay=exp(g) directly)."
210212
),
211213
)
212214

213215

216+
def _path_e_status_for_inputs(*args, **kwargs) -> PathStatus:
217+
"""Input-aware KDA Path E status: importability + shape eligibility.
218+
219+
KDA has no gate-sign constraint, only the fast-kernel shape (Dk%32==0 &
220+
Dv%4==0). ``auto_pick`` consumes this so it SKIPS Path E for ineligible
221+
shapes rather than selecting the slow upstream ops fallback. Best-effort:
222+
unparseable inputs fall back to the static status.
223+
"""
224+
base = _path_e_status()
225+
if not base.available:
226+
return base
227+
# KDA signature: (q, k, v, g, beta, ...). q.shape[-1]=Dk, v.shape[-1]=Dv.
228+
try:
229+
from cppmega_v4.nn._external._path_e_eligibility import kda_eligibility
230+
231+
q = kwargs.get("q", args[0] if len(args) > 0 else None)
232+
v = kwargs.get("v", args[2] if len(args) > 2 else None)
233+
if q is None or v is None:
234+
return base
235+
elig = kda_eligibility(int(q.shape[-1]), int(v.shape[-1]))
236+
except Exception:
237+
return base
238+
if elig.eligible:
239+
return base
240+
return PathStatus(path="path_e", available=False, reason=elig.reason)
241+
242+
214243
def _path_e_call(*args, allow_fallback: bool = True, **kwargs):
215244
if not _path_e_status().available:
216245
return _fallback_or_raise(
@@ -272,7 +301,16 @@ def kda_recurrent_dispatch(
272301
status_overrides: Mapping[PathName, PathStatus] | None = None,
273302
**kwargs,
274303
):
275-
"""Call the auto-selected KDA backend, falling back to Path A."""
304+
"""Call the auto-selected KDA backend, falling back to Path A.
305+
306+
In auto-mode Path E availability is recomputed from the actual shape so
307+
``auto_pick`` SKIPS Path E for ineligible shapes (which would otherwise
308+
drop to the slow upstream ops fallback).
309+
"""
310+
effective_overrides = dict(status_overrides) if status_overrides else {}
311+
if path is None and "path_e" not in effective_overrides:
312+
effective_overrides["path_e"] = _path_e_status_for_inputs(*args, **kwargs)
313+
status_overrides = effective_overrides or None
276314
statuses = kda_path_statuses(status_overrides=status_overrides)
277315
if path is None:
278316
path = kda_auto_mode_for_inputs(status_overrides=status_overrides)

cppmega_v4/_tilelang/linear_attention_paths.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,9 @@ def _path_e_status() -> PathStatus:
219219
return PathStatus(
220220
path="path_e", available=True,
221221
reason=(
222-
"vendored mlx-lm PR #1217 gated_delta_update (Metal kernel "
223-
"for Dk%32==0 & Dv%4==0; ops fallback otherwise)"
222+
"vendored mlx-lm PR #1217 gated_delta_update (fast Metal kernel "
223+
"for Dk%32==0 & Dv%4==0 and gate g<=0; fails closed otherwise "
224+
"so the dispatcher falls back to Path B/A)"
224225
),
225226
)
226227
except Exception:
@@ -235,6 +236,38 @@ def _path_e_status() -> PathStatus:
235236
)
236237

237238

239+
def _path_e_status_for_inputs(*args, **kwargs) -> PathStatus:
240+
"""Input-aware Path E status: combine importability with eligibility.
241+
242+
Path E is only *truly* available for a concrete call when (a) the adapter
243+
imports AND (b) the gate is representable (g<=0; no amplifying gate) AND
244+
(c) the shape hits the fast Metal kernel. ``auto_pick`` consumes this so it
245+
SKIPS Path E (falls through to D/A) rather than selecting a path that would
246+
silently clamp the gate or drop to the slow upstream ops fallback.
247+
248+
Best-effort: if inputs cannot be parsed (e.g. unusual call shape) we fall
249+
back to the static status so behaviour is never worse than before.
250+
"""
251+
base = _path_e_status()
252+
if not base.available:
253+
return base
254+
# Expected signature: (q, k, v, beta, g, ...). Probe g + dims defensively.
255+
try:
256+
from cppmega_v4.nn._external._path_e_eligibility import gdn_eligibility
257+
258+
q = kwargs.get("q", args[0] if len(args) > 0 else None)
259+
v = kwargs.get("v", args[2] if len(args) > 2 else None)
260+
g = kwargs.get("g", args[4] if len(args) > 4 else None)
261+
if q is None or v is None or g is None:
262+
return base
263+
elig = gdn_eligibility(g, int(q.shape[-1]), int(v.shape[-1]))
264+
except Exception:
265+
return base
266+
if elig.eligible:
267+
return base
268+
return PathStatus(path="path_e", available=False, reason=elig.reason)
269+
270+
238271
def _path_e_call(*args, allow_fallback: bool = True, **kwargs):
239272
status = _path_e_status()
240273
if not status.available:
@@ -299,7 +332,16 @@ def gated_delta_recurrent_dispatch(
299332
"""Call the auto-selected GDN backend, falling back to Path A.
300333
301334
Same callable signature as ``naive_recurrent_gated_delta_rule``.
335+
336+
In auto-mode (``path is None``) Path E availability is recomputed from the
337+
*actual* inputs (gate sign + shape) so ``auto_pick`` SKIPS Path E for
338+
amplifying gates or ineligible shapes — it never selects a path that would
339+
silently clamp the gate or drop to the slow upstream ops fallback.
302340
"""
341+
effective_overrides = dict(status_overrides) if status_overrides else {}
342+
if path is None and "path_e" not in effective_overrides:
343+
effective_overrides["path_e"] = _path_e_status_for_inputs(*args, **kwargs)
344+
status_overrides = effective_overrides or None
303345
statuses = linear_attention_path_statuses(status_overrides=status_overrides)
304346
if path is None:
305347
path = linear_attention_auto_mode_for_inputs(status_overrides=status_overrides)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
{
2+
"_comment": [
3+
"Provenance pin for the mlx-lm upstream code vendored into cppmega_v4 Path E",
4+
"and the V4 mHC / MTP stack. Each entry records the upstream PR, the path it",
5+
"was copied from, whether it is a verbatim snapshot or a cppmega-authored",
6+
"derivative, and a sha256 of the vendored file as committed. The drift check",
7+
"(_vendored_provenance.py, exercised by tests/v4/test_vendored_provenance.py)",
8+
"recomputes these hashes and fails if a vendored file diverges from its",
9+
"recorded snapshot, so silent edits to a 'verbatim' copy cannot slip in.",
10+
"Hashes are over the file bytes exactly as stored in the repo."
11+
],
12+
"schema_version": 1,
13+
"upstream_repo": "https://github.com/ml-explore/mlx-lm",
14+
"upstream_license": "MIT (Copyright Apple Inc.)",
15+
"recorded_at": "2026-05-30",
16+
"entries": [
17+
{
18+
"file": "_mlx_lm_gated_delta_vendored.py",
19+
"pr": 1217,
20+
"pr_url": "https://github.com/ml-explore/mlx-lm/pull/1217",
21+
"upstream_path": "mlx_lm/models/gated_delta.py",
22+
"kind": "verbatim",
23+
"used_by": "GDN/KDA Path E (gated_delta_update, gated_delta_kernel, gated_delta_ops, compute_g)",
24+
"sha256": "18b3a77250b246d46e63d8fa9e8208936ea09bd46b97b036c2ec34edfc0a176b",
25+
"notes": "Snapshot includes the PR #1066 Kahan-compensated kv_mem accumulation + 4-way time-loop unroll; this is NOT identical to an older mlx-lm checkout that predates PR #1066."
26+
},
27+
{
28+
"file": "_mlx_lm_fp8_dequant_vendored.py",
29+
"pr": 1224,
30+
"pr_url": "https://github.com/ml-explore/mlx-lm/pull/1224",
31+
"upstream_path": "mlx_lm/models/qwen3_5_moe.py (Model.sanitize FP8 dequant block, generalized)",
32+
"kind": "derived",
33+
"used_by": "Lightning Indexer FP8 (pure-MLX, NOT Path E) + MoE FP8 loaders",
34+
"sha256": "3a48eec1797aee1a9c6d16e819064a045624a3eb62faf69a5670faf1b978f2a8",
35+
"notes": "Inline upstream snippet pulled into a standalone dequant_block_fp8 utility; functionally faithful but reorganised, hence 'derived'."
36+
},
37+
{
38+
"file": "_mlx_lm_gated_delta_vjp_metal_vendored.py",
39+
"pr": 1217,
40+
"pr_url": "https://github.com/ml-explore/mlx-lm/pull/1217",
41+
"upstream_path": "mlx_lm/models/gated_delta.py (kernel reused; VJP is cppmega-authored)",
42+
"kind": "derived",
43+
"used_by": "GDN Path E training backward (fused Metal VJP)",
44+
"sha256": "dfe959d0ed50d60d75d93532a80b0b6f769aad74ce39bc00a0770128c9903f4e",
45+
"notes": "cppmega-authored fused Metal VJP built on the vendored PR #1217 gated_delta_kernel; upstream has no registered VJP."
46+
},
47+
{
48+
"file": "_mlx_lm_gated_delta_vjp_vendored.py",
49+
"pr": 1217,
50+
"pr_url": "https://github.com/ml-explore/mlx-lm/pull/1217",
51+
"upstream_path": "mlx_lm/models/gated_delta.py (Python-ops reference; VJP is cppmega-authored)",
52+
"kind": "derived",
53+
"used_by": "GDN Path E training backward (gradient-checkpointed Python reference)",
54+
"sha256": "1a5040eb617070f6fbc0712af88cd35104d83a6917a0c5fdfb518dc3a1436b10",
55+
"notes": "cppmega-authored chunked Python VJP wrapping the vendored PR #1217 ops; bounds T>=2048 memory."
56+
},
57+
{
58+
"file": "_mlx_lm_hyper_connection_vendored.py",
59+
"pr": 1189,
60+
"pr_url": "https://github.com/ml-explore/mlx-lm/pull/1189",
61+
"upstream_path": "mlx_lm/models/hyper_connection.py",
62+
"kind": "verbatim",
63+
"used_by": "V4 mHC residual (HyperConnection, HyperHead)",
64+
"sha256": "6febc66a50e6504fad2f60b2af51e3eb01a9fb83713b4189d39cf9c499b7b9c7",
65+
"notes": "Recorded for completeness; not part of Path E."
66+
},
67+
{
68+
"file": "_mlx_lm_sinkhorn_vendored.py",
69+
"pr": 1189,
70+
"pr_url": "https://github.com/ml-explore/mlx-lm/pull/1189",
71+
"upstream_path": "mlx_lm/models/sinkhorn.py",
72+
"kind": "verbatim",
73+
"used_by": "V4 mHC Sinkhorn projection (hc_split_sinkhorn)",
74+
"sha256": "af2dee5ef7db71c9bc254bae8000ade7e92fd8e5a8367f3bd3bf4b713b1a587f",
75+
"notes": "Recorded for completeness; not part of Path E."
76+
},
77+
{
78+
"file": "_mlx_lm_mtp_module_vendored.py",
79+
"pr": 990,
80+
"pr_url": "https://github.com/ml-explore/mlx-lm/pull/990",
81+
"upstream_path": "mlx_lm/models/qwen3_5.py (MTPModule excerpt)",
82+
"kind": "derived",
83+
"used_by": "V4 MTP speculative-decode module",
84+
"sha256": "3131e547b15a43b2538ccfb1c14a275f91f51739008dca4770a28c7fb414444c",
85+
"notes": "Excerpt only (MTPModule + decoder layer); the PR's generate/server drivers are out of scope."
86+
}
87+
]
88+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Path E eligibility checks — single source of truth.
2+
3+
Path E wraps the vendored mlx-lm ``gated_delta`` Metal kernel (PR #1217). It
4+
has two HARD limits that, if ignored, silently trade correctness or speed:
5+
6+
1. GATE SIGN (correctness).
7+
Upstream parameterises the per-step decay as
8+
``g_decay = exp(-exp(A_log) * softplus(a + dt_bias))`` which is bounded by
9+
``1`` for all real inputs. The GDN adapter recovers our FLA log-decay ``g``
10+
via ``a = softplus_inverse(-g)``, which only exists for ``g <= 0`` (decay
11+
``<= 1``). A model whose gate ``g > 0`` (an *amplifying* gate, decay > 1)
12+
CANNOT be represented; the historic adapter silently clamped ``g`` to ``0``
13+
and lost information. We instead FAIL-CLOSE: Path E reports itself
14+
unavailable / raises so the dispatcher falls back to Path B/A, which can
15+
represent amplifying gates exactly.
16+
17+
KDA passes ``g_decay = exp(g)`` straight into the kernel, so KDA itself has
18+
no upper bound on the gate — but the kernel multiplies state by a *positive*
19+
decay each step, and a NEGATIVE ``g_decay`` (which never arises from
20+
``exp``) would be ill-defined. KDA's amplifying gates (``g > 0``) are
21+
therefore representable; only the GDN parameterisation is bounded.
22+
23+
2. SHAPE (silent slow path).
24+
The fast Metal kernel only runs when ``Dk % 32 == 0`` and ``Dv % 4 == 0``.
25+
For other dims the upstream falls back to a pure-MLX ops reference, which is
26+
correct but SLOW. ``auto_pick`` must not select Path E for such shapes —
27+
otherwise it picks a "kernel" that is really the slow reference. We record
28+
the eligibility in the Path E status so ``auto_pick`` skips it.
29+
30+
These helpers are imported by the GDN/KDA adapters (to fail-close at call
31+
time) and by the dispatch ``_path_e_status`` functions (to skip E in
32+
auto-mode). Keeping them here means the rule is defined once.
33+
"""
34+
35+
from __future__ import annotations
36+
37+
from dataclasses import dataclass
38+
39+
import mlx.core as mx
40+
41+
# Fast Metal kernel constraints (mlx-lm gated_delta.py ``_make_gated_delta_kernel``).
42+
KERNEL_DK_MULTIPLE = 32
43+
KERNEL_DV_MULTIPLE = 4
44+
45+
46+
@dataclass(frozen=True)
47+
class PathEEligibility:
48+
"""Result of a Path E eligibility probe for a concrete call.
49+
50+
``eligible`` is True only when Path E can run on the fast Metal kernel
51+
AND represents the requested gate exactly. ``reason`` explains a False.
52+
"""
53+
54+
eligible: bool
55+
reason: str
56+
fast_kernel: bool # shape qualifies for the fast Metal kernel
57+
gate_representable: bool # gate sign is representable by this path
58+
59+
60+
def shape_uses_fast_kernel(dk: int, dv: int) -> bool:
61+
"""True iff (Dk, Dv) hit the fast vendored Metal kernel (not slow ops)."""
62+
return (dk % KERNEL_DK_MULTIPLE == 0) and (dv % KERNEL_DV_MULTIPLE == 0)
63+
64+
65+
def _gate_has_amplifying(g: mx.array) -> bool:
66+
"""True iff any element of the log-decay ``g`` is > 0 (decay > 1)."""
67+
# tolerance: treat tiny positive jitter as non-amplifying so that
68+
# round-trip noise on a g==0 gate does not spuriously fail-close.
69+
return bool(mx.any(g.astype(mx.float32) > 1e-6).item())
70+
71+
72+
def gdn_eligibility(g: mx.array, dk: int, dv: int) -> PathEEligibility:
73+
"""Eligibility for the GDN Path E adapter (bounded, decay <= 1).
74+
75+
Fails closed when the gate is amplifying (``g > 0``) — the upstream
76+
parameterisation cannot represent it — or when the shape forces the slow
77+
ops fallback.
78+
"""
79+
fast = shape_uses_fast_kernel(dk, dv)
80+
amplifying = _gate_has_amplifying(g)
81+
gate_ok = not amplifying
82+
if amplifying:
83+
reason = (
84+
"GDN Path E cannot represent an amplifying gate: g>0 (decay>1) is "
85+
"outside upstream g_decay=exp(-exp(A_log)*softplus(...)) which is "
86+
"bounded by 1; falling back so correctness is not silently clamped"
87+
)
88+
elif not fast:
89+
reason = (
90+
f"GDN Path E shape ineligible: Dk={dk} (need %{KERNEL_DK_MULTIPLE}==0) "
91+
f"or Dv={dv} (need %{KERNEL_DV_MULTIPLE}==0) would force the slow "
92+
"upstream ops fallback, not the fast Metal kernel"
93+
)
94+
else:
95+
reason = "GDN Path E eligible: gate g<=0 and fast Metal kernel shape"
96+
return PathEEligibility(
97+
eligible=gate_ok and fast,
98+
reason=reason,
99+
fast_kernel=fast,
100+
gate_representable=gate_ok,
101+
)
102+
103+
104+
def kda_eligibility(dk: int, dv: int) -> PathEEligibility:
105+
"""Eligibility for the KDA Path E adapter.
106+
107+
KDA passes ``g_decay = exp(g)`` directly, so any finite gate is
108+
representable (decay is always positive). The only hard constraint is the
109+
fast-kernel shape; ineligible shapes force the slow ops fallback.
110+
"""
111+
fast = shape_uses_fast_kernel(dk, dv)
112+
if not fast:
113+
reason = (
114+
f"KDA Path E shape ineligible: Dk={dk} (need %{KERNEL_DK_MULTIPLE}==0) "
115+
f"or Dv={dv} (need %{KERNEL_DV_MULTIPLE}==0) would force the slow "
116+
"upstream ops fallback, not the fast Metal kernel"
117+
)
118+
else:
119+
reason = "KDA Path E eligible: fast Metal kernel shape (gate always representable)"
120+
return PathEEligibility(
121+
eligible=fast,
122+
reason=reason,
123+
fast_kernel=fast,
124+
gate_representable=True,
125+
)
126+
127+
128+
class PathEUnavailable(RuntimeError):
129+
"""Raised by a Path E adapter when it cannot run correctly/fast.
130+
131+
The dispatcher catches this and falls back to Path B/A. Subclassing
132+
``RuntimeError`` keeps it compatible with the existing
133+
``_dispatch_failure_or_fallback`` handlers.
134+
"""
135+
136+
137+
__all__ = [
138+
"KERNEL_DK_MULTIPLE",
139+
"KERNEL_DV_MULTIPLE",
140+
"PathEEligibility",
141+
"PathEUnavailable",
142+
"gdn_eligibility",
143+
"kda_eligibility",
144+
"shape_uses_fast_kernel",
145+
]

0 commit comments

Comments
 (0)