Skip to content

Sync with Microsoft ONNX Runtime - 17072026#1202

Merged
hdharpure9922 merged 7 commits into
ovep-developfrom
sync_msft_17072026
Jul 17, 2026
Merged

Sync with Microsoft ONNX Runtime - 17072026#1202
hdharpure9922 merged 7 commits into
ovep-developfrom
sync_msft_17072026

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 7 commits July 16, 2026 14:50
Builds are blocked by onnxruntime-github-vs2022-latest.
Try unblock our limited PRs for release.

Co-authored-by: GitHub Copilot <copilot@example.com>
… (sm_120) (microsoft#29706)

### Summary

GroupQueryAttention's XQA decode kernel failed on consumer Blackwell
GPUs (RTX 50-series, sm_120) with `cudaErrorInvalidValue`, while working
fine on A100 (sm_80) and H200 (sm_90). This adds a runtime shared-memory
capability check so XQA is only selected when the device can actually
satisfy the kernel's dynamic shared-memory request, and otherwise falls
back to cuDNN SDPA / Flash. A100/H200 behavior is unchanged.

### Root cause

XQA bakes its shared-memory layout at compile time from `__CUDA_ARCH__`:

- sm_80 / sm_87 / sm_90 use the large K/V-tile layout
(`preferedKHeadPartBytes=128`, `cacheVTileSeqLen=64`) → up to ~140 KB of
dynamic shared memory for head_size 128/256.
- sm_86 / sm_89 / sm_120 use the small layout (64 / 32) → ~78–96 KB.

Release/packaging binaries are built with a maximum arch of `90-virtual`
(compute_90 PTX only, no native sm_120 SASS). On sm_120 the driver
JIT-compiles that sm_90 PTX, so the kernel's `smemSize` carries the
Hopper value (~140 KB). `launchMHA` then calls
`cudaFuncSetAttribute(..., cudaFuncAttributeMaxDynamicSharedMemorySize,
size)`, which exceeds sm_120's ~99 KB per-block opt-in limit
(`sharedMemPerBlockOptin`) and returns `cudaErrorInvalidValue`. A100
(163 KB) and H200 (227 KB) have enough room, so they were unaffected.

### Key changes

| File | Change |
|---|---|
| `xqa/xqa_impl_gen.cuh` | Add `GetSmemSize()` host helper that reads
the per-kernel `smemSize` device symbol (accurate even for a PTX kernel
JIT-compiled for the running SM). |
| `xqa/xqa_loader_fp16_impl.cuh` | Add
`GetXQAKernelSmemBytes(group_size)` head-dim dispatcher. The
non-quantized fp16 footprint is an upper bound for the int8/fp8/bf16
variants (smaller cache element), so one query covers all XQA paths. |
| `xqa/xqa_loader_fp16.cu`, `xqa/xqa_loader.h` | Expose
`GetXQARequiredSharedMemoryBytes(device_prop, head_size, num_heads,
kv_num_heads)`; a single non-templated entry point used by both the fp16
and bf16 GQA kernels. |
| `group_query_attention.cc`, `group_query_attention.h` | Gate XQA
selection on `required_smem <= device_prop.sharedMemPerBlockOptin`; fall
back to cuDNN SDPA / Flash when it does not fit. Result is cached per
node (`xqa_shared_memory_ok_`) since head_size/group are constant. |
| `xqa/mha_impl.cuh` | Defensive backstop in `launchMHA`: if the
requested shared memory still exceeds the device limit, throw an
actionable message (which SM to build for / how to disable XQA) instead
of the opaque `cudaErrorInvalidValue`. |

### CUDA graph safety

`GetXQARequiredSharedMemoryBytes` uses `cudaMemcpyFromSymbol`, which
synchronizes and is illegal during CUDA graph capture. The query is:

- **cached** per node, so it runs at most once;
- **guarded** with
`onnxruntime::llm::common::isCapturing(Stream(context))` so the
synchronizing call is only issued when the compute stream is not
capturing;
- resolved during ORT's non-captured warm-up run(s) before capture
begins.

If the value is somehow still unresolved while capturing, XQA is
conservatively skipped for that run (safe fallback) without caching, so
a later non-capturing run can resolve it. Warm-up and capture therefore
make the same XQA/fallback decision, keeping the captured graph
consistent with replay.

### Testing notes

- Built the affected TUs (GQA dispatcher + fp16/bf16/int8/fp8 XQA
loaders) with `CMAKE_CUDA_ARCHITECTURES="80;90"` (the configuration that
reproduces the failure); all compile cleanly.
- To validate the fix end-to-end, run a fp16/bf16 GQA decode workload on
an sm_120 GPU (e.g. RTX 5090): it should now run (via fallback) instead
of returning `cudaErrorInvalidValue`. Set
`ORT_ENABLE_ATTENTION_KERNEL_DEBUG_INFO=1` to confirm the selected
backend.
- To actually run XQA (the fast path) on Blackwell, build with native
arch `120` in `CMAKE_CUDA_ARCHITECTURES` (and `100` for datacenter
Blackwell). With a native sm_120 cubin the layout is ~80 KB and fits, so
XQA is selected.
To avoid hard dependency on nvrtc dll even when it is not used for some
models.
Drop 52-real; 90-virtual
Add 120-real; 120-virtual
Ensure 86-real is included

Q: Why not add 100-real to cuda 12.8 build?
A: We assume that those machines will have cuda 13.x for best
performance.

Q: Why drops 52-real
A: Many applications require float16 support, while 52-real cannot
support it.
…lugin EP (microsoft#29620)

### Summary

Phase 2 of the CUDA plugin execution provider "no-cuDNN" work. It lets
single last-axis `ArgMax`/`ArgMin` run through a small custom CUDA
kernel instead of cuDNN, fixes `LogSoftmax` classification in the plugin
adapter, and adds a non-throwing cuDNN handle accessor so reduction
kernels can fall back gracefully when cuDNN is disabled.

### Key Changes

| Area | Change |
|---|---|
| `reduction_functions.cu` / `.h` | New `arg_min_max_last_axis<TIn,
IsArgMax>` kernel (instantiated for `half`, `float`, `double`) that
computes ArgMax/ArgMin indices over the last dimension of a row-major
matrix without cuDNN. |
| `reduction_ops.cc` | In `ReduceComputeCore`, route a single last-axis
ArgMax/ArgMin (`CUDNN_REDUCE_TENSOR_FLATTENED_INDICES`) to the custom
kernel when shapes fit `int`; otherwise fall through to the existing
cuDNN path. `ReduceKernel::ComputeImpl` now uses `TryGetCudnnHandle`. |
| `cuda_kernel.h` (native) / `cuda_kernel_adapter.h` (plugin) | Add
`TryGetCudnnHandle`, which returns the cuDNN handle when available and
`nullptr` otherwise (instead of throwing at handle acquisition). |
| `softmax.h` | Detect `LogSoftmax` from `node.OpType()` instead of
`info.GetKernelDef().OpName()`, so the plugin EP adapter classifies it
correctly. |
| `test_cuda_plugin_ep.py` | Add `LogSoftmax` and `ArgMin` tests; drop
the `@requires_cudnn` gate from `ArgMax`, `ReduceMean`, `ReduceSum`;
reduce over the last axis to exercise the cuDNN-free paths. |
| `docs/cuda_plugin_ep/QUICK_START.md` | Drop `ArgMax` and reductions
from the list of ops that still require cuDNN. |

### Correctness Notes

- `select_last_index == 1` is already rejected on the CUDA EP, so the
kernel keeping the first matching index (strict `>` / `<`) is
spec-correct for the supported case.
- The custom path guards `n > 0`, returns early for `m == 0`, computes
the row offset in `int64_t`, and only engages when `m` and `n` fit in
`int` (`gsl::narrow_cast`); larger tensors fall back to cuDNN.

### Testing

- `python -m pytest
onnxruntime/test/python/transformers/test_cuda_plugin_ep.py -k
"log_softmax or argmax or argmin or reduce_mean or reduce_sum"`
- Plugin no-cuDNN validation: `bash .env/cuda_130_plugin_no_cudnn.sh
--build --test_plugin`
- `onnxruntime_provider_test --gtest_filter='*Reduce*:*ArgM*'`
…9) + retiring equivalence proof + corpus-collapse tripwire (microsoft#29504)

### Motivation
ONNX onnx/onnx#7959 removes the on-disk `onnx/backend/test/data/node/`
corpus (targeted for ONNX 1.23), replacing it with on-the-fly in-memory
generation. ORT's C++ `onnx_test_runner` reads that corpus from disk —
after the deletion it would load **0 node tests and silently exit 0**
(green CI on a corpus that no longer exists). This PR detaches ORT from
the deleted artifacts *ahead* of the bump.

### What this does (3 pieces)
1. **Detach** — a build-time materializer
(`tools/python/materialize_onnx_node_tests.py`) regenerates the node
corpus to disk from ONNX's surviving generator (`collect_testcases`), so
the C++ runner keeps reading from disk unchanged. EP-agnostic
(CPU/CUDA/QNN share one materialized tree).
2. **Equivalence proof** — a *retiring* test
(`onnxruntime/test/python/onnx_node_test_equivalence_test.py` +
`tools/python/compare_node_test_corpora.py`) proves the materialized
corpus is byte-identical to the original (modulo a documented ULP band);
it auto-skips once the ONNX on-disk oracle disappears post-microsoft#7959.
3. **Cause-agnostic tripwire** — build-time `--min-cases` gate (FATALs
the build on every MATERIALIZE=ON leg if generation < floor) + a runtime
`-m` floor on the CPU node ctest, turning silent-green-on-empty into a
loud red.

### Key decisions
- **numpy**: cmake configure = HARD FATAL on off-pin numpy (build-env
reproducibility across CI legs); Python materializer = SOFT WARN
(standalone advisory). Different layers, different questions. The onnx
pin stays HARD on both sides.
- **QNN legs**: repointed to the materialized tree + pinned onnx/numpy
installed pre-build. Node-dir runs carry a **low `-m 1` collapse
sentinel** (NOT the 1500 build floor): `-e qnn` legitimately reduces the
collected set to ~1529, so a 1500 runtime floor would be a false-red
timebomb on the next opset bump — the strict count is enforced at build
time via `--min-cases`, while `-m 1` (zero false-red risk) catches a
per-leg `MATERIALIZE=OFF` that still runs the node dir. The android
leg's single-case `cp -r` source is repointed to the materialized tree
(the old `data/node` source vanishes post-microsoft#7959).

### Testing
- Runtime tripwire empirically verified: empty/truncated corpus → runner
FATAL (nonzero); full → exit 0; default (no `-m`) unchanged.
- Equivalence: 1799-case byte-probe (dir-set match; byte-identical
modulo the documented ULP band).
- cmake configure verified to generate cleanly on latest main
(ep_context + node-test regions coexist, no collisions).
- **[needs-run]** on a pinned-numpy CI box: the `MATERIALIZE=ON` inner
path (actual materialization + the two add_test bodies) — this dev box
is off-pin so the numpy gate FATALs by design.

Relates to: onnx/onnx#7959


---
## Cross-consumer viability note (re: onnx/onnx#7959)

While detaching, we assessed all of ORT's node-test consumers. The
in-memory generator approach behind microsoft#7959 works cleanly for
**single-version** consumers (C++, C#, docs), but has a gap for
consumers needing a **historical multi-opset matrix**:
- ORT's JS/web tests pull node data for opset 7–21 from 15 immutable
`rel-*` release archives. Since onnx's generator is single-version, old
opsets can't be regenerated from a new onnx.
- Those consumers stay green post-microsoft#7959 only because released branches
are immutable — there is no generator-based path to add a *new*
post-microsoft#7959 opset for them.

Net: workable for single-version consumers; the multi-version case would
need a small per-version serialization utility or a documented migration
note upstream.
---

---------

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

@hdharpure9922 hdharpure9922 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

@hdharpure9922
hdharpure9922 merged commit 1da1bf8 into ovep-develop Jul 17, 2026
7 of 9 checks passed
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.

5 participants