[WebGPU] Support continuous decoding (RewindTo) with graph capture - #2083
Conversation
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.
There was a problem hiding this comment.
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
UpdateAttentionMaskfast 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. |
…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.
…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
|
Addressed the review comments in 80be21c: 1.
2. Fixed Tested on WebGPU (NVIDIA RTX):
Both tests: generate 10 tokens -> |
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).
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.
5c071f2 to
ed071d2
Compare
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
d0b1e6a to
1a62d40
Compare
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.
- 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
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.
…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)
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:
DefaultPositionInputs::RewindMaskwith 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 bothint32_tandint64_ttypes.WebGPU backend improvements:
mask_staging_buffer_) to theInterfaceImplstruct for efficient attention mask updates, avoiding repeated allocations and redundant writes.UpdateAttentionMaskmethod 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 bothint32_tandint64_tmask types.