Skip to content

[GDN2] Add Hopper SM90 CuTe DSL prefill - #113

Open
Aharrypotter wants to merge 5 commits into
inclusionAI:mainfrom
Aharrypotter:gdn2-sm90-release
Open

[GDN2] Add Hopper SM90 CuTe DSL prefill#113
Aharrypotter wants to merge 5 commits into
inclusionAI:mainfrom
Aharrypotter:gdn2-sm90-release

Conversation

@Aharrypotter

@Aharrypotter Aharrypotter commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📌 Description

This PR adds the first production Gated DeltaNet-2 (GDN2) prefill backend for
NVIDIA Hopper SM90a, implemented as a fully fused CuTe DSL kernel.

  • exposes cula.gdn2.chunk_gdn2 with direct architecture dispatch and no
    FLA, Triton, C++, or environment-selected fallback;
  • supports packed variable-length BF16 prefill, optional initial/final FP32
    recurrent state, MHA, GVA2, and GVA4 head mappings;
  • uses one 384-thread CTA per sequence with TMA/WGMMA and a load-balanced
    longest-processing-time sequence-to-CTA ordering;
  • keeps total tokens and sequence count dynamic within a dispatch route,
    with the compile cache keyed by device, value-head count, the two state
    modes, and two shape-derived route booleans;
  • computes the intra-chunk factor matrices from a blockwise-rebased form
    whose stored exponents are bounded, so no valid input overflows FP32;
  • adds an independent pure-PyTorch tokenwise reference, production, boundary
    and adversarial-decay correctness tests, a deterministic stress matrix,
    four-tool Compute Sanitizer coverage, benchmarks, and documentation.

Across the frozen five-row H20 matrix, the fused kernel is faster than the
pinned FLA GDN2 Triton public path on every row: 2.165x equal-weight
geometric-mean speedup and 1.319x minimum row speedup.

Revision note. The first revision of this PR reported 2.379x. That
number came from an intra-chunk factorization that overflows FP32 for
inputs the public contract accepts, which review correctly rejected. The
difference is the cost of the numerically stable form and is largely
intrinsic; see Numerical stability cost.

Supported SM90 contract

Dimension Supported values
Architecture SM90a
Query heads Hq=16
Value heads Hv={16,32,64}
Attention modes MHA, GVA2, GVA4
Packed sequences 1 <= N <= 32, each sequence non-empty
Total tokens Dynamic within the supported packed-sequence contract
Key/value dimensions K=V=128
Q/K/V/B/W dtype BF16
G/state dtype FP32
Log decay g finite, elementwise in [-5, 0]
CuTeDSL nvidia-cutlass-dsl>=4.5.1,<4.7 (both endpoints exercised on H20)
Public state layout [N,Hv,V,K]
State modes Initial state and final-state output independently optional
Output allocation Allocating or caller-provided output/state buffers

Kernel and dispatch

  • fully fused raw-G recurrent prefill kernel;
  • load-balanced longest-processing-time sequence-to-CTA ordering;
  • one CTA per sequence, 384 threads per CTA, and one CTA-wide barrier;
  • compile cache keyed by (device, Hv, has_initial_state, store_final_state, use_n1_hv16_v64, retain_final_tail); the last two are
    shape-derived route booleans, so T and N stay dynamic within a
    dispatch route while crossing the N=1 / T<=64 route boundaries
    compiles a new specialization (at most three per final-state mode). See
    docs/gdn2_sm90_pipeline.md and test_compile_cache_boundaries;
  • blockwise-rebased intra-chunk factorization: every stored operand exponent
    spans at most 15 in-block token gaps
    (docs/gdn2_sm90_stable_factor.md);
  • direct product backend identity
    sm90a_cutedsl_gdn2_prefill_v1;
  • unsupported inputs fail before compilation or launch.

🔍 Related issue and scope

  • This PR is based directly on main; it does not depend on PR [GDN] Add Hopper SM90 CuTe DSL prefill #108.
  • GDN2 is self-contained under cula.gdn2 / cula.ops.gdn2 and has no
    source or import dependency on the existing GDN-v1 kernel.
  • This PR body is bound to pushed head 36a4946.
  • The performance denominator is pinned FLA GDN2 Triton commit
    d1ce07369d581813553f30a750af3b6b5f9af6a9.
  • Partially addresses [Feature] Add GDN2 support #112 for SM90 only. SM100 implementation and actual
    SM100 validation remain a separate follow-up; this PR does not close the
    issue.

🧪 Final-source validation

Validation environment

