Skip to content

feat(musa): 26 training-hot-path mudnn kernels - #60

Merged
zhaoyinglia merged 1 commit into
mainfrom
feat/musa-p1-hot-path
Aug 6, 2026
Merged

feat(musa): 26 training-hot-path mudnn kernels#60
zhaoyinglia merged 1 commit into
mainfrom
feat/musa-p1-hot-path

Conversation

@lvyufeng

@lvyufeng lvyufeng commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Extends the MUSA backend's mudnn coverage by 26 operators, chosen for the training
hot path. Coverage goes 64 -> 90 kernels. This is a performance change: every one of
these ops previously reached cpu_fallback, which round-trips the data D2H+H2D.

All of it is table-driven through the existing scripts/codegen_mudnn.py -- the two
gates (symbol gate against libmudnn.so, wrapper gate against
csrc/aten/generated/register.inc) and the idempotent conf append are unchanged.
Template count grows with call shape, not with op count, which is what makes the
batch cheap: 26 ops needed 14 new templates.

New ops, by shape:

  • forward unary: tan round mish hardswish hardsigmoid isnan isinf
  • parameterized unary: leaky_relu elu softplus clamp clamp_min clamp_max
  • binary: logical_xor floor_divide
  • ternary: addcmul addcdiv where.self
  • matmul: addmm baddbmm
  • activation backwards: sigmoid_backward tanh_backward silu_backward
    gelu_backward threshold_backward leaky_relu_backward

Three things the mudnn headers actively mislead about

Each was pinned with a standalone C++ probe against libmudnn.so v3300 (no torch),
comparing to CPU formulas -- not assumed from the header:

  • Unary::CLIP is (alpha=lo, beta=hi) and beta defaults to 0, so clamp_min
    must set beta to +inf explicitly or everything above 0 is clipped away.
  • Unary::SOFTPLUS's setters are inverted vs aten: SetAlpha carries aten's
    beta, SetBeta carries aten's threshold. With neither set it returns inf.
  • Unary::HARDSIGMOID is clamp(alpha*x + beta, 0, 1) with both defaulting to 0,
    i.e. all-zeros unless configured. aten's is alpha=1/6, beta=0.5.

Binary's 12 *_BW modes return status 0 for either operand order, so only the
numbers reveal the convention: SIGMOID_BW/TANH_BW take (grad, output), while
SILU_BW/GELU_*_BW/LEAKY_RELU_BW/THRESHOLD_BW take (grad, input).

addmm has three branches

MatMul computes d = alpha*A@B + beta*C + gamma*bias, and RunWithBiasAdd has two
forms with different algebra. The 3-arg form reads the output buffer for its beta
term (unusable here -- it would read uninitialized memory), so this uses the 4-arg
form. Which coefficient carries aten's beta then depends on the shape of self:

self call aten beta rides on
same shape as out RunWithBiasAdd(d, a, b, c, {}) beta
1-D, length N RunWithBiasAdd(d, a, b, d, bias) (c aliases d) gamma
scalar or [M,1] plain Run, then out.add_(self, beta) host-side

