[MLAS] Add AVX2 (+VNNI) 2-bit weight CPU kernels#29619
Conversation
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Pull request overview
This PR extends MLAS’s SQNBitGemm (2-bit weight, CompInt8) CPU backend with native AVX2 and AVX2-VNNI compute kernels (BlkLen 32/64/128), wiring them into the AVX2/AVX2-VNNI dispatch tables and adding direct-kernel unit tests to cover both the VNNI and non-VNNI dot-product paths.
Changes:
- Adds AVX2/AVX2-VNNI W2 (2-bit) CompInt8 kernels for BlkLen 32/64/128 (header-only compute implementations).
- Wires W2 entries into
MlasSQNBitGemmDispatchAvx2andMlasSQNBitGemmDispatchAvx2vnni, including BlkLen-routing forwarders. - Adds new unit tests to directly exercise the AVX2 and AVX2-VNNI W2 kernels (with/without zero points) using the existing test harness.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| onnxruntime/test/mlas/unittest/test_sqnbitgemm_2bit_gemm.cpp | Adds AVX2/AVX2-VNNI direct-kernel tests for W2 across BlkLen 32/64/128, with/without zero points. |
| onnxruntime/core/mlas/lib/sqnbitgemm_kernel_avx2.cpp | Includes new W2 AVX2 headers, adds BlkLen-routing forwarders, and populates W2 function pointers in AVX2/AVX2-VNNI dispatch tables. |
| onnxruntime/core/mlas/lib/sqnbitgemm_kernel_avx2_2bit.h | Declares the AVX2/AVX2-VNNI W2 dispatch forwarders for use by dispatch tables and unit tests. |
| onnxruntime/core/mlas/lib/sqnbitgemm_kernel_avx2_2bit_blklen32.h | Implements the AVX2/AVX2-VNNI W2 kernel for BlkLen=32 (R1xC4 and R2xC4 tiles + tails + BlkSum correction). |
| onnxruntime/core/mlas/lib/sqnbitgemm_kernel_avx2_2bit_blklen64.h | Implements the AVX2/AVX2-VNNI W2 kernel for BlkLen=64 (R1xC4 and R2xC4 tiles + tails + BlkSum correction). |
| onnxruntime/core/mlas/lib/sqnbitgemm_kernel_avx2_2bit_blklen128.h | Implements the AVX2/AVX2-VNNI W2 kernel for BlkLen=128 (R1xC4 tile + tails + BlkSum correction). |
| # Needed because this helper runs at model load on AVX-512-only | ||
| # (non-VNNI) hosts via the AVX-512 W2 dispatch. TU is pure C++ -- no | ||
| # AVX-512 intrinsics inside. | ||
| # Build the W2 scalar oracle / pack-helper TU without any AVX-512 |
There was a problem hiding this comment.
Why is this change needed ? The previous state had it to just strip away avx512 vnni flags so that auto-vectorization (if the compiler fdoes do it) didn't end up using avx512 vnni but we can use avx512 ? Or does the code there get dispatched on avx2 only hosts with this change and hence necessaitates avx512 flags strip off ?
There was a problem hiding this comment.
Yes, the latter. With this PR the AVX2 and AVX2-VNNI dispatch tables point at the pack helpers in this TU, so it now runs at model load on hosts with no AVX-512 at all. The first CI round caught exactly that: the three Linux GPU pipelines run on AVX2-only CPUs, and onnxruntime_provider_test died with an illegal instruction there, since with -mavx512bw/dq/vl still on the file the compiler is free to autovectorize the scalar pack loops with EVEX encodings. It is the same failure mode the existing -mavx512vnni strip was guarding against, one ISA level down.
The TU is intrinsic-free scalar and the pack is one-time load work, so baseline codegen should not cost anything measurable, and ARM64 already builds this same file with no x86 flags. If you would rather keep avx512 codegen for the avx512 dispatch path, I can give the avx2 side its own copy in a separate TU instead, stripping the flags on the shared file just seemed like the smaller change. The second commit should turn those three jobs green once CI reruns.
| #include "sqnbitgemm_kernel_avx2_2bit.h" | ||
| #include "sqnbitgemm_kernel_avx2_2bit_blklen32.h" | ||
| #include "sqnbitgemm_kernel_avx2_2bit_blklen64.h" | ||
| #include "sqnbitgemm_kernel_avx2_2bit_blklen128.h" | ||
|
|
Review: PR #29619 — [MLAS] Add AVX2 (+VNNI) 2-bit weight CPU kernelsApprove pending CI green on commit 2d86379. Well-scoped, well-documented PR that closes the W2 gap on AVX2 / AVX2-VNNI hosts (Alder Lake through Arrow Lake, Zen 1–3 on the AVX2 path) — the same gap that #29064 closed on AVX-512 and #29466 closed on ARM64. Design mirrors the AVX-512 sibling exactly: no packed-layout change, no operator change, no A-quantization change ( What looks correct
Non-blocking observations
CIAuthor noted first commit reached 83/86 checks OK (three failures on AVX2-only Linux GPU pipelines from the AVX-512 flag issue). Second commit (2d86379) currently 1/68 — CI is still running. Once the second-commit CI turns green, no other blockers from my side. If any of those 68 still fail on 2d86379, please share the specific failure and I'll re-review. SummaryClean, focused MLAS kernel PR that closes a real gap for the AVX2/AVX-VNNI-only client x86 audience. Design mirrors the AVX-512 sibling exactly, test coverage is thorough (24 new tests, non-VNNI variants deliberately also run on AVX-512 hosts to exercise the maddubs fallback), and the CI-caught cmake fix is well-reasoned. Approve once commit 2d86379's CI is green; non-blocking items (comment on the shared TU, factor the duplicated dispatch setup, |
|
Thanks for the contribution. Could you please take a look at the lone copilot comment so that we can merge it ? :) If optionally, you'd like to address the non-blocking comments above, that would be nice as well. |
|
Pushed the fixes. The copilot comment is addressed, sqnbitgemm_kernel_avx2.cpp now includes sqnbitgemm_kernel_avx512_2bit.h directly instead of leaning on the transitive include. From the non-blocking list I also picked up 1, 2 and 5: the cmake note above the flag strip (used your wording, thanks), the effective-BlockCountK one-liner is now a constexpr helper next to kBlockGroupBlks in sqnbitgemm_kernel_avx512_2bit.h with all four dispatch tables referencing it, and there is a comment above the GCC guard in each kernel header now. I left 3, 6 and 7 alone on purpose: the two table blocks mirror how sqnbitgemm_kernel_avx512.cpp lays out its own pair, the asserts match the AVX-512 sibling's, and the BiasPtr ternary is the idiom the existing int8 blklen kernels use, so changing those here felt like drift from the files this one is modeled on. Happy to revisit any of them if you disagree. On perf: grabbed AVX2-only (maddubs path) numbers on the same host. Prefill kernel efficiency with the shipped tile config, single thread:
Even the maddubs path sits at or under the fp32 SGEMM ceiling on this core (2x256-bit FMA = 16 MAC/cycle = 0.0625 c/MAC at perfect utilization) before you count the dequant pass itself or the 16x weight bytes the fp32 path has to stream, so Zen 1-3 class hosts should still come out well ahead of dequant+SGEMM, and decode is the lopsided case since it is bandwidth bound. An end-to-end model comparison is something I would like to do as a follow-up once I have a proper W2 model set up locally. |
| QuantBDataColPtr += GroupStrideBytes; | ||
| QuantBScaleColPtr += GroupStrideScale; | ||
| BiasPtr += BiasPtr != nullptr ? kNCols4 : 0; | ||
| SumPtr += kNCols4; |
There was a problem hiding this comment.
Adding 0 to a null pointer is well defined in C++, [expr.add]/4 makes it a null pointer; it is the C rule that makes it UB. This is also the exact BiasPtr idiom the AVX-512 sibling and the existing int8 blklen kernels use (sqnbitgemm_kernel_avx512_2bit_blklen32.h does the same thing), so I kept it for consistency. Same call as non-blocking item 7 in the review above.
| QuantBDataColPtr += GroupStrideBytes; | ||
| QuantBScaleColPtr += GroupStrideScale; | ||
| BiasPtr += BiasPtr != nullptr ? kNCols4 : 0; | ||
| SumPtr += kNCols4; |
| QuantBDataColPtr += GroupStrideBytes; | ||
| QuantBScaleColPtr += GroupStrideScale; | ||
| BiasPtr += BiasPtr != nullptr ? kNCols4 : 0; | ||
| SumPtr += kNCols4; |
| QuantBDataColPtr += GroupStrideBytes; | ||
| QuantBScaleColPtr += GroupStrideScale; | ||
| BiasPtr += BiasPtr != nullptr ? kNCols4 : 0; | ||
| SumPtr += kNCols4; |
| QuantBDataColPtr += GroupStrideBytes; | ||
| QuantBScaleColPtr += GroupStrideScale; | ||
| BiasPtr += BiasPtr != nullptr ? kNCols4 : 0; | ||
| SumPtr += kNCols4; |
Description
This adds native (non-LUT) 2-bit weight CompInt8 kernels to MLAS for AVX2 and AVX-VNNI, BlkLen 32/64/128, modeled on the AVX-512 kernels from #29064 and the ARM64 kernels from #29466. On these hosts a W2 MatMulNBits currently has the LUT path (opt-in, and only when N is a multiple of 128) or the fp32 dequant + SGEMM fallback.
The packed layout is untouched. The block-group packer, scale layout and BlkSum machinery in sqnbitgemm_kernel_avx512_2bit.{h,cpp} are portable scalar C++ and the ARM64 kernels already reuse them, so this only adds the 256-bit compute kernels and the dispatch wiring. The four W2 entries are populated in MlasSQNBitGemmDispatchAvx2 and MlasSQNBitGemmDispatchAvx2vnni with BlkLen routing forwarders like the AVX-512 ones, and A quantization reuses the QuantizeARow_CompInt8_avx2 those tables already register. No cmake changes (the kernels are header-only and the avx2 source list already carries -mavxvnni where the compiler supports it) and no operator changes.
Per-node routing:
Tile shapes are per BlkLen and I picked them by measuring. BlkLen 32 and 64 use an R2xC4 main tile (one B block-group load and unpack shared across two rows) with an R1xC4 tail for an odd trailing row. BlkLen 128 stays R1xC4: the R2 variant measured 3 to 5% slower there, which tracks with register pressure, since a 128-byte group needs four B registers live plus eight accumulators and that does not fit sixteen YMM. M=1 always takes the R1 path, so decode is unaffected by the tiling choice either way.
Hosts with AVX2 but no AVX-VNNI use the vpmaddubsw + vpmaddwd fallback, guarded the same way as the existing int8 kernels.
Testing:
Motivation and Context
#29064 closed this gap on AVX-512 and #29466 on ARM64, but AVX2/AVX-VNNI without AVX-512 covers most client x86 (Alder Lake through Arrow Lake, plus Zen 1-3 on the plain AVX2 path) and those hosts still land on dequant + SGEMM by default.
Kernel-level numbers from a Core Ultra 5 225 (Arrow Lake, AVX2 + AVX-VNNI, no AVX-512), single thread, interleaved arms, min of 9 rounds on rdtsc:
These are tile-level microbenchmarks, not end-to-end model numbers; my dev box has no MSVC so my validation is kernel-level and the MSVC build rides on CI. Happy to run whatever end-to-end comparison you want on top of this, and happy to restructure the tiles if you would rather keep all three BlkLens on the same shape.