Skip to content

perf(cuda,engine): batched GDN trunk — fused delta-net scan + batched-query SDPA (#114-B)#117

Merged
pekkah merged 2 commits into
masterfrom
perf/gdn-batched-trunk-residual-114
Jun 3, 2026
Merged

perf(cuda,engine): batched GDN trunk — fused delta-net scan + batched-query SDPA (#114-B)#117
pekkah merged 2 commits into
masterfrom
perf/gdn-batched-trunk-residual-114

Conversation

@pekkah

@pekkah pekkah commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Issue #114 direction (B): batch the per-position GDN-hybrid prefill trunk

#114-A (register-tiled N-input routed-MoE dots, PR #116) made the routed MoE flat
with context. With that merged, the trunk became the dominant prefill cost and
it grew with context: profiled on a 4070 Ti (SHARPI_PREFILL_PROFILE=1), trunk
≈1.9s at startPos 0, climbing ~+0.5ms/token to ~20s at ~34K cumulative tokens.

Cause: #111 GEMM-batched the trunk projections but deliberately left conv1d,
the delta-net recurrence (GdnRecurrenceDecode), KV-append and SDPA per-position
over N via CudaBackend.View. GDN launches dominate ~12:1 over attention, so the
trunk was launch-overhead bound at short context and under-utilised the GPU at long
context. CudaBackend.FullSeqAttention previously threw.

What this does

Collapses those per-position launches into batched kernels, bit-identical to the
per-token TrunkLayerSequential path.

New CUDA kernels (CudaTextKernels.cs):

  • llm_gdn_recurrence_scan — one block per v-head loops the N positions internally,
    carrying per-head state in global memory. This is the fused sequential scan, not
    the parallel chunked-scan (which reorders FP reductions and would break parity).
  • llm_gdn_conv1d_decode_batched (+ _state_update_batched), _l2_norm_per_head_batched,
    _tile_heads_batched — each GDN sub-stage batched over N.
  • llm_kv_append_batched (+bf16) and llm_full_seq_attention (+bf16) — the N prompt
    queries attend over their causal prefix in one launch (shared-scores path,
    startPos+N ≤ 4096; falls back to the per-position loop beyond — wave-based >4096
    batched SDPA is a noted follow-up).

Host wiring (CudaHybridGdnForwardPass): GdnBlockBatched / AttnBlockBatched
use the batched paths behind SHARPI_BATCHED_GDN_SCAN / SHARPI_BATCHED_ATTN
(default on, A/B-toggleable like SHARPI_BATCHED_TRUNK).

Parity (hard constraint)

  • 6 new per-kernel bit-exactness tests (CudaGdnBatchedTrunkTests): conv1d +
    state-advance, L2-norm, tile-heads, fused recurrence scan (output and final
    state), batched KV-append + SDPA (fp32 and bf16) — all bit-identical to the
    single-token kernels.
  • Existing oracles green: BatchedTrunk_BitwiseMatchesSequentialTrunk_Carnice,
    BatchedPrefill_BitwiseMatchesSequential_Carnice (single + multi-chunk),
    MtpDecoder_GreedyParity_LlamaCpp.
  • Full Tests.ForwardPass suite: 0 failures. (A process-exit crash during a
    Vulkan-shader test's teardown is a pre-existing, non-deterministic GPU-driver flake,
    unrelated to these CUDA changes.)

Performance (warm, RTX 4070 Ti)

A/B vs the #111 per-position trunk (SHARPI_BATCHED_GDN_SCAN/_ATTN off vs on):

README

The throughput table's Prefill t/s column was previously measured on ~10–60-token
prompts (launch-overhead noise). Re-benchmarked every on-disk row at a uniform
~1K-token warm-cache context so the column is comparable; decode left as the
near-zero-ctx generation rate (added a methodology note). Llama-4 Scout (not on disk)
and the Qwen3-Coder Vulkan-hybrid row (errored at 1K) keep their prior values, annotated.

🤖 Generated with Claude Code

…-query SDPA (#114-B)

Collapse the per-position GDN-hybrid prefill trunk into batched CUDA launches,
bit-identical to the per-token TrunkLayerSequential path. #111 GEMM-batched the
trunk projections but left conv1d, the delta-net recurrence, KV-append and SDPA
per-position over N via CudaBackend.View, so the trunk dominated prefill and grew
with context (profiled ~2.3s@startPos0 -> 16s+ at ~25K cumulative tokens).

New CUDA kernels (CudaTextKernels.cs):
- llm_gdn_recurrence_scan: one block per v-head loops the N positions internally,
  carrying per-head state in global memory — the fused *sequential* scan (NOT the
  parallel chunked-scan, which would reorder FP reductions and break parity).
- llm_gdn_conv1d_decode_batched (+ _state_update_batched), _l2_norm_per_head_batched,
  _tile_heads_batched: per-stage batched over N.
- llm_kv_append_batched (+bf16) and llm_full_seq_attention (+bf16): the N prompt
  queries attend over their causal prefix in one launch (shared-scores path,
  startPos+N <= 4096; falls back to the per-position loop beyond).

Host wiring (CudaHybridGdnForwardPass): GdnBlockBatched / AttnBlockBatched use the
batched paths behind SHARPI_BATCHED_GDN_SCAN / SHARPI_BATCHED_ATTN (default on,
A/B-toggleable like SHARPI_BATCHED_TRUNK).

Parity: 6 new per-kernel bit-exactness tests (CudaGdnBatchedTrunkTests), plus the
existing BatchedTrunk / BatchedPrefill (single+multi-chunk) / MtpDecoder_GreedyParity
oracles — full Tests.ForwardPass suite green (0 failures).

Perf (warm, 4070 Ti, ~4K-ctx A/B vs the #111 per-position trunk): trunk -35..-50% and
~5x flatter context-growth slope; end-to-end prefill +17% (Qwen3.6-35B-A3B / -MTP)
to +31% (Carnice). README throughput table re-benchmarked at a uniform ~1K-token
warm-cache context across all on-disk rows (prefill column); decode left as the
generation-start rate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request implements batched GDN trunk and batched-query scaled dot-product attention (SDPA) kernels (Issue #114-B) to collapse per-position decode launches into single batched launches, significantly improving prompt prefill performance. It includes a new benchmarking script (bench-allrows-1k.ps1) and updates the README with consistent ~1K-token prefill measurements. Additionally, a comprehensive suite of bit-exactness tests is added to ensure the batched implementations match sequential outputs. Review feedback identified a potential stack buffer overflow in the llm_gdn_conv1d_state_update_batched CUDA kernel where the local array tmp is hardcoded to size 4, recommending a larger size and a safety guard for larger kernel sizes.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +3018 to +3020
int retained = kernel_size - 1;
float tmp[4]; // K-1 <= 4 for our models
#pragma unroll 4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The local array tmp is hardcoded to size 4 with the assumption that kernel_size - 1 <= 4. If a model with a larger kernel_size is loaded, this will cause a stack buffer overflow/corruption in the CUDA kernel. To prevent undefined behavior or GPU crashes, we should increase the array size to a safe maximum (e.g., 16) and add a guard to safely handle cases where retained exceeds this limit.

    int retained = kernel_size - 1;
    if (retained > 16) return;
    float tmp[16];
    #pragma unroll

…lback observability, host-wiring oracle

From the pr-review-toolkit cycle on PR #117:

- comment-analyzer: refresh TrunkLayerBatched/GdnBlockBatched/AttnBlockBatched summaries that still claimed the recurrence/SDPA stay per-position (they batch by default since #114-B).

- silent-failure-hunter (MED): log once when the batched-query SDPA falls back to the per-position path past the 4096 shared-scores window (bit-identical, just slower) so the perf cliff is observable.

- pr-test-analyzer (sev 9): add BatchedGdnScanAndAttn_BitwiseMatchesPerPosition_Carnice — the first model-level oracle that toggles BatchedGdnScanEnabled/BatchedAttnEnabled off-vs-on, proving the GdnBlockBatched/AttnBlockBatched host glue (strides/offsets) is bit-identical to the per-position View-loop fallback. Plus conv/scan N=1 (max state-aliasing edge) and the exact startPos+N==4096 attention boundary in the per-kernel tests.

All 7 added/expanded tests green (bit-identical).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pekkah
pekkah merged commit 3de6c82 into master Jun 3, 2026
1 check passed
@pekkah
pekkah deleted the perf/gdn-batched-trunk-residual-114 branch June 3, 2026 19:14
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.

1 participant