Skip to content

perf(pt): fuse the mixing-stack per-layer backward chain (opt-in) - #6060

Open
long-yi-2019 wants to merge 4 commits into
deepmodeling:masterfrom
long-yi-2019:pr/stack-bwd-fused
Open

long-yi-2019 wants to merge 4 commits into
deepmodeling:masterfrom
long-yi-2019:pr/stack-bwd-fused

Conversation

@long-yi-2019

@long-yi-2019 long-yi-2019 commented Oct 8, 2026 •

Copy link
Copy Markdown

Motivation

Each gated layer of the mixing-stack backward runs as a three-kernel relay: recompute the gate sigmoids, pointwise backward writing the pre-activation gradient gz, then the transposed-weight GEMM reading gz back. At the SeZM N=4096 eval workload (E ~ 646k, Cf = 32, lmax = 3) the relay materializes two surfaces per stack and walks them through HBM every gated layer:

  • sig (F, E, L*Cf) fp32, ~0.5 GB, written by the recompute kernel and read straight back by the pointwise kernel;
  • gz (F, E, ROW), ~1.65 GB, written by the pointwise kernel and read straight back by the GEMM.

Change

Opt-in via DP_STACK_BWD_FUSED_INFER (default OFF, read when the backward traversal runs via use_stack_bwd_fused(), matching the repository's convention of construction/call-time reads for these switches). When enabled, and in the supported regime — fp32, Cf < GATE_BMM_MIN_FOCUS_DIM (no cuBLAS gate path), Cf == BLOCK_K so one K tile is exactly one row group, no weight gradients, no second-order surfaces, no upstream cotangents, and no higher-order gradient graph being built — a single kernel per layer replaces the relay: each program recomputes its row group's gate sigmoid, forms the gz tile in registers with exactly the pointwise kernel's operator sequence, and accumulates the GEMM with exactly the backward GEMM kernel's tl.dot order, so results match the relay bit-for-bit.

The m = 0 block (which carries the full scalar chain) and the |m| = 1 stripes launch as two PART passes over disjoint column segments: a shared kernel would push the light segment onto the heavy one's register profile (BLOCK_M=64/BN=64 spills 226 slots; the shipped 32/64 config stays in budget).

Outside the regime the dispatch falls back to the existing relay unchanged. The fused kernel registers no next-derivative formula; create_graph=True (e.g. force-loss training) keeps the operator that carries the hand-derived second order — the official backward already routes those differentiations structurally, and not torch.is_grad_enabled() at the dispatch is a second belt.

Measurements

SeZM model, N=4096 atoms (~646k edges), RTX 5090, torch 2.12.1+cu130, DP_COMPILE_INFER=1, DP_TRITON_INFER=3, throughput mode (16 evals, P50):

build per-eval (ms) steady peak VRAM alloc (MB)
master 153.2 22740
this PR (flag on) 150.6 (-1.7%) 22742 (unchanged)

Peak VRAM is unchanged at this workload: the eliminated 2.15 GB standing scratch does not coincide with the frame's peak allocation on this shape (the forward does); what it removes is the allocation itself and the per-layer HBM round-trip of both surfaces. Bit-identity of the fused kernel against the relay is pinned by TestSeZMStackBwdFused (fp32, apply_alpha on and off); two-state compiled-eval accuracy at N=4096: energy rel diff 3.7e-10, max force diff 4.9e-6 with max |f| ~ 3.9.

Tests

  • TestSeZMStackBwdFused (CUDA): fused-vs-relay bitwise cross-check; dispatch follows the env flag (recording stand-in); higher-order backward preserved with the flag on; wide-channel (Cf >= GATE_BMM_MIN_FOCUS_DIM) and non-fp32 fallbacks.
  • TestSeZMStackBwdFlagParser (CPU): env parsing.
  • Full source/tests/pt/model/test_descriptor_sezm_triton.py (43 passed) and test_descriptor_sezm.py (61 passed) on the branch with the flag off (default).

Summary by CodeRabbit

  • New Features
    • Added an opt-in fused backward path for supported gated mixing-stack layers, which can speed up gradient computation during training.
    • Control the option with the DP_STACK_BWD_FUSED_INFER setting; it is disabled by default. Unsupported configurations and higher-order gradient calculations continue to use the existing backward path.
    • The fused path is available for supported fp32 workloads; other input types and configurations continue to use the existing behavior.

…D_FUSED_INFER)

