frost(sdpa): SM120 backward — all head sizes (≤256), d192/d256, non-compact layouts, deterministic dQ - #533
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughSM120/SM121 SDPA backward support now includes deterministic execution, dense-flex layouts, padded head dimensions through 256, workspace-backed staging, capacity-aware buffering, expanded tests, and API documentation. ChangesSM120 SDPA backward
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant sdpa_bwd_wrapper_dsl_sm120
participant SdpaBwdDsl
participant SM120FusedMultiHeadAttentionFP16Backward
participant Workspace
Client->>sdpa_bwd_wrapper_dsl_sm120: submit tensors and deterministic option
sdpa_bwd_wrapper_dsl_sm120->>SdpaBwdDsl: validate layouts and select padded dimension
SdpaBwdDsl->>Workspace: allocate staging, dq_accum, and dq_sem
SdpaBwdDsl->>SM120FusedMultiHeadAttentionFP16Backward: launch three-kernel backward chain
SM120FusedMultiHeadAttentionFP16Backward->>Workspace: accumulate dQ and write dK/dV
SdpaBwdDsl-->>Client: scatter staged gradients
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
@cudnn-ci-bot run frost |
|
🚀 Running mirror pipeline Branch: cudnn-gh/pr-533-c1593cb |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (8)
docs/fe-oss-apis/attention/sdpa_bwd_sm120.md (2)
92-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe GEMM4 notation disagrees with the kernel docstring.
This line states
GEMM4 dQ = dS · K. The helper docstring inpython/cudnn/sdpa/bwd/kernels/bprop_f16_sm120.pyat line 288 statesacc_dq = dS @ K^T.For
dSof shape(S_q, S_kv)andKof shape(S_kv, D), the correct product isdS · K, which matches this document. Correct the kernel docstring instead.📝 Proposed fix in the kernel file
- """GEMM 4: acc_dq = dS @ K^T (reads only sdS/sK, never sQ).""" + """GEMM 4: acc_dq = dS @ K (reads only sdS/sK, never sQ)."""🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/fe-oss-apis/attention/sdpa_bwd_sm120.md` at line 92, Update the helper docstring in the bprop_f16_sm120.py kernel around acc_dq to state that acc_dq = dS @ K, matching the documented tensor shapes and GEMM4 notation; leave the attention documentation unchanged.
74-74: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a language to the two fenced code blocks.
markdownlint reports MD040 for both fences. Use
textfor the pipeline diagrams.📝 Proposed fix
-``` +```text dot delta = rowsum(O ∘ dO); zeroes dq_accum (and, when deterministic, the relay counters)-``` +```text GEMM1 S = Q · Kᵀ (K streamed from SMEM)Also applies to: 86-86
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/fe-oss-apis/attention/sdpa_bwd_sm120.md` at line 74, Specify the language for both fenced code blocks in the attention backward documentation by changing their opening fences to use text, including the pipeline-diagram block around dot and delta and the block around GEMM1 and S.Source: Linters/SAST tools
python/cudnn/sdpa/bwd/kernels/bprop_f16_sm120.py (2)
1030-1042: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueThe SWA turn derivation is correct; add the exact-multiple case to the comment.
det_turn = n_block - max((m_block * M + diag_off - W) // N, 0)reproduces the first visiting KV tile. The derivation holds for both the exact-multiple and the remainder cases, and negative values clamp to 0 under either truncating or flooring integer division.The comment states the formula but not why the visitor set is contiguous. Contiguity follows because
m_block_max(n)andm_block_min(n)are both non-decreasing inn, so a fixedm_blocksees an interval ofn. Recording that fact prevents a later change to the loop bounds from silently breaking the relay.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@python/cudnn/sdpa/bwd/kernels/bprop_f16_sm120.py` around lines 1030 - 1042, Update the SWA derivation comment near det_turn to state that the visitor KV tiles form a contiguous interval because m_block_max(n) and m_block_min(n) are non-decreasing in n, covering both exact-multiple and remainder cases. Keep the det_turn calculation unchanged.
389-428: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winDocument the relay’s CTA-scheduling precondition. CUDA does not guarantee CTA scheduling order, so
_bwd_det_waitcan deadlock if a lowern_blockCTA is not resident or complete. Document this launch constraint.barrier_id=6orders the precedingred.global.addoperations before lane 0’s GPU-scopest.release; the PTX rule thatreddoes not form acquire patterns does not invalidate this pattern.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@python/cudnn/sdpa/bwd/kernels/bprop_f16_sm120.py` around lines 389 - 428, Document the deterministic relay launch precondition near _bwd_det_wait/_bwd_det_release: CTAs must be scheduled so lower n_block CTAs are resident or complete before higher ones wait, since CUDA does not guarantee CTA scheduling order and otherwise the relay may deadlock. Preserve the existing synchronization explanation, including that barrier_id=6 orders prior red.global.add operations before lane 0’s GPU-scope release and that red’s lack of acquire patterns does not invalidate this ordering.python/cudnn/sdpa/bwd/engines.py (1)
243-244: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueFix the comment wording and consider an envelope-style check.
The comment reads "Any head size multipled of 8". The correct wording is "Any head size that is a multiple of 8".
Also consider matching the forward row's
d_envelopepattern (python/cudnn/sdpa/fwd/engines.py), which checksd <= capandd % 8 == 0and produces a short reason string. The enumerated set makes the mismatch message print 32 values.♻️ Comment fix
- # Any head size multipled of 8 + # Any head size that is a multiple of 8, up to the largest native bin d=frozenset(range(8, max(_SM120_HEAD_DIMS) + 1, 8)),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@python/cudnn/sdpa/bwd/engines.py` around lines 243 - 244, Correct the comment above the backward `d` constraint to say “Any head size that is a multiple of 8.” Replace the enumerated `frozenset(range(...))` constraint with an envelope-style check matching the forward `d_envelope` pattern: accept dimensions with `d <= max(_SM120_HEAD_DIMS)` and `d % 8 == 0`, and return a concise reason string.test/python/sdpa/frost/test_sdpa_bwd_dsl_sm120.py (2)
389-392: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove the large parameter sweeps out of
L0.These new tests add roughly 30
L0cases. Several compile a distinct CuTe DSL specialization per parameter combination, and the bitwise tests execute three full graph runs ats_q = 1024withDup to 256. CuTe DSL compilation dominates the runtime of each new specialization.Keep one representative case per code path at
L0and move the sweeps toL1or higher. Suggested split:
- Keep at
L0: onelarge_d_wrappercase, onepadded_head_dimcase, onedeterministic_bitwisecase, anddeterministic_large_d_numeric(it covers theQ_STAGES == 1branch).- Move to
L1: the remaininghead_dimandmaskcombinations oftest_sdpa_bwd_dsl_sm120_large_d_wrapper,test_sdpa_bwd_dsl_sm120_padded_head_dim,test_sdpa_bwd_dsl_sm120_deterministic_bitwise, andtest_sdpa_bwd_dsl_sm120_deterministic_large_d_bitwise.As per path instructions: "Mark every new Python test with a level from
L0throughL4; keepL0tests fast and place large parameter sweeps at higher levels."Also applies to: 436-438, 536-539, 594-599
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/python/sdpa/frost/test_sdpa_bwd_dsl_sm120.py` around lines 389 - 392, Reduce the new SM120 test parameter sweeps marked L0: retain only one representative case for large_d_wrapper, padded_head_dim, deterministic_bitwise, and deterministic_large_d_numeric, while moving the remaining head_dim/mask combinations in test_sdpa_bwd_dsl_sm120_large_d_wrapper, test_sdpa_bwd_dsl_sm120_padded_head_dim, test_sdpa_bwd_dsl_sm120_deterministic_bitwise, and test_sdpa_bwd_dsl_sm120_deterministic_large_d_bitwise to L1 or higher.Source: Path instructions
505-533: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a negative control to the bitwise test.
_run_bitwise_caseasserts that three deterministic runs match bitwise. The assertion also passes if the non-deterministic path happens to be stable for the chosen shape, so it does not prove thatdeterministic=Truechanged anything.Consider adding one case that runs with
deterministic=Falseon a shape with many KV tiles per q-tile and records the result. If that case is also stable in CI, the deterministic assertion carries no signal and the shape should be enlarged. This is a test-strength improvement, not a defect.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/python/sdpa/frost/test_sdpa_bwd_dsl_sm120.py` around lines 505 - 533, Add a negative-control execution to _run_bitwise_case using deterministic=False on a shape with many KV tiles per q-tile, record its outputs across runs, and verify whether they remain bitwise stable. Ensure the selected case is enlarged if the nondeterministic path is consistently stable in CI, while preserving the existing deterministic=True assertions.test/python/sdpa/frost/test_sdpa_graph_analyzer.py (1)
952-956: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a positive assertion for an accepted non-native head dimension.
The test now asserts that
d=100andd=264are rejected. It does not assert that a multiple of 8 inside the envelope but outside the native bins is accepted. That is the new behavior this PR introduces at the analyzer layer.♻️ Suggested addition
monkeypatch.setattr(ga, "_device_cc", lambda: (12, 0)) + # Inside the envelope and a multiple of 8: accepted (the adapter pads to the next bin). + assert _BWD_ENGINE in _bwd_eligible(_mk_bwd_graph(d=40)) + assert _BWD_ENGINE in _bwd_eligible(_mk_bwd_graph(d=200)) assert not _bwd_eligible(_mk_bwd_graph(d=100)) assert not _bwd_eligible(_mk_bwd_graph(d=264))🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/python/sdpa/frost/test_sdpa_graph_analyzer.py` around lines 952 - 956, Add a positive assertion in test_bwd_probe_rejects_unsupported_head_dim using an accepted non-native head dimension, such as d=104, which is a multiple of 8 within the adapter’s supported envelope. Keep the existing rejection assertions for d=100 and d=264.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/fe-oss-apis/attention/sdpa_bwd_sm120.md`:
- Line 15: Add cudnn.sdpa.bwd.sdpa_bwd_wrapper_dsl_sm120 to the lazy-export
table in cudnn.__init__ while preserving lazy loading; reference the module and
symbol without eagerly importing torch or cutlass.
---
Nitpick comments:
In `@docs/fe-oss-apis/attention/sdpa_bwd_sm120.md`:
- Line 92: Update the helper docstring in the bprop_f16_sm120.py kernel around
acc_dq to state that acc_dq = dS @ K, matching the documented tensor shapes and
GEMM4 notation; leave the attention documentation unchanged.
- Line 74: Specify the language for both fenced code blocks in the attention
backward documentation by changing their opening fences to use text, including
the pipeline-diagram block around dot and delta and the block around GEMM1 and
S.
In `@python/cudnn/sdpa/bwd/engines.py`:
- Around line 243-244: Correct the comment above the backward `d` constraint to
say “Any head size that is a multiple of 8.” Replace the enumerated
`frozenset(range(...))` constraint with an envelope-style check matching the
forward `d_envelope` pattern: accept dimensions with `d <=
max(_SM120_HEAD_DIMS)` and `d % 8 == 0`, and return a concise reason string.
In `@python/cudnn/sdpa/bwd/kernels/bprop_f16_sm120.py`:
- Around line 1030-1042: Update the SWA derivation comment near det_turn to
state that the visitor KV tiles form a contiguous interval because
m_block_max(n) and m_block_min(n) are non-decreasing in n, covering both
exact-multiple and remainder cases. Keep the det_turn calculation unchanged.
- Around line 389-428: Document the deterministic relay launch precondition near
_bwd_det_wait/_bwd_det_release: CTAs must be scheduled so lower n_block CTAs are
resident or complete before higher ones wait, since CUDA does not guarantee CTA
scheduling order and otherwise the relay may deadlock. Preserve the existing
synchronization explanation, including that barrier_id=6 orders prior
red.global.add operations before lane 0’s GPU-scope release and that red’s lack
of acquire patterns does not invalidate this ordering.
In `@test/python/sdpa/frost/test_sdpa_bwd_dsl_sm120.py`:
- Around line 389-392: Reduce the new SM120 test parameter sweeps marked L0:
retain only one representative case for large_d_wrapper, padded_head_dim,
deterministic_bitwise, and deterministic_large_d_numeric, while moving the
remaining head_dim/mask combinations in test_sdpa_bwd_dsl_sm120_large_d_wrapper,
test_sdpa_bwd_dsl_sm120_padded_head_dim,
test_sdpa_bwd_dsl_sm120_deterministic_bitwise, and
test_sdpa_bwd_dsl_sm120_deterministic_large_d_bitwise to L1 or higher.
- Around line 505-533: Add a negative-control execution to _run_bitwise_case
using deterministic=False on a shape with many KV tiles per q-tile, record its
outputs across runs, and verify whether they remain bitwise stable. Ensure the
selected case is enlarged if the nondeterministic path is consistently stable in
CI, while preserving the existing deterministic=True assertions.
In `@test/python/sdpa/frost/test_sdpa_graph_analyzer.py`:
- Around line 952-956: Add a positive assertion in
test_bwd_probe_rejects_unsupported_head_dim using an accepted non-native head
dimension, such as d=104, which is a multiple of 8 within the adapter’s
supported envelope. Keep the existing rejection assertions for d=100 and d=264.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Enterprise
Run ID: 920e811f-f9ce-4dea-8ada-c01c4997c0e2
📒 Files selected for processing (9)
docs/fe-oss-apis/attention/sdpa_bwd_sm120.mddocs/fe-oss-apis/overview.mdpython/cudnn/sdpa/bwd/api_dsl.pypython/cudnn/sdpa/bwd/config_sm120.pypython/cudnn/sdpa/bwd/engines.pypython/cudnn/sdpa/bwd/kernels/bprop_f16_sm120.pypython/cudnn/sdpa/graph_analyzer.pytest/python/sdpa/frost/test_sdpa_bwd_dsl_sm120.pytest/python/sdpa/frost/test_sdpa_graph_analyzer.py
|
@cudnn-ci-bot run frost |
|
🚀 Running mirror pipeline Branch: cudnn-gh/pr-533-2f1a661 |
|
@cudnn-ci-bot run frost |
|
🚀 Running mirror pipeline Branch: cudnn-gh/pr-533-6c9c805 |
…d_deterministic_dQ
|
@cudnn-ci-bot run frost |
|
🚀 Running mirror pipeline Branch: cudnn-gh/pr-533-89b5c16 |
…ced its own copy) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* SM80 (A100) SDPA: FROST engines + cudnn.sdpa adapters CuTe-DSL SDPA forward and backward for SM80 as FROST engines, plus standalone cudnn.sdpa APIs. * sdpa_fwd_prefill_sm80 joins the FrostSdpaFwdEngines family (the manifest row's sm_lo drops to 80); sdpa_bwd_sm80 introduces the first backward opset as a new FrostSdpaBwdEngines family on the reserved FROST_SDPA_BWD_ID_BASE block, with a frost_sdpa_bwd manifest row anchored on SDPA_BWD. * The shared graph_analyzer learns sdpa_backward() graphs: backward facts, K/V transposed-input-view canonicalization, and a forward-direction gate in the shared mismatch(). * Kernels (fwd generic + d256; bwd generic + d64 fast path) build on the shared frost/tile_dsl library; torch-native host code, per-shape self-caching, stream-aware and CUDA-graph-capturable. * Standalone SdpafwdSm80 / SdpabwdSm80 APIBase adapters + wrappers, including a packed-THD path the engines do not expose yet. * Docs: FE OSS API pages + Attention.md sections. Verified on A100: reference suites (103), engine/analyzer/stream suites (123), and full test_mhas_v2 with FROST auto-selection: 1268 passed / 0 failed, 41.2% of graphs served on FROST. The kernels originate from earlier internal work by Roman Anders. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: address CodeRabbit findings * drop the stray fe_cuda_13.3.cfg (local build config, added by accident) * THD forward: resolve the default softmax scale from the user's head dim BEFORE flavor padding (a d=96 call silently used 1/sqrt(128)) * THD forward/backward: reject dense-only features (bias / RoPE / block_mask / seq lens / scale_output / scheduler) instead of silently computing without them; drop the never-used backward max_s_q kwarg * dense mask resolution: window_size=(-1, r) without is_causal is now rejected instead of silently selecting a 0-token SWA window * bprop kernel: assert K's head dim matches Q's, and reject RoPE at d_qk > 128 (the sDQ SMEM staging exceeds the A100 budget beyond that) * bwd reference in the fe_api suite anchors the causal diagonal top-left, matching the mask the wrapper actually requests * docs: requirements sections no longer cite the retired ctm package; provenance notes restored in both kernel packages; THD feature gating documented * stale comments: flavor envelope (dsv3->qwen), d64 'not yet routed', gptoss draft artifact, prefill header diagram / swizzle note / mainloop wait counts, d256 wait count * review: actually drop the stray fe_cuda_13.3.cfg The previous commit unstaged it, then a blanket git add -A swept the untracked local file back in before committing. Now excluded locally. * review: round 2 — backward twins of the round-1 fixes + test tiering * bwd dense mask resolution: reject window_size=(-1, r) without is_causal instead of silently selecting a 0-token SWA window (the fwd adapter and both THD paths already resolve it this way) * bwd THD: resolve the default softmax scale from the user's head dim BEFORE flavor padding (same silent-wrong-gradients bug as the fwd twin) * fe_api sweeps (48 cases each) move from L0 to L2 per the repo guideline; a single representative smoke case stays at L0 for each direction * underscore the unused unpacks Ruff flags in the fwd reference * docs: purge the last ctm mentions — the retired DSL package is not a dependency of anything in this PR * test: skip the SM80 fe_api suites when the CuTe DSL is missing or predates cutlass.experimental The package imports are lazy (PEP 562) since the engine-family cleanup, so a missing or old nvidia-cutlass-dsl no longer fails at wrapper import — it erupts at kernel-load time mid-test. The oss:rel CI leg (older DSL) showed 9 such errors; probe cutlass.experimental in the module skip so those environments skip cleanly. * review: round 3 — require the bound seq_len_q buffer + Ruff lints * resolve_feature_operands: a graph that BINDS seq_len_q must get its buffer through _need() like every other feature operand — silently omitting it would execute a different query-length contract (missed BR-under-padding base and padded-row LSE trim) * Ruff RUF059 (two unused unpacks) and E731 (lambda-to-def) * fix: drop duplicated stale helpers the analyzer merge left behind The rebase onto the engine-family cleanup appended our helper block including copies of resolve_variant_pack (identical, harmless) and tensor_desc_from_ir (STALE: still referencing the deleted _DTYPE_FROM_CUDNN map). The stale copy shadowed upstream's fixed one and took down every SM100/SM120 lowering with a NameError — invisible on A100, where those paths skip; pipeline 61601513's Blackwell frost leg caught it (716 failures). * fix: never wrap the default stream handle in ExternalStream (CI determinism zeros) Root cause of the frost_tests:sdpa[Ampere] determinism failures (104/104 is_determin backward configs, second execution returning all-zero grads): the harness's cudnn handle carries raw stream 0, and _stream_ctx wrapped it in torch.cuda.ExternalStream(0). On the CI image's NGC torch build (2.12.0a0), every kernel launch inside that context after the compile run silently no-ops. Reproduced and verified in the CI container itself (gitlab/cudnn_frontend:cudnn_13.3.0 + the pipeline's build artifact): before — run 0 correct, runs 1+ all-zero; after — bitwise-identical runs, test_sdpa_random_bwd_L0 176 passed / 0 failed under -n 4. _stream_ctx now maps a raw handle equal to torch's current/default stream onto that torch stream object and reserves ExternalStream for genuine foreign streams — the same guard fwd/api_dsl._torch_stream_context and gemm/cutedsl/grouped/backend_utils.py already carry (these adapters were the only unguarded spot in the tree). Known residual, unrelated to CI: on NGC torch the FIRST execute after re-pointing a handle to a brand-new stream still no-ops once on a cached kernel (suspected DSL/tvm-ffi launch-state caching; to be reported upstream — the kernels cannot compile without tvm-ffi, so it could not be isolated further). * test: pin SM80 plans via frost_test_utils.select_engine (post-#528 plan names carry knobs) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: fold SM80 adapters into api.py, configs into config_sm80.py, drop the docs pages Per maintainer feedback on the PR: - SdpafwdSm80/SdpabwdSm80 + wrappers move from api_sm80.py into each opset's api.py, following the SM100 classes there (one api.py per opset, no per-arch files). Lazy exports and the engine lowerings repoint; no signature changes. - The per-flavor kernel configs leave kernels/ for parent-level config_sm80.py (one per direction), matching config_sm100/config_sm120. - The FE OSS docs pages and their overview/Attention/llms.txt entries are dropped for now; the engines remain the PR's product. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * rebase: dedupe the bwd Capabilities.layouts field (#533 introduced its own copy) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Before submitting
pre-commit runand committed any formatting changes.cat-*, one or moremod-*, and oneorig-*(see label list).Affected area
Summary
Extends the
sdpa_bwd_sm120FROST engine:dense_flex) layouts. Q/K/V/O/dO/dQ/dK/dV no longer must be BSHD-physical: any dense layout with the head dim innermost-contiguous (non-broadcast, non-overlapping; padded strides allowed) is accepteddeterministic=Trueserializes the per-(batch, head,q_tile)additions in ascending KV-tile order through a GMEM turn-counter arraydocs/fe-oss-apis/attention/sdpa_bwd_sm120.mdWhy
Related issues
#381
API and compatibility impact
Testing
Summary by CodeRabbit
New Features
Documentation
Tests