Conversation
… 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>
…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 (83ffded) — 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 - 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>
Use stable SDPA documentation URLs in overview and mark the DSA architecture block as text to avoid code highlighter parsing issues.
…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 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
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 * Add behavior note serialization regression sample
…s.txt (#359) * Add dev dependency group * Reorder pyproject sections
…ut (#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 #342 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…truct 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
Organize FE OSS tests into flat feature directories and update imports and documentation paths.
* 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
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>
…d 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>
* 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 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
* 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
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 * 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 arguments. Call sites whose inputs were already absolute values keep the same semantics via kind="fmax", nan=True; the three quant_sfd_col paths (dglu, dswiglu, discrete-dglu) that silently relied on the hardcoded .abs modifier now apply math.absf explicitly, same as their glu counterparts. Verified on sm_100: gemm_amax (206 passed), glu/hadamard/wgrad/quant/ srelu/dsrelu suites, and rmsnorm_rht_amax (24 passed). The dglu fp8 variants fail NVVM backend compilation on 4.6.0 with or without this change (fp4 variants pass) - that is a separate pre-existing 4.6.0 regression previously masked by the import errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Relax nvidia-cutlass-dsl pin to >=4.5.0 Match the internal cudnn_frontend pyproject policy rather than pinning an exact version. The CuTe DSL kernels now use only public cute.arch APIs (warp_redux_sync), which are compatible across 4.5.x and 4.6.x. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* generalize-utmastg * a * per-tensor-tmastg * fix-code-rabbit-comment
…on-contiguous, dense-compatible layouts (#712) * Enable FROST SDPA forward engines to write dense LSE directly to non-contiguous, dense-compatible layouts Signed-off-by: Haobin Guo <haobing@nvidia.com> * Address comments --------- Signed-off-by: Haobin Guo <haobing@nvidia.com>
* benchmark: sample the SM clock of the GPU the benchmark actually runs on The peak-MMA/SOL clock sampler indexed NVML with torch.cuda.current_device(), but NVML enumerates PHYSICAL GPUs and ignores CUDA_VISIBLE_DEVICES while torch indexes only the visible subset — so a shard pinned to GPU n via CUDA_VISIBLE_DEVICES always sampled physical GPU 0. When several single-GPU shards run side by side on a multi-GPU node, a shard whose neighbor GPU 0 has drained records GPU 0's IDLE clock as the window peak, collapsing the chart's MMA-max line and SOL% by the idle-vs-boost ratio (observed >10x too low on GB200/GB300). Map the torch index through CUDA_VISIBLE_DEVICES (index, UUID and MIG forms) before asking NVML for the handle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * benchmark: add the Ampere (sm80) row to the peak-MMA table _FLOPS_PER_CLOCK_PER_SM had sm90/sm100/sm12x entries only, so A100 runs computed no peak_mma_tflops and their charts drew no MMA-throughput max line. A100: 312 dense BF16/FP16 TFLOPS (FP32 accumulate; 624 is the sparsity figure) = 108 SMs x 1.41 GHz x 2048 FLOPs/clk/SM. No fp8/mxfp8 entries -- Ampere has neither datapath, and those cases already record unsupported. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* refactor-m-major-output * remove-dead-code
Add a pr-merge-requirements workflow that fails while a PR has no Milestone or is not on any Project board, so it can be made a required status check. Bot-authored PRs and PRs labeled cat-routine-update are exempt. The check queries live PR state, so a manual re-run after setting the fields is enough to turn it green. Runs as pull_request_target (fork PRs need the repo secret) without checking out PR code. The Projects lookup needs a PROJECT_READ_TOKEN repository secret, since the built-in GITHUB_TOKEN cannot read Projects v2. Also add a PR-template checkbox reminding authors to set both fields. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
If the PROJECT_READ_TOKEN PAT is rejected (e.g. NVIDIA enterprise forbids classic tokens with >366-day lifetime), the GraphQL call aborted the script via errexit with only a cryptic exit code. Capture the failure and surface the API error message instead. The check still fails closed either way. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#728) With pre-9.26 cuDNN headers (or _WIN32) the whole #if body compiles away and 'm' is unused; -Werror=unused-parameter then fails the pip source build (seen in containers shipping older cuDNN headers, where every 'pip install .' of current develop breaks). Mark it [[maybe_unused]]. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* style: apply black formatting to benchmark and frost test files pre-commit's black hook (26.3.1, line-length 160) reformats these three files; clean them up so `pre-commit run --all-files` passes in CI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: enforce pre-commit style check via GitHub Actions Runs `pre-commit run --all-files` (clang-format v21.1.6 + black 26.3.1, as pinned in .pre-commit-config.yaml) on every PR and on pushes to develop/main, on a plain ubuntu-latest runner — no GPU needed. This replaces the internal analysis:clang-format CI job and enforces the formatting contract already documented in CONTRIBUTING.md. Also fix a stale reference to ci/run_style_check_diff.sh in the frost README (that script is internal-only and superseded by pre-commit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… (default SM100/SM90) (#572) The default indexer-backward pipeline mutates attn_score/index_score in place in kernel 1 and only faults (or corrupts memory) later in the GEMM when the plan signature is inconsistent, so validation problems used to fail dirty. Validate the full signature up front, before any kernel launch: * check_support now enforces the output-dtype contract (d_index_q and d_weights bf16-only -- the kernel dW store rounds the fp32 accumulator to bf16, so an fp32 d_weights buffer cannot be produced faithfully and is rejected instead of silently receiving bf16-precision values; d_index_k accepts bf16 or fp32), the semantic shape relationships between all nine tensors, and compact-contiguous layouts (the kernels address K/dK with a hard-coded compact (D, 1) stride and the backend caches do not key the layout). * execute() re-validates the runtime tensors against the descriptors captured at plan-build time (dtype/shape/stride) so a directly-built or exported plan reused with a mismatched tensor raises a clean ValueError while the score buffers are still pristine. * IndexerBackward and the wrapper validate ranks (index_q 4D, index_k / topk_indices 3D) before deriving plan dimensions. * An fp32 d_index_k output buffer is now zeroed internally on the selected stream (the dK epilogue atomic-adds into it) on both SM100 and SM90, removing the fragile caller pre-zero contract. * The wrapper plan cache keys the output dtypes (d_index_q / d_weights / d_index_k) so the output-dtype validation cannot be skipped on a cache hit. The bf16 compute path is untouched. On SM100, dQ/dW and the in-place score-grad outputs stay bitwise-identical to the previous default backend; on SM90, dQ and the score-grad outputs are bitwise-identical, while dW sits in the pre-existing SMEM-atomicAdd jitter band (SM90 dW was never run-to-run deterministic, even on the unpatched base). dK is an fp32 atomic scatter (within jitter) on both. Signed-off-by: zky <kaiyue.zhou@z.ai>
Six independent changes to FlashAttentionDSABackwardSm100, measured on a B200 at a locked 1830 MHz, S=8192, over topk 128/512/2048 x causal 0/1. All twelve scenarios improve: D512 by 4.9-8.9%, D576 by 11.1-13.3%. dq is bit-identical to the previous kernel in every scenario (relative error exactly 0), and the launch shape is unchanged: grid 4096, block 640, 96 regs/thread, 216 KiB SMEM/CTA on D512 and 232 KiB on D576. Gather-index path. The per-tile top-k indices were read by lane 0 one row at a time into an rmem tensor, then broadcast. Lane i now reads its own row in _load_tile_topk_idx and the consumer shuffles it out, so the indices live in a single register and the next tile's can be fetched at the tail of the current iteration instead of at the head of the next one. Register budget. num_regs_load_KV 40 -> 56 clears the spills on the gather address path (all of them on D512, ~99% on D576). The per-warp counts must exactly exhaust the CTA pool, which the 640-thread launch fixes at 96 regs/thread: 128*56 + 128*128 + 256*128 + 128*40 = 61440 = 96 * 640. MMA order. dQ = K @ dS now issues before dKV = Q @ dS, which puts load_mma_K_pipeline.consumer_release ahead of the dKV GEMMs rather than after them, freeing the K buffer earlier in the iteration. S lifetime. The fenced T2R of S has fully consumed TMEM S and nothing below reads it, so its consumer_release moves up to the fence instead of trailing P's publication. This unbinds S's lifetime from P's, nothing more. dQ epilogue. store_dQ moves from Ld32x32bOp -- the last one in the file -- to the Ld16x256bOp/StMatrix form the P and dS paths already use, and the four 128-dim sub-tiles each get their own staging slot in the dead K buffer, so a store no longer waits on the previous store's SMEM read. The 64-column D576 tail stages through the dead P buffer instead of sK: sK does have spare room, but only at an offset past the four dQ sub-tiles, which would need its own swizzle/TMA-box proof. That alias is an exact fit today (cosize 4096 == 4096), so it and the sK alias both gain an assert to catch a future stage bump. test/python/fe_api/dsa/test_DSA_sparse_attention_backward.py: 15 passed, 1 skipped. Signed-off-by: Butterfingrz <13524387014@163.com>
…734) * samples: skip deterministic sdpa backward test on old cuDNN versions Blackwell + cuDNN<9.18 + deterministic SDPA backward is a combination explicitly marked as unsupported at include/cudnn_frontend/node/scaled_dot_product_flash_attention.h:1385. * samples: fix invalid double destroy of child CUDA graph in cudagraphs sample In "Cuda graphs with matmul add", `cudnn_cuda_graph_new` is obtained via `cudaGraphChildGraphNodeGetGraph(cudnn_node_in_main_graph, &cudnn_cuda_graph_new)`. Per the CUDA Runtime API documentation for `cudaGraphChildGraphNodeGetGraph`: "This call does not clone the graph. Changes to the graph will be reflected in the node, and the node retains ownership of the graph." Destroying `main_cuda_graph` with `cudaGraphDestroy(main_cuda_graph)` destroys the parent graph and all embedded child graphs owned by its nodes. Calling `cudaGraphDestroy(cudnn_cuda_graph_new)` afterwards attempts to destroy an already-destroyed graph handle, returning cudaErrorInvalidValue and leaving a sticky CUDA runtime error in the process device context that corrupts subsequent tests executing on that context in monolithic test runners like Catch2. Remove the invalid `cudaGraphDestroy(cudnn_cuda_graph_new)` call and wrap destruction calls in `CUDA_CHECK`. --------- Co-authored-by: Marcin Radomski <dextero@google.com>
* add sm120 matmul support * resolve conflicts for sm120 matmul * add benchmark test file for sm120 matmul * resolve issues about nvvm.elect_sync(), nvvm.griddepcontrol(wait), and drain of TMA warp for SM120 matmul
* add det 2k * add bias * fix * fix pipeline * refactor * fix * NFC change, refactor code * add _checked_lse/sink/bias_view * NFC refactor
…cks (#754) _run_dsl_graph returns the stats buffer allocated by make_dense_stats, whose shape is (B, H, S, 1); the pack_gqa features/qtrim tests compared it against the (B, H, S) reference and failed on the shape check. Squeeze at the comparison, like the strided-stats test does. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
) * frost(sdpa): derive THD token capacity from the view's element span (#613) The zero-host-read THD execute (#606/#608) derives the packed token extents host-side as numel() // token_stride. That is wrong on both edges for the buffers real integrations bind: - A non-packed VIEW — a K/V slice of a kv-interleaved [T, 2, H, D] record, the layout torch.nn.attention.varlen users produce by slicing a fused KV projection — holds T tokens but only T*H*D of the record's elements, so the derived extent HALVES and the TMA descriptors cut off half the tokens: silently wrong O on every such call (issue #613; also 40 upstream PyTorch test_varlen_attention failures through the python-API integration). - Deriving from the untyped storage instead over-claims into ALLOCATOR SLACK, which is not benign: rows between the real packed total and the extent are masked but still multiplied (P == 0 times V), so they must be FINITE — TMA zero-fill only covers rows at or beyond the extent. A slack row carrying NaN bit patterns poisons whole sequences through 0 * NaN. Fix: capacity = the largest T whose final token's ROW still fits in the buffer's own element SPAN (1 + sum((size-1)*stride)). The span is exact on both edges: flat capacity buffers give exactly their token capacity (no slack), and interleaved/gapped views give exactly T. Every row below the capacity lies in caller-provided finite elements; every row at or beyond it TMA-clips to zeros. One shared helper serves the SM100 f16 path and the SM120/FP8 _cap sites. Verified on SM100 (isolated env): the #613 kv-interleave repro 41% -> 0 mismatches (frost-served); test_sdpa_random_fwd_ragged_L0 5-seed slice 84/84 (no regressions); fp8 THD ragged slice green; the new deterministic regression test (fused-record K/V views vs packed binding, torch.equal) fails on develop and passes with the fix; upstream PyTorch test_varlen_attention returns from 100 pass / 69 fail to its 140 / 29 impl-identity baseline with the torch-ops stack applied on top. Fixes #613. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(sdpa): actually fuzz ragged token gaps in the randomized sweeps The seeded per-tensor token-gap draw (#516) lives in ExecConfig.fill_derived_fields and only fills strides left None — but RandomizationContext, which drives every test_sdpa_random_*_ragged sweep, explicitly assigned packed bshd strides in its ragged branch. Net effect: the randomized ragged fleet has NEVER bound a non-packed THD stride, and for packed buffers the numel()//token_stride capacity heuristic is exact — which is precisely why these sweeps stayed green while issue #613 (interleaved K/V views halving the TMA extent) shipped and had to be found through an external integration. Fix: the ragged branch leaves Q/K/V/O strides None and __call__ ends with fill_derived_fields() — one source of truth for the gap draw and its auto-packed fallbacks (cu / offset-multiplier forms #538, 1-byte dtypes #537). The head_major stats stride and the whole dense branch are untouched. Census over the fwd ragged L0 slice (84 configs): before, 0/84 drew a gap although each config's own rng_geom_seed hand-draws nonzero gaps; after, 84/84 draw gaps and ALL 84 would have failed under the old capacity formula. Verified on SM100 (cuDNN 9.26.0.33, CUDNN_FRONTEND_ENABLE_FROST_ENGINES=1): with the #613 fix the gapped fwd ragged L0 slice passes 84/84 (all frost-served) — with the pre-fix adapter swapped in it fails 80/84, i.e. this wiring alone would have caught #613 the day the heuristic merged. bwd ragged L0 slice 158/158, identical to the unwired control on the same lib (the backend serves every gapped gradient combination); ragged_unified_L1 24/24 and offset_multiplier_unified_L1 24/24 (cu / mult forms stay packed via the existing fallbacks — 20/20 each in the offline census); the stride-override unit test still passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sdpa): accept max_total_seq_len_q/kv on the forward SDPA node `sdpa_backward` has taken `max_total_seq_len_q/kv` since cuDNN 9.6; the forward node never did. That asymmetry is the root of a whole bug class. A ragged (THD) graph declares `(B, H, S_max, D)` plus a device-side ragged-offset tensor, so the packed token total is not expressible anywhere in the forward graph — and reading `cu_seqlens[-1]` host-side is exactly the D2H sync the zero-host-read THD execute (#552) exists to eliminate. The FROST forward path therefore has to INFER an upper bound on the token axis from the bound buffers' element span (#613/#706). That bound is memory-safe but loose, and looseness is not benign: rows between the real total and the extent are masked yet still multiplied (`P == 0` times V), so they must be FINITE. A caller that over-allocates and leaves the tail unwritten poisons whole tiles through `0 * NaN` (#624). Every framework already has this number — it is `q.shape[0]` in vLLM, SGLang, TransformerEngine, Megatron-Core, PyTorch and FlashInfer alike — and today it gets thrown away at the graph boundary. This lets callers declare it. - C++: `max_total_seq_len_q/kv` on `SDPA_attributes` with setters and serialization, mirroring `SDPA_backward_attributes`. Frontend-side only: like the backward twin it is never lowered to a backend attribute, so it cannot affect backend validation (#704). - Forward node validation rejects it on a non-ragged layout, mirroring backward's "only supported with packed layout". - pybind: `sdpa(..., max_total_seq_len_q=None, max_total_seq_len_kv=None)`. - FROST forward consumes it: the declared total is min'd against the buffer-derived capacity, so it can only TIGHTEN the extent, never widen it. A stale or wrong value cannot make a launch address memory the caller does not own — it can only make it address less. Both the SM100 f16 and the SM120/FP8 extent sites go through one helper. Effect on #624, measured on SM100 (bf16, cuDNN 9.26.0.33, FROST forced), `seq_lens=[200,150,47]` (total 397) bound into `(640, H, D)` buffers whose `[397, 640)` tail is NaN — only the tail fill differs between runs: undeclared: 201,728 NaNs in O (49.6%) declared: 0 NaNs, bit-identical to the zero-tail run Verified: new L0 regression test (asserts the clamp AND that the undeclared path still reaches the tail, so it tests the clamp rather than a benign shape); dense graph + attribute correctly rejected; the #613 interleaved-KV-views test and the gap-wired ragged L0 slice (84/84, all FROST-served) unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sdpa): expose max_total_seq_len_q/kv on sdpa_fp8 too Review follow-up. `PyGraph::sdpa_fp8` routes through `sdpa_internal`, so it already builds the same `SDPA_attributes` that now carries the packed totals -- only the entry point was missing them, and it hard-coded `py::none()` at the forwarding call. An FP8 THD caller therefore had no way to declare its totals even though the adapter side (`_thd_declared_total` at the SM100 f16 and SM120/FP8 extent sites) was already wired for them. Adds the two optional arguments to the declaration, the definition, the pybind binding and the docstring, and forwards them instead of `py::none()`. `sdpa_mxfp8` is deliberately left out: it does not go through `sdpa_internal` and builds `SDPA_fp8_attributes`, which has no such field, so covering it means extending that struct as well. Note the reviewer's stated motivation does not actually hold for FP8: the FP8/MXFP8 kernels already clamp their K/V descriptor extents to `cu_k[B]` device-side in `build_thd_meta_o_kv_descs_kernel`, so an unwritten K/V capacity tail is already TMA-unreachable there, and Q is the parallel dimension (a garbage Q row poisons only its own row, which is never stored). The change is still worth making for API symmetry and for exact rather than inferred extents. Test: `test_fp8_thd_declared_totals` runs the THD FP8 path with and without the declaration from the same seed and asserts O is bit-identical, plus the usual accuracy check against the reference. Verified: `test_sdpa_fwd_fp8_sm100.py` 61 passed; f16 THD tests 195 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(sdpa): expose max_total_seq_len_q/kv on sdpa_mxfp8 too Correcting my own note on the previous commit: I claimed `sdpa_mxfp8` was out of scope because it "builds `SDPA_fp8_attributes`, which has no such field". That is wrong — `SDPA_fp8_attributes` is a type ALIAS for `SDPA_attributes` (graph_properties.h), so the field has been there all along and the only gap was the pybind entry point. `sdpa_mxfp8` does not route through `sdpa_internal`, so it needed its own declaration, definition, attribute plumbing, binding and docstring — but no struct change. The MXFP8 forward row serves THD (`thd_d_shapes` covers the d128 kernel), and the adapter side (`_thd_declared_total`) was already shared, so this completes the forward family: `sdpa`, `sdpa_fp8` and `sdpa_mxfp8` all now accept the packed totals. Still missing, and genuinely needing a struct change: the FP8/MXFP8 BACKWARD nodes. `SDPA_fp8_backward_attributes` is a distinct class (not an alias) with no such field, so `sdpa_fp8_backward` / `sdpa_mxfp8_backward` cannot take the totals while plain `sdpa_backward` has since cuDNN 9.6. Tracked separately. Test: `test_mxfp8_thd_declared_totals` runs the MXFP8 THD path with and without the declaration from the same seed and asserts O is bit-identical, plus the usual accuracy and amax checks. Verified: `test_sdpa_fwd_mxfp8_sm100.py` THD selection 10 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#720) * frost(sdpa): run the KV split the heuristic chose, on the true cluster extent choose_split_kv computed a split and then nothing used it. Two defects, both on the delivery path rather than in the cost model: - _split_points returned [no_split, chosen], so the chosen split landed at plan[1]. build_plans() stops at the first entry that builds and execute() runs _plan_index, so a plain build ALWAYS took the unsplit plan; the split was reachable only through select_plan or an ALL-policy autotune. Return [chosen, no_split] instead — the split leads, and no-split stays reachable behind it. - The model was fed rows_per_tile = tile_m * cga, but an SM100 d128 cluster covers TILES_Q * TILE_M * CTA_MMA Q rows on its CTA pair — twice that. The doubled tile count reads a half-empty machine as full, so the chooser under-splits or declines to split at all. Use _pack_gqa_tile_q, the helper that already answers "Q rows one grid tile covers", and the same extent every test in test_split_kv_heuristic.py already assumed. Flipping the lead moved the split into the base knob set, which exposed a third: the "a split set rides the plain scheduler" coupling lived only in the splits[1:] runner-up loop, so a LEADING split inherited the derived LPT_L2 policy on causal graphs — unbuildable on SM120, which raises on split_kv > 1 under an LPT remap. The coupling now binds whichever leg leads, and scheduler runners ride an unsplit leg. The chooser itself is unchanged, so a grid that already fills the machine still does not split. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * frost(sdpa): one KV-split candidate list, and price the combine pass split_kv had two lists. choose_split_kv scored an implicit power-of-two ladder bounded by _SPLIT_KV_MAX, while _split_points projected the winner onto caps.split_kvs and returned usable[-1]. They agreed only because {1,2,4} was a prefix of the ladder; on any other domain the returned split was one the model had never scored. Separate the two roles that field was sharing. Capabilities.split_kvs becomes split_kv_supported, a boolean gate on whether the row wires the split path at all — mismatch() checks it in the block that already special-cases split_kv > 1, rather than in the uniform domain table, and it imposes no upper bound because the kernels have none. WHICH splits are worth scoring becomes split_kv_candidates(sm_count, kv_tiles): powers of two up to 2**ceil(log2(sm_count)), bounded by kv_tiles // _SPLIT_KV_MIN_TILES. choose_split_kv loops exactly that list; _SPLIT_KV_MAX, max_split and the usable[-1] snap are gone. A split launches two kernels, so cost(s) is now two latencies summed: cost(s) = waves(s) * (ceil(kv_tiles/s) + CTA_COST) + combine_waves * (s * COMBINE_COST) combine_waves = ceil(S_q*H_q*B / sm_count), because split_combine_sm100's grid is (S_q, H, B) — one block per output row, independent of s; only the per-block work grows with s. Both terms are latency, so the combine cannot double-count the parallelism the wave factor already divided out. Without it s reached the model only through waves(s), a step function, leaving a larger split free between wave boundaries. _B300_FIT is re-measured for the new model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…or layout (#693) * GGEMM+GLU+RHT+quant outputs column-wise RHT in ragged tensor layout Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Tim Moon <tmoon@nvidia.com> * Debug tests Loosen RHT tols since it can accumulate error from multiple BF16 casts. Skip relative error check when scales have been driven to zero (scales are verified separately from FP4 values). Signed-off-by: Tim Moon <tmoon@nvidia.com> --------- Signed-off-by: Tim Moon <tmoon@nvidia.com> Co-authored-by: Codex <noreply@openai.com>
…-trip (#729) get_knobs_for_engine() converts backend knob types into KnobType_t; a knob the mapping does not carry arrives as NOT_SET, and passing that knob back through create_execution_plan() fails convert_to_backend_knob_type with CUDNN_STATUS_INVALID_VALUE for every knob combination on that engine. CUDNN_KNOB_TYPE_TILE_CGA (id 26) is deprecated in the backend enum but some engines still report it, so any caller enumerating knobs and replaying explicit (engine, knobs) plans loses those engines entirely. Add the mapping in both directions and expose the enum value to the Python bindings. The numeric value is used on the backend side to avoid the deprecated-enum warning.
…758) choose_split_kv gained a required combine_rows keyword-only argument for the combine-pass cost term (#720), but the sm120 expected-split helper wasn't updated to match, unlike its sm100 sibling in test_sdpa_fwd_split_kv_sm100.py which already passes it. The squash-merge of #720 dropped the follow-up fix (yanzhuo607#1), so develop's sm120 CI (frost:rel:sdpa:sm120) is broken again: TypeError: choose_split_kv() missing 1 required keyword-only argument: 'combine_rows' Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…aster (fp32-accurate d_index_k at no extra cost) (#640) * dsa(indexer_backward): opt-in SM100 sparse backward v2 - 1.16-1.92x faster (fp32-accurate d_index_k at no extra cost) Add a backend string enum (backend="sm100_v2", legal values {"default", "sm100_v2"}) to IndexerBackward / indexer_backward_wrapper, selecting an SM100-only alternative GEMM stage (kernel 2) that keeps the exact 3-stage wrapper contract (kernel 1 score-grad precompute is shared, same in-place score consumption: attn_score is left holding exactly kernel 1 grad_signal for every supported sm_scale). The selector is keyword-only on indexer_backward_wrapper and appended last on IndexerBackward.__init__, so positional callers are unaffected. The default backend's kernels and dispatch are not modified; the one piece of shared code this touches is the wrapper's plan-cache key, which now also carries the tensor device and the three output dtypes for both backends (before, a plan built for one device or one output dtype could be handed back for another). The win is speed: 1.16-1.92x on kernel 2 across the supported envelope, with no dtype opt-in and no downstream cooperation. The same GEMM restructuring also removes the bf16 product rounding that dominates the default path's fp32 dK error and adds to its bf16-stored dQ error, so d_index_k comes out fp32-accurate at no extra cost for callers that keep it in fp32: * weights are upcast to fp32 in-register (exact) and the per-slot fp32 gradient matrix A = g * w is split into a two-term bf16 expansion (hi = bf16(A), lo = bf16(A - hi)) before the MMAs. Each individual bf16 x bf16 product is exact in the fp32 accumulator; the expansion itself carries ~16 of the 24 fp32 significand bits (not the correctly rounded A @ K), measured ~679x lower gradient-matrix representation error than the default single-bf16 rounding (rms-relative 1.66e-3 -> 2.45e-6 over 1e6 randn samples), * d_weights accumulated in fp32 and reduced deterministically in-CTA (bitwise run-to-run stable, as is d_index_q), * d_index_k accumulated with vectorized four-element fp32 atomics, the same numerics class as the default backend. Output dtype selects output precision: d_weights / d_index_k accept caller-supplied fp32 buffers which receive the fp32 accumulators directly -- the d_index_k accuracy gain requires them; the default bf16 outputs round back to the bf16 representation floor (documented in docs/fe-oss-apis/dsa.md, honest numbers for both in the test suite and PR). fp32 d_index_k is zeroed internally. After its first execute, execute() performs no further allocations and no dtype conversions on the host (kernel 1 + one dK zero-fill + kernel 2 + a cast only for bf16 d_index_k): the weights upcast happens in-register, sm_scale is a runtime kernel argument (sm_scale > 0 required and validated: the relu gate reads unscaled scores, equivalent to the default backend's gate on scaled scores for positive scales, except where the scaled score underflows to zero), and local per-batch top-k ids are masked against the per-batch S_k BEFORE the batch offset is applied, in-kernel -- a positive out-of-range local id contributes nothing instead of aliasing the next batch. The dynamic-ticket counter and the bf16-dK fp32 scratch are per-plan workspace, allocated on the first execute and resident on the plan's device; one plan serves one device (execute rejects indexer tensors from any other device before kernel 1 touches the score buffers) and executions of one plan must not overlap on the device (documented). For backend="sm100_v2" the wrapper additionally keys its plan cache on the resolved stream -- plus the calling thread's id for cudaStreamPerThread, the one CUDA handle that denotes a different stream in every host thread -- so concurrent wrapper use from different explicit streams, from different ambient stream contexts, and from different threads under cudaStreamPerThread each get a private plan. check_support validates the full metadata matrix (cross-tensor shapes, output dtypes, device, contiguity) before kernel 1 mutates the score buffers. The kernel is a persistent dynamic-ticket-scheduled gather-GEMM (one CTA per SM, 16 warps: TMA / MMA / ticket-writer / S-epilogue / gather+restage / dK-reduce warp specialization). The cross-row metadata WAR hazard on the sIdx/sG parity double buffer is explicitly barriered (2-stage PipelineAsync over the parity slots) below the tile count at which the K->S->A->DK acquire chain covers it (topk < 1024); at topk >= 1024 the barrier is constexpr-eliminated. The ticket ring is placed in the alignment-padding hole between the dW partials and the 1024-aligned sQ buffer, so shared memory tops out at exactly the SM100 232448 B dynamic limit at topk == 2048 (16 tiles/row), the largest supported shape. Measured on B200 (sm_100a), SK=4096, one seeded construction (q/k scaled 0.1, w scaled 0.5 in bf16, uniform-random valid top-k ids, softmax-distributed fp32 grad signal, consumed bit-identically by both backends). Caliber: kernel 2 only (kernel 1 is shared and identical), nsys pure-kernel medians recomputed per instance from the CUPTI_ACTIVITY_KIND_KERNEL rows of the sqlite export (nsys profile -t cuda-sw, N=60 per backend, both backends interleaved in one cudaProfilerApi window). A torch.profiler CUPTI capture in the same session agrees to within 0.20% on every ratio: S=8192 topk=128 : 242.94 us vs 466.55 us default -> 1.92x S=8192 topk=256 : 431.48 us vs 677.45 us default -> 1.57x S=8192 topk=384 : 532.95 us vs 815.44 us default -> 1.53x S=8192 topk=512 : 679.86 us vs 969.23 us default -> 1.43x S=8192 topk=640 : 816.84 us vs 1040.27 us default -> 1.27x S=8192 topk=1024: 1294.97 us vs 1693.57 us default -> 1.31x S=8192 topk=1536: 1948.90 us vs 2415.69 us default -> 1.24x S=8192 topk=2048: 2671.70 us vs 3137.57 us default -> 1.17x S=4096 topk=128 : 127.33 us vs 236.16 us default -> 1.85x S=4096 topk=256 : 222.54 us vs 343.19 us default -> 1.54x S=4096 topk=512 : 352.03 us vs 492.52 us default -> 1.40x S=4096 topk=1024: 668.29 us vs 855.59 us default -> 1.28x S=4096 topk=1536: 1000.11 us vs 1221.65 us default -> 1.22x S=4096 topk=2048: 1373.67 us vs 1590.62 us default -> 1.16x A repeat capture of S=8192/topk=1024 gave 1295.00 vs 1693.80 us (1.3079x vs 1.3078x), which sets the run-to-run scale of these ratios. Public-wrapper steady state (all GPU kernels per call, real kernel 1, same nsys caliber, N=60) at S=8192/topk=1024: 1337.54 us vs 1750.93 us default -> 1.31x with bf16 outputs, and 1335.64 us vs 1744.57 us -> 1.31x with fp32 d_weights/d_index_k; CUDA-event medians of the whole wrapper call agree (1336.40 vs 1747.10 -> 1.31x bf16, 1335.30 vs 1740.90 -> 1.30x fp32). The kernel-2 ratio carries through because the auxiliary work (kernel 1 at ~48-49 us, backend-invariant, plus the dK fill/cast) is ~4.1% of the v2 call and ~3.1% of the default's. Accuracy, rms-relative error vs a strict fp64 oracle consuming the identical (bit-shared) grad signal; B=1, S=8192, S_k=4096, sm_scale=1.0; one caliber, ratio of mean errors over 5 seeds. With fp32 d_index_k both backends emit real fp32, so the difference is purely the hi/lo expansion of A: d_index_k is 9.9-48.8x closer across topk {128,256,384,512,640,1024,2048} (13.4x at topk=1024). The ratio is itself run-variable -- v2's fp32-atomic d_index_k error moves run to run while the default sits pinned at its single-bf16-A floor (1.66e-3 to 1.68e-3) -- so the aggregate is a band, not a constant: a 20-seed soak at topk 128/256/384 lands at 14.2-19.0x where the 5-seed sweep gave 15.9-48.8x. The robust claim is about an order of magnitude across the envelope, never the peak. d_index_q is bf16-only in both and v2 sits flat at the bf16 output floor (1.66e-3) while the default is 1.16-1.38x above it (the gap shrinks as topk grows). d_weights follows the same formula in both backends, but the default hard-rounds it to bf16 at the store regardless of buffer dtype, so with an fp32 buffer v2's error is ~1.55e4x smaller: an output-dtype effect, not a compute-precision claim (at matched bf16 output the two agree to an error ratio of 1.000000001, though not bitwise). At matched bf16 outputs the gains reduce to the bf16 floor: d_index_k 1.41x, d_weights 1.000x, d_index_q 1.16-1.38x. Supported envelope (request-or-fail, raises cleanly otherwise): SM100 capability exactly (10, 0), H == 64, D == 128, block_I == 128, topk % 128 == 0 with 128 <= topk <= 2048, sm_scale > 0, bf16 d_index_q, bf16/fp32 d_weights and d_index_k, contiguous same-device tensors. The [128, 2048] envelope covers 1-2 tiles/row (topk 128/256): the K/S pipelines and the MMA lookahead min-clamp to the tile count so a whole row is resident at once, while a_stage/dk_stage stay 2 (the odd-tile and paired dK drains both reference two dK accumulators). At topk >= 384 the clamps are all no-ops, so the schedule is the unclamped one. Low-topk validation: fp64-oracle 5-seed sweep + 20-seed soak at topk 128/256/384 (d_weights output-dtype gain ~1.55e4x, d_index_k 14.2-19.0x closer over 20 seeds, all finite); kernel 2 is 1.53-1.92x faster than the default backend at topk 128/256/384. Tests: v2 wrapper parametrized over topk {128, 256, 384, 512, 640, 1024, 2048}; full-valid topk=2048; envelope rejection (8 cases); B=2 local/global id parity + positive/negative OOB semantics; non-unit sm_scale + scratch parity with the default backend; fp32-output accuracy vs a strict fp64 oracle + bitwise determinism; two-stream interleaved execution vs serial references; stream=None under distinct ambient stream contexts; cudaStreamPerThread from two host threads (per-thread plans, barrier-forced overlap); two-device same-shape default-stream execution (per-device plans, interleaved, cross-device bitwise dq/dw parity, wrong-device rejection without score-buffer mutation). Errors inside the declared envelope fail the suite (no skip conversion past the SM100 gate). Suite: 42 passed on a 2-GPU B200 host -- 31 in test_DSA_indexer_backward.py plus test_DSA_dense_indexer_backward.py (2), test_api_signature_parity.py (4) and test_import_boundaries.py (5). Signed-off-by: zky <kaiyue.zhou@z.ai> * dsa(indexer_backward): make v2 tests honour --dsa-s_kv and B > 1 Two review findings in the v2 test suite, both about tests that silently assumed the default DSA test shape: - ``full_valid_topk2048`` needs at least topk keys per batch for its "every slot is valid" premise; ``--dsa-s_kv 1024`` made the setup assertion fail instead of the test adapting. Raise s_kv locally, the same way the low-tile metadata WAR test already raises s_q/s_kv. - the ``index_k_dims`` rejection case reshaped index_k to ``(2, s_kv, D/2)``, which only has the right element count at B == 1; with ``--dsa-b 2`` the reshape itself raised RuntimeError before the API could reject the bad rank. Scale dim 0 by b so the case keeps testing what it means to test. Signed-off-by: zky <kaiyue.zhou@z.ai> * test(dsa): keep the v2 indexer-backward cases out of the L0 smoke run Every topk in the v2 sweep JITs its own SM100 kernel variant (20-60 s apiece), so the cases added for the v2 backend put 30 tests and ~515 s of mostly compile time into the default ``-m L0`` run, against test/AGENTS.md ("L0 must stay fast (default CI smoke); big parameter sweeps go to higher levels"). Keep one numeric point at L0 - topk=512, which exercises the multi-I-block path - together with the zero-compile envelope, dispatch and multi-device/-stream checks, and move the sweep and the seven compile-heavy scenario tests to L1. The topk list carries its levels per-parameter, matching test_gemm_proj_rope_mxfp8.py. For this file, -m L0 goes from 31 cases / 530 s to 17 cases / 53 s, and 16 s of what is left is the pre-existing default-backend test. -m L1 picks up the other 14 cases; no case is dropped. Signed-off-by: zky <kaiyue.zhou@z.ai> * dsa(indexer_backward): stop pinning the v2 fp32 dK scratch to the plan A BF16 ``d_index_k`` needs a ``B * S_k * D`` fp32 accumulator for the atomics; v2 kept it in the per-plan workspace. That makes a cached plan hold 4 * B * S_k * D bytes for as long as the cache lives - 64 MiB at B=1, S_k=128K, D=128, and the wrapper caches one plan per (device, stream), so a caller that rotates streams multiplies it. Nothing was gained by caching it: the buffer has to be re-zeroed on every execute either way. Take it from the caching allocator per call instead. Same-size, same-stream allocations come back from the pool, so steady state does no device allocation at all, and the buffer becomes reclaimable via ``empty_cache()`` instead of staying pinned to the plan. Measured over 20 steady-state B=1 / S_k=4096 / topk=1024 calls on B200, with and without ``expandable_segments``: segment.all.allocated delta 0, num_device_alloc 0, num_alloc_retries 0, num_sync_all_streams 0, reserved bytes flat; the allocation itself costs 2.9 us of host time per call (6.7 vs 3.8 us for the zero_() that both variants pay) and synchronizes nothing. The fp32 ``d_index_k`` path is untouched: it still accumulates straight into the caller's buffer with no scratch at all. The dynamic-ticket counter stays per-plan workspace - it is 8 bytes, and the kernel's self-reset contract depends on it persisting across launches. Signed-off-by: zky <kaiyue.zhou@z.ai> * dsa(indexer_backward): fix docs the per-call dK scratch invalidated, guard two oracle call sites The previous commit moved the fp32 dK accumulator out of the plan workspace, which left five places claiming it is still per-plan state: the execute-path comment ("steady-state execute() allocates nothing"), the two stream-keying tests and the multi-device test, whose docstrings and assertion messages named the dK scratch as the reason one plan must not serve two streams. The reason is the self-resetting ticket counter alone; the scratch no longer participates. Also guard the two remaining ``_fp64_oracle`` call sites with ``cfg["b"] == 1``, the same guard ``test_DSA_indexer_backward_wrapper_v2`` already uses. The oracle recomputes at B == 1 only, so a ``--dsa-b`` override turned those two cases into hard failures instead of running everything but the oracle bands. No behavior change outside tests. Signed-off-by: zky <kaiyue.zhou@z.ai> * test(dsa): form the indexer-backward reference grad signal analytically at the kernel clip constant ref_indexer_backward modeled the clipped-log KL by autograd through log(predict.clamp(min=float32.tiny)), while the score-grad kernel clips at CLIP_PROB_MIN = exp(-100) (as ref_dense_indexer_backward already does). The two clip masks diverge on every slot whose predict falls in [exp(-100), float32.tiny) - and on sharply peaked predict distributions (large valid top-k over a wide score range, e.g. topk=2048 with s_kv=1024, where ~33 slots/row land in that window) the mask difference alone measures ~0.55 rms_rel on all three gradients, i.e. the whole 0.55 tolerance budget is spent on a reference-modeling artifact rather than kernel error. Verified in fp64: the eps-window effect with everything else identical reproduces the observed deviation to four digits (0.5591), both backends sit within 1.7e-3 of the fp64 recompute of their own contract, and v2 == default to the fourth decimal on every metric. The reference cannot simply adopt exp(-100) inside the clamp: the log backward's 1/predict overflows fp32 for subnormal predict (which is why the tiny clamp was there). Instead form the grad signal analytically from the supplied fp32 index_score - the exact tensor kernel 1 reads (recomputing the operand through the reference's bf16 forward would flush kernel-eligible subnormal probabilities to zero and perturb the signal everywhere) - with exactly kernel 1's formula, g = -max(target, eps) * [predict >= eps], signal = g - predict * sum(g), and backpropagate it through the recomputed scores via grad_outputs, mirroring ref_dense_indexer_backward. Measured effect (B200, seeds 0-9): the previously marginal wrapper_v2[...-2048] with --dsa-s_kv 1024 drops from rms_rel 0.51-0.57 (straddling the 0.55 gate, 4/10 seeds failing) to <= 0.0045; every other config in the sweep drops from 0.24-0.60 to <= 0.0045 with cosine ~1.0, for both the default and the sm100_v2 backends. All L0/L1 suites pass with and without --dsa-b 2 / --dsa-s_kv 1024. Signed-off-by: zky <kaiyue.zhou@z.ai> --------- Signed-off-by: zky <kaiyue.zhou@z.ai>
…l < 4.8 (#764) * Gate the packed-FP4 wgrad layout workaround on cutlass-dsl < 4.8 Public cutlass-dsl wheels before 4.8 interpret packed sub-byte from_dlpack layouts in byte units, so BlockScaledMoEGroupedGemmWgradKernel recasts the FP4 A/B layouts to element units. The 4.8.0a0 public wheels adopted the internal wheel's native sub-byte layout semantics, so the recast now double-corrects: the MMA consumes byte-aliased data and every fp4 wgrad test fails with ~94% mismatched output (and occasionally an illegal memory access from the corrupted discrete-pointer TMA path). Gate the workaround on the cutlass-dsl version instead of only on internal-wheel presence. Verified on sm100 (torch 2.13, dense compile_execute fp4 sf_e4m3, mma 128x128/256x128, cluster 1x1/2x1): bit-exact vs the torch reference on both cutlass-dsl 4.7.0 (gate on) and 4.8.0a0+20260823210556.ac70faa (gate off); before this change 4.8 fails with ~94% mismatch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Trim the workaround-gate comment Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Apply black to test_grouped_gemm_glu_hadamard_quant.py The pre-commit GitHub Action runs black on --all-files and this pre-existing file was not black-clean, failing the check for every PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
cuDNN Frontend v1.28.0 Release Notes
cuDNN Frontend v1.28.0 is the recommended version for cuDNN 9.25.1 and later releases.
New:
cudnn.fla— a drop-in accelerator for flash-linear-attention 🚀 🚀cudnn.fla(#596) monkeypatches the flash-linear-attention ops that cuDNN can serve onto cuDNN's Blackwell (SM100) kernels, with a transparent fallback to FLA everywhere else, so results never change:chunk_gated_delta_rule) — the GDN convention (log-space decayg, post-sigmoidbeta, GVA whereHV > H) mapped onto cuDNN's native op, reproducing the fused-layer knobsuse_gate_in_kernel,use_beta_sigmoid_in_kernel, anduse_qk_l2norm_in_kernel.chunk_kda) — channel-wise gate plus scalar beta, l2norm forward and backward through cuDNN. BF16 only; FP16 declines and falls back.GatedMLP(Add an opt-in cuDNN FLA GatedMLP shim #686) — an opt-in adapter (accelerate_fla(targets="gated_mlp")) backed bycudnn.gemm.ops.swiglu_mlp. The patch registry is target-selective, incremental, idempotent, and independently restorable viarestore_fla(targets=...)/is_accelerated(target).Configurations cuDNN cannot serve raise
cudnnGraphNotSupportedError/NotImplementedErrorand fall back to FLA — never a wrong answer. Correctness is pinned bytest/python/linear_attention/test_fla_compat.py, which requires cuDNN to match FLA within FLA's own BF16 noise on the output and every gradient.Underneath, the linear-attention stack gained KDA and GDN-2 backward support (#556), a safe beta guard for GDN-2 (#722), packed-QKV views for native GDN (#685), state-layout and convention alignment with FLA/FlashInfer plus a context/IMA fix (#644), and successive CPU-overhead and instruction-cache/numerics passes on the FROST linear-attention kernels (#616, #708, #759). See docs/fe-oss-apis/fla.md.
New: JAX support across the CuTeDSL GEMM APIs 🚀 🚀
The GEMM CuTeDSL APIs are now type-erased (#529): every API under
python/cudnn/gemm/cutedsl/accepts JAX arrays alongside torch tensors, and the modules import and resolve their public symbols without torch installed — torch is imported only when torch tensors or dtypes are passed, and JAX only when JAX arrays are.On top of that,
cudnn.jax.call(#553) wraps CuTeDSL's native JAX integration (cutlass.jax.cutlass_call) and gives every JAX-reachable GEMM API ajax.jitentry point —gemm_amax,gemm_swiglu(including blockscaled MXFP8),gemm_srelu,gemm_dsrelu,gemm_proj_rope_mxfp8(both BF16 and MXFP8 input paths), and the grouped and discrete-grouped families in their pointer-array modes. APIs without a JAX data path raise a clear error rather than failing obscurely. JAX outputs the kernel already writes are no longer zero-initialized first (#631).New: First-class
cudnn.Handle🚀 🚀cudnn.create_handle()now returns aHandleobject that owns{backend handle, device, stream}instead of a bare int (#612). The per-handle state that had accreted as module-global side tables and per-engine device queries — the stream cache, and the three parallel device stacks used by the backend handle,pygraph, and FROST — unify behindHandle.streamandHandle.device. This matters because the Python engines (FROST, CuTeDSL, linear attention) need a device and a stream, not acudnnHandle_t.Backward compatibility is transparent for normal use: every handle-taking API (
execute,set_stream,get_stream,destroy_handle, all graph methods) is Handle-aware, extracting the backend handle explicitly at each named handoff. Design notes and a full call-site inventory are in docs/handle_first_class_design.md.New: GNN simple aggregation 🚀 🚀
cudnn.gnn.agg_simple(#647) exposes the cuDNN GNN AggSimple backend as a PyTorch custom operator with autograd, fake-tensor, andtorch.compilesupport, handling graph validation and backend invocation so callers never touch the low-level GNN structures. Requires cuDNN 9.26 or newer and compute capability 8.0+; not supported on Windows. See docs/operations/gnn/agg_simple.md.New: FROST SDPA on Ampere, Ada/Blackwell-consumer, and Rubin 🚀 🚀
The FROST engine family introduced in v1.27.0 now spans every architecture the frontend targets.
cudnn.sdpaadapters (SM80 (A100) SDPA: FROST engines + cudnn.sdpa adapters #493), later ported to plan-time compilation withTemplateParamskernels andsym_intTHD extents ([SDPA] SM80 fwd: TemplateParams kernels, plan-time compile, sym_int THD extents #689). Previously the manifest had no engine below SM100.d_qk=192/d_v=128support (frost(sdpa): add mixed head-dim support for SM120 frost sdpa_fwd engine #507), and a backward engine (Add SM120 FROST SDPA backward engine (sdpa_bwd_sm120) #486) extended to all head sizes ≤256 including d192/d256, non-compact layouts and deterministic dQ (frost(sdpa): SM120 backward — all head sizes (≤256), d192/d256, non-compact layouts, deterministic dQ #533), sliding-window attention (Add sliding-window attention support to the SM120 FROST SDPA backward engine #505), GQA/MQA, padding masks, right-band widening and sink-token gradients (frost(sdpa): GQA/MQA, padding mask, right-band widening, and sink-token gradients for the SM120 f16 backward #557), deterministic 2-kernel mode and dBias (add det 2k and dbias support for sm120 sdpa bwd #707), and native service of declared strided layouts (frost(sdpa): serve declared layouts natively in the SM120 backward — strided stats/io, TMA zero-fill head-dim envelope #666).has_lsespecialization and a static SMEM guard (sdpa fp8 sm107: port the has_lse specialization; add a static SMEM guard #579), a fused LDTM row-max and row-sum-in-MMA epilogue (sdpa fp8 sm107: fused LDTM row-max + row-sum-in-MMA #580), and thesoftmax_precisionknob axis lit up with an F16x2 exponent on the d128 sibling (sdpa fp8: light up the softmax_precision knob axis — f16x2 exponent on the d128 SM107 sibling #651).sdpa_bwdgains MLA support (feat(frost): add mla support for sdpa bwd #643).Head-dim envelopes and engine identity. Per-tensor FP8 now serves the dense head-dim ENVELOPE through the same TMA zero-padding path the F16/BF16 flavors use, and the engine table collapses to one engine per architecture × dtype family — head dims became a lowering concern (kernel-flavor selection) rather than an engine identity (#587).
Ragged / THD, with zero host reads. THD execute on SM100 (#606) and SM120 (#608) now performs no device-to-host reads at all — no
.tolist()syncs, no host cumsum, no pageable H2D — building its metadata on device against a plan-time envelope grid, which makes the path CUDA-graph capturable (issue #552, with #543 binding host prep to the launch stream and plan-time-only THD compile keys). The FP8/MXFP8 SM100/SM107 engines were moved onto the same envelope design (#648) and the legacy pre-envelope THD leg removed (#622). Supporting work: native THD declared-stride support in the SM100/SM120 F16 forward kernels (#526), thecu_seq_lenprefix-sum length form (#522), ragged stats on SM100 (#512) and SM120 (#508), and raggedS_kvtails served on the F16 rows via synthesized padding (#581).Masking, splitting, and heuristics. Forward heuristics can now recommend the same engine several times under different complete knob assignments, which makes split-KV graph-reachable for the first time (#692);
recommend()is a pure, backend-blind entry point that autotuners can call with hand-built graph facts. Split-KV also landed for the SM100/SM120 prefill kernels (#658), the KV split the heuristic chose now runs on the true cluster shape (#720), andpack_gqais supported (#709). On masking, SM100 gained causal right-band widening with per-sequence THD bottom-right diagonals (#485), bottom-right diagonal plus sliding window (#584) — after which thebottom_right_with_swanotch was retired because every row serves BR + SWA (#623) — and full causal mask support (#498).Other FROST SDPA work. SM100 MXFP8 for d192/d128 (#661); dense LSE written directly to non-contiguous, dense-compatible layouts (#712); an execute path made async where it can be, no longer re-deriving build-time facts (#570); FP8 scales folded in-kernel with a baked 2⁴ P-cast bias, removing
Scale_Sfrom below the graph (#619); theAmax_Soutput dropped from the FP8 kernels (#602); ahas_lsespecialization for the FP8/MXFP8 SM100 flavors (#574); and a strict LSE/sink/seq-lens execute contract with notorch.emptyinexecute(#484).Updates to Graph API 🚀 🚀
SDPA
max_total_seq_len_q/max_total_seq_len_kvon the forward node (feat(sdpa): accept max_total_seq_len_q/kv on the forward SDPA node #740).sdpa_backwardhas accepted these since cuDNN 9.6; the forward node never did, so a ragged graph could not express its packed token total and the FROST forward path had to infer a loose upper bound from the bound buffers' element span. A loose bound is memory-safe but not benign — masked rows are still multiplied, so an over-allocated, unwritten tail poisons whole tiles through0 * NaN. Every framework already holds this number (q.shape[0]in vLLM, SGLang, TransformerEngine, Megatron-Core, PyTorch, FlashInfer); it can now be declared.Statswith a narrower dtype — explicitly, or implicitly by leaving it unset with a non-FP32io_data_type— built and executed fine, and the kernel then wrote FP32 rows past the end of the caller's buffer, surfacing as silent corruption of adjacent allocations, illegal memory accesses, or driver launch failures.Statsis now set to FLOAT at creation, an unset dtype defaults to FLOAT, and a narrower declared dtype is rejected.post_validate_node(fix(sdpa): move the pre-9.26 Stats packed-BHSD check to post_validate_node #642).Capabilities.bottom_right_padded_seq_qwas retiled (frost(sdpa): retire Capabilities.bottom_right_padded_seq_q #683).Serialization and plan management
Graph::deserialize(blob)overload (andpygraph.deserialize(blob)) rehydrates a serialized execution plan from aDevicePropertiesdescriptor instead of acudnnHandle_t, enabling ahead-of-time compilation: build and serialize a plan on a GPU node, deserialize it later where no CUDA context or cuDNN handle exists. Requires cuDNN ≥ 9.8 at compile and runtime; the API compiles on older headers and returns a runtime error.Tensor_attributes::alignmentis now serialized (fix : serialize Tensor_attributes::alignment #564).CUDNN_KNOB_TYPE_TILE_CGAis mapped (Map CUDNN_KNOB_TYPE_TILE_CGA so knob queries and explicit plans round-trip #729). An engine reporting a knob the mapping did not carry returned it asNOT_SET, and feeding that back throughcreate_execution_plan()failed for every knob combination on that engine — making the engine impossible to drive through the explicit-plan API at all.KnobType_t::TILE_CGAis added and mapped in both directions, and exposed to the Python bindings.Python dispatch
propose_plans, theRouter, andheuristics_sortare deleted. An engine cannot rank plans — it sees neither its siblings nor the backend's entries — and all four in-treepropose_planswere the base class's default copied verbatim.create_execution_plans()now goes straight toheuristics.rank(...), which delegates to each family'srecommend().graph.execute(). This closes three cases where the same public call answered differently depending on which plan the heuristics happened to pick — bare device addresses,override_shapeson a FROST plan, and related identity-dependent behavior.Operations
cudnn.ops.fft_causal_conv1d(x, weight)following cuhyena's medium/long selection, padding, trimming and autograd behavior, preserved long-forward reserve space for the matching backward, C++ samples, notebooks, and docs. SM107 causal conv1d tests are skipped before cuDNN 9.26 (Skip SM107 causal conv1d samples before cuDNN 9.26 #632).Open-Source Kernels 🚀 🚀
GEMM and MoE fusions
cudnn.gemm.ops.swiglu_mlp(Add a dense BF16 SwiGLU MLP autograd op with fused forward and dSwiGLU backward #609) — a dense BF16 autograd op forout = (silu(x @ Wg.T) * (x @ Wu.T)) @ Wd.Ton SM100. The forward gate/up GEMMs, SiLU, and multiply run as one FORT-native runtime-fusion kernel that also emitsgateandup, avoiding two recompute GEMMs in training; the backward fusesdh = dout @ Wdwith the two-output dSwiGLU epilogue in one FROST kernel, keepingdhon chip. Unsupported layouts, architectures, and missing optional dependencies decline to nvjet plus pointwise.dprobfor grouped GEMM dsrelu (feat(determinism): grouped gemm dsrelu deterministic dprob #521) and FP32 row-scaled FP4 grouped GEMM (Support FP32 output and dynamic M in row-scaled FP4 grouped GEMM #461).grouped_gemm_wrapper_sm100118.1 → 39.6 µs,grouped_gemm_glu_wrapper_sm100161.1 → 40.6 µs,grouped_gemm_dglu_wrapper_sm100185.0 → 51.1 µs (kernel time 18.4 µs).elect_onecompilation hint optimized (optimze-elect-one-compilation-hint #504).DSA (DeepSeek Sparse Attention)
backend="sm100_v2", a drop-in for the SM100 sparse indexer backward that is 1.16–1.92× faster on the GEMM stage (1.92× at topk=128, 1.31× at topk=1024, 1.17× at topk=2048; 1.31× end-to-end through the public wrapper at S=8192/topk=1024). A two-term BF16 hi/lo expansion ofA = g·wadditionally keepsd_index_kFP32-accurate at ~no cost for consumers that keepindex_kin FP32. Scope is SM100 exactly, H=64, D=128, topk ∈ [128, 2048] in multiples of 128,sm_scale > 0, request-or-fail with no silent fallback; the default backend is untouched.d_qk=576,d_v=512BF16 (Add SM100 H16 DSA backward specialization #664).indexer_backwardnow validates the output and plan signature before kernel 1 on the default SM100/SM90 backends (dsa(indexer_backward): validate output & plan signature before the score-grad precompute (default SM100/SM90) #572), andrange_constexprwas restored in eightkernel_gemmepilogue loops (dsa(indexer_backward): restorerange_constexprin 8kernel_gemmepilogue loops #549).Block-sparse attention (BSA)
block_sparse_attention_fp8_forwardAPI that quantizes contiguous BF16 BHSD inputs to FP8 E4M3 internally using the Sage recipe and returns contiguous BF16, gated on CUTLASS DSL 4.6.1 at runtime. Covers SM100/SM103 (blk64, with automatic split-KV selection) and a dedicated SM120 kernel with sequence tails, fixed or variable sparse counts, and batchedblock_sizeslayouts. Persistent CLC scheduling now works together with split-KV: the scheduler's work-tile mapping explicitly encodes and decodes the split dimension.CSA (Compressor)
ratio=2(CSA compressor: extend the validated ratio envelope to ratio=2 #710) —ratio ∈ {2, 4, 128}withcoff ∈ {1, 2}, the configuration used in production training for the model family this operation serves. No kernel changes; the previous gate encoded validation scope, not a kernel limitation. Review-response fixups for the ratio=128 kernels landed in CSA compressor: review-response fixups for the ratio=128 kernels (follow-up to #427) #452.Toolchain
nvidia-cutlass-dsl≥ 4.7.0 and check the version at support time, declining rather than failing when the installed DSL is older; the package itself is deliberately not pinned to that floor so it stays compatible with consumers holding the DSL back. The packed-FP4 wgrad layout workaround is now gated on cutlass-dsl < 4.8 ([1.28.0-rc] Gate the packed-FP4 wgrad layout workaround on cutlass-dsl < 4.8 #764).Tooling, CI, and Build ✨✨
pre-commitnow runs as a GitHub Actions job.merge-requirementsjob fails while a PR has no Milestone or is not on any Project board, with a matching PR-template checkbox. Bot-authored PRs and PRs labeledcat-routine-updateare exempt, and a failed Projects lookup reports a clear error rather than a false pass.setup.pydefaults to a parallel extension build (build: default to a parallel extension build in setup.py #565), and a-Werrorunused-parameter build break ininit_gnn_submodulewas fixed (python: fix -Werror unused-parameter build break in init_gnn_submodule (pre-9.26 headers) #728).Samples, Benchmarks, and Tests 📊
benchmark/attention_inference/measures attention as served, in two phases: context (TFLOPS; full prefill and chunked prefill of 512/1024-token chunks against 64k/128k caches, bottom-right causal) and generation (GB/s and % of memory SOL;q_tokens = 1 + MTPfor MTP 0–3 against a 128k cache). Two backends are swept and charted —cudnnon native backend engines, andcudnn_ossplanned withheur_mode.OPENSOURCEplusCUDNN_FRONTEND_ENABLE_FROST_ENGINES=1so only the frontend's open-source engines may serve it, recording the winning plan per case.QwenImageTransformer2DModel. These exercise the merged production paths: packed-QKV GDN (Fix packed-QKV views in the cuDNN FLA GDN shim #685), thecudnn.flaGatedMLP shim (Add an opt-in cuDNN FLA GatedMLP shim #686), and the backend-only public d256 SDPA after SDPA: drop the legacy standalone d=256 fwd/bwd stacks; port SM80 forward to the SdpaFwdDsl adapter path #682.cudnn_oss(FROST) backend with unified sustained-clock SOL and peak lines (benchmark/sdpa: add cudnn_oss (FROST) backend, sustained-clock SOL + peak lines; refresh gb200/gb300 and add RTX PRO 6000 results #597), FA4 auto split-KV enabled by default (benchmark(sdpa): enable FA4 auto split-KV by default (num_splits=0) #607), a Qwen3-VL vision-encoder (ViT) config with GB300 results (benchmark: add Qwen3-VL vision-encoder (ViT) SDPA config + GB300 results #598, benchmark(sdpa): qwen3vl_vit writes to results/ like every other config #629), SM-clock sampling on the GPU the benchmark actually runs on (benchmark: fix peak-MMA/SOL clock sampling under CUDA_VISIBLE_DEVICES (peak-MMA line collapse on multi-GPU nodes) #699), and an Ampere (sm80) row in the peak-MMA table (benchmark: add the Ampere (sm80) row to the peak-MMA table #715). GDN benchmarking was added in Add GDN benchmarking #501.test_mhas_v2(test(sdpa): fuzz per-tensor ragged token-stride gaps in test_mhas_v2 #516) and sink tokens are drawn in the ragged backward suites (test_mhas_v2: draw sink tokens in the ragged bwd suites #630); DSA comparisons use only the effective top-k slice (test(dsa): compare only the effective top-k slice #672); render-only tests were removed (remove-render-only-tests #688); samples skip an unsupported case and drop an invalidcudaGraphDestroy(samples: skip unsupported test case, remove invalid cudaGraphDestroy #734); the02_low_level_apinotebook uses an FP32Statstensor (samples: use FP32 Stats tensor in 02_low_level_api notebook #701).Bug Fixes 🐛
SDPA
sdpa_backwardgraph that setmax_total_seq_len_qsilently returned all-zero dQ/dK/dV whenStatswas head-major — the forwardOandstatswere correct and no error was raised, so this corrupted training without ever surfacing. Head-major[h, total_q]is FlashAttention's and PyTorch varlen'ssoftmax_lselayout, i.e. exactly the integrations that would reach formax_total_seq_lenin the first place. Present unchanged in 9.22 through 9.26 on both sm90 and sm100.seq_len_kvsweeps (sdpa: fix SM100 frost zero-KV cluster deadlock + enable zero-length seq_len_kv sweeps #575).O-descriptor row stride on the SM100 FP8 path (sdpa fp8 sm100: fix THD O-descriptor row stride (latent) #577).Python and device handling
ensure_current_contextalso returned as soon as any context was current, so a thread bound to another GPU's context kept it. It now resolves the target context instead of accepting the incumbent. The missingensure_current_contextimports introduced with First-class cudnn.Handle (create_handle returns an object owning {backend handle, device, stream}) #612 were added in python: add the ensure_current_context #612 imports but never defined #638.probe()survives a graph capture (Tell DLPack we are only reading, so probe() survives a graph capture #506).Kernels
test_grouped_gemm_quanttests failing withCUDA_LAUNCH_INVALID_CONFIG:_compute_stagespicked its A/B pipeline stage count from a byte-sum model that ignored the 1024-byte alignment padding ofSharedStorage, so the launch requested 335,872 bytes of dynamic SMEM against an sm_107a cap of 334,848.is_exclusiveguard (Fix ci breakage for missing protection to is_exclusive #605).Acknowledgements 🙏
Thanks to everyone who contributed to this release:
@Adnios, @adshen, @Anerudhan, @Aneureka, @brandonfzhang, @Butterfingrz, @dextero, @egilliam-nv, @filaretov, @GarlGuo, @harryzhou2000, @hwanseoc, @hxbai, @icavan, @jhjpark, @jiayus-nvidia, @kainzhong, @msalasooNV, @pmdavies-nv, @sraman-rgb, @timmoon10, @tingyu66, @tp5uiuc, @vasunvidia, @vedaanta, @YangXu1990uiuc, @yanqinz2, @yanzhuo607, @yeliu-oss, @yihuawei, @ZhiyuLi-Nvidia, @zianglih, @zkyue, Ali Hassani, Shreya Gaur, huangyitong.hyt@alibaba-inc.com and wenting.swt@alibaba-inc.com.