Skip to content

Add tirx silu_mul kernel - #78

Open
yagneekp wants to merge 2 commits into
mlc-ai:mainfrom
yagneekp:tirx_silu_mul
Open

Add tirx silu_mul kernel#78
yagneekp wants to merge 2 commits into
mlc-ai:mainfrom
yagneekp:tirx_silu_mul

Conversation

@yagneekp

Copy link
Copy Markdown

silu_mul TIRx vs Triton — speedup (bf16, B200)

Pure-kernel time, min-of-N; speedup = triton_time / tirx_time
(>1.00 = TIRx faster).

Input Config Fwd Speedup Bwd Speedup
2048×768 1.02x 1.00x
2048×1408 1.01x 1.00x
2048×2048 1.00x 1.01x
2048×4096 1.00x 1.00x
8192×768 0.99x 1.00x
8192×1408 1.05x 1.00x
8192×2048 1.00x 1.00x
8192×4096 1.03x 1.00x
32768×768 1.00x 1.00x
32768×1408 1.00x 1.00x
32768×2048 1.03x 0.99x
32768×4096 1.03x 1.00x
4096×32768 1.03x 1.00x
16384×16384 1.04x 1.00x

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a TIRx-based implementation of the fused SwiGLU activation function (silu(gate) * up) with custom autograd forward and backward passes. The review feedback highlights critical safety improvements, specifically recommending early returns for empty tensors (n == 0) in both the forward and backward passes to prevent CUDA driver crashes, and adding assertions to verify that the input tensors reside on the same CUDA device.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +207 to +217
out = torch.empty_like(gate)
dtype, elem_bytes = _DTYPE_MAP[gate.dtype]
n = gate.numel()
assert n % (_VEC_BYTES // elem_bytes) == 0, "numel must be vector-divisible"
block, tile = _select_config(n)
_fwd_exec(dtype, block, tile, elem_bytes)(
gate.reshape(-1),
up.reshape(-1),
out.reshape(-1),
)
return out

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If the input tensors are empty (i.e., n == 0), launching the CUDA kernel with 0 blocks will cause a CUDA driver error or crash. Add a check to return early when n == 0. Additionally, improve the vector-divisibility assertion message to be more descriptive.

Suggested change
out = torch.empty_like(gate)
dtype, elem_bytes = _DTYPE_MAP[gate.dtype]
n = gate.numel()
assert n % (_VEC_BYTES // elem_bytes) == 0, "numel must be vector-divisible"
block, tile = _select_config(n)
_fwd_exec(dtype, block, tile, elem_bytes)(
gate.reshape(-1),
up.reshape(-1),
out.reshape(-1),
)
return out
out = torch.empty_like(gate)
n = gate.numel()
if n == 0:
return out
dtype, elem_bytes = _DTYPE_MAP[gate.dtype]
vec_size = _VEC_BYTES // elem_bytes
assert n % vec_size == 0, f"numel ({n}) must be divisible by vector size ({vec_size})"
block, tile = _select_config(n)
_fwd_exec(dtype, block, tile, elem_bytes)(
gate.reshape(-1),
up.reshape(-1),
out.reshape(-1),
)
return out

Comment on lines +229 to +241
grad_gate = torch.empty_like(gate)
grad_up = torch.empty_like(up)
dtype, elem_bytes = _DTYPE_MAP[gate.dtype]
n = gate.numel()
block, tile = _select_config(n)
_bwd_exec(dtype, block, tile, elem_bytes)(
grad_output.reshape(-1),
gate.reshape(-1),
up.reshape(-1),
grad_gate.reshape(-1),
grad_up.reshape(-1),
)
return grad_gate, grad_up

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Similarly to the forward pass, if the input tensors are empty (n == 0), the backward pass will attempt to launch a CUDA kernel with 0 blocks and crash. Add an early return check for n == 0 in the backward pass.

Suggested change
grad_gate = torch.empty_like(gate)
grad_up = torch.empty_like(up)
dtype, elem_bytes = _DTYPE_MAP[gate.dtype]
n = gate.numel()
block, tile = _select_config(n)
_bwd_exec(dtype, block, tile, elem_bytes)(
grad_output.reshape(-1),
gate.reshape(-1),
up.reshape(-1),
grad_gate.reshape(-1),
grad_up.reshape(-1),
)
return grad_gate, grad_up
grad_gate = torch.empty_like(gate)
grad_up = torch.empty_like(up)
n = gate.numel()
if n == 0:
return grad_gate, grad_up
dtype, elem_bytes = _DTYPE_MAP[gate.dtype]
block, tile = _select_config(n)
_bwd_exec(dtype, block, tile, elem_bytes)(
grad_output.reshape(-1),
gate.reshape(-1),
up.reshape(-1),
grad_gate.reshape(-1),
grad_up.reshape(-1),
)
return grad_gate, grad_up

Comment on lines +201 to +206
assert gate.shape == up.shape, f"shape mismatch: {gate.shape} vs {up.shape}"
assert gate.dtype == up.dtype, f"dtype mismatch: {gate.dtype} vs {up.dtype}"
assert gate.is_contiguous(), "gate must be contiguous"
assert up.is_contiguous(), "up must be contiguous"
assert gate.dtype in _DTYPE_MAP, f"unsupported dtype: {gate.dtype}"
ctx.save_for_backward(gate, up)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add assertions to verify that both input tensors are on the same CUDA device and that they are indeed CUDA tensors. Since the TVM target is hardcoded to "cuda", passing CPU tensors or tensors on mismatched devices will cause runtime crashes or undefined behavior.

Suggested change
assert gate.shape == up.shape, f"shape mismatch: {gate.shape} vs {up.shape}"
assert gate.dtype == up.dtype, f"dtype mismatch: {gate.dtype} vs {up.dtype}"
assert gate.is_contiguous(), "gate must be contiguous"
assert up.is_contiguous(), "up must be contiguous"
assert gate.dtype in _DTYPE_MAP, f"unsupported dtype: {gate.dtype}"
ctx.save_for_backward(gate, up)
assert gate.shape == up.shape, f"shape mismatch: {gate.shape} vs {up.shape}"
assert gate.dtype == up.dtype, f"dtype mismatch: {gate.dtype} vs {up.dtype}"
assert gate.device == up.device, f"device mismatch: {gate.device} vs {up.device}"
assert gate.is_cuda, "gate must be on a CUDA device"
assert gate.is_contiguous(), "gate must be contiguous"
assert up.is_contiguous(), "up must be contiguous"
assert gate.dtype in _DTYPE_MAP, f"unsupported dtype: {gate.dtype}"
ctx.save_for_backward(gate, up)

@haok1402

Copy link
Copy Markdown
Collaborator

@claude review

Comment on lines +49 to +61
# Flat-grid configs as (BLOCK threads, ELEMS_PER_CTA), selected by element count.
_CONFIG_SMALL = (256, 2048) # small problems
_CONFIG_MID = (128, 2048) # medium problems ("current")
_CONFIG_LARGE = (128, 1024) # large problems


def _select_config(n: int) -> tuple[int, int]:
"""Pick (BLOCK, ELEMS_PER_CTA) for a flat array of ``n`` elements."""
if n < 4194304:
return _CONFIG_SMALL
if n < 16777216:
return _CONFIG_MID
return _CONFIG_LARGE

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Compactness: The sibling Triton silu_mul.py runs every shape through one fixed _BLOCK_SIZE = 1024; this port adds three configs plus _select_config with two magic thresholds. The PR's own benchmark table is ~1.00–1.05x across all sizes, so the extra branches appear to buy near-nothing over a single config. Unless the SMALL/LARGE variants measurably beat MID in isolation, collapse to the one _CONFIG_MID constant (matching the sibling) and drop _select_config — it removes the thresholds, two config tuples, and a param threaded through every builder/cache function.

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Compactness Review

Clean, self-contained TIRx port of the sibling Triton silu_mul.py, and it stays close to that file. The only structural addition beyond what the sibling needs is the three-way size-based config selection (_select_config + _CONFIG_SMALL/MID/LARGE), where the sibling gets by with one fixed constant and the PR benchmark shows ~1.00–1.05x across all shapes — flagged inline as likely tuning surface for a marginal payoff. Everything else (dtype map, vectorization, compile+cache) is inherent to TIRx and warranted. No other compactness concerns.

@@ -0,0 +1,258 @@
"""
TIRx implementation of the fused SwiGLU-style ``silu(gate) * up`` autograd
function (a port of the Triton kernels in ``silu_mul.py``).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consistency: silu_mul.py is this file's own basename, so a reader can't tell it points at the sibling Triton module (pithtrain/operators/silu_mul.py) rather than self-referencing. Qualify the path.

Suggested change
function (a port of the Triton kernels in ``silu_mul.py``).
function (a port of the Triton kernels in ``operators/silu_mul.py``).


# Flat-grid configs as (BLOCK threads, ELEMS_PER_CTA), selected by element count.
_CONFIG_SMALL = (256, 2048) # small problems
_CONFIG_MID = (128, 2048) # medium problems ("current")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Consistency: ("current") is a leftover tuning annotation that describes nothing to a future reader — and it misleads, since MID is not an always-selected default (config is chosen by n in _select_config). Drop it.

Suggested change
_CONFIG_MID = (128, 2048) # medium problems ("current")
_CONFIG_MID = (128, 2048) # medium problems

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Consistency Review

Add-only PR (one new file, pithtrain/operators/tirx/silu_mul.py); no symbols renamed, files moved, or signatures changed, so nothing elsewhere in the repo goes stale — no stale references found. The new docstring accurately mirrors the code (grid/tile math, load/store counts, config-by-n selection all check out). Two minor doc/comment nits inline: the module docstring references silu_mul.py by a basename that collides with this file's own name (should point at the sibling Triton module explicitly), and a leftover ("current") tuning annotation on _CONFIG_MID that describes nothing and slightly misleads.

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Performance Review

No performance regression identified. The new TIRx kernel is a standalone addition (pithtrain/operators/tirx/silu_mul.py) that is wired into nothing — all models still import the Triton silu_mul — so it cannot affect training step time or peak memory today. Forward saves only gate/up (no extra activation memory vs the Triton path), and the committed min-of-N microbenchmark shows parity (fwd 1.00–1.05×, bwd ~1.00×), which is honest and adequate evidence for a kernel swap.

Two notes for when/if this replaces the Triton kernel: (1) the sole call site (Experts.forward inside forward_stage3) runs eager — forward_stage3 is not @torch.compiled, unlike stage1/stage5 — so there is no graph-break concern, but there is also no compiled fusion to gain here; and (2) the microbenchmark is pure-kernel time and excludes the autograd wrapper — notably backward calls grad_output.contiguous() where the Triton reference only asserts contiguity; harmless while grad is already contiguous (as it is at this call site) but a silent full-tensor copy per backward step otherwise. Given parity, the swap is lateral, not a win.

return ex


class _SiLUMulTirx(torch.autograd.Function):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correctness: This is a custom-autograd kernel (defines the backward for silu(gate)*up), but the PR's only evidence is a speedup table — no numerical/gradient parity is shown, and nothing tests it: tests/test_silu_mul.py imports the Triton silu_mul, not silu_mul_tirx, and the op is not wired into any model. A wrong grad_gate/grad_up here would leave forward and loss identical while silently corrupting gradients, and there is no test to catch it.

Add a test analogous to test_silu_mul.py that compares forward output and both grads against F.silu(gate)*up / the Triton kernel. Note the shapes must be vector-divisible (numel % 8 for bf16/fp16) — the forward asserts this (line 210), so this port does not accept the arbitrary shapes the Triton version handles (e.g. the (1023, 777) case in the existing test would raise).

Comment on lines +209 to +211
n = gate.numel()
assert n % (_VEC_BYTES // elem_bytes) == 0, "numel must be vector-divisible"
block, tile = _select_config(n)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correctness: Empty input (n == 0) launches a zero-block grid — (0 + TILE - 1)//TILE == 0 at line 83 — which raises a CUDA invalid-configuration error at launch. The Triton reference tolerates grid=(0,) as a no-op, so this port diverges on the empty-group edge case (an MoE expert receiving 0 tokens). Guard it with an early return in both forward and backward:

Suggested change
n = gate.numel()
assert n % (_VEC_BYTES // elem_bytes) == 0, "numel must be vector-divisible"
block, tile = _select_config(n)
n = gate.numel()
if n == 0:
return out
assert n % (_VEC_BYTES // elem_bytes) == 0, "numel must be vector-divisible"
block, tile = _select_config(n)

Backward needs the same guard (if n == 0: return grad_gate, grad_up after line 232).

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Correctness Review

The kernel math is a faithful port of the Triton reference: the forward silu(gate)*up and the backward grad_up = g*silu, grad_gate = g*up*sig*(1+gate-silu) match exactly, and the flat-grid tiling / col < n guard are sound given the vector-divisible-n invariant. No numerical bug found in the compute.

The gaps are around evidence and edge cases:

  • No correctness evidence. This is a custom-autograd kernel affecting gradients, but the PR ships only a speedup table — no forward/grad parity, no test (test_silu_mul.py covers the Triton op, not this one), and it is not wired into any model. A wrong backward would keep loss identical while corrupting gradients, uncaught. A parity test vs F.silu(gate)*up on both grads would settle it.
  • Empty-group edge case. n == 0 produces a zero-block grid → CUDA launch error, diverging from the Triton path which no-ops. Needs an early return in forward and backward.

Both are inline. One caveat worth noting: the forward asserts numel is vector-divisible, so this is not a drop-in for the arbitrary shapes the Triton version accepts.

@MasterJH5574

Copy link
Copy Markdown
Member

We'll need to test whether all kernels work on H100/200.

@haok1402

haok1402 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Also, would TVM be a new dependency? I believe the current environment won't install it.

@yagneekp

Copy link
Copy Markdown
Author

We'll need to test whether all kernels work on H100/200.

How are we testing on Hopper? Are we using modal?

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.

3 participants