Repository navigation
perf: 2-bit MatMulNBits models end to end on Strix Halo (u2 fp32 I/O, fp32-KV flash prefill, Q/DQ chain fusions) - #1318
Draft
penglu12-quark wants to merge 4 commits into
Draft
penglu12-quark wants to merge 4 commits into
penglu12-quark wants to merge 4 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 |
Kernel Unit Test Results
Leaves: 16 (0 failed) | Cases: 289 (0 failed) Mode: lookup | Coverage: 1 | Run: 8120 - Commit: |
L2 Accuracy Results (EP vs CPU)
Threshold: 0.01 | Run: 8120 - Commit: |
Binary sizesBaseline: run 40919992229 -
MorphiZen EP Performance Results
EPContext Export Performance
EPContext Import Performance
OGA Benchmark Results
OGA Wheel Smoke (Python benchmark_e2e.py)
VLM Benchmark Results
TTFT / TPS vs mainBaseline: run 40919992229 -
Run: 8120 - Commit: |
…s=2 MatMulNBits - u2 GEMV reads fp32 A and writes fp32 C directly; the u2 WMMA path casts A to fp16 once (its tiles re-read A once per N block) and writes fp32 C from the epilogue. fp32 graphs no longer pay the A/C casts and fp16 scratch round trip on every call. - hip_matmul_nbits_ex takes a uniform zero point. The runtime detects an fp16 zero-point tensor whose values are all equal (Quark 2-bit: 1.5) once per buffer and passes it as a scalar, so the kernels never read the tensor. - u2 GEMV autotune times candidates against rotating weight copies (median of bursts) instead of cache-hot replays, which picked configs from glitch readings. - naive u2 kernel takes the default zero point instead of hard-coding 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Made-with: Cursor Co-authored-by: Cursor <cursoragent@cursor.com>
With fp32 activations over an fp32 past/present KV cache, prompt attention fell back to the decomposed path, which materializes fp32 score matrices quadratic in the prompt length. Split, RoPE and the cache append now stay fp32 (the cache holds exactly what the decomposed path writes, so decode is unaffected); only the attention itself runs on the fused fp16 flash prefill kernel, over roped Q and cache rows cast into fp16 scratch, with the output cast back. HIPDNN_EP_GQA_DISABLE_FP32_CACHE_FLASH_PREFILL=1 restores the decomposed path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Made-with: Cursor Co-authored-by: Cursor <cursoragent@cursor.com>
…fusions convert-onnx-to-hip sizes each DPS init from tensor.dim of the producer's result. Those reads count as uses, so single-use checks rejected every dynamically shaped Q/DQ chain (QMatMul, requantize-through-layout, SwiGLU). hip-fusion-transform now folds tensor.dim onto the producer's own init via the standard canonicalization, leaving only data consumers. Also let the layout requantize rewrite go through hip.gather, so an embedding lookup gathers rows of the quantized table instead of dequantizing the whole table first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Made-with: Cursor Co-authored-by: Cursor <cursoragent@cursor.com>
A second greedy phase of hip-fusion-transform rewrites activation Q/DQ chains that the first phase leaves behind (or forms): - hip.qswiglu: Q(gate), hip.qsigmoid, two hip.qmul, Q(up) and the trailing DQ become one f32 kernel that requantizes every intermediate to its UINT16 per-tensor code, bit-identical to the chain. Each consumer must read an intermediate with the scale/zero point its producer wrote. - hip.fake_quantize: a per-tensor f32 Q read only by DQs with the same scale and zero point (8/16-bit storage) becomes one kernel; the integer codes are never stored. The phase runs after the first so it cannot preempt a first-phase fusion that consumes the DQ. The SwiGLU shape trace now also walks through per-element Q/DQ/fake_quantize, and its shape helpers are shared with the quantized fusion. Adds the ops, bufferization models, HIP-to-LLVM lowering, real and mock runtime wrappers, kernels, LIT tests (fusion, lowering, e2e) and a numeric test. test_qdq_model.mlir now uses a different DQ scale so it keeps exercising the unfused Q and DQ lowerings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Made-with: Cursor Co-authored-by: Cursor <cursoragent@cursor.com>
penglu12-quark
force-pushed
the
perf/2bit-strix-halo
branch
from
October 30, 2026 02:14
cbeb5f2 to
2bda8fa
Compare
This branch has not been deployed
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
End-to-end speedup for 2-bit (
MatMulNBitsbits=2) phi4-14B on Strix Halo (gfx1151):main: decode is 2.7ΓÇô2.9x faster and time to first token 2.0ΓÇô3.5x shorter. This uses a last-tokenlm_head/ fp16 KV re-export of the model. On the unmodified export, commits 2+3 alone give 1.5ΓÇô2.3x decode and 1.4ΓÇô2.3x time to first token.The PR has four commits, each independently reviewable:
perf(kernels): bits=2 MatMulNBits. fp32 activations and output are handled natively, a uniform fp16 zero point becomes a scalar, and GEMV autotune uses cold timing.perf(gqa): fp32 KV cache prefill. Attention runs on the fp16 flash prefill kernel instead of the decomposed fp32 path, which is quadratic in prompt length.fix(hip-fusion):tensor.dimuses. Shape-onlytensor.dimreads no longer block the existing Q/DQ fusions (QMatMul forlm_head, SwiGLU, layout requantize). Requantize also passes throughhip.gather, for embedding lookups.feat(hip-fusion): new fused ops.hip.qswiglu(quantized SwiGLU chain) andhip.fake_quantize(Q→DQ round trip), each exactly equal to the unfused chain.Related issue or design
None.
Why
Profiling the Quark 2-bit phi4 export showed it running slower than 4-bit on
main, for reasons unrelated to 2-bit math:convert-onnx-to-hipsizes each DPS init fromtensor.dimof the producer, and those reads counted as uses. As a result, every token dequantized the full 2 GBlm_headand embedding table to fp32 and ran the quantized SwiGLU unfused (commit 3).1.5, cost as many bytes per token as the scales. The u2 GEMV autotuner timed cache-hot replays (about 1 TB/s readings) and kept glitch winners (commit 1).What
hip_matmul_nbits_ex(..., float uniform_zp). The runtime detects an all-equal fp16 zero-point tensor once per buffer (hip_matmul_nbits_fp16_uniform_value, cached inMatmulNbitsState), and the kernels then never read it.GemvWeightRotation+timeConfigRotated, the median of cold bursts.docs/design/custom_kernel_design.md.[0, total_seq)are cast into fp16 scratch, run through the flash prefill kernel, and the output is cast back.HIPDNN_EP_GQA_DISABLE_FP32_CACHE_FLASH_PREFILL=1restores the old path.tensor::DimOpcanonicalization runs insidehip-fusion-transform's pattern set, folding shape reads onto the producer's own init.hip.gatheris added to the layout ops that requantize can pass through.hip-fusion-transform(qdq_chain_fusion.cpp). It runs after the first so it cannot preempt a first-phase fusion that consumes the DQ.docs/supported-operations.mdanddocs/pipeline_pass_menu.md.Performance
Hardware and method:
HIP_ARCHITECTURES=gfx1151.model_benchmark -l <prompt> -g 128 -r 3 -w 1, runs strictly serial, no other GPU or CPU load.HIPDNN_EP_PERF/DEBUG.Model: phi4-14B Quark QAD 2-bit export,
MatMulNBitsbits=2 with block 64, fp16 zero points (all 1.5), fp32 activations, UINT16 activation QDQ. The cumulative table uses the same export re-saved with a last-tokenlm_headslice (as the 4-bit baseline export already does) and an fp16 KV cache. Those re-exports are model-side and not part of this PR.Same 2-bit model,
mainvs this PR (commits 2+3), time to first token in seconds / decode tokens/s:mainCumulative, pruned
lm_head+ fp16 KV export of the same model (time to first token / decode tokens/s / prefill tokens/s):Overall, 2-bit
mainvs 2-bit with this PR (with the last-tokenlm_headand fp16 KV exports): time to first token is 2.0x / 2.3x / 3.5x shorter and decode is 2.9x / 2.8x / 2.7x faster at 128 / 2048 / 16256 tokens.Against 4-bit phi4 (
phi4-14B-int4-rtn-block-32_directml-pruned_lm_head):main4-bit regression check (same int4 model):
main: 0.143 / 2.041 / 19.77 s, decode 25.44 / 24.11 / 18.07.Mixed precision {2, 4}-bit: Qwen3.8-27B study
These kernels let one graph mix
bits=4andbits=2MatMulNBits node by node, with no per-config code in hip-ep. To measure the accuracy/latency trade-off, I ran a study on Qwen3.8-27B (int4 k_quant g128; 48 Gated DeltaNet + 16 GQA layers) on gfx1151. The study scripts are outside this PR; only the kernels it exercises are here.Method:
lm_headstays int4.sens*): for each unit alone at 2-bit, measured dNLL on 8k calibration tokens, then a greedy knapsack on dNLL/params up to the 2-bit budget.proxy47ranks units by weight reconstruction error;role47puts only mlp gate/up at 2-bit.gsm8k_cotprompt; "keep" = accuracy / int4 accuracy).model_benchmark -l 128/2048/16384 --use_random_tokens -g 128 -r 3 -w 1(OGA 0.17 / ORT 1.30), text-only prompts,HIPDNN_EP_STRICT=1and CPU fallback disabled. Run serially on this branch rebased ontoddb207c7.main(ddb207c7), decode tok/s @128 / 2048 / 16384: int4 14.67 / 14.49 / 13.59 (this PR within +-1%), sens47 16.37 / 15.88 / 15.17 (this PR +3-6%), all 2-bit 20.95 / 20.32 / 18.34 (this PR +5-7%).Limitations:
Test plan
ctest -R MorphizenMLIRLitTests. 675 discovered: 666 passed, 9 unsupported, 0 failed. New tests:qdq_chain.mlir,test_qswiglu.mlir,test_fake_quantize_model.mlir, plus new cases inqmatmul.mlir,qdq_round_trip.mlirandswiglu.mlir.hipdnn-kernel-ut-matmul-nbits-xfp32-wu2-yfp32and-xfp16-wu2-yfp16, 72/72 cases each.HIPDNN_EP_STRICT=1,--backend ort_ep):test_qdq_chain.py: 5/5 passed.test_gqa.py: 8 passed, 18 failed. Plainmainatddb207c7fails exactly the same 18 tests in repeated runs (shared-buffer KV limitation); all 4 new fp32-cache prefill cases pass. Those cases compare only the present-cache rows[0, total)that the kernel writes: with separate past/present buffers the tail is not written and holds whatever the allocator left.test_topk.py: 13/13 passed (main's tests; TopK itself is unchanged by this PR).pre-commit run --from-ref origin/main --to-ref HEADpasses (lintrunner, license headers, whitespace).Notes for reviewers
main: benchmarks were taken on the pre-rebase base (9d38afb9). After rebasing onto702a9481, the only conflict was adopting #639'sHip_DpsOp_AutoReifyInferfor the two new ops. The branch is now rebased ontoddb207c7. Two TopK commits briefly added here were dropped, because #1264 landed the samelargest/sortedconversion fix and a broader block-parallel TopK. LIT, unit and numeric tests above were rerun on theddb207c7rebase.ddb207c7(Qwen3.8-27B int4 k_quant, gfx1151,model_benchmark -l 128/2048/16384 --use_random_tokens -g 128 -r 3 -w 1, text-only): int4 decode is unchanged vsmain(14.57 / 14.53 / 13.65 vs 14.67 / 14.49 / 13.59 tokens/s). With 47% of linear weights re-quantized to 2-bit g64 it is +3-6% (17.15 / 16.90 / 15.67 vs 16.37 / 15.88 / 15.17), and with all linear weights at 2-bit +5-7% (21.92 / 21.64 / 19.68 vs 20.95 / 20.32 / 18.34). Time to first token for the 2-bit variants is 1-3% longer thanmain, which is close to run-to-run variation.wheeltarget fails because this machine's ROCm SDK has notensilelite-host.dll. This is environment-only; all compile and link targets build.tools/kernel_resources_*.jsonwere not regenerated for the renamed u2 instantiations.Checklist
AI assistance: the implementation, profiling, benchmarks and this description were produced with Cursor (Claude Opus 5.5) and validated with the LIT, kernel unit, numeric and full-model accuracy runs listed above. Commits carry
Co-Authored-By/Made-withtrailers.