fix(matmul-nbits): free autotune dequant scratch when a fused config wins - #653
Draft
BoarQing wants to merge 2 commits into
Draft
fix(matmul-nbits): free autotune dequant scratch when a fused config wins#653BoarQing wants to merge 2 commits into
BoarQing wants to merge 2 commits into
Conversation
|
Thanks for opening a PR! This project follows LLVM's incremental-development and AI-tool-use Before requesting review, please check that:
Reviewers are assigned through |
…wins Autotuning benchmarks the dq+gemm candidates, so it materialises the N*K fp16 dequantisation scratch before a winner is known. The buffer is a process-global static that only ever grows and is never released, so a losing candidate on the widest matmul in a model pins that size for the lifetime of the process even when the winning config never reads it. On gemma-4-12B the vocabulary projection is N=262144, K=3840, which makes the scratch 2,013,265,920 bytes. That is 15.5% of the 12.571 GiB the gfx1151 carveout exposes, held for a config that does not use it. Release the buffer once the winner is known to be fused. The dispatch paths already call ensureDqBuffer again whenever a non-fused config is selected, so a later dispatch that does need the scratch re-allocates it on demand. Made with [Cursor](https://cursor.com) Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
The dq-scratch release in the previous commit had to be pasted into tuneWmmaConfig, tuneWmmaConfigU3 and tuneWmmaConfigU2 at the same point, because the three tuners are copies of one sweep. That is the real defect: any change to autotuning has to be made three times, and when it isn't, the copies drift. They already have -- the u3/u2 tuners gained a clock-settle phase to stop GPU boost/throttle from biasing the sweep by table position, and the 4-bit tuner never got it. Extract the sweep into tuneWmmaSweep(), templated on the config type because the three tables are distinct structs sharing the fields the sweep reads. The caller supplies a launch lambda and a skip predicate; the remaining per-variant differences go in a WmmaTuneParams struct. Each variant keeps exactly the values it had: variant warmup iters default_id clock settle skip predicate 4-bit 1 5 9 no bn < 64 && N > 2*bn u3 2 7 12 yes (id 12) bn > N*2 u2 2 7 12 yes (id 12) bn > N*2 Reconciling those is a behaviour change, not a refactor, so the differences are preserved and recorded in the struct rather than smoothed over. In particular the missing 4-bit clock settle is left alone and flagged in a comment for a separate look. releaseDqBufferIfWinnerFused() is now called from one place. Co-authored-by: Cursor <cursoragent@cursor.com>
L2 Accuracy Results (EP vs CPU)
Threshold: 0.01 | Run: 3614 - Commit: |
MorphiZen EP Performance Results
EPContext Export Performance
EPContext Import Performance
OGA Benchmark Results
OGA Wheel Smoke (Python benchmark_e2e.py)
Run: 3614 - Commit: |
Contributor
Author
Contributor
Author
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.
Summary
Two commits. The first frees a large autotune scratch buffer that is never read; the second removes the triplication that forced the fix to be written three times.
1. Release the dq scratch when the autotune winner is fused
tuneWmmaConfigbenchmarks both fused and dequant-then-gemm candidates, so it has to materialise theN*Kfp16 scratch even when a fused candidate ends up winning.g_dq_bufis process-global and only ever grows, so one losing candidate on the widest matmul in the model pins that size for the life of the process.On
gemma-4-12Bthe vocabulary projection is 262144x3840, which is 1.875 GiB of scratch that nothing goes on to read. That is a large fraction of the 12.571 GiB carveout on gfx1151.releaseDqBufferIfWinnerFused()frees it once the winner is known to need no scratch. The dispatch paths callensureDqBuffer()again if a non-fused config is selected, so a fused winner simply stops holding memory it will not touch.2. Unify the three WMMA autotune sweeps
Writing that fix exposed the actual problem: it had to be pasted into
tuneWmmaConfig,tuneWmmaConfigU3andtuneWmmaConfigU2at the same point, because the three tuners are copies of a single sweep. Every autotune change has to be made three times, and when it isn't, the copies drift.They have already drifted. The u3 and u2 tuners gained a clock-settle phase — without it the GPU is still on boost clock for the early configs and throttled by the late ones, which biased the sweep by table position (the u3 comment records dq+gemm configs, which sit last, looking 30-40% slower than they are). The 4-bit tuner never got it.
The sweep now lives once in
tuneWmmaSweep(), templated on the config type because the three tables are distinct structs that share the fields the sweep reads. The caller passes a launch lambda and a skip predicate; remaining differences go in aWmmaTuneParamsstruct.Each variant keeps exactly the values it had:
bn < 64 && N > 2*bnbn > N*2bn > N*2Reconciling those would be a behaviour change rather than a refactor, so the differences are preserved and recorded in the struct instead of smoothed over. The missing 4-bit clock settle is deliberately left alone and flagged in a comment — see the note below.
Net for the refactor commit: 139 insertions, 195 deletions, and
releaseDqBufferIfWinnerFused()is called from one place.Test plan
-Wpass-failednotes).gemma-4-12Bint4-kquant, 270-token multimodal prompt + 8 decode steps: passes, and the 4-bit sweep runs all 8 shapes.dq+gemmwinner (scratch correctly retained), the rest selectfused(scratch released).On verifying that the refactor did not change tuning
Comparing selected config ids before and after the refactor shows differences, so I measured whether that means anything. Same prompt, autotune cache cleared before each run, four runs — two with the pre-refactor DLL and two with the post-refactor DLL:
The same binary run twice disagrees on 6 of 8 shapes; old-vs-new disagrees on 5 of 8.
N=3840 K=4096returns four different winners across four runs, two of which are the same binary. Only one shape is stable throughout.So the refactor is indistinguishable from run-to-run noise, which is the honest claim — I cannot assert bit-identical tuning, because the 4-bit tuner does not produce identical tuning against itself.
This is a pre-existing condition, not something introduced here, and it is the same phenomenon the disk cache exists to paper over ("several configs are within measurement noise of each other -- can settle on a DIFFERENT winner run-to-run"). It is also fairly direct evidence that the 4-bit tuner wants the clock-settle phase u3/u2 already have. I have not added it, since that is a behaviour change and belongs in its own PR, but I am happy to follow up.
Open question for reviewers
g_dq_bufandg_dq_buf_elemsare mutated here outside any mutex. The autotune entry points are serialised by their per-variant tune mutex, butensureDqBuffer()is also reachable from the dispatch paths. If two streams can dispatch concurrently this was already racy before this PR, and the fix does not make it worse — but it would be good to have someone who knows the threading model confirm whether that is actually reachable.