Skip to content

Add full-range zero-point CompInt8 SQNBit tests; harden AVX2 M=1 kernels - #29886

Open
tianleiwu with Copilot wants to merge 2 commits into
mainfrom
copilot/fix-sqnbit-gemm-asymmetric-weights
Open

Add full-range zero-point CompInt8 SQNBit tests; harden AVX2 M=1 kernels#29886
tianleiwu with Copilot wants to merge 2 commits into
mainfrom
copilot/fix-sqnbit-gemm-asymmetric-weights

Conversation

Copilot AI commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Description

Adds regression coverage for asymmetric (zero-point) 4-bit CompInt8 SQNBit GEMM and fixes two latent defects in the AVX2 M=1 kernels.

Tests (test_sqnbitgemm.cpp)

  • TestFullRangeZeroPointCompInt8 + SQNBitGemmFullRangeZeroPointShortExecuteTest (180 tests across BlkLen 16/32/64/128/256). Builds explicit quantized B, scales, and zero points spanning the full 0..15 range (deterministic LCG), packs via MlasQNBitGemmPackQuantBData, and compares MlasQNBitGemmBatch against an exact double-precision integer reference.
  • Shapes target the AVX2 M=1 code paths: the 4-column block loop (Q4Int8GemmM1C4*) and its 1–3 column remainder (Q4Int8GemmM1C1*), even/odd BlockCountK, partial trailing K block, plus a few M>1 rows for cross-check.
  • Uses a magnitude-aware tolerance rather than the harness's plain 0.5% relative CloseEnough — see Motivation.

Kernel hardening

  • SQ4BitGemmM1Kernel_CompInt8_avx2 had silently-empty if (BlkLen == 16) { } branches in both the zero-point and non-zero-point arms, leaving C uninitialized. Replaced with an explicit precondition:
    // BlkLen < 32 is not implemented here. Callers must route it to the general
    // (block sum based) kernel instead, otherwise C would be left uninitialized.
    assert(BlkLen >= 32);
  • MlasQ4Int8GemmM1KernelBlkLen32Avx2 / MlasQ4Int8GemmKernelBlkLen64Avx2 performed QuantBZeroPoint + multipleCols * StrideQuantBZeroPoint on the remainder-column path even when QuantBZeroPoint == nullptr (UB, UBSan-visible). Now guarded.

Motivation and Context

The linked issue reports ~46% error from SQ4BitGemmM1Kernel_CompInt8_avx2 for asymmetric 4-bit weights on AVX2-only hosts, reproduced under Intel SDE -hsw.

The numeric defect did not reproduce on native AVX2 hardware. Verification ran on an AMD EPYC 7763 (avx2, no avx512f, no avx_vnni — the exact MlasSQNBitGemmDispatchAvx2/vnni=false path), with the suspect kernel confirmed to dispatch via temporary instrumentation:

  • existing suite (13,520 tests) — pass
  • 14,600-shape sweep of M=1 asymmetric CompInt8 (N 1..132 × 17 K values × 5 BlkLens), including the reported M=1, N=96, K=256, BlkLen=32 — pass
  • 1,296-config standalone fuzz harness with explicit full-range zero points against an exact integer reference — pass

Hand audit of the named suspects also came up clean: get_zp nibble selection and strides are consistent; _mm256_maddubs_epi16(|b|, sign(a,b)) cannot saturate (|b| ≤ 15, |a| ≤ 127); and _mm256_sign_epi8 would only mis-negate a = -128, which QuantizeARow_CompInt8_avx2 cannot produce (scale = maxAbs/127). Note that accumulate_mul_sum_avx2 / load_and_mul_sum_s8_quads_with_zp_avx2 are reachable only from dead code (SQ4BitGemmM1Kernel_BlkLen32_CompInt8_Impl2, hardcoded HasZeroPoint = false).

Forcing extreme zero points does produce failures, but they are not a kernel bug: the M>1/AVX-512 path applies zero points through the ABlockSum · QuantBBlkSum correction (QuantBBlkSum = -scale_b * zp), which cancels catastrophically in fp32 when zp is far from the data's natural value. Evidence: zp=0 (no correction term) → 0 failures, zp=10 → 828 failures, and failures also appear under CompFp32, which shares no AVX2 kernel. The M=1 path subtracts the zero point inline and is numerically better, so it is deliberately left in place rather than rerouted through block sums.

What the issue did expose is a real coverage gap: MlasQuantizeBlockwise on the random test inputs always derives zero points near 8, so the full 0..15 range — and therefore the asymmetric dequantization — was effectively unverified. The new tests close that gap and were validated by fault injection into the AVX2 M=1 zero-point kernel (wrong nibble select → 25 failures; hardcoded zp=8 → 25 failures).