Each gated layer's backward runs as recompute -> pointwise -> GEMM,
materializing the sigmoid surface and the pre-activation gradient to
HBM and reading both straight back.  With the flag on (and the
supported regime: fp32, narrow channels, no higher-order graph), one
kernel walks the chain with both surfaces in registers, matching the
relay bit-for-bit; the m = 0 and |m| = 1 column segments launch
separately to keep the lighter segment off the heavy one's register
profile.  The 2.15 GB standing scratch never allocates; on the
N=4096 eval workload it does not coincide with the frame's peak.
Copilot AI balanced review requested due to automatic review settings October 8, 2026 11:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added the Python label Oct 8, 2026
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a7009f6d-ff26-462d-9fe8-c87bb8175601
📥 Commits

Reviewing files that changed from the base of the PR and between 4455e67 and ad9af59.

📒 Files selected for processing: 1
  • source/tests/pt/model/test_descriptor_sezm_triton.py

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

📝 Walkthrough

The mixing-stack backward adds an opt-in fused Triton path for eligible fp32 gradients. The path recomputes gate sigmoids and writes input gradients directly. Tests compare fused and unfused gradients and check higher-order gradients and fallback dispatch.

Changes: Fused mixing-stack backward

Layer / File(s) Summary
Environment switch
deepmd/pt_expt/kernels/utils.py, source/tests/pt/model/test_descriptor_sezm_triton.py
Adds use_stack_bwd_fused(), which reads DP_STACK_BWD_FUSED_INFER. Tests cover accepted truthy values, false values, unrecognized values, and an unset variable.
Fused kernel and launch
deepmd/pt_expt/kernels/triton/sezm/so2_value_path.py
Adds a kernel that recomputes gate sigmoids and pre-activation gradients, contracts them with block weights, and writes gradient columns for m = 0 and `
Eligibility, dispatch, and validation
deepmd/pt_expt/kernels/triton/sezm/so2_value_path.py, source/tests/pt/model/test_descriptor_sezm_triton.py
Selects the fused path when the flag and eligibility conditions are met. Eligible calls bypass the pointwise-backward and block-GEMM relay. Tests check gradient equivalence, higher-order gradients, dispatch counts, and fallback cases.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Traversal as Backward traversal
  participant Launcher as _launch_stack_bwd_fused
  participant Kernel as _stack_bwd_fused_kernel
  participant Buffer as Spare gradient buffer
  Traversal->>Launcher: Select fused path for an eligible layer
  Launcher->>Kernel: Launch m = 0 and |m| = 1 blocks
  Kernel->>Buffer: Write next-layer gradient columns
Loading
Merge Risk: ⚪ Minimal · up to ad9af

The change adds an opt-in fused backward kernel that is off by default, so default behavior is unchanged. No actionable merge-blocking risk was identified in the supplied review context.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely identifies the main change: an opt-in performance optimization that fuses the PyTorch mixing-stack per-layer backward chain.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches: 💡 1
🧪 Generate unit tests (beta)
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@njzjz-bot njzjz-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agent: dot

Reviewed the three-file delta from dbca0b1 to 4455e67, including the existing recompute/pointwise/GEMM implementations, launch configuration lookup, autograd routing, and new tests. One nonblocking regression-test issue is noted inline; source inspection did not establish a production kernel correctness defect.

Validation: eight isolated checks of the exact-source environment parser passed. Repository tests and CUDA/Triton numerical or compilation checks were not run: this executor has neither PyTorch nor Triton. The author-reported CUDA results were not independently reproduced. At review time all seven PR-triggered Actions runs reported action_required (the CUDA run has no jobs); pre-commit passed and Read the Docs reported failure. No CI runs were activated or rerun.


def test_non_fp32_fall_back(self):
"""The fused kernel assumes fp32 rounding; other dtypes keep the relay."""
calls, out = self._record_launches(3, 32, dtype=torch.bfloat16)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Agent: dot

[P3] Enable fusion before asserting that unsupported inputs fall back

This test and test_wide_channels_fall_back never set DP_STACK_BWD_FUSED_INFER, and _record_launches does not set it either. In the normal default-off test run, fused short-circuits at use_stack_bwd_fused(), so these assertions pass without exercising the unsupported-dtype/channel dispatch contract. In particular, removing the fp32 guard would still leave this bf16 regression test green. Please scope both calls with mock.patch.dict(os.environ, {"DP_STACK_BWD_FUSED_INFER": "1"}) (or equivalent) so the tests actually request the feature they expect to reject.

@long-yi-2019

Copy link
Copy Markdown
Author

@OutisLi @wanghan-iapcm @njzjz requesting a review, please. Following the 2026-10-13 discussion we validated the combined stack of our six performance PRs (#6056, #6057, #6060, #6070, #6071, #6072) with a multi-scale gate: diamond + bcc-Cu at ~500 / ~2000 / ~4400 atoms, three columns (pure torch.compile with all custom kernels off / current master / our changes on). All accuracy cells sit within 2.2x the pure baseline's own run-to-run band (criterion: 3x), and no tested atom count regresses in time or memory — full numbers in the PR descriptions. Suggested review order for the six: #6056 -> #6057 -> #6060 (small and self-contained) -> #6070 -> #6071 -> #6072.

@iProzd iProzd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I reviewed the exact head. The existing test-contract finding at source/tests/pt/model/test_descriptor_sezm_triton.py:2272 remains valid: the fallback tests never enable DP_STACK_BWD_FUSED_INFER, so they can pass without exercising the unsupported-input guards. Please scope the tests with the flag enabled and rerun the failing documentation check.

@iProzd iProzd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I reviewed the exact head. The existing test-contract finding at source/tests/pt/model/test_descriptor_sezm_triton.py:2272 remains valid: the fallback tests never enable DP_STACK_BWD_FUSED_INFER, so they can pass without exercising the unsupported-input guards. Please scope the tests with the flag enabled and rerun the failing documentation check.

@long-yi-2019

Copy link
Copy Markdown
Author

Thank you for the review. Addressed in ad9af59: both fallback tests now run with the flag enabled (renamed test_wide_channels_fall_back_with_flag_on / test_non_fp32_fall_back_with_flag_on), so zero fused launches is the guards' doing rather than the flag's — the wide-channel (Cf >= GATE_BMM_MIN_FOCUS_DIM) and non-fp32 contracts are each exercised under DP_STACK_BWD_FUSED_INFER=1. The higher-order test was already flag-on (test_higher_order_backward_preserved_with_flag_on), and test_dispatch_follows_env_flag covers both states.

On the documentation check: the RTD failure is repo-wide (the same red check shows on other open PRs, e.g. #6058); this diff touches no docs inputs.

@OutisLi OutisLi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed ad9af590248897b720b7401e03269f09bbb68181.

This PR removes intermediate sigmoid/pre-activation-gradient surfaces from the fp32 mixing-stack backward by recomputing them inside the matrix-multiply kernels. The optimization has a real use case, but the public control, tuning integration, duplicated mathematics, and test cost need changes before merge.

Changes required

  1. Use the existing inference levels; remove the new environment variable.
    DP_STACK_BWD_FUSED_INFER is a fine-grained development switch and should not become a public interface. Do not replace it with another switch or introduce another level. This optimization belongs within the existing DP_TRITON_INFER >= 2 value-path selection: level 2 already covers table-tuned kernels, while level 3 adds validated fp16x3 arithmetic. Keep supported-shape and profitability decisions internal. See the new selector and the existing level contract.

  2. Integrate the optimization into freeze-time autotuning, rather than shipping one fixed launch configuration.
    Extend the existing sweep_tile_configs.py machinery and its registration/lookup path so that freeze_pt2._tune_triton_configs() can discover and tune the new choice before tracing. A standalone benchmark that freeze never invokes is not sufficient. Compare numerically valid fused candidates against the current optimized relay, select the fastest valid candidate among those evaluated, and retain the existing path when fusion does not win. Record that outcome so repeated tuning does not redo completed work. Existing coverage for the old GEMM family must not incorrectly imply coverage for this new choice. See the fixed configuration, the sweep registry, and the freeze hook.

    Please provide results from running the added autotuner on your own available hardware: GPU/software versions, tested shapes, selected configurations or fallback, numerical agreement, and timings against the existing path. You are not required to populate the existing built-in configuration tables for hardware you do not have. The automatic tuning integration and evidence from hardware you can actually test are required.

    The need for selection is measurable. On an RTX PRO 6000 Blackwell Server Edition with PyTorch 2.13.0+cu130 / Triton 3.7.1, the same synthetic fp32 backward workload (E=300003, Cf=32, F=2, three gated layers plus the final linear layer, alpha enabled) gave:

    lmax Existing path Fused path Time change
    3 11.58 ms 10.49 ms -9.4%
    6 26.82 ms 45.39 ms +69.3%

    These are backward-operator microbenchmarks, not whole-model timings: identical inputs, CUDA-graph timing, off/on/on/off order, and the average of two median measurements per state. Outputs were bitwise equal. The degree-6 fused binaries also reached 255 registers with n_spills=68–92; the fixed configuration does not universally avoid spills.

  3. Consolidate the duplicated derivative implementation.
    The new kernel repeats the existing SiLU/gate derivative and scalar-gradient accumulation while requiring bitwise agreement. Shared mathematics and control flow should have one implementation wherever they can be unified; keep only genuinely different execution schedules separate. Small inlined Triton helpers are one option, subject to numerical and performance validation. Avoidable duplication is a maintainability issue to fix, not optional polish, and replacing it with a wrapper-heavy framework would not address that issue. See the duplicated chain.

  4. Reduce redundant test work and keep ordinary GPU CI inexpensive.
    Remove the new environment-parser tests along with the switch, and extend existing level/configuration-selection tests instead. The dispatch-only cases need not all allocate the shared 20,000-edge fixture. Use small representative inputs for numerical checks where they still exercise the intended kernel and tile boundaries; reuse existing higher-order/compile coverage rather than adding another heavy suite. Keep production-scale benchmarks in the tuning/benchmark workflow, not ordinary GPU CI.

    Do not mechanically shrink the higher-order case: its current 2 * 20000 * 320 * 4-byte surface is just above the existing 48 MiB traversal threshold, so a smaller fixture changes the branch exercised. Preserve the intended coverage explicitly when reducing cost. See the fixture and the threshold.

Verification

Seven targeted tests passed, including 15 subtests and the SO2Convolution integration comparison. Additional off/on probes across 11 shape cases were bitwise equal; the first-order autograd and symbolic FX-to-Inductor checks also agreed bitwise, with fused dispatch confirmed. Ruff and diff whitespace checks passed. No numerical correctness defect was established within this coverage; the author's whole-model 1.7% result was not independently reproduced.

The earlier comment about fallback tests failing to enable the switch has been addressed in this head and is not being raised again. The requested changes above concern the architecture and acceptance criteria of the optimization, not that already-fixed test issue.


def _record_launches(self, lmax, focus_dim, apply_alpha=False, dtype=torch.float32):
"""Run the traversal with a recording stand-in for the fused launch."""
import deepmd.pt_expt.kernels.triton.sezm.so2_value_path as vp
@codecov

codecov Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 3.88350% with 99 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.49%. Comparing base (668b60c) to head (633e87c).

Files with missing lines Patch % Lines
...epmd/pt_expt/kernels/triton/sezm/so2_value_path.py 1.98% 99 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6060      +/-   ##
==========================================
- Coverage   77.79%   77.49%   -0.31%     
==========================================
  Files        1171     1171              
  Lines      141562   141663     +101     
  Branches     5066     5065       -1     
==========================================
- Hits       110135   109785     -350     
- Misses      29550    30004     +454     
+ Partials     1877     1874       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants