Download eugen_sparse.py from JakCwi/eugen-sparse: direct link, hf CLI and curl.
- Browser
- Download file 44.9 kB
-
https://huggingface.co/JakCwi/eugen-sparse/resolve/main/eugen_sparse.py
- Command line
-
hf download hf://JakCwi/eugen-sparse/eugen_sparse.py
-
curl -L -o eugen_sparse.py https://huggingface.co/JakCwi/eugen-sparse/resolve/main/eugen_sparse.py
44.9 kB
| # SPDX-License-Identifier: CC-BY-NC-4.0 | |
| # Copyright (c) 2026 Jakub Ćwirlej | |
| """EUGEN sparse SwiGLU FFN: grouped top-k neuron selection + Triton kernels (forward and backward). | |
| HOW IT WORKS | |
| FFN neurons are split into groups of `group_size` consecutive neurons. For every token, | |
| `grouped_topk_select` scores each group by its summed |SiLU(gate)| energy (optionally on a fixed | |
| subsample of neurons per group, see `make_subsample_index`) and keeps the top `num_active_groups`. | |
| `sparse_swiglu_ffn` then computes gate, up AND down only for the selected groups — the full | |
| [tokens, d_ff] activation is never materialised, neither in the forward nor in the backward pass. | |
| `update_load_balance_bias` keeps group usage close to uniform (auxiliary-loss-free balancing). | |
| Inside `sparse_swiglu_ffn` (one torch.library.triton_op, traced by torch.compile, no graph breaks): | |
| * token->group pairs are bucketed per group (CSR) by a deterministic Triton counting sort, split | |
| into token chunks so that the atomic accumulators of one chunk stay in L2; | |
| * forward: gate and up GEMMs in one pass over X (SiLU stays in registers), then the down GEMM | |
| accumulated into y with relaxed fp32 atomics; | |
| * backward: dP with fused epilogues (SiLU' computed in place), dX for both input projections in | |
| one pass, weight gradients per group (deterministic when there is a single token chunk). | |
| Without CUDA/Triton, or for an unsupported shape, the same math runs as a dense masked PyTorch | |
| path (slow; a warning is printed once on CUDA tensors). | |
| PRACTICAL GUIDANCE | |
| 1. Density (active fraction = num_active_groups * group_size / d_ff) | |
| - Recommended: 5-10% active. Below ~5% the layer becomes too sparse; above ~15% the sparse path | |
| stops paying off against a dense FFN of the same width. Our training configuration is 6.25%: | |
| d_ff 4096, group_size 32, 8 active groups, gate subsample of 8 rows per group. | |
| - Measured at 6.25%: a single FFN block ran 1.26x (d_ff 4096) and 1.34x (d_ff 8192) faster than its | |
| dense equivalent; a full model (d_model 512, d_ff 4096) trained 1.23x faster than the dense model | |
| of the same FFN capacity, before further kernel optimisations (+36% throughput). | |
| - The gain grows with FFN width: prefer a wide FFN at low density over a narrow FFN at high density. | |
| d_ff does not have to be a power of two (e.g. 1536, 3072, 3200) — only a multiple of group_size. | |
| - Keep enough tokens per group in a micro-batch: num_tokens * num_active_groups / num_groups should be | |
| well above the token tile (64), ideally in the hundreds; small micro-batches leave tiles half empty. | |
| 2. Shapes and tiles (Triton, bf16 tensor cores) | |
| - group_size must be a power of two >= 16 (it is a tl.arange extent and a tl.dot dimension). | |
| 32 is our training value; up to 64 the fused kernels are used, above that an unfused pair of | |
| kernels (same result, more memory traffic) because of the shared-memory limit. | |
| - d_ff must be a multiple of group_size (num_groups = d_ff // group_size, any number >= 2). | |
| - d_model must be a multiple of 16; the d_model tiles (64 / 128 / 128 by default) shrink to the | |
| largest power of two that divides d_model, so multiples of 128 run at full tile width. | |
| - Use bf16: tl.dot runs on tensor cores with fp32 accumulation. The kernels inherit the input dtype, | |
| so under autocast `sparse_swiglu_ffn` casts x and the weights to the autocast dtype itself. | |
| - Gradients: x, gate, up and down weights; only the selected groups receive weight gradients. | |
| The selection itself is a hard top-k (not differentiable). | |
| 3. Correctness check | |
| - reference_sparse_swiglu_ffn is a dense masked reference: compare outputs and gradients against it | |
| on small shapes before training (test_eugen_sparse.py does this; on a machine without a GPU run it | |
| with TRITON_INTERPRET=1). | |
| Tuning knobs (environment, read at import; defaults = the configuration we trained with): | |
| EUGEN_SPARSE_CH (4) token chunks, _NT (8) token programs per (group, chunk), _BT (64) token tile, | |
| _BD (64) / _BDY (128) / _BDW (128) d_model tiles, _NS (1), _DW_DET (0; 1 = always deterministic | |
| weight gradients), _WARPS (8), _WARPS_W (8), _FUSION_G_MAX (64), _CSR_CTA (256), _CSR_CHUNK (0 = auto). | |
| """ | |
| import os | |
| import warnings | |
| from typing import Tuple | |
| import torch | |
| import torch.nn.functional as F | |
| try: | |
| import triton | |
| import triton.language as tl | |
| _HAS_TRITON = True | |
| except Exception: # pragma: no cover | |
| _HAS_TRITON = False | |
| try: | |
| from torch.library import triton_op, wrap_triton | |
| _HAS_TRITON_OP = True | |
| except Exception: # pragma: no cover | |
| _HAS_TRITON_OP = False | |
| __all__ = [ | |
| "make_subsample_index", | |
| "grouped_topk_select", | |
| "update_load_balance_bias", | |
| "sparse_swiglu_ffn", | |
| "reference_sparse_swiglu_ffn", | |
| "kernel_supported", | |
| "plan", | |
| ] | |
| def _env(name, default): | |
| return int(os.environ.get('EUGEN_SPARSE_' + name, str(default)) or default) | |
| _CH = max(1, _env('CH', 4)) | |
| _NT = _env('NT', 8) | |
| _BT = _env('BT', 64) | |
| _BD = _env('BD', 64) | |
| _BDY = _env('BDY', 128) | |
| _BDW = _env('BDW', 128) | |
| _NS = max(1, _env('NS', 1)) | |
| _DW_DET = os.environ.get('EUGEN_SPARSE_DW_DET', '0') == '1' | |
| _WARPS = _env('WARPS', 8) | |
| _WARPS_W = _env('WARPS_W', 8) | |
| _FUSION_G_MAX = _env('FUSION_G_MAX', 64) | |
| _CSR_CTA = _env('CSR_CTA', 256) | |
| _CSR_CHUNK = _env('CSR_CHUNK', 0) | |
| _SUB = 64 # counting-sort sub-tile (rank O(SUB^2) in registers) | |
| _CHUNK = 8192 # reference chunk; defines the token chunks | |
| def _fusion_ok(G): | |
| return G <= _FUSION_G_MAX | |
| def _pick_block(dim, preferred): | |
| """Largest power of two <= preferred that divides dim (0 if none >= 16).""" | |
| block = 1 << (min(preferred, dim).bit_length() - 1) | |
| while block >= 16 and dim % block != 0: | |
| block //= 2 | |
| return block if block >= 16 else 0 | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Selection, load balancing and the dense masked reference (PyTorch) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def make_subsample_index(d_ff, group_size, samples_per_group, seed=0): | |
| """Fixed gate-row subsample for grouped_topk_select: the same number of random rows inside every | |
| group. Draw ONCE per layer and keep it with the model (register_buffer(..., persistent=True)), so | |
| the network can make these rows representative and a resumed run uses the same rows. | |
| Returns (subsample_idx int64 [num_groups * samples_per_group], samples_per_group).""" | |
| num_groups = d_ff // group_size | |
| spg = max(1, min(group_size, samples_per_group)) | |
| gen = torch.Generator().manual_seed(seed) | |
| loc = torch.stack([torch.randperm(group_size, generator=gen)[:spg] for _ in range(num_groups)]) | |
| return (loc + torch.arange(num_groups).unsqueeze(1) * group_size).reshape(-1), spg | |
| def grouped_topk_select(x, gate_weight, group_size, num_active_groups, | |
| subsample_idx=None, samples_per_group=None, | |
| load_balance_bias=None): | |
| """Indices [..., num_active_groups] of the groups with the largest |SiLU(gate)| energy. | |
| With subsample_idx the energy is estimated from those gate rows only and scaled to the full | |
| group (unbiased); load_balance_bias [num_groups] multiplies the energy by exp(bias).""" | |
| d_model = x.shape[-1] | |
| num_groups = gate_weight.shape[0] // group_size | |
| x2 = x.reshape(-1, d_model) | |
| if subsample_idx is not None: | |
| assert samples_per_group is not None, \ | |
| "samples_per_group is required together with subsample_idx" | |
| gate_act = F.silu(F.linear(x2, gate_weight[subsample_idx])) | |
| energy = gate_act.abs().view(-1, num_groups, samples_per_group).sum(-1) | |
| energy = energy * (group_size / samples_per_group) | |
| else: | |
| gate_act = F.silu(F.linear(x2, gate_weight)) | |
| energy = gate_act.abs().view(-1, num_groups, group_size).sum(-1) | |
| if load_balance_bias is not None: | |
| energy = energy * torch.exp(load_balance_bias.to(energy.dtype)) | |
| idx = energy.topk(num_active_groups, dim=-1).indices | |
| return idx.view(*x.shape[:-1], num_active_groups) | |
| def update_load_balance_bias(bias, selection_freq_ema, current_freq, target_freq, | |
| gamma, bias_max, momentum): | |
| """Call once per optimizer step. current_freq [num_groups] = fraction of tokens that selected each | |
| group in this step; target_freq = num_active_groups / num_groups. A group whose smoothed frequency | |
| leaves the dead zone [0.5, 2] x target gets its bias moved by -gamma * sign(freq - target). | |
| Values we trained with: gamma 1e-3, bias_max 0.5, momentum 0.99.""" | |
| selection_freq_ema.mul_(momentum).add_(current_freq, alpha=1.0 - momentum) | |
| outside = (selection_freq_ema < 0.5 * target_freq) | (selection_freq_ema > 2.0 * target_freq) | |
| step = -gamma * torch.sign(selection_freq_ema - target_freq) | |
| bias.add_(torch.where(outside, step, torch.zeros_like(bias))) | |
| bias.clamp_(-bias_max, bias_max) | |
| def reference_sparse_swiglu_ffn(x, gate_weight, up_weight, down_weight, group_idx, group_size): | |
| """Dense masked reference: same math as the kernel, plain PyTorch, differentiable.""" | |
| d_model = x.shape[-1] | |
| x2 = x.reshape(-1, d_model) | |
| d_ff = gate_weight.shape[0] | |
| num_groups = d_ff // group_size | |
| idx2 = group_idx.reshape(-1, group_idx.shape[-1]).to(torch.int64) | |
| num_tokens = x2.shape[0] | |
| group_mask = torch.zeros(num_tokens, num_groups, dtype=torch.bool, device=x2.device) | |
| group_mask.scatter_(1, idx2, True) | |
| full_mask = group_mask.unsqueeze(-1).expand(num_tokens, num_groups, group_size) | |
| full_mask = full_mask.reshape(num_tokens, d_ff) | |
| gate = F.silu(F.linear(x2, gate_weight)) | |
| up = F.linear(x2, up_weight) | |
| hidden = gate * up * full_mask.to(gate.dtype) | |
| y = F.linear(hidden, down_weight) | |
| return y.view(*x.shape[:-1], d_model) | |
| def _plan_chunks(P): | |
| """(ncta, ch, r, chunk). ch = number of token chunks (affects numerics: ch == 1 and NS == 1 give | |
| deterministic weight gradients), derived ONLY from the reference chunk; ncta = counting-sort CTAs | |
| (pure performance, no effect on the result), raised towards EUGEN_SPARSE_CSR_CTA by halving the | |
| chunk while keeping ncta divisible by ch.""" | |
| ncta_ref = triton.cdiv(P, _CHUNK) if _HAS_TRITON else max(1, -(-P // _CHUNK)) | |
| ch = min(_CH, ncta_ref) | |
| while ch > 1 and ncta_ref % ch != 0: | |
| ch -= 1 | |
| if not _HAS_TRITON: | |
| return ncta_ref, ch, ncta_ref // ch, _CHUNK | |
| if _CSR_CHUNK > 0: | |
| n = triton.cdiv(P, _CSR_CHUNK) | |
| if n % ch == 0: | |
| return n, ch, n // ch, _CSR_CHUNK | |
| return ncta_ref, ch, ncta_ref // ch, _CHUNK | |
| chunk, ncta = _CHUNK, ncta_ref | |
| c = _CHUNK | |
| while c > _SUB and ncta < _CSR_CTA: | |
| c //= 2 | |
| n = triton.cdiv(P, c) | |
| if n % ch == 0: | |
| chunk, ncta = c, n | |
| assert ncta % ch == 0, (ncta, ch) | |
| return ncta, ch, ncta // ch, chunk | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Triton kernels. A "pair" p = tok * kg + slot indexes group_idx [M, kg]; the CSR sorts pairs by | |
| # group. Segment = (token chunk cid, group gid); off2 [ch+1, ng] holds its bounds. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| if _HAS_TRITON: | |
| def _csr_hist(GIDX, HIST, P, | |
| NG: tl.constexpr, NG_POW: tl.constexpr, | |
| SUB: tl.constexpr, CHUNK: tl.constexpr): | |
| """Per-CTA histogram HIST[cta, g]. Register block NG_POW = next_power_of_2(NG); | |
| masked elements use the sentinel -1, which never matches a bin.""" | |
| pid = tl.program_id(0) | |
| bins = tl.arange(0, NG_POW) | |
| m_b = bins < NG | |
| run = tl.zeros([NG_POW], dtype=tl.int32) | |
| start = pid * CHUNK | |
| end = tl.minimum(start + CHUNK, P) | |
| for s0 in range(start, end, SUB): | |
| offs = s0 + tl.arange(0, SUB) | |
| m = offs < end | |
| gv = tl.load(GIDX + offs, mask=m, other=0) | |
| g = tl.where(m, gv, -1) | |
| oh = (g[:, None] == bins[None, :]) | |
| run += tl.sum(oh.to(tl.int32), axis=0) | |
| tl.store(HIST + pid * NG + bins, run, mask=m_b) | |
| def _csr_scan(HIST, COLPRE, OFF, NCTA, | |
| NG: tl.constexpr, NG_POW: tl.constexpr): | |
| """Single CTA: per-CTA column prefixes COLPRE (NCTA+1 rows; the last row holds the | |
| per-group totals) and the exclusive scan of group sizes -> OFF[ng+1].""" | |
| bins = tl.arange(0, NG_POW) | |
| m_b = bins < NG | |
| run = tl.zeros([NG_POW], dtype=tl.int32) | |
| for c in range(0, NCTA): | |
| h = tl.load(HIST + c * NG + bins, mask=m_b, other=0) | |
| tl.store(COLPRE + c * NG + bins, run, mask=m_b) | |
| run += h | |
| tl.store(COLPRE + NCTA * NG + bins, run, mask=m_b) | |
| inc = tl.cumsum(run, axis=0) | |
| tl.store(OFF + bins, inc - run, mask=m_b) | |
| tl.store(OFF + NG, tl.sum(run, axis=0)) | |
| def _csr_scatter(GIDX, COLPRE, OFF, PAIRS, P, | |
| NG: tl.constexpr, NG_POW: tl.constexpr, | |
| SUB: tl.constexpr, CHUNK: tl.constexpr): | |
| """Scatter with a deterministic rank: position = off[g] + CTA prefix + prefix of | |
| earlier sub-tiles + rank inside the sub-tile (O(SUB^2)). Equals a stable argsort.""" | |
| pid = tl.program_id(0) | |
| bins = tl.arange(0, NG_POW) | |
| m_b = bins < NG | |
| a = tl.arange(0, SUB) | |
| base = (tl.load(OFF + bins, mask=m_b, other=0) | |
| + tl.load(COLPRE + pid * NG + bins, mask=m_b, other=0)) | |
| run = tl.zeros([NG_POW], dtype=tl.int32) | |
| start = pid * CHUNK | |
| end = tl.minimum(start + CHUNK, P) | |
| for s0 in range(start, end, SUB): | |
| offs = s0 + a | |
| m = offs < end | |
| gv = tl.load(GIDX + offs, mask=m, other=0) | |
| g = tl.where(m, gv, -1) | |
| oh = (g[:, None] == bins[None, :]) | |
| eq = (g[:, None] == g[None, :]) & (a[None, :] < a[:, None]) | |
| lrank = tl.sum(eq.to(tl.int32), axis=1) | |
| pref = tl.sum(tl.where(oh, (base + run)[None, :], 0), axis=1) | |
| tl.store(PAIRS + pref + lrank, offs.to(tl.int32), mask=m) | |
| run += tl.sum(oh.to(tl.int32), axis=0) | |
| def _gate_up(X, WG, WU, GACC, UP, PSEL, PAIRS, OFF2, | |
| D: tl.constexpr, FDIM: tl.constexpr, G: tl.constexpr, | |
| KG: tl.constexpr, NG: tl.constexpr, NT: tl.constexpr, | |
| BT: tl.constexpr, BD: tl.constexpr): | |
| """Gate and up projections for the selected groups in ONE pass over the CSR (one gather | |
| of X): GACC = X @ WG[g]^T (rounded to the input dtype BEFORE silu, exactly as | |
| F.silu(F.linear(...)) does under autocast), UP = X @ WU[g]^T, PSEL = silu(GACC) * UP. | |
| silu(GACC) lives only in registers; the backward recomputes it from GACC.""" | |
| gc = tl.program_id(0) | |
| gid = gc % NG | |
| cid = gc // NG | |
| pid_t = tl.program_id(1) | |
| lo = tl.load(OFF2 + cid * NG + gid) | |
| hi = tl.load(OFF2 + (cid + 1) * NG + gid) | |
| offs_g = tl.arange(0, G) | |
| start = lo + pid_t * BT | |
| while start < hi: | |
| offs_t = start + tl.arange(0, BT) | |
| m_t = offs_t < hi | |
| par = tl.load(PAIRS + offs_t, mask=m_t, other=0) | |
| tq = par // KG | |
| slot = par - tq * KG | |
| tok = tq.to(tl.int64) | |
| acc_g = tl.zeros((BT, G), dtype=tl.float32) | |
| acc_u = tl.zeros((BT, G), dtype=tl.float32) | |
| for d0 in range(0, D, BD): | |
| offs_d = d0 + tl.arange(0, BD) | |
| xb = tl.load(X + tok[:, None] * D + offs_d[None, :], | |
| mask=m_t[:, None], other=0.0) | |
| wgb = tl.load(WG + (gid * G + offs_g)[None, :].to(tl.int64) * D | |
| + offs_d[:, None]) | |
| wub = tl.load(WU + (gid * G + offs_g)[None, :].to(tl.int64) * D | |
| + offs_d[:, None]) | |
| acc_g += tl.dot(xb, wgb) | |
| acc_u += tl.dot(xb, wub) | |
| base = tok[:, None] * (KG * G) + slot[:, None] * G + offs_g[None, :] | |
| gb = acc_g.to(GACC.dtype.element_ty) | |
| tl.store(GACC + base, gb, mask=m_t[:, None]) | |
| gf = gb.to(tl.float32) | |
| sb = gf * tl.sigmoid(gf) | |
| sbc = sb.to(PSEL.dtype.element_ty) | |
| ub = acc_u.to(UP.dtype.element_ty) | |
| tl.store(UP + base, ub, mask=m_t[:, None]) | |
| tl.store(PSEL + base, sbc * ub, mask=m_t[:, None]) | |
| start += NT * BT | |
| def _gate(X, WG, GACC, SIL, PAIRS, OFF2, | |
| D: tl.constexpr, G: tl.constexpr, | |
| KG: tl.constexpr, NG: tl.constexpr, NT: tl.constexpr, | |
| BT: tl.constexpr, BD: tl.constexpr): | |
| """Gate projection for the selected groups only (used when G is above the fusion | |
| threshold): GACC = X @ WG[g]^T rounded to the input dtype, SIL = silu(GACC).""" | |
| gc = tl.program_id(0) | |
| gid = gc % NG | |
| cid = gc // NG | |
| pid_t = tl.program_id(1) | |
| lo = tl.load(OFF2 + cid * NG + gid) | |
| hi = tl.load(OFF2 + (cid + 1) * NG + gid) | |
| offs_g = tl.arange(0, G) | |
| start = lo + pid_t * BT | |
| while start < hi: | |
| offs_t = start + tl.arange(0, BT) | |
| m_t = offs_t < hi | |
| par = tl.load(PAIRS + offs_t, mask=m_t, other=0) | |
| tq = par // KG | |
| slot = par - tq * KG | |
| tok = tq.to(tl.int64) | |
| acc = tl.zeros((BT, G), dtype=tl.float32) | |
| for d0 in range(0, D, BD): | |
| offs_d = d0 + tl.arange(0, BD) | |
| xb = tl.load(X + tok[:, None] * D + offs_d[None, :], | |
| mask=m_t[:, None], other=0.0) | |
| wb = tl.load(WG + (gid * G + offs_g)[None, :].to(tl.int64) * D | |
| + offs_d[:, None]) | |
| acc += tl.dot(xb, wb) | |
| base = tok[:, None] * (KG * G) + slot[:, None] * G + offs_g[None, :] | |
| gb = acc.to(GACC.dtype.element_ty) | |
| tl.store(GACC + base, gb, mask=m_t[:, None]) | |
| gf = gb.to(tl.float32) | |
| sb = gf * tl.sigmoid(gf) | |
| tl.store(SIL + base, sb.to(SIL.dtype.element_ty), mask=m_t[:, None]) | |
| start += NT * BT | |
| def _up(X, WU, SIL, UP, PSEL, PAIRS, OFF2, | |
| D: tl.constexpr, FDIM: tl.constexpr, G: tl.constexpr, | |
| KG: tl.constexpr, NG: tl.constexpr, NT: tl.constexpr, | |
| BT: tl.constexpr, BD: tl.constexpr): | |
| """UP = X @ WU[g]^T for the selected groups, PSEL = SIL * UP (used when G is above the | |
| fusion threshold).""" | |
| gc = tl.program_id(0) | |
| gid = gc % NG | |
| cid = gc // NG | |
| pid_t = tl.program_id(1) | |
| lo = tl.load(OFF2 + cid * NG + gid) | |
| hi = tl.load(OFF2 + (cid + 1) * NG + gid) | |
| offs_g = tl.arange(0, G) | |
| start = lo + pid_t * BT | |
| while start < hi: | |
| offs_t = start + tl.arange(0, BT) | |
| m_t = offs_t < hi | |
| par = tl.load(PAIRS + offs_t, mask=m_t, other=0) | |
| tq = par // KG | |
| slot = par - tq * KG | |
| tok = tq.to(tl.int64) | |
| acc = tl.zeros((BT, G), dtype=tl.float32) | |
| for d0 in range(0, D, BD): | |
| offs_d = d0 + tl.arange(0, BD) | |
| xb = tl.load(X + tok[:, None] * D + offs_d[None, :], | |
| mask=m_t[:, None], other=0.0) | |
| wb = tl.load(WU + (gid * G + offs_g)[None, :].to(tl.int64) * D | |
| + offs_d[:, None]) | |
| acc += tl.dot(xb, wb) | |
| ub = acc.to(UP.dtype.element_ty) | |
| sb = tl.load(SIL + tok[:, None] * (KG * G) + slot[:, None] * G | |
| + offs_g[None, :], mask=m_t[:, None], other=0.0) | |
| base = tok[:, None] * (KG * G) + slot[:, None] * G + offs_g[None, :] | |
| tl.store(UP + base, ub, mask=m_t[:, None]) | |
| tl.store(PSEL + base, sb * ub, mask=m_t[:, None]) | |
| start += NT * BT | |
| def _down(PSEL, WD, Y, PAIRS, OFF2, | |
| D: tl.constexpr, FDIM: tl.constexpr, G: tl.constexpr, | |
| KG: tl.constexpr, NG: tl.constexpr, NT: tl.constexpr, | |
| BT: tl.constexpr, BDY: tl.constexpr, WDT: tl.constexpr = 0): | |
| """Y[tok, :] += PSEL[tok, slot, :] @ WD[:, g]^T, fp32 atomic_add with relaxed semantics | |
| (Y is read only after the kernel finishes). D tile = BDY.""" | |
| gc = tl.program_id(0) | |
| gid = gc % NG | |
| cid = gc // NG | |
| pid_t = tl.program_id(1) | |
| pid_d = tl.program_id(2) | |
| lo = tl.load(OFF2 + cid * NG + gid) | |
| hi = tl.load(OFF2 + (cid + 1) * NG + gid) | |
| offs_g = tl.arange(0, G) | |
| offs_d = pid_d * BDY + tl.arange(0, BDY) | |
| start = lo + pid_t * BT | |
| while start < hi: | |
| offs_t = start + tl.arange(0, BT) | |
| m_t = offs_t < hi | |
| par = tl.load(PAIRS + offs_t, mask=m_t, other=0) | |
| tq = par // KG | |
| slot = par - tq * KG | |
| tok = tq.to(tl.int64) | |
| pb = tl.load(PSEL + tok[:, None] * (KG * G) + slot[:, None] * G | |
| + offs_g[None, :], mask=m_t[:, None], other=0.0) | |
| if WDT: | |
| wb = tl.load(WD + (gid * G + offs_g)[:, None].to(tl.int64) * D | |
| + offs_d[None, :]) | |
| else: | |
| wb = tl.load(WD + offs_d[None, :].to(tl.int64) * FDIM | |
| + (gid * G + offs_g)[:, None]) | |
| acc = tl.dot(pb, wb) | |
| tl.atomic_add(Y + tok[:, None] * D + offs_d[None, :], acc, | |
| mask=m_t[:, None], sem='relaxed') | |
| start += NT * BT | |
| def _dp(DY, WD, GACC, UPSEL, DGATE, DUP, PAIRS, OFF2, | |
| D: tl.constexpr, FDIM: tl.constexpr, G: tl.constexpr, | |
| KG: tl.constexpr, NG: tl.constexpr, NT: tl.constexpr, | |
| BT: tl.constexpr, BD: tl.constexpr, WDT: tl.constexpr = 0): | |
| """dP = dY @ WD[:, g] with fused epilogues: dgate = dP * up * silu'(gacc) and | |
| dup = dP * silu(gacc); dP never touches memory, silu is recomputed from GACC.""" | |
| gc = tl.program_id(0) | |
| gid = gc % NG | |
| cid = gc // NG | |
| pid_t = tl.program_id(1) | |
| lo = tl.load(OFF2 + cid * NG + gid) | |
| hi = tl.load(OFF2 + (cid + 1) * NG + gid) | |
| offs_g = tl.arange(0, G) | |
| start = lo + pid_t * BT | |
| while start < hi: | |
| offs_t = start + tl.arange(0, BT) | |
| m_t = offs_t < hi | |
| par = tl.load(PAIRS + offs_t, mask=m_t, other=0) | |
| tq = par // KG | |
| slot = par - tq * KG | |
| tok = tq.to(tl.int64) | |
| acc = tl.zeros((BT, G), dtype=tl.float32) | |
| for d0 in range(0, D, BD): | |
| offs_d = d0 + tl.arange(0, BD) | |
| dyb = tl.load(DY + tok[:, None] * D + offs_d[None, :], | |
| mask=m_t[:, None], other=0.0) | |
| if WDT: | |
| wb = tl.load(WD + (gid * G + offs_g)[None, :].to(tl.int64) * D | |
| + offs_d[:, None]) | |
| else: | |
| wb = tl.load(WD + offs_d[:, None].to(tl.int64) * FDIM | |
| + gid * G + offs_g[None, :]) | |
| acc += tl.dot(dyb, wb) | |
| base = tok[:, None] * (KG * G) + slot[:, None] * G + offs_g[None, :] | |
| gb = tl.load(GACC + base, mask=m_t[:, None], other=0.0).to(tl.float32) | |
| sg = tl.sigmoid(gb) | |
| sb = gb * sg | |
| ub = tl.load(UPSEL + base, mask=m_t[:, None], other=0.0).to(tl.float32) | |
| tl.store(DGATE + base, | |
| (acc * ub * sg * (1.0 + gb * (1.0 - sg))).to(DGATE.dtype.element_ty), | |
| mask=m_t[:, None]) | |
| tl.store(DUP + base, (acc * sb).to(DUP.dtype.element_ty), | |
| mask=m_t[:, None]) | |
| start += NT * BT | |
| def _dx2(DUP, WU, DGATE, WG, DX, PAIRS, OFF2, | |
| D: tl.constexpr, FDIM: tl.constexpr, G: tl.constexpr, | |
| KG: tl.constexpr, NG: tl.constexpr, NT: tl.constexpr, | |
| BT: tl.constexpr, BDY: tl.constexpr): | |
| """dX[tok, :] += dup @ WU[g] + dgate @ WG[g] in one pass (both contributions summed in | |
| registers before a single relaxed fp32 atomic_add).""" | |
| gc = tl.program_id(0) | |
| gid = gc % NG | |
| cid = gc // NG | |
| pid_t = tl.program_id(1) | |
| pid_d = tl.program_id(2) | |
| lo = tl.load(OFF2 + cid * NG + gid) | |
| hi = tl.load(OFF2 + (cid + 1) * NG + gid) | |
| offs_g = tl.arange(0, G) | |
| offs_d = pid_d * BDY + tl.arange(0, BDY) | |
| start = lo + pid_t * BT | |
| while start < hi: | |
| offs_t = start + tl.arange(0, BT) | |
| m_t = offs_t < hi | |
| par = tl.load(PAIRS + offs_t, mask=m_t, other=0) | |
| tq = par // KG | |
| slot = par - tq * KG | |
| tok = tq.to(tl.int64) | |
| base = tok[:, None] * (KG * G) + slot[:, None] * G + offs_g[None, :] | |
| dub = tl.load(DUP + base, mask=m_t[:, None], other=0.0) | |
| dgb = tl.load(DGATE + base, mask=m_t[:, None], other=0.0) | |
| wrow = (gid * G + offs_g)[:, None].to(tl.int64) * D + offs_d[None, :] | |
| wu = tl.load(WU + wrow) | |
| wg = tl.load(WG + wrow) | |
| acc = tl.dot(dub, wu) + tl.dot(dgb, wg) | |
| tl.atomic_add(DX + tok[:, None] * D + offs_d[None, :], acc, | |
| mask=m_t[:, None], sem='relaxed') | |
| start += NT * BT | |
| def _dx(DUP, WU, DX, PAIRS, OFF2, | |
| D: tl.constexpr, FDIM: tl.constexpr, G: tl.constexpr, | |
| KG: tl.constexpr, NG: tl.constexpr, NT: tl.constexpr, | |
| BT: tl.constexpr, BDY: tl.constexpr): | |
| """dX[tok, :] += dsel @ W[g] (relaxed fp32 atomic_add); used twice when G is above the | |
| fusion threshold.""" | |
| gc = tl.program_id(0) | |
| gid = gc % NG | |
| cid = gc // NG | |
| pid_t = tl.program_id(1) | |
| pid_d = tl.program_id(2) | |
| lo = tl.load(OFF2 + cid * NG + gid) | |
| hi = tl.load(OFF2 + (cid + 1) * NG + gid) | |
| offs_g = tl.arange(0, G) | |
| offs_d = pid_d * BDY + tl.arange(0, BDY) | |
| start = lo + pid_t * BT | |
| while start < hi: | |
| offs_t = start + tl.arange(0, BT) | |
| m_t = offs_t < hi | |
| par = tl.load(PAIRS + offs_t, mask=m_t, other=0) | |
| tq = par // KG | |
| slot = par - tq * KG | |
| tok = tq.to(tl.int64) | |
| dub = tl.load(DUP + tok[:, None] * (KG * G) + slot[:, None] * G | |
| + offs_g[None, :], mask=m_t[:, None], other=0.0) | |
| wb = tl.load(WU + (gid * G + offs_g)[:, None].to(tl.int64) * D | |
| + offs_d[None, :]) | |
| acc = tl.dot(dub, wb) | |
| tl.atomic_add(DX + tok[:, None] * D + offs_d[None, :], acc, | |
| mask=m_t[:, None], sem='relaxed') | |
| start += NT * BT | |
| def _dwd(DY, GACC, UPSEL, DWD, PAIRS, OFF2, | |
| D: tl.constexpr, FDIM: tl.constexpr, G: tl.constexpr, | |
| KG: tl.constexpr, NG: tl.constexpr, NS: tl.constexpr, | |
| BT: tl.constexpr, BDW: tl.constexpr, ATOM: tl.constexpr): | |
| """dWD[d tile, g] = sum over the segment of dY^T @ (silu(GACC) * UP). ATOM=False (one | |
| token chunk, NS=1): direct deterministic store; otherwise fp32 atomic_add.""" | |
| gc = tl.program_id(0) | |
| gid = gc % NG | |
| cid = gc // NG | |
| pid_d = tl.program_id(1) | |
| pid_s = tl.program_id(2) | |
| lo = tl.load(OFF2 + cid * NG + gid) | |
| hi = tl.load(OFF2 + (cid + 1) * NG + gid) | |
| n = hi - lo | |
| seg = (n + NS - 1) // NS | |
| s0 = lo + pid_s * seg | |
| s1 = tl.minimum(hi, s0 + seg) | |
| offs_g = tl.arange(0, G) | |
| offs_d = pid_d * BDW + tl.arange(0, BDW) | |
| acc = tl.zeros((BDW, G), dtype=tl.float32) | |
| for st in range(s0, s1, BT): | |
| offs_t = st + tl.arange(0, BT) | |
| m_t = offs_t < s1 | |
| par = tl.load(PAIRS + offs_t, mask=m_t, other=0) | |
| tq = par // KG | |
| slot = par - tq * KG | |
| tok = tq.to(tl.int64) | |
| dyb = tl.load(DY + tok[:, None] * D + offs_d[None, :], | |
| mask=m_t[:, None], other=0.0) | |
| _b = tok[:, None] * (KG * G) + slot[:, None] * G + offs_g[None, :] | |
| _g = tl.load(GACC + _b, mask=m_t[:, None], other=0.0).to(tl.float32) | |
| sb = (_g * tl.sigmoid(_g)).to(dyb.dtype) | |
| ub = tl.load(UPSEL + _b, mask=m_t[:, None], other=0.0) | |
| acc += tl.dot(tl.trans(dyb), sb * ub) | |
| ptr = DWD + offs_d[:, None].to(tl.int64) * FDIM + gid * G + offs_g[None, :] | |
| if ATOM: | |
| tl.atomic_add(ptr, acc, sem='relaxed') | |
| else: | |
| tl.store(ptr, acc.to(DWD.dtype.element_ty)) | |
| def _dwu2(X, DUP, DGATE, DWU, DWG, PAIRS, OFF2, | |
| D: tl.constexpr, FDIM: tl.constexpr, G: tl.constexpr, | |
| KG: tl.constexpr, NG: tl.constexpr, NS: tl.constexpr, | |
| BT: tl.constexpr, BDW: tl.constexpr, ATOM: tl.constexpr): | |
| """dWU[g, d tile] and dWG[g, d tile] in one pass over the segment (one gather of X).""" | |
| gc = tl.program_id(0) | |
| gid = gc % NG | |
| cid = gc // NG | |
| pid_d = tl.program_id(1) | |
| pid_s = tl.program_id(2) | |
| lo = tl.load(OFF2 + cid * NG + gid) | |
| hi = tl.load(OFF2 + (cid + 1) * NG + gid) | |
| n = hi - lo | |
| seg = (n + NS - 1) // NS | |
| s0 = lo + pid_s * seg | |
| s1 = tl.minimum(hi, s0 + seg) | |
| offs_g = tl.arange(0, G) | |
| offs_d = pid_d * BDW + tl.arange(0, BDW) | |
| acc_u = tl.zeros((G, BDW), dtype=tl.float32) | |
| acc_g = tl.zeros((G, BDW), dtype=tl.float32) | |
| for st in range(s0, s1, BT): | |
| offs_t = st + tl.arange(0, BT) | |
| m_t = offs_t < s1 | |
| par = tl.load(PAIRS + offs_t, mask=m_t, other=0) | |
| tq = par // KG | |
| slot = par - tq * KG | |
| tok = tq.to(tl.int64) | |
| base = tok[:, None] * (KG * G) + slot[:, None] * G + offs_g[None, :] | |
| dub = tl.load(DUP + base, mask=m_t[:, None], other=0.0) | |
| dgb = tl.load(DGATE + base, mask=m_t[:, None], other=0.0) | |
| xb = tl.load(X + tok[:, None] * D + offs_d[None, :], | |
| mask=m_t[:, None], other=0.0) | |
| acc_u += tl.dot(tl.trans(dub), xb) | |
| acc_g += tl.dot(tl.trans(dgb), xb) | |
| row = (gid * G + offs_g)[:, None].to(tl.int64) * D + offs_d[None, :] | |
| if ATOM: | |
| tl.atomic_add(DWU + row, acc_u, sem='relaxed') | |
| tl.atomic_add(DWG + row, acc_g, sem='relaxed') | |
| else: | |
| tl.store(DWU + row, acc_u.to(DWU.dtype.element_ty)) | |
| tl.store(DWG + row, acc_g.to(DWG.dtype.element_ty)) | |
| def _dwu(X, DUP, DWU, PAIRS, OFF2, | |
| D: tl.constexpr, FDIM: tl.constexpr, G: tl.constexpr, | |
| KG: tl.constexpr, NG: tl.constexpr, NS: tl.constexpr, | |
| BT: tl.constexpr, BDW: tl.constexpr, ATOM: tl.constexpr): | |
| """dW[g, d tile] = sum over the segment of dsel^T @ X; used twice when G is above the | |
| fusion threshold.""" | |
| gc = tl.program_id(0) | |
| gid = gc % NG | |
| cid = gc // NG | |
| pid_d = tl.program_id(1) | |
| pid_s = tl.program_id(2) | |
| lo = tl.load(OFF2 + cid * NG + gid) | |
| hi = tl.load(OFF2 + (cid + 1) * NG + gid) | |
| n = hi - lo | |
| seg = (n + NS - 1) // NS | |
| s0 = lo + pid_s * seg | |
| s1 = tl.minimum(hi, s0 + seg) | |
| offs_g = tl.arange(0, G) | |
| offs_d = pid_d * BDW + tl.arange(0, BDW) | |
| acc = tl.zeros((G, BDW), dtype=tl.float32) | |
| for st in range(s0, s1, BT): | |
| offs_t = st + tl.arange(0, BT) | |
| m_t = offs_t < s1 | |
| par = tl.load(PAIRS + offs_t, mask=m_t, other=0) | |
| tq = par // KG | |
| slot = par - tq * KG | |
| tok = tq.to(tl.int64) | |
| dub = tl.load(DUP + tok[:, None] * (KG * G) + slot[:, None] * G | |
| + offs_g[None, :], mask=m_t[:, None], other=0.0) | |
| xb = tl.load(X + tok[:, None] * D + offs_d[None, :], | |
| mask=m_t[:, None], other=0.0) | |
| acc += tl.dot(tl.trans(dub), xb) | |
| ptr = DWU + (gid * G + offs_g)[:, None].to(tl.int64) * D + offs_d[None, :] | |
| if ATOM: | |
| tl.atomic_add(ptr, acc, sem='relaxed') | |
| else: | |
| tl.store(ptr, acc.to(DWU.dtype.element_ty)) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # triton_op: the op body is traced by dynamo/AOT (allocations, casts and zeros become graph | |
| # nodes); kernels enter the graph through wrap_triton. | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| if _HAS_TRITON and _HAS_TRITON_OP: | |
| # TRITON_INTERPRET=1 (CPU debugging): interpreted kernels are not triton.jit objects and | |
| # wrap_triton rejects them, so they are launched directly (eager only). | |
| _wrap = (lambda fn: fn) if os.environ.get('TRITON_INTERPRET', '0') == '1' else wrap_triton | |
| def _csr(idx, ng, kg): | |
| """(pairs [P] int32, off2 [ch+1, ng] int32, ch). off2[j, g] = start of the pairs of group g | |
| coming from token chunk j; off2[ch, g] = end of the segment of group g.""" | |
| M = idx.shape[0] | |
| P = M * kg | |
| g = idx.reshape(-1) | |
| ncta, ch, r, chunk = _plan_chunks(P) | |
| ngp = triton.next_power_of_2(ng) | |
| hist = torch.empty(ncta, ng, dtype=torch.int32, device=idx.device) | |
| colpre = torch.empty(ncta + 1, ng, dtype=torch.int32, device=idx.device) | |
| off = torch.empty(ng + 1, dtype=torch.int32, device=idx.device) | |
| pairs = torch.empty(P, dtype=torch.int32, device=idx.device) | |
| _wrap(_csr_hist)[(ncta,)](g, hist, P, NG=ng, NG_POW=ngp, SUB=_SUB, CHUNK=chunk) | |
| _wrap(_csr_scan)[(1,)](hist, colpre, off, ncta, NG=ng, NG_POW=ngp) | |
| _wrap(_csr_scatter)[(ncta,)](g, colpre, off, pairs, P, NG=ng, NG_POW=ngp, SUB=_SUB, CHUNK=chunk) | |
| rows = torch.arange(0, ch + 1, device=idx.device, dtype=torch.int64) * r | |
| off2 = off[:ng].unsqueeze(0) + colpre.index_select(0, rows) | |
| return pairs, off2, ch | |
| def _fwd_core(xc, gwc, uwc, dwc, idx, G): | |
| M, D = xc.shape | |
| bd, bdy, bdw = _pick_block(D, _BD), _pick_block(D, _BDY), _pick_block(D, _BDW) | |
| Fdim = uwc.shape[0] | |
| ng = Fdim // G | |
| kg = idx.shape[1] | |
| pairs, off2, ch = _csr(idx, ng, kg) | |
| gacc = torch.empty(M, kg * G, device=xc.device, dtype=xc.dtype) | |
| up_sel = torch.empty(M, kg * G, device=xc.device, dtype=xc.dtype) | |
| p_sel = torch.empty(M, kg * G, device=xc.device, dtype=xc.dtype) | |
| if _fusion_ok(G): | |
| _wrap(_gate_up)[(ng * ch, _NT)]( | |
| xc, gwc, uwc, gacc, up_sel, p_sel, pairs, off2, | |
| D=D, FDIM=Fdim, G=G, KG=kg, NG=ng, NT=_NT, BT=_BT, BD=bd, num_warps=_WARPS) | |
| else: # large G: unfused pair (shared memory limit) | |
| sil_tmp = torch.empty(M, kg * G, device=xc.device, dtype=xc.dtype) | |
| _wrap(_gate)[(ng * ch, _NT)]( | |
| xc, gwc, gacc, sil_tmp, pairs, off2, | |
| D=D, G=G, KG=kg, NG=ng, NT=_NT, BT=_BT, BD=bd, num_warps=_WARPS) | |
| _wrap(_up)[(ng * ch, _NT)]( | |
| xc, uwc, sil_tmp, up_sel, p_sel, pairs, off2, | |
| D=D, FDIM=Fdim, G=G, KG=kg, NG=ng, NT=_NT, BT=_BT, BD=bd, num_warps=_WARPS) | |
| y32 = torch.zeros(M, D, device=xc.device, dtype=torch.float32) | |
| _wrap(_down)[(ng * ch, _NT, D // bdy)]( | |
| p_sel, dwc, y32, pairs, off2, | |
| D=D, FDIM=Fdim, G=G, KG=kg, NG=ng, NT=_NT, BT=_BT, BDY=bdy, num_warps=_WARPS) | |
| return y32.to(xc.dtype), gacc, up_sel, pairs, off2 | |
| def sparse_swiglu_fwd(x: torch.Tensor, gate_w: torch.Tensor, up_w: torch.Tensor, | |
| down_w: torch.Tensor, idx: torch.Tensor, G: int | |
| ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: | |
| """x [M, D]; gate_w, up_w [F, D]; down_w [D, F]; idx [M, kg]. | |
| Returns (y [M, D], gacc [M, kg*G], up_sel [M, kg*G], pairs, off2) — compact, no [M, F].""" | |
| return _fwd_core(x.contiguous(), gate_w.contiguous(), up_w.contiguous(), | |
| down_w.contiguous(), idx.to(torch.int32).contiguous(), G) | |
| def _bwd(dy, x, gate_w, up_w, down_w, gacc, up_sel, pairs, off2, G): | |
| """Returns (dX, dW_gate, dW_up, dW_down).""" | |
| dyc = dy.contiguous(); xc = x.contiguous() | |
| gwc = gate_w.contiguous(); uwc = up_w.contiguous(); dwc = down_w.contiguous() | |
| gaccc = gacc.contiguous() | |
| M, D = xc.shape | |
| bd, bdy, bdw = _pick_block(D, _BD), _pick_block(D, _BDY), _pick_block(D, _BDW) | |
| Fdim = uwc.shape[0] | |
| K = up_sel.shape[1] | |
| kg = K // G | |
| ng = Fdim // G | |
| ch = off2.shape[0] - 1 | |
| dgate = torch.empty(M, K, device=dyc.device, dtype=xc.dtype) | |
| dup = torch.empty(M, K, device=dyc.device, dtype=xc.dtype) | |
| _wrap(_dp)[(ng * ch, _NT)]( | |
| dyc, dwc, gaccc, up_sel, dgate, dup, pairs, off2, | |
| D=D, FDIM=Fdim, G=G, KG=kg, NG=ng, NT=_NT, BT=_BT, BD=bd, num_warps=_WARPS) | |
| dX32 = torch.zeros(M, D, device=dyc.device, dtype=torch.float32) | |
| if _fusion_ok(G): | |
| _wrap(_dx2)[(ng * ch, _NT, D // bdy)]( | |
| dup, uwc, dgate, gwc, dX32, pairs, off2, | |
| D=D, FDIM=Fdim, G=G, KG=kg, NG=ng, NT=_NT, BT=_BT, BDY=bdy, num_warps=_WARPS) | |
| else: | |
| for dsel, w in ((dup, uwc), (dgate, gwc)): | |
| _wrap(_dx)[(ng * ch, _NT, D // bdy)]( | |
| dsel, w, dX32, pairs, off2, | |
| D=D, FDIM=Fdim, G=G, KG=kg, NG=ng, NT=_NT, BT=_BT, BDY=bdy, num_warps=_WARPS) | |
| dX = dX32.to(dy.dtype) | |
| if _DW_DET or (ch == 1 and _NS == 1): # whole group segment in one CTA: direct, deterministic store | |
| off2w = off2 if ch == 1 else torch.stack([off2[0], off2[-1]]) | |
| chw, atom = 1, False | |
| dWd = torch.empty(D, Fdim, device=dyc.device, dtype=down_w.dtype) | |
| dWu = torch.empty(Fdim, D, device=dyc.device, dtype=up_w.dtype) | |
| dWg = torch.empty(Fdim, D, device=dyc.device, dtype=gate_w.dtype) | |
| else: | |
| off2w = off2; chw, atom = ch, True | |
| dWd = torch.zeros(D, Fdim, device=dyc.device, dtype=torch.float32) | |
| dWu = torch.zeros(Fdim, D, device=dyc.device, dtype=torch.float32) | |
| dWg = torch.zeros(Fdim, D, device=dyc.device, dtype=torch.float32) | |
| _wrap(_dwd)[(ng * chw, D // bdw, _NS)]( | |
| dyc, gaccc, up_sel, dWd, pairs, off2w, | |
| D=D, FDIM=Fdim, G=G, KG=kg, NG=ng, NS=_NS, BT=_BT, BDW=bdw, ATOM=atom, num_warps=_WARPS_W) | |
| if _fusion_ok(G): | |
| _wrap(_dwu2)[(ng * chw, D // bdw, _NS)]( | |
| xc, dup, dgate, dWu, dWg, pairs, off2w, | |
| D=D, FDIM=Fdim, G=G, KG=kg, NG=ng, NS=_NS, BT=_BT, BDW=bdw, ATOM=atom, num_warps=_WARPS_W) | |
| else: | |
| for dsel, dw in ((dup, dWu), (dgate, dWg)): | |
| _wrap(_dwu)[(ng * chw, D // bdw, _NS)]( | |
| xc, dsel, dw, pairs, off2w, | |
| D=D, FDIM=Fdim, G=G, KG=kg, NG=ng, NS=_NS, BT=_BT, BDW=bdw, ATOM=atom, num_warps=_WARPS_W) | |
| if atom: | |
| dWd = dWd.to(down_w.dtype) | |
| dWu = dWu.to(up_w.dtype) | |
| dWg = dWg.to(gate_w.dtype) | |
| return dX, dWg, dWu, dWd | |
| def _setup_ctx(ctx, inputs, output): | |
| x, gate_w, up_w, down_w, idx, G = inputs | |
| y, gacc, up_sel, pairs, off2 = output | |
| ctx.save_for_backward(x, gate_w, up_w, down_w, gacc, up_sel, pairs, off2) | |
| ctx.G = G | |
| def _backward(ctx, dy, d_gacc, d_up_sel, d_pairs, d_off2): | |
| x, gate_w, up_w, down_w, gacc, up_sel, pairs, off2 = ctx.saved_tensors | |
| dX, dWg, dWu, dWd = _bwd(dy, x, gate_w, up_w, down_w, gacc, up_sel, pairs, off2, ctx.G) | |
| return dX, dWg, dWu, dWd, None, None | |
| torch.library.register_autograd("eugen_sparse::sparse_swiglu_fwd", _backward, setup_context=_setup_ctx) | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| # Public entry point | |
| # ══════════════════════════════════════════════════════════════════════════════ | |
| def kernel_supported(is_cuda, d_model, d_ff, group_size, num_active_groups=None): | |
| """True if the Triton path will be used: CUDA, Triton with torch.library.triton_op, d_model a | |
| multiple of 16, group_size a power of two >= 16, d_ff a multiple of group_size, >= 2 groups.""" | |
| G = group_size | |
| ok = (_HAS_TRITON and _HAS_TRITON_OP and is_cuda and _pick_block(d_model, _BD) >= 16 | |
| and G >= 16 and (G & (G - 1)) == 0 and d_ff % G == 0 and d_ff // G >= 2) | |
| return ok and (num_active_groups is None or 1 <= num_active_groups <= d_ff // G) | |
| def plan(num_tokens, d_model, d_ff, group_size, num_active_groups): | |
| """Diagnostics (no GPU work): will the kernel run and how the work is split.""" | |
| P = num_tokens * num_active_groups | |
| ncta, ch, r, chunk = _plan_chunks(P) | |
| return dict(kernel=kernel_supported(True, d_model, d_ff, group_size, num_active_groups), | |
| num_groups=d_ff // group_size, pairs=P, token_chunks=ch, csr_ctas=ncta, csr_chunk=chunk, | |
| tiles=dict(BD=_pick_block(d_model, _BD), BDY=_pick_block(d_model, _BDY), | |
| BDW=_pick_block(d_model, _BDW)), | |
| fused=_fusion_ok(group_size), | |
| deterministic_weight_grads=bool(_DW_DET or (ch == 1 and _NS == 1))) | |
| _WARNED = [False] | |
| def _autocast_dtype(device_type): | |
| try: | |
| if torch.is_autocast_enabled(device_type): | |
| return torch.get_autocast_dtype(device_type) | |
| except TypeError: # older PyTorch API | |
| if device_type == 'cuda' and torch.is_autocast_enabled(): | |
| return torch.get_autocast_gpu_dtype() | |
| return None | |
| def sparse_swiglu_ffn(x, gate_weight, up_weight, down_weight, group_idx, group_size): | |
| """x [..., d_model]; gate_weight, up_weight [d_ff, d_model]; down_weight [d_model, d_ff] | |
| (nn.Linear layout); group_idx [..., num_active_groups] from grouped_topk_select. | |
| Returns y [..., d_model] = sum over the selected groups of (SiLU(x W_g^T) * x W_u^T) W_d^T.""" | |
| d_model = x.shape[-1] | |
| x2 = x.reshape(-1, d_model) | |
| i2 = group_idx.reshape(-1, group_idx.shape[-1]) | |
| if kernel_supported(x.is_cuda, d_model, up_weight.shape[0], group_size, i2.shape[-1]): | |
| dt = _autocast_dtype(x.device.type) | |
| args = (x2, gate_weight, up_weight, down_weight) | |
| if dt is not None: # kernels inherit the input dtype | |
| args = tuple(t.to(dt) for t in args) | |
| y = sparse_swiglu_fwd(*args, i2, group_size)[0] | |
| else: | |
| if x.is_cuda and not _WARNED[0]: | |
| _WARNED[0] = True | |
| warnings.warn('eugen_sparse: Triton kernel not used for this shape/environment ' | |
| '(see kernel_supported); running the slow dense masked path') | |
| y = reference_sparse_swiglu_ffn(x2, gate_weight, up_weight, down_weight, i2, group_size) | |
| return y.view(*x.shape[:-1], d_model) | |