Add FROST engines - #476
Conversation
Port the FROST engine work from the internal cudnn_frontend frost_devel branch (GitLab MR !2310) onto feat/frost_develop. Includes the FROST/cuTile engine implementations and routing, the GDN cuTile path, GEMM and linear-attention benchmarks, and the accompanying Python tests. The internal ci/ directory is intentionally excluded: it has no counterpart in this repository. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (212)
📝 WalkthroughWalkthroughThis PR adds three new opt-in OSS execution engines under a unified cuDNN Python router: FROST GEMM (CuTe-DSL kernels for matmul, block-scale, MoE, SwiGLU), FROST/cuTile linear attention (GDN, KDA, GDN-2), and FROST SDPA-forward (SM100/SM120). It adds a segmented engine-ID namespace, manifest-based lazy engine discovery, and a unified ChangesEngine Routing Infrastructure
FROST Tile-DSL Core Infrastructure
FROST GEMM Engine
FROST/cuTile Linear Attention Engines
FROST SDPA-Forward Engine
FFT Causal Conv1d Removal
Test Infrastructure and Docs
Estimated code review effort: 5 (Critical) | ~240 minutes Sequence Diagram(s)sequenceDiagram
participant User as Caller
participant Graph as pygraph
participant Router
participant Manifest
participant FrostEngine as FrostGemmEngine
participant Backend as cuDNN Backend
User->>Graph: build_plans() / execute()
Graph->>Manifest: engines_for(graph, sm)
Manifest-->>Graph: candidate engines
Graph->>Router: plan(graph, engines)
Router->>FrostEngine: check_support(graph)
FrostEngine-->>Router: PlanConfig or decline
Router->>Backend: backend_plan_entries(graph)
Backend-->>Router: ranked backend plans
Router-->>Graph: unified graph.plans list
User->>Graph: execute(variant_pack)
Graph->>FrostEngine: build_plan / __call__(uid_to_data, workspace, stream)
FrostEngine->>Backend: (if declined) fall through
FrostEngine-->>User: output tensors
Estimated code review effort: 5 (Critical) | ~240 minutes Possibly related PRs
Suggested labels: Suggested reviewers: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
… overlapping strides - _thd_view validates the runtime buffer against its declaration before reinterpreting storage: dtype/device must match and the base address must be 16-byte aligned (TMA global-address rule / assumed_align=16); as_strided already rejects views past the underlying allocation. - _thd_check_strides_native additionally requires covering (non-overlapping) strides — head >= d, token >= heads*head — matching the SM120 kernel's is_layout_supported, so sub-dense declarations are declined at check_support instead of failing at the per-execute compile (or racing on O writes on SM100). - Kernel _fake_bshd guards: the head dim must be innermost-contiguous; d256/d512 validate the O stride at BPE_O (the O storage dtype byte size). - Clearer SM120 layout-rejection message (the entry validator accepts padded storage now; the text still demanded compact). The THD host-prep stream binding flagged in the same review round is a pre-existing issue (NVIDIA#476) and is split into a separate PR. Addresses CodeRabbit review feedback on NVIDIA#526. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…6 fwd kernels (#526) * frost(sdpa): native THD stride support in the SM100/SM120 f16 fwd kernels; decline what TMA cannot express A THD tensor may declare a wider token stride than the packed h*d — e.g. a K/V view of a kv-interleaved [T, 2, H, D] buffer (token stride 2*h*d), the layout torch.nn.attention.varlen users produce by slicing a fused KV projection. The THD lowerings rebuilt packed (1, T, H, D) views with hardcoded strides, so such graphs were claimed and silently mis-addressed (100% of O wrong on both sdpa_fwd_prefill_sm120 and the sm100 flavors; caught by PR #516's fuzz coverage and PyTorch's own varlen suite). Native support, no fallback (AGENTS Hard Rule 2): - compile() on all five f16 kernels (sm120, sm100 d128/d192_d128/d256/ d512) takes optional caller-declared (batch, seq, head, elem) strides per tensor (lru cache-key); None keeps the compact specialization bit-for-bit. Strided fakes via make_fake_tensor, validated against the TMA 16-byte global-stride rule. - SM120: kv_tma_desc reads the tensor's strides instead of recomputing packed ones (Q/O offset math was already layout-driven); the entry validator accepts padded 16-byte-granular BSHD storage (compact = the equality special case). - SM100: the Q/K/V/O TMA descriptors are built from the tensor views, so declared strides flow in unchanged; the THD O-descriptor builder steps per-batch bases by O's declared seq-axis stride (o_tensor.stride[1]). - Adapters bind declared-stride (1, T, H, D) views directly. What TMA cannot express is REJECTED in check_support (NotImplementedError naming the offending strides), so the Router falls back to an engine that honors the declaration: non-innermost-contiguous head dim, or token/head strides that are not multiples of 8 elements (sub- granularity strides also violate the graph API's pointer-alignment contract for the backend, so declining is correct, not conservative). - The SM120 FP8 THD path (#509) keeps the packed contract for now: non-packed declarations are declined (_thd_check_strides_packed); extending native strides there is tracked as a follow-up. Verified (torch nightly cu132, ToT develop + PR #516's fuzz tests): gapped seeded repros pass with the frost engines serving natively on cc 10.0 (sm100) and RTX 5080 (sm120); 128-test fwd ragged L0 sweep slice green on cc 10.0 (all four sm100 flavors) and 84-test slice on sm120; ex-ops suite incl. kv-interleaved views 11/11 on both; dense fwd slice 182 passed (dense compile paths pass no strides -> unchanged); packed THD configs bit-for-bit unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(agents): Hard Rule 2 — serve the declared layout natively or decline, never adapt Closes the loophole Rule 1's letter leaves open: adapter-side normalization copies that make an unsupported layout runnable. Workspace carving does not legitimize a data-tensor copy (the carve exemption is for metadata and dead-slot dummies), the dense path's grandfathered normalization is not a license for new ones, and whatever check_support accepts the kernel must address natively. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * frost(sdpa): review hardening — validate runtime THD buffers, decline overlapping strides - _thd_view validates the runtime buffer against its declaration before reinterpreting storage: dtype/device must match and the base address must be 16-byte aligned (TMA global-address rule / assumed_align=16); as_strided already rejects views past the underlying allocation. - _thd_check_strides_native additionally requires covering (non-overlapping) strides — head >= d, token >= heads*head — matching the SM120 kernel's is_layout_supported, so sub-dense declarations are declined at check_support instead of failing at the per-execute compile (or racing on O writes on SM100). - Kernel _fake_bshd guards: the head dim must be innermost-contiguous; d256/d512 validate the O stride at BPE_O (the O storage dtype byte size). - Clearer SM120 layout-rejection message (the entry validator accepts padded storage now; the text still demanded compact). The THD host-prep stream binding flagged in the same review round is a pre-existing issue (#476) and is split into a separate PR. Addresses CodeRabbit review feedback on #526. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * frost(sdpa): make the THD native-stride gate dtype-aware (16 // itemsize) The gate hardcoded the TMA 16-byte global-stride rule as 8 elements, the f16/bf16 case. It lives in the shared base class, so express the quantum in the tensor's own element units — 8 at 2 B/elem, 16 at 1 B/elem (fp8), 4 at 4 B/elem — per descriptor, so mixed-precision declarations check each tensor at its own dtype. No behavior change for the f16 paths this PR enables; the fp8 native-stride follow-up (#537) inherits the correct quantum for free. Suggested by @Aneureka in review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The THD execute paths do torch host work before the kernel launch — the [seq_kv | cu_q | cu_k] metadata allocation + D2H length reads + one-shot H2D upload (SM100 and the shared SM120 _thd_pack), the per-sequence O-descriptor buffer (SM100), the dummy-sink buffer (SM100), and the cached seq_q dummy's first-use allocation (SM120). These enqueued on torch's CURRENT stream while the kernel launches on the stream carried by the execute-time handle (ExecutionContext.stream): when the two differ, the prep and the kernel race. Run the prep inside _torch_stream_context (the same helper the fp8/mxfp8 amax paths already use), and resolve the launch stream BEFORE _thd_pack in both SM120 callers. Allocations happen inside the context too, so caching-allocator blocks are stream-tagged to the stream that uses them. Pre-existing since the FROST engines landed (NVIDIA#476); split out of the NVIDIA#526 review round to keep that PR scoped to native THD stride support. Only direct graph-API users with an explicit handle stream are affected — the PyTorch integration launches on torch's current stream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two per-execute fill kernels on the THD execute hot path initialized buffers whose contents provably do not matter: - The per-sequence O-descriptor buffer: the kernel's builder pass copies every qword of each slot from the base descriptor (then patches address/extent) before the fence and before any consumer read; stale workspace bytes never survive to a read. The +16-qword tail is never read at all. - The dummy sinks buffer: the sinks slot is always part of the kernel ABI, but CFG.HAS_SINK is a compile-time fold — when the graph declares no sink the kernel never reads the buffer (and execute() enforces has_sink <=> sinks is not None, so the dummy only exists in the never-read case). Both fills date to the original FROST landing (NVIDIA#476) as belt-and-braces. Rule 1: no adapter-side fills on the execute hot path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The THD execute paths do torch host work before the kernel launch — the [seq_kv | cu_q | cu_k] metadata allocation + D2H length reads + one-shot H2D upload (SM100 and the shared SM120 _thd_pack), the per-sequence O-descriptor buffer (SM100), the dummy-sink buffer (SM100), and the cached seq_q dummy's first-use allocation (SM120). These enqueued on torch's CURRENT stream while the kernel launches on the stream carried by the execute-time handle (ExecutionContext.stream): when the two differ, the prep and the kernel race. Run the prep inside _torch_stream_context (the same helper the fp8/mxfp8 amax paths already use), and resolve the launch stream BEFORE _thd_pack in both SM120 callers. Allocations happen inside the context too, so caching-allocator blocks are stream-tagged to the stream that uses them. Pre-existing since the FROST engines landed (NVIDIA#476); split out of the NVIDIA#526 review round to keep that PR scoped to native THD stride support. Only direct graph-API users with an explicit handle stream are affected — the PyTorch integration launches on torch's current stream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tor scratch (SM100) A per-execute fill kernel on the THD execute hot path initialized the per-sequence O TMA-descriptor buffer, whose contents provably do not matter: the kernel's builder pass copies every qword of each sequence's slot from the base descriptor (then patches address/extent) before the fence and before any consumer read — stale workspace bytes never survive to a read. The +16-qword tail is never read at all. The fill dates to the original FROST landing (NVIDIA#476) as belt-and-braces. Rule 1: no adapter-side fills on the execute hot path. (The matching dummy-sinks fill removal is split into its own PR.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The THD execute paths do torch host work before the kernel launch — the [seq_kv | cu_q | cu_k] metadata allocation + D2H length reads + one-shot H2D upload (SM100 and the shared SM120 _thd_pack), the per-sequence O-descriptor buffer (SM100), the dummy-sink buffer (SM100), and the cached seq_q dummy's first-use allocation (SM120). These enqueued on torch's CURRENT stream while the kernel launches on the stream carried by the execute-time handle (ExecutionContext.stream): when the two differ, the prep and the kernel race. Run the prep inside _torch_stream_context (the same helper the fp8/mxfp8 amax paths already use), and resolve the launch stream BEFORE _thd_pack in both SM120 callers. Allocations happen inside the context too, so caching-allocator blocks are stream-tagged to the stream that uses them. Pre-existing since the FROST engines landed (NVIDIA#476); split out of the NVIDIA#526 review round to keep that PR scoped to native THD stride support. Only direct graph-API users with an explicit handle stream are affected — the PyTorch integration launches on torch's current stream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tor scratch (SM100) A per-execute fill kernel on the THD execute hot path initialized the per-sequence O TMA-descriptor buffer, whose contents provably do not matter: the kernel's builder pass copies every qword of each sequence's slot from the base descriptor (then patches address/extent) before the fence and before any consumer read — stale workspace bytes never survive to a read. The +16-qword tail is never read at all. The fill dates to the original FROST landing (NVIDIA#476) as belt-and-braces. Rule 1: no adapter-side fills on the execute hot path. (The matching dummy-sinks fill removal is split into its own PR.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The THD execute paths do torch host work before the kernel launch — the [seq_kv | cu_q | cu_k] metadata allocation + D2H length reads + one-shot H2D upload (SM100 and the shared SM120 _thd_pack), the per-sequence O-descriptor buffer (SM100), the dummy-sink buffer (SM100), and the cached seq_q dummy's first-use allocation (SM120). These enqueued on torch's CURRENT stream while the kernel launches on the stream carried by the execute-time handle (ExecutionContext.stream): when the two differ, the prep and the kernel race. Run the prep inside _torch_stream_context (the same helper the fp8/mxfp8 amax paths already use), and resolve the launch stream BEFORE _thd_pack in both SM120 callers. Allocations happen inside the context too, so caching-allocator blocks are stream-tagged to the stream that uses them. Pre-existing since the FROST engines landed (NVIDIA#476); split out of the NVIDIA#526 review round to keep that PR scoped to native THD stride support. Only direct graph-API users with an explicit handle stream are affected — the PyTorch integration launches on torch's current stream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tor scratch (SM100) A per-execute fill kernel on the THD execute hot path initialized the per-sequence O TMA-descriptor buffer, whose contents provably do not matter: the kernel's builder pass copies every qword of each sequence's slot from the base descriptor (then patches address/extent) before the fence and before any consumer read — stale workspace bytes never survive to a read. The +16-qword tail is never read at all. The fill dates to the original FROST landing (NVIDIA#476) as belt-and-braces. Rule 1: no adapter-side fills on the execute hot path. (The matching dummy-sinks fill removal is split into its own PR.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…THD compile keys (#552) (#543) * frost(sdpa): bind the THD host prep to the launch stream The THD execute paths do torch host work before the kernel launch — the [seq_kv | cu_q | cu_k] metadata allocation + D2H length reads + one-shot H2D upload (SM100 and the shared SM120 _thd_pack), the per-sequence O-descriptor buffer (SM100), the dummy-sink buffer (SM100), and the cached seq_q dummy's first-use allocation (SM120). These enqueued on torch's CURRENT stream while the kernel launches on the stream carried by the execute-time handle (ExecutionContext.stream): when the two differ, the prep and the kernel race. Run the prep inside _torch_stream_context (the same helper the fp8/mxfp8 amax paths already use), and resolve the launch stream BEFORE _thd_pack in both SM120 callers. Allocations happen inside the context too, so caching-allocator blocks are stream-tagged to the stream that uses them. Pre-existing since the FROST engines landed (#476); split out of the #526 review round to keep that PR scoped to native THD stride support. Only direct graph-API users with an explicit handle stream are affected — the PyTorch integration launches on torch's current stream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * frost(sdpa): drop the redundant zero-fill of the never-read O-descriptor scratch (SM100) A per-execute fill kernel on the THD execute hot path initialized the per-sequence O TMA-descriptor buffer, whose contents provably do not matter: the kernel's builder pass copies every qword of each sequence's slot from the base descriptor (then patches address/extent) before the fence and before any consumer read — stale workspace bytes never survive to a read. The +16-qword tail is never read at all. The fill dates to the original FROST landing (#476) as belt-and-braces. Rule 1: no adapter-side fills on the execute hot path. (The matching dummy-sinks fill removal is split into its own PR.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * frost(sdpa): compile THD with dynamic token extents — plan-time-only compile keys The THD execute paths keyed the per-shape kernel compile on the packed token totals (sq=t_q, skv=t_kv, and max_sq on SM120). Under continuous batching the totals change every step, so the lru_cache degenerated into a fresh multi-second cute.compile per execute (issue #552's worst leg). - Kernel modules (SM100 d128/d192_d128/d256/d512 f16, SM120 f16/fp8): under THD the fake tensors' token extents are cute.sym_int symbols (one per ragged group — Q/O/LSE share t_q, K/V share t_kv) and the batch stride is rebuilt symbolically (the real view's batch stride is t * token_stride, a runtime value that never steps at batch extent 1). sq/skv are ignored under THD; SM100's _host reads the runtime totals from the dynamic tensor shapes. SM120's max_sq moves from a compile parameter to a runtime __call__ argument that sizes the per-sequence grid; trace-time shape checks compare only statically-known modes. - Adapter: the THD compile key is now derivable from the graph declaration alone, so compile() builds the artifact at PLAN time (the "thd-deferred" sentinel remains only for the unwired SM100 fp8 THD) and the execute paths' lru-cached compile calls are guaranteed hits; a shared _thd_compile_kwargs() keeps the two call sites identical. The all-KV-zero clamp's swapped K/V strides mint their own entry. - The D2H .tolist() round-trip still feeds the metadata upload, the ragged views' extents and the exact grid — removing it (and the CUDA- graph capture blocker) needs the plan-time-max grid + device cu_seqlens redesign tracked in #552. - New regression tests (SM100 + SM120) prove one compiled artifact serves different packed totals, checking numerics per total and asserting zero cache misses across executes. Verified on SM100 (B200-class): 487 passed / 4 skipped across the f16 dense+THD flavors, fp8, mxfp8, graph-level THD and sdpa op suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(frost): AGENTS.md Hard Rules 4 (plan-time-only compile keys) and 5 (launch-stream-ordered execute) Renumbered after #570 landed Rule 3 (no D2H reads on execute); these two complement it. Rule 4 codifies issue #552's compile-key lesson: never key a kernel compile on runtime data values — runtime extents compile dynamic (cute.sym_int), runtime launch scalars are call arguments, derived values (batch strides computed from totals) count as leaks, and with a plan-time-only key the compile belongs at plan time with a cache-miss regression test guarding the execute path. Rule 3 bans the read that feeds such a key; Rule 4 bans the key itself. The SM80 _compile_cached (#493) is flagged as the known open cleanup. Rule 3's THD known-violation entry is updated: the compile-side half is done (dynamic token extents), so t_q/t_kv now reach the host only for the metadata upload, ragged view extents and the launch grid. Rule 5 codifies this PR's stream-binding fix: every torch operation on the execute path (H2D uploads, buffer resets, allocator calls, post-kernel consumers) is ordered on the launch stream via _torch_stream_context, never implicitly on torch's current stream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
cudnn frost engines
Open source cudnn engines written using cutlass-primitives.
What's included
With contributions from cudnn team
Summary by CodeRabbit