Skip to content

Commit 78c7c90

Browse files
committed
[bugfix]: scope affine dq-GEMM to H3
1 parent 2e6a586 commit 78c7c90

4 files changed

Lines changed: 39 additions & 13 deletions

File tree

.github/workflows/ci-macos-mlx.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ jobs:
8181
fastvideo/tests/mlx/test_mlx_compile_parity.py \
8282
fastvideo/tests/mlx/test_mlx_checkpoint.py \
8383
fastvideo/tests/mlx/test_mlx_checkpoint_compat.py \
84+
fastvideo/tests/mlx/test_mlx_affine_dq_gemm.py \
8485
fastvideo/tests/mlx/test_mlx_minimax_h3_parity.py \
8586
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa.py \
8687
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_regressions.py \
@@ -143,6 +144,7 @@ jobs:
143144
fastvideo/tests/mlx/test_mlx_compile_parity.py \
144145
fastvideo/tests/mlx/test_mlx_checkpoint.py \
145146
fastvideo/tests/mlx/test_mlx_checkpoint_compat.py \
147+
fastvideo/tests/mlx/test_mlx_affine_dq_gemm.py \
146148
fastvideo/tests/mlx/test_mlx_minimax_h3_parity.py \
147149
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa.py \
148150
fastvideo/tests/mlx/test_mlx_minimax_h3_vsa_regressions.py \

fastvideo/mlx_runtime/fastwan.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,10 @@ def quantize_matrix(weight, spec: MLXQuantizationSpec | None):
306306
# Affine quantized_matmul is slower than dequantize + steel GEMM at H3's packed
307307
# token width. Measured on Apple M4 Max / MLX 0.32.2, INT6 group 64, BF16 acts,
308308
# Q 5376→7168: M=256 qmm is faster; M=512 dequant+GEMM is +6.4%; M≥1024 ~10%.
309-
# 832×480×124 packed M is ~14862–14994. Do not cache dequantized weights.
310-
# Override: FASTVIDEO_MLX_DQ_GEMM=0 off, =1 measured floor, =<int> explicit floor.
309+
# 832×480×124 packed M is ~14862–14994. H3 opts into this path explicitly;
310+
# shared FastWan and Wan 2.2 linears stay on quantized_matmul. Do not cache
311+
# dequantized weights. Override: FASTVIDEO_MLX_DQ_GEMM=0 off, =1 measured
312+
# floor, =<int> explicit floor.
311313
_AFFINE_DQ_GEMM_BITS = frozenset({2, 3, 4, 5, 6, 8})
312314
MLX_AFFINE_DQ_GEMM_DEFAULT_MIN_M = 768
313315
_dq_gemm_engaged = 0
@@ -345,12 +347,12 @@ def _matmul_leading_rows(x) -> int:
345347
return int(x.size) // last
346348

347349

348-
def _quantized_linear(x, weight: QuantizedMatrix):
350+
def _quantized_linear(x, weight: QuantizedMatrix, *, use_affine_dq_gemm: bool = False):
349351
import mlx.core as mx
350352

351353
global _dq_gemm_engaged, _dq_gemm_logged
352354
spec = weight.spec
353-
min_m = affine_dq_gemm_min_m()
355+
min_m = affine_dq_gemm_min_m() if use_affine_dq_gemm else None
354356
rows = _matmul_leading_rows(x)
355357
if (min_m is not None and spec.mode == "affine" and spec.bits in _AFFINE_DQ_GEMM_BITS
356358
and spec.group_size is not None and rows >= min_m):
@@ -381,8 +383,9 @@ def _quantized_linear(x, weight: QuantizedMatrix):
381383
).astype(x.dtype)
382384

383385

384-
def linear(x, weight, bias=None):
385-
y = _quantized_linear(x, weight) if isinstance(weight, QuantizedMatrix) else x @ weight.T
386+
def linear(x, weight, bias=None, *, use_affine_dq_gemm: bool = False):
387+
y = (_quantized_linear(x, weight, use_affine_dq_gemm=use_affine_dq_gemm)
388+
if isinstance(weight, QuantizedMatrix) else x @ weight.T)
386389
if bias is not None:
387390
y = y + bias
388391
return y

fastvideo/mlx_runtime/minimax_h3.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
MLXQuantizationSpec,
6262
QuantizedMatrix,
6363
ensure_quantization_supported,
64-
linear,
64+
linear as _shared_linear,
6565
quantize_matrix,
6666
silu,
6767
timestep_embedding,
@@ -81,6 +81,12 @@
8181

8282
logger = init_logger(__name__)
8383

