Skip to content

Sync with Microsoft ONNX Runtime - 07082026 - #1250

Open
ai-fw-intg wants to merge 15 commits into
ovep-developfrom
sync_msft_07082026
Open

Sync with Microsoft ONNX Runtime - 07082026#1250
ai-fw-intg wants to merge 15 commits into
ovep-developfrom
sync_msft_07082026

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.

tianleiwu and others added 15 commits August 5, 2026 21:00
…icrosoft#31635)

## Description

Extends the CUDA plugin EP packaging pipeline with Windows ARM64
support, replaces the shared x64/ARM64 architecture lists with
per-platform lists that reflect the GPUs each package actually targets,
and adds two build-time knobs for controlling binary size. The plugin
`.so`/`.dll` is currently very large (~720 MB uncompressed on the CUDA
12.8 Linux leg, ~90% of which is `.nv_fatbin`), and the architecture
list is the single biggest lever on that number, so it needs to be tuned
per platform rather than shared.

## Summary of Changes

### Windows ARM64 packaging

| File | Change |
|------|--------|
| `plugin-cuda-pipeline.yml` | Add `build_windows_arm64`; rename
`invalidAArch64Config` to `invalidArm64Config` and extend it to cover
Windows ARM64, since NVIDIA only ships Windows-on-ARM CUDA for 13.x |
| `plugin-cuda-packaging-stage.yml` | Add `build_windows_arm64` and
`arm64_cuda_version` (13.1), wire the ARM64 stage and its artifacts into
NuGet and Foundry Local zip packaging |
| `plugin-win-cuda-stage.yml` | Add an `arm64` arch path: ARM64 agent
pool, `win-arm64/` CUDA SDK blob prefix, separate cuDNN folder, native
ARM64 toolset |

### Per-platform CUDA architecture lists

`cmake_x64_cuda_archs` / `cmake_arm64_cuda_archs` are split into four
independent lists, since Windows x64, Linux x64, Windows ARM64, and
Linux aarch64 serve very different GPU populations:

| Parameter | CUDA 12.8 | CUDA 13.x |
|------|------|------|
| `cmake_windows_x64_cuda_archs` | `61,75,86,89,120` | `75,80,86,89,120`
|
| `cmake_windows_arm64_cuda_archs` | n/a | `120,121` |
| `cmake_linux_x64_cuda_archs` | `75,80,86,89,90,120` |
`75,80,86,89,90,120` |
| `cmake_linux_aarch64_cuda_archs` | n/a | `89,90,100,103,120,121` |

Notable decisions:

- **`120-virtual` dropped everywhere.** The `compute_120` PTX measured
176 MB, 27% of the entire `.nv_fatbin` — by far the most expensive
single entry. It also cannot carry the NVFP4 kernels, which are only
valid as real `sm_120a` (`cuobjdump -ptx | grep -c e2m1x2` returns 0),
so it was paying full price for partial coverage.
- **Linux aarch64 targets the platforms that actually exist on ARM**:
GH200 (`90`), GB200 (`100`), GB300 (`103`), DGX Spark GB10 (`121`), plus
discrete cards in ARM chassis (`89`, `120`). `103` is required alongside
`100` because ORT normalizes `100` to `100a-real`, and `a` targets are
locked to their exact SM.
- **`75` dropped from Linux aarch64** — Turing was never paired with an
ARM host in practice.

### CUDA architecture normalization

| File | Change |
|------|--------|
| `cmake/external/cuda_configuration.cmake` |
`ARCHITECTURES_WITH_ACCEL`: add `103` and `121`, drop `101` (removed by
NVIDIA after CUDA 12.9). Without this, `103` and `121` would be built as
plain targets and would silently lose the CUTLASS block-scaled/TMA
kernels, which are gated on `__CUDA_ARCH_FEAT_SM1xx_ALL`. |

### Build size controls

Two new pipeline parameters, both plumbed through the packaging stage to
all four platform stages:

