Skip to content

[GDN] Add Hopper SM90 CuTe DSL prefill#108

Open
Aharrypotter wants to merge 3 commits into
inclusionAI:mainfrom
Aharrypotter:issue76-sm90-cutedsl-p8-pr
Open

[GDN] Add Hopper SM90 CuTe DSL prefill#108
Aharrypotter wants to merge 3 commits into
inclusionAI:mainfrom
Aharrypotter:issue76-sm90-cutedsl-p8-pr

Conversation

@Aharrypotter

@Aharrypotter Aharrypotter commented Jul 20, 2026

Copy link
Copy Markdown

📌 Description

This PR adds a Hopper SM90 Gated DeltaNet prefill path implemented entirely in
CuTe DSL.

  • ports the upstream 512-thread TMA/WGMMA warp-specialized algorithm into
    cula.ops.gdn.sm90 while preserving upstream authorship and provenance;
  • exposes packed variable-length BF16 prefill through
    cula.gdn.chunk_gated_delta_rule, including optional gates, initial/final
    state, MHA, GQA, and GVA head mappings;
  • keeps the product path independent of the cuLA C++ extension and FlashInfer
    headers/submodules at runtime;
  • adds the exact 28-row Issue [Feature] Integrate Lightning SM90 Implementation via CuTe DSL #76 benchmark, independent PyTorch reference
    tests, safety coverage, and Hopper user documentation.

🔍 Related Issues

Closes #76.

Issue checklist

Issue requirement Delivery
Port Hopper kernels through CuTe DSL cula/ops/gdn/sm90/ implements the SM90a 512-thread TMA/WGMMA kernel and direct TVM-FFI launch path.
Adapt the Python interface cula.gdn.chunk_gated_delta_rule validates packed inputs and exposes optional gates and recurrent state.
Add benchmarks benchmarks/bench_gdn_prefill.py defaults to all 10 fixed and 18 packed-varlen rows from the issue.
Validate performance H20 three-backend, three-process comparison: 0.993329822x geometric mean and 1.147581166x worst row versus frozen upstream C++; all 28 rows pass the 1.10x / 1.25x gates.
Add unit tests Independent blockwise reference plus C01–C14, head mappings, tails, redzones, repeat, continuation, and stream behavior.
Update documentation README quick start, repository layout, API/requirements guide, and pipeline guide.

🚀 Pull Request Checklist

✅ Pre-commit Checks

  • I have run the repository-wide pre-commit hook set.
  • Ruff lint and format checks pass for every changed Python file.
  • git diff --check passes.

🧪 Tests

  • H20/SM90 product suite: 22 passed.
  • C01–C14 correctness, no-C++ dispatch, non-default stream,
    repeat/continuation, and output/state redzones pass.
  • Compute Sanitizer C04 ragged-tail gate: memcheck and synccheck report
    zero errors; racecheck reports zero hazards.
  • Source manifest matches before and after validation (182/182 files).
  • Fresh SM90a codegen: 512 threads, 100 WGMMA/HGMMA, 22 TMA,
    128 registers, zero stack/spill/local memory, and one active CTA/SM.

⚡ Performance

Measured on one idle NVIDIA H20-3e with BF16 Hq=Hk=Hv=64, D=128, 20
warmups and 100 CUDA-event timed iterations per row. Each backend ran in three
independent processes; each table entry is the median of the three process
averages. Compilation and first-call setup are excluded. A ratio below 1.0x
means the CuTe DSL kernel is faster.

Check Limit Result
DSL / frozen FlashInfer geometric mean <= 1.10x 0.993329822x
Worst row <= 1.25x 1.147581166x (fixed-b1-t512)
Coverage 28 rows/backend DSL, migrated CUTLASS, and frozen FlashInfer each 28/28
Workload group Rows DSL / FlashInfer geomean DSL / migrated CUTLASS geomean Max DSL / FlashInfer
Fixed length 10 1.003484x 0.972185x 1.147581x
Packed variable length 18 0.987733x 0.976909x 0.999400x
Overall 28 0.993330x 0.975219x 1.147581x
Full 28-row median latency table
Row CuTe DSL ms Migrated CUTLASS ms Frozen FlashInfer ms DSL / FlashInfer
fixed-b1-t512 0.058701 0.056546 0.051152 1.147581x
fixed-b1-t1024 0.097943 0.106133 0.099455 0.984794x
fixed-b1-t4096 0.373638 0.383458 0.377599 0.989509x
fixed-b1-t8192 0.741560 0.754011 0.748508 0.990718x
fixed-b1-t16384 1.477579 1.499711 1.494760 0.988506x
fixed-b2-t512 0.100763 0.110224 0.102313 0.984847x
fixed-b2-t1024 0.192742 0.202722 0.195238 0.987216x
fixed-b2-t4096 0.743809 0.757794 0.751203 0.990157x
fixed-b2-t8192 1.480442 1.497819 1.492454 0.991951x
fixed-b2-t16384 2.951255 2.984504 2.980956 0.990037x
varlen-n10-t4096-uniform 0.385270 0.400514 0.392410 0.981806x
varlen-n10-t4096-random 0.354295 0.365720 0.358675 0.987790x
varlen-n10-t4096-skewed 0.351345 0.361929 0.355406 0.988573x
varlen-n10-t8192-uniform 0.696737 0.713239 0.705871 0.987060x
varlen-n10-t8192-random 0.668328 0.683563 0.675664 0.989143x
varlen-n10-t8192-skewed 0.653087 0.665490 0.659418 0.990399x
varlen-n10-t16384-uniform 1.366857 1.383636 1.378220 0.991755x
varlen-n10-t16384-random 1.273788 1.291183 1.285643 0.990779x
varlen-n10-t16384-skewed 1.247147 1.264023 1.258424 0.991039x
varlen-n20-t4096-uniform 0.430677 0.447858 0.439974 0.978869x
varlen-n20-t4096-random 0.408003 0.420942 0.413756 0.986093x
varlen-n20-t4096-skewed 0.401375 0.413276 0.406397 0.987642x
varlen-n20-t8192-uniform 0.725918 0.747053 0.739044 0.982240x
varlen-n20-t8192-random 0.701505 0.721255 0.713559 0.983107x
varlen-n20-t8192-skewed 0.707668 0.714869 0.708092 0.999400x
varlen-n20-t16384-uniform 1.315303 1.338503 1.331811 0.987604x
varlen-n20-t16384-random 1.345815 1.369917 1.363760 0.986842x
varlen-n20-t16384-skewed 1.298094 1.317645 1.312225 0.989232x

All backend/process inputs are byte-identical. The canonical benchmark can be
reproduced with:

python benchmarks/bench_gdn_prefill.py --output gdn-sm90-issue76.json

Reviewer Notes

  • The kernel is adapted from FlashInfer's Apache-2.0 SM90 CuTe DSL rewrite:
    original rewrite eb31811856e07f3c5fc94a7aaafeaeaabd954f4f, imported source
    97996a4123924140d28a3079292535aa534eba92, and frozen C++ oracle
    41e5aa29a0e218bde2e2045316ed88e3f4dc8ca2.
  • The imported revision includes the corrected final-tail s < B and t < B
    predicate and explicit compile-cache key; cuLA supplies the public API,
    packaging, tests, benchmark, and documentation around the upstream kernel.
  • The SM90 path requires nvidia-cutlass-dsl==4.5.1 and fails explicitly on an
    unsupported GPU or DSL version; it never silently falls back to C++.
  • Intermediate state checkpoints, zero-length packed sequences, non-BF16
    Q/K/V, head sizes other than 128, decode, and backward are intentionally out
    of scope and documented as unsupported.
  • The changes are split by product responsibility: implementation,
    correctness/benchmark coverage, and documentation.

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

Code Review

This pull request introduces a packed variable-length Gated DeltaNet (GDN) prefill operator for NVIDIA Hopper (SM90) GPUs, implemented in CuTe DSL. The feedback highlights a critical concurrency hazard in the workspace allocation for tensormaps_t that could lead to data corruption, as well as high-severity performance bottlenecks caused by synchronous host-device transfers during tensor validation. Additionally, a defensive check is recommended to prevent crashes when checking prefill availability on non-CUDA devices.

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 +2364 to +2365
workspace_size = get_device_sm_count(q.device) * 128
tensormaps_t = _get_cache_buf("gdn_prefill_tensormaps", workspace_size, q.device)

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.

critical

Using a shared global/cached buffer _get_cache_buf for tensormaps_t introduces a critical concurrency hazard. If multiple streams or concurrent threads launch the GDN kernel on the same device, they will share the same tensormaps_t buffer. Since the kernel indexes into this buffer using smid(), concurrent execution on the same Streaming Multiprocessors (SMs) will cause silent data corruption or illegal memory access crashes. Replacing this with a direct allocation via PyTorch's caching allocator is thread-safe, stream-safe, and has virtually zero overhead for such a small buffer (~16 KB).

Suggested change
workspace_size = get_device_sm_count(q.device) * 128
tensormaps_t = _get_cache_buf("gdn_prefill_tensormaps", workspace_size, q.device)
workspace_size = get_device_sm_count(q.device) * 128
tensormaps_t = torch.empty(workspace_size, dtype=torch.uint8, device=q.device)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for flagging this. I audited the exact specialization used by this PR and do not believe a per-launch workspace is required here.

The TensorMap workspace is indexed by the physical smid() with one 128-byte slot per SM. The source-bound H20 resource and occupancy capture for this 512-thread kernel reports 185,728 bytes of dynamic shared memory and exactly one active CTA per SM. In addition, the output S2G path executes cp_async_bulk_wait_group(0) before the CTA can exit.

Consequently, CTAs from different streams cannot be resident concurrently on the same SM and write the same slot. The next CTA can reuse that SM's slot only after the previous CTA's bulk operation has retired and the CTA has exited.

I added a comment in 6bc5c4d to make this specialization-bound invariant explicit. If a future resource layout permits more than one resident CTA per SM, the cache design will need to be revisited.

Comment thread cula/gdn/prefill.py Outdated
Comment on lines +176 to +177
def validate() -> tuple[int, ...]:
offsets = tuple(int(value) for value in cu_seqlens.detach().cpu().tolist())

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.

high

Calling cu_seqlens.detach().cpu().tolist() forces a synchronous host-device transfer and CPU-GPU synchronization. In variable-length workloads where sequence lengths change dynamically, cu_seqlens is recreated at each step, causing the cache to miss and introducing a major performance bottleneck. Consider refactoring _GDNPrefillInputs to store num_seqs: int (which is simply cu_seqlens.numel() - 1 and can be retrieved without synchronization) instead of seq_lens: tuple[int, ...], and bypassing or making these synchronous validation checks optional in production/steady-state execution.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 6bc5c4d.

The default path no longer calls cu_seqlens.detach().cpu().tolist(). It derives num_seqs from tensor metadata (numel() - 1) and retains the existing rank, dtype, device, contiguity, and alignment checks.

CUDA-resident value checks are now an explicit validate_inputs=True diagnostic path, so production calls do not force a device-to-host synchronization.

An H20 CPU-profiler trace covered five calls with a new set of Q/K/V/g/beta/cu_seqlens/output tensors on every call and observed none of the following operations:

  • aten::_local_scalar_dense
  • aten::_to_copy
  • aten::all
  • aten::gt
  • aten::isfinite
  • aten::item

The complete SM90 test file also passes: 25 tests on H20 with CuTe DSL 4.5.1.

Comment thread cula/gdn/prefill.py Outdated
Comment on lines +251 to +255
def validate_alpha() -> None:
if not bool(torch.isfinite(alpha).all()) or not bool((alpha > 0).all()):
raise ValueError("g must contain finite, strictly positive forget factors")

_cached_content_validation("alpha", alpha, validate_alpha)

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.

high

Calling bool(tensor.all()) on CUDA tensors forces a synchronous device-to-host transfer to retrieve the boolean value, blocking the CPU. When gate factors (g and beta) are dynamically computed by the network at each step, they are new tensors, causing cache misses and introducing constant pipeline stalls. Bypassing or making these validation checks optional in production/steady-state execution will significantly improve throughput.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I removed the default bool(torch.isfinite(...).all()) and bool((g > 0).all()) reductions, along with the object-identity content cache.

The default fast path now treats finite and positive CUDA values as caller preconditions, while validate_inputs=True retains the synchronous checks for debugging and input-pipeline validation. The API documentation and regression tests describe this contract.

The fresh-object Hopper profiler gate observed no scalar extraction or validation-reduction operations across five calls, and the complete SM90 test file passes 25/25.

Comment thread cula/gdn/prefill.py
@Aharrypotter
Aharrypotter marked this pull request as ready for review July 20, 2026 07:48
@Aharrypotter

Copy link
Copy Markdown
Author

@icavan @zheyang0825 cc

@Aharrypotter
Aharrypotter force-pushed the issue76-sm90-cutedsl-p8-pr branch from 6bc5c4d to 7f7619d Compare July 22, 2026 17:21
@Aharrypotter
Aharrypotter force-pushed the issue76-sm90-cutedsl-p8-pr branch from 7f7619d to 88737e9 Compare July 22, 2026 17:27
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.

[Feature] Integrate Lightning SM90 Implementation via CuTe DSL

1 participant