metal: PQ2_0 and Q2_0 mat-vec dot in exact float (+5-7% decode) - #159
Merged
Conversation
The dot accumulated each 2-bit field through two ands, two bool tests, two selects and two adds per element, none of which this ISA can co-issue with the floating-point adds. A byte is a base-4 fraction of 256. With u = b/256 and g_k = floor(4^k*u), exact in fp32, the floor chain peels the fields from the top down because plain bit packing keeps field 0 in the low bits: g_1 is field 3, g_2 - 4*g_1 is field 2, g_3 - 4*g_2 is field 1 and b - 4*g_3 is field 0. Summing field*y over a byte collapses to g_1*(y_3 - 4*y_2) + g_2*(y_2 - 4*y_1) + g_3*(y_1 - 4*y_0) + b*y_0, whose coefficients depend only on the activations, so the caller stages those in place of the raw y and reuses them across every row. Three floors and four fmas per byte, no integer work. The shared select-chain overload is left in place for its other callers. The field order was checked against the packing over all 256 bytes before the kernel was touched; a first derivation that assumed the base-3 digit order was wrong and was caught by that check. The collapse is exact. test-backend-ops on Metal: MUL_MAT 45 PQ2_0 cases pass, exit code 0 of 1719; MUL_MAT_ID 75 pass, exit code 0 of 1022. Not bit-identical: coefficient staging reassociates the fp32 sum. Measured on an M5 Pro, llama-bench r=3, two interleaved passes, dense models: small decode 285.8 -> 305.1 tok/s (+6.8%) large decode 80.2 -> 83.1 tok/s (+3.6%) Prefill unchanged.
Q2_0 and PQ2_0 use the same 2-bit codec at group sizes 64 and 128, and their mat-vecs stage activations the same way, so the base-4 collapse from the previous commit applies unchanged: the dot becomes a template over the block type and the Q2_0 caller stages the same coefficients. Three floors and four fmas per byte replace the select chain. Not bit-identical: coefficient staging reassociates the fp32 sum. The same idea was tried on Q1_0 and rejected on measurement: with one bit per element the select is already a single op, so seven floors per byte lose to it by 5 to 7 percent. Q1_0 keeps its select chain. test-backend-ops on Metal: MUL_MAT 45 Q2_0 cases pass, exit code 0 of 1719; MUL_MAT_ID 75 pass, exit code 0 of 1022. Measured on an M5 Pro, llama-bench r=3, two interleaved passes, a small dense model: decode 283.6 -> 297.3 tok/s (+4.8%). Prefill unchanged.
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.
The base-4 analogue of the exact-float extraction from #157, applied to the two 2-bit mat-vecs. PQ2_0 and Q2_0 share one codec at group sizes 128 and 64, so one templated dot serves both.
What
M5 Pro,
llama-bench -p 512 -n 128 -r 3, two interleaved passes. Prefill unchanged in all cases; the mat-mul path does not use this routine.Why
The dot accumulated each 2-bit field through two ands, two bool tests, two selects and two adds per element, none of which this ISA can co-issue with the floating-point adds.
How
A byte is a base-4 fraction of 256. With
u = b/256andg_k = floor(4^k * u), exact in fp32, the floor chain peels the fields from the top down, because plain bit packing keeps field 0 in the low bits:g_1is field 3,g_2 - 4*g_1is field 2,g_3 - 4*g_2is field 1 andb - 4*g_3is field 0. Summing field times activation over a byte collapses tog_1*(y_3 - 4*y_2) + g_2*(y_2 - 4*y_1) + g_3*(y_1 - 4*y_0) + b*y_0, whose coefficients depend only on the activations, so the caller stages those in place of the rawyand reuses them across every row. Three floors and four fmas per byte, no integer work. The shared select-chain overload is left in place for its other callers.The field order was checked against the packing over all 256 bytes before the kernel was touched. A first derivation that assumed the base-3 digit order was wrong and was caught by that check; the base-3 codec stores digit 0 as the leading fractional digit, which is why the same chain runs bottom-up there and top-down here.
What was tried and rejected
The same rewrite on Q1_0 is a measured regression of 5 to 7 percent on two dense models and is not included. With one bit per element the select is already a single op, so seven floors per byte cannot beat it and floor's lower throughput loses. The rule that falls out: the float-floor form pays only where the integer path costs two or more selects, or a multiply-shift chain, per element.
Where the 2-bit formats now sit
Against a weight-bandwidth ceiling of roughly 270 GB/s, PQ2_0 decode on the larger model is at about 75 percent of the ceiling after this change, so there is little ALU headroom left there. The base-3 formats sit lower (PTQ1_0 about 61 percent, TQ1_0 about 49 percent) and remain the place where kernel work still moves the number.
Correctness
test-backend-opson Metal, rebuilt on this branch's base:MUL_MAT45 cases pass for each of Q2_0 and PQ2_0 with exit code 0 out of 1719 total;MUL_MAT_ID75 pass for each with exit code 0 out of 1022; Q1_0 is unchanged and passes the same. The collapse is exact; results are not bit-identical to the select chain because the coefficient staging reassociates the fp32 sum. A/B arms were separate build directories with the embedded shader confirmed distinct before each measurement.