| Parameter | Default | Effect |
|------|------|------|
| `enable_cuda_fatbin_size_compression` | `false` | Sets the new
`onnxruntime_CUDA_FATBIN_COMPRESS_SIZE` cmake option, forcing
`-Xfatbin=-compress-all -compress-mode=size` on the CUDA 12.8 leg. CUDA
>= 13.0 already does this unconditionally, so the parameter only changes
12.8. |
| `enable_fpa_intb_gemm` | `true` | Sets the existing
`onnxruntime_USE_FPA_INTB_GEMM` cmake option. fpA_intB GEMV/GEMM is ~141
MB of device code (22% of `.nv_fatbin`), second only to flash attention.
|

`onnxruntime_CUDA_FATBIN_COMPRESS_SIZE` fails configuration on CUDA <
12.8 rather than silently passing an unsupported flag to nvcc.

Windows composes these via `FatbinCompressOption` / `FpaIntBGemmOption`
job variables appended to the `build.py` invocations, mirroring the
existing `$(TelemetryOption)` pattern. Linux composes them into
`EXTRA_CMAKE_DEFINES`, which `build_cuda_plugin_package.sh` already
forwards.

### Packaged binary hardening and verification

| File | Change |
|------|--------|
| `cmake/onnxruntime_providers_cuda_plugin.cmake` | Compile
`onnxruntime_providers_cuda.rc` into the plugin DLL on Windows so the
packaged binary carries version info; set `SKIP_BUILD_RPATH` on Linux so
the build machine's CUDA path is not embedded in a binary that ships
as-is |
| `plugin-linux-cuda-stage.yml` | Fail the build if the plugin `.so` has
an empty `RPATH`/`RUNPATH` component or a hard-coded CUDA path |
| `plugin-win-cuda-stage.yml` | Fail the build if the plugin DLL is
missing required version-info properties |

## Testing

- Pipeline changes are validated by running the CUDA plugin packaging
pipeline. Both new parameters default to current behavior
(`enable_cuda_fatbin_size_compression: false`, `enable_fpa_intb_gemm:
true`), so a default run produces the same build flags as before this PR
aside from the architecture list changes.
- The cmake `-compress-mode` selection logic was verified in isolation
across four combinations:

  | Toolkit | Option | Result |
  |---|---|---|
  | 12.8 | OFF | `-Xfatbin=-compress-all` |
  | 12.8 | ON | `-Xfatbin=-compress-all -compress-mode=size` |
  | 13.1 | OFF | `-Xfatbin=-compress-all -compress-mode=size` |
  | 12.6 | ON | configure-time fatal error, as designed |

- The new RPATH and DLL version-info checks are self-verifying: they
fail the packaging stage rather than publishing a bad artifact.

## Motivation and Context

The primary consumer is Foundry Local (vision, audio, and mostly LLM
models), which ships this plugin to end-user machines, so download size
matters directly.

Trade-offs worth flagging for reviewers:

- **`-compress-mode=size` raises the minimum driver** to the CUDA 12.4
level (Linux >= 550.54.14, Windows >= 551.61); older drivers cannot
decompress the fatbin at all. It also increases module load time
(measured ~0.8 ms to ~4.3 ms for a ~6.5 MB SASS module) and adds a few
percent to nvcc time. This is why the parameter defaults to `false` and
is opt-in per run.
- **The architecture lists are all `-real` with no virtual entry**, so
any GPU whose compute capability is not explicitly listed gets
`cudaErrorNoKernelImageForDevice` (209) instead of falling back to JIT.
This is deliberate given the PTX cost, but it means new architectures
must be added explicitly.
- **`enable_fpa_intb_gemm: false` is not yet validated end to end.** The
fpA_intB path is opt-in at run time via `ORT_FPA_INTB_GEMM` /
`ep.cuda.fpa_intb_gemm`, but `matmul_nbits.cc` forces it on whenever
weights are prepacked, independent of that flag. The fallback path
should be exercised before shipping a package built with this off.

## Checklist

