Kernels
TaehyunKim TaehyunKimMotif Claude Fable 5.1 commited on
Commit
133a8b1
·
unverified ·
1 Parent(s): e82fa9b

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 ADDED
@@ -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
+ }
benchmarks/bench_epolynorm_tanh.py ADDED
@@ -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()
build.toml CHANGED
@@ -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",
setup.py CHANGED
@@ -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
 
tests/test_epolynorm_tanh.py ADDED
@@ -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"))
torch-ext/activation/__init__.py CHANGED
@@ -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",
torch-ext/activation/epolynorm_tanh.py ADDED
@@ -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.")
torch-ext/activation/layers.py CHANGED
@@ -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)
torch-ext/torch_binding.cpp CHANGED
@@ -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)
torch-ext/torch_binding.h CHANGED
@@ -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);