Skip to content

[WebGPU] Support continuous decoding (RewindTo) with graph capture - #2083

Merged
kunal-vaishnavi merged 15 commits into
mainfrom
continuous-decoding-graph-capture
Apr 24, 2026
Merged

[WebGPU] Support continuous decoding (RewindTo) with graph capture#2083
kunal-vaishnavi merged 15 commits into
mainfrom
continuous-decoding-graph-capture

Conversation

@qjia7

@qjia7 qjia7 commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces improvements to the handling of attention masks in both the CUDA and WebGPU backends, focusing on more efficient and correct updates of mask buffers during decoding. The main changes are the implementation of a CPU-side update for static attention masks in CUDA and the addition of a reusable staging buffer for efficient mask updates in WebGPU, with logic to avoid redundant work for single-beam cases.

CUDA backend improvements:

  • Replaced the previous (commented-out and incorrect) CUDA memory set logic in DefaultPositionInputs::RewindMask with a CPU-side update that correctly sets attended and non-attended positions in the attention mask for each batch/beam, followed by a copy back to the device. This ensures the mask is set with 1s for attended tokens and 0s for future tokens, supporting both int32_t and int64_t types.

WebGPU backend improvements:

  • Added a reusable CPU staging buffer (mask_staging_buffer_) to the InterfaceImpl struct for efficient attention mask updates, avoiding repeated allocations and redundant writes.
  • Implemented the UpdateAttentionMask method to efficiently update the mask for single-beam cases by only filling new positions with 1s and copying the relevant portion to the device, falling back to CPU for multi-beam cases. This method handles static update path and supports both int32_t and int64_t mask types.

qjia7 added 2 commits April 13, 2026 21:42
Previously, RewindMask threw 'Static buffer is not supported for continuous
decoding' when graph capture was enabled. The original CUDA implementation
was disabled due to cudaMemsetAsync semantics issues.

Fix: For static mask handling (graph capture), rewind the attention mask
by zeroing positions [index, max_length) in-place on the static buffer.
This correctly resets the mask to reflect the target sequence length without
reallocating or reshaping the buffer.

Tested with phi4-graph-prune (graph capture ON, WebGPU):
- RewindTo(0): produces identical sequences across multiple runs
- RewindTo(10): first 10 tokens preserved, generation continues coherently
  Run 1 (15 tokens): 'The capital of France is Paris. Paris is known for
    its rich history, culture, and landmarks such'
  RewindTo(10) + 5 tokens: 'The capital of France is Paris. Paris is known
    for its rich history, culture'
  First 10 tokens match: True
Add WebGPU-native UpdateAttentionMask for both static and non-static paths
when batch_beam_size == 1. For single-batch no-padding, the mask is always
all 1s for attended positions. Upload total_length elements of 1s directly
from a reusable CPU staging buffer to GPU via CopyTensors.

Works for both update_only=true (graph capture static mask) and
update_only=false (non-static growing mask) since both reduce to writing
total_length 1s to the target buffer. Falls back to CPU for batch_beam_size > 1.
Copilot AI review requested due to automatic review settings April 13, 2026 14:44

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 improves continuous decoding support when graph capture/static attention masks are used, by updating attention mask buffers more efficiently and correctly across backends (notably CUDA/static mask rewind handling and WebGPU mask updates).

Changes:

  • Implement CPU-side rewind/update logic for static attention masks used with graph capture (write attended positions to 1 and future positions to 0, then copy back to device).
  • Add a reusable CPU staging buffer and a WebGPU UpdateAttentionMask fast path for single-beam cases to avoid repeated allocations and redundant CPU fills.

Reviewed changes

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

File Description
src/webgpu/interface.cpp Adds a reusable staging buffer and implements WebGPU-side UpdateAttentionMask to upload an all-ones prefix for single-beam decoding.
src/models/position_inputs.cpp Replaces the previous “unsupported”/commented logic with a CPU-written static mask rewind implementation for graph capture.

Comment thread src/webgpu/interface.cpp
Comment thread src/webgpu/interface.cpp Outdated
Comment thread src/models/position_inputs.cpp
…ment

- Fix aliasing UB: change mask_staging_buffer_ from vector<uint8_t> to
  vector<int32_t>. For int64 masks, use std::memcpy to write int64_t(1)
  values without reinterpret_cast aliasing violations.
- Add bounds check in RewindMask: throw if index > max_length to prevent
  unsigned underflow in std::fill_n size calculation.
- Use size_t consistently for max_length in RewindMask to avoid
  signed/unsigned comparison surprises.
- Add threading model comment on mask_staging_buffer_: WebGPU/Dawn is
  single-threaded, document this assumption for future reference.
Comment thread src/models/position_inputs.cpp
…sistency

- Use ShouldUseStaticMaskHandling() instead of use_graph_capture to gate
  the static mask path, consistent with UpdateAttentionMask.
- Add dynamic mask fallback: set attention_mask_shape_[1] = index so the
  next Update() creates a correctly sized tensor. For batch_beam_size == 1
  the CPU UpdateAttentionMask fills the entire mask with 1s, so no data
  fixup is needed.
- Fix signed/unsigned type mixing: use size_t consistently for max_len,
  batch_beam_size, and loop indices in the static mask path.

Tested both paths on WebGPU:
- Static mask (phi4-graph-prune, enableGraphCapture=1): PASS
- Dynamic mask (phi4-prune, past_present_share_buffer=false): PASS
@qjia7

qjia7 commented Apr 15, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review comments in 80be21c:

1. RewindMask() now handles both static and dynamic mask paths

  • Static path (ShouldUseStaticMaskHandling() — graph capture or NvTensorRtRtx with shared buffers): overwrites the fixed [batch_beam_size, max_length] tensor with 1s for [0, index) and 0s for [index, max_length), then copies to device. Same as before but now gated on ShouldUseStaticMaskHandling() instead of just use_graph_capture, consistent with UpdateAttentionMask().

  • Dynamic path (everything else): sets attention_mask_shape_[1] = index. The next Update() call will:

    1. CreateNextAttentionMaskTensor(total_length) — creates a correctly sized tensor using the adjusted shape
    2. CPU UpdateAttentionMask for batch_beam_size == 1 — fills the entire new mask with 1s (ignores old data)
    3. Replaces attention_mask_ with attention_mask_next_

    No tensor recreation or is_first_update_ reset needed — RewindTo already restricts to batch_size == 1 (no padding), so a shape-only adjustment is sufficient.

2. Fixed max_len type mixingbatch_beam_size, max_len, and loop indices now all use size_t consistently.

Tested on WebGPU (NVIDIA RTX):

  • Static mask (phi4-graph-prune, enableGraphCapture=1): ✅
  • Dynamic mask (phi4-prune, past_present_share_buffer=false): ✅

Both tests: generate 10 tokens -> rewind_to(prompt_len + 5) -> generate 5 more tokens. Output is consistent and coherent after rewind.

@qjia7
qjia7 requested a review from kunal-vaishnavi April 15, 2026 08:26
qjia7 added 2 commits April 18, 2026 14:01
Add C++ and Python tests covering:
- RewindTo(0): full rewind and regenerate, verify identical output
- Multiple sequential RewindTo: rewind to 7, 5, then 0 in succession
- RewindTo with divergent tokens: rewind, append different tokens,
  then rewind again and recover original output

All tests use the bundled tiny-random-gpt2-fp32 model (CPU, no GPU needed).
@qjia7
qjia7 marked this pull request as draft April 20, 2026 01:42
Replace DML-specific test with NvTensorRtRtx EP test that uses the existing
phi3-fp16-nvtrt model. Uses GTEST_SKIP() when model is unavailable (no
compile guard needed). NvTensorRtRtx triggers ShouldUseStaticMaskHandling()
via past-present share buffer, exercising the static mask RewindTo path.

This test fails on main (RewindMask throws 'Static buffer is not supported
for continuous decoding') and passes with our fix.
@qjia7
qjia7 force-pushed the continuous-decoding-graph-capture branch from 5c071f2 to ed071d2 Compare April 20, 2026 01:51
@qjia7
qjia7 marked this pull request as ready for review April 20, 2026 04:47
Comment thread src/models/position_inputs.cpp
Comment thread src/webgpu/interface.cpp
Add a self-contained 1-layer GQA model (tiny-graph-capture-gqa) that enables
graph capture and past_present_share_buffer. This exercises the static mask
handling path in RewindTo without requiring a specific GPU or large model.

Model and tests are under a webgpu/ subfolder since graph capture is
WebGPU-EP-specific.

- test/test_models/webgpu/tiny-graph-capture-gqa/: 1-layer model with GQA,
  enableGraphCapture=1, past_present_share_buffer=true (~100KB)
- test/test_models/webgpu/tiny-graph-capture-gqa/create_model.py: regeneration script
- test/c_api_tests.cpp: RewindGraphCaptureGqaCAPI test (rewind_to(0) + rewind_to(7))
- test/python/test_onnxruntime_genai_api.py: test_rewind_graph_capture
- .gitignore: whitelist webgpu/ test model directory
@qjia7
qjia7 force-pushed the continuous-decoding-graph-capture branch from d0b1e6a to 1a62d40 Compare April 21, 2026 03:13
qjia7 added 2 commits April 21, 2026 11:16
og.is_webgpu_available() may return True even when the WebGPU EP cannot
actually run (e.g., CPU-only CI builds with WebGPU compiled in but no GPU).
Catch the RuntimeError from model loading and skip gracefully.

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

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

Comment thread src/webgpu/interface.cpp
Comment thread test/test_models/webgpu/tiny-graph-capture-gqa/create_model.py Outdated
Comment thread test/test_models/webgpu/tiny-graph-capture-gqa/create_model.py Outdated
- UpdateAttentionMask: add (void) casts for unused params (next_mask_data,
  new_kv_length, max_length) and validate type is int32 or int64
- create_model.py: fix comment to match actual computation
  (ReduceMax(sum_mask), not ReduceMax(seqlens_k) + 1)
- create_model.py: change default --output from hard-coded Windows path
  to portable relative path
Comment thread test/test_models/webgpu/tiny-graph-capture-gqa/create_model.py Outdated

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

Copilot reviewed 8 out of 10 changed files in this pull request and generated 1 comment.

Comment thread src/webgpu/interface.cpp Outdated
qjia7 added 3 commits April 21, 2026 14:59
The Python test_rewind_graph_capture caused fatal aborts on non-WebGPU CI
machines. The C++ RewindGraphCaptureGqaCAPI test (guarded by #if USE_WEBGPU)
is sufficient and doesn't have this issue.
Remove RewindGraphCaptureGqaCAPI test (#if USE_WEBGPU) and its tiny-graph-capture-gqa
model files. The WebGPU CI doesn't set USE_WEBGPU=ON so the test was never compiled.
Will add WebGPU-specific tests in a separate PR with proper CI configuration.

Keep RewindGraphCaptureNvTensorRtRtxCAPI which tests the same RewindTo + static mask
code path via NvTensorRtRtx past-present shared buffers.
Comment thread src/webgpu/interface.cpp Outdated
@qjia7
qjia7 requested a review from kunal-vaishnavi April 23, 2026 05:40
Comment thread src/models/position_inputs.cpp
Comment thread src/models/position_inputs.cpp
Comment thread src/webgpu/interface.cpp
@kunal-vaishnavi
kunal-vaishnavi merged commit 2a2ef8c into main Apr 24, 2026
16 of 17 checks passed
@kunal-vaishnavi
kunal-vaishnavi deleted the continuous-decoding-graph-capture branch April 24, 2026 02:14
baijumeswani pushed a commit that referenced this pull request May 19, 2026
…sts (#2099)

- Add enable_graph_capture flag to model download config in
_test_utils.py
- When enable_graph_capture=True and device=webgpu, pass
enable_webgpu_graph=true to the model builder so generated models have
enableGraphCapture=1
- Enable graph capture for qwen-2.5-0.5b model (used by guidance tests)
- Remove #if !USE_DML guard from multi-turn guidance tests since PR
#2083 fixed RewindTo for DML (DML always uses graph capture/static mask)
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