Skip to content

Commit 3eacee2

Browse files
committed
feat(v4): vendor mlx-lm PR #1224 FP8 block-dequant as generic utility
Pulled the FP8 dequant snippet out of qwen3_5_moe.py's Model.sanitize (which mirrors the same pattern in models/{deepseek_v3,deepseek_v32, minimax,mimo_v2_flash,ministral3}.py) and exposed it as a standalone utility under cppmega_v4/nn/_external/_mlx_lm_fp8_dequant_vendored.py: - dequant_block_fp8(weight, scale_inv) -> bfloat16 Block size 128; uses mx.from_fp8 + per-block scale_inv broadcast. Handles non-block-aligned shapes via pad → reshape → unpad. - sanitize_fp8_weights(weights) -> dict Iterates a state-dict, dequants every (<prefix>.weight, <prefix>.weight_scale_inv) pair, drops activation_scale PTQ artefacts, leaves non-fp8 tensors untouched. No-op if no weight_scale_inv keys present. Rationale for vendoring as utility rather than model-specific copy: ROI 7 (Lightning Indexer FP8) needs the same dequant path, and we don't want to re-implement it in each loader. PRs #1199 (EngGPT MoE), #991 (sarvam), #560 (LLada2): inspected and skipped — those are vanilla Mixtral-style (softmax routing + SwitchGLU). Our moe_v4.py is already more advanced (aux-loss-free + sqrtsoftplus + hash routing) so vendoring them would be a regression. Tests (5 new): shape round-trip, signal recovery within fp8 precision, non-block-aligned shapes, sanitize replaces paired keys + drops activation_scale, sanitize no-op on non-fp8 weights. v4 suite: 195 passed / 2 skipped.
1 parent f81a5ac commit 3eacee2

2 files changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Vendored from ml-explore/mlx-lm PR #1224 (mlx_lm/models/qwen3_5_moe.py
2+
# `Model.sanitize` FP8 dequant block), generalized to a standalone utility.
3+
# Upstream license: MIT, © 2025-2026 Apple Inc.
4+
#
5+
# Mirrors the inline FP8 dequant pattern that lives in models/{deepseek_v3,
6+
# deepseek_v32, minimax, mimo_v2_flash, ministral3, qwen3_5_moe}.py — pulled
7+
# out so cppmega_v4 (MoE + Lightning Indexer ROI 7) can dequant FP8
8+
# checkpoints without re-copying the snippet in every loader.
9+
10+
"""FP8 block-scaled dequant utility (block size 128, paired weight_scale_inv).
11+
12+
Triggered by HF checkpoints with quant_method=fp8 (e.g. Qwen/Qwen3.6-27B-FP8,
13+
DeepSeek-V3-FP8). Pairs each weight tensor with its `weight_scale_inv` and
14+
multiplies them block-wise to recover bfloat16 weights.
15+
"""
16+
17+
from typing import Mapping
18+
19+
import mlx.core as mx
20+
21+
_BLOCK_SIZE = 128
22+
23+
24+
def dequant_block_fp8(weight: mx.array, scale_inv: mx.array) -> mx.array:
25+
"""Block-dequantize a single FP8 weight tensor.
26+
27+
Args:
28+
weight: [m, n] in fp8.
29+
scale_inv: [ceil(m/128), ceil(n/128)] in fp32; broadcast across
30+
the [128, 128] block.
31+
32+
Returns:
33+
[m, n] in bfloat16.
34+
"""
35+
weight = mx.from_fp8(weight, dtype=mx.bfloat16)
36+
bs = _BLOCK_SIZE
37+
m, n = weight.shape
38+
pad_bottom = (-m) % bs
39+
pad_side = (-n) % bs
40+
weight = mx.pad(weight, ((0, pad_bottom), (0, pad_side)))
41+
weight = weight.reshape(
42+
((m + pad_bottom) // bs, bs, (n + pad_side) // bs, bs)
43+
)
44+
weight = (weight * scale_inv[:, None, :, None]).reshape(
45+
m + pad_bottom, n + pad_side
46+
)
47+
return weight[:m, :n].astype(mx.bfloat16)
48+
49+
50+
def sanitize_fp8_weights(weights: Mapping[str, mx.array]) -> dict[str, mx.array]:
51+
"""Dequantize all FP8-quantized tensors in a state-dict.
52+
53+
Iterates over keys and for every ``<prefix>.weight_scale_inv`` pair with a
54+
matching ``<prefix>.weight`` in fp8, replaces the fp8 weight with its
55+
bfloat16 dequant. ``activation_scale`` keys are dropped (PTQ artefact).
56+
Returns the rewritten state-dict (or the input unchanged if no fp8 keys).
57+
"""
58+
if not any("weight_scale_inv" in k for k in weights):
59+
return dict(weights)
60+
61+
new_weights: dict[str, mx.array] = {}
62+
for k, v in weights.items():
63+
if "weight_scale_inv" in k:
64+
wk = k.replace("_scale_inv", "")
65+
new_weights[wk] = dequant_block_fp8(weights[wk], v)
66+
elif "activation_scale" in k:
67+
continue
68+
elif k not in new_weights:
69+
new_weights[k] = v
70+
return new_weights
71+
72+
73+
__all__ = ["dequant_block_fp8", "sanitize_fp8_weights"]

tests/v4/test_fp8_dequant.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Tests for vendored mlx-lm PR #1224 FP8 block-dequant utility."""
2+
3+
import mlx.core as mx
4+
import numpy as np
5+
import pytest
6+
7+
from cppmega_v4.nn._external._mlx_lm_fp8_dequant_vendored import (
8+
dequant_block_fp8,
9+
sanitize_fp8_weights,
10+
)
11+
12+
13+
def _make_fp8_weight(m: int, n: int, seed: int = 0) -> tuple[mx.array, mx.array]:
14+
"""Build a (weight_fp8, scale_inv) pair from a bf16 reference tensor."""
15+
rng = np.random.default_rng(seed)
16+
bs = 128
17+
blocks_m = (m + bs - 1) // bs
18+
blocks_n = (n + bs - 1) // bs
19+
scale_inv = mx.array(rng.uniform(0.5, 1.5, (blocks_m, blocks_n)).astype(np.float32))
20+
bf16_ref = mx.array(rng.standard_normal((m, n)).astype(np.float32) * 0.1).astype(
21+
mx.bfloat16
22+
)
23+
# Simulate fp8 quantization: divide by per-block scale, cast to fp8.
24+
pad_bottom = (-m) % bs
25+
pad_side = (-n) % bs
26+
padded = mx.pad(bf16_ref.astype(mx.float32), ((0, pad_bottom), (0, pad_side)))
27+
blocks = padded.reshape(blocks_m, bs, blocks_n, bs)
28+
scaled = (blocks / scale_inv[:, None, :, None]).reshape(
29+
m + pad_bottom, n + pad_side
30+
)[:m, :n]
31+
fp8 = mx.to_fp8(scaled) # uint8 storage
32+
return fp8, scale_inv
33+
34+
35+
def test_dequant_block_fp8_round_trip_shape():
36+
m, n = 256, 384
37+
fp8, scale_inv = _make_fp8_weight(m, n)
38+
out = dequant_block_fp8(fp8, scale_inv)
39+
assert out.shape == (m, n)
40+
assert out.dtype == mx.bfloat16
41+
42+
43+
def test_dequant_block_fp8_recovers_signal():
44+
"""Quant→dequant must preserve signal within fp8 precision (~5% rel error)."""
45+
m, n = 128, 128
46+
fp8, scale_inv = _make_fp8_weight(m, n, seed=7)
47+
out = dequant_block_fp8(fp8, scale_inv)
48+
# Build the expected value: scale_inv * fp8_as_fp32
49+
expected = (
50+
mx.from_fp8(fp8, dtype=mx.bfloat16).astype(mx.float32)
51+
* scale_inv[0, 0, None, None]
52+
).astype(mx.bfloat16)
53+
np.testing.assert_allclose(
54+
np.array(out.astype(mx.float32)),
55+
np.array(expected.astype(mx.float32)),
56+
atol=1e-3,
57+
)
58+
59+
60+
def test_dequant_block_fp8_handles_non_block_aligned_shapes():
61+
"""Shapes not divisible by 128 must still produce correct (m, n) output."""
62+
m, n = 100, 200 # both < 128 multiples, edge of pad path
63+
fp8, scale_inv = _make_fp8_weight(m, n)
64+
out = dequant_block_fp8(fp8, scale_inv)
65+
assert out.shape == (m, n)
66+
67+
68+
def test_sanitize_fp8_weights_replaces_paired_keys():
69+
fp8_a, scale_a = _make_fp8_weight(64, 64, seed=1)
70+
fp8_b, scale_b = _make_fp8_weight(128, 128, seed=2)
71+
other = mx.array(np.random.randn(8, 8).astype(np.float32))
72+
weights = {
73+
"model.layers.0.mlp.gate_proj.weight": fp8_a,
74+
"model.layers.0.mlp.gate_proj.weight_scale_inv": scale_a,
75+
"model.layers.1.mlp.gate_proj.weight": fp8_b,
76+
"model.layers.1.mlp.gate_proj.weight_scale_inv": scale_b,
77+
"model.norm.weight": other,
78+
# PTQ artefact that must be dropped:
79+
"model.layers.0.mlp.gate_proj.activation_scale": mx.array([1.0]),
80+
}
81+
out = sanitize_fp8_weights(weights)
82+
assert "model.layers.0.mlp.gate_proj.weight" in out
83+
assert "model.layers.0.mlp.gate_proj.weight_scale_inv" not in out
84+
assert "model.layers.0.mlp.gate_proj.activation_scale" not in out
85+
assert out["model.layers.0.mlp.gate_proj.weight"].dtype == mx.bfloat16
86+
assert out["model.norm.weight"] is other # untouched
87+
88+
89+
def test_sanitize_fp8_weights_no_op_when_no_fp8():
90+
"""Without any weight_scale_inv key, return a dict copy unchanged."""
91+
weights = {
92+
"model.norm.weight": mx.array([1.0, 2.0]),
93+
"model.embed_tokens.weight": mx.array([[0.0, 1.0]]),
94+
}
95+
out = sanitize_fp8_weights(weights)
96+
assert out == dict(weights)
97+
# Different dict object (defensive copy), same values
98+
assert out is not weights

0 commit comments

Comments
 (0)