- [x] No breaking changes to default pipeline behavior (both new
parameters default to existing behavior)
- [x] cmake option gated on toolkit version with an explicit error
rather than a silent no-op
- [ ] Tests added/updated — not applicable; changes are build/packaging
configuration
…nd head sink (microsoft#29912)

### Description

`PagedAttention` is ORT's continuous-batching attention operator, but on
`main` it only supports
FP16/BF16 caches with RoPE and softcap, has no paged decode kernel, and
forces a device→host
synchronization on every node on every step (which makes it uncapturable
by CUDA graphs). This PR
brings it to feature parity with `GroupQueryAttention` for the popular
LLM families and adds the
paging and latent-cache primitives that serving frameworks need,
**additively** — every model valid
under the shipped `com.microsoft::PagedAttention` opset-1 schema keeps
working unchanged.

The design rationale, the compatibility invariant, and the alternatives
that were considered and
rejected are written up in the new design document

[`docs/contrib_ops/cuda/paged_attention.md`](docs/contrib_ops/cuda/paged_attention.md);
the section
numbers referenced below point into it.

### Summary of Changes

#### Schema (`bert_defs.cc`, `docs/ContribOperators.md`)

All additions are trailing optional inputs, new attributes whose
defaults reproduce current
behavior, or widened type constraints (§4).

| New input | Idx | Purpose |
|---|---|---|
| `slot_mapping` | 10 | Explicit per-token cache slot, so the scheduler
owns placement instead of the kernel re-deriving it (§5) |
| `head_sink` | 11 | Attention sink / smooth softmax, matching GQA (§6)
|
| `q_norm_weight` / `k_norm_weight` | 12, 13 | Fused QK-RMSNorm (Qwen3,
gpt-oss) (§7) |
| `k_scale` / `v_scale` | 14, 15 | Per-tensor or per-channel
dequantization scales for a quantized cache (§8) |
| `attention_metadata` | 16 | Optional CPU input carrying *replay-wide
upper bounds* `[max_query_len, max_kv_len]`, which removes the per-node
per-step D→H sync (§4.7) |

| New attribute | Default | Purpose |
|---|---|---|
| `qk_norm_epsilon` | `1e-6` | Epsilon for the fused QK-Norm |
| `k_quant_type` / `v_quant_type` | `NONE` | `NONE` \| `PER_TENSOR` \|
`PER_CHANNEL` |
| `k_cache_dtype` / `v_cache_dtype` | `""` | Logical cache element type,
named after the ONNX type it denotes |
| `kv_cache_layout` | `SEPARATE` | `SEPARATE` \| `LATENT` (absorbed MLA:
one cache, no `value`/`value_cache`) |
| `v_head_size` | `0` | Narrower V head, `LATENT` only (DeepSeek-V3 uses
576/512) |
| `rotary_offset` | `0` | Applies RoPE to `[rotary_offset, rotary_offset
+ rotary_dim)` so MLA can rotate only the positional suffix |

`key_cache` / `value_cache` move from `T` to a new `T_CACHE` constraint
(`float16`, `bfloat16`,
`int8`, `float8e4m3fn`), and `value_cache` / `value_cache_out` become
optional so a `LATENT` node can
omit them. Shape inference now takes the cache element type from inputs
3/4 rather than from `query`,
which was wrong for a quantized cache.

#### CUDA kernels (`paged_attention_impl.cu`, `paged_attention.cc/.h`,
`paged_attention_helper.h`)

- **Paged decode kernel** (`LaunchPagedDecodeAttention`) — split-KV,
block-table-aware decode with
native head-sink, softcap, sliding-window and on-the-fly cache
dequantization.
- **XQA paged decode** (`onnxruntime/contrib_ops/cuda/bert/xqa/`) —
TensorRT-LLM's XQA kernels
  extended to the paged block layout: 8 new translation units
`xqa_paged_{fp16,bf16}_{int8,fp8}_{64,128}.cu` plus a shared paged
loader. Selected for
  quantized-cache decode.
- **Quantized paged cache** — `ReshapeAndCache` quantizes on write; all
read paths dequantize with
  `k_scale`/`v_scale` under `PER_TENSOR` or `PER_CHANNEL` granularity.
