Fix and enable non-transposed NAX qmm - #4051
Open
gordofreemo wants to merge 2 commits into
Open
Conversation
qmm() only lets NAX through when transpose is true, so affine_qmm_n_nax never actually runs and nothing in CI touches it. Two things are broken in it. The weight addressing is set up for [N, K]: it offsets by the K_w/K_g row strides and passes K as the leading dim. With transpose=false the operand is [K, N], grouped along N. The loader instantiation is already the same one the generic qmm_n_impl uses, so the offsets just need to match it. Same impl backs affine_gather_qmm_n_nax, so that gets fixed as well. It also ignores M entirely. The bounds line is commented out and the tile load/store are unguarded, but the grid is ceil(M/BM), so partial tiles do get dispatched and anything not a multiple of 64 writes past the end of y. Pulled sgp_sm + dispatch_bool + load_safe/store_safe over from qmm_t_nax_tgp_impl. The split is compile time so the aligned path doesn't change. Used min(int(SM), ...) rather than truncating the distance to short, same as the int16 fix over there. The guard now also needs N % 64 == 0 for the non-transposed case since the weight tile isn't clamped in N, but any group_size >= 64 gives that anyway. Left gather_qmm() gated since I haven't tested that dispatch path. Test covers unaligned M, mainly 33..63 where a whole simdgroup ends up past the end of the matrix, plus batched x and a large-M partial tile.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
qmm()only lets NAX through whentranspose == true, soaffine_qmm_n_naxis compiled but never actually run and is currently unreachable. It has two separate problems, both of which this PR fixes, and then enables the path to be reachable.FIX: Weight addressing uses the transposed layout
qmm_n_nax_tgp_impluses the sameQuantizedBlockLoaderinstantiation as thegeneric
qmm_n_implinkernels/quantized.h:but set the pointers up for
w = [N, K]rather than[K, N]:qmm_n_impl(correct)qmm_n_nax_tgp_impl(before)wl += y_col * bytes_per_pack / pack_factorwl += y_col * K_wscales += y_col / group_sizescales += y_col * K_gbiases += y_col / group_sizebiases += y_col * K_gloader_w(..., N, ...)loader_w(..., K, ...)w = [K, N], grouped along Nw = [N, K], grouped along KThis fix makes those four lines match
qmm_n_impl.affine_gather_qmm_n_naxshares the impl, so this fixes that too.FIX: No partial M tile handling
This same function had
(void)M;, its bounds line commented out, and calledAtile.load/Dtile.storeunconditionally.qmm_nax()sizes the grid with(M + bm - 1) / bm, so partial M tiles are dispatched and anyM % 64 != 0read
xand wroteyout of bounds.Fixed by porting what
qmm_t_nax_tgp_implalready does:sgp_smplus adispatch_boolpickingload_safe/store_safe. That split is compile time, so the aligned path keeps the original unguarded loads and costs nothing, and only one branch is instantiated per call so thex += BKwalk isn't duplicated.sgp_smismin(int(SM), M - (y_row + tm))rather than truncating the distance toshort, matching the int16 fix already made for the transposed kernel.FIX: The dispatch guard
The non-transposed weight tile is
BK x BNoverw = [K, N]and the loader isn't clamped in N, so it also needsN % 64 == 0. Affine quantization withgroup_size >= 64gives that structurally, since the groups run along N. This only rejectsgroup_size == 32with N not a multiple of 64, which falls back to the generic kernel like before.Scope: this patches
qmm()only.gather_qmm()stays gated ontranspose.Tests
test_qmm_non_transposedinpython/tests/test_quantized.py:K/N: (2048, 2048), (512, 2048), (2048, 512), (11008, 2048)Maligned and unaligned: 1, 2, 31, 32, 33, 63, 64, 65, 96, 97, 100, 127, 128, 129, 250, 256. The 33 to 63 range is where a whole simdgroup of the M tile lands past the end of the matrixx, 3D and 4D, with unaligned MM = 33000with a partial M tile, for the int16 distance casegroup_size64 and 128, bits 4 and 8Tolerances and the
1/sqrt(K)operand scaling follow the existingtest_qmm. Worth noting the existingtest_qmmalready coverstranspose=Falseat M in {8, 32, 33, 64} with N, K in {128, 256}.Validation
I don't have M5 hardware. All NAX validation was on an iPhone 17 Pro (A19 Pro), iOS 26.5.2, through the Swift bindings, against a dequantized reference: 52 shape/M/batch cases covering every unaligned M above plus batched. Before the fix, relative error ran 1.5 to 7826, with an output norm of 427,429 against an expected 24,009 at K=N=2048, M=500. After, it's bit exact against the reference on that device.
On the M3 I have,
is_nax_available()is false, so the fulltest_quantized.pysuite passes there only through the generic path. That confirms no regression on the fallback, not the NAX path. The kernel itself does compile (quantized_nax.airbuilds clean).Performance
Same iPhone 17, but this time running LoRA fine tuning on SmolLM3-3B 4-bit (group_size 64, r=8, batch 1).
One full training run with 405 examples and 1215 iterations. It took 2.9 hours before the fix and 1.5 after the fix (~1.93x speedup). Final loss 0.8829 with the fix against 0.8813 without, a 0.18% difference, and the two loss traces correlate at 0.9999 across 1210 steps.

A paired per op benchmark on fixed synthetic shapes puts the backward phase at 1.65 to 2.13x and the whole iteration around 1.55x.

Checklist
Put an
xin the boxes that apply.pre-commit run --all-filesto format my code / installed pre-commit prior to committing changes