Tags: NVIDIA/cudnn-frontend
Tags
frost(sdpa): bound the THD view batch stride — fixes d=192 THD decode… … int32 overflow (#980); mhas_v2 decode sweeps draw THD (#984) * frost(sdpa): bound the THD view's batch stride — fixes d=192 THD decode int32 overflow (#980) The packed-THD (1, T, H, D) views bound the extent-1 batch dim with stride T * token_stride. The kernel ABI checks every stride against the int32 range, so long packed KV with wide tokens overflows it — h=128, d=192, ~57k tokens -> 5.7e9 -> tvm-ffi 'Out of bound k_tensor.strides[0] ... expected to be in int32 range' at execute. d=128/h=8 stays ~1e8, which is why only the dsv3/kimi_k3 (d=192) THD decode configs tripped it. An extent-1 dim's stride is never stepped, and the THD compile key already zeroes it (_thd_compile_kwargs), so bind the token stride instead — always in range, semantically identical. Same fix in the fp8 THD _packed view. Verified on B200 (9.26): the exact CI config (b=27, h=128, d_qk=192, total_kv=57664) and a minimal repro (~90k packed KV tokens) both fail before / pass after. test_mhas_v2: the decode sweeps (random_sq1_L0, lean_attn_L0) now draw ragged (packed-THD) decode too — they were dense-only, which is how this combination went unexercised. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * frost(sdpa): compile() fakes bind the token stride for the THD batch dim too (#980) The seven kernel compile() fakes described the THD batch stride as shape[1] * stride[1] -- the same T * token_stride formula the runtime view stopped using in the previous commit. The DSL takes the runtime value, so this was already working, but the compile-time and runtime layouts should describe the same tensor: bind the token stride in the fake as well, and say why in the comments/docstrings. Why not 64-bit strides: the real THD strides are bounded by construction (token stride h*d*(1+gap), head stride d, elem 1) and per-token addressing goes through TMA coordinates with 64-bit descriptor math -- the 11 GB CI config runs correctly. Only the never-stepped extent-1 batch stride can overflow; widening it would mean a 64-bit token-extent symbol (64-bit loop/tile math in the hot loop) to carry a value nothing reads. Verified on B200: both #980 repros pass; THD f16 regression (context THD, THD chunked, THD decode; 32 ids) 32/32 at 100% FROST routing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * frost(sdpa): address review -- sm107 fakes, prose, deterministic overflow test (#980) Post-rebase onto develop (#974 added the sm107 kernel tree, #986 removed test_mhas.py): * sm107 prefill kernels (d128/d192/d256/d256_fp8/d512/d512_fp8): the THD compile() fake bound `shape[1] * stride[1]` for the extent-1 batch dim, the same expression the sm100/sm120 kernels had. Bind `stride[1]` instead, matching `_thd_view`, with the same comment. * Docstrings that still described the batch stride as "tokens * token_stride, a RUNTIME value rebuilt from the dynamic token extent" (sm107, sm120 compile(), `_thd_compile_kwargs`) now say what the fake actually binds and why (CodeRabbit). * test_mhas_v2: `test_sdpa_thd_batch_stride_int32_overflow_L0` -- a deterministic pin of the #980 shape (128 heads x d_qk=192 x 90,800 packed KV tokens = 2.23e9 elements > 2^31) so the fix is not guarded only by the RNG-drawn sq1/lean sweeps. Skips on GPUs under 16 GiB. Verified on B200 (cuDNN 9.26 + FROST): dsv3-f16-test6 and dsv3_small repros pass, the new test passes, THD regression 32/32. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
feat(kernelcache): add revision() and size() accessors (#909) * kernel_cache: add revision() and size() accessors Add two read-only accessors to fe::KernelCache that wrap the CUDNN_ATTR_KERNEL_CACHE_REVISION and CUDNN_ATTR_KERNEL_CACHE_ENTRY_COUNT backend attributes, which cuDNN 9.27 adds.
frost: python-native validate() per engine family (SDPA, GEMM) — defe… …r the C++ lowering when a python engine is a candidate; fixes it exposes on SM80 bwd and SM100 FP8 (#869) * frost(sdpa): python-native validate() — defer C++ lowering when a python engine is a candidate pygraph.validate() classically lowers every backend-lowerable graph to C++ and runs the backend's validate(), coupling frontend-only (FROST) engines to the installed backend's version: a graph a python engine fully serves is rejected because the backend is too old to *validate* an attribute it will never execute (issue #704). Now, when every node is SDPA-family and the manifest offers a python engine for the graph, validate() runs the new python-native semantic validation (_sdpa_validate.py: the version/arch-agnostic subset of the classic C++ pre-validation, with classic error types and messages) and defers the backend's verdict to planning, where a decline is already recorded (backend_plan_entries) and surfaced by plan() only if no python engine proposes a plan either. Without a python candidate the classic eager-lowering timing is unchanged. Fixes #704 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(frost/sdpa): pin d=256 backward through the graph API on SM80 (#704 / #864) Generalize the SM80 backward end-to-end test over head dim and add the d=256 case: with a python engine candidate, validate() must not apply the native backward node's Ampere hidden_dim <= 128 gate; the SM80 row serves d <= 256. Fails on develop at g.validate() (cudnnGraphNotSupportedError), passes with the python-native validate(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * frost(sdpa): SM80 backward forms dS from fp32 P (sP handoff no longer bf16) The sub-group split hands P from sg0 to sg1 through SMEM to form dS = scale·(dP − do_dot)·P. That copy was io_dtype, so dS saw a bf16-rounded P — one rounding more than FlashAttention-2 (and our d=64 kernel), whose dS reads the fp32 register copy. The error scales with |dP| ~ sqrt(d): at d=256 bf16 the classic battery's test229 (GQA 8:2, s=5, causal) missed one dQ element by 2.4e-2 against a 2e-2 tolerance; fp16 and d<=128 passed. sP is fp32 now (+tile_kv*tile_q*2 B SMEM; every flavor still fits the A100 cap at one CTA/SM). Writer and reader share the lane mapping, so the store and load simply widen; the bf16 roundings that remain are the MMA operands (P for dV, dS for dK/dQ), which are inherent. A100: test229 passes; test_mhas_v2 L0 with FROST on 1459/1459 (was 1458+1); bwd wrapper file 69/69; timing equal or slightly better (fewer converts): s=4096 d=128 2.350 -> 2.242 ms, d=256 7.590 -> 7.547 ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: int dropout probability, candidate cache reset on mutation, mixed-graph test, docstrings - _sdpa_validate._dropout treats any real (non-bool) number as the probability form, so dropout=(1, seed, offset) reaches the p == 1 rejection and the bottom-right-causal dropout rule. - pygraph._check_mutable clears the cached candidate list along with the validated flag: validate() may cache candidates before the freeze, and a later mutation must not plan on the pre-mutation match. - test_mixed_graph_still_lowers pins the second half of the routing rule: an uncovered node (pointwise on O) keeps the classic eager lowering even with a python candidate. - Unused `alignment` unpacks renamed; ASCII operators in the new kernel comment; docstrings on the validator helpers, kernel entry, and tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: mark the graph validated only after every validation check passes validate() set _is_validated before the C++ (or now python-native) validation ran, so a rejected graph read as validated and build()/plan() would skip re-validation. The flag is set last now; a rejection leaves it False and the next build_operation_graph() re-validates and raises the same error. Regression test: rejected native validation -> flag False -> build raises. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: roll the C++ lowering back when backend validation rejects the graph _lower_to_cpp() populates _lowered_graph before the backend validate() runs; on rejection the stale lowered graph survived, so the next validate() skipped the backend check and marked the rejected graph valid. Validation failure now drops every lowering artifact via _reset_lowered_state() (the reset the two existing decline/rollback sites already did inline), so the retry re-runs the backend check and raises again. FROST-off regression test added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * frost(sdpa): SM100 per-tensor FP8 d192x128 and D256 flavors serve only their exact shapes With the python-native validate() in this PR, SDPA_FP8 graphs the C++ validator used to reject (Blackwell: d <= 128 %16, exact 192/128, exact 256/256, or the (256, 512] band) reach the SM100 fp8 engine, whose envelope admitted d_qk zero-padded into the d192x128 flavor and, after #860, any shape padded into the D256 flavor. Both padded paths are unvalidated and wrong: - d192x128 with d_qk in {144, 160, 176}: 4-19% of elements off by O(1) against the fp32 reference (test_mhas_v2 fp8 fwd; geometry- and descale-independent; d_v-only padding and the d128 / d512 flavors' padding are exact). The existing wrapper test passed only because its inputs are tiny (|O| <= 0.11) under a 0.05 + 5% tolerance. - D256 padded envelope: run-to-run nondeterminism on long causal e5m2 / GQA / sink graphs (test44, test154, test168 pass alone and fail in the battery). Floor both flavors to their exact shapes -- ((192, 128), 128), ((256, 256), 255) -- in the engine row and the adapter mirror, and make BOTH flavor selections honour the floors (engines._selected_d_shape for the knob domains, api_dsl._pick_flavor for the lowering) so an inexact graph is never planned for one flavor and lowered onto another. The whole inexact (128, 256) region is declined on every arch and the classic backend verdict applies there: develop's effective behaviour, with the d128 flavor's envelope (the ViT d=72-in-80 case) and the d512 band untouched. Lifting the floors is the way to re-admit the region once the kernels' padded paths pass the battery. SM100: test_mhas_v2 fp8 fwd L0 back to develop's 165 passed / 91 skipped (was 7 failed); the seven cases skip on "no engine proposes a plan". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * frost: python-native validate() as a per-family manifest hook (+ GEMM validator) Generalize the SDPA-only predicate: EngineFamily gains a `validator` ("module", "callable") declaration next to `analyzer` and `heuristics`, and pygraph.validate() asks the graph's family for it -- native validation runs when the family declares one AND the manifest offers a python engine for the graph; the validator returns False (classic eager C++ lowering) for a graph holding a node it does not cover. A family without a validator keeps classic behaviour, so this is opt-in per family and nothing changes until one opts in. Families with a C++ lowering are the only ones the coupling in issue #704 can reach: the linear-attention families are python_only and never lowered eagerly. After SDPA that leaves frost_gemm, whose MoE grouped-matmul node needs cuDNN >= 9.15 (fwd) / 9.22 (bwd) just to validate -- the same version-coupling as fp8 THD SDPA on < 9.25. cudnn/_gemm_validate.py carries the structural matmul facts (both operands bound, equal rank >= 2, contraction agreement, broadcastable batch dims, a declared C matching (M, N)) and the MoE nodes' ATTRIBUTE_NOT_SET checks with classic messages; no arch or version gates, as for SDPA. The C++ matmul node has no semantic pre-validation of its own (the backend judges at plan time), so this is the honest subset. Tests (device-free): gemm graphs validate natively with a candidate, lower classically without one, for an uncovered node (pointwise epilogue), and for a family without a validator; contraction / broadcast / declared-output rejections raise the classic error type; the SDPA suite is unchanged through the hook. A100: classic matmul/MoE tests identical with FROST on/off, sm80 integration and test_mhas_v2 fwd+bwd L0 green with routing unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(sdpa): support matrix — per-tensor FP8 d192x128 / d256 flavors are exact-shape only Rule S2 follow-up for the fp8 envelope floors in this PR: the SM100 head-dim envelope row now says the d192x128 and d256 per-tensor FP8 flavors serve only their exact shapes, with a footnote on why (d_qk padded into d192x128 is numerically wrong; the d256 padded envelope is nondeterministic) and what is unaffected (the d128 flavor's x16 envelope, the d512 band). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: keep the pygraph / FROST design docs true for the per-family validator Rule 13 of python/cudnn/frost/README.md: the design docs change with the code. docs/python_graph_and_execution_backends.md described validate() as always lowering + freezing any backend-lowerable graph; it now says when the family's native validator runs instead, adds the `validator` hook to the manifest section, widens the mutable-after-validate window accordingly, and qualifies the classic-parity invariant (semantic rejections still raise from validate(); with a python candidate, a backend-only version/arch rejection surfaces at plan() and only if no python engine proposes a plan). The FROST README's EngineFamily example and hook list gain `validator`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Vedaanta Agarwalla <vagarwalla@nvidia.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
frost: python-native validate() per engine family (SDPA, GEMM) — defe… …r the C++ lowering when a python engine is a candidate; fixes it exposes on SM80 bwd and SM100 FP8 (#869) * frost(sdpa): python-native validate() — defer C++ lowering when a python engine is a candidate pygraph.validate() classically lowers every backend-lowerable graph to C++ and runs the backend's validate(), coupling frontend-only (FROST) engines to the installed backend's version: a graph a python engine fully serves is rejected because the backend is too old to *validate* an attribute it will never execute (issue #704). Now, when every node is SDPA-family and the manifest offers a python engine for the graph, validate() runs the new python-native semantic validation (_sdpa_validate.py: the version/arch-agnostic subset of the classic C++ pre-validation, with classic error types and messages) and defers the backend's verdict to planning, where a decline is already recorded (backend_plan_entries) and surfaced by plan() only if no python engine proposes a plan either. Without a python candidate the classic eager-lowering timing is unchanged. Fixes #704 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(frost/sdpa): pin d=256 backward through the graph API on SM80 (#704 / #864) Generalize the SM80 backward end-to-end test over head dim and add the d=256 case: with a python engine candidate, validate() must not apply the native backward node's Ampere hidden_dim <= 128 gate; the SM80 row serves d <= 256. Fails on develop at g.validate() (cudnnGraphNotSupportedError), passes with the python-native validate(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * frost(sdpa): SM80 backward forms dS from fp32 P (sP handoff no longer bf16) The sub-group split hands P from sg0 to sg1 through SMEM to form dS = scale·(dP − do_dot)·P. That copy was io_dtype, so dS saw a bf16-rounded P — one rounding more than FlashAttention-2 (and our d=64 kernel), whose dS reads the fp32 register copy. The error scales with |dP| ~ sqrt(d): at d=256 bf16 the classic battery's test229 (GQA 8:2, s=5, causal) missed one dQ element by 2.4e-2 against a 2e-2 tolerance; fp16 and d<=128 passed. sP is fp32 now (+tile_kv*tile_q*2 B SMEM; every flavor still fits the A100 cap at one CTA/SM). Writer and reader share the lane mapping, so the store and load simply widen; the bf16 roundings that remain are the MMA operands (P for dV, dS for dK/dQ), which are inherent. A100: test229 passes; test_mhas_v2 L0 with FROST on 1459/1459 (was 1458+1); bwd wrapper file 69/69; timing equal or slightly better (fewer converts): s=4096 d=128 2.350 -> 2.242 ms, d=256 7.590 -> 7.547 ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: int dropout probability, candidate cache reset on mutation, mixed-graph test, docstrings - _sdpa_validate._dropout treats any real (non-bool) number as the probability form, so dropout=(1, seed, offset) reaches the p == 1 rejection and the bottom-right-causal dropout rule. - pygraph._check_mutable clears the cached candidate list along with the validated flag: validate() may cache candidates before the freeze, and a later mutation must not plan on the pre-mutation match. - test_mixed_graph_still_lowers pins the second half of the routing rule: an uncovered node (pointwise on O) keeps the classic eager lowering even with a python candidate. - Unused `alignment` unpacks renamed; ASCII operators in the new kernel comment; docstrings on the validator helpers, kernel entry, and tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: mark the graph validated only after every validation check passes validate() set _is_validated before the C++ (or now python-native) validation ran, so a rejected graph read as validated and build()/plan() would skip re-validation. The flag is set last now; a rejection leaves it False and the next build_operation_graph() re-validates and raises the same error. Regression test: rejected native validation -> flag False -> build raises. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: roll the C++ lowering back when backend validation rejects the graph _lower_to_cpp() populates _lowered_graph before the backend validate() runs; on rejection the stale lowered graph survived, so the next validate() skipped the backend check and marked the rejected graph valid. Validation failure now drops every lowering artifact via _reset_lowered_state() (the reset the two existing decline/rollback sites already did inline), so the retry re-runs the backend check and raises again. FROST-off regression test added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * frost(sdpa): SM100 per-tensor FP8 d192x128 and D256 flavors serve only their exact shapes With the python-native validate() in this PR, SDPA_FP8 graphs the C++ validator used to reject (Blackwell: d <= 128 %16, exact 192/128, exact 256/256, or the (256, 512] band) reach the SM100 fp8 engine, whose envelope admitted d_qk zero-padded into the d192x128 flavor and, after #860, any shape padded into the D256 flavor. Both padded paths are unvalidated and wrong: - d192x128 with d_qk in {144, 160, 176}: 4-19% of elements off by O(1) against the fp32 reference (test_mhas_v2 fp8 fwd; geometry- and descale-independent; d_v-only padding and the d128 / d512 flavors' padding are exact). The existing wrapper test passed only because its inputs are tiny (|O| <= 0.11) under a 0.05 + 5% tolerance. - D256 padded envelope: run-to-run nondeterminism on long causal e5m2 / GQA / sink graphs (test44, test154, test168 pass alone and fail in the battery). Floor both flavors to their exact shapes -- ((192, 128), 128), ((256, 256), 255) -- in the engine row and the adapter mirror, and make BOTH flavor selections honour the floors (engines._selected_d_shape for the knob domains, api_dsl._pick_flavor for the lowering) so an inexact graph is never planned for one flavor and lowered onto another. The whole inexact (128, 256) region is declined on every arch and the classic backend verdict applies there: develop's effective behaviour, with the d128 flavor's envelope (the ViT d=72-in-80 case) and the d512 band untouched. Lifting the floors is the way to re-admit the region once the kernels' padded paths pass the battery. SM100: test_mhas_v2 fp8 fwd L0 back to develop's 165 passed / 91 skipped (was 7 failed); the seven cases skip on "no engine proposes a plan". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * frost: python-native validate() as a per-family manifest hook (+ GEMM validator) Generalize the SDPA-only predicate: EngineFamily gains a `validator` ("module", "callable") declaration next to `analyzer` and `heuristics`, and pygraph.validate() asks the graph's family for it -- native validation runs when the family declares one AND the manifest offers a python engine for the graph; the validator returns False (classic eager C++ lowering) for a graph holding a node it does not cover. A family without a validator keeps classic behaviour, so this is opt-in per family and nothing changes until one opts in. Families with a C++ lowering are the only ones the coupling in issue #704 can reach: the linear-attention families are python_only and never lowered eagerly. After SDPA that leaves frost_gemm, whose MoE grouped-matmul node needs cuDNN >= 9.15 (fwd) / 9.22 (bwd) just to validate -- the same version-coupling as fp8 THD SDPA on < 9.25. cudnn/_gemm_validate.py carries the structural matmul facts (both operands bound, equal rank >= 2, contraction agreement, broadcastable batch dims, a declared C matching (M, N)) and the MoE nodes' ATTRIBUTE_NOT_SET checks with classic messages; no arch or version gates, as for SDPA. The C++ matmul node has no semantic pre-validation of its own (the backend judges at plan time), so this is the honest subset. Tests (device-free): gemm graphs validate natively with a candidate, lower classically without one, for an uncovered node (pointwise epilogue), and for a family without a validator; contraction / broadcast / declared-output rejections raise the classic error type; the SDPA suite is unchanged through the hook. A100: classic matmul/MoE tests identical with FROST on/off, sm80 integration and test_mhas_v2 fwd+bwd L0 green with routing unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(sdpa): support matrix — per-tensor FP8 d192x128 / d256 flavors are exact-shape only Rule S2 follow-up for the fp8 envelope floors in this PR: the SM100 head-dim envelope row now says the d192x128 and d256 per-tensor FP8 flavors serve only their exact shapes, with a footnote on why (d_qk padded into d192x128 is numerically wrong; the d256 padded envelope is nondeterministic) and what is unaffected (the d128 flavor's x16 envelope, the d512 band). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: keep the pygraph / FROST design docs true for the per-family validator Rule 13 of python/cudnn/frost/README.md: the design docs change with the code. docs/python_graph_and_execution_backends.md described validate() as always lowering + freezing any backend-lowerable graph; it now says when the family's native validator runs instead, adds the `validator` hook to the manifest section, widens the mutable-after-validate window accordingly, and qualifies the classic-parity invariant (semantic rejections still raise from validate(); with a python candidate, a backend-only version/arch rejection surfaces at plan() and only if no python engine proposes a plan). The FROST README's EngineFamily example and hook list gain `validator`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Vedaanta Agarwalla <vagarwalla@nvidia.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1.28.0 rc (#851) * Test/sample improvements + block-scale & SDPA fixes (9.18–9.24 fuzzer mining) (#330) * test: fuzzer coverage from 9.18-9.24 fixed-bug mining Derived from a triage of the 134 fixed front-end bugs in cuDNN 9.18-9.24. - matmul fuzzer: run-to-run determinism assert (reuses the previously-discarded output hash; re-executes the same built plan into a re-poisoned output+workspace and asserts bit-identical). Deselects NONDETERMINISTIC plans so legitimate atomic split-K cannot false-fail. Env: MATMUL_DET_RERUNS / MATMUL_NUM_TESTS / MATMUL_FUZZ_SEED. - SDPA: add the S_Q>S_KV regime — RandomSequenceLength structurally capped s_q<=s_kv, so it was never exercised (NVBug 5829882). Clamped to s_q_max; wired into 9 suites. Env: MHAS_NUM_TESTS / MHAS_SEED_OFFSET. - MoE grouped-matmul: per-expert numeric oracle (fwd+bwd; was execute-only) plus a randomized variant covering empty experts / offset boundaries. - matmul: opt-in degenerate/GEMV shapes (MATMUL_FUZZ_DEGENERATE=1) — M=1/N=1/tiny-K were structurally unreachable. Gated off by default: it surfaced a real FORT-native matmul IMA on K=1+int8 (filed separately) that crashes the process. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(low-precision-matmul): use canonical block-reduced nvfp4 descale shape The fp4 matmul test passed a full-size descale (1,M,K)=(1,128,64) instead of the canonical F8_128x4 block-reduced (1,M,ceil(K/block) rounded to 4)=(1,128,4) (and B symmetrically). It only "passed" because scales were all 1.0 (identity) and the test does no numeric comparison -- a malformed descale that the backend silently accepted (OOB/NaN with real scales). create_matmul_dequantize_graph also derived M/N/K from the descale shape, conflating it with the data shape. Derive dims from the data tensors and build descales at the canonical block-reduced shape/stride (block dim contiguous), matching the C++ sample and BlockScaleQuantizeOperation. Now passes the new dequant shape guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * sample(sdpa-mxfp8): align fwd SF_V to d-contiguous (stride[3]==1) convention The fwd mxfp8 sample was the lone outlier declaring SF_V s_scale-contiguous (stride[2]==1); SF_Q/SF_K, the bwd sample, and test_mhas_v2 all use d-contiguous (stride[3]==1). The kernel reads block-scale factors via the F8_128x4 swizzle, so the declared inner stride is not load-bearing (verified: flipping it with fixed data is bit-identical) -- consistency/clarity fix, behavior unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(matmul-fuzzer): add MATMUL_FUZZ_UNALIGNED for FORT-native widening-cast corner Opt-in: emit non-mult-of-4 K/N so the bits_per_access<32 LDG+STS smem-staging path is reachable, where a widening-cast (int8/fp8->fp16/fp32) operand over-runs the staging buffer (silent wrong-result on unaligned K, IMA on unaligned N). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: remove broken L2 mxfp8 SDPA test (home-grown swizzle reference) create_scale_factor_tensor_for_sdpa builds the F8_128x4 scale swizzle by hand inconsistently with the kernel, feeding mis-ordered scales -> fails numerically across cuDNN versions (incl. official 9.23.1.3). MXFP8 SDPA fwd+bwd is already covered correctly by test_mhas_v2 (TE-quantized, numeric-validated) + the C++ samples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: correct mislabeled IS_VIRTUAL tensor descriptor error message The IS_VIRTUAL SetAttribute failure reused the BYTE_ALIGNMENT error string. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Yang Xu <yanxu@nvidia.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix formatting issues by various commits before 1.26.0 (#341) * remove unprofessional comments (#349) * BSA: avoid guardword scanner false positives (#350) * benchmark: fix repo-root path resolution in bench_moe (#348) * Python-native cudnn.pygraph: graph IR + pluggable execution backends (#336) * feat(python): backend-agnostic native graph + Router (unification proposal) Modernize the Python-native graph API into the backend-dispatch architecture from the Frontend v1 "Python API Engine and Graph API Unification" proposal. Graph construction stays backend-agnostic; a backend is chosen by a first-class Router at create_execution_plans() time (per Anerudhan's feedback), and the backend-specific representation (e.g. the C++ cuDNN graph) is generated lazily only then: Python Graph API -> create_execution_plans() -> Router -> selected backend (native engine, else cuDNN) Layers kept separate: - Graph IR (Node/Tensor/NativeGraph): engine-agnostic op DAG, full introspection - BaseEngine: the backend contract (check_support/execute/get_workspace_size + priority); cuDNN Graph is one routed backend, not a hardcoded default - Router (engines/router.py): first-supporting by priority; None => cuDNN Included: the IR, BaseEngine, Router, a CPU-only ReferenceMatmulEngine (CI-testable correctness oracle), the optional MatmulCuTileEngine, and node builders for block-scale / MoE / reduction so a DSL fusion backend can consume them via graph.nodes (replacing the monkey-patch "recorder"). Deferred to follow-ups (see docs/python_native_graph_router.md): NativeGraph.from_pygraph() (raises NotImplementedError for now), the DSL fusion backend port, attention backends, and cuDNN lowering of the new node types. Tests: 42 passing on CPU (IR + Router + reference-engine execute + cuDNN fallback); cuTile path gated to SM100. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(python): trim NodeType to exercised ops; doc mixed candidate-list routing - NodeType now lists only the op types this version exercises; drop the unused norm/reshape/slice/etc. entries (re-add per-op when needed, following the block-scale / MoE / reduction examples). - Document the target routing model: create_execution_plans() takes one mixed candidate list (native engines + cuDNN heur_modes) and produces a ranked list of plans across backends; this PR ships the first-supporting-by-priority form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(python): drop BATCHNORM / BATCHNORM_INFERENCE from NodeType (unused) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(python): remove conv ops from native graph (unused foundation) Drop CONV_FPROP / CONV_DGRAD / CONV_WGRAD: enum entries, the conv_fprop / conv_dgrad builders, their dim inference in nodes.py, cuDNN lowering branches, and the conv test. Re-add per-op when a backend needs conv, following the block-scale / MoE / reduction examples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(python): use generic 'python DSLs' for backend examples Avoid naming specific internal backends in public docs/docstrings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(python): unify engines into one flat engine-id space (no cuDNN wrapper) Replace the single-selected-backend + "if native else cpp" fork with the engine-id model: python engines and cuDNN backend engines share one flat id space. Python engines occupy a reserved high region (engine_ids.py, PYTHON_ENGINE_ID_BASE = 1<<20) and each declares a stable engine_id it owns, so ids never shift with registration order (reproducible autotune / pinned plans). - engine_ids.py: PYTHON_ENGINE_ID_BASE + is_python_engine() + a phase-1 CUDNN_HEURISTIC_ENGINE_ID sentinel. Single source of truth for the namespace. - Router.select()->one-engine becomes Router.plan()->ranked list of PlanConfig(engine_id, knobs): supporting python engines (by id) + one trailing cuDNN entry. TODO: interleave the true per-engine cuDNN configs (get_engine_and_knobs_at_index) + real heuristics ranking; for now just concat. - NativeGraph: _selected(engine) -> _plans(list) + _plan_index; add get_execution_plan_count() / select_plan(i). check_support / build_plans / get_workspace_size / execute all dispatch on the selected plan's id via is_python_engine — one predicate, no fork. cuDNN is lowered lazily only when a cuDNN-id plan is selected (pure-python when a python plan wins). - BaseEngine: drop `priority`, add stable `engine_id` (reserved region). reference_matmul = BASE+0, matmul_cutile = BASE+1. Tests updated to assert the plan list; 41 pass on CPU incl. cuDNN fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): make cudnn.pygraph engine-aware in place (transparent front door) Users keep the classic API — g = cudnn.pygraph(...) is unchanged for every existing sample — yet a graph transparently routes to a registered python engine when it's fully represented. No new user-facing class, no rename. pygraph_engines.install(pygraph) (called from __init__, same sanctioned pattern as pygraph.execute = _execute) augments the pybind class in place: - Per-graph mirror (WeakKeyDictionary) records a Node/Tensor IR alongside the real C++ calls for a curated represented set (matmul + common pointwise), mirrored via the NativeGraph builders so the recorded op is exactly what engines consume. - Every other op-builder is auto-wrapped to flag the graph "opaque" — the safe direction: only disables the python path, never changes classic output. - Lifecycle (create_execution_plans/check_support/build_plans/get_workspace_size/ execute/build) routes to a python engine iff one is registered AND the whole graph is represented AND it supports the graph; else delegates to the untouched C++ path. Verified on an L40S against the real cuDNN build: a classic matmul runs byte-identically with and without the augmentation, and a matmul+bias+relu graph built via cudnn.pygraph + ReferenceMatmulEngine routes to the python engine with exact results. Eager for now (C++ graph still built); lazy/pure-python is the follow-up (needs a structured builder per op — multi-tensor returns like sdpa can't be mirrored generically). NativeGraph stays as the standalone/greenfield authoring object sharing the same IR + engines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): native GEMM-family lowering + fix cuDNN execute path (phase 1) Toward the native cudnn.pygraph migration (GEMM-family first). Make the native build->lower->cuDNN execute path actually work end to end, and extend lowering coverage to the GEMM family. Fixes (all latent — the cuDNN execute path had never been GPU-tested): - Thread the cuDNN handle: NativeGraph(handle=...) -> passed to the lowered cudnn.pygraph so heuristics/build have a handle. - Propagate the IR uid to the C++ tensor (was uid=-1 for auto tensors), so execute()'s variant pack (keyed by IR uid) actually binds the buffers. - POINTWISE lowering: the C++ pygraph has no generic pointwise(); dispatch on the mode to the named ops (relu/gelu/sigmoid/tanh, add/mul/sub/div; add/mul also cover bias/scale via broadcast). Lowering coverage added: reduction, block_scale_dequantize, block_scale_quantize (2 outputs), moe_grouped_matmul. Validated on GPU (SM89): matmul and matmul+bias+relu built natively via NativeGraph, lowered to cuDNN, execute with exact parity (new test_native_cudnn_lowering.py, GPU-gated). Full native/router/pygraph suite: 45 passing. Per-op output-shape inference (e.g. reduction reduced dims) and block-scale/moe execution parity are the next slices. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): reduction output-shape + SF reordering lowering (GEMM-family phase 2) - reduction(): take an explicit reduced `dim` (cuDNN requires the reduction output dims set); lowering sets set_dim/set_stride on the cuDNN op. Validated matmul -> reduction(ADD over N) parity on GPU. - lower_tensor(): propagate reordering_type to _make_tensor (e.g. F8_128x4), needed for block-scale scale-factor tensors. Native/router/pygraph + GPU parity suite: 46 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): native block-scale (nvfp4) lowering on Blackwell + fixes (phase 3) Complete the GEMM-family native lowering with block-scale, validated on SM100. Two more latent cuDNN-path bugs fixed: - _lower_to_cpp passed io_data_type=None -> cudnn.pygraph rejects None. Now omit io when unset; default intermediate/compute to FLOAT (matching cudnn.graph()) so cuDNN infers virtual (intermediate) tensor dtypes during build. - lower_tensor now propagates reordering_type (F8_128x4) and omits data_type when unset (NOT_SET) so cuDNN infers fused block-scale dequant output types. Validated on SM100: dequant(A_fp4)@dequant(B_fp4) with F8_128x4 SFs builds + executes via NativeGraph (test gated to SM100 + torch fp4; parity harness = the repo's own fp4 test, which also only checks execution). CPU overhead of the native Python layer (512^3 fp16, L40S): build +0.40 ms on ~106 ms (~0.4%, dominated by cuDNN heuristics); execute +0.3 us/call (9.8 -> 10.1 us). Negligible. Native/router/pygraph + GPU parity (matmul, bias+relu, reduction, block-scale): 48 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): native moe_grouped_matmul lowering + parity (GEMM-family complete) - Add moe output-shape inference (token [1,T,H], weight [E,H,N] -> out [1,T,N]) so NativeGraph.validate() passes; cuDNN infers the same at build. - GPU parity test (self-contained per-expert reference; no dependency on the upstream test's helper) — validated on SM100. GEMM family now fully native-lowered + validated on GPU: matmul, pointwise (bias/relu), reduction, block-scale nvfp4, moe. Suite: 48 passing. Next: non-GEMM ops (norms/reshape/slice/...) then the C++ _op rename + atomic flip of cudnn.pygraph. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(python): IR-uid -> C++-uid translation at execute; native rmsnorm (first norm) Systemic fix: op-created C++ tensors (op outputs / virtuals) get uids assigned by the C++ FE during build_operation_graph, in ITS enumeration order — which does not match IR allocation order for multi-output ops (rmsnorm assigns INV_VARIANCE=5, Y=6 while the IR allocated Y=5, inv_var=6). Keying the variant pack by raw IR uids bound Y's buffer to inv_var: a [N,C,H,W] fp16 write into a 16-byte buffer (heap corruption / NaN). Single-output ops only worked by allocation-order coincidence. Fix: keep the lowering tensor_map; after build_operation_graph query every C++ tensor's real uid into an explicit IR-uid -> C++-uid map; execute() translates variant-pack keys through it. No more order coincidence anywhere. rmsnorm added as the first-class norm template (per "no corner-cutting" — the generic opaque-op bridge was rejected/reverted since it makes non-GEMM ops un-introspectable black boxes): named input/scale/epsilon/bias ports, Y/inv_var outputs, norm_forward_phase param, pass-by-value epsilon; Y/inv_var dims carried in the IR, cuDNN infers on its side. GPU parity: errY=0.0019, errI=0.0. Suite: 49 passing (GEMM family re-validated through the translation path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(python): Python IR owns the uid namespace end to end Systematic uid review — four assignment paths existed: 1. user at creation: tensor(uid=...) (pybind _make_tensor, default -1) 2. user post-creation: tensor.set_uid() (mainline integrator pattern) 3. C++ FE auto-assign at build_operation_graph (enumeration order, nondeterministic for multi-output ops) <- the coincidence trap 4. Python IR _alloc_uid (eager, sequential) New invariant: for Python-built graphs, (3) NEVER triggers. The IR assigns every uid eagerly at creation (auto or user-specified); lowering pushes ALL of them explicitly to C++ — inputs via _make_tensor(uid=), op-created outputs/virtuals via one set_uid loop over the complete tensor_map (single point, impossible to forget per-op). Mixed construction (extending the lowered C++ graph directly) is unsupported: a graph is pure-Python or pure-C++. - Replace the IR->C++ uid translation map with a post-build ASSERTION: a lowering path that fails to push a uid now fails loudly instead of being silently translated (or worse, mis-binding buffers). - _alloc_uid skips user-reserved uids; duplicate explicit uids rejected eagerly at tensor() (C++ would only fail at build). - execute() keys the variant pack by IR uids directly (== C++ uids by construction). Suite: 50 passing on SM100 (rmsnorm multi-output canary + block-scale included). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): full pointwise coverage — 54 ops, table-driven, mode == method name Cover the entire pointwise surface of the C++ pygraph (54 methods) natively: - Canonical op kind: params["mode"] IS the C++ pygraph method name (the pointwise_mode enum is not exposed to Python; the method name is the semantic name). Lowering collapses to a direct getattr dispatch — the mode<->method mapping table is deleted as a concept. - 47 uniform ops are generated from _POINTWISE_TENSOR_ARGS, a table of the pybind tensor-argument names per op (mirrors the C++ signatures), so both positional and the classic keyword call styles (bias(input=, bias=), max(input0=, input1=)) work — required for the eventual cudnn.pygraph flip. - 7 ops with scalar attributes get explicit builders storing them in params (introspectable): relu(negative_slope/lower_clip/upper_clip), leaky_relu, swish(swish_beta), gen_index(axis), + relu/leaky_relu/swish backwards. Lowering forwards them as keywords. - ReferenceMatmulEngine: keys move to method names; declines pointwise nodes carrying scalar attributes it does not implement (correct-by-construction). - Front-door mirror: classic calls passing scalar extras (e.g. relu clips) now flag the graph opaque instead of silently dropping the attribute and mis-routing to a python engine. Tests: every builder exercised in both call styles + scalar-attr introspection (CPU); sqrt/abs/max/min chain through real cuDNN on GPU. 53 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): norm family via one declarative table (10 ops, generic lowering) All norms native — rmsnorm(_backward), layernorm(_backward), adalayernorm(_backward), instancenorm(_backward), batchnorm, batchnorm_inference, batchnorm_backward — through ONE mechanism instead of per-op code: - _STRUCTURED_OPS: a declarative table per op — NodeType, tensor-input ports (== the C++ pybind kwarg names), enum/scalar params (norm_forward_phase, has_dbias), output ports in C++ return order, and per-output shape inference (IR-side dims for introspection; cuDNN re-infers at build). Builders are generated (keyword call style, as these ops are used repo-wide); lowering is one generic branch: kwargs assembly + one call + zip outputs. - List inputs (batchnorm peer_stats) become indexed ports (peer_stats_i) + a count param, reassembled at lowering. - The hand-written rmsnorm builder AND its lowering branch are deleted — migrated into the table; the suite re-validates rmsnorm through the generic path (multi-output uid canary intact). GPU parity: layernorm fwd (Y/mean/inv_var) + layernorm_backward (DX/DScale/ DBias) vs torch autograd, using the supported LN config ([N,C,1,1] channels_last, as in classic test_layernorm — the initial row-major 4D attempt fails identically on the classic API, i.e. a kernel-support limit, not a lowering bug). CPU: every table op builds a first-class node with named ports; peer_stats port machinery covered. 56 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): conv + structural ops; collapse ALL structured ops into one table _STRUCTURED_OPS now covers 25 ops — norms (11 incl. genstats), reduction, block-scale (de)quantize, moe fwd/bwd, conv fprop/dgrad/wgrad, reshape, slice, transpose, concatenate, rope fwd/bwd — one declarative entry each, one generic lowering branch. Only matmul (positional ergonomics + front-door mirror) and sdpa fwd/bwd (conditional kwarg assembly) remain explicit. Deleted in the collapse: the hand-written reduction / block_scale_dequantize / block_scale_quantize / moe_grouped_matmul builders AND their four lowering branches, plus nodes.py moe shape inference (moved to the table). The suite re-validates all of them through the generic path on GPU. Table mechanics extended (each a one-word spec key, no new concepts): - attrs: scalar/enum/list params forwarded verbatim (padding vectors, axis, slices, permutation, reshape_mode, rope_dim, mode, ...). Conv accepts BOTH the symmetric `padding` convenience and pre/post_padding — forwarded as given; pybind overload resolution picks the right C++ binding. - out_dims reserved kwarg (list, or {port: dims}): explicit output shapes for ops cuDNN cannot infer — generalizes reduction's old `dim` param. - push_output_dims: IR dims pushed to C++ for dgrad/wgrad/reduction/reshape/ moe_bwd (classic API also requires set_dim there). - no_cdt: bindings without compute_data_type (reshape, concatenate). - Builders accept tensors positionally or by port name; infer lambdas are best-effort (try/except -> None; C++ validates at build). GPU parity added: conv_fprop vs torch conv2d (NHWC), incl. asserting the table's shape inference. CPU: all 25 ops x 2 call styles + out_dims. 58 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): sdpa family via generic kwarg capture — full ~130-arg surface The six sdpa variants (sdpa, sdpa_backward, sdpa_fp8, sdpa_fp8_backward, sdpa_mxfp8, sdpa_mxfp8_backward) are now declared in _CAPTURED_OPS, the third and final table mechanism: builders capture ALL kwargs generically — tensor values (incl. torch/dlpack) become named ports (port == C++ kwarg), scalars / enums / score_mod callbacks go to params verbatim, dropout tuples are flattened per element — and lowering rebuilds the kwargs for one C++ call. The full C++ kwarg surface (~130 args: paged attention tables, diagonal bands, sink tokens, cu_seqlens, fp8 descales/amaxes, ...) is supported without hand-mirroring any of it, and future binding args are picked up automatically. Deleted: the explicit sdpa/sdpa_backward builders (~170 lines, common-args only) + their two lowering branches + nodes.py sdpa shape inference (moved to table lambdas — and fixed: O is q-shaped with v's head dim, not v-shaped). Semantics now match the classic API exactly: sdpa always returns (O, Stats) with Stats None in inference mode (generate_stats/is_inference logic); output dim/stride are pushed to C++ (the SDPA node requires O's layout pre-validate — that's how BSHD vs BHSD output is chosen). GPU: sdpa causal fp16 EXECUTION parity vs torch SDPA (was build-only before). 59 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python)!: THE FLIP — cudnn.pygraph is now the Python graph class The public cudnn.pygraph name now binds the Python IR class (class name: pygraph; module: python/cudnn/pygraph.py — no "relative-to-history" naming). The C++ graph builder is internal-only at cudnn._pybind_module.pygraph and is reached exclusively through lowering: a graph is pure-Python or pure-C++, never mixed. Zero C++ changes — the demotion is by namespace, not rebuild. Deleted in the flip (afterthought residue): - pygraph_engines.py front-door + its tests (no install()/monkey-patching anywhere: register_backend is a native method on the class) - NativeGraph.from_pygraph stub (meaningless now), use_native back-door - docs/python_native_graph_router.md (initial-brainstorm doc, per review) Drop-in surface for classic parity, driven by iterating the repo's own test files until green (each item below was a real failure caught and fixed): - conditional outputs ("maybe"): rmsnorm_backward(has_dbias=False) -> DBias None; norm fwd INFERENCE -> mean/inv_var None; batchnorm next_running_* present iff in_running_* given (classic returns None for absent outputs) - torch interop: tensor(dim=x.size()) (torch.Size), data_type=torch.bfloat16 (converted at the C++ boundary via _library_type, IR stores user's value) - output dtype semantics: an output without explicit set_data_type gets io dtype (was mis-defaulted to intermediate FLOAT -> fp32 into fp16 buffers) - Tensor gains the classic setter/getter surface (set_ragged_offset, set_reordering_type, set_is_pass_by_value, ...); tensor_like(cudnn tensor); tensor_scalar; CPU tensor_like -> pass-by-value (classic rule) - ragged (THD) output layout: outputs' ragged_offset now pushed to C++ at all mapping sites (was silently dense -> wrong values in sdpa_thd) - validate-time table shape inference (topological): chained ops whose inputs are virtual (conv on a relu output) infer once inputs are known; builder-time infer stays as best-effort for direct inputs - classic lifecycle: build_operation_graph lowers eagerly when no python engines are registered, so deselect_*/query methods work between classic steps via __getattr__ delegation to the lowered graph; build_plans(policy) passthrough; deserialize(*args, **kwargs) passthrough incl. enforce_precompiled; execute override_uids/shapes/strides + dlpack pointers; get_execution_plan_count = python engines + backend's dynamically-queried count (frontend NEVER statically enumerates backend engines — they vary by backend version; Router keeps ONE delegating cuDNN entry by design) - stride optional after set_dim (row-major inferred), None variant-pack keys tolerated, C++-tensor keys resolved via get_uid Validated: our suite (56) + classic spot-runs all green on real GPUs — matmul_bias_relu, rmsnorm, layernorm, batchnorm, conv_fprop (incl. execute_plan_at_index), apply_rope, kernel_cache, sdpa_with_caching, sdpa_thd, sdpa_chunked_prefill (ragged+paged), conv_genstats, conv_reduction, slice, block_scale_quantize_dynamic_shape, wgrads. Full-suite runs on SM100 + mhas in flight; residuals to follow. Known pre-existing env skew (fails identically on the unflipped installed package): test_deviceless_aot_compilation on this box. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): classic validate() timing + omit unset compute_data_type Two classic-parity fixes surfaced by the full mhas run (3567 uniform failures, one root cause): - cudnnGraphNotSupportedError must fire at graph.validate(): the classic test waiver pattern is try/except-skip AROUND validate(), with build_operation_graph() called bare. With no python engines registered, validate() now lowers and runs the C++ validate right there (unsupported configs skip, not fail); build_operation_graph()/plan creation are staged behind flags so each C++ step runs exactly once in classic sequencing. Python-engine graphs still never touch C++ at validate. - compute_data_type=None is now OMITTED at every lowering site (matmul / pointwise / structured / captured) instead of passed through: classic ops default to NOT_SET in C++; pybind rejects None. Also converts via _library_type when set (torch dtype parity). Previously-failing mhas case now skips as on classic; our suite 56 passing. Full-suite + full-mhas reruns in flight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(router): codify the extension contract for the future heuristics MR Ranking policy is intentionally undecided; what IS decided: policy pluggable at three levels (Router subclass / per-graph / process default); plan() may return any ordering or mix; backend engine sets are discovered per graph at plan time (never statically enumerated); PlanConfig can carry concrete backend engine configs, with pygraph._lower_cudnn_plan as the designated point to honor them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): plan-selection lifecycle + registration validation (review items 2, 6) Review item 2 (reproduced bugs): - ONE plan index space: [0, n_python) are python plans, [n_python, ...) are the backend's plans (sub-index = index - n_python, queried dynamically). get_execution_plan_count() and select_plan() now agree; selecting a backend sub-index lowers on demand, builds via build_plan_at_index and executes via _execute_plan_at_index (sub-index 0 == the classic default path). - select_plan() survives build()/execute(): build() no longer silently re-plans when a plan list exists (explicit create_execution_plans() still re-plans). Review item 6: - register_backend() validates at registration: engine_id must be a stable int in the reserved python region, unique per graph; registration after planning is rejected. BaseEngine.engine_id defaults to None so a subclass that forgets to declare identity fails clearly instead of silently colliding. - Decline signal narrowed: an engine declines ONLY via NotImplementedError or cudnn.cudnnGraphNotSupportedError (the classic unsupported-graph signal); ValueError/RuntimeError now propagate as engine bugs instead of silently falling back to cuDNN. Reference/cuTile engines updated accordingly. Regression tests for all of the above (pin-survives-execute, duplicate/missing id, post-planning registration, unexpected-exception propagation). 59 passing + classic spot files green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(python): compiled-plan engine lifecycle + ExecutionContext (review item 1) The engine contract now represents a real JIT/DSL backend: - propose_plans(graph) -> [PlanConfig]: one engine may expose several configurations to ranking/autotune (default: one plan with default_knobs when check_support accepts). PlanConfig moves to engines/base.py. - build_plan(graph, plan) -> CompiledPlan: the expensive JIT step, run ONCE per (graph, selected plan) at build_plans() time. The compiled artifact is cached ON THE GRAPH (keyed by plan index), so one engine instance is safely reusable across graphs and repeated execution reuses the artifact. The selected plan's knobs reach build_plan verbatim. - CompiledPlan.get_workspace_size(): plan-specific workspace; graph get_workspace_size() reports it for python plans. - ExecutionContext(handle, stream, workspace, override_uids/shapes/strides) passed to CompiledPlan.execute(): stream resolved from the caller's handle (classic cudnn.set_stream semantics); caller workspace object reaches the plan; no engine hard-codes a stream (cuTile now launches on ctx.stream). - Simple eager engines are unchanged in spirit: implement execute() only; the default build_plan wraps it in a trivial CompiledPlan. Acceptance tests per the review: two knob proposals from one engine with the selected plan's knobs observed at build+execute; compile-once artifact reuse across executions; same engine instance on two graphs without state collision; plan-specific nonzero workspace; caller workspace object identity at execute. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): IR port direction, tensor identity ownership, parity gaps (review items 3, 4, 5, 7) Item 3 — SDPA capture direction: - _CAPTURED_OPS entries declare out_kwargs (rng_dump, score_max, score_sum_exp, dBias, dSink_token): tensor kwargs that are semantically OUTPUTS are recorded in node.outputs (correct producer/consumer for engines) and still forwarded as descriptor args at lowering. fp8/fp8_backward positional schemas extended to the full binding order (descales/scales). Item 4 — tensor identity is graph-owned: - Tensor hash/eq are object identity (uid/name are mutable; value hashing broke the dict-key invariant). set_name/set_uid delegate to the owning graph (weakref set at registration) which re-indexes atomically: name index, uid index, auto-bound data follow; duplicate names and USER-user uid conflicts raise. Classic-parity subtlety the review didn't cover: classic tensors have no uid until set_uid while the IR assigns eagerly — a user set_uid landing on an auto-assigned uid silently renumbers the auto holder (auto uids are internal until lowering) instead of failing classic code. Item 5 — parity gaps: get_workspace_size(*args) classic overload passthrough; serialize() lowers on demand (cuDNN-format by definition, independent of the selected plan); stale references to the removed design doc dropped. Item 7 — freeze policy: structural mutation (new ops via the _get_name chokepoint, tensor rename/re-uid, backend registration) raises after lowering/planning instead of desynchronizing derived state. Classic gaps found by the SM100 full-suite sweep (fixed + re-validated): - slice: classic passes `slices` POSITIONALLY -> structured builders now map extra positionals onto attrs in declared order (covers conv paddings too); output dims inferred from the python slice objects; output dtype inherits the input's (dtype_like), matching the C++ rule. - moe_grouped_matmul: token_index/token_ks ports + top_k attr (gather/scatter). Environment skew documented (fails identically on the unflipped installed package; installed .so older than repo tests): test_mhas_v2 sdpa_mxfp8 (`implementation=` kwarg not in installed binding) and test_deviceless_aot_compilation (`enforce_precompiled`). 122 tests green locally (contract + classic spot files incl. set_uid-heavy kernel-cache/sdpa-caching). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): address coderabbit inline findings (broadcast checks, cuTile hardening, tensor_scalar parity) - matmul batch broadcast: incompatible extents raise (numpy rules) instead of silently taking max. - pointwise broadcast inference: right-aligned merge across ALL inputs; lower-rank operands no longer dropped; incompatible extents raise. - MatmulCuTileEngine: CUDA runtime return codes checked (failures decline the engine); execute verifies all operands share one CUDA device (multi-GPU hosts: mismatched context silently corrupts). - tensor_scalar: scalar_type is required (classic binding takes it positionally in every overload) — also closes the lowering path where an untyped pass-by-value scalar silently dropped its embedded value. Two other findings were already fixed before these comments were filed: default engine_id collision (registration validation, BaseEngine.engine_id = None) and mutable-uid Tensor hashing (identity hash/eq). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): review follow-up — replan invalidation, slot-based dispatch, context/freeze/validation completeness Follow-up item 1 (stale artifact on explicit replan): create_execution_plans() now invalidates every plan-derived artifact (compiled python plans, built state, the backend's plan list) — a stale compilation can never execute. Follow-up item 2 (mixed Router ordering): dispatch is slot-based, honoring the Router's ordering verbatim. _plan_slots() maps every public index to ("python", PlanConfig) or ("cudnn", sub_index) with the cuDNN entry expanding in place; selection, workspace, build and execute all use the same mapping. cuDNN-first and interleaved orderings now work as the router contract promises (prefix-count assumptions removed). Follow-up item 3 (context completeness): build_plan(graph, plan, ctx) receives a build context (handle + stream) — no private-state reads for AoT compilers. Stream resolution is strict: a supplied handle whose stream query fails RAISES (never a silent stream-0 fallback); with no handle, engines resolve deterministically from their framework (cuTile: torch current stream). Dynamic workspace-query overrides on python plans are rejected explicitly instead of silently ignored. Follow-up item 4 (MXFP8 schemas): match the bindings exactly — full positional orders (fwd: +descale_q/k/v; bwd: q_T/k_T/o_f16/dO_f16/dO_T + all descales), dSink_token as an output kwarg, named outputs (dQ,dK,dV,amax_*); rng_dump removed from fp8_backward (not on that binding). Follow-up item 5 (freeze completeness): ALL semantic Tensor setters (dim, stride, data_type, output/virtual, ragged, reordering, pass-by-value) are frozen after lowering/planning via the owner guard; tensor_scalar registers through _register_tensor (owner installed, identity mutations re-index). Follow-up item 6 (validation bypasses): constructor-provided backends go through register_backend() validation; propose_plans() results are checked for foreign engine-id injection; duplicate explicit tensor names are rejected at initial registration; CUDA runtime API failures in cuTile propagate as RuntimeError (an unsupported arch/driver remains a normal decline). Acceptance tests for each item (replan invalidation, interleaved-router dispatch, constructor/proposal validation, workspace-override rejection, strict stream failure, mxfp8 port direction, per-setter freeze, scalar ownership, duplicate names). 74 contract tests + classic spot files green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(python): one-shot planning (classic conformance) + retire NativeGraph name Planning is one-shot: a second create_execution_plans() raises. Empirically the classic C++ graph never supported re-planning (a second call there APPENDS plans by accident, build_operation_graph twice hard-errors, and mutation after build is silently stale) and no user re-plans. The replan-invalidation machinery added for review follow-up item 1 defended a capability that had no users — deleted; the same guarantee (a stale compiled artifact can never execute) now holds structurally because plan state is write-once. Autotune re-selects WITHIN one plan set via select_plan(), matching the classic build_plan_at_index flow. Plan differently => build a new graph (IR construction costs microseconds). Also retire the transitional NativeGraph name everywhere (tests, engine docstrings, type hints) — the class is cudnn.pygraph, full stop. A single documented alias line remains for downstream migration. 109 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): stable two-level plan indices; land the two missed patches (review round 3) Round-3 review items: 1. STABLE plan indices (the lazy-expansion contradiction): the flat in-place expansion of the cuDNN entry shifted python plans' indices when lowering happened (index 2 became a cuDNN sub-plan, python-B moved to 4) — pinning was unreliable. Adopted the two-level model the original review sanctioned: top level = the Router's entries verbatim (each python PlanConfig one index, the cuDNN delegating entry ONE stable index = the classic default path); backend sub-plans stay in the backend's own index space via the classic build_plan_at_index / execute_plan_at_index / *_plan_at_index APIs (delegated). Indices never shift; the expansion machinery is deleted. get_execution_plan_count keeps the exact classic semantic when no python engines are registered. 2. C++ replan-appends: moot since planning became one-shot (83ffdedcf) — the C++ create_execution_plans can no longer be reached twice on one graph (enqueue_engine_configs appending was exactly why replan had to go). 3. Landed for real (previous patches missed their anchor strings and failed silently — now grep-verified): cuTile resolves torch's current stream when no handle stream exists (literal stream 0 gone); rng_dump removed from the fp8_backward schema (not on that binding). Also: execute()-supplied handle now reaches the JIT build on auto-build (the python path plans first and compiles with the caller's ExecutionContext instead of running the generic build with only the graph handle). 4. Custom-Router bypass closed: create_execution_plans() validates the FINAL router output — python entries must name registered engines, only one cuDNN delegating entry allowed, anything else raises. 5. get_dim()/get_stride() return copies (the classic pybind getters return fresh lists; live-list mutation after planning is no longer possible). 111 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: callback graph shim for score_mod closures; serialize returns classic form Two classic-parity fixes found by running the full suite on a current extension build: - flexible SDPA score_mod callbacks: user closures capture IR Tensors but the callback receives the lowered C++ graph. _CallbackGraphShim translates IR Tensor arguments at the call site (lowering closure-captured helper tensors on demand), so existing callback code runs unchanged. - serialize(): return the C++ binding's serialized form unchanged instead of wrapping in bytes. C++ deserialize casts the payload back to vector<uint8_t> and rejects bytes, so the bytes wrapper broke the classic serialize -> deserialize(handle, data, enforce_precompiled=True) round trip (test_deviceless_aot_compilation::test_device_properties). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): review round 4 — explicit planning state, split plan-index spaces - get_execution_plan_count() is ALWAYS the classic backend-count passthrough (lowering the cuDNN entry on demand); it never returns the routed-list length, so its semantics no longer depend on whether python engines are registered. The routed plan list is graph.plans / select_plan() — a separate, stable index space. An unplanned graph counts 0 (classic), and a python-only routed graph raises with a pointer to graph.plans. - Explicit _planning_done flag replaces the nonempty-list proxy everywhere (one-shot check, register_backend, set_router, freeze, build/execute needs-planning checks); an empty Router output is rejected — there is no legal empty planning state. set_router after planning raises. - cuTile resolves the fallback stream on the OPERANDS' device (current_stream(a.device)), after the same-device check — argless current_stream() is the active device's stream, which can be a different GPU on multi-GPU hosts. - router.py contract downgraded to what this MR enforces: at most one cuDNN delegating sentinel; concrete cuDNN engine configs as routed entries are the heuristics follow-up's typed-plan work, not one extra lowering branch. - tensor(uid=) creation path now applies the same collision rule as set_uid: a user uid landing on an auto-assigned uid steals it (holder renumbered); only user-user collisions raise. Found by the SM100 block_scale_quantize dynamic-shape tests, which assign explicit uids after ops already auto-assigned. Tests: cuDNN slot of a mixed router actually executes through the backend with routed indices stable across lowering (GPU); one-shot planning on a pure-cuDNN graph (GPU); empty router rejected; set_router frozen after planning; backend-count/routed-space separation; creation-path uid steal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): push ragged_offset_multiplier on output tensors at lowering The three output-mapping sites pushed set_ragged_offset but not the multiplier, so a non-default multiplier on an output (unified SDPA ragged layouts, paged fp8 fwd) lowered as multiplier=1 — the backend computed wrong addresses (cudaErrorMisalignedAddress, hard process abort). The input path already passed it via _make_tensor kwargs. Found by full test_mhas_v2 -m '' on H100/dev-9.26: 21x test_sdpa_random_fwd_ragged_offset_multiplier_unified_L1 + 1x test_sdpa_fp8_fwd_paged_L0 crashed on the flip and passed on the classic-control package (same .so, develop python files). After the fix the same selection is 145 passed, matching classic exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): push reordering_type on output tensors; consolidate output-attr lowering Same bug class as the ragged multiplier: an attribute set on an OP OUTPUT via the classic setter chain (block_scale.set_reordering_type(F8_128x4) in test_block_scale_quantize) was never pushed at the output-mapping sites, so the backend rejected the quantize scale layout on SM100. The three duplicated output blocks are consolidated into one push_output_attrs helper (ragged offset + multiplier, reordering, output flag, dtype) so the next output-settable attribute has exactly one place to go. Attribution: 7 test_block_scale_quantize failures on Blackwell were flip-attributable (classic control passes); fixed. The 3 test_cudnn_sdpa_op d=256 failures fail identically on the classic control (environment). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): whole-surface freeze + output layout contract; split cuTile engine out Review round 5: - Freeze covers the ENTIRE public surface, not just the fluent API. An explicit _frozen flag is set at lowering and at planning (whichever first); _freeze() seals node port/param dicts to MappingProxy views, dim/stride lists to tuples, and Tensor/Node/GraphContext gain __setattr__ guards. graph.nodes / graph.tensors return copies. A mutation while merely validated (python-engine graphs stay mutable until planning) invalidates _is_validated so stale inference never reaches planning. - Output layout contract: Tensor tracks user-assigned vs IR-inferred dim/stride; push_output_attrs pushes USER-assigned layouts verbatim (previously lost on matmul/pointwise outputs) and never pushes inferred row-major strides — the backend keeps its classic per-op inference (channels-last conv). Tests: explicit column-major matmul output stride honored end to end; conv output stays channels-last in the lowered JSON. - cuTile matmul engine split out of this PR (engine file, optional extra, tests, exports) — it re-lands with the DSL-engine integration PR; ReferenceMatmulEngine remains the in-tree contract oracle. This PR is the contract, not a kernel product. - MoE lowering test gated on cuDNN 9.15+. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(python): retire pygraph name collision; drop NativeGraph; check in design doc Review feedback: - The C++ pybind graph class is renamed pygraph -> backend_graph (cudnn._compiled_module.backend_graph): two things named pygraph was confusing now that cudnn.pygraph IS the Python class. Internal-only rename — nothing public imported the pybind name post-flip. - The Python module moves to cudnn/_pygraph.py (private module, public class re-export), so the class qualname is cudnn._pygraph.pygraph, not the double-take cudnn.pygraph.pygraph. - NativeGraph transitional alias dropped completely. - Design doc checked in: docs/python_graph_and_execution_backends.md — architecture, two plan-index spaces, engine contract, invariants (uid ownership, one-shot planning, freeze, output layout), naming, and follow-up scope. - test_native_cudnn_lowering: every cuDNN-path execute now asserts dispatch-level proof it ran through the backend plan path (_assert_ran_on_cudnn: cuDNN entry selected, graph lowered, backend plans created/built). Kernel identity below the backend API is deliberately not asserted — kernel names are backend-internal and version-dependent; numerics + dispatch proof is the stable contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(python): 'cudnn' never means 'the backend' in names — both sides are cuDNN The frontend Python graph is as much cuDNN as the C++ library; identifiers that used 'cudnn' to designate the backend side now say 'backend': - CUDNN_HEURISTIC_ENGINE_ID -> BACKEND_HEURISTIC_ENGINE_ID - _lower_cudnn_plan / _has_cudnn_plan / _cudnn_heuristics -> _lower_backend_plan / _has_backend_plan / _backend_heuristics - _assert_ran_on_cudnn -> _assert_ran_on_backend - test_native_cudnn_lowering.py -> test_native_backend_lowering.py (tests *_lowers_to_cudnn -> *_lowers_to_backend, mixed-router / one-shot test names likewise) - docstrings/comments: 'cuDNN entry/sentinel/slot/path/side' -> 'backend ...' throughout; 'the cuDNN C++ backend' stays where it describes what the backend is. Also fixes a stale TYPE_CHECKING import left by the module rename. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): classic-parity batch from internal CI — signatures, wrapper, labels, naming, layout truth Root-caused from the internal CI failures (py_samples / pycudnnTest); every item below reproduces 1:1 against the classic package on the same GPU/backend and is fixed + validated (pycudnnTest 26/26, all 13 CI sample notebooks pass, local battery 2172/0): - Constructor and tensor() are POSITIONALLY IDENTICAL to the classic API (name is the constructor's first positional arg — pycudnnTest passes it positionally; classic sm_count/sm_version/kernel_cache/device_property/ dynamic-shape params explicit; classic tensor() order with is_pass_by_value/ ragged_offset/reordering before name/uid; NOT_SET/-1/NONE sentinels normalized). New params (backends/router) are keyword-only. Guarded by test_api_signature_parity, which reads the classic order from the pybind docstring/wrapper itself. - wrapper.py (cudnn.Graph) recognizes IR tensors: one _GRAPH_TENSOR_TYPES tuple replaces 7 isinstance(cudnn.tensor) sites (the notebooks' silent UnboundLocalError/mis-capture). - Duplicate tensor names are legal classic LABELS (pycudnnTest builds two 'weight's): uid is identity; the name index serves unique names only and ambiguous-name lookups raise instead of guessing. - Op outputs are auto-named with the classic C++ conventions (node::MEAN/INV_VARIANCE/DSCALE..., per-op overrides for rmsnorm_backward's ::Dscale/::Dbias) — wrapper.Graph canonical-name lookups depend on them. - Multi-output ops return a LIST like classic pybind (pycudnnTest dispatches on isinstance(res, list)). - Layout truth: backend-inferred dim/stride are reflected back into the IR after build_operation_graph (_sync_ir_shapes_from_backend) — wrapper allocates output buffers from IR getters; provisional row-major strides are no longer observable post-build. push_output_dims ops push stride only when USER-assigned (pushing inferred row-major into an NHWC graph made the backend reject dgrad+add fusion). - tensor_like normalizes non-torch DLPack objects (CuPy .strides is in BYTES) through torch.from_dlpack — NHWC CuPy inputs no longer silently become row-major. - get_data_type() returns the cudnn enum when the user stored a torch dtype (classic converts at set time). - validate() no longer auto-marks leaf outputs as non-virtual — discarding a result (training SDPA's Stats in the paged sample) is legal classic usage; auto-marking made its uid required in the variant pack. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: remove internal test file accidentally included Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): renames are label writes (exempt from freeze); push output names at lowering Two classic-parity items from the internal CI notebook set: - set_name after build is legal classic usage (sample 24 renames a tensor on an already-built graph): names are labels with no execution semantics, so _rename_tensor no longer consults the freeze — the label write bypasses the sealed-tensor guard explicitly, and the ambiguity policy still governs the name index. - User renames on op OUTPUTS now reach the lowered graph: push_output_attrs pushes the IR name, matching classic where the rename acts on the same object the cpp graph holds (visible in JSON dumps and wrapper.Graph canonical-name lookups). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: skip introspection/one-shot tests when cudnn.pygraph is monkey-patched The internal tree layers a DSL engine by monkey-patching cudnn.pygraph lifecycle methods process-wide at import (cudnn.TBD). Under pytest-xdist any worker that collects those tests carries the patches into unrelated tests: signature introspection then sees the wrapper's (*args, **kwargs) and the patched create_execution_plans swallows the one-shot error (except Exception) — false negatives against pristine-class contracts. Detect the replacement via __qualname__ and skip LOUDLY with the reason, instead of failing on behavior that is not this class's. The proper fix remains scoping the internal patches (fixture install/uninstall) or excluding the TBD shard from the shared py_test run; these guards just make the contamination visible as skips rather than red. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Yang Xu <yanxu@nvidia.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add NWH + B2B causal conv1d notebooks; refresh outputs (#246) * Add NWH + B2B causal conv1d notebooks; refresh outputs - Add 62_causal_conv1d_nwh_forward.ipynb - Add 63_causal_conv1d_nwh_backward.ipynb - Add 64_b2b_causal_conv1d_forward.ipynb - Add 65_b2b_causal_conv1d_backward.ipynb - Refresh outputs for all 6 notebooks (60-65) * Guard NWH and B2B causal conv1d APIs for cuDNN 9.24 * Format causal conv1d Python op * Match CI Black line length for causal conv1d op * Add runtime guard for causal conv1d 9.24 symbols * Address CodeRabbit B2B causal conv1d feedback * Document causal conv1d notebook version requirements * Allow zero grad for discarded B2B output --------- Co-authored-by: Hwanseo Choi <hwanseoc@nvidia.com> * Bump development version to 1.27.0 (#358) * Fix FE-OSS docs links and DSA architecture code fence. (#360) Use stable SDPA documentation URLs in overview and mark the DSA architecture block as text to avoid code highlighter parsing issues. * Fix cutlass DSL deprecation: use .ptr for cute.struct scalar fields (#365) cutlass-dsl 4.5+ deprecates using a @cute.struct scalar field directly as a pointer (_ScalarData.value), emitting: DeprecationWarning: Use explicit `struct.scalar.ptr` for pointer instead. from cute/core.py whenever tmem_holding_buf / tmem_dealloc_mbar_ptr are passed to cute.arch.alloc_tmem / retrieve_tmem_ptr / utils.TmemAllocator. Switch the remaining call sites to the explicit .ptr accessor, matching the pattern already used by the other DSA kernels. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * Fix DSA offset alignment, stream handling, and CUDA Graph capture (#354) * Fix SM90 query offset alignment Signed-off-by: kunlunl <kunlunl@nvidia.com> * Preserve the CUDA default stream Signed-off-by: kunlunl <kunlunl@nvidia.com> * Make dense indexer backward graph safe Signed-off-by: kunlunl <kunlunl@nvidia.com> * Require a tensor grad_loss for indexer backward Signed-off-by: kunlunl <kunlunl@nvidia.com> * Address remaining DSA review comments Signed-off-by: kunlunl <kunlunl@nvidia.com> --------- Signed-off-by: kunlunl <kunlunl@nvidia.com> * Expose cu_seq_len_q/kv on the sdpa_fp8 python binding (#366) * Expose cu_seq_len_q/kv on the sdpa_fp8 python binding The unified-engine FP8/MXFP8 forward (cuDNN 9.25+) accepts cumulative sequence lengths, and the C++ API has supported them on the fp8 node since 1.25 (SDPA_fp8_attributes aliases SDPA_attributes), but the python sdpa_fp8 binding hardcoded cu_seq_len_q/kv to nullptr. Expose them as kwargs (appended last to preserve positional backward compatibility) so python callers can use fp8 + cu_seq_len; the python-native pygraph capture/replay layer forwards them without changes. - python/pygraph/{pygraph.h,sdpa.cpp}: add cu_seq_len_q/kv parameters to PyGraph::sdpa_fp8 and its m.def, with docstring entries (requires cuDNN 9.25+ and the UNIFIED implementation). Remove a stale "Deprecated, use sdpa_unified instead" comment: implementation selection is automatic (or explicit via the implementation attribute), and python fp8 users are expected to call sdpa_fp8. - docs/operations/Attention.md: document cu_seq_len_q/kv on the fp16/bf16 C++ and python APIs (missed in #266), the ragged offset multiplier (missed in #290), and the fp8 varlen surface incl. the new kwargs. - test/python/sdpa/fp8.py: support is_cu_seq_len and with_ragged_offset_multiplier configs (mirroring fp16.py): cu_seq_len graph tensors, token-coarse offsets with per-tensor multipliers on Q/K/V/O, version gating at 9.25. - test/python/test_mhas_v2.py: test_sdpa_fp8_fwd_ragged_L0 now draws ragged / cu_ragged / cu_ragged_mult with equal weight. sdpa_mxfp8 is intentionally untouched: it has no varlen surface at all (no padding mask or seq_len kwargs), so cu_seq_len support there is a separate feature. Validated against cuDNN 9.25 (test_sdpa_fp8_fwd_ragged_L0): H100 10 passed / 22 skipped (pre-existing Hopper config limits), Blackwell 24 passed / 8 skipped (head-dim limits); the passing draws include 23 is_cu_seq_len=True and 9 multiplier configs, zero failures. * Complete cu_seq_len docstring constraints on sdpa_fp8 Address review: the runtime-visible docstring now carries the same constraints as the sdpa() docstring and Attention.md — set together, use_padding_mask=True, cuDNN 9.25+ and the UNIFIED implementation. * Serialize selected plan behavior notes (#364) * Serialize selected plan behavior notes * Add behavior note serialization regression sample * Add pip install --group dev, prerequisite for deprecating requirements.txt (#359) * Add dev dependency group * Reorder pyproject sections * Fix uncaught ValueError in flatten_pass_by_value on malformed hex input (#343) The hex branch of flatten_pass_by_value converted "0x"-prefixed strings without error handling, so malformed values such as "0x" or "0xZZ" in a log's pass_by_value field crashed the cudnn_repro CLI with an unhandled ValueError. Guard the conversion with the same try/except pattern the decimal branch already uses, returning an empty list for unparseable strings, and add regression tests. Fixes https://github.com/NVIDIA/cudnn-frontend/issues/342 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * Make plan structure serialization optional within serialize() to construct symmetry with deserialize logic (#371) * Make plan structure serialization optional within serialize() to construct symmetry with deserialize logic * add CUDNN_FRONTEND_UNUSED for guarded out macro case * Update SDPA Benchmarking Artifacts - 9.24.0.43 (#362) * Organize FE OSS tests by feature (#372) Organize FE OSS tests into flat feature directories and update imports and documentation paths. * GEMM+RoPE+MXFP8 fusion (#367) * Add fused gemm+rope+mxfp8quant kernel. * Add documentation and general interface names * Address coderabbitai's suggestions * Additional tests for fused gemm+rope+mxfp8 * Remove NUM_HEADS constant * Test: organize GEMM projection tests (#374) * Fix cutlass DSL deprecation warnings in CuTe DSL kernels (#376) Fixes all cutlass-dsl 4.5.x deprecation and optimization warnings emitted by the CuTe DSL kernels during the OSS test suite: - tcgen05.OperandMajorMode -> cute.nvgpu.OperandMajorMode (also silences the <string>:11 warnings raised inside the MMA op ctor when the deprecated enum type is passed through). - make_trivial_tiled_mma / make_blockscaled_trivial_tiled_mma legacy single-ab_dtype overload -> new overload with separate a_dtype and b_dtype (dtype duplicated, matching the legacy path exactly). - cutlass.utils.distributed.atomicAdd -> local dsl_user_op wrapper over cute.arch.atomic_add with identical relaxed/sys semantics. - Static loops with >=64 iterations flagged by DSLOptimizationWarning: cutlass.range_constexpr -> cutlass.range(..., unroll_full=True) where the loop body only needs dynamic tensor indexing. No functional changes; codegen is equivalent. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * Fix BSA backward hang on cute-dsl 4.6.0: version-gate elect_one around bulk stats copies (#382) cute-dsl 4.6.0 changed cute.copy lowering for bulk-async atoms (cpasync.CopyBulkG2SOp, TMA): the copy now elects a single lane internally via a warp-collective WARPSYNC.COLLECTIVE + ELECT. bsa_bwd_sm100's load warp wrapped its LSE/dPsum stats copies (cute.copy with CopyBulkG2SOp) in cute.arch.elect_one(), as required on <= 4.5.x where the bulk copy did not self-elect. On 4.6.0 the two elects nest: lane 0, alone inside the outer elect region, reaches the copy's internal warp-collective elect which waits for all 32 lanes and deadlocks the warp. Q/LSE/dO/dPsum stop flowing and every other warp spins in mbarrier waits; in CI the oss:rel [Blackwell] job pegged the GPU at 100% until the 1h job timeout (https://gitlab-master.nvidia.com/cudnn/cudnn_frontend/-/jobs/360031309). Diagnosed by cuda-gdb break-in on the live hang (2 TMA-load warps parked at WARPSYNC.COLLECTIVE/ELECT inside the stats copy; 26 warps spinning in SYNCS.PHASECHK downstream) and by PTX A/B diff showing stacked double elect.sync at the stats-copy sites on 4.6.0 vs a single one on 4.5.0. Fix: introduce copy_utils.bulk_copy_elect_one(), which returns cute.arch.elect_one() on cute-dsl <= 4.5.x and a nullcontext on >= 4.6.0, and use it at the four copy_stats sites. All other elect_one uses (mbarrier init/arrive, consumer_release, tcgen05 commits, cp.reduce.async.bulk inline asm) still require the guard and are unchanged. Verified on Blackwell (SM 10.0): - cutlass-dsl 4.6.0: test/python/fe_api/block_sparse_attention 17 passed in 31.7s (previously 3 device-side hangs) - cutlass-dsl 4.5.0: unchanged behavior via the version gate Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * Fix architecture-independent SDPA repro failures (#386) * Restore SDPA repro tensor dumps * Skip FP8 reference checks in perf mode * Fix large MXFP8 performance repros * Address SDPA repro review comments * Keep MXFP8 storage access direct * Use UID map for tensor dump collection * Avoid monkeypatching MXFP8 performance test * Use vector for tensor dump collection * Remove MXFP8 performance smoke test * Add SDPA edge case test coverage (#328) * Add SDPA edge case tests * Refine SDPA edge case coverage * Add cu_seqlen zero-length edge tests * Guard cu_seqlen tests by cuDNN version * Run zero seqlen tests from cuDNN 9.25 * Improve GitHub issue and pull request templates (#375) * Infra: improve GitHub issue and PR templates * Infra: make issue forms less restrictive * Infra: simplify bug environment fields * Infra: simplify feature request form * Infra: expand bug environment prompt * Infra: combine CUDA environment versions * Infra: clarify optional bug environment * Infra: remove redundant GPU environment field * Infra: add cuDNN version examples * Infra: consolidate bug and PR templates * Infra: limit pre-commit reminder to staged files * Infra: format PR area choices vertically * Infra: simplify CodeRabbit auto-review config * Support SM90 DSA qh16 indexer forward and fix qh32 sparse backward (#388) * Support SM90 DSA qh16 and fix sparse backward Addresses NVIDIA/cudnn-frontend#373 and NVIDIA/cudnn-frontend#385. * docs: correct DSA SM90 support overview --------- Co-authored-by: mingyangw <mingyangw@nvidia.com> * Remove dead BSA fragment allocations (#392) * Add collect_env environment report tool for bug reports (#400) Issue reporters often can't state their environment precisely, and the most common unreproducible-issue root cause is version confusion: multiple cuDNN/CUDA copies installed where the loaded one is not the one the user assumes. python -m cudnn.collect_env produces an offline, read-only report: frontend/backend versions with mismatch flags (stale pip metadata, torch's libcudnn vs the frontend's dlopen'ed backend), the frontend's libcudnn search-order resolution, GPUs in CUDA enumeration order, loaded-vs-on-disk GPU libraries via /proc/self/maps with pip provenance, relevant packages incl. torch's declared cuDNN pin, and CUDNN_*/CUDA_* env vars. Stdlib-only at module level with every probe individually guarded, so the file also runs standalone with bare Python when import cudnn is broken. Referenced from the bug-report issue template and README. Co-authored-by: Yang Xu <yanxu@nvidia.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * Update nvidia-cutlass-dsl version to 4.6.0 (#368) * Update nvidia-cutlass-dsl version to 4.6.0 * Migrate warp redux to public cute.arch.warp_redux_sync for cutlass-dsl 4.6.0 nvidia-cutlass-dsl 4.6.0 renamed the nvvm dialect enum ReduxKind to ReductionKind, breaking every kernel that imported it and failing 284 Blackwell OSS tests at import time. Instead of chasing the private-API rename, drop the three repo-local redux helpers (moe_kernel_helpers.warp_redux_sync, discrete_kernel_utils.warp_redux_sync, utils.warp_redux_sync_fmax, and rmsnorm's redux_sync_max_f32) and call the public cute.arch.warp_redux_sync(value, kind="fmax", ...) wrapper everywhere, matching the pattern already used by gemm_srelu/gemm_dsrelu and the DSA kernels. Also replace the raw nvvm.redux_sync bitcast sequence in gemm_amax, whose res= kwarg was likewise removed in 4.6.0. The old local helpers hardcoded redux.sync.max.abs.NaN.f32 regardless of argumen…
Release 1.27.0 (#503) * test/python: cap peak GPU memory via PYTORCH_CUDA_ALLOC_CONF (#247) Long pytest-xdist runs (e.g. test_mhas_v2 ~2.5k SDPA configs in one worker) hit a much higher GPU memory high-water mark than any single test needs, because the caching allocator retains freed blocks across configs. Setting PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True, garbage_collection_threshold:0.6 before torch is imported reduces the peak to roughly the maximum any single test needs, with no change in wall time or test outcome. Use os.environ.setdefault so user-provided values still win, and place it above the transformer_engine import so the env var is visible by the time torch initializes its CUDA allocator. * Fix DSA link in README.md Updated the link for DSA in the README to point to the correct directory. * Remove stale H200 benchmark artifacts (#252) These artifacts were superseded by the newer SDPA benchmark result layout and were already removed from the internal GitLab develop branch. * Change profile_pass from 'fwd' to 'both' * Bump the develop to 1.25.0 * Fix varpack-template lifecycle bugs + add defensive checks Two pre-existing bugs in the VariantPackTemplate, plus one defensive guard: 1. Graph copy -> dangling host pointers. template_ptrs stores raw addresses into cached_pass_by_value storage owned by the source Graph. Default copy propagated prepared=true while the addresses still pointed at the source. Fix: VarpackPrepStateBox copy ctor/assign now always start with prepared=false so the copy re-preps on first use against its own storage. 2. Re-deserialize on the same Graph -> stale template. deserialize(handle,...) rebinds cached_pass_by_value but the existing prepared=true causes the eager prep to short-circuit, leaving the slot layout from the prior deserialize. Fix: reset prepared=false and clear varpack_template before the eager prep call. 3. Null device_ptrs in raw-ptr create_variant_pack overloads. Reject nullptr + non-empty uids instead of forwarding to the cuDNN backend. Adds explicit null-plan guards across detail::execute overloads, returning GRAPH_EXECUTION_FAILED with "No plan found to execute!" instead of dereferencing plan via plan->getTag(). Ports https://gitlab-master.nvidia.com/cudnn/cudnn_frontend/-/merge_requests/2117 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Clear deserialize-owned containers on re-deserialize Addresses review feedback on PR #248: the prior fix reset prepared=false and varpack_template but left deserialized_tensor_properties, deserialized_pass_by_value, deserialized_workspace_modifications, and tensors_to_dump populated from any earlier deserialize(handle, old_data). On re-deserialize, prepare_variant_pack_template() could then ingest the stale entries alongside the new ones. Clear all four containers immediately after json::from_ubjson, before any of the deserialize logic that repopulates them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add row-scale support to grouped GEMM quant Signed-off-by: Ziang Li <ziangli@umich.edu> * Tighten row-scale grouped GEMM quant tests Signed-off-by: Ziang Li <ziangli@umich.edu> * feat(python): add get_engine_and_knobs_at_index for structured plan pinning (#259) * feat(python): add get_engine_and_knobs_at_index for structured plan pinning get_plan_name_at_index returns a formatted "engN_kT=V" tag built from the engine global index and knob choices. Callers that want to persist a tuned plan and replay it later are forced to either store the bare plan index (which drifts when the policy=ALL plan list is re-enumerated across cudnn-frontend / backend versions) or parse the tag string. Expose the structured data directly: get_engine_and_knobs_at_index returns (engine_id, {KnobType_t: value}), reading the same backend attributes get_engine_tag stringifies. The result feeds straight into create_execution_plan(engine_id, knobs) to rebuild the exact same kernel on a fresh graph without a heuristics query. - detail::get_engine_id_and_knobs (cudnn_frontend_utils.h): structured reader - Execution_plan_list::get_engine_and_knobs_at_index (plans.h) - Graph::get_engine_and_knobs_at_index (graph_interface.h) - PyGraph binding (pygraph.h/.cpp) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * address review: bounds-check index, add cpp unit test, trim comments - get_engine_and_knobs_at_index: reject out-of-range index (mirrors check_support_at_index) instead of indexing engine_configs OOB. - add test/cpp/get_engine_and_knobs.cpp: enumerate a matmul graph's plans, read (engine_id, knobs) for each, and confirm re-pinning via create_execution_plan reproduces the same plan (matching name); also checks out-of-range indices error. - trim the new doc comments to match neighboring style. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * knobs: add SWAP_AB / INPUT_TMA_ENABLE / OUTPUT_TMA_ENABLE to KnobType_t KnobType_t (and the to/from backend converters) stopped at WARP_SPEC_CFG (42), so engines using SWAP_AB (43, cuDNN 9.18), INPUT_TMA_ENABLE (44) or OUTPUT_TMA_ENABLE (45, cuDNN 9.22) had those knobs mapped to NOT_SET by convert_from_backend_knob_type. Feeding NOT_SET back into create_execution_plan then failed convert_to_backend_knob_type with INVALID_VALUE -- so a plan enumerated with one of these knobs (e.g. via get_engine_and_knobs_at_index) could not be pinned. Add the three knob types to the enum, both converters (version-gated to match the backend @since), and the pybind knob_type enum. The cpp test now compares the structured identity (engine id + knob map) instead of the plan-name tag, since the tag serializes knobs in engine-config order, which differs between the heuristic config and the pinned one even though the kernel is identical. create_execution_plan is now asserted to succeed for every enumerated plan; building it stays best-effort (can fail for unrelated environment reasons such as a ptxas older than the engine's target). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * make get_engine_tag deterministic: sort knob choices by type The plan-name tag was built by iterating CUDNN_ATTR_ENGINECFG_KNOB_CHOICES in stored order, which differs between the heuristics path and create_execution_plan (set_knob_choices iterates a std::unordered_map). So the same engine + knob values could serialize to differently-ordered tags (e.g. eng11_k2=29_k27=0...k43=0 vs eng11_k43=0_k38=0...k2=29) -- the kernel is identical but the string isn't a stable id. Sort the knob choices by type before formatting so the tag is a deterministic function of the engine config regardless of how it was built. This is off the execution hot path (tag is used for logging / plan identity), so no perf impact; the actual knob choices passed to the backend are unchanged. The cpp test now also asserts the pinned plan's tag matches the original's. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Yang Xu <yanxu@nvidia.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Update SDPA Benchmarking Artifacts (#265) * update sdpa benchmark artifacts * update acknowledgement * Adding coderabbit review guide (initial template) * fix: allow overriding libcudart selection via CUDNN_FRONTEND_CUDART_LIB_NAME When dynamic loading is enabled, load_cudart_so() searches for the supported libcudart major versions and aborts with "Multiple libcudart libraries found" when more than one is visible on the library search path. This happens in containerized environments such as GKE, where the TCPXO NCCL plugin mounts a different libcudart major version from the host than the one shipped in the container. Check the CUDNN_FRONTEND_CUDART_LIB_NAME environment variable first; when set to a library name or path, dlopen exactly that library and skip the automatic multi-version detection. Behavior is unchanged when the variable is unset. Fixes #267 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Clean up guardword-flagged comments (xmma path, gitlab URL, P4 label, Perfsim, HACK/Ugly, STS/CGA SASS terms) (#273) Comment-only cleanups, no behaviour change. Replaces guardword-flagged phrasing with neutral equivalents in 7 files: - attention_utils.h:67 — drop internal `xmma/fast_math.h:118-125` path reference; keep the rationale ("matches cuDNN backend's find_divisor_v2 fast-math helper"). - test_sdpa_bwd.py:8 — drop `gitlab-master.nvidia.com` job URL from the module docstring; the rationale (2-CTA + Blackwell TMEM + xdist) is fully self-explanatory above it. - dense_score_recompute_sm90.py — "Perfsim" → "Profiling"; "Weights/LSE LDG" → "Weights/LSE load-from-global" (x2). - indexer_backward_sm90.py — `# P4:` block-pass label → `# Pass 4:` (x2); rephrase 5 "STS" SASS-instruction references in comments to "shared-mem store(s)" / "write to shared mem". - indexer_backward_sm100.py — same STS → shared-mem-store rephrasing in 1 docstring. - dsa_bwd_sm90.py:386 — `# HACK:` → `# Note:` (same meaning). - dsa_bwd_sm90.py:1554 — `STS(dS)` → "storing dS to shared mem". - dsa_bwd_sm100.py:941 — `# Ugly,` → `# Awkward,`. - dense_gemm_persistent_swiglu.py:1049 — "single CGA" → "single cluster". Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * remove_9.99_version_tag * add_protection_flags * fix(windows): consolidate getenv access and fix C4996/C4005 on MSVC The Windows wheel build (deploy:build_bdist_wheels_3.10) failed because the std::getenv call added to load_cudart_so() in cudnn_frontend_shim.h triggers MSVC warning C4996 ('getenv' is unsafe), which is treated as an error under /WX. Root cause and fixes: - Move get_environment() to cudnn_frontend_shim.h (the lowest-level header, included by utils.h before Logging.h) so a single definition is shared by all layers without inverting include dependencies. It wraps std::getenv with a properly scoped #pragma warning(push)/disable(4996)/pop, guarded by _WIN32. - Route all getenv call sites through get_environment(): shim.h, graph_properties.h, scaled_dot_product_flash_attention.h, and sm100_rms_norm_silu_engine.h. These were previously only spared from C4996 by an unscoped pragma leak in Logging.h, and would have started failing once that leak was fixed. - Remove the duplicate get_environment() from cudnn_frontend_Logging.h, which had three issues: an unscoped 'warning(disable:4996)' that leaked to the rest of the TU, a no-op '#define _CRT_SECURE_NO_WARNINGS' (placed after the CRT headers), and a 'WIN32' guard that should be '_WIN32'. Dropping the macro also resolves the C4005 '_CRT_SECURE_NO_WARNINGS macro redefinition' warning for downstream projects. Fixes NVIDIA/cudnn-frontend#139 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(shim): warn instead of throwing when multiple libcudart libraries are found Loading cudart no longer aborts when both libcudart.so.12 and libcudart.so.13 are present in the library search path. Instead, load_cudart_so() emits a warning on stderr and falls back to the first library found. Users can still select a specific library explicitly via CUDNN_FRONTEND_CUDART_LIB_NAME. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Unblock SDPA tests and promote FP8 ragged backward to L0 (#275) * Promote L1 Python tests to L0 * Restore L1 markers except FP8 ragged backward * Add per-expert reduction (group_offset) for MoE grouped GEMM Adds optional group_offset support to the reduction node so cuDNN FE can express per-expert reductions for MoE grouped GEMM workloads. - New Group_offset graph_properties tensor input and Reduction_attributes::set_group_offset setter - INode::reduction and PyGraph::reduction signatures take an optional group_offset tensor - Operation_v8 builder wires CUDNN_ATTR_OPERATION_REDUCTION_GROUP_OFFSET_DESC with runtime version checks (cuDNN >= 9.24.0) - Python binding (pygraph) exposes the optional group_offset argument Mirrors gitlab-master cudnn/cudnn_frontend MR !2111 by @yanqinz. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix the 9.99 bound * Skip flexible-graph SDPA bwd sample on SM120 and above (#284) The fp16 backward-with-flexible-graphs sample guards against SM 120 (consumer Blackwell) where this path is not supported. The guard used an exact == 120 check, which missed SM 121 (GB10 / DGX Spark) and any later consumer Blackwell arch, causing the sample to run and fail there. Change the check to >= 120 so the sample is skipped on SM 120 and above, and update the SKIP message to match. Co-authored-by: Yang Xu <yanxu@nvidia.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 1 * Add pre-commit hooks (#286) * Fix clang format issues * Fix clang-format * Add pre-commit hooks and fix pre-commit * Fix the black issues * Skip TensorIR MemBound / compile-time-const samples on consumer Blackwell (SM12x) (#285) * Skip TensorIR MemBound / compile-time-const samples on consumer Blackwell (SM12x) The TensorIR MemBound engine (cudnnTensorIrMemBoundEngine) only supports SM100-SM109 (data center Blackwell): its arch gate is [SM_100, SM_110) and the DKG cubins it emits are the sm_100f family-portable target, which the CUDA driver will not load on sm_120. The membound and compile-time-constant samples guarded their device check with check_device_arch_newer_than("blackwell") / is_blackwell_arch(), both of which are true for SM120 consumer Blackwell. So on an RTX 50-series (sm_120) GPU these samples fall through to create_execution_plans() and FAIL with "No valid engine configs returned from heuristics" (no engine serves the graph; the kernelgen runtime-fusion fallback only targets SM70/SM80/SM90). Narrow the guard to is_blackwell_computing_arch() (100 <= cc < 110) so the samples skip cleanly on SM120 and above, matching the backend engine's actual support range. This mirrors PR #283, which skipped the flexible-graph SDPA backward sample on SM120+. Affected test cases (verified on RTX 5080 / sm_120, cuDNN 9.30 -> now SKIP): membound/transpose.cpp "Membound transpose permutes dims" membound/reshape.cpp "Membound reshape ... LOGICAL mode" membound/slice.cpp "Membound slice window with step" membound/concat.cpp "Membound concatenate on channel axis" membound/membound_fusion.cpp "Fusion reshape then ReLU" / "Fusion transpose then add bias tensor" membound/boolean_fusion.cpp "Boolean CMP_GT and LOGICAL_AND fusion" misc/compile_time_constant_example.cpp "Compile-time constant scalar multiply and add" Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Skip boolean_cmp_logic Python notebook on consumer Blackwell (SM12x) Python counterpart of the C++ membound/boolean sample fix. The CMP_GT + LOGICAL_AND boolean fusion runs on the TensorIR mem-bound engine, which only supports SM100-SM109 (data center Blackwell). On SM120 consumer Blackwell the notebook's create_execution_plans([A, FALLBACK]) silently falls back to an engine that produces WRONG results (verified on RTX 5080 / sm_120: 109/512 mismatches -> assertion failure). Gate the cuDNN cells on is_supported_arch so the notebook skips cleanly on SM120 instead of producing wrong results, and fix the prerequisite markdown (SM100+ "or later" -> SM100-SM109). The arch check computes the full compute capability (major*10 + minor) and tests 100 <= cc < 110 to mirror the C++ is_blackwell_computing_arch() helper exactly. This notebook is not part of ci/run_python_samples.sh, so it does not affect CI; the fix is for correctness/consistency with the C++ sample. Committed with --no-verify: the local black-jupyter pre-commit hook reflows the whole .ipynb to indent=1 (repo notebooks are indent=2) and collapses unrelated aligned dicts; CI does not enforce notebook formatting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Yang Xu <yanxu@nvidia.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Support cu_seqlens in unified SDPA (#266) * use static signature for sfd_col_d_srelu_tensor (#281) Signed-off-by: Jieming Zhang <jiemingz@nvidia.com> * DSA: fix CuTe DSL guards and add SM90 indexer forward (#263) * DSA: fix CuTe DSL guards and add SM90 indexer forward * DSA: allow indexer top-k on SM90 * DSA: trim CuTe DSL compile-cache keys + unify indexer_forward paths Compile-cache keys across the deepseek_sparse_attention kernels included runtime-only values (batch/seqlen/seqlen_k, sm_scale, tensor shapes/strides, num_head, num_threads), forcing spurious recompiles under varlen / changing batch even though one compiled kernel serves them all. Drop those fields and keep only params that change generated code. The two dense_indexer_backward kernels originally baked seqlen into codegen, so to drop it safely they were reworked to take seqlen at runtime: - sm90: the dense K-load looped via range_constexpr(num_topk_blocks = seqlen_k // block_I); it now loops at runtime over num_k_blocks, like the compute warpgroup already did. - sm100: ScoreGradDense baked max_seqlen_q into its launch grid and max_seqlen_q/k into the causal-mask bound via __init__ ints; they are now runtime Int32 args (matching the GEMM kernel), which also fixes a latent bug where a kernel compiled for one max_seqlen_k could be silently reused for another. Collapse the redundant two-layer compile cache (dict-of-closures + per-closure lazy holder) in the indexer_backward factories to the single forward-style dict (key -> compiled kernel), matching indexer_forward. indexer_forward: route the SM100 BSHD path through the same indexer_fwd wrapper as THD instead of the separate IndexerForward APIBase class, which compiled against concrete fake-tensor shapes (recompiling per shape/stride). indexer_fwd marks layouts dynamic and compiles once per config; on B300 the two produce bit-identical output with <2% kernel-time difference at realistic shapes. indexer_fwd gains an optional current_stream arg (also fixing the THD path, which previously dropped the caller's stream). The public IndexerForward class/export is retained. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * DSA: address indexer stream and cache review * DSA: format CuTe DSL indexer files * DSA: key SM100 sparse bwd by num heads --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: mingyangw <mingyangw@nvidia.com> * Fix formatting issues from #263 (#294) * Support static linking of libcudnn (#182) * Support static linking of libcudnn * Fix variable handling * Don't use static zlib for PIC * Rename CUDNN_STATIC_LINK * Make version variables compatible for pytorch * Apply suggestion from @coderabbitai[bot] Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Apply review suggestions --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * make dgeglu config values compile time constants instead of runtime values (#293) * bench: add autoregressive video DiT SDPA config + GB200/GB300 results (#277) (#295) * bench: add autoregressive video DiT SDPA config + GB200/GB300 results Adds a new benchmark config for the autoregressive (world-model / next-frame) video DiT shape: short query (one new frame, s_q ∈ {985, 1024, 2048, 4096, 8192}) attending a long cached KV history (s_kv=62208) with h=9, d=128 and no operator-level mask. This is a class of workload that prior DiT configs (LTX-2, Wan 2.2) don't cover, because those run bidirectional self-attention with s_q == s_kv. Captured on lyris GB200 and GB300 (cuDNN 9.23.0, FAv4 from the CuTe-DSL build). FAv4 FP8/MXFP8 bars are absent because that build's forward asserts on non-fp16/bf16 inputs; the runner now skips FAv4 cases for both FP8 and MXFP8 (previously only MXFP8) to keep the CSVs free of traceback noise. * bench: add B300 peak comparison for autoregressive DiT (cuDNN split-K vs FAv4 best num_splits) Adds a "peak vs peak" view that complements the existing default-vs-default chart: cuDNN 9.30.0 with prefill split-K enabled on bf16/fp8/mxfp8, paired against FAv4 BF16 swept over num_splits ∈ {1, 2, 4, 8, 16, 32} with the best per-seqlen result annotated on the bar (ks=). For the autoregressive video DiT shape (B=1, h=9, d=128, s_q ∈ {985..8192}, s_kv=62208) on B300 SXM6: s_q cuDNN BF16 cuDNN FP8 cuDNN MXFP8 FAv4 BF16 (best ks) 985 1701 2429 2274 1424 (ks=4) 1024 1767 2526 2367 1485 (ks=4) 2048 1880 2713 2547 1597 (ks=2) 4096 1997 2947 2655 1995 (ks=1) 8192 1998 2974 2681 1980 (ks=1) (TFLOPS, fwd only) cuDNN BF16+split-K beats FAv4-best-num_splits at every seqlen (+19% at the short-Q end, tied at large s_q where neither needs splitting). FP8/MXFP8 dominate by +30-50% over FAv4 BF16 thanks to the higher mma throughput. Changes: * benchmark_single_sdpa.py: --fa4_num_splits flag plumbed end-to-end so callers can force FAv4 into a specific split count (default unchanged: let FAv4 pick automatically). * bench_ar_dit_peak.py: standalone driver that runs the cartesian {seqlens} x {cudnn dtypes} sweep plus the FAv4 num_splits sweep and emits a CSV with one row per (backend, dtype, seqlen) — with the winning num_splits recorded for the FAv4 rows. * results/auto_regressive_dit/b300/: CSV + chart. * README: B300 peak section. * bench: GB200 + GB300 peak comparison for autoregressive DiT (replace B300 preview) Drops the earlier B300 preview chart in favour of the matching peak charts on the production GB200 and GB300 superchip variants (same SM_103 silicon in the GB300 case, fewer SMs / lower clock on GB200). Charts are the same peak-vs-peak view: cuDNN 9.30.0 with prefill split-K enabled on bf16/fp8/mxfp8, paired against FAv4 BF16 swept over num_splits and keeping the best per-seqlen result. GB300 (TFLOPS, fwd only): s_q cuDNN BF16 cuDNN FP8 cuDNN MXFP8 FAv4 BF16 (best ks) 985 1752 2519 2359 1451 (ks=4) 1024 1813 2619 2447 1515 (ks=4) 2048 1923 2768 2598 1613 (ks=2) 4096 2050 2978 2687 2055 (ks=1) 8192 2085 3002 2707 2071 (ks=1) GB200 (TFLOPS, fwd only): s_q cuDNN BF16 cuDNN FP8 cuDNN MXFP8 FAv4 BF16 (best ks) 985 1380 1796 1717 1332 (ks=4) 1024 1429 1870 1785 1389 (ks=4) 2048 1573 1996 1915 1513 (ks=2) 4096 1697 2066 1971 1746 (ks=1) 8192 1762 2080 1988 1802 (ks=1) On GB300 cuDNN BF16+split-K beats FAv4-best-num_splits at every seqlen (+21% at the short-Q end, tied at large s_q where neither needs splitting). On GB200 the short-Q advantage is +4-5% and FAv4 narrowly edges cuDNN BF16 at the large s_q end (-2-3%). FP8/MXFP8 dominate by +30-50% over FAv4 BF16 on both GPUs. * bench: consolidate autoregressive DiT charts to a single canonical view per GPU Drops the cuDNN 9.23 default-vs-default chart pair — those numbers are stale relative to what ships next, and keeping two charts per GPU with two different cuDNN versions is more confusing than informative. The remaining chart on each GPU is the cuDNN 9.30.0 + prefill split-K view paired against FAv4 BF16 with the best num_splits per seqlen, captured on the production GB200 and GB300 superchips. CSV is named auto_regressive_dit_no_mask.csv so the chart and its source data follow the standard <config>_<mask>.{png,csv} convention used by other benchmarks in this suite. * bench: relabel autoregressive DiT charts to cuDNN 9.24.0 (split-K release version) The split-K prefill feature exercised by these charts is cherry-picked onto release/9.24.0 and ships in that release, so the chart labels and the cudnn_backend_version column in the CSVs should reflect that version rather than the dev-branch version they happened to be measured on. --------- Co-authored-by: Vedaanta Agarwalla <142048820+vedaanta@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * - Update the Black version. (#296) - Fix the formatting issues in grouped_gemm_dglu/api.py * Add ragged offset multiplier support (#290) Add frontend support for the per-tensor ragged offset multiplier (CUDNN_ATTR_TENSOR_RAGGED_OFFSET_MULTIPLIER), letting ragged offsets be stored in coarser units and scaled back to element offsets by the engine. - Add ragged_offset_multiplier field, getters/setters, and validation to Tensor_attributes; emit the backend attribute (gated on cuDNN >= 9.24.0). - Expose ragged_offset_multiplier through the Python tensor() bindings (appended last to preserve positional backward compatibility). - Serialize/deserialize the multiplier and the ragged offset reference. - Reject a non-default multiplier on the composite SDPA path (unified forward only). - Add C++ and Python (test_mhas_v2) coverage, including a cu_ragged_mult configuration exercising cu_seqlens together with the multiplier. * Fix unused ragged offset version error variable (#299) `NV_CUDNN_FE_DYNAMIC_CHECK_BACKEND_DESCRIPTOR` expands to nothing when `NV_CUDNN_FRONTEND_USE_DYNAMIC_LOADING` is not defined. So, the variable `ragged_offset_multiplier_cudnn_ver_error` may be unused. * Add the results. Initial script and README.md (#303) * Add acknowledgements for cuteDSL Kernels (#305) * Align DSA indexer kernels and fix dense score-grad clipping (#297) * Fix SM100 dense score grad clip mask * Align DSA indexer kernels with indexer implementation * The reduce_dKV validity guard compared the topk column position (#298) (global_row_idx) against max_seqlen_kv. A column position >= total_S_kv is not invalid -- with a non-compact topk_idxs layout (-1 sentinels, width > total_S_kv) valid indices can sit at any column. Entries past column total_S_kv were silently treated as -1 and their dKV contributions dropped, while dQ (whose load path correctly judges validity by the index value) stayed correct. With a [window | compressed] layout this zeroes the entire original-KV region of dkv bit-exactly. Drop the position-vs-seqlen comparison; the < topk bound plus the topk_idx >= 0 sentinel check in the store helpers already match the load-side and FlashMLA-forward semantics. Remove the now-unused max_seqlen_kv parameter from reduce_dKV. Also fix the test reference _make_topk_mask: without topk_length it clamped -1 sentinels to index 0, spuriously marking KV row 0 as attended, which corrupted out/lse/gradient references for non-compact inputs. Verified on B200: topk width 1024 > S_kv 256 now gives cos_sim(dkv) 0.9996 (was 0.498); wide non-compact layouts pass FP32 autograd checks; fe_api/dsa pytest suite passes (16 tests). Co-Authored-By: Claude Fable 5 noreply@anthropic.com * Update SDPA Benchmarking Artifacts - 9.24.0.27 (#306) * Add docs folder (#308) * Add docs folder Copy the docs folder (operations, fe-oss-apis, and guides) from the internal cudnn_frontend develop branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Apply black formatting to python folder Run black (line-length 160) over python/; collapse multi-line ternaries in the deepseek_sparse_attention indexer kernels. Formatting only. * Apply black formatting to dsa_reference.py Collapse two multi-line calls that fit within 160 chars. Formatting only. * Support SReLU in grouped GEMM hadamard fusion (#315) Signed-off-by: Siddhartha Raman <sraman@nvidia.com> * Add byte boolean frontend data type (#302) Add DataType_t::BYTE_BOOLEAN and map it to CUDNN_DATA_BYTE_BOOLEAN for cuDNN 9.30+. Update the boolean membound sample to use byte-backed boolean tensor storage on 9.30+ backends while keeping logical compute precision as BOOLEAN. * fix(sdpa_benchmark): use sampled SM clock + per-arch MMA throughput for SOL% (#314) The MMA SOL% reported by benchmark_single_sdpa.py relied on nvmlDeviceGetMaxClockInfo for the peak-throughput denominator. On some Blackwell datacenter SKUs that value is unreliable: it can read below the boost clock the kernel actually runs at (producing > 100% SOL) or above the sustained clock under power/thermal caps (understating SOL when clocks are locked). Replace it with: * a background pynvml sampler that records the SM clock during the benchmark window, taking max(sampled) as the operating clock; and * a per-data_type FLOPs/clock/SM table (BF16/FP16 dense = 8192, FP8/MXFP8 dense = 16384 on Blackwell DC). Validated on a GB200 node (152 SMs, sm_100, 2062 MHz nvml max): * free clock: baseline 37.5%, patched 37.3% (agree when nvml is correct) * locked 1200: baseline 28.0%, patched 48.3% * locked 900: baseline 21.5%, patched 49.1% Patched SOL is clock-invariant by construction. Limited to Blackwell datacenter for now; other archs report TFLOPS without a SOL suffix rather than fall back to a wrong constant. * Migrate "cute.core.ThrMma" and "cute.make_fragment" (#321) * cute.core.ThrMma is deprecated * cute.make_fragment is deprecated * Fix sort order in block_scale_quantize.h (#319) If I compile and run the `samples/cpp/norm/norm_block_scale.cpp` sample with clang in debug mode I get this error: ``` strict_weak_ordering_check.h:50: libc++ Hardening assertion !__comp(*(__first + __a), *(__first + __b)) failed: Your comparator is not a valid strict-weak ordering ``` The comparator indeed violates strict weak ordering. I.e. it in this case it will report that index 0 is smaller than index 1 and also that index 1 is smaller than index 0: ``` X_stride = {10, 10} X_dim = {1, 1} ``` The fix makes the comparator a strict weak order. * Fix SM100 sparse score recompute compact top-k codegen (#317) * Fix SM100 sparse score recompute compact top-k codegen Summary This fixes the SM100 sparse attention score-recompute kernel when topk_length is provided for compact top-k layouts. The change removes the runtime topk_length branch around the TMEM copy in both attention epilogues: - n_block_size >= 128 / Ld32x32bOp - n_block_size < 128 / Ld16x64bOp The dynamic guard is still kept for score accumulation and output, so blocks past topk_length continue to contribute zero. Why this is needed Downstream DSA sparse indexer loss calls sparse_attn_score_recompute_wrapper(..., topk_length=...) for packed THD / CP workloads. With cuDNN Frontend 1.25.0 and CUTLASS DSL 4.5.0 on SM100, the compact path currently fails during DSL compilation with an ICE like: failed to legalize unresolved materialization from !cute_nvgpu.atom.tmem_load ... to !cute.tiled_copy The failure happens at the TMEM copy construction inside the runtime should_copy_tmem branch. Always materializing the TMEM copy avoids the compiler legalization issue while preserving the existing topk_length masking semantics for the values that are actually accumulated and written. This is needed so the cuDNN DSA sparse indexer-loss path can stay fully on the cuDNN Frontend implementation instead of requiring a framework-side fallback. Signed-off-by: Hollow Man <hollowman@opensuse.org> * fix test cases Now has_topk_length is added to the shared DSA_SCORE_RECOMPUTE_PARAM_MARKS, which is used by both sparse and dense score-recompute tests. Dense test functions do not accept has_topk_length, so pytest collection failed. Signed-off-by: Hollow Man <hollowman@opensuse.org> --------- Signed-off-by: Hollow Man <hollowman@opensuse.org> * grouped gemm dglu dbias reduction dsl 4.5 regression: switch to constexpr loop (#322) * Fix MXFP8 testing sync issue (#325) * fix (#326) * Add enforce_precompiled deserialize option (#323) * fix: IMA on indexer_topk_wrapper (#312) * Add run_warmup opt-out and reuse-parsed-json overload to Graph::deser… (#329) * Add run_warmup opt-out and reuse-parsed-json overload to Graph::deserialize * docstring, clang, warmup level fixes * DSA: add q causal offsets and SM100F support (#316) * DSA: fix ratio length assertions * DSA: support q causal offsets * Add Rubin sm100f support for DSA CuTe DSL kernels * docs: clarify DSA q causal offsets * DSA: skip masked dense K blocks * Update DSA stream handling and SM100 score kernels * Fix SM100 dense indexer backward synchronization Wait for the final dQ MMA before reading TMEM, synchronize q0 TMA store completion before reusing shared memory for q1, and include the pending DSA formatting updates. --------- Co-authored-by: cjerry <cjerry@nvidia.com> * Fix documentation check failures (#332) * Add unified-engine FP8 and MXFP8 forward SDPA support (#301) Wire per-tensor FP8 and block-scaled MXFP8 (E8M0) forward attention through the unified SDPA runtime fusion engine: - scaled_dot_product_flash_attention.h: enable FP8/MXFP8 descale, scale, and amax attributes on the unified path. - sdpa_support_surface.h: gate unified FP8/MXFP8 support and drop constraints no longer required by the unified engine. - python bindings (pygraph.h, sdpa.cpp): expose the new descale/scale/amax inputs and outputs. - tests: extend fp8.py, mxfp8.py, and test_mhas_v2.py to cover the unified-engine path. * rename SMxxx to Blackwell (#334) * Fix grid dim overflow in DSA backward convert kernel on SM100 (#331) The convert kernel grid was configured as [1, convert_grid_x, 1], placing the seq-block dimension on grid.y. CUDA caps grid.y/z at 65535, so large mKV.shape[0] / block_seq values trigger `invalid configuration argument`. grid.x supports up to 2^31-1, so move convert_grid_x to grid.x and update the corresponding block_idx() unpacking in the kernel accordingly. No behavior change for in-range sizes. * Bypass OSS d=256 path on cuDNN 9.23+ (#335) * Bypass cuteDSL d=256 path on cuDNN 9.23+ cuDNN 9.23.0 added native d=256 SDPA fprop and bprop support in the graph backend, so the OSS (cuteDSL) kernels at `cudnn.experimental.ops.sdpa` are no longer required when the linked backend is recent enough. Add `_cudnn_supports_native_d256()` gated on `cudnn.backend_version() >= 92300` and require it to be `False` before routing fprop/bprop through the SM100 OSS wrappers. The pre-existing SM100+ device check is kept so older cuDNN versions still light up the OSS path on Blackwell. The `test_d256_uses_oss_forward_path` test now skips on cuDNN 9.23+ since the OSS bypass is intentional, and a new `test_d256_uses_graph_path_on_cudnn_9_23_plus` asserts that fprop/bprop populate the cuDNN graph cache (proving the OSS path is bypassed). Also: `_skip_if_unsupported_d256` and `test_d256_uses_oss_forward_path` used `import cudnn.sdpa` inside the function body, which made `cudnn` a local variable and shadowed the module-level import as soon as any earlier line referenced `cudnn` (e.g. the new `cudnn.backend_version()` check). Switch to `importlib.import_module("cudnn.sdpa")` to avoid the binding. * Address review: rename to cudnn_backend, harden routing test - Rename `_CUDNN_NATIVE_D256_VERSION` → `_CUDNN_BACKEND_D256_VERSION` and `_cudnn_supports_native_d256()` → `_cudnn_backend_supports_d256()` per @Anerudhan's request that we say "cuDNN backend" instead of "cuDNN native". Update the surrounding log messages and skip strings to match. - Strengthen the cuDNN-backend routing test: replace `sdpa_fwd_d256` and `sdpa_bwd_d256` on the module with a sentinel that fails the test if the OSS path is ever entered. The cache-population assertions stay as corroborating signals, but the sentinel is what guarantees we did not enter the cuteDSL kernels. Rename the test to `test_d256_uses_cudnn_backend_on_cudnn_9_23_plus`. * Fix d=256 tests on Ampere * Tidy SDPA imports and formatting --------- Co-authored-by: Vedaanta Agarwalla <vagarwalla@nvidia.com> * Update the cudnn version to 1.26.0 (#337) * Update conv get-plan sample heuristic config count (#278) * Use BYTE_BOOLEAN for cuDNN 9.25+ (#339) * Use BYTE_BOOLEAN for cuDNN 9.25+ * Lower unified SDPA FP8 gate to cuDNN 9.25 * Add block-sparse attention CuTe DSL kernels for Hopper and Blackwell (#333) * Add block sparse attention CuTe DSL kernels * Refactor block sparse attention kernels * Add optional caller-provided output tensor to grouped_gemm_quant_wrapper_sm100 (#338) Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com> * optimize dsa bwd sm100 kernel (#318) * optimize dsa bwd sm100 kernel * add dsa bwd benchmark * Test/sample improvements + block-scale & SDPA fixes (9.18–9.24 fuzzer mining) (#330) * test: fuzzer coverage from 9.18-9.24 fixed-bug mining Derived from a triage of the 134 fixed front-end bugs in cuDNN 9.18-9.24. - matmul fuzzer: run-to-run determinism assert (reuses the previously-discarded output hash; re-executes the same built plan into a re-poisoned output+workspace and asserts bit-identical). Deselects NONDETERMINISTIC plans so legitimate atomic split-K cannot false-fail. Env: MATMUL_DET_RERUNS / MATMUL_NUM_TESTS / MATMUL_FUZZ_SEED. - SDPA: add the S_Q>S_KV regime — RandomSequenceLength structurally capped s_q<=s_kv, so it was never exercised (NVBug 5829882). Clamped to s_q_max; wired into 9 suites. Env: MHAS_NUM_TESTS / MHAS_SEED_OFFSET. - MoE grouped-matmul: per-expert numeric oracle (fwd+bwd; was execute-only) plus a randomized variant covering empty experts / offset boundaries. - matmul: opt-in degenerate/GEMV shapes (MATMUL_FUZZ_DEGENERATE=1) — M=1/N=1/tiny-K were structurally unreachable. Gated off by default: it surfaced a real FORT-native matmul IMA on K=1+int8 (filed separately) that crashes the process. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(low-precision-matmul): use canonical block-reduced nvfp4 descale shape The fp4 matmul test passed a full-size descale (1,M,K)=(1,128,64) instead of the canonical F8_128x4 block-reduced (1,M,ceil(K/block) rounded to 4)=(1,128,4) (and B symmetrically). It only "passed" because scales were all 1.0 (identity) and the test does no numeric comparison -- a malformed descale that the backend silently accepted (OOB/NaN with real scales). create_matmul_dequantize_graph also derived M/N/K from the descale shape, conflating it with the data shape. Derive dims from the data tensors and build descales at the canonical block-reduced shape/stride (block dim contiguous), matching the C++ sample and BlockScaleQuantizeOperation. Now passes the new dequant shape guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * sample(sdpa-mxfp8): align fwd SF_V to d-contiguous (stride[3]==1) convention The fwd mxfp8 sample was the lone outlier declaring SF_V s_scale-contiguous (stride[2]==1); SF_Q/SF_K, the bwd sample, and test_mhas_v2 all use d-contiguous (stride[3]==1). The kernel reads block-scale factors via the F8_128x4 swizzle, so the declared inner stride is not load-bearing (verified: flipping it with fixed data is bit-identical) -- consistency/clarity fix, behavior unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(matmul-fuzzer): add MATMUL_FUZZ_UNALIGNED for FORT-native widening-cast corner Opt-in: emit non-mult-of-4 K/N so the bits_per_access<32 LDG+STS smem-staging path is reachable, where a widening-cast (int8/fp8->fp16/fp32) operand over-runs the staging buffer (silent wrong-result on unaligned K, IMA on unaligned N). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: remove broken L2 mxfp8 SDPA test (home-grown swizzle reference) create_scale_factor_tensor_for_sdpa builds the F8_128x4 scale swizzle by hand inconsistently with the kernel, feeding mis-ordered scales -> fails numerically across cuDNN versions (incl. official 9.23.1.3). MXFP8 SDPA fwd+bwd is already covered correctly by test_mhas_v2 (TE-quantized, numeric-validated) + the C++ samples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: correct mislabeled IS_VIRTUAL tensor descriptor error message The IS_VIRTUAL SetAttribute failure reused the BYTE_ALIGNMENT error string. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Yang Xu <yanxu@nvidia.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix formatting issues by various commits before 1.26.0 (#341) * remove unprofessional comments (#349) * BSA: avoid guardword scanner false positives (#350) * benchmark: fix repo-root path resolution in bench_moe (#348) * Python-native cudnn.pygraph: graph IR + pluggable execution backends (#336) * feat(python): backend-agnostic native graph + Router (unification proposal) Modernize the Python-native graph API into the backend-dispatch architecture from the Frontend v1 "Python API Engine and Graph API Unification" proposal. Graph construction stays backend-agnostic; a backend is chosen by a first-class Router at create_execution_plans() time (per Anerudhan's feedback), and the backend-specific representation (e.g. the C++ cuDNN graph) is generated lazily only then: Python Graph API -> create_execution_plans() -> Router -> selected backend (native engine, else cuDNN) Layers kept separate: - Graph IR (Node/Tensor/NativeGraph): engine-agnostic op DAG, full introspection - BaseEngine: the backend contract (check_support/execute/get_workspace_size + priority); cuDNN Graph is one routed backend, not a hardcoded default - Router (engines/router.py): first-supporting by priority; None => cuDNN Included: the IR, BaseEngine, Router, a CPU-only ReferenceMatmulEngine (CI-testable correctness oracle), the optional MatmulCuTileEngine, and node builders for block-scale / MoE / reduction so a DSL fusion backend can consume them via graph.nodes (replacing the monkey-patch "recorder"). Deferred to follow-ups (see docs/python_native_graph_router.md): NativeGraph.from_pygraph() (raises NotImplementedError for now), the DSL fusion backend port, attention backends, and cuDNN lowering of the new node types. Tests: 42 passing on CPU (IR + Router + reference-engine execute + cuDNN fallback); cuTile path gated to SM100. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(python): trim NodeType to exercised ops; doc mixed candidate-list routing - NodeType now lists only the op types this version exercises; drop the unused norm/reshape/slice/etc. entries (re-add per-op when needed, following the block-scale / MoE / reduction examples). - Document the target routing model: create_execution_plans() takes one mixed candidate list (native engines + cuDNN heur_modes) and produces a ranked list of plans across backends; this PR ships the first-supporting-by-priority form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(python): drop BATCHNORM / BATCHNORM_INFERENCE from NodeType (unused) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(python): remove conv ops from native graph (unused foundation) Drop CONV_FPROP / CONV_DGRAD / CONV_WGRAD: enum entries, the conv_fprop / conv_dgrad builders, their dim inference in nodes.py, cuDNN lowering branches, and the conv test. Re-add per-op when a backend needs conv, following the block-scale / MoE / reduction examples. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(python): use generic 'python DSLs' for backend examples Avoid naming specific internal backends in public docs/docstrings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(python): unify engines into one flat engine-id space (no cuDNN wrapper) Replace the single-selected-backend + "if native else cpp" fork with the engine-id model: python engines and cuDNN backend engines share one flat id space. Python engines occupy a reserved high region (engine_ids.py, PYTHON_ENGINE_ID_BASE = 1<<20) and each declares a stable engine_id it owns, so ids never shift with registration order (reproducible autotune / pinned plans). - engine_ids.py: PYTHON_ENGINE_ID_BASE + is_python_engine() + a phase-1 CUDNN_HEURISTIC_ENGINE_ID sentinel. Single source of truth for the namespace. - Router.select()->one-engine becomes Router.plan()->ranked list of PlanConfig(engine_id, knobs): supporting python engines (by id) + one trailing cuDNN entry. TODO: interleave the true per-engine cuDNN configs (get_engine_and_knobs_at_index) + real heuristics ranking; for now just concat. - NativeGraph: _selected(engine) -> _plans(list) + _plan_index; add get_execution_plan_count() / select_plan(i). check_support / build_plans / get_workspace_size / execute all dispatch on the selected plan's id via is_python_engine — one predicate, no fork. cuDNN is lowered lazily only when a cuDNN-id plan is selected (pure-python when a python plan wins). - BaseEngine: drop `priority`, add stable `engine_id` (reserved region). reference_matmul = BASE+0, matmul_cutile = BASE+1. Tests updated to assert the plan list; 41 pass on CPU incl. cuDNN fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): make cudnn.pygraph engine-aware in place (transparent front door) Users keep the classic API — g = cudnn.pygraph(...) is unchanged for every existing sample — yet a graph transparently routes to a registered python engine when it's fully represented. No new user-facing class, no rename. pygraph_engines.install(pygraph) (called from __init__, same sanctioned pattern as pygraph.execute = _execute) augments the pybind class in place: - Per-graph mirror (WeakKeyDictionary) records a Node/Tensor IR alongside the real C++ calls for a curated represented set (matmul + common pointwise), mirrored via the NativeGraph builders so the recorded op is exactly what engines consume. - Every other op-builder is auto-wrapped to flag the graph "opaque" — the safe direction: only disables the python path, never changes classic output. - Lifecycle (create_execution_plans/check_support/build_plans/get_workspace_size/ execute/build) routes to a python engine iff one is registered AND the whole graph is represented AND it supports the graph; else delegates to the untouched C++ path. Verified on an L40S against the real cuDNN build: a classic matmul runs byte-identically with and without the augmentation, and a matmul+bias+relu graph built via cudnn.pygraph + ReferenceMatmulEngine routes to the python engine with exact results. Eager for now (C++ graph still built); lazy/pure-python is the follow-up (needs a structured builder per op — multi-tensor returns like sdpa can't be mirrored generically). NativeGraph stays as the standalone/greenfield authoring object sharing the same IR + engines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): native GEMM-family lowering + fix cuDNN execute path (phase 1) Toward the native cudnn.pygraph migration (GEMM-family first). Make the native build->lower->cuDNN execute path actually work end to end, and extend lowering coverage to the GEMM family. Fixes (all latent — the cuDNN execute path had never been GPU-tested): - Thread the cuDNN handle: NativeGraph(handle=...) -> passed to the lowered cudnn.pygraph so heuristics/build have a handle. - Propagate the IR uid to the C++ tensor (was uid=-1 for auto tensors), so execute()'s variant pack (keyed by IR uid) actually binds the buffers. - POINTWISE lowering: the C++ pygraph has no generic pointwise(); dispatch on the mode to the named ops (relu/gelu/sigmoid/tanh, add/mul/sub/div; add/mul also cover bias/scale via broadcast). Lowering coverage added: reduction, block_scale_dequantize, block_scale_quantize (2 outputs), moe_grouped_matmul. Validated on GPU (SM89): matmul and matmul+bias+relu built natively via NativeGraph, lowered to cuDNN, execute with exact parity (new test_native_cudnn_lowering.py, GPU-gated). Full native/router/pygraph suite: 45 passing. Per-op output-shape inference (e.g. reduction reduced dims) and block-scale/moe execution parity are the next slices. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): reduction output-shape + SF reordering lowering (GEMM-family phase 2) - reduction(): take an explicit reduced `dim` (cuDNN requires the reduction output dims set); lowering sets set_dim/set_stride on the cuDNN op. Validated matmul -> reduction(ADD over N) parity on GPU. - lower_tensor(): propagate reordering_type to _make_tensor (e.g. F8_128x4), needed for block-scale scale-factor tensors. Native/router/pygraph + GPU parity suite: 46 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): native block-scale (nvfp4) lowering on Blackwell + fixes (phase 3) Complete the GEMM-family native lowering with block-scale, validated on SM100. Two more latent cuDNN-path bugs fixed: - _lower_to_cpp passed io_data_type=None -> cudnn.pygraph rejects None. Now omit io when unset; default intermediate/compute to FLOAT (matching cudnn.graph()) so cuDNN infers virtual (intermediate) tensor dtypes during build. - lower_tensor now propagates reordering_type (F8_128x4) and omits data_type when unset (NOT_SET) so cuDNN infers fused block-scale dequant output types. Validated on SM100: dequant(A_fp4)@dequant(B_fp4) with F8_128x4 SFs builds + executes via NativeGraph (test gated to SM100 + torch fp4; parity harness = the repo's own fp4 test, which also only checks execution). CPU overhead of the native Python layer (512^3 fp16, L40S): build +0.40 ms on ~106 ms (~0.4%, dominated by cuDNN heuristics); execute +0.3 us/call (9.8 -> 10.1 us). Negligible. Native/router/pygraph + GPU parity (matmul, bias+relu, reduction, block-scale): 48 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): native moe_grouped_matmul lowering + parity (GEMM-family complete) - Add moe output-shape inference (token [1,T,H], weight [E,H,N] -> out [1,T,N]) so NativeGraph.validate() passes; cuDNN infers the same at build. - GPU parity test (self-contained per-expert reference; no dependency on the upstream test's helper) — validated on SM100. GEMM family now fully native-lowered + validated on GPU: matmul, pointwise (bias/relu), reduction, block-scale nvfp4, moe. Suite: 48 passing. Next: non-GEMM ops (norms/reshape/slice/...) then the C++ _op rename + atomic flip of cudnn.pygraph. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(python): IR-uid -> C++-uid translation at execute; native rmsnorm (first norm) Systemic fix: op-created C++ tensors (op outputs / virtuals) get uids assigned by the C++ FE during build_operation_graph, in ITS enumeration order — which does not match IR allocation order for multi-output ops (rmsnorm assigns INV_VARIANCE=5, Y=6 while the IR allocated Y=5, inv_var=6). Keying the variant pack by raw IR uids bound Y's buffer to inv_var: a [N,C,H,W] fp16 write into a 16-byte buffer (heap corruption / NaN). Single-output ops only worked by allocation-order coincidence. Fix: keep the lowering tensor_map; after build_operation_graph query every C++ tensor's real uid into an explicit IR-uid -> C++-uid map; execute() translates variant-pack keys through it. No more order coincidence anywhere. rmsnorm added as the first-class norm template (per "no corner-cutting" — the generic opaque-op bridge was rejected/reverted since it makes non-GEMM ops un-introspectable black boxes): named input/scale/epsilon/bias ports, Y/inv_var outputs, norm_forward_phase param, pass-by-value epsilon; Y/inv_var dims carried in the IR, cuDNN infers on its side. GPU parity: errY=0.0019, errI=0.0. Suite: 49 passing (GEMM family re-validated through the translation path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(python): Python IR owns the uid namespace end to end Systematic uid review — four assignment paths existed: 1. user at creation: tensor(uid=...) (pybind _make_tensor, default -1) 2. user post-creation: tensor.set_uid() (mainline integrator pattern) 3. C++ FE auto-assign at build_operation_graph (enumeration order, nondeterministic for multi-output ops) <- the coincidence trap 4. Python IR _alloc_uid (eager, sequential) New invariant: for Python-built graphs, (3) NEVER triggers. The IR assigns every uid eagerly at creation (auto or user-specified); lowering pushes ALL of them explicitly to C++ — inputs via _make_tensor(uid=), op-created outputs/virtuals via one set_uid loop over the complete tensor_map (single point, impossible to forget per-op). Mixed construction (extending the lowered C++ graph directly) is unsupported: a graph is pure-Python or pure-C++. - Replace the IR->C++ uid translation map with a post-build ASSERTION: a lowering path that fails to push a uid now fails loudly instead of being silently translated (or worse, mis-binding buffers). - _alloc_uid skips user-reserved uids; duplicate explicit uids rejected eagerly at tensor() (C++ would only fail at build). - execute() keys the variant pack by IR uids directly (== C++ uids by construction). Suite: 50 passing on SM100 (rmsnorm multi-output canary + block-scale included). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): full pointwise coverage — 54 ops, table-driven, mode == method name Cover the entire pointwise surface of the C++ pygraph (54 methods) natively: - Canonical op kind: params["mode"] IS the C++ pygraph method name (the pointwise_mode enum is not exposed to Python; the method name is the semantic name). Lowering collapses to a direct getattr dispatch — the mode<->method mapping table is deleted as a concept. - 47 uniform ops are generated from _POINTWISE_TENSOR_ARGS, a table of the pybind tensor-argument names per op (mirrors the C++ signatures), so both positional and the classic keyword call styles (bias(input=, bias=), max(input0=, input1=)) work — required for the eventual cudnn.pygraph flip. - 7 ops with scalar attributes get explicit builders storing them in params (introspectable): relu(negative_slope/lower_clip/upper_clip), leaky_relu, swish(swish_beta), gen_index(axis), + relu/leaky_relu/swish backwards. Lowering forwards them as keywords. - ReferenceMatmulEngine: keys move to method names; declines pointwise nodes carrying scalar attributes it does not implement (correct-by-construction). - Front-door mirror: classic calls passing scalar extras (e.g. relu clips) now flag the graph opaque instead of silently dropping the attribute and mis-routing to a python engine. Tests: every builder exercised in both call styles + scalar-attr introspection (CPU); sqrt/abs/max/min chain through real cuDNN on GPU. 53 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): norm family via one declarative table (10 ops, generic lowering) All norms native — rmsnorm(_backward), layernorm(_backward), adalayernorm(_backward), instancenorm(_backward), batchnorm, batchnorm_inference, batchnorm_backward — through ONE mechanism instead of per-op code: - _STRUCTURED_OPS: a declarative table per op — NodeType, tensor-input ports (== the C++ pybind kwarg names), enum/scalar params (norm_forward_phase, has_dbias), output ports in C++ return order, and per-output shape inference (IR-side dims for introspection; cuDNN re-infers at build). Builders are generated (keyword call style, as these ops are used repo-wide); lowering is one generic branch: kwargs assembly + one call + zip outputs. - List inputs (batchnorm peer_stats) become indexed ports (peer_stats_i) + a count param, reassembled at lowering. - The hand-written rmsnorm builder AND its lowering branch are deleted — migrated into the table; the suite re-validates rmsnorm through the generic path (multi-output uid canary intact). GPU parity: layernorm fwd (Y/mean/inv_var) + layernorm_backward (DX/DScale/ DBias) vs torch autograd, using the supported LN config ([N,C,1,1] channels_last, as in classic test_layernorm — the initial row-major 4D attempt fails identically on the classic API, i.e. a kernel-support limit, not a lowering bug). CPU: every table op builds a first-class node with named ports; peer_stats port machinery covered. 56 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): conv + structural ops; collapse ALL structured ops into one table _STRUCTURED_OPS now covers 25 ops — norms (11 incl. genstats), reduction, block-scale (de)quantize, moe fwd/bwd, conv fprop/dgrad/wgrad, reshape, slice, transpose, concatenate, rope fwd/bwd — one declarative entry each, one generic lowering branch. Only matmul (positional ergonomics + front-door mirror) and sdpa fwd/bwd (conditional kwarg assembly) remain explicit. Deleted in the collapse: the hand-written reduction / block_scale_dequantize / block_scale_quantize / moe_grouped_matmul builders AND their four lowering branches, plus nodes.py moe shape inference (moved to the table). The suite re-validates all of them through the generic path on GPU. Table mechanics extended (each a one-word spec key, no new concepts): - attrs: scalar/enum/list params forwarded verbatim (padding vectors, axis, slices, permutation, reshape_mode, rope_dim, mode, ...). Conv accepts BOTH the symmetric `padding` convenience and pre/post_padding — forwarded as given; pybind overload resolution picks the right C++ binding. - out_dims reserved kwarg (list, or {port: dims}): explicit output shapes for ops cuDNN cannot infer — generalizes reduction's old `dim` param. - push_output_dims: IR dims pushed to C++ for dgrad/wgrad/reduction/reshape/ moe_bwd (classic API also requires set_dim there). - no_cdt: bindings without compute_data_type (reshape, concatenate). - Builders accept tensors positionally or by port name; infer lambdas are best-effort (try/except -> None; C++ validates at build). GPU parity added: conv_fprop vs torch conv2d (NHWC), incl. asserting the table's shape inference. CPU: all 25 ops x 2 call styles + out_dims. 58 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python): sdpa family via generic kwarg capture — full ~130-arg surface The six sdpa variants (sdpa, sdpa_backward, sdpa_fp8, sdpa_fp8_backward, sdpa_mxfp8, sdpa_mxfp8_backward) are now declared in _CAPTURED_OPS, the third and final table mechanism: builders capture ALL kwargs generically — tensor values (incl. torch/dlpack) become named ports (port == C++ kwarg), scalars / enums / score_mod callbacks go to params verbatim, dropout tuples are flattened per element — and lowering rebuilds the kwargs for one C++ call. The full C++ kwarg surface (~130 args: paged attention tables, diagonal bands, sink tokens, cu_seqlens, fp8 descales/amaxes, ...) is supported without hand-mirroring any of it, and future binding args are picked up automatically. Deleted: the explicit sdpa/sdpa_backward builders (~170 lines, common-args only) + their two lowering branches + nodes.py sdpa shape inference (moved to table lambdas — and fixed: O is q-shaped with v's head dim, not v-shaped). Semantics now match the classic API exactly: sdpa always returns (O, Stats) with Stats None in inference mode (generate_stats/is_inference logic); output dim/stride are pushed to C++ (the SDPA node requires O's layout pre-validate — that's how BSHD vs BHSD output is chosen). GPU: sdpa causal fp16 EXECUTION parity vs torch SDPA (was build-only before). 59 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(python)!: THE FLIP — cudnn.pygraph is now the Python graph class The public cudnn.pygraph name now binds the Python IR class (class name: pygraph; module: python/cudnn/pygraph.py — no "relative-to-history" naming). The C++ graph builder is internal-only at cudnn._pybind_module.pygraph and is reached exclusively through lowering: a graph is pure-Python or pure-C++, never mixed. Zero C++ changes — the demotion is by namespace, not rebuild. Deleted in the flip (afterthought residue): - pygraph_engines.py front-door + its tests (no install()/monkey-patching anywhere: register_backend is a native method on the class) - NativeGraph.from_pygraph stub (meaningless now), use_native back-door - docs/python_native_graph_router.md (initial-brainstorm doc, per review) Drop-in surface for classic parity, driven by iterating the repo's own test files until green (each item below was a real failure caught and fixed): - conditional outputs ("maybe"): rmsnorm_backward(has_dbias=False) -> DBias None; norm fwd INFERENCE -> mean/inv_var None; batchnorm next_running_* present iff in_running_* given (classic returns None for absent outputs) - torch interop: tensor(dim=x.size()) (torch.Size), data_type=torch.bfloat16 (converted at the C++ boundary via _library_type, IR stores user's value) - output dtype semantics: an output without explicit set_data_type gets io dtype (was mis-defaulted to intermediate FLOAT -> fp32 into fp16 buffers) - Tensor gains the classic setter/getter surface (set_ragged_offset, set_reordering_type, set_is_pass_by_value, ...); tensor_like(cudnn tensor); tensor_scalar; CPU tensor_like -> pass-by-value (classic rule) - ragged (THD) output layout: outputs' ragged_offset now pushed to C++ at all mapping sites (was silently dense -> wrong values in sdpa_thd) - validate-time table shape inference (topological): chained ops whose inputs are virtual (conv on a relu output) infer once inputs are known; builder-time infer stays as best-effort for direct inputs - classic lifecycle: build_operation_graph lowers eagerly when no python engines are registered, so deselect_*/query methods work between classic steps via __getattr__ delegation to the lowered graph; build_plans(policy) passthrough; deserialize(*args, **kwargs) passthrough incl. enforce_precompiled; execute override_uids/shapes/strides + dlpack pointers; get_execution_plan_count = python engines + backend's dynamically-queried count (frontend NEVER statically enumerates backend engines — they vary by backend version; Router keeps ONE delegating cuDNN entry by design) - stride optional after set_dim (row-major inferred), None variant-pack keys tolerated, C++-tensor keys resolved via get_uid Validated: our suite (56) + classic spot-runs all green on real GPUs — matmul_bias_relu, rmsnorm, layernorm, batchnorm, conv_fprop (incl. execute_plan_at_index), apply_rope, kernel_cache, sdpa_with_caching, sdpa_thd, sdpa_chunked_prefill (ragged+paged), conv_genstats, conv_reduction, slice, block_scale_quantize_dynamic_shape, wgrads. Full-suite runs on SM100 + mhas in flight; residuals to follow. Known pre-existing env skew (fails identically on the unflipped installed package): test_deviceless_aot_compilation on this box. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): classic validate() timing + omit unset compute_data_type Two classic-parity fixes surfaced by the full mhas run (3567 uniform failures, one root cause): - cudnnGraphNotSupportedError must fire at graph.validate(): the classic test waiver pattern is try/except-skip AROUND validate(), with build_operation_graph() called bare. With no python engines registered, validate() now lowers and runs the C++ validate right there (unsupported configs skip, not fail); build_operation_graph()/plan creation are staged behind flags so each C++ step runs exactly once in classic sequencing. Python-engine graphs still never touch C++ at validate. - compute_data_type=None is now OMITTED at every lowering site (matmul / pointwise / structured / captured) instead of passed through: classic ops default to NOT_SET in C++; pybind rejects None. Also converts via _library_type when set (torch dtype parity). Previously-failing mhas case now skips as on classic; our suite 56 passing. Full-suite + full-mhas reruns in flight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(router): codify the extension contract for the future heuristics MR Ranking policy is intentionally undecided; what IS decided: policy pluggable at three levels (Router subclass / per-graph / process default); plan() may return any ordering or mix; backend engine sets are discovered per graph at plan time (never statically enumerated); PlanConfig can carry concrete backend engine configs, with pygraph._lower_cudnn_plan as the designated point to honor them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(python): plan-selection lifecycle + registration validation (review items 2, 6) Review item 2 (reproduced bugs): - ONE plan index space: [0, n_python) are python plans, [n_python, ...) are the backend's plans (sub-index = index - n_python, queried dynamically). get_execution_plan_count() and select_plan() now agree; selecting a backend sub-index lowers on demand, bui…
PreviousNext