Skip to content

webgpu: fix RecurrentState graph capture with shared buffer aliasing - #2244

Merged
baijumeswani merged 8 commits into
mainfrom
fix/webgpu-recurrent-state-graph-capture
Jul 10, 2026
Merged

webgpu: fix RecurrentState graph capture with shared buffer aliasing#2244
baijumeswani merged 8 commits into
mainfrom
fix/webgpu-recurrent-state-graph-capture

Conversation

@qjia7

@qjia7 qjia7 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • RecurrentState::share_buffers_ now derives from past_present_share_buffer config (same pattern as DefaultKeyValueCache) instead of being hardcoded to false for WebGPU
  • Add a graph-capture guard that throws when share_buffers_ is false and graph capture is requested, matching the existing guard in kv_cache.cpp
  • Add Ort::Experimental::Get_*_Fn accessor shim to onnxruntime_api.h so genai builds against ORT builds where onnxruntime_experimental_cxx_api.h cannot be included directly

Motivation

WebGPU graph capture freezes GPU buffer handles at the first captured decode step. RecurrentState was using separate past/present buffers for WebGPU with a per-step C++ pointer swap — but graph capture ignores those swaps on replay, so every decode step after the first read a stale buffer, producing garbage output.

With past_present_share_buffer: true, the ORT LinearAttention and CausalConvWithState kernels detect the aliased buffers via initial_state_in_present_state / conv_state_in_present_state and switch to a single read_write binding, which satisfies the WebGPU spec constraint that originally motivated the per-device split. Buffer handles stay stable across graph capture replays.

The original comment in the code noted a TODO to remove the WebGPU special case once the ORT WebGPU EP kernels natively supported past/present buffer sharing — that support is already present, so this PR resolves the TODO.

Test plan

  • Qwen3.5-0.8B-webgpu-fused correctness test: all queries pass with enableGraphCapture=1
  • Qwen3.5-0.8B-webgpu-fused multi-gen test: sequential and overlapping generators pass with enableGraphCapture=1
  • Both tests also pass with enableGraphCapture=0 (no regression on non-graph-capture path)
  • clang-format: no issues on changed files
  • Build: succeeds against local ORT WebGPU EP build

RecurrentState used separate past/present buffers for WebGPU and swapped
them per step. WebGPU graph capture freezes GPU buffer handles at the
first captured decode step; subsequent replays ignored the C++ pointer
swap and kept reading a stale buffer, producing garbage output on every
decode step after the first.

Fix: derive share_buffers_ from the past_present_share_buffer config
(matching the DefaultKeyValueCache pattern) and add a graph-capture guard
that throws when share_buffers_ is false. When share_buffers_ is true,
the ORT LinearAttention and CausalConvWithState kernels detect the aliased
buffers via initial_state_in_present_state / conv_state_in_present_state
and use a single read_write binding, satisfying the WebGPU spec constraint
that was the original reason for the per-device split.

Also add an Ort::Experimental::Get_*_Fn accessor shim to onnxruntime_api.h
so genai builds against ORT builds where onnxruntime_experimental_cxx_api.h
cannot be included directly due to a transitive onnxruntime_cxx_api.h
conflict with genai's vendored Ort wrappers.

Verified: Qwen3.5-0.8B-webgpu-fused correctness and multi-gen tests pass
with enableGraphCapture=1 and with enableGraphCapture=0.
@qjia7
qjia7 marked this pull request as ready for review June 26, 2026 08:54
@qjia7
qjia7 requested a review from a team as a code owner June 26, 2026 08:54
Copilot AI review requested due to automatic review settings June 26, 2026 08:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes incorrect WebGPU graph-capture behavior for models that use RecurrentState by making recurrent past/present buffer sharing follow the past_present_share_buffer configuration (so GPU buffer handles remain stable across graph-capture replays). It also adds a small ONNX Runtime experimental-API accessor shim so GenAI can build in environments where the experimental C++ header cannot be included.

Changes:

  • Derive RecurrentState::share_buffers_ from GeneratorParams::IsPastPresentShareBufferEnabled(...) (instead of a WebGPU hardcoded behavior).
  • Add a runtime guard that rejects graph capture when effective past/present sharing is disabled.
  • Add Ort::Experimental::Get_*_Fn accessor shims in onnxruntime_api.h to avoid including onnxruntime_experimental_cxx_api.h.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/models/recurrent_state.h Updates the member comment to describe config-driven past/present sharing semantics.
src/models/recurrent_state.cpp Switches share_buffers_ to be config-derived and adds a graph-capture precondition check.
src/models/onnxruntime_api.h Adds an experimental accessor shim to support model-package functions without including ORT experimental C++ headers.

Comment thread src/models/recurrent_state.cpp
Comment thread src/models/recurrent_state.cpp
qjia7 added 2 commits June 26, 2026 17:06
- Error message now mentions num_beams=1 constraint since beam search
  also disables past/present buffer sharing via IsPastPresentShareBufferEnabled.
- Replace "WebGPU: separate past/present buffers" comment in Add() with
  EP-neutral description; the separate-buffer path applies to any EP when
  share_buffers_=false.
Comment thread src/models/onnxruntime_api.h Outdated
Comment thread src/models/onnxruntime_api.h Outdated
qjia7 added 2 commits July 2, 2026 19:12
… lookups

Remove the reopened namespace Ort::Experimental block in onnxruntime_api.h
that expanded onnxruntime_experimental_c_api.inc to define
Get_<NAME>_SinceV<VER>_Fn accessors. Extending an upstream ORT namespace with
genai-owned inline functions is fragile: any TU that also includes upstream
onnxruntime_experimental_cxx_api.h would hit redefinition errors on identical
inline definitions, and new .inc entries whose signatures reference
auxiliary types from the C++ experimental header would fail to compile.

Replace the accessors with a locally-scoped GENAI_MP_V28_FN macro inside
GetModelPackageApi() that reinterpret_casts api->GetExperimentalFunction(k...)
directly to the corresponding OrtExperimental_OrtModelPackageApi_*_SinceV28_Fn
typedef, then undefines itself. Only the C typedefs and name constants from
onnxruntime_experimental_c_api.h are relied on.

Verified with two Release builds: the incremental build against the existing
downloaded ORT and a fresh --ort_home D:\jiajia\ort_out build with
USE_WEBGPU=ON both produce onnxruntime-genai.dll, unit_tests.exe,
model_benchmark.exe, and the Python wheel with no errors or warnings.
lint-cpp CI job on the prior commit rejected the hand-aligned continuations
inside the GENAI_MP_V28_FN macro definition. Re-run clang-format so the
column alignment of the trailing backslashes and the body indent match the
project style.
@qjia7
qjia7 requested a review from kunal-vaishnavi July 3, 2026 04:29
qjia7 added 2 commits July 8, 2026 16:30
…KAGE

Replace the raw `ORT_API_VERSION >= 28 && ORT_GENAI_HAS_EXPERIMENTAL_C_API`
condition guarding the ModelPackageApi block in onnxruntime_inline.h with
ORT_GENAI_HAS_MODEL_PACKAGE. The macro is already defined in
onnxruntime_api.h as exactly that condition, so behavior is unchanged; both
files now route through a single named gate and cannot drift.

Verified with a Release build against ORT_HOME (ORT_API_VERSION=28) and
USE_WEBGPU=ON: onnxruntime-genai.dll builds clean and UnitTests pass.
Comment thread src/models/onnxruntime_inline.h
@qjia7
qjia7 requested a review from baijumeswani July 9, 2026 02:32
@baijumeswani
baijumeswani enabled auto-merge (squash) July 9, 2026 05:49
@baijumeswani
baijumeswani merged commit f604a5e into main Jul 10, 2026
63 of 67 checks passed
@baijumeswani
baijumeswani deleted the fix/webgpu-recurrent-state-graph-capture branch July 10, 2026 02:49
tianleiwu pushed a commit that referenced this pull request Jul 11, 2026
…2244)

## Summary

- `RecurrentState::share_buffers_` now derives from
`past_present_share_buffer` config (same pattern as
`DefaultKeyValueCache`) instead of being hardcoded to `false` for WebGPU
- Add a graph-capture guard that throws when `share_buffers_` is false
and graph capture is requested, matching the existing guard in
`kv_cache.cpp`
- Add `Ort::Experimental::Get_*_Fn` accessor shim to `onnxruntime_api.h`
so genai builds against ORT builds where
`onnxruntime_experimental_cxx_api.h` cannot be included directly

## Motivation

WebGPU graph capture freezes GPU buffer handles at the first captured
decode step. `RecurrentState` was using separate past/present buffers
for WebGPU with a per-step C++ pointer swap — but graph capture ignores
those swaps on replay, so every decode step after the first read a stale
buffer, producing garbage output.

With `past_present_share_buffer: true`, the ORT `LinearAttention` and
`CausalConvWithState` kernels detect the aliased buffers via
`initial_state_in_present_state` / `conv_state_in_present_state` and
switch to a single `read_write` binding, which satisfies the WebGPU spec
constraint that originally motivated the per-device split. Buffer
handles stay stable across graph capture replays.

The original comment in the code noted a TODO to remove the WebGPU
special case once the ORT WebGPU EP kernels natively supported
past/present buffer sharing — that support is already present, so this
PR resolves the TODO.

## Test plan

- [x] `Qwen3.5-0.8B-webgpu-fused` correctness test: all queries pass
with `enableGraphCapture=1`
- [x] `Qwen3.5-0.8B-webgpu-fused` multi-gen test: sequential and
overlapping generators pass with `enableGraphCapture=1`
- [x] Both tests also pass with `enableGraphCapture=0` (no regression on
non-graph-capture path)
- [x] clang-format: no issues on changed files
- [x] Build: succeeds against local ORT WebGPU EP build
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.

4 participants