84+
85+
def linear(x, weight, bias=None):
86+
"""Run an H3 linear with its measured wide-row affine dispatch enabled."""
87+
return _shared_linear(x, weight, bias, use_affine_dq_gemm=True)
88+
89+
8490
# ---------------------------------------------------------------------------
8591
# Constants (mirrors fastvideo/pipelines/basic/minimax_h3/packing.py)
8692
# ---------------------------------------------------------------------------

fastvideo/tests/mlx/test_mlx_affine_dq_gemm.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
quantize_matrix,
1919
reset_dq_gemm_telemetry,
2020
)
21+
from fastvideo.mlx_runtime.minimax_h3 import linear as h3_linear # noqa: E402
2122

2223
AFFINE_BITS = (2, 3, 4, 5, 6, 8)
2324
GROUP_SIZES = (32, 64, 128)
@@ -65,7 +66,7 @@ def test_dq_gemm_matches_qmm_for_supported_bit_widths(bits: int, group_size: int
6566
x = mx.random.normal((16, in_features)).astype(mx.bfloat16)
6667
mx.eval(x)
6768
before = dq_gemm_engaged()
68-
got = linear(x, quantized)
69+
got = h3_linear(x, quantized)
6970
ref = _qmm(x, quantized)
7071
mx.eval(got, ref)
7172
assert dq_gemm_engaged() == before + 1
@@ -84,7 +85,7 @@ def test_dq_gemm_with_bias_and_batched_rows(monkeypatch: pytest.MonkeyPatch) ->
8485
x = mx.random.normal((2, 8, 128)).astype(mx.bfloat16)
8586
bias = mx.random.normal((64, )).astype(mx.bfloat16)
8687
mx.eval(x, bias)
87-
got = linear(x, quantized, bias)
88+
got = h3_linear(x, quantized, bias)
8889
ref = _qmm(x, quantized) + bias
8990
mx.eval(got, ref)
9091
assert dq_gemm_engaged() == 1
@@ -99,7 +100,7 @@ def test_dq_gemm_stays_on_qmm_below_threshold(monkeypatch: pytest.MonkeyPatch) -
99100
quantized = _try_quantize(64, 128, bits=6, group_size=64)
100101
x = mx.random.normal((32, 128)).astype(mx.bfloat16)
101102
mx.eval(x)
102-
got = linear(x, quantized)
103+
got = h3_linear(x, quantized)
103104
ref = _qmm(x, quantized)
104105
mx.eval(got, ref)
105106
assert dq_gemm_engaged() == 0
@@ -113,9 +114,23 @@ def test_dq_gemm_env_zero_disables_dispatch(monkeypatch: pytest.MonkeyPatch) ->
113114
quantized = _try_quantize(64, 128, bits=6, group_size=64)
114115
x = mx.random.normal((1024, 128)).astype(mx.bfloat16)
115116
mx.eval(x)
116-
linear(x, quantized)
117-
mx.eval(x)
117+
got = h3_linear(x, quantized)
118+
ref = _qmm(x, quantized)
119+
mx.eval(got, ref)
120+
assert dq_gemm_engaged() == 0
121+
np.testing.assert_array_equal(np.asarray(got.astype(mx.float32)), np.asarray(ref.astype(mx.float32)))
122+
123+
124+
def test_shared_linear_stays_on_qmm_at_wide_m(monkeypatch: pytest.MonkeyPatch) -> None:
125+
monkeypatch.setenv("FASTVIDEO_MLX_DQ_GEMM", "1")
126+
reset_dq_gemm_telemetry()
127+
quantized = _try_quantize(64, 128, bits=6, group_size=64)
128+
x = mx.random.normal((1024, 128)).astype(mx.bfloat16)
129+
got = linear(x, quantized)
130+
ref = _qmm(x, quantized)
131+
mx.eval(got, ref)
118132
assert dq_gemm_engaged() == 0
133+
np.testing.assert_array_equal(np.asarray(got.astype(mx.float32)), np.asarray(ref.astype(mx.float32)))
119134

120135

121136
def test_non_affine_weights_stay_on_quantized_matmul(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -130,7 +145,7 @@ def test_non_affine_weights_stay_on_quantized_matmul(monkeypatch: pytest.MonkeyP
130145
pytest.skip(f"mxfp8 unsupported: {exc}")
131146
x = mx.random.normal((1024, 64)).astype(mx.bfloat16)
132147
mx.eval(x)
133-
got = linear(x, quantized)
148+
got = h3_linear(x, quantized)
134149
mx.eval(got)
135150
assert dq_gemm_engaged() == 0
136151
assert got.shape == (1024, 64)

0 commit comments

Comments
 (0)