- **`ApplyHeadSink`** — exact post-hoc LSE rescale (`1/(1+exp(s_h −
lse))`) applied *after* the
quantized/unquantized branch, so no backend can silently drop the sink
(§6).
- **`QkNormRotaryTNH`** — fuses QK-RMSNorm, RoPE (with `rotary_offset`)
and the packed-QKV unpack
  into one pass.
- **Absorbed MLA** (`PagedLatentAttentionKernel` / `LatentAttention`) —
single latent cache, V read
as the leading `v_head_size` channels of the same row that supplies K
(§12).
- **CUDA-graph safety** — backend dispatch, grid sizing and workspace
extents now come from static
shapes and the `block_table.shape[1] * block_size` capacity bound;
per-step quantities are read on
device. The unconditional `cudaStreamSynchronize` is gone from the
capturable path (§4.7).
- **`int8`→`fp16` conversion fast path** (`xqa/utils.cuh`,
`cvtS8x4ToF16x4`) — replaces a scalar
`I2F` loop with a `prmt` + `sub.f16x2` sequence (5 full-rate
instructions per 4 elements, bit
  identical). Shared with the non-paged GQA loader.
- Kernel registration is now `<T, T_CACHE>`-typed; FP8 combinations are
behind
  `USE_FP8_KV_CACHE && !DISABLE_FLOAT8_TYPES`.

#### GQA bug fix (`flash_api.{h,cc}`, `group_query_attention_impl.cu`)

`mha_fwd` had `constexpr void* head_sink = nullptr;` hardcoded inside
it, and
`FlashAttentionAndQuantizeKV` — the *only* GQA prompt path taken when
the KV cache is quantized —
called it. So for gpt-oss with an INT8/FP8 KV cache, the attention sinks
were silently dropped for
the entire prompt on every layer while decode stayed correct. `mha_fwd`
now takes `head_sink` and
GQA forwards it.

Op-level prefill error drops **0.074829 → 0.000122**. On gpt-oss-20b
(int4 body, INT8 per-channel
KV), MMLU-Pro-800 goes **0.6175 (494/800) → 0.7200 (576/800)**. Existing
CI missed this because
`atol["int8_fp16"] = 1e-1` in `test_gqa.py` is ~800× wider than the
post-fix error.

#### Tooling and docs

- `symbolic_shape_infer.py`: correct output width for packed-QKV and
`LATENT` nodes, and cache
  outputs typed from the cache inputs.
- New `docs/contrib_ops/cuda/paged_attention.md` design document;
regenerated `ContribOperators.md`
  and `OperatorKernels.md`.

### Testing

`test_paged_attention_cuda.py` grows from a smoke test to ~2k lines /
198 cases, with new suites for
features (`slot_mapping`, head sink, QK-Norm), quantized cache (int8/fp8
× per-tensor/per-channel),
the paged decode kernel, the XQA decode path, `attention_metadata`, and
MLA — each against a PyTorch
reference.

```bash
python onnxruntime/test/python/transformers/test_paged_attention_cuda.py   # 198 passed
python onnxruntime/test/python/transformers/test_gqa.py -k xqa             # 714 passed
```

The GQA suite is included because the int8 conversion fast path is
shared with the non-paged loader.

**Backward compatibility.** A node with none of the new
inputs/attributes takes exactly the code path
it does today: `T_CACHE == T`, `value_cache` present, `kv_cache_layout
== SEPARATE`, all quantization
`NONE`. The compatibility invariant is stated normatively in §4.2.

### Experimental Results

Measured on gpt-oss-20b, H200. E2E numbers are driven through
onnxruntime-genai; the CUDA-graph and
engine-side plumbing they depend on is **not** part of this PR — they
are included to show what the
operator-side changes enable, not as a claim about this diff alone.

#### Paged decode kernel (isolated, `nh=64 / kvh=8 / hs=64 / block=256`)

XQA on/off at `b=8, ctx=4096`, per decode call:

| cache | before | after |
|---|---|---|
| int8 `PER_TENSOR` | 2315 µs | **122 µs** |
| int8 `PER_CHANNEL` | 2339 µs | **128 µs** |
| fp8 `PER_TENSOR` | 1633 µs | **57 µs** |
| fp8 `PER_CHANNEL` | 1566 µs | **57 µs** |

Before XQA the quantized paths were ~2.5× *slower* than fp16 — the
generic kernel was the bottleneck,
not the KV bytes.

The `cvtS8x4ToF16x4` conversion path then closes the residual
int8-vs-fp8 gap (nsys median, SASS goes
from 3928 to 3592 instructions with 192 → 0 `I2F`):

| ctx | batch | int8 before | int8 after | gain | fp8 |
|---|---|---|---|---|---|
| 1024 | 32 | 19.91 µs | 13.60 µs | −31.7% | 12.64 µs |
| 4096 | 8 | 24.32 µs | 17.41 µs | −28.4% | 16.48 µs |
| 4096 | 32 | 66.62 µs | 42.66 µs | −36.0% | 41.98 µs |
| 16384 | 8 | 78.11 µs | 52.58 µs | −32.7% | 49.18 µs |
| 16384 | 32 | 244.71 µs | 162.27 µs | −33.7% | 176.70 µs |

The int8/fp8 gap goes from up to +59% down to ≤ 7.6% (int8 is faster at
the largest config), so the
two cache formats can now be chosen on accuracy grounds.

#### End-to-end decode throughput (mxfp4 body, INT8 KV, prompt 128 / new
256)

| batch | baseline | + `attention_metadata` (no sync) | + CUDA graphs |
total |
|---|---|---|---|---|
| 1 | 242.7 tok/s | 263.8 | **305.7** | **+26.0%** |
| 8 | 796.1 | 811.5 | **870.5** | +9.3% |
| 32 | 2635.7 | 2682.0 | **2879.2** | +9.2% |

Including XQA, batch-1 int8 decode goes **199.2 → 305.7 tok/s
(+53.5%)**. After this work attention
is no longer the bottleneck at b=1 — the MoE GEMMs and 49 `MatMulNBits`
nodes dominate the step, with
XQA at 24 × 8.6 µs.

#### PagedAttention vs GroupQueryAttention, matched models

Two models built from the identical recipe (int4 body, INT8 per-channel
KV, identical
`num_heads`/`kv_num_heads`/`scale`/window/rotary, byte-identical weight
file), differing only in the
attention operator. Greedy generation on this stack is bit-reproducible
(0/198 discordance across
replicates), so there is no sampling noise to subtract.

| | GQA | PagedAttention | read as |
|---|---|---|---|
| MMLU-Pro-800 | 0.7200 (576/800) | 0.7163 (573/800) | +0.4 pp, 3
questions |
| GPQA-diamond | 0.6061 (120/198) | 0.6212 (123/198) | −1.5 pp, 3
questions |

The two benchmarks disagree in direction and both deltas are 3
questions: **equivalent within noise.**
(An apparent +8 pp advantage for paged in earlier runs turned out to be
the GQA sink bug fixed above,
seen from the other side.)

| config | GQA tok/s | paged tok/s | delta |
|---|---|---|---|
| b=1, p=128, n=256 | 374.4 | **376.3** | +0.5% |
| b=2, p=4096, n=256 | 684.3 | **686.6** | +0.3% |
| b=8, p=128, n=256 | 1792.7 | 1683.8 | −6.1% |
| b=32, p=128, n=256 | 5285.9 | 4109.5 | −22.3% |

Peak device memory at matched KV capacity agrees to within 24 MiB
(0.16%) from 16k to 128k
`max_length` — paged costs nothing extra, and its advantage is
structural (a shared pool sized to
aggregate demand rather than `batch × max_length`).

The b=32 gap was profiled with `nsys --cuda-graph-trace=node`: the
captured graph body is at parity
with GQA's eager model pass (6.220 ms vs ~6.2 ms) and the entire
regression is a 2.384 ms
search/sampling tail, which the onnxruntime-genai `Engine` runs once
**per request** rather than once
per batch. It is not attributable to this operator, and a partial
engine-side fix already recovers
b=32 to 4517 tok/s.

### Follow-ups (not in this PR)

- `attention_bias` and `output_qk` (§10, §11) — schema slots reserved,
kernels deferred.
- Sub-byte (`int4` / `float4e2m1`) packed caches — attribute vocabulary
reserved and rejected at
  validation until a backend exists (§21.4).
- `.Alias(3, 1).Alias(4, 2)` on the kernel def, so a non-aliasing
allocation plan fails at partition
  time instead of run time (§4.4).
- Re-tightening `atol["int8_fp16"]` in `test_gqa.py` now that the sink
bug is fixed.
### Description
<!-- Describe your changes. -->

Add an `ort-release-notes` skill with preset configuration, scoped path
support, and a concise workflow for metadata generation and draft
output.

Also include a lightweight docs entry point and WebGPU EP scoped paths
file.

### 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. -->

Make it easier for AI to generate release note drafts.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copilot-Session: 3611670d-4e92-4183-8e0e-b2cb659ab624
…t#31632)

This change adds a build option for excluding the prebuilt TensorRT
fused multi-head attention cubins from the CUDA Execution Provider. The
option is enabled by default to preserve existing behavior; disabling it
removes approximately 14 MB of embedded cubin data and leaves attention
selection to the other available kernels and unfused fallback paths.

## Key Changes

- Add `onnxruntime_USE_TRT_FUSED_ATTENTION`, dependent on CUDA and
enabled by default.
- Propagate the option to CUDA provider compilation and guard TensorRT
fused-attention cubin declarations, metadata, and lookup paths.
- Filter the prebuilt TensorRT fused-attention cubin sources from both
the regular CUDA provider and CUDA plugin provider when the option is
disabled, while retaining the shared driver wrapper needed by sparse
attention.
- Add Windows plugin DLL version metadata and suppress build-machine
RPATH embedding for packaged Linux plugin binaries.
- Exercise `onnxruntime_USE_TRT_FUSED_ATTENTION=OFF` in the Windows CUDA
no-cuDNN plugin build.

## Testing Notes

- `git diff --check origin/main...HEAD` passes.
- The Windows CUDA no-cuDNN workflow now builds the CUDA plugin with
TensorRT fused attention disabled, providing CI coverage for the opt-out
configuration.
- No local CUDA/Windows build was run in this environment.
This pull request adds input validation checks to prevent integer
overflow issues during CUDA kernel indexing in the LayerNorm and RMSNorm
CUDA operators. The main goal is to ensure that the product of
`num_rows` and `norm_size` does not exceed `INT_MAX`, which could lead
to incorrect behavior or crashes.

Input validation for CUDA kernel indexing:

* Added a check in `LayerNorm::ComputeInternal` (in `layer_norm.cc`) to
return an error if `num_rows * norm_size` exceeds `INT_MAX`, preventing
integer overflow during CUDA kernel indexing.
* Added a similar check in `RMSNorm::ComputeInternal` (in `rms_norm.cc`)
to ensure the input size does not exceed CUDA kernel indexing limits.

Code maintenance:

* Included the `<limits>` header in both `layer_norm.cc` and
`rms_norm.cc` to support the new input validation logic.
[[1]](diffhunk://#diff-ebda3d3b7054f5d14c679ebe8e6520a2c84a5a558d8fdcc0798c08e7032345feR9)
[[2]](diffhunk://#diff-adebea99f15800767eb7b84d40be4873e81ca4df99ffe9dc7f849eabe0a3c563R9)

---------

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

### Description
[CPU] Tighten cache_indirection shape contract in MultiHeadAttention

### 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. -->
…fy (microsoft#31159)

### Description

Four changes to the fused NVFP4 QMoE decode GEMV. Stacked on microsoft#31154 —
**review only the top four
commits**; the base branch is that PR.

1. **Packed E2M1 dequantize** (`Fp4I2FConverter::decode_quad`) — decode
a whole 32-bit weight
   word (eight codes) per step instead of one code at a time.
2. **`ORT_FP4_GEMV_DEFAULT_TILING`** — env switch to bypass the
autotuner and take the default
   tiling, for A/B and for avoiding autotune cost in short runs.
3. **Cut memory and ALU traffic in the decode GEMV.**
4. **`kMaxProfiledExpandedRows` 8 -> 64** so MTP verify steps stay on
the GEMV path.

### Motivation and Context

Prior profiling established that this kernel is **ALU-pipeline bound**,
not memory- or
tiling-bound. On the actual Qwen3.6 decode shapes (`hidden=2048`,
`inter=512`, `E=256`,
`top_k=8`, bf16, SwiGLU), ncu reported for the FC1 SwiGLU-fused GEMV:

> ALU 78.9%, DRAM 7.3%, occupancy 21% (register-limited)

That is why the levers here are instruction-count levers. Two things
were measured and
explicitly **dropped** because of it: smaller `CtaN` tiling (the
autotuner still picks
`threads64`/`CtaN=8`; `CtaN=4` never wins because the kernel is
compute-bound, not
occupancy-bound), and halving scale bandwidth by storing combined scales
as 1-byte e4m3 (DRAM is
only ~7%, so it cannot move the needle).

All numbers below: 1x H200 SXM (SM90, 132 SM, ~4.8 TB/s HBM), CUDA 13.0,
Qwen3.6-35B-A3B-NVFP4
+ MTP `N=3` (verify batch `M=4`).

### 1. Packed E2M1 dequantize

`prmt` selects four bytes per instruction, so a 4-element magnitude
lookup costs one instruction
instead of four. Bit-identical to the per-element path (same magnitude
tables, same sign
handling). The FP4 GEMV kernel SASS shrinks ~30%, and the two QMoE GEMVs
drop:

| kernel | before | after |
|---|---:|---:|
| fc1 (SwiGLU-fused) | 33.2 µs | **26.2 µs** |
| fc2 | 30.2 µs | **22.2 µs** |

### 2. Cut memory and ALU traffic — −0.46 ms/step (−5.1%)

The scales of the `CtaN` columns a block owns sit `Interleave` elements
apart, so for the
non-interleaved ColumnMajor layout (`Interleave == 1`) the whole
`CtaN`-wide scale vector is
contiguous and can be fetched with one wide access instead of `CtaN`
scalar ones. This matters
far more than the byte count suggests: with a groupwise scale (NVFP4
`GroupSize = 16`) and
`StepK = 8`, a warp's 32 lanes cover 16 distinct scale rows that are `n`
elements apart, so
*every* scale load touches 16 different sectors — `CtaN * 16` sectors,
using 2 bytes out of each
32-byte sector.

Per-kernel (graph OFF, 40 launches/step each):

| kernel | before | after |
|---|---:|---:|
| `moe_gemv_interleaved_swiglu_kernel` | 0.956 ms/step | **0.678
ms/step** |
| `moe_gemv_kernel` | 0.700 ms/step | **0.494 ms/step** |
| **family total** | **1.657 ms/step** | **1.174 ms/step** |

End-to-end (4 interleaved `.so`-swap reps per arm):

* before: 8.973 / 9.009 / 8.992 / 9.017
* after: 8.567 / 8.508 / 8.498 / 8.583

**8.998 -> 8.539 ms/step.** No overlap between the two sets.

### 3. `kMaxProfiledExpandedRows` 8 -> 64

The fused GEMV rejects `expanded_num_rows > kMaxProfiledExpandedRows`.
Qwen3.6 is top-8, so
single-token decode expands to 8 rows (accepted), but an MTP verify does
not: an `(N+1)`-token
verify for `num_speculative_tokens = N` expands to `(N+1) * 8` rows,
i.e. **up to 64 for N=7**.
Those steps fell out of the window and back onto the dequantize +
CUTLASS grouped-GEMM path,
which re-dequantizes all 256 experts per token.

The impact of that fallback is large: with the limit at 8, the 2-token
verify (expanded 16)
dropped MTP to **~2.4 tok/s**; raising the limit put it at **~30–55
tok/s (12–23x)**. 64 covers
the `N=3` shape used today with headroom to `N=7`.

### Tests

* `onnxruntime_provider_test` FP4/FP8/QMoE: 18/18 pass.
* `onnxruntime/test/python/transformers/test_qmoe_nvfp4_cuda.py`: 22/22
pass, including new
multi-token GEMV cases and a `gemv_mode="0"` dequant-fallback companion
on the identical shape,
  so both must match the same exact dequantized reference.

### Methodology note

End-to-end deltas are quoted as **ms/step** from a fixed-step
measurement, never tok/s: any
numerics change alters the generated sequence and therefore the MTP
acceptance rate, which swamps
the speed delta. Per-kernel durations are taken with CUDA graphs **off**
—
`nsys --cuda-graph-trace=node` inflates durations ~35% globally and up
to 3.8x for large-grid
kernels.

> [!IMPORTANT]
> `Fp4I2FConverter::convert()` gained a `PairInterleaved` template
parameter in microsoft#31154. The
> packed path added here assumes the **plain** nibble order (nibble `j`
of the word is logical
> element `j`), which is what its `prmt` selectors encode, so it is
nested inside
> `if constexpr (!PairInterleaved)`. Please check that guard carefully
during review — applied
> without it, the pair-interleaved SM80 layout would silently decode to
the wrong values.
…oft#29611)

### Description
Accumulate directly in output_element_t (e.g., f16 for f16 models)
instead of hardcoding an f32 accumulator in the MatMulNBits wide-tile
shader.

**Intel Panther Lake**
| | Prefill Length | Default Prefill TPS | Optimized Prefill TPS |
Improvement |
| :--- | ---: | ---: | ---: | ---: |
| gpt-oss-20b-ONNX | 128 | 305.70 | 344.49 | 113% |
| gpt-oss-20b-ONNX | 1024 | 396.50 | 429.80 | 108% |
| Phi-4-mini-instruct-ONNX | 128 | 515.90 | 592.36 | 115% |
| Phi-4-mini-instruct-ONNX | 1024 | 615.39 | 753.40 | 122% |

[1] https://huggingface.co/onnx-community/gpt-oss-20b-ONNX
[2] https://huggingface.co/onnx-community/Phi-4-mini-instruct-ONNX

### Motivation and Context
See above.
…ch (microsoft#31480)

### Description

When GroupQueryAttention runs with a quantized KV cache, the K cache and
the V cache were dequantized by two separate kernel launches. They have
identical shapes and identical per-head scale layouts, so the second
launch adds a full grid setup and a second pass over the same index
arithmetic for no reason.

This change dequantizes both caches in a single launch. The kernel is
moved into a new `group_query_attention_qdq.cuh` header and given a `2
*` grid in the cache dimension, so one block range covers K and the
other covers V; the buffer pointers and scale pointers are selected from
the block index.

Output is bit-identical to the two-launch path -- the per-element
arithmetic is unchanged, only the launch geometry differs.

### Motivation and Context

This is on the decode path of speculative-decoding (MTP) workloads,
where the launch is issued every layer, every step, and the per-launch
fixed cost is a meaningful fraction of a short kernel. Halving the
number of launches removes that fixed cost without changing numerics.

Measured on H200 (SM90) with a Qwen3.6-35B-A3B MTP configuration.
Adds odd-N support to WebGPU subgroup-matrix MatMul by padding constant
FP16 weights to an even row stride and caching the result.
…icrosoft#31685)

### Description
<!-- Describe your changes. -->

Add onnxruntime/test/providers/webgpu to plugin-ep-webgpu/paths.txt.

### 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. -->

Missed a test path.
…ft#31683)

### Description
<!-- Describe your changes. -->

Announce JSEP deprecation to developers via docs in the repo. The main
doc is `docs/JSEP_Deprecation.md`.

Also update the JSEP to WebGPU EP migration design doc to include this
initial step as well as some other clarifications.

### 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. -->

JSEP deprecation.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Add more paths related to cuda plugin ep to address release note skills
comment
microsoft#31669 (comment).

@jatinwadhwa921 jatinwadhwa921 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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.

9 participants