GPU:                  NVIDIA H20-3e
Driver:               595.45.04
CUDA compiler:        13.1.r13.1/compiler.37061995_0
Python:               3.12.13
PyTorch:              2.9.1+cu130
PyTorch CUDA:         13.0
Triton:               3.5.1
nvidia-cutlass-dsl:   4.5.1
cuda-python:          13.2.0
FLA commit:           d1ce07369d581813553f30a750af3b6b5f9af6a9
FLA tree:             e5ea97e3041c3e4dd0bf6974c2259f7ed104ddc2

Validation summary

Gate Coverage Result
Final H20 product suite 22 tests 22 passed in 386.92s
Independent PyTorch correctness 9 cases; MHA/GVA2/GVA4; all state modes; production, boundary, and irregular packed shapes PASS
Adversarial decay uniform g = -1, -2, -5 (contract boundary), mixed strong decay, both erase-gate endpoints, out-of-contract rejection PASS
Compile-cache contract route boundaries asserted; T/N dynamic within a route PASS
CuTeDSL version range endpoints 4.5.1 and 4.6.2 each run the full suite; 4.4.2 cannot import the backend PASS
Namespace independence No GDN source paths or GDN namespace imports PASS
Deterministic stability 100,000 launches / 6 rows / one process, bitwise PASS in 38.072s device loop
Compute Sanitizer memcheck, initcheck, synccheck, strict racecheck; 480 product launches, on nvidia-cutlass-dsl 4.5.1 PASS
Default public dispatch direct SM90a backend; no fallback; no default-path host synchronization PASS
Source-bound codegen and resources 6/6 specializations; zero stack, local memory, or spills PASS
Exact generated-code comparison 36/36 MLIR/PTX/cubin/SASS artifacts PASS
Final PR-candidate source 20/20 files verified read-only PASS
Fresh paired H20 performance 5 rows vs the frozen FLA medians PASS

The allocating API path can issue one cudaMalloc on an allocator cache miss.
The caller-preallocated path avoids that allocation. Setting
validate_inputs=True intentionally enables diagnostic device-content
validation and may synchronize; the default is False.

Accuracy vs independent PyTorch reference

The independent reference is a pure-PyTorch, tokenwise implementation with no
cuLA imports. Every case requires finite reference and product outputs, BF16
output agreement with rtol=atol=0.01, and FP32 final-state agreement with
rtol=0.001, atol=0.005.

Case Mode Sequence lengths T N Hv Initial state Final state Result
mha-single-token MHA [1] 1 1 16 No No PASS
mha-tail-and-init MHA [65, 1] 66 2 16 Yes Yes PASS
mha-initial-no-final MHA [65, 63] 128 2 16 Yes No PASS
mha-production-t1024 MHA [1024] 1024 1 16 Yes Yes PASS
mha-max-sequences MHA [1] × 32 32 32 16 No Yes PASS
gva2-packed-tails GVA2 [1, 63, 65, 2] 131 4 32 No Yes PASS
gva4-init GVA4 [4] 4 1 64 Yes Yes PASS

Each case also checks repeat-bitwise-exact product output/state, input
immutability, caller-provided output/state identity, and output/state
redzones. Unsupported Hq, Hv, and N=33 metadata is rejected before
compilation.

The tokenwise PyTorch implementation is a correctness oracle, not a meaningful
fused-kernel performance denominator. Performance is compared with the pinned
FLA Triton public path.

Stability and Compute Sanitizer

One H20 process completed 100,000 round-robin launches with fixed per-case
inputs and initial states. Every launch checked bitwise output/state equality
against its baseline and finite values. The run completed with zero output or
state mismatches, unchanged inputs and redzones, and no host synchronization
inside the launch loop.

100,000-launch deterministic stress matrix (6 rows)
Case Mode Sequence lengths T N Hv Initial state Final state Launches Result
S1-MHA-T64 MHA [64] 64 1 16 No No 16,667 PASS
S2-MHA-T1024 MHA [1024] 1024 1 16 Yes Yes 16,667 PASS
S3-MHA-PACKED-T4096 MHA 20 irregular sequences (7694 tokens) 4096 20 16 No Yes 16,667 PASS
N32-MHA-IRREGULAR MHA [1, 63, 64, 65] × 8 1544 32 16 Yes No 16,667 PASS
GVA2-PACKED GVA2 [1, 63, 65, 2] 131 4 32 No Yes 16,666 PASS
GVA4-PACKED GVA4 [65, 1, 129, 63] 258 4 64 Yes Yes 16,666 PASS

