Skip to content

Commit be7299d

Browse files
committed
feat(v4-path-c): wire indexer wq_b block-FP8 GEMM through fused_fp8_gemm + explicit dispatch (#14)
Scaffold #14: the lightning-indexer score contraction (einsum bqhd,bkd,bqh->bqk) is NOT a fused_fp8_gemm-shaped op -- both q and k are dynamic bf16 activations, no static FP8 weight; routing it through fused_fp8_gemm would require fabricating a fake static weight (RULE #1 forbids). So the score einsum stays pure-MLX BY DESIGN (now documented). The genuine block-FP8 weight x activation GEMM in the indexer -- the q projection qr @ wq_b.T (wq_b [128,64] static uint8 e4m3 + [1,1] block scale) -- IS wired through fused_fp8_gemm. Replaced the forbidden `try: fused_fp8_gemm except Exception: dequant_block_fp8` silent fallback with explicit shape dispatch (_wq_b_fp8_gemm_supported): malformed FP8 weight RAISES TypeError/ValueError before any fused call; only the legitimate q_lora_rank==0 degenerate uses the explicit dequant path. M4: fused_fp8_gemm_status available (cooperative-tensor prim live); top-k overlap 0.984 vs fp32 ref; cooperative-vs-scalar q-proj 9.1e-4 (fp8 tol). 16 indexer tests passed. No silent fallback.
1 parent 90f608c commit be7299d

1 file changed

Lines changed: 99 additions & 19 deletions

File tree

cppmega_v4/nn/lightning_indexer_fp8.py

Lines changed: 99 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,32 @@
1313
The K side (wk) and weights_proj stay bfloat16 — they're tiny (head_dim ~32
1414
and n_heads ~32) so the FP8 overhead is not worth it.
1515
16+
Fused FP8 GEMM wiring (Path C):
17+
The indexer's ``q`` projection ``qr @ wq_b.T`` — the inner block-FP8
18+
weight × activation GEMM that feeds the score einsum — is routed through
19+
the TileLang cooperative-tensor ``fused_fp8_gemm`` prim (the keystone
20+
block-FP8 GEMM) via :meth:`LightningIndexerFP8._wq_b_apply`, under an
21+
*explicit* shape-dispatch (RULE #1: no silent dequant fallback hiding a
22+
broken fused path — a malformed FP8 weight RAISES). ``fused_fp8_gemm``
23+
internally picks its cooperative-tensor prim or a numerically e4m3-exact
24+
scalar MSL kernel; both agree to FP8 tolerance.
25+
26+
The score contraction itself (``einsum bqhd,bkd,bqh->bqk``) is NOT a
27+
``fused_fp8_gemm``-shaped op: both ``q`` and ``k`` are *dynamic bf16
28+
activations* (there is no static block-FP8 weight operand), and the
29+
contraction is a per-head-weighted, head-summed attention score, not a
30+
plain ``A @ W.T``. It therefore stays pure-MLX by design — wiring it
31+
through ``fused_fp8_gemm`` would require fabricating a fake static FP8
32+
weight, which RULE #1 forbids.
33+
1634
Provenance / labelling note:
1735
The only vendored upstream piece here is ``dequant_block_fp8`` (mlx-lm
18-
PR #1224); everything else is plain MLX ops. There is NO fused Path-E
19-
Metal indexer kernel — do not label this module "Path E". The GDN/KDA
20-
Path E (``mlx_lm_gated_delta_update`` / ``mlx_lm_kda_update``) is a
21-
separate, genuinely fused vendored Metal kernel. A future Metal/TileLang
22-
fused indexer GEMM could replace the inner matmul here without touching
23-
this wrapper's external API; until then this is a pure-MLX path.
36+
PR #1224). There is NO fused Path-E Metal indexer kernel — do not label
37+
this module "Path E". The GDN/KDA Path E
38+
(``mlx_lm_gated_delta_update`` / ``mlx_lm_kda_update``) is a separate,
39+
genuinely fused vendored Metal kernel. The ``wq_b`` projection here is a
40+
real fused TileLang block-FP8 GEMM (Path C), but the wrapper as a whole is
41+
not "Path E".
2442
"""
2543

2644
from dataclasses import dataclass
@@ -95,31 +113,93 @@ def __init__(self, config: LightningIndexerFP8Config):
95113
self.weights_proj = nn.Linear(config.hidden_size, config.n_heads, bias=False)
96114

97115
def _wq_b_apply(self, qr: mx.array) -> mx.array:
98-
"""Apply wq_b: qr @ wq_b.T with dequant-on-the-fly.
99-
100-
For the fp8-blocks path this reuses the fused cooperative-tensor
101-
``fused_fp8_gemm`` (block-FP8 weight × activation -> ``qr @ W.T``)
102-
when available, falling back to a pure-MLX ``dequant_block_fp8`` +
103-
matmul otherwise. ``fused_fp8_gemm`` is itself fail-safe (it drops to
104-
its own scalar kernel if TileLang is unavailable), so the indexer keeps
105-
working on every host.
116+
"""Apply the FP8 score-path projection ``qr @ wq_b.T``.
117+
118+
This is the indexer's one genuine block-FP8 weight × activation GEMM
119+
(the ``q`` projection that feeds the score einsum downstream): ``wq_b``
120+
is stored as a static ``[out_dim, q_lora_rank]`` uint8 e4m3 weight with
121+
a ``[ceil(out_dim/128), ceil(q_lora_rank/128)]`` per-block ``scale_inv``,
122+
which is exactly the ``fused_fp8_gemm`` contract (``out = a @ W.T`` with
123+
a precomputed block-FP8 weight).
124+
125+
RULE #1 — explicit dispatch, NO silent fallback:
126+
* fp8-blocks path: the shape is validated against the
127+
``fused_fp8_gemm`` block-FP8 contract and the fused
128+
cooperative-tensor kernel is called *directly*. If the kernel
129+
raises it propagates — we do NOT swallow it into a dequant matmul
130+
that would hide a broken fused path. ``fused_fp8_gemm`` itself
131+
chooses its TileLang cooperative-tensor prim vs scalar MSL kernel
132+
(both numerically e4m3-exact); that is an internal, numerically
133+
equivalent choice, not a degraded fallback.
134+
* a shape that does NOT satisfy the block-FP8 contract is handled by
135+
an *explicit*, shape-correct ``dequant_block_fp8`` + matmul path
136+
with a stated reason — reached only when the contract check says so,
137+
never as an exception-swallowing escape hatch.
138+
* non-fp8 (bf16) path: plain matmul, no fp8 involved.
106139
"""
107140
if self.config.fp8_blocks:
108-
try:
109-
from cppmega_v4._tilelang.fused_fp8_gemm import fused_fp8_gemm
141+
from cppmega_v4._tilelang.fused_fp8_gemm import fused_fp8_gemm
110142

143+
if self._wq_b_fp8_gemm_supported(qr):
111144
# W = wq_b [out_dim, q_lora_rank]; fused_fp8_gemm computes
112-
# qr @ W.T = [B, T, out_dim].
145+
# qr @ W.T = [B, T, out_dim]. Errors here propagate (RULE #1).
113146
return fused_fp8_gemm(
114147
self._wq_b_fp8, self._wq_b_scale_inv, qr
115148
).astype(mx.bfloat16)
116-
except Exception: # noqa: BLE001 -- never break the indexer forward
117-
w = dequant_block_fp8(self._wq_b_fp8, self._wq_b_scale_inv)
149+
# Explicit (not silent) shape-correct path: the weight does not
150+
# satisfy the block-FP8 GEMM contract, so dequant the e4m3 weight
151+
# and matmul in bf16. The reason is reported by
152+
# ``_wq_b_fp8_gemm_supported`` raising on a genuinely broken shape.
153+
w = dequant_block_fp8(self._wq_b_fp8, self._wq_b_scale_inv)
118154
else:
119155
w = self._wq_b_bf16
120156
# qr [B, T, q_lora_rank] @ w.T [q_lora_rank, out_dim]
121157
return qr.astype(mx.bfloat16) @ w.T
122158

159+
def _wq_b_fp8_gemm_supported(self, qr: mx.array) -> bool:
160+
"""Explicit dispatch predicate for the ``fused_fp8_gemm`` score GEMM.
161+
162+
Returns ``True`` when the stored ``wq_b`` weight + ``qr`` activation
163+
satisfy the ``fused_fp8_gemm`` block-FP8 contract (uint8 2D weight,
164+
matching contraction dim, correctly-shaped per-128-block ``scale_inv``).
165+
166+
RULE #1: a malformed FP8 weight (wrong dtype / rank / scale-block shape)
167+
is a real bug, so this RAISES rather than silently steering to a
168+
dequant fallback. ``False`` is returned only for the one legitimate
169+
non-fused case — an empty contraction (``q_lora_rank == 0``) where the
170+
GEMM is degenerate — which the explicit dequant path then handles.
171+
"""
172+
w = self._wq_b_fp8
173+
s = self._wq_b_scale_inv
174+
if w.dtype != mx.uint8:
175+
raise TypeError(
176+
f"_wq_b_apply: wq_b fp8 weight must be uint8 e4m3 storage; "
177+
f"got {w.dtype}"
178+
)
179+
if w.ndim != 2:
180+
raise ValueError(
181+
f"_wq_b_apply: wq_b fp8 weight must be 2D [out_dim, q_lora_rank]; "
182+
f"got shape {w.shape}"
183+
)
184+
out_dim, k = w.shape
185+
if qr.shape[-1] != k:
186+
raise ValueError(
187+
f"_wq_b_apply: qr last dim ({qr.shape[-1]}) must match wq_b "
188+
f"contraction dim ({k})"
189+
)
190+
blocks_m = (out_dim + _FP8_BLOCK - 1) // _FP8_BLOCK
191+
blocks_k = (k + _FP8_BLOCK - 1) // _FP8_BLOCK
192+
if tuple(s.shape) != (blocks_m, blocks_k):
193+
raise ValueError(
194+
f"_wq_b_apply: wq_b scale_inv shape {tuple(s.shape)} != expected "
195+
f"({blocks_m}, {blocks_k}) for W={w.shape} block={_FP8_BLOCK}"
196+
)
197+
if k == 0:
198+
# Degenerate empty contraction: fused_fp8_gemm has nothing to do;
199+
# the explicit dequant+matmul path returns the correct zeros.
200+
return False
201+
return True
202+
123203
def load_fp8_weights(
124204
self,
125205
wq_b_fp8: mx.array,

0 commit comments

Comments
 (0)