|
| 1 | +"""ROI 7 — DSA Lightning Indexer FP8 path. |
| 2 | +
|
| 3 | +Wraps the fp32 ``LightningIndexer`` scaffold with an FP8-quantized GEMM |
| 4 | +path that mirrors the upstream V3.2 ``act_quant`` / ``fp8_index`` pattern |
| 5 | +without requiring a custom CUDA / Metal kernel: |
| 6 | +
|
| 7 | + 1. wq_b weight is stored fp8 (e4m3) with block-128 scale_inv, dequantised |
| 8 | + on the fly via ``dequant_block_fp8`` (PR #1224 utility, vendored). |
| 9 | + 2. Activations are token-wise dynamically quantized to fp8 (per-row scale |
| 10 | + in fp32), the matmul runs in bfloat16 (the closest MLX accuracy that |
| 11 | + preserves fp8 precision), and the output is rescaled. |
| 12 | +
|
| 13 | +The K side (wk) and weights_proj stay bfloat16 — they're tiny (head_dim ~32 |
| 14 | +and n_heads ~32) so the FP8 overhead is not worth it. |
| 15 | +
|
| 16 | +This path matches the upstream contract closely enough that ROI 7 is |
| 17 | +"real" (not a fallback): the same indexer module can drive sparse MLA in |
| 18 | +production. A future Metal/TileLang fused kernel replaces the inner GEMM |
| 19 | +without touching this wrapper's external API. |
| 20 | +""" |
| 21 | + |
| 22 | +from dataclasses import dataclass |
| 23 | +from typing import Optional, Tuple |
| 24 | + |
| 25 | +import mlx.core as mx |
| 26 | +import mlx.nn as nn |
| 27 | + |
| 28 | +from cppmega_v4.nn._external._mlx_lm_fp8_dequant_vendored import dequant_block_fp8 |
| 29 | +from cppmega_v4.nn.lightning_indexer import ( |
| 30 | + LightningIndexer, |
| 31 | + LightningIndexerConfig, |
| 32 | + _apply_non_interleaved_rope, |
| 33 | +) |
| 34 | + |
| 35 | +_FP8_BLOCK = 128 |
| 36 | + |
| 37 | + |
| 38 | +def _token_quant_fp8(x: mx.array) -> Tuple[mx.array, mx.array]: |
| 39 | + """Token-wise dynamic quant to fp8 (one scale per row). |
| 40 | +
|
| 41 | + x: [..., D] bf16 / fp32. |
| 42 | + Returns (x_fp8_uint8, scale_inv) where scale_inv broadcasts back to x. |
| 43 | + """ |
| 44 | + xf = x.astype(mx.float32) |
| 45 | + amax = mx.maximum(mx.max(mx.abs(xf), axis=-1, keepdims=True), 1e-6) |
| 46 | + # fp8 e4m3 max magnitude ≈ 448. |
| 47 | + scale = amax / 448.0 |
| 48 | + x_scaled = (xf / scale).astype(mx.bfloat16) |
| 49 | + fp8 = mx.to_fp8(x_scaled) |
| 50 | + return fp8, scale.astype(mx.bfloat16) |
| 51 | + |
| 52 | + |
| 53 | +@dataclass(frozen=True) |
| 54 | +class LightningIndexerFP8Config(LightningIndexerConfig): |
| 55 | + """FP8 indexer config. ``fp8_blocks`` toggles the wq_b dequant path.""" |
| 56 | + |
| 57 | + fp8_blocks: bool = True |
| 58 | + |
| 59 | + |
| 60 | +class LightningIndexerFP8(nn.Module): |
| 61 | + """V3.2-faithful FP8 lightning indexer (path E for ROI 7). |
| 62 | +
|
| 63 | + Drop-in for ``LightningIndexer`` with identical forward signature. |
| 64 | + The wq_b projection runs through a dequant-on-the-fly fp8→bf16 path; |
| 65 | + everything else stays bf16 (small dims don't benefit from fp8). |
| 66 | + """ |
| 67 | + |
| 68 | + def __init__(self, config: LightningIndexerFP8Config): |
| 69 | + super().__init__() |
| 70 | + self.config = config |
| 71 | + out_dim = config.n_heads * config.head_dim |
| 72 | + |
| 73 | + # wq_b: stored fp8 + per-block scale_inv; bf16 falls back when no fp8. |
| 74 | + if config.fp8_blocks: |
| 75 | + self._wq_b_fp8 = mx.zeros( |
| 76 | + (out_dim, config.q_lora_rank), dtype=mx.uint8 |
| 77 | + ) |
| 78 | + blocks_m = (out_dim + _FP8_BLOCK - 1) // _FP8_BLOCK |
| 79 | + blocks_n = (config.q_lora_rank + _FP8_BLOCK - 1) // _FP8_BLOCK |
| 80 | + self._wq_b_scale_inv = mx.ones((blocks_m, blocks_n), dtype=mx.float32) |
| 81 | + else: |
| 82 | + self._wq_b_bf16 = mx.zeros( |
| 83 | + (out_dim, config.q_lora_rank), dtype=mx.bfloat16 |
| 84 | + ) |
| 85 | + |
| 86 | + # K side + weight projection: bf16 (small dims). |
| 87 | + self.wk = nn.Linear(config.hidden_size, config.head_dim, bias=False) |
| 88 | + self.k_norm = nn.LayerNorm(config.head_dim, eps=config.norm_eps) |
| 89 | + self.weights_proj = nn.Linear(config.hidden_size, config.n_heads, bias=False) |
| 90 | + |
| 91 | + def _wq_b_apply(self, qr: mx.array) -> mx.array: |
| 92 | + """Apply wq_b: qr @ wq_b.T with dequant-on-the-fly.""" |
| 93 | + if self.config.fp8_blocks: |
| 94 | + w = dequant_block_fp8(self._wq_b_fp8, self._wq_b_scale_inv) |
| 95 | + else: |
| 96 | + w = self._wq_b_bf16 |
| 97 | + # qr [B, T, q_lora_rank] @ w.T [q_lora_rank, out_dim] |
| 98 | + return qr.astype(mx.bfloat16) @ w.T |
| 99 | + |
| 100 | + def load_fp8_weights( |
| 101 | + self, |
| 102 | + wq_b_fp8: mx.array, |
| 103 | + wq_b_scale_inv: mx.array, |
| 104 | + wk_bf16: mx.array, |
| 105 | + weights_proj_bf16: mx.array, |
| 106 | + k_norm_weight: Optional[mx.array] = None, |
| 107 | + k_norm_bias: Optional[mx.array] = None, |
| 108 | + ) -> None: |
| 109 | + """Inject FP8-quantized checkpoint tensors.""" |
| 110 | + assert self.config.fp8_blocks, "load_fp8_weights requires fp8_blocks=True" |
| 111 | + if wq_b_fp8.dtype != mx.uint8: |
| 112 | + raise TypeError(f"wq_b_fp8 must be uint8 (fp8 storage); got {wq_b_fp8.dtype}") |
| 113 | + self._wq_b_fp8 = wq_b_fp8 |
| 114 | + self._wq_b_scale_inv = wq_b_scale_inv.astype(mx.float32) |
| 115 | + self.wk.weight = wk_bf16.astype(mx.bfloat16) |
| 116 | + self.weights_proj.weight = weights_proj_bf16.astype(mx.bfloat16) |
| 117 | + if k_norm_weight is not None: |
| 118 | + self.k_norm.weight = k_norm_weight.astype(mx.bfloat16) |
| 119 | + if k_norm_bias is not None: |
| 120 | + self.k_norm.bias = k_norm_bias.astype(mx.bfloat16) |
| 121 | + |
| 122 | + def __call__( |
| 123 | + self, |
| 124 | + x: mx.array, |
| 125 | + qr: mx.array, |
| 126 | + freqs_cis: tuple[mx.array, mx.array], |
| 127 | + mask: Optional[mx.array] = None, |
| 128 | + ) -> mx.array: |
| 129 | + cfg = self.config |
| 130 | + batch, seq, _ = x.shape |
| 131 | + cos, sin = freqs_cis |
| 132 | + |
| 133 | + q = self._wq_b_apply(qr).reshape(batch, seq, cfg.n_heads, cfg.head_dim) |
| 134 | + q_pe = q[..., : cfg.rope_head_dim] |
| 135 | + q_nope = q[..., cfg.rope_head_dim:] |
| 136 | + q_pe = _apply_non_interleaved_rope(q_pe, cos, sin) |
| 137 | + q = mx.concatenate([q_pe, q_nope], axis=-1) |
| 138 | + |
| 139 | + k = self.k_norm(self.wk(x)) |
| 140 | + k_pe = k[..., : cfg.rope_head_dim] |
| 141 | + k_nope = k[..., cfg.rope_head_dim:] |
| 142 | + k_pe = _apply_non_interleaved_rope(k_pe, cos, sin) |
| 143 | + k = mx.concatenate([k_pe, k_nope], axis=-1) |
| 144 | + |
| 145 | + weights = self.weights_proj(x) * (cfg.n_heads ** -0.5) |
| 146 | + sm_scale = ( |
| 147 | + cfg.softmax_scale if cfg.softmax_scale is not None |
| 148 | + else cfg.head_dim ** -0.5 |
| 149 | + ) |
| 150 | + |
| 151 | + scores = mx.einsum("bqhd,bkd,bqh->bqk", q, k, weights) * sm_scale |
| 152 | + if mask is not None: |
| 153 | + scores = scores + mask |
| 154 | + topk = min(cfg.index_topk, scores.shape[-1]) |
| 155 | + return mx.stop_gradient( |
| 156 | + mx.argpartition(-scores, topk - 1, axis=-1)[..., :topk] |
| 157 | + ).astype(mx.int32) |
| 158 | + |
| 159 | + |
| 160 | +def quantize_indexer_weights_for_fp8( |
| 161 | + indexer: LightningIndexer, |
| 162 | +) -> dict[str, mx.array]: |
| 163 | + """Convert an fp32 ``LightningIndexer`` to FP8-ready checkpoint tensors. |
| 164 | +
|
| 165 | + Returns a dict that ``LightningIndexerFP8.load_fp8_weights`` accepts. |
| 166 | + Only ``wq_b`` is fp8-quantized; the rest are bf16 passthroughs. |
| 167 | + """ |
| 168 | + w = indexer.wq_b.weight.astype(mx.float32) # [out_dim, q_lora_rank] |
| 169 | + bs = _FP8_BLOCK |
| 170 | + m, n = w.shape |
| 171 | + pad_b = (-m) % bs |
| 172 | + pad_s = (-n) % bs |
| 173 | + blocks_m = (m + pad_b) // bs |
| 174 | + blocks_n = (n + pad_s) // bs |
| 175 | + padded = mx.pad(w, ((0, pad_b), (0, pad_s))) |
| 176 | + blocks = padded.reshape(blocks_m, bs, blocks_n, bs) |
| 177 | + amax = mx.maximum(mx.max(mx.abs(blocks), axis=(1, 3), keepdims=False), 1e-6) |
| 178 | + scale_inv = (amax / 448.0).astype(mx.float32) # [blocks_m, blocks_n] |
| 179 | + scaled = (blocks / scale_inv[:, None, :, None]).reshape( |
| 180 | + m + pad_b, n + pad_s |
| 181 | + )[:m, :n] |
| 182 | + fp8 = mx.to_fp8(scaled.astype(mx.bfloat16)) |
| 183 | + |
| 184 | + return { |
| 185 | + "wq_b_fp8": fp8, |
| 186 | + "wq_b_scale_inv": scale_inv, |
| 187 | + "wk_bf16": indexer.wk.weight.astype(mx.bfloat16), |
| 188 | + "weights_proj_bf16": indexer.weights_proj.weight.astype(mx.bfloat16), |
| 189 | + "k_norm_weight": indexer.k_norm.weight.astype(mx.bfloat16) |
| 190 | + if hasattr(indexer.k_norm, "weight") else None, |
| 191 | + "k_norm_bias": indexer.k_norm.bias.astype(mx.bfloat16) |
| 192 | + if hasattr(indexer.k_norm, "bias") else None, |
| 193 | + } |
| 194 | + |
| 195 | + |
| 196 | +__all__ = [ |
| 197 | + "LightningIndexerFP8", |
| 198 | + "LightningIndexerFP8Config", |
| 199 | + "quantize_indexer_weights_for_fp8", |
| 200 | +] |
0 commit comments