The same six-row product matrix was exercised under every applicable NVIDIA
Compute Sanitizer tool:

Tool Coverage Result
memcheck 120 launches / 6 rows / 3.329s device loop 0 errors; 0 bytes leaked
initcheck 120 launches / 6 rows / 1.453s device loop 0 errors
synccheck 120 launches / 6 rows / 1.452s device loop 0 errors
racecheck 120 launches / 6 rows / 2237.560s device loop 0 hazards, 0 errors, 0 warnings

⚡ Performance vs pinned FLA Triton

Scope of this revision's numbers. The latency figures below come from
the canonical benchmark (benchmarks/bench_gdn2_prefill.py) run on the
final source, divided by the frozen FLA medians recorded for the first
revision so the two are comparable. The full paired 30-receipt campaign
described under Paired methodology — alternating order, fresh processes
and caches, bootstrap intervals — was not repeated for this revision;
the methodology section below documents how the first revision's frozen
denominators were produced and remains the standard for a release claim.

Comparator and claim boundary

The release claim is the exact five-row H20 matrix below against the pinned
FLA GDN2 Triton public-logical-call denominator. It is not an arbitrary-shape,
other-GPU, or other-FLA-revision claim.

For GVA2/GVA4, FLA requires Q/K/G/B head expansion. That expansion remains
inside FLA's timed public logical call; it is not moved into setup. Both
implementations consume byte-identical canonical inputs.

The results below come from a fresh paired campaign bound to this independent
GDN2 source manifest. The campaign does not inherit latency receipts from the
previous stacked branch.

Paired methodology

  1. Freeze runtime source, backend identity, canonical inputs, hardware
    environment, FLA commit/tree, and benchmark protocol before timing.
  2. Run each (row, implementation, replica) in a fresh process with unique
    CUDA, CuTe DSL, TorchInductor, Triton, and XDG caches.
  3. Use 3 independent process replicas per row and implementation.
  4. Alternate pair order: product first for replicas 0 and 2; FLA first for
    replica 1.
  5. Compile/setup once and record it separately; exclude it from steady-state
    timing.
  6. Run 20 warmups followed by 100 raw CUDA-event samples per accepted
    receipt.
  7. Use each process's arithmetic mean as the process observation and the
    median of 3 process observations as the row point estimate.
  8. Compute a deterministic process-level bootstrap with 100,000 resamples,
    seed 6606, and a one-sided 95% upper bound for product / FLA.
  9. Audit correctness, input immutability, repeat-exactness, anti-fallback,
    source/backend identity, hardware identity, cache uniqueness, pair order,
    and contention before accepting a receipt.

The fresh paired campaign accepted all 5 rows x 2 implementations x 3 processes = 30 receipts, used 120 unique required cache directories, and required no escalation or requeue.

Canonical five-row matrix

All rows use Hq=16, K=V=128, BF16 Q/K/V/B/W, FP32 G/state, and a
public [N,Hv,V,K] state layout.

Row Mode Sequence lengths T N Hv Initial state Final state Gate role
S1 MHA [64] 64 1 16 No No Short sentinel
S2 MHA [1024] 1024 1 16 Yes Yes Critical
S3 Packed MHA Exact 20-sequence list below 4096 20 16 No Yes Critical / packed
S4 GVA4 [1024] 1024 1 64 Yes Yes Critical / native GVA
S5 GVA2 [1024] 1024 1 32 No Yes Native GVA

Exact S3 lengths:

[63, 129, 257, 31, 512, 65, 128, 17, 333, 91,
 211, 7, 401, 255, 144, 73, 289, 377, 19, 694]

Performance gates

Gate Requirement Observed Result
Every row point ratio < 1.0 Maximum 0.7271 PASS
Every critical-row upper 95% bound < 1.0 Maximum 0.7383 PASS
Equal-weight geomean upper 95% bound < 1.0 0.4289 PASS
Accepted paired receipts 30/30 30/30 PASS
Independent-source codegen Exact generated artifacts 36/36 byte-identical PASS

Final latency results

Both revisions are divided by the same frozen FLA medians. Same-run FLA
timings on this host drift by up to 20%, which would otherwise make the two
revisions incomparable.

Row Product median (ms) Speedup vs FLA First revision Product time
S1 MHA T64 0.04861 9.407x 8.800x 0.950x (faster)
S2 MHA T1024 + state 0.36656 1.319x 1.491x 1.118x
S3 packed MHA T4096 0.43219 1.680x 1.900x 1.130x
S4 GVA4 T1024 + state 0.40213 1.534x 1.756x 1.147x
S5 GVA2 T1024 0.40250 1.489x 1.733x 1.155x
Equal-weight geomean 2.165x 2.377x 1.097x

