Skip to content

Route affine qmv_wide by bit-width and batch size#5

Merged
khosravipasha merged 6 commits into
prismfrom
perf/dspark-qmv-wide
Jul 13, 2026
Merged

Route affine qmv_wide by bit-width and batch size#5
khosravipasha merged 6 commits into
prismfrom
perf/dspark-qmv-wide

Conversation

@bri-prism

@bri-prism bri-prism commented Jul 13, 2026

Copy link
Copy Markdown

Note: this branch is stacked on the DSpark spec-decode work and an upstream cherry-pick, so the diff against prism currently spans three separable pieces. It will be split into a clean stack (see below) before merge — flagging now so reviewers know the true scope.

Contents of this branch vs prism

  1. spec_decode_verify fast op (commits a29368ea, 67f3651d, 8e6c9feb, c8eaa12e) — CPU/composition v1 + fused Metal kernel v2, mx.fast plumbing, docs, and test_spec_verify.py. The core MLX-side primitive DSpark needs.
  2. Upstream qmv_wide cherry-pick (c9a59b06, Add small-batch quantized matvec kernel (qmv_wide) ml-explore/mlx#3764) — small-batch quantized matvec kernel.
  3. Selective qmv_wide dispatch (bff994ff, this PR's intended change) — see below.

The intended change (selective dispatch)

Make the affine qmv_wide dispatch bit-width and batch-size aware instead of routing every affine matvec with M >= 2 to the wide kernel.

qmv_wide dequantizes a full group of weights into registers and reuses them across the M input rows. That reuse only amortizes when enough rows share a group:

  • 2-bit: breaks even at M = 2, wins from M >= 3 (larger N benefits more).
  • 1-bit: weight traffic is small enough that per-row register dequant dominates, so the specialized qmv is faster — routing 1-bit through qmv_wide is a measurable regression.

use_qmv_wide now takes bits and M: 1-bit and 2-bit M < 2 stay on the specialized qmv; 2-bit routes to qmv_wide only at M >= 3 on gen-15+; fp modes unchanged. Adds 1-bit affine coverage to test_qmv_wide.

Testing

  • test_qmv_wide passes, including new 1-bit cases.
  • Affine 2-bit matvecs across a range of projection shapes: qmv_wide wins at widths >= 3, no regression at width <= 2 or for 1-bit.

Planned split before merge

  • PR A: spec_decode_verify fast op -> prism.
  • PR B: qmv_wide cherry-pick + selective dispatch, stacked on A.

bri-prism and others added 6 commits July 13, 2026 17:29
Fused greedy speculative-decoding verify as mx.fast.spec_decode_verify:
inputs draft_tokens [B,K] + target_logits [B,K+1,V]; outputs n_accepted [B]
and committed [B,K+1] (accepted draft prefix ++ corrected token). v1 is a pure
op composition (argmax -> longest-match prefix -> corrected), so it needs no new
Primitive/Metal/stubs and runs on any backend; it is the intended fallback for a
future fused Metal kernel. Greedy only (temperature==0); caller prepends the
seed 'last' token. Validated vs a Swift-derived oracle across fp32/fp16/bf16.
Promote the v1 composition to a fast::Custom primitive (SpecDecodeVerify). The
op reuses mlx argmax on-device (result stays on GPU), then the fused primitive
does only the trivial per-row prefix-match in one thread/row -> no large-V
reduction, no tie-break risk. use_fallback routes CPU to the op composition
(the correctness oracle) and GPU to the kernel. Adds metal eval_gpu +
spec_decode.metal, CMake wiring, and no_gpu/cuda link stubs. Validated on a
Metal build: kernel(gpu) and fallback(cpu) both match the oracle across
fp32/fp16/bf16. Perf to be judged on an e2e decode loop, not this microbench
(the win is the host round-trip/launch collapse, not FLOPs).
The qmv_wide kernel dequantizes a full group of weights into registers and
reuses them across the M input rows. That amortization only pays off for 2-bit
once three or more rows share a group; at M=2 it breaks even, and for 1-bit the
weight traffic is small enough that the per-row dequant dominates and the
specialized qmv is faster.

Gate the dispatch accordingly: 1-bit and 2-bit M<2 keep the specialized qmv;
2-bit routes to qmv_wide only at M>=3 on gen-15+; fp modes are unchanged. Add
1-bit affine coverage (full sweep + tiny shapes) to test_qmv_wide.

Measured on affine 2-bit matvecs across a range of projection shapes: qmv_wide
wins at widths >=3 (more so as N grows), with no regression at width <=2 or for 1-bit.
@bri-prism
bri-prism requested a review from khosravipasha July 13, 2026 22:45
@bri-prism

Copy link
Copy Markdown
Author

@copilot review

@khosravipasha
khosravipasha requested a review from Copilot July 13, 2026 23:45
@khosravipasha
khosravipasha merged commit fee5273 into prism Jul 13, 2026
5 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds selective qmv_wide dispatch alongside the stacked speculative-decoding operation and upstream wide-kernel implementation.

Changes:

  • Routes affine qmv_wide by bit width, batch size, and GPU generation.
  • Adds affine/FP wide kernels and quantized-matmul coverage.
  • Introduces mx.fast.spec_decode_verify with Metal and fallback implementations.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test_spec_verify.py Adds standalone verification tests.
python/tests/test_quantized.py Tests wide quantized matvec paths.
python/src/fast.cpp Exposes the Python fast API.
mlx/fast.h Declares the C++ API.
mlx/fast.cpp Implements validation and fallback composition.
mlx/fast_primitives.h Defines the verification primitive.
mlx/backend/no_gpu/primitives.cpp Enables non-GPU fallback.
mlx/backend/cuda/primitives.cpp Enables CUDA fallback.
mlx/backend/metal/spec_decode.cpp Dispatches the Metal kernel.
mlx/backend/metal/kernels/spec_decode.metal Implements fused verification.
mlx/backend/metal/quantized.cpp Adds selective wide dispatch.
mlx/backend/metal/kernels/quantized.h Implements affine wide matvec.
mlx/backend/metal/kernels/quantized.metal Instantiates affine kernels.
mlx/backend/metal/kernels/fp_quantized.h Implements FP wide matvec.
mlx/backend/metal/kernels/fp_quantized.metal Instantiates FP kernels.
mlx/backend/metal/kernels/CMakeLists.txt Builds the new Metal kernel.
mlx/backend/metal/CMakeLists.txt Builds the Metal dispatcher.
docs/src/python/fast.rst Adds API documentation entry.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mlx/fast.cpp
// Reuse mlx's fast argmax on-device (result stays on the GPU); the fused
// primitive does only the tiny per-row prefix-match. Primitive inputs are
// int32 token arrays, so the kernel needs no large-V reduction.
auto dft = astype(draft_tokens, int32, s);
Comment thread mlx/fast.cpp
Comment on lines +993 to +995
auto d = inputs[0]; // [B, K] int32
auto t = inputs[1]; // [B, K+1] int32
auto t_pref = slice(t, Shape{0, 0}, Shape{B, K}, s);
Comment thread test_spec_verify.py
@@ -0,0 +1,111 @@
"""Validate mx.fast.spec_decode_verify against a pure-Python oracle mirroring the
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants