Skip to content

[CUDA] Extend GatedDeltaNet and CausalConvWithState for S<=8 speculative decode - #33471

Open
Akshay Sonawane (apsonawane) wants to merge 21 commits into
mainfrom
asonawane/main-ops-gdn-convolution
Open

Akshay Sonawane (apsonawane) wants to merge 21 commits into
mainfrom
asonawane/main-ops-gdn-convolution

Conversation

@apsonawane

@apsonawane Akshay Sonawane (apsonawane) commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

Extends com.microsoft::CausalConvWithState and com.microsoft::GatedDeltaNet on the CUDA Execution Provider to support $S \le 8$ speculative decoding, packed-channel / packed-QKV projections, tiled $K=4$ prefill convolution, and compact state_update capture (state_update_capacity in [0, 8]).

Summary of Changes

com.microsoft::CausalConvWithState Extended Projection, Tiled Prefill & Compact state_update

File Change
onnxruntime/core/graph/contrib_ops/bert_defs.cc Adds input_channels, input_channel_offset, state_length, state_channels_last, and state_update_capacity (0..8) attributes plus optional Output(2, "state_update") ([B, state_update_capacity, C]), documenting that unused tail rows beyond min(sequence_length, state_update_capacity) are zero-filled
onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state.{h,cc}, causal_conv_with_state_impl.{h,cu} Implements the extended packed-channel CUDA path with: (1) CausalConvProjectionS2K4Kernel for short decode ($1 \le S \le 8$, kernel_size=4, state_length=4, dilation=1), (2) CausalConvProjectionTiledK4Kernel (8 positions per CTA with sliding registers) for prefill ($S \ge 16$), and (3) a graph-capturable cudaMemset2DAsync zero-fill for unused state_update rows when length < state_update_capacity
onnxruntime/contrib_ops/cpu/bert/causal_conv_with_state.{cc,helper.h}, onnxruntime/contrib_ops/webgpu/bert/causal_conv_with_state.cc Rejects CUDA-only extended projection attributes/outputs cleanly on CPU and WebGPU

com.microsoft::GatedDeltaNet Packed QKV, TG Gates & $S \le 8$ QwenS2 Decode

File Change
onnxruntime/core/graph/contrib_ops/bert_defs.cc Adds TG type constraint so decay and beta can match the query element type (float, float16, bfloat16), and documents gate_activation="softplus" with "qwen" retained as a legacy alias
onnxruntime/contrib_ops/cuda/bert/gated_delta_net.{h,cc}, gated_delta_net_impl.{h,cu}, gated_delta_net_split_impl.cu, gated_delta_net_mma.cuh, gated_delta_net_plan.h Supports packed QKV without intermediate unpacking on short decode ($S \le 8$), templated GateT for decay/beta, compact state_update_capacity capture (0..8), and the 8-warp QwenS2 recurrent decode specialization (ORT_GDN_S2_SPECIALIZE=1 by default) for single-request $1 \le S \le 8$, 16 Q/K heads, 48 V heads, and head size 128
onnxruntime/contrib_ops/webgpu/bert/gated_delta_net.cc Supports "softplus" and legacy alias "qwen" for gate_activation

Tests and Documentation

  • onnxruntime/test/contrib_ops/causal_conv_with_state_op_test.cc: Covers packed-channel extended history across lengths {1, 2, 3, 5, 8, 9, 16, 17, 24} (exercising both CausalConvProjectionS2K4Kernel and CausalConvProjectionTiledK4Kernel), full and partial channel blocks, both state layouts, present/absent past history, FP32/FP16/BF16, state_update capture verification, and tail zeroing when state_update_capacity > sequence_length.
  • onnxruntime/test/contrib_ops/gated_delta_net_op_test.cc: Covers input-type decay/beta (FP16/BF16), QwenS2 ($S \in {1, 2, 4, 8}$) packed-QKV parity, state_update_capacity capture, empty requests in recurrent state, and bit-exact parity between gate_activation="softplus" and "qwen" on CUDA and WebGPU.
  • docs/ContribOperators.md, docs/OperatorKernels.md, docs/contrib_ops/cuda/gdn.md: Syncs operator schemas/kernel tables and documents ORT_GDN_S2_SPECIALIZE / ORT_CAUSAL_CONV_S2_SPECIALIZE eligibility and H200 op-level & end-to-end benchmarks.

Performance

Measured on NVIDIA H200 (SM90, CUDA 13.0) at Qwen3.8-Flash-Next RC3 geometry ((B, S, 10240) BF16, state_update_capacity=7):

  • GatedDeltaNet (QwenS2): 1.15x–1.25x faster across $S \in {1, 2, 4, 8}$ (18.82 us vs 21.60 us at $B=1, S=8$), saving ~0.10–0.15 ms per DFlash2 $S=8$ verification cycle across 36 layers.
  • CausalConvWithState: 1.19x–1.37x faster on short decode ($S \le 8$) and 4.06x faster on prefill at $S=2048$ (55.13 us vs 223.97 us), reducing end-to-end TTFT by 2.9–3.1% at 2K–32K context lengths.

Testing

  • Run provider unit tests:
    onnxruntime_provider_test --gtest_filter="CausalConvWithStateTest.*:GatedDeltaNetTest.*:GatedDeltaNetWebGpuTest.*"
  • End-to-end verification on qwen_flash_next_int4_int8_rc3_contrib_dflash2 ($S=8$, state_update_capacity=7).

Checklist

  • Tests added/updated
  • No breaking changes (or documented in description)
  • Documentation updated (if applicable)

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.

🟡 Changes recommended

State initialization, regression expectations, and generated documentation need correction before approval.

3 open issues
What changed in this PR

Extends ONNX Runtime’s GatedDeltaNet and causal convolution operators with CUDA-focused fused input and state handling while preserving default modes.

Changes:

  • Adds activation-type GDN gates, direct packed-QKV decoding, and intermediate-state output.
  • Adds convolution channel slicing, configurable state layout/history, and first-state output.
  • Updates schemas, provider validation, and regression tests.
File Description
onnxruntime/​test/​contrib_ops/​gated_delta_net_op_test.cc Adds gate, packed-QKV, and intermediate-state tests.
onnxruntime/​test/​contrib_ops/​causal_conv_with_state_op_test.cc Tests extended convolution modes and validation.
onnxruntime/​core/​graph/​contrib_ops/​bert_defs.cc Extends schemas and shape inference.
onnxruntime/​contrib_ops/​webgpu/​bert/​gated_delta_net.cc Adds alias support and capability guards.
onnxruntime/​contrib_ops/​webgpu/​bert/​causal_conv_with_state.cc Rejects CUDA-only extensions.
onnxruntime/​contrib_ops/​cuda/​bert/​gated_delta_net.h Adds intermediate-state configuration.
onnxruntime/​contrib_ops/​cuda/​bert/​gated_delta_net.cc Handles typed gates, packed inputs, and state outputs.
onnxruntime/​contrib_ops/​cuda/​bert/​gated_delta_net_split_impl.cu Loads activation-type gates.
onnxruntime/​contrib_ops/​cuda/​bert/​gated_delta_net_plan.h Renames the fused decay enum.
onnxruntime/​contrib_ops/​cuda/​bert/​gated_delta_net_mma.cuh Uses the renamed decay mode.
onnxruntime/​contrib_ops/​cuda/​bert/​gated_delta_net_impl.h Extends kernel arguments and gate loading.
onnxruntime/​contrib_ops/​cuda/​bert/​gated_delta_net_impl.cu Implements typed gates, packed decoding, and state capture.
onnxruntime/​contrib_ops/​cuda/​bert/​causal_conv_with_state.h Stores extended convolution attributes.
onnxruntime/​contrib_ops/​cuda/​bert/​causal_conv_with_state.cc Validates and dispatches extended modes.
onnxruntime/​contrib_ops/​cuda/​bert/​causal_conv_with_state_impl.h Declares the extended kernel launcher.
onnxruntime/​contrib_ops/​cuda/​bert/​causal_conv_with_state_impl.cu Implements sliced convolution and state outputs.
onnxruntime/​contrib_ops/​cpu/​bert/​causal_conv_with_state.cc Rejects CUDA-only extensions.
docs/​ContribOperators.md Removes a trailing blank line only.

Review effort: Balanced

Comment thread onnxruntime/contrib_ops/cuda/bert/gated_delta_net_impl.cu Outdated
Comment thread onnxruntime/core/graph/contrib_ops/bert_defs.cc
Comment thread onnxruntime/test/contrib_ops/gated_delta_net_op_test.cc Outdated
Initialize mid_state for empty ragged requests in both recurrent kernels, correct the softplus reference alias, and add regression coverage. Use the adapter-supported output-count API and synchronize operator documentation with the extended schemas.

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.

🔵 Needs a closer look

Unresolved optional-output handling, weight-index overflow, and no-exception test failures block approval.

No open issues 🎉

3 previously missed issues

In code that hasn't changed since last review

Medium severity Prevent 32-bit overflow in weight offset calculation

onnxruntime/​contrib_ops/​cuda/​bert/​causal_conv_with_state_impl.cu:48

The weight offset uses signed 32-bit multiplication. With 65,537 channels, a 32,768-tap kernel, and dilation 1, the extended-path dimension checks pass, but channel 65,536 computes c * kernel_size == 2^31. This overflows the index and can cause an out-of-bounds device read. Cast c to int64_t before multiplication, as the input offsets already do.

Medium severity Detect optional output presence instead of output count

onnxruntime/​core/​graph/​contrib_ops/​bert_defs.cc:4772

An omitted optional output may be encoded as outputs=["output", "present_state", ""]. Its placeholder still counts toward getNumOutputs(), so this condition rejects a valid default channels-first convolution as requiring extended mode. The CPU constructor, CUDA extended-path selection, and WebGPU output guard repeat the same count-only check. Check actual output presence throughout: use ctx.hasOutput(2) in inference, NodeArg::Exists() where available, and a plugin-compatible presence check for WebGPU (for example, checking Output(2, state_shape) after the shape is known). Add an AddOptionalOutputEdge regression covering these provider paths.

Medium severity Guard rejection tests against no-exception builds

onnxruntime/​test/​contrib_ops/​causal_conv_with_state_op_test.cc:403

Both new rejection tests can terminate no-exception test runs. With CUDA available, PackedChannelsInvalidConfigurations reaches fail_shape_inference; PackedChannelsRejectsUnsupportedCpu reaches the constructor's ORT_ENFORCE even in CPU-only builds. Under ORT_NO_EXCEPTIONS, these paths abort rather than return a failure that kExpectFailure can check. Wrap both tests (lines 403–441) in #ifndef ORT_NO_EXCEPTIONS, following the existing shape-inference rejection tests below, or add a conditional GTEST_SKIP() before constructing each tester.

3 resolved since last review

Review effort: Balanced

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

Copy link
Copy Markdown
Contributor Author

Added the scoped GDN/convolution decode experiments on top of the current PR head, preserving the existing empty-request and WebGPU review fixes.

Published commit: 62bd923adb13df65127adccd4d7162bb539e41df.

  • Opt-in ORT_GDN_S2_SPECIALIZE=1: guarded packed-QKV, single-request/two-token specialization (16 query/key heads, 48 value heads, head size 128).
  • Opt-in ORT_CAUSAL_CONV_S2_SPECIALIZE=1: length-two, kernel/state-four, dilation-one specialization.
  • ORT_GDN_DECODE_WARPS=2|4|8, default 8.
  • Added exact FP16/BF16 packed-QKV parity tests and documented eligibility/fallbacks.
  • No multi-draft state outputs, dense-prefill normalization experiments, weights, binaries, or benchmark artifacts were included.

Validation of the publishing sources: all changed CUDA translation units compile. Process-isolated native tests pass with specialization switches both off and on: 6 focused GDN packed/input-type tests and 53 convolution tests per mode. This is an incremental validation against retained build dependencies, not a clean full upstream build.

The broader GatedDeltaNetTest.RecurrentMidStatePreservesEmptyRequests test fails in this incremental build. I also compiled the unmodified PR-head GDN kernel and reproduced an invalid-argument failure (32-wide case). The broader run can poison later CUDA tests, so it is not reported as clean. Clean-build CI is still required; this update does not attempt an unrelated baseline fix.

Historical experimental-package benchmarks improved 8K decode from 213.51 to 228.06 TPS and 16K from 227.85 to 242.60 TPS in a controlled bracket. These numbers are not a re-benchmark of the transplanted PR tip. Full model accuracy remains deferred, and this is not a claim of sustained 250 TPS at 8K.

Companion updates: sparse attention/indexer #33472, MoE/FP8 #33473, and GenAI packaging microsoft/onnxruntime-genai#2759.

@apsonawane

Copy link
Copy Markdown
Contributor Author

Fresh-build integration validation update (no source changes pushed to this ORT PR in this update):

  • Rebuilt ORT from scratch for H200 (sm90a), CUDA provider as a plugin, with FP4 QMoE, FP8/INT4 KV cache, GQA value layout, and FPA-INTB GEMM enabled. onnxruntime_QUICK_BUILD=OFF; no retained build objects were reused.
  • Rebuilt GenAI against the fresh ORT headers/libraries, and rebuilt the fold custom library.
  • Focused convolution/GDN/fused-tensor/hyper-connection/INT2 tests: 193 passed.
  • Sparse/indexer/FP8 suite plus the isolated QSA capacity test: 140 additional passes.
  • Known limitations: QSA and CSA MalformedStateLengthsAreClamped tests still fail, including with specialized top-k disabled. These are not being claimed as passing. This is selected-suite validation, not a clean full-suite qualification.
  • Selected GenAI Qwen/executor/weight-pool tests: 69 passed, including the opt-in real-GPU mapped-weight test with proper CUDA plugin registration. One additional test is disabled.

The latest fused INT2 model completed one warmup plus three measured requests at each of 2K/8K/16K/32K, generating 512 tokens each. All 16 requests matched the previous-runtime token IDs exactly, with zero generation failures or standard fallbacks. Runtime-map inspection confirmed only the fresh ORT/GenAI libraries were used.

Prompt Pooled decode TPS Mean TTFT
2048 269.90 0.584 s
8192 250.17 2.448 s
16384 266.92 5.065 s
32768 264.12 10.578 s

Evidence scope: this is the full dirty experimental source stack containing this PR's optimizations plus other local work, not a standalone build/benchmark of this exact PR head. It is not an isolated speedup attribution, a fresh paired reference comparison, or proof of cross-process sustained 250 TPS. Full model accuracy qualification remains deferred.

The rebuild exposed local expert-cache function-pointer and INT2 direct-link test wiring issues, which were fixed locally. Those expert-cache extensions are not in the published PR and have deliberately not been mixed into this update.

…ations

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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e seq_len==0 blocks in GatedDeltaNet

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tianleiwu Tianlei Wu (tianleiwu) changed the title Extend GDN and causal convolution fused state handling [CUDA] Extend GatedDeltaNet and CausalConvWithState for S<=8 decode and prefix states Oct 24, 2026
…DeltaNet docs

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

- Add state_update_capacity (0..8) attribute, shape inference, and device emission to extended CausalConvWithState (output 2 of shape [batch, state_update_capacity, channels]).
- Enable state_update_capacity > 0 in the QwenS2 warp-specialized GatedDeltaNetDecodeWarpKernel and gate state_update_active on cu_seqlens != nullptr || total_tokens <= (state_update_capacity + 1) * batch.
- Preserve packed-QKV uniform prefill engine/precision selection in GatedDeltaNet and pass DefaultCudaExecutionProvider() in RecurrentOutputsIncludeFirstRowState for TensorRT CI compatibility.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tianlei Wu (tianleiwu) added a commit that referenced this pull request Oct 24, 2026
#33473 (S<=8 decode & fused validation)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tianlei Wu (tianleiwu) added a commit that referenced this pull request Oct 24, 2026
…2 QMoE, and PR cleanups