Intel SDE was unavailable in this environment. If the reporter can share the failing caller, it is worth checking whether the kernel was invoked directly with unpacked B or un-rearranged scales rather than through MlasQNBitGemmBatch, which overwrites QuantBScale/PackedQuantBData/QuantBBlkSum from the packed workspace.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI changed the title [WIP] Fix MLAS AVX2 M=1 CompInt8 SQNBit GEMM for asymmetric weights Add full-range zero-point CompInt8 SQNBit tests; harden AVX2 M=1 kernels Jul 26, 2026
Copilot AI requested a review from tianleiwu July 26, 2026 20:58
@tianleiwu
tianleiwu marked this pull request as ready for review July 26, 2026 22:50
@blazingphoenix7

Copy link
Copy Markdown
Contributor

I ran this PR's tests plus an independent verification harness on the configurations this thread has not been able to reach. Machine is an Arrow Lake (AVX2 + AVX-VNNI, no AVX-512), which natively executes both AVX2 dispatch tables: MlasSQNBitGemmDispatchAvx2 (the vnni=false template SDE -hsw exercises) runs fine on it since the instantiation only uses AVX2 instructions, and MlasSQNBitGemmDispatchAvx2vnni (vnni=true) is its actual native dispatch, which as far as I can tell nobody had run against full range zero points before, including the EPYC run in the PR description.

Results, all at head with this PR applied:

  • The 180 new FullRangeZeroPoint tests pass natively with the AVX2-VNNI table dispatched (first coverage of vnni=true).
  • Under WSL2 plus Intel SDE -hsw (the issue's environment: dispatch selects the plain AVX2 table, so vnni=false), the full 4 bit CompInt8 suite plus the new tests pass 13,701 of 13,701, the only skips being the three ARM KleidiAI cases. Same result under -adl.
  • An independent probe that calls SQ4BitGemmKernel_BlkSum_CompInt8 directly out of both dispatch tables with full range random zero points, against an exact integer reference (per block int8 A quantization replicated bit for bit, dots in int64, accumulation in double): 12 configurations covering the issue's minimal shape, BlkLen 32/64/128, N remainders 1/2/3/7, odd block counts, a partial tail block (K=129), bias on and off, and M=2/M=11 controls. Max error across every properly fed path is 3.2e-06. vnni=false and vnni=true agree with the reference and with each other.

So I do not think there is a kernel bug here, and I can offer a concrete candidate for where #29853's numbers came from. The packed buffer produced by MlasQNBitGemmPackQuantBData re-arranges the per block scales into the 4 column interleaved order the CompInt8 kernels consume (ComputePackBlkSum in sqnbitgemm_kernel_avx_common.h), and MlasQNBitGemmBatch swaps DataParams->QuantBScale to that re-arranged array before the kernel ever sees it. A harness that calls the kernel directly and hands it the original column major scales gets exactly the reported failure signature: at the issue's minimal shape (M=1, N=96, K=256, BlkLen=32, asymmetric) my probe measures max abs error 5.09 against a reported 5.19, and across the other shapes the same misfeed produces relative errors from 12 to 437 percent, bracketing the reported ~46. Consistent with that, the functions the issue names as suspects (accumulate_mul_sum_avx2, load_and_mul_sum_s8_quads_with_zp_avx2, get_zp) sit under SQ4BitGemmM1Kernel_BlkLen32_CompInt8_Impl2, which has no callers anywhere in the tree; the live M=1 path is MlasQ4Int8GemmM1KernelBlkLen32Avx2 into accumulate_blklen32_r1c1blk2_zp_avx2. A repro driving internals directly would explain both the numbers and the suspect list.

On the changes themselves, three notes:

  1. The new tests are the real value in this PR and I would keep them regardless: the existing suite derives zero points from MlasQuantizeBlockwise over gaussian data, which lands on 8 for essentially every block, so the asymmetric kernels were effectively untested until now. Worth merging for that alone.
  2. The assert(BlkLen >= 32) is fine as documentation but the rationale overstates it: both dispatch call sites already gate BlkLen >= 32 && CountM == 1 (sqnbitgemm_kernel_avx2.cpp lines 577 and 715), so the removed empty BlkLen == 16 arms were unreachable through dispatch and C could not have been left uninitialized that way.
  3. For the same reason, the BlkLen=16 FullRangeZeroPoint registrations run the general (block sum) kernel, not the M=1 kernel, so the PR body's claim of M=1 kernel coverage across BlkLen 16 through 256 is off by one entry. The BlkLen 32/64/128 registrations are the ones that matter for this issue, plus 256 which also routes to the blklen64 kernel.

The QuantBZeroPoint ? ... : nullptr changes are correct hygiene (the old code formed nullptr + offset, which is UB even though the templated callee never dereferences it in the symmetric instantiation).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MLAS AVX2 M=1 CompInt8 SQNBit GEMM produces wrong results for asymmetric (zero-point) 4-bit weights

3 participants