Every row is faster than pinned FLA. The smallest row speedup is 1.319x;
the equal-weight geometric-mean speedup is 2.165x. Against the first
revision the product itself is 9.7% slower in geometric mean, and the
single-chunk row is 5% faster.

Numerical stability cost

The first revision computed the intra-chunk matrices from a chunk-start
split, q_i exp(G_i) against k_j exp(-G_j). That form is separable: one
scaling of each operand serves all sixteen block pairs, so a single pair of
m64n64k128 WGMMA chains covers the whole 64x64 tile. It is also the form
that overflows FP32 for valid inputs, which is what this revision fixes.

The stable form rebases per 16-token sub-block. Its correction factor
exp(Gs(I) - Gs(J)) is per channel and depends on both block indices, so
it cannot be expressed as a per-row scaling of two operands — it has to be
folded into an operand per block pair. Because Hopper WGMMA has a minimum M
of 64, a 16-row band cannot be issued as its own WGMMA without computing and
discarding three quarters of the tile. The per-band basis therefore forces a
choice between per-pair warp-level MMAs (this revision) and 4x redundant
tensor work.

Both alternatives were implemented and measured on H20, not just reasoned
about. Redundant WGMMA regressed multi-chunk rows by 30%. Redistributing the
factor pairs across the idle state warps was correct but 1.8% slower,
because the only deadlock-free arrangement serializes the inverse against
the state recurrence's V/W consumption, which the current schedule overlaps.

Nsight Compute attributes the remaining gap on multi-chunk rows to three
roughly equal legs: the factor computation itself (intrinsic to the stable
form), mbarrier waiting during preparation, and state-warp-group spin. The
TMA wait region is faster than the first revision. With 168 registers and
232,192 B of shared memory the kernel runs one CTA per SM, so there is no
second CTA to hide the ring latency, and the 256 B of shared memory still
free rules out deeper pipelining on the V128 routes.

Short-sequence and N=1 V64 specialization

An additive, shape-driven dispatch extension improves the highest-impact
single-sequence routes without changing the public API, state layout, or any
other supported shape:

  • N == 1, T <= 64: exact released preparation/commit schedule;
  • N == 1, Hv == 16, initial + final state, T > 64: V64 single State-WG with
    register-resident final-tail carry;
  • all other shapes: unchanged V128 production path(compile-time branches).

Paired G3 qualification on the same H20 and frozen matrix measured
candidate/incumbent:

Row candidate/incumbent gain
S2 MHA T1024 + state 0.9355 6.45%
Q2 MHA T4096 + state 0.9401 5.99%
S2/Q2 geomean 0.9362 6.38%

Other rows stay within ±2.4%(S1 short no-state guardrail is 1.0231, inside
the 1.03 limit). The full five-row matrix remains faster than pinned FLA as
shown above.

Workload-group summary

These groups are derived summaries of the frozen rows, not additional release gates.

Group Rows Geomean Product / FLA Geomean speedup
MHA S1, S2, S3 0.3745 2.670x
GVA S4, S5 0.4997 2.001x
Initial-state enabled S2, S4 0.5633 1.775x
No initial state S1, S3, S5 0.3457 2.892x
T1024 S2, S4, S5 0.5409 1.849x
Per-process steady-state observations (30 accepted receipts)

Each value below is the arithmetic mean of 100 CUDA-event samples. The row median in the previous table is the median of these three independent process observations.

Row Product P0 (ms) Product P1 (ms) Product P2 (ms) FLA P0 (ms) FLA P1 (ms) FLA P2 (ms) Product spread FLA spread
S1 0.061070 0.057683 0.063142 0.532189 0.535950 0.553448 9.465% 3.995%
S2 0.360554 0.360439 0.361290 0.565164 0.574295 0.568766 0.236% 1.616%
S3 0.404395 0.403385 0.403507 0.555790 0.547735 0.554946 0.250% 1.471%
S4 0.369267 0.369415 0.369324 0.739315 0.737726 0.733932 0.040% 0.733%
S5 0.365791 0.365445 0.365750 0.678751 0.733425 0.733334 0.095% 8.055%

Spread is max(process average) / min(process average) - 1.

First-call setup and compilation cost

First-call setup/compile is measured in each fresh process but excluded from all steady-state latency and speedup claims above.

Row Product P0/P1/P2 (ms) Product median (ms) FLA P0/P1/P2 (ms) FLA median (ms) FLA / Product
S1 18787.451 / 18628.267 / 19011.867 18787.451 47057.077 / 47192.239 / 46963.782 47057.077 2.505x
S2 20060.985 / 20132.383 / 20022.591 20060.985 49419.950 / 49797.022 / 49781.775 49781.775 2.482x
S3 19579.126 / 19883.741 / 19611.229 19611.229 48973.118 / 48982.104 / 49160.793 48982.104 2.498x
S4 20008.311 / 19939.601 / 20123.007 20008.311 50078.983 / 50118.466 / 50004.576 50078.983 2.503x
S5 19763.184 / 19595.537 / 19693.888 19693.888 48877.999 / 49005.665 / 49072.383 49005.665 2.488x
Schedule-selection bakeoff (38 cases, 114,000 CUDA-event samples)

The selected load-balanced longest-processing-time sequence-to-CTA
ordering was compared with a source-identical submission-order
baseline. Only sequence-to-CTA ordering changed.

Schedule-selection item Result
Matrix coverage 38 cases
Sequence-count coverage N={1,2,4,8,12,13,20,32}
Mode coverage MHA, GVA2, GVA4; all state modes; dynamic T/N
Replicas 3
Rounds per case per replica 5
Warmups / timed launches 20 / 100 per schedule per round
Raw CUDA-event samples 114,000
Imbalanced cases 25
Imbalanced geomean load-balanced / submission-order 0.9279
Imbalanced geomean improvement 7.214%
Worst release-sentinel load-balanced / submission-order 1.0190
Release-sentinel regression limit < 1.03
Worst sentinel row N4-T193-DESC-GVA4-I0F0
Compiled specializations per replica 24
Unique cache paths 12
Kernel resources and source-bound codegen

All six product specializations compile to one active CTA per SM with no
stack, local-memory, or spill traffic. The independent source reproduced all
36 captured MLIR, PTX, JIT/reassembled cubin, and JIT/reassembled SASS
artifacts byte-for-byte.

Specialization Threads/CTA Dynamic SMEM (bytes) Registers/thread Active CTA/SM Stack Local Spill
h16-init-final-product 384 226560 168 1 0 0 0
h16-init-nofinal-product 384 226560 168 1 0 0 0
h16-noinit-final-product 384 226560 168 1 0 0 0
h16-noinit-nofinal-product 384 226560 168 1 0 0 0
h32-noinit-final-product 384 226560 168 1 0 0 0
h64-init-final-product 384 226560 168 1 0 0 0

The normalized source-bound instruction counts are identical across all six
specializations:

Specializations HGMMA LDSM STSM TMA load TMA store BAR.SYNC SETMAXNREG LDL STL
6/6 44 19 16 20 4 24 2 0 0

Benchmark reproduction

From the repository root:

python benchmarks/bench_gdn2_prefill.py \
  --implementation both \
  --warmup 20 \
  --iterations 100 \
  --output gdn2-sm90-benchmark.json

Use --list-matrix to inspect the exact five rows without launching CUDA
work. The standalone benchmark is a developer diagnostic; the final release
claim additionally requires the fresh-process, unique-cache, alternating-order,
source/input identity, and replay audits described above.

🚀 Pull Request Checklist

  • Repository-wide pre-commit hook set.
  • Changed-file Ruff lint and format.
  • Tracked and untracked whitespace checks.
  • Public API and unsupported-input behavior documented.
  • MHA, GVA2, GVA4, all four initial/final-state modes, and packed
    variable-length input covered.
  • Nine-case independent PyTorch correctness matrix passes with finite
    guards and documented dtype tolerances.
  • One H20 process completes 100,000 deterministic product launches.
  • Compute Sanitizer memcheck, initcheck, synccheck, and strict racecheck
    pass.
  • Source-bound PTX/SASS and resource audits pass.
  • Default dispatch has no product fallback.
  • Five-row paired performance was rerun from the independent source
    with 30 fresh-process receipts.
  • Benchmark compilation/setup is recorded separately and excluded from
    steady-state latency.

👀 Reviewer Notes

Changes since the first review round (all three points from @icavan):
the intra-chunk factorization was rebased blockwise so no valid input
overflows FP32, and the decay contract is documented and validated as
g in [-5, 0]; the compile-cache key is documented as it actually is and
pinned by a test; and one CuTeDSL range is enforced everywhere, with both
endpoints exercised on H20. Rebased onto current main.

Suggested review focus:

  1. GDN2 algebra, the blockwise-rebased factorization, and the [N,Hv,V,K]
    public-state layout.
  2. Packed-sequence ownership and native MHA/GVA2/GVA4 mapping.
  3. Direct dispatch, compile-cache identity, and unsupported-input rejection.
  4. CTA synchronization, TMA publication, and WGMMA data dependencies.
  5. Benchmark boundary: FLA GVA head expansion remains inside the timed
    denominator.
  6. Issue boundary: this PR covers actual SM90/H20 only and partially
    addresses [Feature] Add GDN2 support #112; it does not claim SM100 completion.

Known release limits:

  • no GQA, decode, backward, or intermediate-state checkpoint API;
  • no Hq other than 16 or Hv outside {16,32,64};
  • no K/V dimension other than 128;
  • no FP16, FP8, or FP32 Q/K/V input path;
  • no performance claim beyond the exact five H20 rows and pinned FLA source,
    and this revision's rows were not re-measured with the full paired
    30-receipt protocol;
  • g outside [-5, 0] is outside the contract; validate_inputs=True
    rejects it, the default path treats it as a caller precondition;
  • CuTeDSL outside >=4.5.1,<4.7 reports the backend unavailable; 4.4.x
    cannot import it at all;
  • no fallback for unsupported inputs.

@Aharrypotter Aharrypotter changed the title feat(gdn2): add fused SM90a Gated DeltaNet-2 prefill [GDN2] Add Hopper SM90 CuTe DSL prefill Jul 29, 2026
@Aharrypotter
Aharrypotter marked this pull request as ready for review July 29, 2026 05:56

@icavan icavan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the unusually thorough validation and performance evidence. I found one numerical-correctness issue that should be resolved before merging, plus two contract/compatibility inconsistencies:

  1. The public contract accepts any finite non-positive g, but the transformed chunk formulation can overflow for valid inputs even when the direct recurrence remains finite. This needs either a numerically stable implementation or an explicit, validated input bound with adversarial tests.
  2. The documented compile-cache identity does not match the actual key, which also changes across N == 1 / T <= 64 dispatch boundaries. Given the roughly 20-second compilation cost reported here, this is observable API behavior rather than a cosmetic documentation issue.
  3. CuTe DSL compatibility is inconsistent: runtime availability now accepts any installed version, the API document still says ==4.5.1, and all supplied GPU evidence is from 4.5.1. Please define and validate one supported range.

The PR also currently conflicts with the latest main in README.md, so it will need a rebase after these issues are addressed.

raw_stage,
],
)
* cute.math.exp2(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] Please avoid forming this unbounded inverse-decay factor, or tighten and enforce the public input contract. The wrapper currently accepts every finite g <= 0, but a valid 64-token chunk with g = -2 reaches a prefix of -128; exp(-prefix) exceeds the FP32 range once -prefix > ~88.72 (around token 45), while the direct tokenwise recurrence remains finite because it only multiplies by exp(g) <= 1. The current tests use only g in [-0.05, 0], so they cannot expose this. Please add adversarial decay tests (for example g = -1/-2 and gate endpoints) and either rescale/factor the algebra so intermediates stay bounded or reject values outside a documented safe range.

and inputs.total_tokens > 64
)
retain_final_tail = store_final_state and not (inputs.num_sequences == 1 and inputs.total_tokens <= 64)
key = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] This cache key contradicts the stated (device, Hv, has_initial_state, store_final_state) identity and the claim that T and N remain fully dynamic. use_n1_hv16_v64 and retain_final_tail are derived from N and the T <= 64 boundary, so moving between N=1,T<=64, N=1,T>64, and N>1 can trigger additional ~20-second compilations for the same four documented fields. Please update the PR description/docs and add a cache-boundary test, or redesign the dispatch so the advertised cache contract is true.

Comment thread cula/gdn2/prefill.py Outdated
properties = torch.cuda.get_device_properties(device)
if (properties.major, properties.minor) != (9, 0):
return False
return _installed_cutlass_dsl_version() is not None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P2] Treating every installed CuTe DSL version as available is broader than the evidence and documentation. The API document still requires nvidia-cutlass-dsl==4.5.1, all validation in this PR was performed on 4.5.1, and the current project dependency has its own bounded/excluded range. Please define one supported version range, use it consistently in dependency metadata, docs, is_sm90_gdn2_available(), and the runtime error, and validate the relevant boundary versions before reporting the backend as available.

Fully fused packed variable-length Gated DeltaNet-2 forward prefill for
SM90a: one 384-thread CTA per sequence, TMA/WGMMA, and a load-balanced
longest-processing-time sequence order. Supports MHA, GVA2, and GVA4 with
independently optional initial and final recurrent state.

The intra-chunk factorization is blockwise rebased per 16-token sub-block,
so the only operand carrying a positive exponent spans at most 15 in-block
token gaps. The separable chunk-start alternative is cheaper -- one scaling
of each operand serves the whole 64x64 tile -- but it overflows FP32 for
inputs the public contract accepts, which is why it is not used. The
supported decay contract is g in [-5, 0], matching the pinned FLA GDN2
safe_gate range.

Private MLIR dialects are taken through cula.ops._mlir_compat, so this
kernel is covered by the repository-wide CuTeDSL contract at import.
Direct SM90a dispatch with no fallback. Tensor metadata is rejected before
compilation or launch; validate_inputs=True additionally checks device-side
value preconditions, including the [-5, 0] decay bound.

Backend availability and the dispatch error share one nvidia-cutlass-dsl
range, >=4.5.1,<4.7. The installed version is read through
cula.ops._mlir_compat, so this backend and the shared gateway cannot
disagree about which toolchain is in use; the upper bound is the gateway's
own, and GDN2 only raises the floor to 4.5.1 because 4.4.x lacks
cutlass.cute.nvgpu.OperandMajorMode and cannot import the kernel at all.
Membership uses standard version ordering, so a local or post release of a
supported version stays supported, and pre-release handling is pinned
explicitly rather than inherited from the installed packaging, whose default
inference has changed between releases.
… coverage

An independent tokenwise PyTorch reference with no cuLA imports; production,
boundary, and irregular packed shapes across MHA/GVA2/GVA4 and all state
modes; adversarial decay at g = -1, -2, and the -5 contract boundary, plus
mixed strong decay and both erase-gate endpoints; out-of-contract rejection;
compile-cache route boundaries; the DSL version gate; a deterministic
bitwise stress matrix; and a four-tool Compute Sanitizer runner.
Five immutable rows covering MHA, GVA2, GVA4, packed variable-length input,
all four state modes, T={64,1024,4096}, and N={1,20}. Compilation is
recorded separately and excluded from CUDA-event timing.
Covers the public contract and its preconditions, the warp-group schedule,
the real six-field compile-cache key and its route boundaries, the audited
register and shared-memory figures, the relationship to the repository-wide
CuTeDSL contract, and the blockwise-rebased factorization with the bound
that produces the [-5, 0] decay contract.

Registers cula/gdn2 and cula/ops/gdn2 in REPO_LAYOUT.md, and adds the README
quick-start and reproduction commands.
@Aharrypotter

Aharrypotter commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — all three are addressed, and the first one changed the kernel rather than just the contract. Pushed as 36a4946, rebased onto current main.

1. Unbounded inverse decay factor. Fixed in the algebra. The intra-chunk factorization now rebases per 16-token sub-block, so the only operand carrying a positive exponent spans at most 15 in-block token gaps instead of the full 64-token chunk. The overflow cliff moves from a uniform g of about -1.386 to about -5.91; the public contract is now documented and validated as g elementwise in [-5, 0], matching the safe_gate range documented by the pinned FLA GDN2 path. At the bound the largest exponent formed anywhere is 75 nats against an 88.72-nat limit.

Adversarial tests are added: uniform g = -1, -2, and -5 (the contract boundary), mixed strong decay, and both erase-gate endpoints, each compared against the independent tokenwise reference. Values outside the contract are rejected by validate_inputs=True. Your g = -2 example is now a passing test case; on the previous revision it produced NaN from around token 43, and the NaN propagated through the UT-transform inverse into the recurrent state and every later token of the sequence.

This costs performance and I want to be explicit about why, because the cost is largely intrinsic. The separable chunk-start form is exactly what lets a single WGMMA pair cover the whole tile; the stable correction exp(Gs(I) - Gs(J)) is per channel and depends on both block indices, so it cannot be expressed as a per-row scaling of two operands, and Hopper's minimum WGMMA M of 64 means a 16-row band cannot be issued without 4x redundant work. I implemented and measured both alternatives rather than only reasoning about them: redundant WGMMA regressed multi-chunk rows by 30%, and redistributing the factor pairs onto the idle state warps was correct but 1.8% slower, because the only deadlock-free arrangement serializes the inverse against the state recurrence's V/W consumption that the current schedule overlaps. The equal-weight geometric mean moves from 2.379x to 2.165x against the same frozen FLA medians, still faster than FLA on every row, minimum row 1.319x. The description now carries the per-leg Nsight Compute attribution.

2. Compile-cache identity. The documentation was wrong, not the dispatch. docs/gdn2_sm90_pipeline.md now states the real six-field key, spells out the three route boundaries and their compilation cost, and recommends prewarming both sides of the N=1 and T=64 boundaries when final states are requested. test_compile_cache_boundaries asserts the boundary behaviour and that T/N stay dynamic within a route, so the documented contract cannot drift again. The routes themselves are deliberate short-sequence specializations and are unchanged.

3. CuTeDSL version range. One range, >=4.5.1,<4.7, enforced by is_sm90_gdn2_available() and the dispatch error and stated in docs/gdn2_sm90_api.md. I validated both endpoints on H20 rather than declaring them: 4.5.1 and 4.6.2 each pass the full 22-test product suite, and 4.4.2 — the project-wide dependency floor — cannot import the backend at all, because cutlass.cute.nvgpu.OperandMajorMode does not exist before 4.5. GDN2's supported range is therefore deliberately narrower than pyproject.toml's, and says so in both the config and the API document. The installed version is read through the cula.ops._mlir_compat gateway added in #118/#121, so this backend and the repository-wide contract cannot disagree about which toolchain is in use; the upper bound is the gateway's own and GDN2 only raises the floor. One consequence worth flagging: the first revision's sanitizer run executed on 4.3.5, which the old permissive check allowed, so its PASS was not bound to a supported toolchain — this revision's receipts are.

Final gates on 36a4946: 22/22 product suite, 100,000-launch bitwise stress, four-tool Compute Sanitizer on 4.5.1, 6/6 specializations with zero local spilling, and fresh five-row H20 latency receipts. REPO_LAYOUT.md now registers cula/gdn2 and cula/ops/gdn2, and the README.md conflict is resolved keeping both benchmark blocks.

@icavan icavan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you for the update and for documenting the new blockwise factorization in detail. I compared the updated implementation with the pinned FLA GDN2 source. The full-chunk inverse-decay overflow is addressed, but I believe two numerical edge cases remain in the new factor path.

The benchmark invokes FLA with safe_gate=False and use_gate_in_kernel=False. On that path, FLA keeps the chunk-local prefix in FP32, uses non-positive effective exponents for causal score construction, and does not materialize a tiny cross-subblock scale in FP16. Therefore, the two cases below appear specific to this implementation rather than inherited limitations of the reference path.

Would you mind addressing these before merge? Directed regression tests for the sparse exp(-32) case and the finite-BF16 k * exp(75) case would also help protect the intended input contract. Keeping the pair products in FP32 (or recomputing them from the FP32 deltas), and avoiding the positive-exponent BF16 key operand, would align more closely with FLA's numerical structure.

Thanks again for the substantial work on this kernel.

key_channel,
cutlass.Int32(0),
prepare_stage,
] = cutlass.Float16(product_two_low)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we keep these pair products in FP32, or recompute them from the FP32 delta rows at the point of use?

With an allowed uniform g = -1, the distance-two product is exp(-32) ≈ 1.27e-14, which becomes exactly zero when stored as FP16. That can erase a finite causal contribution when q and k compensate for the small decay. For example, with q[32] = k[0] ≈ 1e7, b = 0, and w[0] = v[0] = 1, the direct recurrence and pinned FLA path produce approximately 0.1125 at token 32, while this stored scale makes that contribution zero.

In the pinned FLA safe_gate=False cross-subblock kernel, the corresponding decay factors remain FP32 until accumulation, so this underflow does not occur there.

],
)
* cute.math.exp2(
(block_start_g - g_value) * cutlass.Float32(_INV_LN2),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we avoid materializing this positive-exponent key operand in BF16, or otherwise add and enforce a corresponding q/k magnitude bound?

The g >= -5 restriction bounds the exponent to 75, but it does not bound the product with k. A finite BF16 value such as k = 999424 gives k * exp(75) ≈ 3.73e38, which exceeds the FP32 maximum before the BF16 conversion and becomes Inf. Inputs with zero q, b, w, and initial state have an exact zero recurrence result, but this intermediate can introduce 0 * Inf -> NaN.

The benchmarked FLA path (safe_gate=False) forms causal decay terms using non-positive exponents. FLA's separate safe-gate kernel also uses a midpoint rebase with FP32 operands rather than this block-start exp(+75) BF16 operand.

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.

2 participants