- Extend FoldMid2/FoldTail to head_rows<=8 and pack tokens[4..7] into ring record words 1, 2, 3, and 5.
- Add state_update_capacity (0..8) support to CausalConvWithState and enable state_update_capacity > 0 in QwenS2 GatedDeltaNetDecodeWarpKernel.
- Raise kMaxProfiledExpandedRows to 80 in moe_gemv.h and kFusedRouteMaxRows to 8 in qmoe_fused_route.cuh; add fast INT3 and offline SM80 INT2/INT4 prepack QMoE support.
- Eliminate pq[8][ROWS] local register array in HcUpPreKernel and sync PR cleanups (#33469, #33470, #33471, #33472, #33474, #33548, #33569).

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

- Remove unused first_state/second_state/third_state outputs from CausalConvWithState and name output 2 state_update (used when state_update_capacity > 0).
- Remove unused state_output attribute and mid_state/mid_state2/mid_state3 outputs from GatedDeltaNet, keeping compact state_update capture.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@tianleiwu Tianlei Wu (tianleiwu) changed the title [CUDA] Extend GatedDeltaNet and CausalConvWithState for S<=8 decode and prefix states [CUDA] Extend GatedDeltaNet and CausalConvWithState for S<=8 speculative decode Oct 25, 2026
…ync docs

- Use standard SelectPlan engine and BF16 precision selection for unpacked packed-QKV prefill so packed and unpacked QKV take the same chunked path at tokens >= 32 (fixing GatedDeltaNetTest.DirectPackedQkv{Float16,BFloat16}ExactParity).
- Sync docs/ContribOperators.md with gen_contrib_doc.py output for CausalConvWithState state_update.

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

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.

🟡 Changes recommended

Requested prefix capture can be silently disabled, and valid empty optional convolution outputs are rejected.

2 open issues

Review effort: Balanced

Comment thread onnxruntime/contrib_ops/cuda/bert/gated_delta_net.cc Outdated
Comment thread onnxruntime/core/graph/contrib_ops/bert_defs.cc Outdated
…prefill

Add CausalConvProjectionTiledK4Kernel for length >= 16 when kernel_size == 4,
dilation == 1, and state_length == 4, reusing weights, bias, and the 3-element
input halo in registers across 8 consecutive token positions per CTA. Cuts
36-layer GDN CausalConvProjection prefill at L=2048 on H200 from 8.53 ms to
2.03 ms (4.10x faster).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Treat an empty trailing state_update output as omitted in shape inference
  and the CPU, CUDA and WebGPU CausalConvWithState kernels.
- Remove the sequence-length veto that silently disabled uniform
  GatedDeltaNet state capture when S > state_update_capacity + 1.
- Use 64-bit weight offsets in the extended CausalConv projection kernels.
- Add regression tests for all three.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The CUDA and WebGPU CausalConvWithState kernels called
info.node().OutputDefs() to detect the optional state_update output.
ep::adapter::Node (used by the CUDA plugin EP and WebGPU plugin/static
plugin builds) has no OutputDefs(), so those builds failed to compile.

Add causal_conv_with_state_helper::HasOutput, which uses the adapter's
OutputCount()/OutputExists() when available and falls back to
OutputDefs() for the in-tree Node, and use it in the CPU, CUDA and
WebGPU kernels.

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

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.

🟡 Changes recommended

Default-enabled tiled convolution and packed Qwen compact capture need targeted regression coverage before merging.

1 open issue
1 previously missed issue

In code that hasn't changed since last review

Medium severity Add compact-capture parity coverage for QwenS2 packed specialization

onnxruntime/​contrib_ops/​cuda/​bert/​gated_delta_net_impl.cu:883

The default-enabled packed QwenS2 specialization lacks compact-capture coverage: TestQwenS2PackedParity uses capacity zero, while the capture regression uses separate Q/K/V and different head counts. Add FP16/BF16 cases with S=8, capacity=7, and full, partial, and inactive capture to validate speculative rollback data. Compare defined captured prefixes against separate-QKV recurrent execution or independent prefix replay, without comparing the unspecified unused suffix.

2 resolved since last review

Review effort: Balanced

Comment thread onnxruntime/contrib_ops/cuda/bert/causal_conv_with_state_impl.cu
…' only

The alias was introduced alongside 'softplus' in this PR. Use a single
spelling across the schema, CUDA/WebGPU kernels, docs and tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rows of the fixed-capacity state_update output beyond min(length, capacity)
were never written. Zero them with a graph-capturable strided memset in the
launcher. Document that ORT_CAUSAL_CONV_S2_SPECIALIZE also gates the tiled
length>=16 kernel and that the activation attribute is honored as configured.

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

Cover lengths 16/17/24 (CausalConvProjectionTiledK4Kernel) across both state
layouts, present/absent history, full and partial channel blocks, and
FP32/FP16/BF16, with positive state_update capacity in short-K4 and tiled
cases. Add a regression where capacity exceeds the sequence length.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Revert the alias removal: CUDA and WebGPU accept 'qwen' again with behavior
identical to 'softplus'. Schema and docs now name 'softplus' canonical and
'qwen' a legacy alias; error messages use the canonical spelling. Tests use
'softplus' and add alias coverage (CUDA bit-exact vs softplus, WebGPU ragged).

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

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

Adapt the split-engine exact-BF16 fallback added by #33523 to this branch's type-erased gate buffers. VariantPack::decay and beta are now const void*, so RunScanStepExactBf16Fallback must read them through LoadGate<T> rather than indexing them directly. The merge is textually clean but nvcc rejects it ("expression must be a pointer to a complete object type" in gated_delta_net_split_impl.cu), which failed every CUDA CI job that builds the merge ref.

Add a regression test that drives that fallback with float and input-type (BF16) gates and requires bit-identical results.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants