[MLAS] Route ARM64 S8U8 QGEMM to the UDOT kernel (#29787)
### Description
ARM64 quantized GEMM never routed the signed-A/unsigned-B (S8U8)
combination to a SIMD dispatch — `MLAS_PLATFORM` didn't even declare a
`GemmS8U8Dispatch` member on ARM64, unlike x86_64 (AVX2/VNNI),
LoongArch64 (LSX), and RISC-V64, which all have one. It fell through to
the generic scalar reference kernel.
This reuses the existing UDOT kernel (already shared by U8U8 and U8S8)
instead of adding new assembly. UDOT only does unsigned x unsigned, so B
is already shifted into the unsigned domain with an XOR 0x80 sign flip
when signed; this does the same for A (`MlasGemmQuantFixupZeroPointA`,
`MlasGemmQuantCopyPackA`). K-padding bytes are left untouched across all
four row-count paths (8/4/2/1-row) so they keep contributing zero to the
dot product and `RowSumBuffer`. `platform.cpp` initializes the new
dispatch member to the scalar default and only overrides it to UDOT
inside the existing dot-product-support check, so cores without
dot-product stay on the scalar path exactly as before.
Also parameterized `bench_qgemm.cpp` to cover S8U8, and added a guard
around the packed-B benchmark: `MlasGemmPackBSize` returns 0 for
combinations the active dispatch doesn't support packing for, and
calling `MlasGemmPackB` anyway was dereferencing a null
`CopyPackBRoutine`.
### Motivation and Context
`QLinearMatMul`, `MatMulInteger`, and `QLinearConv` register their
signed-A kernels with `T2` accepting both `int8` and `uint8`, so S8U8 is
reachable from real models, not just a synthetic case. Existing
`test_qgemm.cpp` correctness tests already cover `int8_t`/`uint8_t`,
they were just exercising the scalar fallback on ARM64.
Tested on Apple M1 (macOS) and Ampere Neoverse-N1 (Ubuntu), both with
NEON dot-product:
- Existing MLAS qgemm suites (S8U8/U8U8/U8S8/S8S8) pass on both, no
regressions.
- Benchmarked `bench_qgemm` (NoPackB, real_time) before/after by
temporarily forcing the scalar dispatch for comparison:
| Shape (M/N/K, threads) | M1 | Neoverse-N1 |
|---|---|---|
| 1/512/512, 1 | 149µs → 29µs | 152µs → 51µs |
| 1/1024/1024, 1 | 610µs → 202µs | 1544µs → 240µs |
| 384/1024/1024, 4 | 4.96ms → 3.98ms | 12.3ms → 1.34ms |
| 384/1024/3072, 4 | 20.7ms → 4.68ms | 37.2ms → 4.17ms |
| 1536/1024/4096, 16 | 124.6ms → 37.6ms | 201.6ms → 37.4ms |
| 3072/4096/1024, 16 | 135.4ms → 53.4ms | 388.8ms → 69.2ms |
Packed-B for S8U8 works correctly on both machines now that they're on
the UDOT dispatch. Without the guard mentioned above, the new packed-B
benchmark case would crash on any target that still resolves to the
scalar dispatch, since `MlasGemmPackBSize` returns 0 there and
`MlasGemmPackB` would call a null `CopyPackBRoutine`.
ARM64 cores without dot-product support are unaffected by the dispatch
change — they stay on `MlasGemmQuantDispatchDefault` (scalar) exactly as
before, `MlasGemmPackBSize` still returns 0 for S8U8 there, and the
benchmark guard keeps that case skipping cleanly instead of crashing.
This PR doesn't add an optimized S8U8 path or packed-B support for those
cores; that's left for a follow-up if there's interest.
The routing change in `qgemm.h` lives inside the `#if
defined(MLAS_TARGET_ARM64)` guard, so native Windows ARM64 (MSVC,
`MLAS_TARGET_ARM64`) picks up the same S8U8 → UDOT routing as
macOS/Linux ARM64 — it wasn't separately benchmarked here. ARM64EC
(`MLAS_TARGET_ARM64EC`) and 32-bit ARM stay on their existing logic,
unaffected.