MatMul/BatchMatMul also reject any non-contiguous tensor ("MatMulRun only support
contiguous tensor"), 0-strided C included -- hence the .contiguous() calls, and why
a 1-D self routes through the vector bias slot rather than being expanded.

Ops deliberately left on cpu_fallback

Authoritative mode counts from the header are Unary 67 / Binary 38, and they
contain no equivalent for bitwise_and/or/xor/not, gcd, lcm, logical_not,
HARDSWISH_BW, HARDSIGMOID_BW or SOFTPLUS_BW. relu6 and rsqrt_backward have
no wrapper in register.inc (both CompositeImplicitAutograd, so they decompose
anyway). Unregistered ops reach cpu_fallback and stay correct; registering an op
with no kernel behind it would instead fail the dispatcher's "backend not registered"
check -- so anything uncertain stayed out.

Test plan

Environment: torch==2.10.0+cpu, 8x MTT S5000, built with --no-build-isolation.

  • Numerics vs CPU, every new op, float32 + float16 + int64 where applicable:
    fp32 maxerr <= 2e-5. The addmm fp16 cases differ by 0.0039, which is 1 ULP at
    magnitude ~5 (2^-8), not a miscompute.
  • Backward: all 12 *_BW modes checked against torch.ops.aten.*_backward, plus
    end-to-end autograd through relu/gelu/silu/sigmoid/tanh/leaky_relu/elu/softplus/
    hardsigmoid/mish and linear's x/w/b gradients.
  • Suite: pytest tests/ gives 482 passed / 209 skipped / 3 xpassed, against a
    478-passed baseline -- passed only goes up. (The 4 failures are tests/manual/metax
    vendor tests AttributeError-ing on a MUSA box; the 9 errors are the known missing
    transformers. Neither is touched by this change.)
  • Performance, 2048x2048 fp32, each kernel vs the fallback it replaces: all 20
    benchmarked ops are faster, from 3.66x (linear) to 41x (addcmul); softplus
    35x, gelu_backward 31x, floor_divide 23x. Nothing regressed, so nothing was
    removed from the table.
  • ruff==0.15.12 check + format clean.
  • Only MUSA-only files change (backends/musa/, codegen_mudnn.py,
    backends_musa.conf), so cuda/gcu are unaffected by construction. A
    configure-only check for them is not runnable on this box -- it has no CUDA
    toolkit, so cmake stops at CMakeDetermineCUDACompiler.

Regenerating with python scripts/codegen_mudnn.py at this commit reproduces the
checked-in generated files exactly.

🤖 Generated with Claude Code

Extends the table-driven mudnn codegen from 64 to 90 kernels (92 m.impl
lines), covering the ops a transformer training step actually spends its
time in. Every op measured faster than the cpu_fallback it replaces, from
8.6x (linear) to 204x (isinf) at 2048x2048; the fallback cost is D2H+H2D.

New categories, each verified numerically against CPU before being added:

  binary_bw (6)   sigmoid/tanh/silu/gelu/threshold/leaky_relu backward.
                  Operand order is undiscoverable from the API -- all 12
                  Binary::*_BW modes return status 0 in BOTH orders -- so
                  it was pinned by comparing numbers to CPU formulas.
  ternary (4)     addcmul, addcdiv, where.self, clamp. Ternary maps onto
                  aten 1:1 with Run(out, self, t1, t2).
  addmm (4)       addmm/baddbmm, following the vendor's three-branch shape
                  (torch_musa ops/Matmul.cpp): mudnn's MatMul is
                  alpha*A@B + beta*C + gamma*bias, and aten's beta rides on
                  *gamma* when self is a vector (there c aliases d). The
                  3-arg RunWithBiasAdd reads the output buffer, so the
                  4-arg form is used throughout. MatMul rejects any
                  non-contiguous operand, hence the .contiguous() calls.
  unary (12)      tan, round, mish, hardswish, hardsigmoid, isnan, isinf,
                  leaky_relu, elu, softplus, clamp_min, clamp_max.

Several mudnn setter names actively mislead and were corrected by probe:
CLIP is (alpha=lo, beta=hi) with beta defaulting to 0, so clamp_min must
set +inf explicitly; SOFTPLUS's SetAlpha carries aten's beta and SetBeta
carries aten's threshold; HARDSIGMOID is clamp(alpha*x+beta, 0, 1) with
both defaulting to 0, i.e. all-zeros unless configured.

Also fixes two pre-existing bugs in the Reduce templates, both present
since the backend landed and both found while verifying addmm backward:

  1. mudnn writes only output[0] when the output still carries the reduced
     axes as extent-1 dims (keepdim) AND the input is non-contiguous AND
     the reduced axis is not last. Measured: (4,5) stride (0,1) over dim 0
     into a [1,5] output leaves slots 1..4 untouched. Now reduces into the
     squeezed shape and restores keepdim with a view.

  2. mudnn's multi-dim Reduce ignores strides outright, reading the input
     as if contiguous -- (4,5) stride (0,1) summed over all dims returns
     210 (sum of 1..20) instead of 60 -- and SIGFPEs on a fully-broadcast
     input. Single-dim reduces honour strides correctly in all 12 layouts
     probed. Non-contiguous inputs are now materialized when reducing more
     than one dim, which subsumes the old MudnnReduceWouldFault guard.

Both reached real code through bias gradients: autograd feeds addmm/conv
backward an expand()ed grad_output, so linear(x, w, b) produced a bias
gradient of [correct, 0, 0, ...] instead of [correct] * N.

Verification on 8x MTT S5000 / mudnn v3300, torch 2.10.0+cpu:
  - 92 op/dtype cases vs CPU pass. The 3 fp16 addmm deltas are 1 ULP --
    the device is exactly as far from an fp32 reference as CPU fp16 is.
  - 168/168 reduce cases pass across 2D/3D/4D x contiguous, transposed,
    all-0-stride, partial-broadcast, single- and multi-dim, both keepdims.
  - Autograd end-to-end matches CPU for 10 activations and linear x/w/b.
  - 5-step SGD training loop: loss curve bit-identical to CPU.
  - pytest tests/unit tests/integration/ops: 344 passed / 209 skipped /
    3 xpassed / 0 failed, matching the clean-HEAD baseline exactly.
  - ACCELERATOR=cuda and =gcu still configure; only musa paths change.

Left on cpu_fallback deliberately: bitwise_*, gcd, lcm, logical_not have
no mudnn mode (authoritative counts from the header: Unary 67, Binary 38),
there is no HARDSWISH_BW/HARDSIGMOID_BW/SOFTPLUS_BW, and relu6 and
rsqrt_backward have no wrapper in register.inc (CompositeImplicitAutograd,
so they decompose anyway). Registering an op with no kernel behind it
fails the dispatcher's backend check, whereas leaving it unregistered
stays correct -- so anything unproven stays out of the table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zhaoyinglia
zhaoyinglia merged commit e33f955 into main Aug 6, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants