Skip to content

Sync with Microsoft ONNX Runtime - 18072026#1205

Open
ai-fw-intg wants to merge 19 commits into
ovep-developfrom
sync_msft_18072026
Open

Sync with Microsoft ONNX Runtime - 18072026#1205
ai-fw-intg wants to merge 19 commits into
ovep-developfrom
sync_msft_18072026

Conversation

@ai-fw-intg

Copy link
Copy Markdown

Automated daily backmerge from ORT main to ovep-develop. No conflicts detected. Do NOT squash or rebase - use merge commit only.

Honry and others added 19 commits July 16, 2026 13:36
…icrosoft#29475)

For per-axis QuantizeLinear/DequantizeLinear, the scale and zero_point
tensors are 1-D. WebNN's quantizeLinear/dequantizeLinear requires them
to have the same rank as the input (it broadcasts per-dimension and does
not left-pad ranks). The reshape was skipped when the quantization axis
was the last dimension, leaving a rank-1 scale against a higher-rank
input and triggering "the rank of scale is not equal to the rank of
input".

Drop the `axis != input_rank - 1` guard so the reshape runs for all
per-axis cases; the existing math already yields the correct shape (e.g.
input [512, 1024], scale [1024], axis 1 -> scale [1, 1024]).
…e via cpuinfo (microsoft#29621)

On Linux/aarch64, `PosixEnv::GetL2CacheSize()` returns
`sysconf(_SC_LEVEL2_CACHE_SIZE)`, but glibc's aarch64 sysconf backend
does not implement the cache-size queries and returns 0. That makes the
`l2_cache_size_ > 0` gate in MultiHeadAttention always false, so the CPU
FlashAttention path is silently disabled and the kernel falls back to
materializing the full attention score tensor (multi-GiB at long
sequence lengths, and OOM on small ARM instances). GroupQueryAttention
divides by the same 0 and ends up with a degenerate tile size of 1.

This queries cpuinfo for the L2 cache size before falling back to
sysconf/sysctl, mirroring the existing `cpuinfo_available_` usage in the
same file for physical-core count and thread affinity. cpuinfo reads
sizes from sysfs cacheinfo and works on aarch64. The change is guarded
by `ORT_USE_CPUINFO` and only returns early on a positive result, so
x86-64 (working sysconf) and Windows (separate `WindowsEnv`
implementation) are unaffected.

Fixes microsoft#29613.
### Description

This updates the Windows BinSkim-compliant build flags so `/Qspectre`
builds also link against the MSVC Spectre-mitigated CRT/STL static
libraries. `/Qspectre` only affects ONNX Runtime's own object files;
BinSkim BA2024 can still report violations when the default non-Spectre
`libcmt.lib`, `libcpmt.lib`, or `libvcruntime.lib` are linked into the
final binary.

### Motivation and Context

Release validation reported BinSkim BA2024 (`EnableSpectreMitigations`)
warnings for `onnxruntime.dll` even when ORT was built with
`--use_binskim_compliant_compile_flags`. The warning identified MSVC
runtime and STL static libraries as the non-mitigated modules. This
change makes the build option select the Spectre-mitigated MSVC library
directory when it is available from the Visual Studio toolset.

### Key Changes

- Adds `get_msvc_spectre_lib_dir()` to locate
`%VCToolsInstallDir%\lib\spectre\<arch>` for the target Windows
architecture.
- Appends a quoted `/LIBPATH:<spectre-lib-dir>` linker flag whenever
Windows BinSkim flags enable `/Qspectre` and AddressSanitizer is not
enabled.
- Emits a warning when the Spectre-mitigated MSVC libraries cannot be
found, with guidance to install the Visual Studio "C++ Spectre-mitigated
libs" component.
- Preserves the existing ASAN behavior because ASAN libraries do not
have Spectre-mitigated variants.

### Testing

- `python3 -m ruff check tools/ci_build/build.py`
- `python3 -m ruff format --check tools/ci_build/build.py`

`lintrunner -a tools/ci_build/build.py` was also attempted. It found the
repository config and applied no file changes, but the local environment
could not execute the Ruff lintrunner adapters because `python` is not
available on PATH; the direct `python3 -m ruff` checks above passed.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
…#29255)

### Description

`GridSample` sizes its output from the grid, so an input with a
zero-size spatial dimension skips the empty-output early return and
reaches interpolation with an invalid spatial extent. This change
validates that every input spatial dimension is non-empty in both the
CPU and CUDA kernels, returning a clear error for such malformed inputs.

### Changes

- CPU `GridSample::Compute`: reject any zero-size input spatial
dimension with a descriptive error.
- CUDA `GridSample::ComputeInternal`: same validation, layout-aware for
NCHW and NHWC, covering both 4-D (H, W) and 5-D (D, H, W) inputs.
Messages are consistent across both EPs.
- Add an expect-failure test (zero-size spatial dim,
`padding_mode=border`) scoped to the CPU and CUDA execution providers,
which are the kernels that carry the guard.

### Motivation

Improves input validation and error diagnostics for malformed
`GridSample` inputs. Covers both the CPU and CUDA execution providers;
no behavior change for valid inputs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…18 / MLAS path) (microsoft#29629)

### Summary

Fixes wrong `AveragePool` output on the CPU EP when `ceil_mode=1`
**and** `count_include_pad=1` for **opset 7–18** (the float MLAS path).
This is the direct fix for the CPU-EP repro in pytorch/pytorch#183528.

### Root cause

`Pool<float, AveragePool>::Compute` routes float `AveragePool` opset
7–18 to MLAS. With `count_include_pad=1`, MLAS divides every window by
the **full kernel size**. Under `ceil_mode=1` the output grid gains a
trailing window whose extent runs past `input + pad_tail` (phantom
cells). MLAS's full-kernel divisor counts those phantom cells, producing
an average that is too small on the boundary windows.

The opset-19 reference functor (`AveragePool{1,2,3}DTask` in
`pool_functors.h`) already handles this correctly: it clamps the window
end to `input + pad_tail` and divides by `1 + (end - start -
1)/dilation`, excluding the phantom cells.

### Fix

Dispatch-fallback, **zero MLAS edits**:

1. Extract the opset-19 reference average-pool loop into a shared free
function `ComputeAveragePoolReference<T>(context, pool_attrs, tp)`
(reads strides from `PoolAttributes`, `p=0`), and make
`AveragePoolV19<T>::Compute` a thin wrapper over it — one canonical
loop, no copy-paste drift.
2. In `Pool<float, AveragePool>::Compute`, route **only** the buggy
combo `ceil_mode == 1 && count_include_pad && !global_pooling` to the
reference loop. Every other case (`ceil_mode=0`,
`count_include_pad=0`/exclude-pad, global pooling, all MaxPool) keeps
the fast MLAS path unchanged.

`global_pooling` is excluded because it has no ceil/pads (already
correct) and leaves `strides[]` unpopulated;
`ComputeAveragePoolReference` `ORT_ENFORCE`s that precondition.

### Perf

Zero impact on the common path. The reference loop runs only for
`ceil_mode=1 && count_include_pad=1` (rare); the guard is a cheap early
branch.

### Relationship to microsoft#16752

ORT PR microsoft#16752 fixed this behavior for **opset ≥ 19** only (via the new
v19 reference functor). Opset 7–18 float still went through MLAS and
remained wrong. This PR closes that gap by reusing the same
already-correct functor for the 7–18 dispatch fallback.

### Tests

Added to `pool_op_test.cc` (CPU-focused; GPU/other EPs excluded so CI
stays green):

- `AveragePool_18_ceil_count_include_pad_1d` — opset-18 clone of the
existing v19 test; the direct #183528 repro.
- `AveragePool_18_ceil_count_include_pad_2d` — the
`arange(1,17).reshape(1,1,4,4)`, k3/s2/pad1 case → `[1.556, 3.333, 2.0,
6.333, 11.0, 6.0, 4.5, 7.5, 4.0]`.
- `AveragePool_18_ceil_count_include_pad_3d` — exercises the 3D functor
path.
- `AveragePool_18_ceil_count_exclude_pad_2d` — `count_include_pad=0`
no-regression guard (stays on MLAS).

Expected values are derived from the CPU v19 reference (equivalently,
hand-computed per the ONNX spec). Full `PoolTest` suite passes locally.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#29687)

This pull request updates the build and CI configuration for
CUDA-related workflows and the main CMake options. The main changes are
the addition of new CMake build flags to enable CUDA quantization
preprocessing, improved formatting for build flags, and a change in the
default for the CUDA quant preprocess build option. These updates
improve clarity, make it easier to customize builds, and ensure that the
CUDA quant preprocess module is only built when explicitly requested.

**Build configuration changes:**

* Added the `--cmake_extra_defines
onnxruntime_BUILD_CUDA_QUANT_PREPROCESS=ON` flag to the CUDA build jobs
in `.github/workflows/linux_cuda_ci.yml` and
`.github/workflows/linux_cuda_plugin_ci.yml`, enabling the CUDA
quantization preprocessing module during CI builds.
[[1]](diffhunk://#diff-04806013a5e7991ba5606145885d9b0fcd99a7df1f3bb96a2d38fc724ccd9b2aL32-R46)
[[2]](diffhunk://#diff-04806013a5e7991ba5606145885d9b0fcd99a7df1f3bb96a2d38fc724ccd9b2aL114-R138)
[[3]](diffhunk://#diff-64cd92765a9461e73a80c6b0401fe1960170834725a7e8a2ea153aff6d8f8388R45)
* Added the `--cmake_extra_defines onnxruntime_QUICK_BUILD=ON` and
`--cmake_extra_defines onnxruntime_USE_FPA_INTB_GEMM=OFF` flags to the
CUDA no-cudnn build job for faster builds and to disable a specific GEMM
implementation.
[[1]](diffhunk://#diff-4e310144ab53bd9b6e48c7ceba29ad2c310724645278a28b85f7ba3a453c4980L35-R47)
[[2]](diffhunk://#diff-04806013a5e7991ba5606145885d9b0fcd99a7df1f3bb96a2d38fc724ccd9b2aL114-R138)

**Formatting and maintainability:**

* Reformatted long `extra_build_flags` strings in workflow YAML files to
use multi-line lists for improved readability and easier maintenance.
[[1]](diffhunk://#diff-04806013a5e7991ba5606145885d9b0fcd99a7df1f3bb96a2d38fc724ccd9b2aL32-R46)
[[2]](diffhunk://#diff-04806013a5e7991ba5606145885d9b0fcd99a7df1f3bb96a2d38fc724ccd9b2aL114-R138)
[[3]](diffhunk://#diff-4e310144ab53bd9b6e48c7ceba29ad2c310724645278a28b85f7ba3a453c4980L35-R47)

**CMake option default change:**

* Changed the default value of the
`onnxruntime_BUILD_CUDA_QUANT_PREPROCESS` CMake option from `ON` to
`OFF` in `cmake/CMakeLists.txt`, so the CUDA quantization preprocessing
module is only built when explicitly enabled.
…ft#29658)

### Description

When ONNX Runtime is built with the CUDA execution provider as a plugin
(`onnxruntime_BUILD_CUDA_EP_AS_PLUGIN=ON`), the EP-API op-kernel adapter
(`ep::adapter::KernelImpl::PrePackWeightImpl`) received a valid
`OrtAllocator*`
from the framework but discarded it and forwarded a **null**
`AllocatorPtr{}`
into the wrapped kernel's `PrePack()`. Any CUDA kernel that pre-packs a
constant
weight (e.g. `MatMulNBits`, `Conv`, `GroupQueryAttention`, quantized
MoE) then
allocated scratch through that null allocator and crashed during session
initialization:

```
IAllocator::ValidateAllocator(const T&) [with T = std::shared_ptr<onnxruntime::IAllocator>]
allocator != nullptr was false
```

This surfaced end to end as an ONNX Runtime GenAI `og.Model(...)`
failure on a
gpt-oss-20b (`MatMulNBits`) model when the CUDA EP was loaded as a
plugin: the
trivial init session succeeded, but the first real model session crashed
while
pre-packing quantized weights.

### Key Changes

| File | Change |
|---|---|
| `include/onnxruntime/ep/adapter/op_kernel.h` | `PrePackWeightImpl` now
wraps the incoming `OrtAllocator*` and forwards a valid `AllocatorPtr`
to `OpKernel::PrePack` instead of a null `AllocatorPtr{}`. |
| `include/onnxruntime/ep/adapter/allocator.h` | Add a **non-owning**
`IAllocatorWrappingOrtAllocator(OrtAllocator*)` constructor. The
framework owns the pre-pack allocator, so the wrapper must not take
ownership (an owning `Ort::Allocator` would release it on destruction).
Calls now go through the raw `OrtAllocator*` function pointers directly,
preserving the `Reserve`→`Alloc` (version ≥ 18) and
`GetStats`/`AllocOnStream` (version ≥ 23) fallbacks. |
| `onnxruntime/test/python/transformers/test_cuda_plugin_ep.py` | Add
`test_registration_matmul_nbits_prepack`: builds a fp16 `MatMulNBits`
model with a runtime-prepacked (`weight_prepacked=0`) quantized weight,
so weight pre-packing (`MatMulNBits::PrePack_B` →
`IAllocator::MakeUniquePtr(alloc, ...)`) runs during session creation.
This crashed before the fix and now passes. |

### Motivation and Context

The pre-pack allocator is provided and owned by the framework for the
duration
of the `PrePack` call. The legacy (in-tree) CUDA EP received it
correctly; only
the plugin op-kernel adapter dropped it. The non-owning wrapper matches
the
lifetime contract used elsewhere in the adapter (e.g.
`KernelInfoGetAllocator`)
and keeps the CUDA-EP-as-plugin build behaviorally identical to the
in-tree EP.

### Testing

- New `test_registration_matmul_nbits_prepack` in
`test_cuda_plugin_ep.py`
passes on a CUDA-EP-as-plugin build (`ORT_TEST_CUDA_PLUGIN_EP=1`) and
skips
gracefully when the device lacks fpA_intB GEMM support. It re-raises
(fails)
  if the `allocator != nullptr` assertion recurs.
- Verified the model that originally reproduced the crash now creates
and runs
end to end through the CUDA plugin EP (gpt-oss-20b, `MatMulNBits`),
including a
  100-sample MMLU sanity run.
- Existing `test_cuda_plugin_ep.py` registration tests continue to pass.
Enable reshape fusion for models with symbolic (dynamic) dimensions,
covering patterns found in MobileNetV2, vision encoders, and SLMs.

Changes:
- Match_Shape: allow dimension matching by symbolic dim_param, not just
static values. Needed for SLMs (e.g. Qwen) where `position_ids` takes
`Shape` from `input_ids` — same `dim_param` names, no static values.
- Match_Shape: allow matching when only the gathered dimension agrees
between `Shape` source and reshape root. Needed for vision encoders
(e.g. moondream2) where `Shape` is taken from pre-projection tensor but
`Reshape` operates on post-projection output.
- Match_Shape: add `GlobalAveragePool` passthrough — accept `Shape` from
pre-pool input when gathering batch dim. Needed for MobileNetV2.
- ReshapeHelper: handle zero-dim shapes (`allow_zero=true`) by computing
unknown dim from non-zero dimension product only. Needed for MobileNetV2
reshape fusion output.
- Add corresponding unit tests.
### 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
microsoft#29064 and the ARM64 kernels from microsoft#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:

| Host | LUT mode | before | after |
|---|---|---|---|
| AVX2 / AVX-VNNI | off (default) | fp32 dequant + SGEMM | native W2
kernel |
| AVX2 / AVX-VNNI | on, N % 128 == 0 | LUT kernel | LUT kernel
(unchanged) |
| AVX2 / AVX-VNNI | on, N % 128 != 0 | fp32 dequant + SGEMM | native W2
kernel |

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:
- new direct-kernel tests in test_sqnbitgemm_2bit_gemm.cpp mirroring the
AVX-512 set, four per BlkLen, with and without zero points. The non-VNNI
variants guard on Avx2Supported_ so they also run on the AVX-512 CI
hosts and cover the maddubs path there; the VNNI variants gate on the
AVX2-VNNI dispatch being the active one.
- the existing MatMul2Bits operator tests exercise the new path
automatically on AVX2 hosts at accuracy_level 4.
- validated the kernels against a float64 reference over 120+ cases (N
tails, K tails, odd M, both dot paths) on packing produced by the
production 3-call pack sequence.

### Motivation and Context

microsoft#29064 closed this gap on AVX-512 and microsoft#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:

| BlkLen | cycles/MAC at prefill (shipped tile) | R2xC4 vs R1xC4 at
prefill |
|---|---|---|
| 32 | ~0.048 | R2 5 to 6% faster (shipped) |
| 64 | ~0.031 | R2 11 to 15% faster (shipped) |
| 128 | ~0.027 | R2 3 to 5% slower (kept R1) |

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.
…t naming (microsoft#28896)

### Description
This PR adds Windows ARM64 support to CUDA plugin packaging and updates
related build/packaging logic for correctness and consistency across
architectures.

In response to review feedback, it also:
- Corrects CMake comments to match actual Windows ARM64 CUDA toolkit
search behavior.
- Prioritizes architecture-specific cuDNN DLL search paths before
generic fallback paths.
- Aligns Windows ARM64 Python artifact naming with the ARM64 CUDA
toolkit version.
- Extends packaging metadata with per-platform CUDA version fields
(including win-arm64) and updates metadata-reading template
validation/exports accordingly.


### Motivation and Context
Windows ARM64 CUDA plugin packaging requires architecture-aware handling
for toolkit/cuDNN discovery and artifact metadata.
These updates prevent cross-architecture ambiguity (especially for CUDA
13.x win-arm64 vs x64), improve downstream artifact selection
reliability, and keep metadata semantics consistent with produced
artifacts.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
- Add an Intel-specific F16 MatMul path built on the 8x16x16
subgroup-matrix config (Xe2/Xe3). A and B are loaded directly from
global memory with no prepacking, and results are written through
workgroup scratch with bounds-checked stores, so M and N may be any size
while only K must be a multiple of 16. The impl is created in
MatMul::PrePackInternal and dispatched from Compute when the device
reports the required config.
- The tile shape and split-K factor are chosen per call rather than
fixed: TileM in {8,16,32,64}, TileN in {16,32,64}, and split-K to top up
occupancy, sized to the device's resident subgroups. The WGSL kernel is
parameterized by sg_mat_count_m/n and split_k, emitting the needed
subgroup-matrix accumulators via generation-time guards.
- An optional offline-tuned config table (generated from an autotuner
sweep) can override the heuristic; selection precedence is tuned table >
heuristic.
- Adds MatMul subgroup-matrix unit tests covering tile selections and
partial/large non-aligned dims.
…crosoft#28786)

### Description
Adds MLAS half-precision GEMM and convolution support for Arm64 KleidiAI
paths.

This change:
- Adds public MLAS HalfGemm backend-native packed-B APIs.
- Adds public MLAS HalfConv prepare, execute, and packed weights+bias
APIs.
- Wires KleidiAI HalfGemm and HalfConv overrides into the MLAS platform
dispatch.
- Adds SME/SME2 FP16 HGEMM and FP16 IMATMUL ukernel selection.
- Adds MLAS unit coverage for packed-B behavior, selector opt-out,
zero-K handling, HalfConv prepare behaviour, and varied HalfGemm
shapes/bias cases.

### Motivation and Context
This is the second MR in the FP16 split and introduces the MLAS API and
Arm64 backend plumbing needed for accelerated FP16 CPU kernels.

The later CPU operator changes can use these MLAS HalfGemm and HalfConv
entry points without carrying KleidiAI-specific details at the operator
layer. The backend selector support also preserves an explicit fallback
path when KleidiAI should not be used for a given call.

Note: This PR is the second offshoot from
[28487](microsoft#28487)

---------

Signed-off-by: Cathal Lawlor <cathal.lawlor@arm.com>
Signed-off-by: Jonathan Clohessy <Jonathan.Clohessy@arm.com>
Co-authored-by: Jonathan Clohessy <Jonathan.Clohessy@arm.com>
### Description
This patch updates Dawn to the latest version.


### Motivation and Context
WebGPU Immediate Data and "subgroup-size-control" extension will be
supported after the upgrade of Dawn.
### Description

Copies the Java setup step from sibling files `windows_webgpu.yml`,
`windows_x64_release_xnnpack.yml`,
`windows_x64_release_build_x64_release.yml` to `windows_qnn_x64.yml`.

This explicitly ensures a compatible Java version, rather than relying
on the JDK already installed on the self-hosted runner. Otherwise there
may be breakages when switching the runner.

### Motivation and Context

I believe this will fix the failures in CI for the Windows x64 QNN CI
Pipeline. I'm seeing it fail in both an unrelated PR:

microsoft#29728

https://github.com/microsoft/onnxruntime/actions/runs/29509983398/job/87780661649?pr=29728

and in commits to main:


https://github.com/microsoft/onnxruntime/actions/runs/29546572913/job/87780090142

https://github.com/microsoft/onnxruntime/actions/runs/29499730505/job/87625355397

This is assuming the chain of causation:

1. microsoft#29731 switches the
runner pool
2. The `windows_qnn_x64.yml` pipeline finds JDK 8 already on the runner
(previous runner pool had JDK 11)
3. The Spotless Gradle plugin v7.2.1 isn't compatible with JDK 8
4. onnxruntime Java project can't be configured
5. Build fails

This new workflow action hopefully installs a compatible JDK & all will
be well.

microsoft#29753
…r. (microsoft#29634)

### Description
<!-- Describe your changes. -->
Fix structure of EpLibraryPlugin::Load so that we call Unload on any
error and not just an exception.
Currently we directly return on error so the Unload is never hit.

### Motivation and Context
<!-- - Why is this change required? What problem does it solve?
- If it fixes an open issue, please link to the issue here. -->
Cleanup failed load fully.
…rosoft#29622)

## Enable fpA_intB GEMM in CUDA builds and add configurable options

### Summary

This PR turns the CUDA fpA_intB (weight-only, FP activation × int
weight) MatMulNBits path on by default in CUDA builds, replaces the
ambiguous `ORT_FPA_INTB_GEMM` bitmask with a simple on/off flag, and
adds session-config keys so the path and its autotuning buckets can be
controlled per session. It also makes the CUTLASS tactic profiler
CUDA-graph safe and configurable.

### Motivation

The fpA_intB kernels were previously gated behind
`onnxruntime_USE_FPA_INTB_GEMM=OFF` and an `ORT_FPA_INTB_GEMM` integer
bitmask (`0x01=all`, `0x02=GEMV`, `0x04=int4`, `0x08=int8`). The bitmask
was ambiguous (e.g. `6` could read as "int4 + GEMV" or "GEMV for int4
and int8") and the GEMM/GEMV kernels actually share one weight layout,
so splitting them was never valid. Shipping the kernels by default and
exposing a plain enable flag plus per-session config makes the feature
usable and tunable without rebuilding.

### Key Changes

#### Build enablement
| File | Change |
|------|--------|
| [cmake/CMakeLists.txt](cmake/CMakeLists.txt) |
`onnxruntime_USE_FPA_INTB_GEMM` becomes a `cmake_dependent_option`,
defaulting **ON** when `onnxruntime_USE_CUDA` is enabled (OFF
otherwise). |
|
[tools/ci_build/github/linux/build_cuda_c_api_package.sh](tools/ci_build/github/linux/build_cuda_c_api_package.sh),
[build_linux_python_package.sh](tools/ci_build/github/linux/build_linux_python_package.sh),
[build_tensorrt_c_api_package.sh](tools/ci_build/github/linux/build_tensorrt_c_api_package.sh)
| Flip packaging builds from `onnxruntime_USE_FPA_INTB_GEMM=OFF` to
`ON`. |

#### Option simplification (bitmask → boolean)
- Removed `kFpAIntBGemmOption_All/Gemv/Int4/Int8` and the old bitmask
parsing.
- Added `ParseFpAIntBEnabled`: `""` / `"0"` / `"off"` → disabled; any
other value → enabled (numeric non-zero still works for back-compat).
- The enable flag now only governs nodes **without** prepacked weights.
A prepacked weight is already stored in the fpA_intB layout, so the
choice was fixed at export time; the constructor forces the path on for
prepacked nodes and only `ORT_ENFORCE`s that the shape/hardware actually
support it.
- GEMV is no longer independently toggleable: it is enabled whenever
supported, since GEMM and GEMV share the same weight layout.

#### New session-config keys (EP-agnostic, config wins over env)
| Config key | Env fallback | Meaning |
|------------|--------------|---------|
| `ep.cuda.fpa_intb_gemm` | `ORT_FPA_INTB_GEMM` | Enable/disable the
fpA_intB path (`0`/`off` vs `1`/`on`). |
| `ep.cuda.fpa_intb_profile_m` | `ORT_FPA_INTB_PROFILE_M` |
Comma-separated initial profile-M buckets (e.g. `"1,8,64,512"`); empty
uses the default bucket set. |

These are read by both the built-in CUDA EP and the CUDA plugin EP via
`OpKernelInfo::GetConfigOptions()`.

#### Profiler: CUDA-graph-safe, in-memory autotuning
- Added `getBestConfigOrProfile()` for lazy single-bucket profiling
outside CUDA-graph capture; during capture the kernel falls back to a
pure lookup (`getBestConfig`) because profiling launches kernels,
records/synchronizes events, and allocates scratch — all illegal during
capture.
- Added configurable profile-M buckets: `ParseProfileMList`,
`setProfileMOverride`, `getProfileMBuckets`, plus `kEnvProfileM` and
`kDefaultProfileMaxM` (default max M lowered to `2048`).
- Clearer error when an M bucket was not profiled before capture ("run a
warmup inference outside capture first").

#### Docs
-
[docs/contrib_ops/cuda/matmul_nbits.md](docs/contrib_ops/cuda/matmul_nbits.md):
`ORT_FPA_INTB_GEMM` documented as int/string on/off; clarified
prepacked-weight strictness.

### Testing Notes
- New:
[onnxruntime/test/python/quantization/test_op_matmulnbits_prepacked_cuda.py](onnxruntime/test/python/quantization/test_op_matmulnbits_prepacked_cuda.py)
— exercises the prepacked fpA_intB path and the boolean/numeric
back-compat values of the enable flag.
- To verify locally (CUDA, SM ≥ 75):
- Build with CUDA (fpA_intB now defaults ON): `./build.sh --use_cuda
...`
- Run: `python -m pytest
onnxruntime/test/python/quantization/test_op_matmulnbits_prepacked_cuda.py`
- Sanity-check config override: set `ep.cuda.fpa_intb_gemm=0` on a
non-prepacked node and confirm the path is skipped; confirm a prepacked
node still forces the path on.
…soft#29537)

## Motivation

MLAS has hand-optimized QNBitGemm kernels for x86 (AVX2/AVX512), ARM
(NEON), and LoongArch, but none for RISC-V — on RISC-V the dispatch was
null, so MlasIsQNBitGemmAvailable returned false and MatMulNBits fell
back to the generic scalar path, leaving the RISC-V Vector unit idle for
the hottest kernel in quantized-LLM inference.

This PR adds a native RISC-V Vector (RVV) implementation of the
QNBitGemm dispatch so weight-quantized matmul runs on the vector unit.

## What's implemented

All compute modes the operator selects among, for rv64gcv:
- 4-bit weights × fp32 activations (SQNBIT_CompFp32) — M=1 GEMV +
dequant-to-SGEMM
- 4-bit weights × int8 activations (SQNBIT_CompInt8) — quantize-A +
int8×int4 kernel
- 8-bit weights × int8 activations (SQNBIT_CompInt8, BlkSum path)
- 4-/8-bit weights × fp16 activations (HQNBIT_CompFp16) — requires the
Zvfh extension (rv64gcv_zvfh, MLAS_USE_RVV_ZVFH)

plus the packing / quantization / workspace-sizing plumbing each mode
needs (including the 3-call PrePack protocol).

## Design notes

- The packed-B / block-sum / dequant-buffer layouts are private to this
dispatch (produced and consumed only by these kernels), so plain layouts
are used — natural for RVV's scalable vectors.
- The SQ8 prepack unit test asserts a per-arch B layout (#ifdef ARM64 /
#else x86); added a MLAS_TARGET_RISCV64 branch describing the RVV
layout, following the existing per-arch pattern.
- fp16 kernels accumulate in fp32 (fp16 → vfwcvt → fp32 FMA → fp16) for
accuracy.

## Files

New:
- onnxruntime/core/mlas/lib/riscv64/qnbitgemm_kernel_rvv.cpp — SQ4/SQ8
kernels, packing, dispatch object
- onnxruntime/core/mlas/lib/riscv64/hqnbitgemm_kernel_rvv.cpp — fp16
(Zvfh) dequant + GEMM
- onnxruntime/test/mlas/unittest/test_qnbitgemm_rvv_fp16.cpp — RISC-V
fp16 e2e test (HQ4 + HQ8)

Modified:
- cmake/onnxruntime_mlas.cmake — add the two sources (RVV / Zvfh source
lists +
- onnxruntime/core/mlas/lib/mlasi.h — extern decl of the dispatch
- onnxruntime/core/mlas/lib/platform.cpp — assign QNBitGemmDispatch on
RISC-V
- onnxruntime/test/mlas/unittest/test_sq8bitgemm.cpp — RISC-V layout
branch in the SQ8 prepack test

## Performance (RVV, VLEN=256, N=256, K=2048)

The K-reduction runs at LMUL=4; profiling showed the kernels are
widening-multip), so cutting per-chunk instruction overhead is the
lever.
- SQ4 CompInt8 (4-bit): ~3.4× vs the naive per-sub-block version (2.2 →
7.6 GOP/s)
- SQ8 CompInt8 (8-bit): ~1.2×
- fp16 GEMM tiled to reuse the B vfwcvt across rows


## Testing

Built and run on real RISC-V hardware (K3 board, rv64gcv_zvfh, VLEN=256)
via onnxruntime_mlas_test:
- SQNBitGemm* : SQ8Bit* : BlockQ4* : BlockQ8* : RvvFp16* → 13,526 /
13,526 pass
- Broader suite run passed with 0 failures as far as it ran (Activation
→ all QGemm); the run was only bounded by the very slow threaded
large-GEMM stress tests, unrelated to this change.

## Build

Enable fp16 with -Donnxruntime_USE_RVV_ZVFH=ON. The 4-/8-bit int8/fp32
paths build with the existing onnxruntime_USE_RVV.
Implement the MatMulBnb4 quantized matmul operator for the WebGPU
execution provider, supporting both FP4 (quant_type=0) and NF4
(quant_type=1) blockwise 4-bit weight dequantization.

### Description
<!-- Describe your changes. -->
Add WebGPU execution provider support for the MatMulBnb4 contrib
operator, enabling blockwise 4-bit quantized matmul (bitsandbytes-style
FP4 and NF4) to run on WebGPU.

### Motivation and Context
<!-- - Why is this change required? What problem does it solve?
- If it fixes an open issue, please link to the issue here. -->
Models quantized with bitsandbytes 4-bit weights fell back to CPU when
running on WebGPU, causing significant slowdowns, and cannot use graph
capture feature.
Adding a native WebGPU kernel keeps these quantized MatMulBnb4 on the
GPU, avoiding costly EP fallback and CPU/GPU data transfers.
marqo-fashionSigLIP-text-bnb4: ~30% faster
marqo-fashionSigLIP-vision-bnb4: ~20% faster
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.