Instructions to use Motif-Technologies/activation with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Kernels
How to use Motif-Technologies/activation with Kernels:
# !pip install kernels from kernels import get_kernel # a version (or an explicit revision) is required; see the "Files and versions" tab for the available ones kernel = get_kernel("Motif-Technologies/activation", version=1) - Notebooks
- Google Colab
- Kaggle
feat: fused grouped EPolyNormTanh 커널 (fwd + bwd, expert별 계수) (#28)
Browse files* feat: fused EPolyNormTanh kernel (fwd+bwd)
* epolynorm_tanh: warp-per-row persistent kernels, one weight-grad atomic per block; tests import the package
* epolynorm_tanh: grouped (per-expert) coefficients like grouped_poly_norm
- kernel takes weight [num_total_experts, 3] + int32 offsets + expert_offset;
in-kernel binary search per row range, padding rows (>= offsets[-1]) give
zero output / zero grads, per-expert weight grads warp-reduced then
atomicAdd'ed on expert change
- two 128-bit vectors per lane in flight (ILP) in both kernels
- backward always returns four tensors (grad_scores = empty [0] without scores)
so opcheck's fake/real output counts match
- python: fused_grouped_epolynorm_tanh + grouped_epolynorm_tanh_ref; the
single-expert fused_epolynorm_tanh / epolynorm_tanh_ref wrap them
- layers.EPolyNormTanh: weight [num_experts, 3] parameter
- tests: grouped forward/backward, expert_offset + NaN padding rows,
hidden_clamp, single-expert wrapper, module vs eager, opcheck; fp32
tolerance 1e-3 (--use_fast_math)
- bench: rows spread over --experts experts (default 48 = 384/EP8)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiM99HZnmc1dRQt4dafowG
* epolynorm_tanh: back to one vector per lane (ILP pairs measured slower); bwd opcheck without AOT output compare
bf16 N=294912xD=1152 on B300: fwd 0.45 -> 0.55 ms and bwd 0.73 -> 1.02 ms with
the two-vector loop (98 regs in bwd, selects + wasted tail lanes); SASS shows
tanhf -> MUFU.TANH (5 per element), so the 16-bit forward is MUFU-bound.
The backward's atomicAdd weight-grad makes its outputs order-dependent, so
opcheck's eager-vs-AOT comparison is skipped for that op.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiM99HZnmc1dRQt4dafowG
* style: pre-commit (yapf/isort/clang-format) on the epolynorm_tanh files
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BiM99HZnmc1dRQt4dafowG
---------
Co-authored-by: Taehyun Kim <taehyun.kim@motiftech.io>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
- activation/epolynorm_tanh.cu +593 -0
- benchmarks/bench_epolynorm_tanh.py +234 -0
- build.toml +2 -0
- setup.py +4 -0
- tests/test_epolynorm_tanh.py +348 -0
- torch-ext/activation/__init__.py +7 -0
- torch-ext/activation/epolynorm_tanh.py +269 -0
- torch-ext/activation/layers.py +75 -0
- torch-ext/torch_binding.cpp +14 -0
- torch-ext/torch_binding.h +27 -0
|
@@ -0,0 +1,593 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Grouped (per-expert) EPolyNormTanh — tanh-bounded elementwise PolyNorm with
|
| 2 |
+
// SiTU-style caps — forward + backward in one kernel each.
|
| 3 |
+
//
|
| 4 |
+
// e = expert of the row. Rows are grouped by expert: offsets[e] is the
|
| 5 |
+
// cumsum of tokens per local expert (int32); the coefficient row is
|
| 6 |
+
// weight[e + expert_offset].
|
| 7 |
+
// g = clamp_max(x, C) (C = hidden_clamp, < 0 =
|
| 8 |
+
// disabled) u = clamp(mul, -C, C) q_k = tanh(g^(k+1) / s_k) k = 0, 1,
|
| 9 |
+
// 2 poly = gate_beta * tanh((w_e0 q_0 + w_e1 q_1 + w_e2 q_2) / gate_beta)
|
| 10 |
+
// up = up_beta * tanh(u / up_beta)
|
| 11 |
+
// out = poly * up * score (score: optional per-row fp32)
|
| 12 |
+
//
|
| 13 |
+
// weight [num_total_experts, 3] fp32 holds the coefficients AFTER the sigmoid;
|
| 14 |
+
// their gradient comes back as fp32 [num_total_experts, 3] and the caller's
|
| 15 |
+
// autograd carries it through the sigmoid. Everything is computed in fp32 and
|
| 16 |
+
// stored in the input dtype. Rows at or beyond offsets[-1] are padding: zero
|
| 17 |
+
// output, zero input gradients, no contribution to the weight gradient.
|
| 18 |
+
//
|
| 19 |
+
// Launch shape: one WARP per row, each warp owning a CONTIGUOUS chunk of rows
|
| 20 |
+
// (persistent grid). Rows of one expert are contiguous, so a warp's weight-grad
|
| 21 |
+
// partials stay in registers until the expert changes (or the chunk ends) and
|
| 22 |
+
// reach global memory with a few atomicAdds per warp. Per-row reductions
|
| 23 |
+
// (grad_scores) are warp shuffles. Loads and stores are 128-bit vectorized when
|
| 24 |
+
// D % width == 0 (width 8 for 16-bit types, 4 for fp32), scalar fallback
|
| 25 |
+
// otherwise.
|
| 26 |
+
//
|
| 27 |
+
// Cost model (B300, bf16, N=294912 x D=1152): tanhf compiles to MUFU.TANH
|
| 28 |
+
// under --use_fast_math, 5 per element in each kernel; at 16 MUFU ops/clk/SM
|
| 29 |
+
// that is a ~0.36 ms floor against a ~0.27 ms HBM floor for the forward, so
|
| 30 |
+
// the 16-bit forward is MUFU-bound (fp32, with twice the bytes, is
|
| 31 |
+
// memory-bound). Two-vectors-per-lane ILP was measured slower (extra selects
|
| 32 |
+
// and wasted lanes on the tail chunk).
|
| 33 |
+
|
| 34 |
+
#include <ATen/Functions.h>
|
| 35 |
+
#include <ATen/cuda/CUDAContext.h>
|
| 36 |
+
#include <c10/cuda/CUDAGuard.h>
|
| 37 |
+
#include <torch/all.h>
|
| 38 |
+
|
| 39 |
+
#include <algorithm>
|
| 40 |
+
|
| 41 |
+
#include "assert_utils.h"
|
| 42 |
+
#include "cuda_compat.h"
|
| 43 |
+
#include "dispatch_utils.h"
|
| 44 |
+
|
| 45 |
+
namespace motif {
|
| 46 |
+
namespace epoly {
|
| 47 |
+
|
| 48 |
+
template <typename type, int N> struct alignas(sizeof(type) * N) vec_t {
|
| 49 |
+
type data[N];
|
| 50 |
+
};
|
| 51 |
+
|
| 52 |
+
constexpr int BLOCK = 256;
|
| 53 |
+
constexpr int WARPS_PER_BLOCK = BLOCK / WARP_SIZE;
|
| 54 |
+
|
| 55 |
+
struct Params {
|
| 56 |
+
float gate_beta, inv_gate_beta;
|
| 57 |
+
float up_beta, inv_up_beta;
|
| 58 |
+
float inv_s0, inv_s1, inv_s2;
|
| 59 |
+
float hidden_clamp; // < 0 = disabled
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
__device__ __forceinline__ float warp_sum(float v) {
|
| 63 |
+
#ifndef USE_ROCM
|
| 64 |
+
constexpr unsigned int FULL_MASK = 0xffffffff;
|
| 65 |
+
#else
|
| 66 |
+
constexpr unsigned long long FULL_MASK = 0xffffffffffffffffULL;
|
| 67 |
+
#endif
|
| 68 |
+
#pragma unroll
|
| 69 |
+
for (int mask = WARP_SIZE / 2; mask > 0; mask >>= 1)
|
| 70 |
+
v += __shfl_xor_sync(FULL_MASK, v, mask);
|
| 71 |
+
return v;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Expert of `row`: first e with offsets[e] > row (offsets = cumsum of counts).
|
| 75 |
+
__device__ __forceinline__ int find_expert(const int32_t *__restrict__ offsets,
|
| 76 |
+
int num_experts, int64_t row) {
|
| 77 |
+
int lo = 0, hi = num_experts;
|
| 78 |
+
while (lo < hi) {
|
| 79 |
+
const int mid = (lo + hi) >> 1;
|
| 80 |
+
if ((int64_t)offsets[mid] <= row)
|
| 81 |
+
lo = mid + 1;
|
| 82 |
+
else
|
| 83 |
+
hi = mid;
|
| 84 |
+
}
|
| 85 |
+
return lo;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Forward math for one element: out = poly * up (score applied by the caller).
|
| 89 |
+
__device__ __forceinline__ float fwd_elem(float x, float m, const Params &p,
|
| 90 |
+
float w0, float w1, float w2,
|
| 91 |
+
bool do_clamp) {
|
| 92 |
+
const float g = do_clamp ? fminf(x, p.hidden_clamp) : x;
|
| 93 |
+
const float u =
|
| 94 |
+
do_clamp ? fminf(fmaxf(m, -p.hidden_clamp), p.hidden_clamp) : m;
|
| 95 |
+
const float g2 = g * g;
|
| 96 |
+
const float q0 = tanhf(g * p.inv_s0);
|
| 97 |
+
const float q1 = tanhf(g2 * p.inv_s1);
|
| 98 |
+
const float q2 = tanhf(g2 * g * p.inv_s2);
|
| 99 |
+
const float poly =
|
| 100 |
+
p.gate_beta * tanhf((w0 * q0 + w1 * q1 + w2 * q2) * p.inv_gate_beta);
|
| 101 |
+
const float up = p.up_beta * tanhf(u * p.inv_up_beta);
|
| 102 |
+
return poly * up;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// Backward math for one element. Writes dx, dm; accumulates dw_k and the
|
| 106 |
+
// grad_scores partial (go * poly * up).
|
| 107 |
+
__device__ __forceinline__ void bwd_elem(float x, float m, float go,
|
| 108 |
+
float score, const Params &p, float w0,
|
| 109 |
+
float w1, float w2, bool do_clamp,
|
| 110 |
+
float &dx, float &dm, float &dw0,
|
| 111 |
+
float &dw1, float &dw2, float &gs) {
|
| 112 |
+
const float g = do_clamp ? fminf(x, p.hidden_clamp) : x;
|
| 113 |
+
const float u =
|
| 114 |
+
do_clamp ? fminf(fmaxf(m, -p.hidden_clamp), p.hidden_clamp) : m;
|
| 115 |
+
const float g2 = g * g;
|
| 116 |
+
const float q0 = tanhf(g * p.inv_s0);
|
| 117 |
+
const float q1 = tanhf(g2 * p.inv_s1);
|
| 118 |
+
const float q2 = tanhf(g2 * g * p.inv_s2);
|
| 119 |
+
const float tz = tanhf((w0 * q0 + w1 * q1 + w2 * q2) * p.inv_gate_beta);
|
| 120 |
+
const float poly = p.gate_beta * tz;
|
| 121 |
+
const float tu = tanhf(u * p.inv_up_beta);
|
| 122 |
+
const float up = p.up_beta * tu;
|
| 123 |
+
|
| 124 |
+
// d out / d poly = up * score ; d out / d up = poly * score
|
| 125 |
+
const float d_poly = go * up * score;
|
| 126 |
+
const float d_up = go * poly * score;
|
| 127 |
+
// poly = gb * tanh(S / gb) -> d poly / d S = 1 - tz^2
|
| 128 |
+
const float dS = d_poly * (1.f - tz * tz);
|
| 129 |
+
// up = ub * tanh(u / ub) -> d up / d u = 1 - tu^2
|
| 130 |
+
float du = d_up * (1.f - tu * tu);
|
| 131 |
+
float dg = dS * (w0 * (1.f - q0 * q0) * p.inv_s0 +
|
| 132 |
+
w1 * (1.f - q1 * q1) * (2.f * g * p.inv_s1) +
|
| 133 |
+
w2 * (1.f - q2 * q2) * (3.f * g2 * p.inv_s2));
|
| 134 |
+
if (do_clamp) {
|
| 135 |
+
if (x > p.hidden_clamp)
|
| 136 |
+
dg = 0.f; // clamp_max(x, C): zero slope above C
|
| 137 |
+
if (fabsf(m) > p.hidden_clamp)
|
| 138 |
+
du = 0.f;
|
| 139 |
+
}
|
| 140 |
+
dx = dg;
|
| 141 |
+
dm = du;
|
| 142 |
+
dw0 += dS * q0;
|
| 143 |
+
dw1 += dS * q1;
|
| 144 |
+
dw2 += dS * q2;
|
| 145 |
+
gs += go * poly * up;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
// Warp-reduce the weight-grad partials and add them to expert `e`'s row.
|
| 149 |
+
__device__ __forceinline__ void flush_weight_grad(float &dw0, float &dw1,
|
| 150 |
+
float &dw2, int e,
|
| 151 |
+
float *__restrict__ wg) {
|
| 152 |
+
const float a = warp_sum(dw0), b = warp_sum(dw1), c = warp_sum(dw2);
|
| 153 |
+
if (threadIdx.x % WARP_SIZE == 0) {
|
| 154 |
+
atomicAdd(&wg[(int64_t)e * 3 + 0], a);
|
| 155 |
+
atomicAdd(&wg[(int64_t)e * 3 + 1], b);
|
| 156 |
+
atomicAdd(&wg[(int64_t)e * 3 + 2], c);
|
| 157 |
+
}
|
| 158 |
+
dw0 = dw1 = dw2 = 0.f;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// The contiguous row range [begin, end) of this warp.
|
| 162 |
+
__device__ __forceinline__ void warp_rows(int64_t N, int64_t &begin,
|
| 163 |
+
int64_t &end) {
|
| 164 |
+
const int64_t total_warps = (int64_t)gridDim.x * WARPS_PER_BLOCK;
|
| 165 |
+
const int64_t gw =
|
| 166 |
+
(int64_t)blockIdx.x * WARPS_PER_BLOCK + threadIdx.x / WARP_SIZE;
|
| 167 |
+
const int64_t per_warp = (N + total_warps - 1) / total_warps;
|
| 168 |
+
begin = gw * per_warp;
|
| 169 |
+
end = begin + per_warp < N ? begin + per_warp : N;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// Load the expert's three coefficients.
|
| 173 |
+
__device__ __forceinline__ void load_w(const float *__restrict__ weight, int e,
|
| 174 |
+
int expert_offset, float &w0, float &w1,
|
| 175 |
+
float &w2) {
|
| 176 |
+
const int64_t wi = (int64_t)(e + expert_offset) * 3;
|
| 177 |
+
w0 = weight[wi];
|
| 178 |
+
w1 = weight[wi + 1];
|
| 179 |
+
w2 = weight[wi + 2];
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
// ---------------------------------------------------------------------------
|
| 183 |
+
// Forward — vectorized (width > 0)
|
| 184 |
+
// ---------------------------------------------------------------------------
|
| 185 |
+
template <typename scalar_t, int width>
|
| 186 |
+
__global__ void __launch_bounds__(BLOCK) epolynorm_tanh_fwd_kernel(
|
| 187 |
+
scalar_t *__restrict__ output, const scalar_t *__restrict__ input,
|
| 188 |
+
const scalar_t *__restrict__ mul,
|
| 189 |
+
const float *__restrict__ weight, // [num_total_experts, 3] fp32
|
| 190 |
+
const int32_t *__restrict__ offsets, // [num_experts]
|
| 191 |
+
const float *__restrict__ scores, // nullable, [N] fp32
|
| 192 |
+
const Params p, const int D, const int64_t N, const int num_experts,
|
| 193 |
+
const int expert_offset) {
|
| 194 |
+
using v_t = vec_t<scalar_t, width>;
|
| 195 |
+
const bool do_clamp = (p.hidden_clamp >= 0.f);
|
| 196 |
+
const int vec_d = D / width;
|
| 197 |
+
const int lane = threadIdx.x % WARP_SIZE;
|
| 198 |
+
const int64_t n_valid = offsets[num_experts - 1];
|
| 199 |
+
int64_t row, end;
|
| 200 |
+
warp_rows(N, row, end);
|
| 201 |
+
|
| 202 |
+
v_t zero_v;
|
| 203 |
+
#pragma unroll
|
| 204 |
+
for (int j = 0; j < width; ++j)
|
| 205 |
+
zero_v.data[j] = (scalar_t)0;
|
| 206 |
+
|
| 207 |
+
int e = -1;
|
| 208 |
+
float w0 = 0.f, w1 = 0.f, w2 = 0.f;
|
| 209 |
+
for (; row < end; ++row) {
|
| 210 |
+
const int64_t base = row * vec_d;
|
| 211 |
+
v_t *__restrict__ out_v = reinterpret_cast<v_t *>(output) + base;
|
| 212 |
+
if (row >= n_valid) { // padding row
|
| 213 |
+
for (int i = lane; i < vec_d; i += WARP_SIZE)
|
| 214 |
+
out_v[i] = zero_v;
|
| 215 |
+
continue;
|
| 216 |
+
}
|
| 217 |
+
if (e < 0 || row >= (int64_t)offsets[e]) {
|
| 218 |
+
e = find_expert(offsets, num_experts, row);
|
| 219 |
+
load_w(weight, e, expert_offset, w0, w1, w2);
|
| 220 |
+
}
|
| 221 |
+
const float score = (scores != nullptr) ? scores[row] : 1.f;
|
| 222 |
+
const v_t *__restrict__ in_v = reinterpret_cast<const v_t *>(input) + base;
|
| 223 |
+
const v_t *__restrict__ m_v = reinterpret_cast<const v_t *>(mul) + base;
|
| 224 |
+
for (int i = lane; i < vec_d; i += WARP_SIZE) {
|
| 225 |
+
const v_t xv = in_v[i];
|
| 226 |
+
const v_t mv = m_v[i];
|
| 227 |
+
v_t ov;
|
| 228 |
+
#pragma unroll
|
| 229 |
+
for (int j = 0; j < width; ++j) {
|
| 230 |
+
const float o = fwd_elem((float)xv.data[j], (float)mv.data[j], p, w0,
|
| 231 |
+
w1, w2, do_clamp) *
|
| 232 |
+
score;
|
| 233 |
+
ov.data[j] = (scalar_t)o;
|
| 234 |
+
}
|
| 235 |
+
out_v[i] = ov;
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
// Scalar fallback forward (D not a multiple of the vector width)
|
| 241 |
+
template <typename scalar_t>
|
| 242 |
+
__global__ void __launch_bounds__(BLOCK)
|
| 243 |
+
epolynorm_tanh_fwd_scalar(scalar_t *__restrict__ output,
|
| 244 |
+
const scalar_t *__restrict__ input,
|
| 245 |
+
const scalar_t *__restrict__ mul,
|
| 246 |
+
const float *__restrict__ weight,
|
| 247 |
+
const int32_t *__restrict__ offsets,
|
| 248 |
+
const float *__restrict__ scores, const Params p,
|
| 249 |
+
const int D, const int64_t N,
|
| 250 |
+
const int num_experts, const int expert_offset) {
|
| 251 |
+
const bool do_clamp = (p.hidden_clamp >= 0.f);
|
| 252 |
+
const int lane = threadIdx.x % WARP_SIZE;
|
| 253 |
+
const int64_t n_valid = offsets[num_experts - 1];
|
| 254 |
+
int64_t row, end;
|
| 255 |
+
warp_rows(N, row, end);
|
| 256 |
+
|
| 257 |
+
int e = -1;
|
| 258 |
+
float w0 = 0.f, w1 = 0.f, w2 = 0.f;
|
| 259 |
+
for (; row < end; ++row) {
|
| 260 |
+
const int64_t off = row * D;
|
| 261 |
+
if (row >= n_valid) {
|
| 262 |
+
for (int i = lane; i < D; i += WARP_SIZE)
|
| 263 |
+
output[off + i] = (scalar_t)0;
|
| 264 |
+
continue;
|
| 265 |
+
}
|
| 266 |
+
if (e < 0 || row >= (int64_t)offsets[e]) {
|
| 267 |
+
e = find_expert(offsets, num_experts, row);
|
| 268 |
+
load_w(weight, e, expert_offset, w0, w1, w2);
|
| 269 |
+
}
|
| 270 |
+
const float score = (scores != nullptr) ? scores[row] : 1.f;
|
| 271 |
+
for (int i = lane; i < D; i += WARP_SIZE) {
|
| 272 |
+
const float o = fwd_elem((float)input[off + i], (float)mul[off + i], p,
|
| 273 |
+
w0, w1, w2, do_clamp) *
|
| 274 |
+
score;
|
| 275 |
+
output[off + i] = (scalar_t)o;
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
// ---------------------------------------------------------------------------
|
| 281 |
+
// Backward — vectorized (width > 0)
|
| 282 |
+
// ---------------------------------------------------------------------------
|
| 283 |
+
template <typename scalar_t, int width>
|
| 284 |
+
__global__ void __launch_bounds__(BLOCK) epolynorm_tanh_bwd_kernel(
|
| 285 |
+
scalar_t *__restrict__ grad_input, scalar_t *__restrict__ grad_mul,
|
| 286 |
+
float *__restrict__ weight_grad, // [num_total_experts, 3] fp32, atomicAdd
|
| 287 |
+
float *__restrict__ grad_scores, // nullable, [N] fp32
|
| 288 |
+
const scalar_t *__restrict__ grad_output,
|
| 289 |
+
const scalar_t *__restrict__ input, const scalar_t *__restrict__ mul,
|
| 290 |
+
const float *__restrict__ weight, const int32_t *__restrict__ offsets,
|
| 291 |
+
const float *__restrict__ scores, const Params p, const int D,
|
| 292 |
+
const int64_t N, const int num_experts, const int expert_offset) {
|
| 293 |
+
using v_t = vec_t<scalar_t, width>;
|
| 294 |
+
const bool do_clamp = (p.hidden_clamp >= 0.f);
|
| 295 |
+
const int vec_d = D / width;
|
| 296 |
+
const int lane = threadIdx.x % WARP_SIZE;
|
| 297 |
+
const int64_t n_valid = offsets[num_experts - 1];
|
| 298 |
+
int64_t row, end;
|
| 299 |
+
warp_rows(N, row, end);
|
| 300 |
+
|
| 301 |
+
v_t zero_v;
|
| 302 |
+
#pragma unroll
|
| 303 |
+
for (int j = 0; j < width; ++j)
|
| 304 |
+
zero_v.data[j] = (scalar_t)0;
|
| 305 |
+
|
| 306 |
+
int e = -1;
|
| 307 |
+
float w0 = 0.f, w1 = 0.f, w2 = 0.f;
|
| 308 |
+
float dw0 = 0.f, dw1 = 0.f, dw2 = 0.f;
|
| 309 |
+
for (; row < end; ++row) {
|
| 310 |
+
const int64_t base = row * vec_d;
|
| 311 |
+
v_t *__restrict__ gi_v = reinterpret_cast<v_t *>(grad_input) + base;
|
| 312 |
+
v_t *__restrict__ gm_v = reinterpret_cast<v_t *>(grad_mul) + base;
|
| 313 |
+
if (row >= n_valid) { // padding row
|
| 314 |
+
for (int i = lane; i < vec_d; i += WARP_SIZE) {
|
| 315 |
+
gi_v[i] = zero_v;
|
| 316 |
+
gm_v[i] = zero_v;
|
| 317 |
+
}
|
| 318 |
+
if (grad_scores != nullptr && lane == 0)
|
| 319 |
+
grad_scores[row] = 0.f;
|
| 320 |
+
continue;
|
| 321 |
+
}
|
| 322 |
+
if (e < 0 || row >= (int64_t)offsets[e]) {
|
| 323 |
+
if (e >= 0)
|
| 324 |
+
flush_weight_grad(dw0, dw1, dw2, e + expert_offset, weight_grad);
|
| 325 |
+
e = find_expert(offsets, num_experts, row);
|
| 326 |
+
load_w(weight, e, expert_offset, w0, w1, w2);
|
| 327 |
+
}
|
| 328 |
+
const float score = (scores != nullptr) ? scores[row] : 1.f;
|
| 329 |
+
const v_t *__restrict__ in_v = reinterpret_cast<const v_t *>(input) + base;
|
| 330 |
+
const v_t *__restrict__ m_v = reinterpret_cast<const v_t *>(mul) + base;
|
| 331 |
+
const v_t *__restrict__ go_v =
|
| 332 |
+
reinterpret_cast<const v_t *>(grad_output) + base;
|
| 333 |
+
float gs = 0.f;
|
| 334 |
+
for (int i = lane; i < vec_d; i += WARP_SIZE) {
|
| 335 |
+
const v_t xv = in_v[i];
|
| 336 |
+
const v_t mv = m_v[i];
|
| 337 |
+
const v_t gv = go_v[i];
|
| 338 |
+
v_t gi, gm;
|
| 339 |
+
#pragma unroll
|
| 340 |
+
for (int j = 0; j < width; ++j) {
|
| 341 |
+
float dx, dm;
|
| 342 |
+
bwd_elem((float)xv.data[j], (float)mv.data[j], (float)gv.data[j], score,
|
| 343 |
+
p, w0, w1, w2, do_clamp, dx, dm, dw0, dw1, dw2, gs);
|
| 344 |
+
gi.data[j] = (scalar_t)dx;
|
| 345 |
+
gm.data[j] = (scalar_t)dm;
|
| 346 |
+
}
|
| 347 |
+
gi_v[i] = gi;
|
| 348 |
+
gm_v[i] = gm;
|
| 349 |
+
}
|
| 350 |
+
if (grad_scores != nullptr) {
|
| 351 |
+
gs = warp_sum(gs);
|
| 352 |
+
if (lane == 0)
|
| 353 |
+
grad_scores[row] = gs;
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
if (e >= 0)
|
| 357 |
+
flush_weight_grad(dw0, dw1, dw2, e + expert_offset, weight_grad);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
// Scalar fallback backward
|
| 361 |
+
template <typename scalar_t>
|
| 362 |
+
__global__ void __launch_bounds__(BLOCK) epolynorm_tanh_bwd_scalar(
|
| 363 |
+
scalar_t *__restrict__ grad_input, scalar_t *__restrict__ grad_mul,
|
| 364 |
+
float *__restrict__ weight_grad, float *__restrict__ grad_scores,
|
| 365 |
+
const scalar_t *__restrict__ grad_output,
|
| 366 |
+
const scalar_t *__restrict__ input, const scalar_t *__restrict__ mul,
|
| 367 |
+
const float *__restrict__ weight, const int32_t *__restrict__ offsets,
|
| 368 |
+
const float *__restrict__ scores, const Params p, const int D,
|
| 369 |
+
const int64_t N, const int num_experts, const int expert_offset) {
|
| 370 |
+
const bool do_clamp = (p.hidden_clamp >= 0.f);
|
| 371 |
+
const int lane = threadIdx.x % WARP_SIZE;
|
| 372 |
+
const int64_t n_valid = offsets[num_experts - 1];
|
| 373 |
+
int64_t row, end;
|
| 374 |
+
warp_rows(N, row, end);
|
| 375 |
+
|
| 376 |
+
int e = -1;
|
| 377 |
+
float w0 = 0.f, w1 = 0.f, w2 = 0.f;
|
| 378 |
+
float dw0 = 0.f, dw1 = 0.f, dw2 = 0.f;
|
| 379 |
+
for (; row < end; ++row) {
|
| 380 |
+
const int64_t off = row * D;
|
| 381 |
+
if (row >= n_valid) {
|
| 382 |
+
for (int i = lane; i < D; i += WARP_SIZE) {
|
| 383 |
+
grad_input[off + i] = (scalar_t)0;
|
| 384 |
+
grad_mul[off + i] = (scalar_t)0;
|
| 385 |
+
}
|
| 386 |
+
if (grad_scores != nullptr && lane == 0)
|
| 387 |
+
grad_scores[row] = 0.f;
|
| 388 |
+
continue;
|
| 389 |
+
}
|
| 390 |
+
if (e < 0 || row >= (int64_t)offsets[e]) {
|
| 391 |
+
if (e >= 0)
|
| 392 |
+
flush_weight_grad(dw0, dw1, dw2, e + expert_offset, weight_grad);
|
| 393 |
+
e = find_expert(offsets, num_experts, row);
|
| 394 |
+
load_w(weight, e, expert_offset, w0, w1, w2);
|
| 395 |
+
}
|
| 396 |
+
const float score = (scores != nullptr) ? scores[row] : 1.f;
|
| 397 |
+
float gs = 0.f;
|
| 398 |
+
for (int i = lane; i < D; i += WARP_SIZE) {
|
| 399 |
+
float dx, dm;
|
| 400 |
+
bwd_elem((float)input[off + i], (float)mul[off + i],
|
| 401 |
+
(float)grad_output[off + i], score, p, w0, w1, w2, do_clamp, dx,
|
| 402 |
+
dm, dw0, dw1, dw2, gs);
|
| 403 |
+
grad_input[off + i] = (scalar_t)dx;
|
| 404 |
+
grad_mul[off + i] = (scalar_t)dm;
|
| 405 |
+
}
|
| 406 |
+
if (grad_scores != nullptr) {
|
| 407 |
+
gs = warp_sum(gs);
|
| 408 |
+
if (lane == 0)
|
| 409 |
+
grad_scores[row] = gs;
|
| 410 |
+
}
|
| 411 |
+
}
|
| 412 |
+
if (e >= 0)
|
| 413 |
+
flush_weight_grad(dw0, dw1, dw2, e + expert_offset, weight_grad);
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
static int grid_for(int64_t N) {
|
| 417 |
+
// Enough warps to give each one a short contiguous row chunk, capped so the
|
| 418 |
+
// grid stays resident (16 blocks of 8 warps per SM).
|
| 419 |
+
const int sms = at::cuda::getCurrentDeviceProperties()->multiProcessorCount;
|
| 420 |
+
const int64_t blocks = (N + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;
|
| 421 |
+
return (int)std::min<int64_t>(blocks, (int64_t)sms * 16);
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
static Params make_params(double gate_beta, double up_beta, double s0,
|
| 425 |
+
double s1, double s2, double hidden_clamp) {
|
| 426 |
+
Params p;
|
| 427 |
+
p.gate_beta = (float)gate_beta;
|
| 428 |
+
p.inv_gate_beta = 1.f / (float)gate_beta;
|
| 429 |
+
p.up_beta = (float)up_beta;
|
| 430 |
+
p.inv_up_beta = 1.f / (float)up_beta;
|
| 431 |
+
p.inv_s0 = 1.f / (float)s0;
|
| 432 |
+
p.inv_s1 = 1.f / (float)s1;
|
| 433 |
+
p.inv_s2 = 1.f / (float)s2;
|
| 434 |
+
p.hidden_clamp = (float)hidden_clamp;
|
| 435 |
+
return p;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
} // namespace epoly
|
| 439 |
+
} // namespace motif
|
| 440 |
+
|
| 441 |
+
// ---------------------------------------------------------------------------
|
| 442 |
+
// Host entry points
|
| 443 |
+
// ---------------------------------------------------------------------------
|
| 444 |
+
static void check_inputs(const torch::Tensor &input, const torch::Tensor &mul,
|
| 445 |
+
const torch::Tensor &weight,
|
| 446 |
+
const torch::Tensor &offsets, int64_t expert_offset,
|
| 447 |
+
const std::optional<torch::Tensor> &scores) {
|
| 448 |
+
TORCH_CHECK(input.is_cuda() && mul.is_cuda(), "inputs must be CUDA tensors");
|
| 449 |
+
TORCH_CHECK(input.dim() == 2 && mul.dim() == 2, "input/mul must be [N, D]");
|
| 450 |
+
TORCH_CHECK(input.sizes() == mul.sizes(), "input and mul must match");
|
| 451 |
+
TORCH_CHECK(input.scalar_type() == mul.scalar_type(),
|
| 452 |
+
"input and mul must share a dtype");
|
| 453 |
+
TORCH_CHECK(input.is_contiguous() && mul.is_contiguous(),
|
| 454 |
+
"input/mul must be contiguous");
|
| 455 |
+
TORCH_CHECK(weight.is_cuda() && weight.scalar_type() == torch::kFloat &&
|
| 456 |
+
weight.dim() == 2 && weight.size(1) == 3 &&
|
| 457 |
+
weight.is_contiguous(),
|
| 458 |
+
"weight must be a contiguous fp32 CUDA tensor [num_experts, 3]");
|
| 459 |
+
TORCH_CHECK(offsets.is_cuda() && offsets.scalar_type() == torch::kInt &&
|
| 460 |
+
offsets.dim() == 1 && offsets.numel() >= 1 &&
|
| 461 |
+
offsets.is_contiguous(),
|
| 462 |
+
"offsets must be a contiguous int32 CUDA tensor [num_experts]");
|
| 463 |
+
TORCH_CHECK(expert_offset >= 0 &&
|
| 464 |
+
expert_offset + offsets.numel() <= weight.size(0),
|
| 465 |
+
"expert_offset + len(offsets) must fit weight's rows");
|
| 466 |
+
if (scores.has_value()) {
|
| 467 |
+
TORCH_CHECK(scores->is_cuda() && scores->scalar_type() == torch::kFloat,
|
| 468 |
+
"scores must be a fp32 CUDA tensor");
|
| 469 |
+
TORCH_CHECK(scores->numel() == input.size(0),
|
| 470 |
+
"scores must have one entry per row");
|
| 471 |
+
TORCH_CHECK(scores->is_contiguous(), "scores must be contiguous");
|
| 472 |
+
}
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
torch::Tensor
|
| 476 |
+
epolynorm_tanh_forward(const torch::Tensor &input, const torch::Tensor &mul,
|
| 477 |
+
const torch::Tensor &weight,
|
| 478 |
+
const torch::Tensor &offsets, int64_t expert_offset,
|
| 479 |
+
const std::optional<torch::Tensor> &scores,
|
| 480 |
+
double gate_beta, double up_beta, double s0, double s1,
|
| 481 |
+
double s2, double hidden_clamp) {
|
| 482 |
+
check_inputs(input, mul, weight, offsets, expert_offset, scores);
|
| 483 |
+
const int D = input.size(-1);
|
| 484 |
+
const int64_t N = input.size(0);
|
| 485 |
+
const int num_experts = offsets.numel();
|
| 486 |
+
auto output = torch::empty_like(input);
|
| 487 |
+
if (N == 0)
|
| 488 |
+
return output;
|
| 489 |
+
const at::cuda::OptionalCUDAGuard device_guard(device_of(input));
|
| 490 |
+
const cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
| 491 |
+
const float *scores_ptr =
|
| 492 |
+
scores.has_value() ? scores->data_ptr<float>() : nullptr;
|
| 493 |
+
const motif::epoly::Params p =
|
| 494 |
+
motif::epoly::make_params(gate_beta, up_beta, s0, s1, s2, hidden_clamp);
|
| 495 |
+
dim3 grid(motif::epoly::grid_for(N));
|
| 496 |
+
dim3 block(motif::epoly::BLOCK);
|
| 497 |
+
|
| 498 |
+
#define FWD_LAUNCH(width_val) \
|
| 499 |
+
MOTIF_DISPATCH_FLOATING_TYPES( \
|
| 500 |
+
input.scalar_type(), "epolynorm_tanh_fwd", [&] { \
|
| 501 |
+
motif::epoly::epolynorm_tanh_fwd_kernel<scalar_t, width_val> \
|
| 502 |
+
<<<grid, block, 0, stream>>>( \
|
| 503 |
+
output.data_ptr<scalar_t>(), input.data_ptr<scalar_t>(), \
|
| 504 |
+
mul.data_ptr<scalar_t>(), weight.data_ptr<float>(), \
|
| 505 |
+
offsets.data_ptr<int32_t>(), scores_ptr, p, D, N, num_experts, \
|
| 506 |
+
(int)expert_offset); \
|
| 507 |
+
})
|
| 508 |
+
if (D % 8 == 0 && input.element_size() == 2)
|
| 509 |
+
FWD_LAUNCH(8);
|
| 510 |
+
else if (D % 4 == 0 && input.element_size() == 4)
|
| 511 |
+
FWD_LAUNCH(4);
|
| 512 |
+
else {
|
| 513 |
+
MOTIF_DISPATCH_FLOATING_TYPES(
|
| 514 |
+
input.scalar_type(), "epolynorm_tanh_fwd_scalar", [&] {
|
| 515 |
+
motif::epoly::epolynorm_tanh_fwd_scalar<scalar_t>
|
| 516 |
+
<<<grid, block, 0, stream>>>(
|
| 517 |
+
output.data_ptr<scalar_t>(), input.data_ptr<scalar_t>(),
|
| 518 |
+
mul.data_ptr<scalar_t>(), weight.data_ptr<float>(),
|
| 519 |
+
offsets.data_ptr<int32_t>(), scores_ptr, p, D, N, num_experts,
|
| 520 |
+
(int)expert_offset);
|
| 521 |
+
});
|
| 522 |
+
}
|
| 523 |
+
#undef FWD_LAUNCH
|
| 524 |
+
return output;
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
|
| 528 |
+
epolynorm_tanh_backward(const torch::Tensor &grad_output,
|
| 529 |
+
const torch::Tensor &input, const torch::Tensor &mul,
|
| 530 |
+
const torch::Tensor &weight,
|
| 531 |
+
const torch::Tensor &offsets, int64_t expert_offset,
|
| 532 |
+
const std::optional<torch::Tensor> &scores,
|
| 533 |
+
double gate_beta, double up_beta, double s0, double s1,
|
| 534 |
+
double s2, double hidden_clamp) {
|
| 535 |
+
check_inputs(input, mul, weight, offsets, expert_offset, scores);
|
| 536 |
+
TORCH_CHECK(grad_output.sizes() == input.sizes() &&
|
| 537 |
+
grad_output.scalar_type() == input.scalar_type() &&
|
| 538 |
+
grad_output.is_contiguous(),
|
| 539 |
+
"grad_output must match input");
|
| 540 |
+
const int D = input.size(-1);
|
| 541 |
+
const int64_t N = input.size(0);
|
| 542 |
+
const int num_experts = offsets.numel();
|
| 543 |
+
auto grad_input = torch::empty_like(input);
|
| 544 |
+
auto grad_mul = torch::empty_like(mul);
|
| 545 |
+
auto weight_grad = torch::zeros_like(weight);
|
| 546 |
+
// grad_scores: [N, 1] fp32 when scores are given, else an empty [0] tensor
|
| 547 |
+
// (never undefined, so the op always returns four tensors).
|
| 548 |
+
auto grad_scores = scores.has_value() ? torch::empty({N, 1}, weight.options())
|
| 549 |
+
: torch::empty({0}, weight.options());
|
| 550 |
+
if (N == 0)
|
| 551 |
+
return {grad_input, grad_mul, weight_grad, grad_scores};
|
| 552 |
+
const at::cuda::OptionalCUDAGuard device_guard(device_of(input));
|
| 553 |
+
const cudaStream_t stream = at::cuda::getCurrentCUDAStream();
|
| 554 |
+
const float *scores_ptr =
|
| 555 |
+
scores.has_value() ? scores->data_ptr<float>() : nullptr;
|
| 556 |
+
float *gs_ptr = scores.has_value() ? grad_scores.data_ptr<float>() : nullptr;
|
| 557 |
+
const motif::epoly::Params p =
|
| 558 |
+
motif::epoly::make_params(gate_beta, up_beta, s0, s1, s2, hidden_clamp);
|
| 559 |
+
dim3 grid(motif::epoly::grid_for(N));
|
| 560 |
+
dim3 block(motif::epoly::BLOCK);
|
| 561 |
+
|
| 562 |
+
#define BWD_LAUNCH(width_val) \
|
| 563 |
+
MOTIF_DISPATCH_FLOATING_TYPES( \
|
| 564 |
+
input.scalar_type(), "epolynorm_tanh_bwd", [&] { \
|
| 565 |
+
motif::epoly::epolynorm_tanh_bwd_kernel<scalar_t, width_val> \
|
| 566 |
+
<<<grid, block, 0, stream>>>( \
|
| 567 |
+
grad_input.data_ptr<scalar_t>(), \
|
| 568 |
+
grad_mul.data_ptr<scalar_t>(), weight_grad.data_ptr<float>(), \
|
| 569 |
+
gs_ptr, grad_output.data_ptr<scalar_t>(), \
|
| 570 |
+
input.data_ptr<scalar_t>(), mul.data_ptr<scalar_t>(), \
|
| 571 |
+
weight.data_ptr<float>(), offsets.data_ptr<int32_t>(), \
|
| 572 |
+
scores_ptr, p, D, N, num_experts, (int)expert_offset); \
|
| 573 |
+
})
|
| 574 |
+
if (D % 8 == 0 && input.element_size() == 2)
|
| 575 |
+
BWD_LAUNCH(8);
|
| 576 |
+
else if (D % 4 == 0 && input.element_size() == 4)
|
| 577 |
+
BWD_LAUNCH(4);
|
| 578 |
+
else {
|
| 579 |
+
MOTIF_DISPATCH_FLOATING_TYPES(
|
| 580 |
+
input.scalar_type(), "epolynorm_tanh_bwd_scalar", [&] {
|
| 581 |
+
motif::epoly::epolynorm_tanh_bwd_scalar<scalar_t>
|
| 582 |
+
<<<grid, block, 0, stream>>>(
|
| 583 |
+
grad_input.data_ptr<scalar_t>(),
|
| 584 |
+
grad_mul.data_ptr<scalar_t>(), weight_grad.data_ptr<float>(),
|
| 585 |
+
gs_ptr, grad_output.data_ptr<scalar_t>(),
|
| 586 |
+
input.data_ptr<scalar_t>(), mul.data_ptr<scalar_t>(),
|
| 587 |
+
weight.data_ptr<float>(), offsets.data_ptr<int32_t>(),
|
| 588 |
+
scores_ptr, p, D, N, num_experts, (int)expert_offset);
|
| 589 |
+
});
|
| 590 |
+
}
|
| 591 |
+
#undef BWD_LAUNCH
|
| 592 |
+
return {grad_input, grad_mul, weight_grad, grad_scores};
|
| 593 |
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bandwidth benchmark for the fused grouped EPolyNormTanh kernels.
|
| 2 |
+
|
| 3 |
+
Compares, per (N, D, dtype, scores):
|
| 4 |
+
- fused CUDA forward / backward (torch.ops._activation.epolynorm_tanh_*)
|
| 5 |
+
- the eager grouped reference (what llm-training runs today, per-expert
|
| 6 |
+
coefficients gathered per row) forward / backward
|
| 7 |
+
- torch.compile'd reference forward / backward
|
| 8 |
+
- fused through autograd (what training uses)
|
| 9 |
+
and reports achieved GB/s against the GPU's theoretical HBM bandwidth (NVML bus
|
| 10 |
+
width x memory clock, DDR) and against a measured device-copy ceiling.
|
| 11 |
+
|
| 12 |
+
Rows are spread over --experts experts (random multinomial counts, like a
|
| 13 |
+
routed MoE microbatch); the weight has --experts + 4 rows and expert_offset 4.
|
| 14 |
+
|
| 15 |
+
Byte accounting (the minimum traffic of each op, in the input dtype):
|
| 16 |
+
forward : read x, mul; write out -> 3 * N * D * esz
|
| 17 |
+
backward: read x, mul, grad_out; write grad_x, grad_mul -> 5 * N * D * esz
|
| 18 |
+
(+ N * 4 for the fp32 scores on each side when scores are used)
|
| 19 |
+
|
| 20 |
+
Usage: python benchmarks/bench_epolynorm_tanh.py [--d 1152] [--n 294912 ...]
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
import argparse
|
| 24 |
+
import statistics
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
|
| 28 |
+
torch._dynamo.config.cache_size_limit = 64
|
| 29 |
+
|
| 30 |
+
from activation.epolynorm_tanh import (DEFAULT_GATE_BETA, DEFAULT_SCALES,
|
| 31 |
+
DEFAULT_UP_BETA,
|
| 32 |
+
fused_grouped_epolynorm_tanh,
|
| 33 |
+
grouped_epolynorm_tanh_ref)
|
| 34 |
+
|
| 35 |
+
_ops = torch.ops._activation
|
| 36 |
+
EXPERT_OFFSET = 4
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def theoretical_bw_gbs() -> tuple[float, str]:
|
| 40 |
+
try:
|
| 41 |
+
import pynvml as n
|
| 42 |
+
n.nvmlInit()
|
| 43 |
+
h = n.nvmlDeviceGetHandleByIndex(torch.cuda.current_device())
|
| 44 |
+
bus = n.nvmlDeviceGetMemoryBusWidth(h)
|
| 45 |
+
clk = n.nvmlDeviceGetMaxClockInfo(h, n.NVML_CLOCK_MEM) # MHz
|
| 46 |
+
name = n.nvmlDeviceGetName(h)
|
| 47 |
+
# HBM3/3e: NVML reports the memory clock; data rate is 2x (DDR).
|
| 48 |
+
gbs = 2.0 * clk * 1e6 * bus / 8 / 1e9
|
| 49 |
+
return gbs, f"{name}: bus {bus} bit x {clk} MHz x2 (NVML)"
|
| 50 |
+
except Exception as e: # pragma: no cover
|
| 51 |
+
name = torch.cuda.get_device_name()
|
| 52 |
+
table = {
|
| 53 |
+
"B300": 8000.0,
|
| 54 |
+
"B200": 8000.0,
|
| 55 |
+
"H100": 3350.0,
|
| 56 |
+
"H200": 4800.0
|
| 57 |
+
}
|
| 58 |
+
for k, v in table.items():
|
| 59 |
+
if k in name:
|
| 60 |
+
return v, f"{name}: spec table ({e.__class__.__name__})"
|
| 61 |
+
return float("nan"), f"{name}: unknown ({e})"
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def bench(fn, iters=50, warmup=10) -> float:
|
| 65 |
+
for _ in range(warmup):
|
| 66 |
+
fn()
|
| 67 |
+
torch.cuda.synchronize()
|
| 68 |
+
times = []
|
| 69 |
+
for _ in range(iters):
|
| 70 |
+
s = torch.cuda.Event(enable_timing=True)
|
| 71 |
+
e = torch.cuda.Event(enable_timing=True)
|
| 72 |
+
s.record()
|
| 73 |
+
fn()
|
| 74 |
+
e.record()
|
| 75 |
+
torch.cuda.synchronize()
|
| 76 |
+
times.append(s.elapsed_time(e))
|
| 77 |
+
return statistics.median(times)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def copy_ceiling_gbs(nbytes: int) -> float:
|
| 81 |
+
x = torch.empty(nbytes // 2, dtype=torch.bfloat16, device="cuda")
|
| 82 |
+
y = torch.empty_like(x)
|
| 83 |
+
ms = bench(lambda: y.copy_(x))
|
| 84 |
+
return 2 * x.nbytes / ms / 1e6
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def make_offsets(n, num_experts):
|
| 88 |
+
torch.manual_seed(0)
|
| 89 |
+
assign = torch.multinomial(torch.ones(num_experts) / num_experts,
|
| 90 |
+
n,
|
| 91 |
+
replacement=True)
|
| 92 |
+
counts = torch.bincount(assign, minlength=num_experts)
|
| 93 |
+
return torch.cumsum(counts, 0).to(torch.int32).cuda()
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def run(n, d, num_experts, dtype, with_scores, peak):
|
| 97 |
+
esz = torch.tensor([], dtype=dtype).element_size()
|
| 98 |
+
x = torch.randn(n, d, device="cuda", dtype=dtype) * 2
|
| 99 |
+
m = torch.randn(n, d, device="cuda", dtype=dtype) * 8
|
| 100 |
+
go = torch.randn(n, d, device="cuda", dtype=dtype)
|
| 101 |
+
offsets = make_offsets(n, num_experts)
|
| 102 |
+
a = torch.full((num_experts + EXPERT_OFFSET, 3), 1 / 3, device="cuda")
|
| 103 |
+
w = torch.sigmoid(a)
|
| 104 |
+
s = (torch.rand(n, device="cuda") * 0.5 + 0.5) if with_scores else None
|
| 105 |
+
consts = (DEFAULT_GATE_BETA, DEFAULT_UP_BETA, *DEFAULT_SCALES, 1.0e6)
|
| 106 |
+
fwd_bytes = 3 * n * d * esz + (n * 4 if with_scores else 0)
|
| 107 |
+
bwd_bytes = 5 * n * d * esz + (n * 8 if with_scores else 0)
|
| 108 |
+
|
| 109 |
+
res = {}
|
| 110 |
+
# fused kernels, called directly (no autograd bookkeeping)
|
| 111 |
+
res["fused fwd"] = (bench(lambda: _ops.epolynorm_tanh_forward(
|
| 112 |
+
x, m, w, offsets, EXPERT_OFFSET, s, *consts)), fwd_bytes)
|
| 113 |
+
res["fused bwd"] = (bench(lambda: _ops.epolynorm_tanh_backward(
|
| 114 |
+
go, x, m, w, offsets, EXPERT_OFFSET, s, *consts)), bwd_bytes)
|
| 115 |
+
|
| 116 |
+
# eager reference: forward, and forward+backward via autograd
|
| 117 |
+
xr = x.clone().requires_grad_(True)
|
| 118 |
+
mr = m.clone().requires_grad_(True)
|
| 119 |
+
sr = s.reshape(-1, 1).clone().requires_grad_(True) if with_scores else None
|
| 120 |
+
wr = w.clone().requires_grad_(True)
|
| 121 |
+
|
| 122 |
+
def eager_fwd():
|
| 123 |
+
return grouped_epolynorm_tanh_ref(x,
|
| 124 |
+
m,
|
| 125 |
+
w,
|
| 126 |
+
offsets,
|
| 127 |
+
EXPERT_OFFSET,
|
| 128 |
+
scores=sr,
|
| 129 |
+
hidden_clamp=1.0e6)
|
| 130 |
+
|
| 131 |
+
def eager_fb():
|
| 132 |
+
out = grouped_epolynorm_tanh_ref(xr,
|
| 133 |
+
mr,
|
| 134 |
+
wr,
|
| 135 |
+
offsets,
|
| 136 |
+
EXPERT_OFFSET,
|
| 137 |
+
scores=sr,
|
| 138 |
+
hidden_clamp=1.0e6)
|
| 139 |
+
out.backward(go)
|
| 140 |
+
|
| 141 |
+
res["eager fwd"] = (bench(eager_fwd, iters=20), fwd_bytes)
|
| 142 |
+
t_fb = bench(eager_fb, iters=20)
|
| 143 |
+
res["eager bwd (fwd+bwd - fwd)"] = (t_fb - res["eager fwd"][0], bwd_bytes)
|
| 144 |
+
|
| 145 |
+
# compiled reference
|
| 146 |
+
try:
|
| 147 |
+
cref = torch.compile(grouped_epolynorm_tanh_ref, dynamic=False)
|
| 148 |
+
|
| 149 |
+
def comp_fwd():
|
| 150 |
+
return cref(x,
|
| 151 |
+
m,
|
| 152 |
+
w,
|
| 153 |
+
offsets,
|
| 154 |
+
EXPERT_OFFSET,
|
| 155 |
+
scores=sr,
|
| 156 |
+
hidden_clamp=1.0e6)
|
| 157 |
+
|
| 158 |
+
def comp_fb():
|
| 159 |
+
out = cref(xr,
|
| 160 |
+
mr,
|
| 161 |
+
wr,
|
| 162 |
+
offsets,
|
| 163 |
+
EXPERT_OFFSET,
|
| 164 |
+
scores=sr,
|
| 165 |
+
hidden_clamp=1.0e6)
|
| 166 |
+
out.backward(go)
|
| 167 |
+
|
| 168 |
+
res["compiled fwd"] = (bench(comp_fwd, iters=20), fwd_bytes)
|
| 169 |
+
t_fb = bench(comp_fb, iters=20)
|
| 170 |
+
res["compiled bwd (fwd+bwd - fwd)"] = (t_fb - res["compiled fwd"][0],
|
| 171 |
+
bwd_bytes)
|
| 172 |
+
except Exception as e: # pragma: no cover
|
| 173 |
+
print(f" (torch.compile failed: {e.__class__.__name__}: {e})")
|
| 174 |
+
|
| 175 |
+
# fused through autograd (what training uses), for the end-to-end number
|
| 176 |
+
xf = x.clone().requires_grad_(True)
|
| 177 |
+
mf = m.clone().requires_grad_(True)
|
| 178 |
+
sf = s.clone().requires_grad_(True) if with_scores else None
|
| 179 |
+
wf = w.clone().requires_grad_(True)
|
| 180 |
+
|
| 181 |
+
def fused_fb():
|
| 182 |
+
out = fused_grouped_epolynorm_tanh(xf,
|
| 183 |
+
mf,
|
| 184 |
+
wf,
|
| 185 |
+
offsets,
|
| 186 |
+
EXPERT_OFFSET,
|
| 187 |
+
scores=sf,
|
| 188 |
+
hidden_clamp=1.0e6)
|
| 189 |
+
out.backward(go)
|
| 190 |
+
|
| 191 |
+
res["fused fwd+bwd (autograd)"] = (bench(fused_fb), fwd_bytes + bwd_bytes)
|
| 192 |
+
|
| 193 |
+
tag = (f"N={n} D={d} E={num_experts} {str(dtype).split('.')[-1]} "
|
| 194 |
+
f"scores={with_scores}")
|
| 195 |
+
print(
|
| 196 |
+
f"\n== {tag} (fwd min bytes {fwd_bytes/1e9:.3f} GB, bwd {bwd_bytes/1e9:.3f} GB)"
|
| 197 |
+
)
|
| 198 |
+
print(f" {'variant':32s} {'ms':>9s} {'GB/s':>9s} {'% peak':>8s}")
|
| 199 |
+
for k, (ms, nb) in res.items():
|
| 200 |
+
gbs = nb / ms / 1e6
|
| 201 |
+
print(f" {k:32s} {ms:9.3f} {gbs:9.0f} {100*gbs/peak:7.1f}%")
|
| 202 |
+
return res
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def main():
|
| 206 |
+
ap = argparse.ArgumentParser()
|
| 207 |
+
ap.add_argument("--d", type=int, nargs="+", default=[1152])
|
| 208 |
+
# 384B at local bs 12: 12*4096 tokens/rank x top_k 6 = 294912 routed rows per microbatch
|
| 209 |
+
ap.add_argument("--n", type=int, nargs="+", default=[294912, 65536, 8192])
|
| 210 |
+
# 384B: 384 experts / EP 8 = 48 local experts per rank
|
| 211 |
+
ap.add_argument("--experts", type=int, default=48)
|
| 212 |
+
ap.add_argument("--dtypes", nargs="+", default=["bfloat16", "float32"])
|
| 213 |
+
ap.add_argument("--no-scores", action="store_true")
|
| 214 |
+
args = ap.parse_args()
|
| 215 |
+
|
| 216 |
+
peak, how = theoretical_bw_gbs()
|
| 217 |
+
print(f"theoretical HBM bandwidth: {peak:.0f} GB/s [{how}]")
|
| 218 |
+
for mb in (256, 2048):
|
| 219 |
+
c = copy_ceiling_gbs(mb * 2**20)
|
| 220 |
+
print(
|
| 221 |
+
f"device copy ceiling @ {mb} MiB: {c:.0f} GB/s ({100*c/peak:.1f}% of theoretical)"
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
for dt in args.dtypes:
|
| 225 |
+
dtype = getattr(torch, dt)
|
| 226 |
+
for d in args.d:
|
| 227 |
+
for n in args.n:
|
| 228 |
+
for with_scores in ((False, ) if args.no_scores else
|
| 229 |
+
(True, False)):
|
| 230 |
+
run(n, d, args.experts, dtype, with_scores, peak)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
if __name__ == "__main__":
|
| 234 |
+
main()
|
|
@@ -20,6 +20,7 @@ src = [
|
|
| 20 |
"activation/rms_norm.cu",
|
| 21 |
"activation/fused_add_rms_norm.cu",
|
| 22 |
"activation/grouped_poly_norm.cu",
|
|
|
|
| 23 |
"activation/cuda_compat.h",
|
| 24 |
"activation/dispatch_utils.h",
|
| 25 |
"activation/assert_utils.h",
|
|
@@ -35,6 +36,7 @@ src = [
|
|
| 35 |
"activation/rms_norm.cu",
|
| 36 |
"activation/fused_add_rms_norm.cu",
|
| 37 |
"activation/grouped_poly_norm.cu",
|
|
|
|
| 38 |
"activation/cuda_compat.h",
|
| 39 |
"activation/dispatch_utils.h",
|
| 40 |
"activation/assert_utils.h",
|
|
|
|
| 20 |
"activation/rms_norm.cu",
|
| 21 |
"activation/fused_add_rms_norm.cu",
|
| 22 |
"activation/grouped_poly_norm.cu",
|
| 23 |
+
"activation/epolynorm_tanh.cu",
|
| 24 |
"activation/cuda_compat.h",
|
| 25 |
"activation/dispatch_utils.h",
|
| 26 |
"activation/assert_utils.h",
|
|
|
|
| 36 |
"activation/rms_norm.cu",
|
| 37 |
"activation/fused_add_rms_norm.cu",
|
| 38 |
"activation/grouped_poly_norm.cu",
|
| 39 |
+
"activation/epolynorm_tanh.cu",
|
| 40 |
"activation/cuda_compat.h",
|
| 41 |
"activation/dispatch_utils.h",
|
| 42 |
"activation/assert_utils.h",
|
|
@@ -24,6 +24,7 @@ CUDA_SOURCES = [
|
|
| 24 |
"activation/rms_norm.cu",
|
| 25 |
"activation/fused_add_rms_norm.cu",
|
| 26 |
"activation/grouped_poly_norm.cu",
|
|
|
|
| 27 |
]
|
| 28 |
|
| 29 |
CPP_SOURCES = [
|
|
@@ -58,6 +59,9 @@ NVCC_FLAGS = [
|
|
| 58 |
cuda_version = tuple(int(x) for x in torch.version.cuda.split(".")[:2])
|
| 59 |
if cuda_version >= (12, 8):
|
| 60 |
NVCC_FLAGS.append("-gencode=arch=compute_100,code=sm_100")
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
CXX_FLAGS = ["-O3", CXX_STD]
|
| 63 |
|
|
|
|
| 24 |
"activation/rms_norm.cu",
|
| 25 |
"activation/fused_add_rms_norm.cu",
|
| 26 |
"activation/grouped_poly_norm.cu",
|
| 27 |
+
"activation/epolynorm_tanh.cu",
|
| 28 |
]
|
| 29 |
|
| 30 |
CPP_SOURCES = [
|
|
|
|
| 59 |
cuda_version = tuple(int(x) for x in torch.version.cuda.split(".")[:2])
|
| 60 |
if cuda_version >= (12, 8):
|
| 61 |
NVCC_FLAGS.append("-gencode=arch=compute_100,code=sm_100")
|
| 62 |
+
# B300 (sm_103) needs CUDA 12.9+ to be named explicitly; sm_100 SASS also runs there.
|
| 63 |
+
if cuda_version >= (12, 9):
|
| 64 |
+
NVCC_FLAGS.append("-gencode=arch=compute_103,code=sm_103")
|
| 65 |
|
| 66 |
CXX_FLAGS = ["-O3", CXX_STD]
|
| 67 |
|
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fused grouped EPolyNormTanh vs the eager reference: forward, backward
|
| 2 |
+
(input, mul, per-expert weight, scores), expert_offset, padding rows,
|
| 3 |
+
hidden_clamp masks, odd D (scalar path), the single-expert wrapper, opcheck."""
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
import torch
|
| 7 |
+
from activation.epolynorm_tanh import (DEFAULT_GATE_BETA, DEFAULT_SCALES,
|
| 8 |
+
DEFAULT_UP_BETA, _has_cuda_ops,
|
| 9 |
+
epolynorm_tanh_ref,
|
| 10 |
+
grouped_epolynorm_tanh_ref)
|
| 11 |
+
|
| 12 |
+
if _has_cuda_ops:
|
| 13 |
+
from activation.epolynorm_tanh import (fused_epolynorm_tanh,
|
| 14 |
+
fused_grouped_epolynorm_tanh)
|
| 15 |
+
|
| 16 |
+
from .utils import assert_close
|
| 17 |
+
|
| 18 |
+
DTYPES = [torch.float, torch.bfloat16, torch.float16]
|
| 19 |
+
NUM_TOKENS = [1, 1024, 4096]
|
| 20 |
+
D = [1152, 256, 1280]
|
| 21 |
+
NUM_EXPERTS = [8, 384]
|
| 22 |
+
EXPERT_OFFSETS = [0, 4]
|
| 23 |
+
CUDA_DEVICES = ["cuda:0"]
|
| 24 |
+
|
| 25 |
+
pytestmark = pytest.mark.skipif(not _has_cuda_ops,
|
| 26 |
+
reason="CUDA ops not available")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _tols(dtype):
|
| 30 |
+
# Both sides compute elementwise in fp32; the kernel is built with
|
| 31 |
+
# --use_fast_math (approximate division / exp inside tanhf), so fp32 agrees
|
| 32 |
+
# to ~1e-4 relative rather than to the last ulp. 16-bit outputs are
|
| 33 |
+
# dominated by their own rounding (bf16 eps = 7.8e-3).
|
| 34 |
+
if dtype == torch.float32:
|
| 35 |
+
return dict(atol=1e-3, rtol=1e-3)
|
| 36 |
+
return dict(atol=2e-2, rtol=2e-2)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _wgrad_tol(dtype):
|
| 40 |
+
return 5e-3 if dtype == torch.float32 else 2e-2
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _make_inputs(n,
|
| 44 |
+
d,
|
| 45 |
+
num_experts,
|
| 46 |
+
dtype,
|
| 47 |
+
device,
|
| 48 |
+
seed=0,
|
| 49 |
+
scale=2.0,
|
| 50 |
+
expert_offset=0,
|
| 51 |
+
padding=0):
|
| 52 |
+
"""Rows grouped by expert (random counts), optional padding tail."""
|
| 53 |
+
torch.manual_seed(seed)
|
| 54 |
+
probs = torch.ones(num_experts) / num_experts
|
| 55 |
+
assign = torch.multinomial(probs, n, replacement=True)
|
| 56 |
+
counts = torch.bincount(assign, minlength=num_experts)
|
| 57 |
+
offsets = torch.cumsum(counts, 0).to(torch.int32).to(device)
|
| 58 |
+
total = n + padding
|
| 59 |
+
x = torch.randn(total, d, device=device, dtype=dtype) * scale
|
| 60 |
+
m = torch.randn(total, d, device=device, dtype=dtype) * scale * 4
|
| 61 |
+
a = torch.full((num_experts + expert_offset, 3),
|
| 62 |
+
1.0 / 3.0,
|
| 63 |
+
device=device,
|
| 64 |
+
dtype=torch.float32)
|
| 65 |
+
a = a + torch.randn_like(a) * 0.3
|
| 66 |
+
return x, m, a, offsets
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _make_scores(n, device):
|
| 70 |
+
return torch.rand(n, 1, device=device, dtype=torch.float32) * 0.5 + 0.5
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _run(fn,
|
| 74 |
+
x,
|
| 75 |
+
m,
|
| 76 |
+
a,
|
| 77 |
+
offsets,
|
| 78 |
+
expert_offset=0,
|
| 79 |
+
scores=None,
|
| 80 |
+
hidden_clamp=None):
|
| 81 |
+
xx = x.clone().detach().requires_grad_(True)
|
| 82 |
+
mm = m.clone().detach().requires_grad_(True)
|
| 83 |
+
aa = a.clone().detach().requires_grad_(True)
|
| 84 |
+
ss = scores.clone().detach().requires_grad_(
|
| 85 |
+
True) if scores is not None else None
|
| 86 |
+
out = fn(xx,
|
| 87 |
+
mm,
|
| 88 |
+
torch.sigmoid(aa),
|
| 89 |
+
offsets,
|
| 90 |
+
expert_offset,
|
| 91 |
+
scores=ss,
|
| 92 |
+
hidden_clamp=hidden_clamp)
|
| 93 |
+
# a random upstream gradient (not .sum()) so every element's chain is tested
|
| 94 |
+
torch.manual_seed(1)
|
| 95 |
+
go = torch.randn_like(out)
|
| 96 |
+
out.backward(go)
|
| 97 |
+
return out, xx.grad, mm.grad, aa.grad, (ss.grad
|
| 98 |
+
if ss is not None else None)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _check_wgrad(ga_c, ga_r, dtype, expert_offset=0):
|
| 102 |
+
"""Per-expert [E, 3] weight grads: reductions over each expert's rows
|
| 103 |
+
(atomicAdd order differs), compared relative to the largest entry."""
|
| 104 |
+
assert ga_c.shape == ga_r.shape
|
| 105 |
+
if expert_offset > 0: # rows below expert_offset get no gradient
|
| 106 |
+
assert (ga_c[:expert_offset] == 0).all()
|
| 107 |
+
assert (ga_r[:expert_offset] == 0).all()
|
| 108 |
+
rel = (ga_c - ga_r).abs().max() / ga_r.abs().max().clamp_min(1e-12)
|
| 109 |
+
assert rel < _wgrad_tol(dtype), (rel, ga_c, ga_r)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@pytest.mark.parametrize("n", NUM_TOKENS)
|
| 113 |
+
@pytest.mark.parametrize("d", D)
|
| 114 |
+
@pytest.mark.parametrize("num_experts", NUM_EXPERTS)
|
| 115 |
+
@pytest.mark.parametrize("dtype", DTYPES)
|
| 116 |
+
@pytest.mark.parametrize("device", CUDA_DEVICES)
|
| 117 |
+
def test_forward(n, d, num_experts, dtype, device):
|
| 118 |
+
torch.set_default_device(device)
|
| 119 |
+
x, m, a, offsets = _make_inputs(n, d, num_experts, dtype, device)
|
| 120 |
+
w = torch.sigmoid(a)
|
| 121 |
+
ref = grouped_epolynorm_tanh_ref(x, m, w, offsets)
|
| 122 |
+
out = fused_grouped_epolynorm_tanh(x, m, w, offsets)
|
| 123 |
+
assert out.shape == ref.shape and out.dtype == dtype
|
| 124 |
+
assert_close(out, ref, **_tols(dtype))
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
@pytest.mark.parametrize("n", NUM_TOKENS)
|
| 128 |
+
@pytest.mark.parametrize("d", D)
|
| 129 |
+
@pytest.mark.parametrize("num_experts", NUM_EXPERTS)
|
| 130 |
+
@pytest.mark.parametrize("dtype", DTYPES)
|
| 131 |
+
@pytest.mark.parametrize("with_scores", [False, True])
|
| 132 |
+
@pytest.mark.parametrize("device", CUDA_DEVICES)
|
| 133 |
+
def test_backward(n, d, num_experts, dtype, with_scores, device):
|
| 134 |
+
torch.set_default_device(device)
|
| 135 |
+
x, m, a, offsets = _make_inputs(n, d, num_experts, dtype, device)
|
| 136 |
+
s = _make_scores(n, device) if with_scores else None
|
| 137 |
+
r = _run(grouped_epolynorm_tanh_ref, x, m, a, offsets, scores=s)
|
| 138 |
+
c = _run(fused_grouped_epolynorm_tanh, x, m, a, offsets, scores=s)
|
| 139 |
+
out_r, gx_r, gm_r, ga_r, gs_r = r
|
| 140 |
+
out_c, gx_c, gm_c, ga_c, gs_c = c
|
| 141 |
+
t = _tols(dtype)
|
| 142 |
+
assert_close(out_c, out_r, **t)
|
| 143 |
+
assert_close(gx_c, gx_r, **t)
|
| 144 |
+
assert_close(gm_c, gm_r, **t)
|
| 145 |
+
_check_wgrad(ga_c, ga_r, dtype)
|
| 146 |
+
if with_scores:
|
| 147 |
+
assert gs_c.shape == gs_r.shape
|
| 148 |
+
assert_close(gs_c, gs_r, atol=1e-3 * d**0.5, rtol=1e-2)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@pytest.mark.parametrize("expert_offset", EXPERT_OFFSETS)
|
| 152 |
+
@pytest.mark.parametrize("padding", [0, 37])
|
| 153 |
+
@pytest.mark.parametrize("dtype", DTYPES)
|
| 154 |
+
@pytest.mark.parametrize("device", CUDA_DEVICES)
|
| 155 |
+
def test_expert_offset_and_padding(expert_offset, padding, dtype, device):
|
| 156 |
+
"""EP rank offset into the weight rows, and padding rows past offsets[-1]
|
| 157 |
+
(zero output, zero grads, no weight-grad contribution). The padding rows
|
| 158 |
+
hold NaN to prove they are never read into anything that matters."""
|
| 159 |
+
torch.set_default_device(device)
|
| 160 |
+
n, d, num_experts = 2048, 1152, 16
|
| 161 |
+
x, m, a, offsets = _make_inputs(n,
|
| 162 |
+
d,
|
| 163 |
+
num_experts,
|
| 164 |
+
dtype,
|
| 165 |
+
device,
|
| 166 |
+
expert_offset=expert_offset,
|
| 167 |
+
padding=padding)
|
| 168 |
+
if padding:
|
| 169 |
+
x[n:] = float("nan")
|
| 170 |
+
m[n:] = float("nan")
|
| 171 |
+
s = _make_scores(n + padding, device)
|
| 172 |
+
r = _run(grouped_epolynorm_tanh_ref, x, m, a, offsets, expert_offset, s)
|
| 173 |
+
c = _run(fused_grouped_epolynorm_tanh, x, m, a, offsets, expert_offset, s)
|
| 174 |
+
out_r, gx_r, gm_r, ga_r, gs_r = r
|
| 175 |
+
out_c, gx_c, gm_c, ga_c, gs_c = c
|
| 176 |
+
t = _tols(dtype)
|
| 177 |
+
assert_close(out_c[:n], out_r[:n], **t)
|
| 178 |
+
assert_close(gx_c[:n], gx_r[:n], **t)
|
| 179 |
+
assert_close(gm_c[:n], gm_r[:n], **t)
|
| 180 |
+
assert_close(gs_c[:n], gs_r[:n], atol=1e-3 * d**0.5, rtol=1e-2)
|
| 181 |
+
_check_wgrad(ga_c, ga_r, dtype, expert_offset)
|
| 182 |
+
if padding:
|
| 183 |
+
assert (out_c[n:] == 0).all()
|
| 184 |
+
assert (gx_c[n:] == 0).all()
|
| 185 |
+
assert (gm_c[n:] == 0).all()
|
| 186 |
+
assert (gs_c[n:] == 0).all()
|
| 187 |
+
assert torch.isfinite(ga_c).all()
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@pytest.mark.parametrize("dtype", DTYPES)
|
| 191 |
+
@pytest.mark.parametrize("hidden_clamp", [1000000.0, 3.0, 0.5])
|
| 192 |
+
@pytest.mark.parametrize("device", CUDA_DEVICES)
|
| 193 |
+
def test_hidden_clamp(dtype, hidden_clamp, device):
|
| 194 |
+
"""clamp_max on x (upper only) and symmetric clamp on mul, with the right
|
| 195 |
+
zero-gradient masks."""
|
| 196 |
+
torch.set_default_device(device)
|
| 197 |
+
x, m, a, offsets = _make_inputs(4096, 1152, 8, dtype, device, scale=1.0)
|
| 198 |
+
s = _make_scores(4096, device)
|
| 199 |
+
r = _run(grouped_epolynorm_tanh_ref, x, m, a, offsets, 0, s, hidden_clamp)
|
| 200 |
+
c = _run(fused_grouped_epolynorm_tanh, x, m, a, offsets, 0, s,
|
| 201 |
+
hidden_clamp)
|
| 202 |
+
out_r, gx_r, gm_r, ga_r, gs_r = r
|
| 203 |
+
out_c, gx_c, gm_c, ga_c, gs_c = c
|
| 204 |
+
t = _tols(dtype)
|
| 205 |
+
assert_close(out_c, out_r, **t)
|
| 206 |
+
assert_close(gx_c, gx_r, **t)
|
| 207 |
+
assert_close(gm_c, gm_r, **t)
|
| 208 |
+
if hidden_clamp < 10:
|
| 209 |
+
assert (gx_c[x > hidden_clamp] == 0).all()
|
| 210 |
+
assert (gm_c[m.abs() > hidden_clamp] == 0).all()
|
| 211 |
+
assert (gx_c[x < -hidden_clamp] != 0).any() # lower side NOT clamped
|
| 212 |
+
_check_wgrad(ga_c, ga_r, dtype)
|
| 213 |
+
assert_close(gs_c, gs_r, atol=1e-3 * 1152**0.5, rtol=1e-2)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
@pytest.mark.parametrize("dtype", DTYPES)
|
| 217 |
+
@pytest.mark.parametrize("device", CUDA_DEVICES)
|
| 218 |
+
def test_single_expert_wrapper(dtype, device):
|
| 219 |
+
"""fused_epolynorm_tanh (dense FFN: weight [3], any leading shape) ==
|
| 220 |
+
the single-expert eager reference."""
|
| 221 |
+
torch.set_default_device(device)
|
| 222 |
+
x, m, a, _ = _make_inputs(2048, 1152, 1, dtype, device, seed=2)
|
| 223 |
+
x3, m3 = x.reshape(4, 512, 1152), m.reshape(4, 512, 1152)
|
| 224 |
+
a1 = a.reshape(3)
|
| 225 |
+
xr, mr, ar = (t.clone().requires_grad_(True) for t in (x3, m3, a1))
|
| 226 |
+
xc, mc, ac = (t.clone().requires_grad_(True) for t in (x3, m3, a1))
|
| 227 |
+
out_r = epolynorm_tanh_ref(xr, mr, torch.sigmoid(ar), hidden_clamp=1e6)
|
| 228 |
+
out_c = fused_epolynorm_tanh(xc, mc, torch.sigmoid(ac), hidden_clamp=1e6)
|
| 229 |
+
assert out_c.shape == x3.shape
|
| 230 |
+
t = _tols(dtype)
|
| 231 |
+
assert_close(out_c, out_r, **t)
|
| 232 |
+
torch.manual_seed(4)
|
| 233 |
+
go = torch.randn_like(out_r)
|
| 234 |
+
out_r.backward(go)
|
| 235 |
+
out_c.backward(go)
|
| 236 |
+
assert_close(xc.grad, xr.grad, **t)
|
| 237 |
+
assert_close(mc.grad, mr.grad, **t)
|
| 238 |
+
_check_wgrad(ac.grad.reshape(1, 3), ar.grad.reshape(1, 3), dtype)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
@pytest.mark.parametrize("dtype", DTYPES)
|
| 242 |
+
@pytest.mark.parametrize("device", CUDA_DEVICES)
|
| 243 |
+
def test_module_matches_eager_module(dtype, device):
|
| 244 |
+
"""activation.layers.EPolyNormTanh (per-expert weight [E, 3]) == an eager
|
| 245 |
+
per-expert module written from the activation's definition."""
|
| 246 |
+
import math
|
| 247 |
+
|
| 248 |
+
import torch.nn as nn
|
| 249 |
+
from activation.layers import EPolyNormTanh
|
| 250 |
+
|
| 251 |
+
class Eager(nn.Module):
|
| 252 |
+
|
| 253 |
+
def __init__(self, num_experts):
|
| 254 |
+
super().__init__()
|
| 255 |
+
self.weight = nn.Parameter(
|
| 256 |
+
torch.full((num_experts, 3), 1 / 3, dtype=torch.float32))
|
| 257 |
+
|
| 258 |
+
def forward(self,
|
| 259 |
+
x,
|
| 260 |
+
mul,
|
| 261 |
+
offsets,
|
| 262 |
+
expert_offset=0,
|
| 263 |
+
scores=None,
|
| 264 |
+
hidden_clamp=None):
|
| 265 |
+
orig = x.dtype
|
| 266 |
+
n = int(offsets[-1].item())
|
| 267 |
+
idx = torch.bucketize(torch.arange(n, device=x.device),
|
| 268 |
+
offsets,
|
| 269 |
+
right=True) + expert_offset
|
| 270 |
+
w = torch.sigmoid(self.weight.float())[idx]
|
| 271 |
+
x, mul = x[:n], mul[:n]
|
| 272 |
+
if hidden_clamp is not None:
|
| 273 |
+
x = x.clamp(max=hidden_clamp)
|
| 274 |
+
mul = mul.clamp(-hidden_clamp, hidden_clamp)
|
| 275 |
+
g, u = x.float(), mul.float()
|
| 276 |
+
s0, s1, s2 = 1.0, math.sqrt(3.0), math.sqrt(15.0)
|
| 277 |
+
q0 = torch.tanh(g / s0)
|
| 278 |
+
q1 = torch.tanh((g * g) / s1)
|
| 279 |
+
q2 = torch.tanh((g * g * g) / s2)
|
| 280 |
+
poly = 4.0 * torch.tanh(
|
| 281 |
+
(w[:, 0:1] * q0 + w[:, 1:2] * q1 + w[:, 2:3] * q2) / 4.0)
|
| 282 |
+
r = poly * (25.0 * torch.tanh(u / 25.0))
|
| 283 |
+
if scores is not None:
|
| 284 |
+
r = r * scores[:n]
|
| 285 |
+
return r.to(orig)
|
| 286 |
+
|
| 287 |
+
torch.set_default_device(device)
|
| 288 |
+
E, eo = 12, 4
|
| 289 |
+
x, m, _, offsets = _make_inputs(2048,
|
| 290 |
+
1152,
|
| 291 |
+
E,
|
| 292 |
+
dtype,
|
| 293 |
+
device,
|
| 294 |
+
seed=3,
|
| 295 |
+
expert_offset=eo)
|
| 296 |
+
s = _make_scores(2048, device)
|
| 297 |
+
eager, fused = Eager(E + eo), EPolyNormTanh(E + eo)
|
| 298 |
+
with torch.no_grad():
|
| 299 |
+
torch.manual_seed(7)
|
| 300 |
+
eager.weight.add_(torch.randn_like(eager.weight) * 0.2)
|
| 301 |
+
fused.weight.copy_(eager.weight)
|
| 302 |
+
xe, me, se = (t.clone().requires_grad_(True) for t in (x, m, s))
|
| 303 |
+
xf, mf, sf = (t.clone().requires_grad_(True) for t in (x, m, s))
|
| 304 |
+
oe = eager(xe, me, offsets, eo, scores=se, hidden_clamp=1e6)
|
| 305 |
+
of = fused(xf, mf, offsets, eo, scores=sf, hidden_clamp=1e6)
|
| 306 |
+
t = _tols(dtype)
|
| 307 |
+
assert_close(of, oe, **t)
|
| 308 |
+
torch.manual_seed(5)
|
| 309 |
+
go = torch.randn_like(oe)
|
| 310 |
+
oe.backward(go)
|
| 311 |
+
of.backward(go)
|
| 312 |
+
assert_close(xf.grad, xe.grad, **t)
|
| 313 |
+
assert_close(mf.grad, me.grad, **t)
|
| 314 |
+
assert_close(sf.grad, se.grad, atol=1e-3 * 1152**0.5, rtol=1e-2)
|
| 315 |
+
_check_wgrad(fused.weight.grad, eager.weight.grad, dtype, eo)
|
| 316 |
+
# every local expert actually received a gradient
|
| 317 |
+
assert (fused.weight.grad[eo:].abs().sum(dim=1) > 0).all()
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
@pytest.mark.parametrize("dtype", DTYPES)
|
| 321 |
+
@pytest.mark.parametrize("device", CUDA_DEVICES)
|
| 322 |
+
def test_no_nan_inf_large_inputs(dtype, device):
|
| 323 |
+
torch.set_default_device(device)
|
| 324 |
+
x, m, a, offsets = _make_inputs(1024, 1152, 8, dtype, device, scale=50.0)
|
| 325 |
+
out, gx, gm, ga, _ = _run(fused_grouped_epolynorm_tanh, x, m, a, offsets)
|
| 326 |
+
for name, t in (("out", out), ("gx", gx), ("gm", gm), ("ga", ga)):
|
| 327 |
+
assert torch.isfinite(t).all(), name
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
@pytest.mark.parametrize("with_scores", [False, True])
|
| 331 |
+
@pytest.mark.parametrize("device", CUDA_DEVICES)
|
| 332 |
+
def test_opcheck(with_scores, device):
|
| 333 |
+
torch.set_default_device(device)
|
| 334 |
+
x, m, a, offsets = _make_inputs(64, 256, 4, torch.float32, device)
|
| 335 |
+
w = torch.sigmoid(a)
|
| 336 |
+
s = _make_scores(64, device).reshape(-1) if with_scores else None
|
| 337 |
+
g0, g1, s0, s1, s2 = (DEFAULT_GATE_BETA, DEFAULT_UP_BETA, *DEFAULT_SCALES)
|
| 338 |
+
torch.library.opcheck(torch.ops._activation.epolynorm_tanh_forward,
|
| 339 |
+
(x, m, w, offsets, 0, s, g0, g1, s0, s1, s2, -1.0))
|
| 340 |
+
go = torch.randn_like(x)
|
| 341 |
+
# The backward accumulates the per-expert weight gradient with atomicAdd
|
| 342 |
+
# (summation order differs run to run), so the eager-vs-AOT output
|
| 343 |
+
# comparison is skipped for it; schema / fake-tensor / autograd checks run.
|
| 344 |
+
torch.library.opcheck(
|
| 345 |
+
torch.ops._activation.epolynorm_tanh_backward,
|
| 346 |
+
(go, x, m, w, offsets, 0, s, g0, g1, s0, s1, s2, -1.0),
|
| 347 |
+
test_utils=("test_schema", "test_autograd_registration",
|
| 348 |
+
"test_faketensor"))
|
|
@@ -2,6 +2,9 @@ import torch
|
|
| 2 |
|
| 3 |
from . import layers, parallel_style
|
| 4 |
from ._ops import ops
|
|
|
|
|
|
|
|
|
|
| 5 |
from .fused_rope import fused_kv_split_rope_cat, fused_q_rope_inplace
|
| 6 |
from .grouped_poly_norm import fused_mul_grouped_poly_norm
|
| 7 |
from .poly_norm import FusedMulPolyNormFunction, PolyNormFunction
|
|
@@ -49,6 +52,10 @@ __all__ = [
|
|
| 49 |
"poly_norm",
|
| 50 |
"fused_mul_poly_norm",
|
| 51 |
"fused_mul_grouped_poly_norm",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
"rms_norm",
|
| 53 |
"fused_add_rms_norm",
|
| 54 |
"fused_q_rope_inplace",
|
|
|
|
| 2 |
|
| 3 |
from . import layers, parallel_style
|
| 4 |
from ._ops import ops
|
| 5 |
+
from .epolynorm_tanh import (epolynorm_tanh_ref, fused_epolynorm_tanh,
|
| 6 |
+
fused_grouped_epolynorm_tanh,
|
| 7 |
+
grouped_epolynorm_tanh_ref)
|
| 8 |
from .fused_rope import fused_kv_split_rope_cat, fused_q_rope_inplace
|
| 9 |
from .grouped_poly_norm import fused_mul_grouped_poly_norm
|
| 10 |
from .poly_norm import FusedMulPolyNormFunction, PolyNormFunction
|
|
|
|
| 52 |
"poly_norm",
|
| 53 |
"fused_mul_poly_norm",
|
| 54 |
"fused_mul_grouped_poly_norm",
|
| 55 |
+
"fused_epolynorm_tanh",
|
| 56 |
+
"fused_grouped_epolynorm_tanh",
|
| 57 |
+
"grouped_epolynorm_tanh_ref",
|
| 58 |
+
"epolynorm_tanh_ref",
|
| 59 |
"rms_norm",
|
| 60 |
"fused_add_rms_norm",
|
| 61 |
"fused_q_rope_inplace",
|
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Grouped (per-expert) EPolyNormTanh — CUDA kernel with autograd wrapper and a
|
| 2 |
+
PyTorch reference.
|
| 3 |
+
|
| 4 |
+
Tanh-bounded elementwise PolyNorm with SiTU-style caps (the ``epolynorm_tanh``
|
| 5 |
+
MoE/FFN activation of llm-training), with one coefficient triple per expert
|
| 6 |
+
like the grouped PolyNorm:
|
| 7 |
+
|
| 8 |
+
e = expert of the row (rows grouped by expert; offsets = cumsum of
|
| 9 |
+
tokens per local expert; coefficient row = e + expert_offset)
|
| 10 |
+
g = clamp_max(x, C) (C = hidden_clamp, None = off)
|
| 11 |
+
u = clamp(mul, -C, C)
|
| 12 |
+
q_k = tanh(g^(k+1) / s_k) k = 0, 1, 2
|
| 13 |
+
poly = gate_beta * tanh((w_e0 q_0 + w_e1 q_1 + w_e2 q_2) / gate_beta)
|
| 14 |
+
up = up_beta * tanh(u / up_beta)
|
| 15 |
+
out = poly * up * score (score: optional per-row fp32)
|
| 16 |
+
|
| 17 |
+
``weight`` holds the coefficients *after* the sigmoid (fp32 [num_total_experts,
|
| 18 |
+
3]); the caller keeps the raw parameters and lets autograd carry the gradient
|
| 19 |
+
back through ``torch.sigmoid``. The kernel computes in fp32 and returns the
|
| 20 |
+
input dtype, exactly like the eager implementation. Rows at or beyond
|
| 21 |
+
``offsets[-1]`` are padding: zero output and zero gradients.
|
| 22 |
+
|
| 23 |
+
CUDA kernel (activation/epolynorm_tanh.cu):
|
| 24 |
+
- one warp per row, persistent grid, 128-bit vectorized loads (width 8 for
|
| 25 |
+
16-bit types, 4 for fp32), scalar fallback otherwise
|
| 26 |
+
- expert lookup by in-kernel binary search over ``offsets`` (per row range)
|
| 27 |
+
- forward: one pass, no saved intermediates (backward recomputes the tanh's)
|
| 28 |
+
- backward: one pass -> grad_input, grad_mul, grad_weight (fp32
|
| 29 |
+
[num_total_experts, 3], warp-reduced then atomicAdd per expert) and
|
| 30 |
+
grad_scores (per-row warp reduction)
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
import math
|
| 34 |
+
|
| 35 |
+
import torch
|
| 36 |
+
from torch import Tensor
|
| 37 |
+
|
| 38 |
+
_ops = None
|
| 39 |
+
try:
|
| 40 |
+
import _activation # noqa: F401 — local setup.py build
|
| 41 |
+
_ops = torch.ops._activation
|
| 42 |
+
except Exception:
|
| 43 |
+
try:
|
| 44 |
+
from ._ops import ops as _ops
|
| 45 |
+
except Exception:
|
| 46 |
+
pass
|
| 47 |
+
|
| 48 |
+
_has_cuda_ops = _ops is not None and hasattr(_ops, "epolynorm_tanh_forward")
|
| 49 |
+
|
| 50 |
+
DEFAULT_GATE_BETA = 4.0
|
| 51 |
+
DEFAULT_UP_BETA = 25.0
|
| 52 |
+
DEFAULT_SCALES = (1.0, math.sqrt(3.0), math.sqrt(15.0))
|
| 53 |
+
|
| 54 |
+
if _has_cuda_ops:
|
| 55 |
+
try:
|
| 56 |
+
|
| 57 |
+
@torch.library.register_fake("_activation::epolynorm_tanh_forward")
|
| 58 |
+
def _fwd_fake(input, mul, weight, offsets, expert_offset, scores,
|
| 59 |
+
gate_beta, up_beta, s0, s1, s2, hidden_clamp):
|
| 60 |
+
return torch.empty_like(input)
|
| 61 |
+
|
| 62 |
+
@torch.library.register_fake("_activation::epolynorm_tanh_backward")
|
| 63 |
+
def _bwd_fake(grad_output, input, mul, weight, offsets, expert_offset,
|
| 64 |
+
scores, gate_beta, up_beta, s0, s1, s2, hidden_clamp):
|
| 65 |
+
gs = (torch.empty(
|
| 66 |
+
input.shape[0], 1, dtype=torch.float32, device=input.device)
|
| 67 |
+
if scores is not None else torch.empty(
|
| 68 |
+
0, dtype=torch.float32, device=input.device))
|
| 69 |
+
return (torch.empty_like(input), torch.empty_like(mul),
|
| 70 |
+
torch.empty_like(weight), gs)
|
| 71 |
+
except Exception:
|
| 72 |
+
pass # already registered
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# ---------------------------------------------------------------------------
|
| 76 |
+
# PyTorch reference (mirrors llm-training GroupedExpertsEPolyNormTanh)
|
| 77 |
+
# ---------------------------------------------------------------------------
|
| 78 |
+
def grouped_epolynorm_tanh_ref(
|
| 79 |
+
input: Tensor,
|
| 80 |
+
mul: Tensor,
|
| 81 |
+
weight: Tensor,
|
| 82 |
+
offsets: Tensor,
|
| 83 |
+
expert_offset: int = 0,
|
| 84 |
+
scores: Tensor | None = None,
|
| 85 |
+
hidden_clamp: float | None = None,
|
| 86 |
+
gate_beta: float = DEFAULT_GATE_BETA,
|
| 87 |
+
up_beta: float = DEFAULT_UP_BETA,
|
| 88 |
+
scales: tuple[float, float, float] = DEFAULT_SCALES,
|
| 89 |
+
) -> Tensor:
|
| 90 |
+
"""Eager reference. ``weight`` = sigmoid(a) as fp32 [num_total_experts, 3].
|
| 91 |
+
|
| 92 |
+
Rows are mapped to experts with ``torch.bucketize`` over ``offsets``; rows
|
| 93 |
+
at or beyond ``offsets[-1]`` are padding and come back as zeros.
|
| 94 |
+
"""
|
| 95 |
+
orig_dtype = input.dtype
|
| 96 |
+
num_valid = int(offsets[-1].item())
|
| 97 |
+
positions = torch.arange(num_valid, device=input.device)
|
| 98 |
+
expert_idx = torch.bucketize(positions, offsets,
|
| 99 |
+
right=True) + expert_offset
|
| 100 |
+
w = weight.float()[expert_idx] # [num_valid, 3]
|
| 101 |
+
|
| 102 |
+
x = input[:num_valid]
|
| 103 |
+
m = mul[:num_valid]
|
| 104 |
+
if hidden_clamp is not None:
|
| 105 |
+
x = x.clamp(max=hidden_clamp)
|
| 106 |
+
m = m.clamp(-hidden_clamp, hidden_clamp)
|
| 107 |
+
g = x.float()
|
| 108 |
+
u = m.float()
|
| 109 |
+
s0, s1, s2 = scales
|
| 110 |
+
q0 = torch.tanh(g / s0)
|
| 111 |
+
q1 = torch.tanh((g * g) / s1)
|
| 112 |
+
q2 = torch.tanh((g * g * g) / s2)
|
| 113 |
+
poly = gate_beta * torch.tanh(
|
| 114 |
+
(w[:, 0:1] * q0 + w[:, 1:2] * q1 + w[:, 2:3] * q2) / gate_beta)
|
| 115 |
+
up_cap = up_beta * torch.tanh(u / up_beta)
|
| 116 |
+
result = poly * up_cap
|
| 117 |
+
if scores is not None:
|
| 118 |
+
assert scores.dtype == torch.float32, (
|
| 119 |
+
f"scores dtype must be float32, got {scores.dtype}")
|
| 120 |
+
result = result * scores[:num_valid].reshape(-1, 1)
|
| 121 |
+
result = result.to(orig_dtype)
|
| 122 |
+
if input.shape[0] > num_valid:
|
| 123 |
+
pad = torch.zeros(input.shape[0] - num_valid,
|
| 124 |
+
input.shape[-1],
|
| 125 |
+
dtype=orig_dtype,
|
| 126 |
+
device=input.device)
|
| 127 |
+
result = torch.cat([result, pad], dim=0)
|
| 128 |
+
return result
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def epolynorm_tanh_ref(
|
| 132 |
+
input: Tensor,
|
| 133 |
+
mul: Tensor,
|
| 134 |
+
weight: Tensor,
|
| 135 |
+
scores: Tensor | None = None,
|
| 136 |
+
hidden_clamp: float | None = None,
|
| 137 |
+
gate_beta: float = DEFAULT_GATE_BETA,
|
| 138 |
+
up_beta: float = DEFAULT_UP_BETA,
|
| 139 |
+
scales: tuple[float, float, float] = DEFAULT_SCALES,
|
| 140 |
+
) -> Tensor:
|
| 141 |
+
"""Single-expert eager reference (dense FFN). ``weight`` = fp32 [3]."""
|
| 142 |
+
D = input.shape[-1]
|
| 143 |
+
x = input.reshape(-1, D)
|
| 144 |
+
offsets = torch.tensor([x.shape[0]], dtype=torch.int32, device=x.device)
|
| 145 |
+
out = grouped_epolynorm_tanh_ref(x, mul.reshape(-1, D),
|
| 146 |
+
weight.reshape(1, 3), offsets, 0, scores,
|
| 147 |
+
hidden_clamp, gate_beta, up_beta, scales)
|
| 148 |
+
return out.reshape(input.shape)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
# ---------------------------------------------------------------------------
|
| 152 |
+
# CUDA autograd function
|
| 153 |
+
# ---------------------------------------------------------------------------
|
| 154 |
+
if _has_cuda_ops:
|
| 155 |
+
|
| 156 |
+
class _GroupedEPolyNormTanhFn(torch.autograd.Function):
|
| 157 |
+
|
| 158 |
+
@staticmethod
|
| 159 |
+
def forward(input, mul, weight, offsets, expert_offset, scores,
|
| 160 |
+
gate_beta, up_beta, s0, s1, s2, hidden_clamp):
|
| 161 |
+
return _ops.epolynorm_tanh_forward(input, mul, weight, offsets,
|
| 162 |
+
expert_offset, scores,
|
| 163 |
+
gate_beta, up_beta, s0, s1, s2,
|
| 164 |
+
hidden_clamp)
|
| 165 |
+
|
| 166 |
+
@staticmethod
|
| 167 |
+
def setup_context(ctx, inputs, output):
|
| 168 |
+
(input, mul, weight, offsets, expert_offset, scores, gate_beta,
|
| 169 |
+
up_beta, s0, s1, s2, hidden_clamp) = inputs
|
| 170 |
+
ctx.save_for_backward(input, mul, weight, offsets, scores)
|
| 171 |
+
ctx.expert_offset = expert_offset
|
| 172 |
+
ctx.consts = (gate_beta, up_beta, s0, s1, s2, hidden_clamp)
|
| 173 |
+
|
| 174 |
+
@staticmethod
|
| 175 |
+
def backward(ctx, grad_output):
|
| 176 |
+
input, mul, weight, offsets, scores = ctx.saved_tensors
|
| 177 |
+
gi, gm, gw, gs = _ops.epolynorm_tanh_backward(
|
| 178 |
+
grad_output.contiguous(), input, mul, weight, offsets,
|
| 179 |
+
ctx.expert_offset, scores, *ctx.consts)
|
| 180 |
+
if scores is None:
|
| 181 |
+
gs = None
|
| 182 |
+
elif scores.dim() == 1:
|
| 183 |
+
gs = gs.reshape(-1)
|
| 184 |
+
return (gi, gm, gw, None, None, gs, None, None, None, None, None,
|
| 185 |
+
None)
|
| 186 |
+
|
| 187 |
+
def fused_grouped_epolynorm_tanh(
|
| 188 |
+
input: Tensor,
|
| 189 |
+
mul: Tensor,
|
| 190 |
+
weight: Tensor,
|
| 191 |
+
offsets: Tensor,
|
| 192 |
+
expert_offset: int = 0,
|
| 193 |
+
scores: Tensor | None = None,
|
| 194 |
+
hidden_clamp: float | None = None,
|
| 195 |
+
gate_beta: float = DEFAULT_GATE_BETA,
|
| 196 |
+
up_beta: float = DEFAULT_UP_BETA,
|
| 197 |
+
scales: tuple[float, float, float] = DEFAULT_SCALES,
|
| 198 |
+
) -> Tensor:
|
| 199 |
+
"""Fused grouped EPolyNormTanh.
|
| 200 |
+
|
| 201 |
+
Args:
|
| 202 |
+
input: (N, D) gate projection (w1 x), fp32/bf16/fp16; rows grouped
|
| 203 |
+
by expert
|
| 204 |
+
mul: (N, D) up projection (w3 x), same dtype
|
| 205 |
+
weight: (num_total_experts, 3) fp32 = sigmoid(a), same device
|
| 206 |
+
offsets: (num_experts,) int32 cumsum of tokens per local expert;
|
| 207 |
+
rows >= offsets[-1] are padding (zero output / zero grads)
|
| 208 |
+
expert_offset: global index of local expert 0 (EP rank offset)
|
| 209 |
+
scores: optional (N,) or (N, 1) fp32 routing scores
|
| 210 |
+
hidden_clamp: clamp_max(input, C) and clamp(mul, -C, C); None = off
|
| 211 |
+
gate_beta, up_beta, scales: the activation's constants
|
| 212 |
+
Returns:
|
| 213 |
+
(N, D) in the input dtype
|
| 214 |
+
"""
|
| 215 |
+
assert input.dim() == 2 and mul.shape == input.shape, (input.shape,
|
| 216 |
+
mul.shape)
|
| 217 |
+
x = input if input.is_contiguous() else input.contiguous()
|
| 218 |
+
m = mul if mul.is_contiguous() else mul.contiguous()
|
| 219 |
+
if offsets.dtype != torch.int32:
|
| 220 |
+
offsets = offsets.to(torch.int32)
|
| 221 |
+
offsets = offsets.contiguous()
|
| 222 |
+
if weight.dtype != torch.float32:
|
| 223 |
+
weight = weight.float()
|
| 224 |
+
weight = weight.contiguous()
|
| 225 |
+
if scores is not None:
|
| 226 |
+
assert scores.dtype == torch.float32, (
|
| 227 |
+
f"scores dtype must be float32, got {scores.dtype}")
|
| 228 |
+
scores = scores.contiguous()
|
| 229 |
+
clamp_val = -1.0 if hidden_clamp is None else float(hidden_clamp)
|
| 230 |
+
s0, s1, s2 = (float(v) for v in scales)
|
| 231 |
+
return _GroupedEPolyNormTanhFn.apply(x, m, weight, offsets,
|
| 232 |
+
int(expert_offset), scores,
|
| 233 |
+
float(gate_beta), float(up_beta),
|
| 234 |
+
s0, s1, s2, clamp_val)
|
| 235 |
+
|
| 236 |
+
def fused_epolynorm_tanh(
|
| 237 |
+
input: Tensor,
|
| 238 |
+
mul: Tensor,
|
| 239 |
+
weight: Tensor,
|
| 240 |
+
scores: Tensor | None = None,
|
| 241 |
+
hidden_clamp: float | None = None,
|
| 242 |
+
gate_beta: float = DEFAULT_GATE_BETA,
|
| 243 |
+
up_beta: float = DEFAULT_UP_BETA,
|
| 244 |
+
scales: tuple[float, float, float] = DEFAULT_SCALES,
|
| 245 |
+
) -> Tensor:
|
| 246 |
+
"""Single-expert fused EPolyNormTanh (dense FFN): ``weight`` fp32 [3],
|
| 247 |
+
any leading shape."""
|
| 248 |
+
D = input.shape[-1]
|
| 249 |
+
x = input.reshape(-1, D)
|
| 250 |
+
offsets = torch.tensor([x.shape[0]],
|
| 251 |
+
dtype=torch.int32,
|
| 252 |
+
device=x.device)
|
| 253 |
+
out = fused_grouped_epolynorm_tanh(x, mul.reshape(-1, D),
|
| 254 |
+
weight.reshape(1, 3), offsets, 0,
|
| 255 |
+
scores, hidden_clamp, gate_beta,
|
| 256 |
+
up_beta, scales)
|
| 257 |
+
return out.reshape(input.shape)
|
| 258 |
+
|
| 259 |
+
else:
|
| 260 |
+
|
| 261 |
+
def fused_grouped_epolynorm_tanh(*args, **kwargs):
|
| 262 |
+
raise RuntimeError(
|
| 263 |
+
"CUDA ops not available. Build with setup.py or kernel-builder "
|
| 264 |
+
"to use fused_grouped_epolynorm_tanh.")
|
| 265 |
+
|
| 266 |
+
def fused_epolynorm_tanh(*args, **kwargs):
|
| 267 |
+
raise RuntimeError(
|
| 268 |
+
"CUDA ops not available. Build with setup.py or kernel-builder "
|
| 269 |
+
"to use fused_epolynorm_tanh.")
|
|
@@ -93,3 +93,78 @@ class FusedAddRMSNorm(nn.Module):
|
|
| 93 |
Resets parameters based on their initialization used in __init__.
|
| 94 |
"""
|
| 95 |
init.ones_(self.weight)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
Resets parameters based on their initialization used in __init__.
|
| 94 |
"""
|
| 95 |
init.ones_(self.weight)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
class EPolyNormTanh(nn.Module):
|
| 99 |
+
"""Fused grouped (per-expert) tanh-bounded elementwise PolyNorm with
|
| 100 |
+
SiTU-style caps.
|
| 101 |
+
|
| 102 |
+
``out = gate_beta * tanh(sum_k sigmoid(a_ek) tanh(x^(k+1)/s_k) / gate_beta)
|
| 103 |
+
* up_beta * tanh(mul / up_beta) [* scores]``
|
| 104 |
+
|
| 105 |
+
``weight`` is a fp32 [num_experts, 3] parameter (the raw coefficients
|
| 106 |
+
a_ek, passed through a sigmoid), one triple per expert like the grouped
|
| 107 |
+
PolyNorm. Drop-in for llm-training's ``GroupedExpertsEPolyNormTanh``: same
|
| 108 |
+
``forward(x, mul, offsets=None, expert_offset=0, scores=None,
|
| 109 |
+
hidden_clamp=None)`` signature. With ``offsets=None`` (dense FFN) all rows
|
| 110 |
+
use expert 0.
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
def __init__(
|
| 114 |
+
self,
|
| 115 |
+
num_experts: int = 1,
|
| 116 |
+
gate_beta: float = 4.0,
|
| 117 |
+
up_beta: float = 25.0,
|
| 118 |
+
scales=(1.0, 3.0**0.5, 15.0**0.5),
|
| 119 |
+
a_init: float = 1.0 / 3.0,
|
| 120 |
+
):
|
| 121 |
+
super().__init__()
|
| 122 |
+
from .epolynorm_tanh import fused_grouped_epolynorm_tanh
|
| 123 |
+
if gate_beta <= 0.0 or up_beta <= 0.0:
|
| 124 |
+
raise ValueError("gate_beta and up_beta must be positive.")
|
| 125 |
+
self._fn = fused_grouped_epolynorm_tanh
|
| 126 |
+
self.num_experts = int(num_experts)
|
| 127 |
+
self.gate_beta = float(gate_beta)
|
| 128 |
+
self.up_beta = float(up_beta)
|
| 129 |
+
self._scales = tuple(float(s) for s in scales)
|
| 130 |
+
self._a_init = float(a_init)
|
| 131 |
+
self.weight = nn.Parameter(
|
| 132 |
+
torch.full((self.num_experts, 3),
|
| 133 |
+
self._a_init,
|
| 134 |
+
dtype=torch.float32))
|
| 135 |
+
|
| 136 |
+
def reset_parameters(self) -> None:
|
| 137 |
+
with torch.no_grad():
|
| 138 |
+
init.constant_(self.weight, self._a_init)
|
| 139 |
+
|
| 140 |
+
def sigmoid_weight(self) -> torch.Tensor:
|
| 141 |
+
return torch.sigmoid(self.weight.float())
|
| 142 |
+
|
| 143 |
+
def forward(
|
| 144 |
+
self,
|
| 145 |
+
x: torch.Tensor,
|
| 146 |
+
mul: torch.Tensor,
|
| 147 |
+
offsets: torch.Tensor | None = None,
|
| 148 |
+
expert_offset: int = 0,
|
| 149 |
+
scores: torch.Tensor | None = None,
|
| 150 |
+
hidden_clamp: float | None = None,
|
| 151 |
+
) -> torch.Tensor:
|
| 152 |
+
shape = x.shape
|
| 153 |
+
D = shape[-1]
|
| 154 |
+
x = x.reshape(-1, D)
|
| 155 |
+
mul = mul.reshape(-1, D)
|
| 156 |
+
if offsets is None:
|
| 157 |
+
offsets = torch.tensor([x.shape[0]],
|
| 158 |
+
dtype=torch.int32,
|
| 159 |
+
device=x.device)
|
| 160 |
+
out = self._fn(x,
|
| 161 |
+
mul,
|
| 162 |
+
self.sigmoid_weight(),
|
| 163 |
+
offsets,
|
| 164 |
+
expert_offset,
|
| 165 |
+
scores=scores,
|
| 166 |
+
hidden_clamp=hidden_clamp,
|
| 167 |
+
gate_beta=self.gate_beta,
|
| 168 |
+
up_beta=self.up_beta,
|
| 169 |
+
scales=self._scales)
|
| 170 |
+
return out.reshape(shape)
|
|
@@ -83,6 +83,20 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
|
| 83 |
"Tensor, Tensor, Tensor, Tensor)");
|
| 84 |
ops.impl("grouped_poly_norm_backward_scored", torch::kCUDA,
|
| 85 |
&grouped_poly_norm_backward_scored);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
|
| 88 |
REGISTER_EXTENSION(TORCH_EXTENSION_NAME)
|
|
|
|
| 83 |
"Tensor, Tensor, Tensor, Tensor)");
|
| 84 |
ops.impl("grouped_poly_norm_backward_scored", torch::kCUDA,
|
| 85 |
&grouped_poly_norm_backward_scored);
|
| 86 |
+
|
| 87 |
+
// epolynorm_tanh (grouped, per-expert coefficients; scores optional,
|
| 88 |
+
// hidden_clamp < 0 = off)
|
| 89 |
+
ops.def("epolynorm_tanh_forward(Tensor input, Tensor mul, Tensor weight, "
|
| 90 |
+
"Tensor offsets, int expert_offset, Tensor? scores, float gate_beta, "
|
| 91 |
+
"float up_beta, float s0, float s1, float s2, float hidden_clamp) "
|
| 92 |
+
"-> Tensor");
|
| 93 |
+
ops.impl("epolynorm_tanh_forward", torch::kCUDA, &epolynorm_tanh_forward);
|
| 94 |
+
|
| 95 |
+
ops.def("epolynorm_tanh_backward(Tensor grad_output, Tensor input, Tensor "
|
| 96 |
+
"mul, Tensor weight, Tensor offsets, int expert_offset, "
|
| 97 |
+
"Tensor? scores, float gate_beta, float up_beta, float s0, float s1, "
|
| 98 |
+
"float s2, float hidden_clamp) -> (Tensor, Tensor, Tensor, Tensor)");
|
| 99 |
+
ops.impl("epolynorm_tanh_backward", torch::kCUDA, &epolynorm_tanh_backward);
|
| 100 |
}
|
| 101 |
|
| 102 |
REGISTER_EXTENSION(TORCH_EXTENSION_NAME)
|
|
@@ -2,6 +2,9 @@
|
|
| 2 |
|
| 3 |
#include <torch/torch.h>
|
| 4 |
|
|
|
|
|
|
|
|
|
|
| 5 |
void poly_norm(torch::Tensor &out, const torch::Tensor &input,
|
| 6 |
const torch::Tensor &weights, const torch::Tensor &bias,
|
| 7 |
double eps);
|
|
@@ -69,3 +72,27 @@ grouped_poly_norm_backward_scored(
|
|
| 69 |
const torch::Tensor &bias, const torch::Tensor &offsets,
|
| 70 |
const torch::Tensor &inv_rms, const torch::Tensor &scores, double eps,
|
| 71 |
int64_t expert_offset, double hidden_clamp);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
#include <torch/torch.h>
|
| 4 |
|
| 5 |
+
#include <optional>
|
| 6 |
+
#include <tuple>
|
| 7 |
+
|
| 8 |
void poly_norm(torch::Tensor &out, const torch::Tensor &input,
|
| 9 |
const torch::Tensor &weights, const torch::Tensor &bias,
|
| 10 |
double eps);
|
|
|
|
| 72 |
const torch::Tensor &bias, const torch::Tensor &offsets,
|
| 73 |
const torch::Tensor &inv_rms, const torch::Tensor &scores, double eps,
|
| 74 |
int64_t expert_offset, double hidden_clamp);
|
| 75 |
+
|
| 76 |
+
// Grouped EPolyNormTanh: tanh-bounded elementwise PolyNorm with SiTU-style
|
| 77 |
+
// caps, per-expert coefficients. weight = sigmoid(a) fp32 [num_total_experts,
|
| 78 |
+
// 3]; offsets int32 [num_experts] = cumsum of tokens per expert (rows >=
|
| 79 |
+
// offsets[-1] are padding); the coefficient row is e + expert_offset; scores
|
| 80 |
+
// optional fp32 [N]; hidden_clamp < 0 = disabled.
|
| 81 |
+
torch::Tensor
|
| 82 |
+
epolynorm_tanh_forward(const torch::Tensor &input, const torch::Tensor &mul,
|
| 83 |
+
const torch::Tensor &weight,
|
| 84 |
+
const torch::Tensor &offsets, int64_t expert_offset,
|
| 85 |
+
const std::optional<torch::Tensor> &scores,
|
| 86 |
+
double gate_beta, double up_beta, double s0, double s1,
|
| 87 |
+
double s2, double hidden_clamp);
|
| 88 |
+
|
| 89 |
+
// -> (grad_input, grad_mul, grad_weight fp32 [num_total_experts, 3],
|
| 90 |
+
// grad_scores fp32 [N, 1], or an empty [0] tensor when scores is None)
|
| 91 |
+
std::tuple<torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor>
|
| 92 |
+
epolynorm_tanh_backward(const torch::Tensor &grad_output,
|
| 93 |
+
const torch::Tensor &input, const torch::Tensor &mul,
|
| 94 |
+
const torch::Tensor &weight,
|
| 95 |
+
const torch::Tensor &offsets, int64_t expert_offset,
|
| 96 |
+
const std::optional<torch::Tensor> &scores,
|
| 97 |
+
double gate_beta, double up_beta, double s0, double s1,
|
| 98 |
+
double s2, double hidden_clamp);
|