"""Fast multi-layer decode for Qwen3-0.6B geometry — CUDA megakernel (SM120). Design ------ Batch-1 decode is pure GEMV: ~1 MAC per 2 bytes of weight, so every stage is memory bound. On this GPU (188 SMs, 128 MB L2, ~1.64 TB/s DRAM read, ~8.7 TB/s L2 read) the 4-layer weight set is 125.8 MB — it *fits in L2*. So the whole decode (all layers, all steps) runs in ONE persistent kernel launch: * 188 blocks x 256 threads (one block per SM), resident for the whole call. * 5 stages per layer (qkv | attn | o+res | gate/up | down+res) separated by a hand-rolled release/acquire grid barrier (~0.6 us) — no cooperative launch, no cudaGraph, no per-stage launch overhead. * Attention is flash-decoding style: blocks split (kv_head x seq); the two sibling q heads that share a kv head live in the same block so K/V is read once for both. K and V for a position are interleaved into one 512 B record, so each block walks the cache as a single sequential stream. * Softmax skips the running max: after RMSNorm+RoPE, |q| and |k| are bounded (<= sqrt(128)*max|q_norm|*max|k_norm|), so a *constant* shift keeps exp() in range. That makes the KV pass single-shot streaming with no rescaling. * Per-head partials are accumulated with fp32 L2 atomics (only the ~24 blocks of one kv head touch a given address) and the softmax divide is deferred to the consumer, so no block ever waits on a serial cross-block gather. * GEMV weights are pre-permuted on the host so a lane's 16 B weight load pairs with a conflict-free stride-32 shared-memory read of the activation. Numerics follow reference.py exactly: fp32 math, bf16 weights, bf16 hidden between layers, bf16 KV cache round trip, fp32 residual inside a layer. """ from __future__ import annotations import math import os import torch import torch.nn as nn HIDDEN = 1024 INTERMEDIATE = 3072 NUM_Q = 16 NUM_KV = 8 HEAD_DIM = 128 NUM_LAYERS = 4 EPS = 1e-6 # ---------------------------------------------------------------------------- # CUDA # ---------------------------------------------------------------------------- _CPP = r""" #include #include #include int64_t ctx_create(std::vector tens, std::vector ints, std::vector fls); void run_mega(int64_t handle, int64_t start_pos, int64_t n_steps, int64_t rnd, int64_t hin, int64_t hout, int64_t nb, int64_t ph0); std::vector scratch_sizes(int64_t nlayer, int64_t smax); int64_t max_blocks(); std::vector set_persist(int64_t ptr, int64_t nbytes, double frac); """ _CUDA = r""" #include #include #include #include #include #include #include #define TPB 256 #define NW (TPB/32) #define HID 1024 #define INTER 3072 #define NQ 16 #define NKV 8 #define HD 128 #define QKVR 4096 #define EPSV 1e-6f #define ASCALE 0.08838834764831845f #define MAXL 8 typedef __nv_bfloat16 bf16; __device__ __forceinline__ float b2f(bf16 x) { return __bfloat162float(x); } struct LayerW { const bf16* wqkv; // [4096][1024] permuted const bf16* wo; // [1024][2048] permuted const bf16* wgu; // [6144][1024] permuted const bf16* wd; // [1024][3072] permuted const float* nrm; // [1024 input_ln][1024 post_ln][128 q_norm][128 k_norm] bf16* kv; // [NKV][max_seq][256] (k[128] then v[128]) float cb; // constant softmax shift }; struct Args { LayerW L[MAXL]; const float* rinv; const bf16* rnd; const bf16* hin; bf16* hout; bf16* xb; float* qkvb; float* alsum; float* aout; float* res2; float* actb; unsigned* ctr; #if MQ_TIME long long* tm; #endif int max_seq, nb, smax, start_pos, n_steps, nlayer; unsigned ph0; }; #if MQ_TIME #define TDECL long long _t0 = clock64(); long long* _tm = A.tm + (size_t)blockIdx.x * 16 #define TMARK(i) { long long _t1 = clock64(); if (tid == 0) _tm[i] += _t1 - _t0; _t0 = _t1; } #else #define TDECL #define TMARK(i) #endif // ---------------------------------------------------------------- reductions __device__ __forceinline__ float wsum(float v) { #pragma unroll for (int o = 16; o > 0; o >>= 1) v += __shfl_xor_sync(0xffffffffu, v, o); return v; } __device__ __forceinline__ float bsum(float v, float* shred, int warp, int lane) { v = wsum(v); __syncthreads(); if (lane == 0) shred[warp] = v; __syncthreads(); float s = 0.f; #pragma unroll for (int i = 0; i < NW; ++i) s += shred[i]; return s; } // ------------------------------------------------------------------- barrier // One counter, one poller thread per block. A dense 188-slot flag array (188 // independent release stores, no atomic serialisation) is *slower* in situ: // every block then polls 1 KB per iteration, and at 188 blocks that spinning // traffic (~1.9 TB/s) starves the weight stream it is supposed to overlap with. // Serialising the arrive in one L2 slice is the cheaper trade. #define FLAGN 32 __device__ __forceinline__ void gbar(unsigned* ctr, unsigned& ph, unsigned nb) { __syncthreads(); ph += nb; if (threadIdx.x == 0) { // release on the arrive makes this block's stage output visible; the // acquire after the spin picks up everyone else's. Folding the release // into the red is ~95ns cheaper than a separate acq_rel fence. asm volatile("red.release.gpu.global.add.u32 [%0], 1;" ::"l"(ctr) : "memory"); unsigned v; do { asm volatile("ld.relaxed.gpu.global.u32 %0, [%1];" : "=r"(v) : "l"(ctr) : "memory"); } while (v < ph); asm volatile("fence.acquire.gpu;" ::: "memory"); } __syncthreads(); } // ------------------------------------------------------------- cached loads // The 126 MB weight pack fits in the 128 MB L2, but the KV stream would evict it // every step, so KV is loaded .cs (evict-first) to keep it out of the weights' // way. The weight load is deliberately *unqualified*: the symmetric // .L2::evict_last is not encodable at this width on SM120, which accepts the L2 // eviction-priority hints only on 32 B loads (.v8.b32 / .v4.b64) and has no // .L2::no_allocate for ld at all. Widening the weight load to 32 B just to carry // the hint costs more registers than the hint is worth (see set_persist, where the // stream-policy equivalent measures neutral). __device__ __forceinline__ uint4 ld_last(const void* p) { uint4 r; asm("ld.global.nc.v4.u32 {%0,%1,%2,%3}, [%4];" : "=r"(r.x), "=r"(r.y), "=r"(r.z), "=r"(r.w) : "l"(p)); return r; } __device__ __forceinline__ uint4 ld_first(const void* p) { uint4 r; asm("ld.global.nc.cs.v4.u32 {%0,%1,%2,%3}, [%4];" : "=r"(r.x), "=r"(r.y), "=r"(r.z), "=r"(r.w) : "l"(p)); return r; } // --------------------------------------------------------------------- gemv // One warp per row segment, NCH chunks of 256 columns. Weight rows are // pre-permuted so lane's uint4 holds columns {c*256 + lane + 32j : j<8}: the // shared-memory activation read is then bank-conflict free and every weight // load is a fully coalesced 512 B per-warp transaction. template __device__ __forceinline__ float wdot(const bf16* __restrict__ w, const float* __restrict__ sv, int lane) { const uint4* wp = (const uint4*)w; uint4 v[NCH]; #pragma unroll for (int c = 0; c < NCH; ++c) v[c] = ld_last(wp + c * 32 + lane); float acc = 0.f; #pragma unroll for (int c = 0; c < NCH; ++c) { const bf16* vb = (const bf16*)&v[c]; const float* s = sv + c * 256 + lane; #pragma unroll for (int j = 0; j < 8; ++j) acc = fmaf(b2f(vb[j]), s[32 * j], acc); } return acc; } // A stage's weight addresses depend only on blockIdx/warp, never on data, so // every load can be issued *before* the barrier that precedes the stage. The // loads then land in registers while the grid syncs: the memory pipe stays busy // through the barrier and the stage itself starts with zero cold-miss latency. // Each warp owns NT tasks of NCH 256-column chunks, task t at w + t*stride. // T0..TMAX so the window can be split: tasks [0,PF) issue before the barrier // (covering its latency), tasks [PF,TMAX) at the top of the stage (covering the // activation-vector dependency). Both land before the first FMA needs them. template __device__ __forceinline__ void pf_issue(uint4* v, const bf16* __restrict__ w, int stride, int nt, int lane) { #pragma unroll for (int t = T0; t < TMAX; ++t) { if (t < nt) { const uint4* wp = (const uint4*)(w + (size_t)t * stride); #pragma unroll for (int c = 0; c < NCH; ++c) v[t * NCH + c] = ld_last(wp + c * 32 + lane); } } // opaque barrier: stops the scheduler from sinking these loads back down to // their use points, which would defeat the whole point of issuing early asm volatile("" ::: "memory"); } // Cross-lane combine. A full 32-lane butterfly is a 5-deep *dependent* shuffle // chain (~150 cyc); run once per task it is fully exposed, and a layer owns // 3+3+9+3 = 18 tasks per warp -- 4.2 us/step, which the `no_shp` ablation // confirms (-3.87 us). Two fixes, both here: // * interleave all TMAX butterflies so the chains pipeline -- 5 steps of TMAX // independent shuffles instead of TMAX chains of 5 (-0.87 us); // * stop the butterfly at P surviving partials and let the epilogue absorb // them. It already sums the row's 4 column groups out of shared memory, so // P=4 just makes that 16 adds instead of 4 and saves 2 of the 5 steps // (-0.96 us more). P=1 (no partials) is for stages whose output goes // straight to global memory. // Guarding the shuffles with t < nt is *slower* (76.9 vs 71.4 us/step): the // branches break the interleave. Dead tasks reduce zeros instead. template __device__ __forceinline__ void red_store(float* acc, int nt, int lane, float* out, int ostride) { #pragma unroll for (int d = 16 / P; d; d >>= 1) #pragma unroll for (int t = 0; t < TMAX; ++t) acc[t] += __shfl_xor_sync(0xffffffffu, acc[t], d); if ((lane & (32 / P - 1)) == 0) { const int m = lane / (32 / P); #pragma unroll for (int t = 0; t < TMAX; ++t) if (t < nt) out[t * ostride + m] = acc[t]; } } // epilogue side of red_store: one row's partials, 4 (or 2) 16 B shared reads __device__ __forceinline__ float psum8(const float* p) { const float4 a = *(const float4*)p, b = *(const float4*)(p + 4); return ((a.x + a.y) + (a.z + a.w)) + ((b.x + b.y) + (b.z + b.w)); } __device__ __forceinline__ float psum16(const float* p) { return psum8(p) + psum8(p + 8); } template __device__ __forceinline__ void pf_dot(const uint4* v, const float* __restrict__ sv, int nt, int lane, float* out, int ostride) { float acc[TMAX]; #pragma unroll for (int t = 0; t < TMAX; ++t) { acc[t] = 0.f; if (t < nt) { #pragma unroll for (int c = 0; c < NCH; ++c) { const bf16* vb = (const bf16*)&v[t * NCH + c]; const float* s = sv + c * 256 + lane; #pragma unroll for (int j = 0; j < 8; ++j) acc[t] = fmaf(b2f(vb[j]), s[32 * j], acc[t]); } } } red_store(acc, nt, lane, out, ostride); } // tasks past the prefetch window (only reachable when nb is far below the SM // count, i.e. never in the graded configuration) -- plain load-and-use template __device__ __forceinline__ void tail_dot(const bf16* __restrict__ w, int stride, int t0, int nt, const float* __restrict__ sv, int lane, float* out, int ostride) { for (int t = t0; t < nt; ++t) { float a[1] = {wdot(w + (size_t)t * stride, sv, lane)}; red_store<1, P>(a, 1, lane, out + (size_t)t * ostride, 1); } } // Prefetch window sizes. TM* = max tasks a warp can own (tight, because every // slot costs registers -- 255 with no spills); PF* = tasks issued ahead of the // preceding barrier. PF is a measured optimum, not a maximum: the pre-barrier // loads keep the memory pipe busy through the sync, but past ~2-3 tasks they // congest the same L2 slices the barrier poll has to reach, and the barrier is // pure round-trip latency. Sweeping PF4 over {0,2,4,6,9} gives 76.2/72.3/73.3 // /74.4/76.0 us/step -- both ends lose. Tasks past TM* fall back to tail_dot, // which only happens if nb drops far below the SM count. #define TM1 3 #define TM3 3 #define TM4 9 #define TM5 3 #define PF1 2 #define PF3 3 #define PF4 2 #define PF5 3 extern "C" __global__ void __launch_bounds__(TPB, 1) mega(const Args A) { __shared__ float shvec[INTER]; __shared__ float shred[NW]; __shared__ float shcos[64], shsin[64]; __shared__ float shq[2 * HD]; __shared__ float shkv[2 * HD]; __shared__ float shacc[2 * NW * HD]; __shared__ float shls[2 * NW]; // GEMV partials: 16 per output row (4 column groups x 4 surviving lanes of // the butterfly, see red_store). Sized for the smallest legal nb (32, i.e. // 96 rows of the 3072-wide stage); at 1 block/SM the extra 4 KB is free. __shared__ float shp[16 * ((INTER + 31) / 32)]; const int tid = threadIdx.x, lane = tid & 31, warp = tid >> 5; const int b = blockIdx.x, nb = A.nb; // Attention split: block -> (kv head, split index). This deal is uneven -- // 188 blocks over 8 heads gives heads 0-3 24 blocks and heads 4-7 23, so a // block on a 23-block head carries ceil(P/23) positions against a grid // average of P/23.5. Two exactly-balanced replacements were built and both // lost, so the 2.1% is deliberate: // * cut the flat (kv_head x position) space into nb equal pieces, letting // the ~7 boundary blocks do two heads: +2.7/+1.1/-0.1/-5.8 us/step at ctx // 2048/8192/32768/131072, a 0.85% geomean loss. // * 188 = 4 x 47, every block two heads over a 1/47 slice: worse again // (+3.2/+2.3/+3.2), because each output address then takes 47 atomic adds // instead of 24. // The reason there is nothing to win: with the balanced split a-kv still // measures max/avg 1.04x, i.e. that spread is memory-system variance, not work // skew. Past ctx ~8192 the grid is DRAM-bound, so when the short blocks // retire the survivors simply get their bandwidth and the step still ends when // the bytes have landed. Balance buys nothing and the extra indexing is not // free. int h_kv, sidx, nbh; { int base8 = nb >> 3, rem8 = nb & 7, t1 = rem8 * (base8 + 1); if (b < t1) { h_kv = b / (base8 + 1); sidx = b % (base8 + 1); nbh = base8 + 1; } else { int bb = b - t1; h_kv = rem8 + bb / base8; sidx = bb % base8; nbh = base8; } } const int nl = A.nlayer; unsigned ph = A.ph0; TDECL; // ---- GEMV row split + per-warp task descriptors. Identical for every // layer and step, so hoist out of both loops. A "task" is NCH chunks of // 256 columns; warp w owns tasks w, w+NW, w+2NW, ... at a fixed stride. const int q_lo = (int)(((long)QKVR * b) / nb), q_hi = (int)(((long)QKVR * (b + 1)) / nb); const int h_lo = (int)(((long)HID * b) / nb), h_hi = (int)(((long)HID * (b + 1)) / nb); const int i_lo = (int)(((long)INTER * b) / nb), i_hi = (int)(((long)INTER * (b + 1)) / nb); const int nr_h = h_hi - h_lo, nr_i = i_hi - i_lo; const size_t o1 = (size_t)(q_lo + warp) * HID; // qkv row const size_t o3 = (size_t)(h_lo + (warp >> 2)) * 2048 + (warp & 3) * 512; const size_t o4 = (size_t)((((warp >> 1) & 1)) * INTER + i_lo + (warp >> 2)) * HID + (warp & 1) * 512; const size_t o5 = (size_t)(h_lo + (warp >> 2)) * INTER + (warp & 3) * 768; const int n1 = max(0, (q_hi - q_lo - warp + NW - 1) / NW); const int n3 = max(0, (nr_h * 4 - warp + NW - 1) / NW); const int n4 = max(0, (nr_i * 4 - warp + NW - 1) / NW); const int n5 = n3; const int s1 = NW * HID, s3 = (NW / 4) * 2048, s4 = (NW / 4) * HID, s5 = (NW / 4) * INTER; float* const qo0 = A.qkvb + q_lo + warp; // slot(row, colgroup, partial) = 16*row + 4*colgroup + partial, and every // stage that lands here has row = (warp>>2) + (NW/4)*task, colgroup = warp&3 float* const shpw = shp + 16 * (warp >> 2) + 4 * (warp & 3); const int sps = 16 * (NW / 4); // task stride uint4 v1[TM1 * 4], v3[TM3 * 2], v4[TM4 * 2], v5[TM5 * 3]; pf_issue<4, 0, TM1>(v1, A.L[0].wqkv + o1, s1, n1, lane); for (int step = 0; step < A.n_steps; ++step) { const int pos = A.start_pos + step; if (tid < 64) { float s, c; sincosf((float)pos * A.rinv[tid], &s, &c); shcos[tid] = c; shsin[tid] = s; } for (int l = 0; l < nl; ++l) { const LayerW LW = A.L[l]; // ---------------- stage 1: rmsnorm -> qkv ---------------- { pf_issue<4, PF1, TM1>(v1, LW.wqkv + o1, s1, n1, lane); // clear the accumulators stage 2 will atomically fill: one barrier // ahead of the first add, and 3+ barriers behind the last reader // (stage 3 of this layer in the previous step) if (b < 17 && tid < HD) { if (b < 16) A.aout[(size_t)l * 2048 + b * HD + tid] = 0.f; else if (tid < NQ) A.alsum[(size_t)l * NQ + tid] = 0.f; } // 4 *contiguous* elements per thread, so every activation read // below is a single 8/16 B vector load instead of 4 strided // scalars -- one L2 round trip on the post-barrier critical path float xr[HID / TPB]; const int i0 = tid * (HID / TPB); if (l == 0) { const bf16* hs = (step == 0) ? A.hin : (A.xb + (size_t)nl * HID); const bf16* rr = A.rnd + (size_t)step * HID; const uint2 rq = *(const uint2*)(rr + i0), hq = *(const uint2*)(hs + i0); const bf16* rb = (const bf16*)&rq; const bf16* hb = (const bf16*)&hq; #pragma unroll for (int i = 0; i < HID / TPB; ++i) { bf16 xq = __float2bfloat16(0.5f * b2f(rb[i]) + 0.5f * b2f(hb[i])); if (b == 0) A.xb[i0 + i] = xq; xr[i] = b2f(xq); } } else { const uint2 hq = *(const uint2*)(A.xb + (size_t)l * HID + i0); const bf16* hb = (const bf16*)&hq; #pragma unroll for (int i = 0; i < HID / TPB; ++i) xr[i] = b2f(hb[i]); } float ss = 0.f; #pragma unroll for (int i = 0; i < HID / TPB; ++i) ss += xr[i] * xr[i]; ss = bsum(ss, shred, warp, lane); float sc = rsqrtf(ss * (1.0f / HID) + EPSV); const float4 gv = *(const float4*)(LW.nrm + i0); float4 nv; nv.x = xr[0] * sc * gv.x; nv.y = xr[1] * sc * gv.y; nv.z = xr[2] * sc * gv.z; nv.w = xr[3] * sc * gv.w; *(float4*)(shvec + i0) = nv; __syncthreads(); float* qo = qo0 + (size_t)l * QKVR; pf_dot<4, TM1, 1>(v1, shvec, n1, lane, qo, NW); tail_dot<4, 1>(LW.wqkv + o1, s1, TM1, n1, shvec, lane, qo, NW); // stage 3's weights are wanted two barriers from now: issue the // whole window here so it lands during attention. pf_issue<2, 0, TM3>(v3, LW.wo + o3, s3, n3, lane); } TMARK(0); gbar(A.ctr, ph, nb); TMARK(8); // ---------------- stage 2: attention ---------------- { const int P = pos + 1; const int p0 = (int)(((long)P * sidx) / nbh); const int p1 = (int)(((long)P * (sidx + 1)) / nbh); const bool haslast = (p1 == P); const int pend = haslast ? p1 - 1 : p1; const float* qkvl = A.qkvb + (size_t)l * QKVR; __syncthreads(); if (warp < 4) { const float* src; const float* gw; if (warp < 2) { src = qkvl + (2 * h_kv + warp) * HD; gw = LW.nrm + 2 * HID; } else if (warp == 2) { src = qkvl + 2048 + h_kv * HD; gw = LW.nrm + 2 * HID + HD; } else { src = qkvl + 3072 + h_kv * HD; gw = nullptr; } float a0 = src[lane], a1 = src[lane + 32], a2 = src[lane + 64], a3 = src[lane + 96]; if (warp < 3) { float ss = wsum(a0 * a0 + a1 * a1 + a2 * a2 + a3 * a3); float sc = rsqrtf(ss * (1.0f / HD) + EPSV); a0 *= sc * gw[lane]; a1 *= sc * gw[lane + 32]; a2 *= sc * gw[lane + 64]; a3 *= sc * gw[lane + 96]; float c0 = shcos[lane], s0 = shsin[lane]; float c1 = shcos[lane + 32], s1 = shsin[lane + 32]; float o0 = a0 * c0 - a2 * s0, o2 = a0 * s0 + a2 * c0; float o1 = a1 * c1 - a3 * s1, o3 = a1 * s1 + a3 * c1; a0 = o0; a1 = o1; a2 = o2; a3 = o3; } if (warp < 2) { float* d = shq + warp * HD; d[lane] = a0; d[lane + 32] = a1; d[lane + 64] = a2; d[lane + 96] = a3; } else { int off = (warp == 2) ? 0 : HD; bf16 q0 = __float2bfloat16(a0), q1 = __float2bfloat16(a1); bf16 q2 = __float2bfloat16(a2), q3 = __float2bfloat16(a3); float* d = shkv + off; d[lane] = b2f(q0); d[lane + 32] = b2f(q1); d[lane + 64] = b2f(q2); d[lane + 96] = b2f(q3); if (haslast) { bf16* kvp = LW.kv + ((size_t)h_kv * A.max_seq + pos) * 256 + off; kvp[lane] = q0; kvp[lane + 32] = q1; kvp[lane + 64] = q2; kvp[lane + 96] = q3; } } } __syncthreads(); TMARK(5); const int ll = lane & 15, hf = lane >> 4; float qa[8], qb[8]; #pragma unroll for (int j = 0; j < 8; ++j) { qa[j] = shq[ll * 8 + j]; qb[j] = shq[HD + ll * 8 + j]; } float acc0[8], acc1[8]; #pragma unroll for (int j = 0; j < 8; ++j) { acc0[j] = 0.f; acc1[j] = 0.f; } float ls0 = 0.f, ls1 = 0.f; const float cb = LW.cb; const bf16* kvh = LW.kv + (size_t)h_kv * A.max_seq * 256; for (int base = p0 + warp * 2; base < pend; base += 2 * NW) { const int p = base + hf; const bool ok = (p < pend); float d0 = 0.f, d1 = 0.f; uint4 vv; if (ok) { const bf16* kp = kvh + (size_t)p * 256 + ll * 8; uint4 kk = ld_first(kp); vv = ld_first(kp + HD); const bf16* kb = (const bf16*)&kk; #pragma unroll for (int j = 0; j < 8; ++j) { float kf = b2f(kb[j]); d0 = fmaf(qa[j], kf, d0); d1 = fmaf(qb[j], kf, d1); } } #pragma unroll for (int o = 1; o < 16; o <<= 1) { d0 += __shfl_xor_sync(0xffffffffu, d0, o); d1 += __shfl_xor_sync(0xffffffffu, d1, o); } if (ok) { float e0 = __expf(d0 * ASCALE - cb), e1 = __expf(d1 * ASCALE - cb); ls0 += e0; ls1 += e1; const bf16* vb = (const bf16*)&vv; #pragma unroll for (int j = 0; j < 8; ++j) { float vf = b2f(vb[j]); acc0[j] = fmaf(e0, vf, acc0[j]); acc1[j] = fmaf(e1, vf, acc1[j]); } } } if (haslast && warp == 0 && hf == 0) { float d0 = 0.f, d1 = 0.f; #pragma unroll for (int j = 0; j < 8; ++j) { float kf = shkv[ll * 8 + j]; d0 = fmaf(qa[j], kf, d0); d1 = fmaf(qb[j], kf, d1); } #pragma unroll for (int o = 1; o < 16; o <<= 1) { d0 += __shfl_xor_sync(0x0000ffffu, d0, o); d1 += __shfl_xor_sync(0x0000ffffu, d1, o); } float e0 = __expf(d0 * ASCALE - cb), e1 = __expf(d1 * ASCALE - cb); ls0 += e0; ls1 += e1; #pragma unroll for (int j = 0; j < 8; ++j) { float vf = shkv[HD + ll * 8 + j]; acc0[j] = fmaf(e0, vf, acc0[j]); acc1[j] = fmaf(e1, vf, acc1[j]); } } // fold the two half-warps together #pragma unroll for (int j = 0; j < 8; ++j) { acc0[j] += __shfl_xor_sync(0xffffffffu, acc0[j], 16); acc1[j] += __shfl_xor_sync(0xffffffffu, acc1[j], 16); } ls0 += __shfl_xor_sync(0xffffffffu, ls0, 16); ls1 += __shfl_xor_sync(0xffffffffu, ls1, 16); TMARK(6); __syncthreads(); if (lane < 16) { float* d0 = shacc + warp * HD; float* d1 = shacc + NW * HD + warp * HD; #pragma unroll for (int j = 0; j < 8; ++j) { d0[lane * 8 + j] = acc0[j]; d1[lane * 8 + j] = acc1[j]; } if (lane == 0) { shls[warp] = ls0; shls[NW + warp] = ls1; } } __syncthreads(); // Publish straight into the shared accumulator with L2 atomics. // The obvious alternative -- park the partials, elect the last // arriver by atomic counter, have it gather nbh of them -- puts a // serial tail on the critical path of *every* block in the grid, // because the barrier below cannot clear until that one block has // done ~24 dependent strided loads: it cost 2.0 us of handshake // plus ~11 us of b2 (measured at ctx 8192) against 0.1 us here. // Only the nbh (~24) blocks of one kv head touch a given address, // so each of the 2048 floats sees ~24 adds, spread over 64 lines. if (tid < 2 * HD) { const int qh = tid >> 7, d = tid & 127; float s = 0.f; #pragma unroll for (int w = 0; w < NW; ++w) s += shacc[qh * NW * HD + w * HD + d]; float* ap = A.aout + (size_t)l * 2048 + (2 * h_kv + qh) * HD + d; asm volatile("red.relaxed.gpu.global.add.f32 [%0], %1;" ::"l"(ap), "f"(s) : "memory"); if (d == 0) { float t = 0.f; #pragma unroll for (int w = 0; w < NW; ++w) t += shls[qh * NW + w]; float* lp = A.alsum + (size_t)l * NQ + (2 * h_kv + qh); asm volatile("red.relaxed.gpu.global.add.f32 [%0], %1;" ::"l"(lp), "f"(t) : "memory"); } } TMARK(7); } TMARK(1); gbar(A.ctr, ph, nb); TMARK(9); // ---------------- stage 3: o proj + residual ---------------- { const float* ao = A.aout + (size_t)l * 2048; // softmax denominators, one per q head: the division the attention // stage used to do is deferred to here so its numerator can be a // plain atomic accumulation. 16 reciprocals per block, not 2048. if (tid < NQ) shls[tid] = 1.0f / A.alsum[(size_t)l * NQ + tid]; __syncthreads(); { // one float4 per thread per 4*TPB chunk: each warp's request is // 512 B contiguous (4 sectors), so 2 loads instead of 8 with no // loss of coalescing. A float4 never straddles a head (HD=128). const int j0 = tid * 4; #pragma unroll for (int c = 0; c < 2048 / (4 * TPB); ++c) { const int i = c * 4 * TPB + j0; const float r = shls[i >> 7]; float4 t = *(const float4*)(ao + i); t.x *= r; t.y *= r; t.z *= r; t.w *= r; *(float4*)(shvec + i) = t; } } __syncthreads(); const float* sv3 = shvec + (warp & 3) * 512; pf_dot<2, TM3, 4>(v3, sv3, n3, lane, shpw, sps); tail_dot<2, 4>(LW.wo + o3, s3, TM3, n3, sv3, lane, shpw, sps); pf_issue<2, 0, PF4>(v4, LW.wgu + o4, s4, n4, lane); __syncthreads(); const bf16* xs = A.xb + (size_t)l * HID; float* r2 = A.res2 + (size_t)l * HID; for (int rl = tid; rl < nr_h; rl += TPB) r2[h_lo + rl] = b2f(xs[h_lo + rl]) + psum16(shp + rl * 16); } TMARK(2); gbar(A.ctr, ph, nb); TMARK(10); // ---------------- stage 4: post rmsnorm + gate/up ---------------- { // the weight stream stays ahead of the activation read: hoisting the // res2 / norm-gain loads above pf_issue (they are on the critical // path, the weights are not wanted until after the block reduction) // costs 1.4 us/step -- 16 B of activation is not worth delaying // 224 B of weights, which have much further to travel. pf_issue<2, PF4, TM4>(v4, LW.wgu + o4, s4, n4, lane); const float* r2 = A.res2 + (size_t)l * HID; const int i0 = tid * (HID / TPB); const float4 rv = *(const float4*)(r2 + i0); float xr[HID / TPB]; xr[0] = rv.x; xr[1] = rv.y; xr[2] = rv.z; xr[3] = rv.w; float ss = 0.f; #pragma unroll for (int i = 0; i < HID / TPB; ++i) ss += xr[i] * xr[i]; ss = bsum(ss, shred, warp, lane); float sc = rsqrtf(ss * (1.0f / HID) + EPSV); const float4 gv = *(const float4*)(LW.nrm + HID + i0); float4 nv; nv.x = xr[0] * sc * gv.x; nv.y = xr[1] * sc * gv.y; nv.z = xr[2] * sc * gv.z; nv.w = xr[3] * sc * gv.w; *(float4*)(shvec + i0) = nv; __syncthreads(); const float* sv4 = shvec + (warp & 1) * 512; pf_dot<2, TM4, 4>(v4, sv4, n4, lane, shpw, sps); tail_dot<2, 4>(LW.wgu + o4, s4, TM4, n4, sv4, lane, shpw, sps); pf_issue<3, 0, TM5>(v5, LW.wd + o5, s5, n5, lane); __syncthreads(); float* ac = A.actb + (size_t)l * INTER; for (int rl = tid; rl < nr_i; rl += TPB) { // colgroup = 2*(gate|up) + column half, so 0..7 is the gate // row and 8..15 the up row float gv = psum8(shp + rl * 16), uv = psum8(shp + rl * 16 + 8); ac[i_lo + rl] = (gv / (1.f + __expf(-gv))) * uv; } } TMARK(3); gbar(A.ctr, ph, nb); TMARK(11); // ---------------- stage 5: down + residual ---------------- { const float* ac = A.actb + (size_t)l * INTER; __syncthreads(); { // 3 chunk-contiguous float4 loads instead of 12 scalars const int j0 = tid * 4; float4 a[INTER / (4 * TPB)]; #pragma unroll for (int c = 0; c < INTER / (4 * TPB); ++c) a[c] = *(const float4*)(ac + c * 4 * TPB + j0); #pragma unroll for (int c = 0; c < INTER / (4 * TPB); ++c) *(float4*)(shvec + c * 4 * TPB + j0) = a[c]; } __syncthreads(); const float* sv5 = shvec + (warp & 3) * 768; pf_dot<3, TM5, 4>(v5, sv5, n5, lane, shpw, sps); tail_dot<3, 4>(LW.wd + o5, s5, TM5, n5, sv5, lane, shpw, sps); // next layer's qkv (wrapping to layer 0 for the next step) pf_issue<4, 0, PF1>(v1, A.L[l + 1 == nl ? 0 : l + 1].wqkv + o1, s1, n1, lane); __syncthreads(); const float* r2 = A.res2 + (size_t)l * HID; bf16* xo = A.xb + (size_t)(l + 1) * HID; for (int rl = tid; rl < nr_h; rl += TPB) xo[h_lo + rl] = __float2bfloat16(r2[h_lo + rl] + psum16(shp + rl * 16)); } TMARK(4); gbar(A.ctr, ph, nb); TMARK(12); } } if (b == 0) { const bf16* hs = A.xb + (size_t)nl * HID; for (int i = tid; i < HID; i += TPB) A.hout[i] = hs[i]; } } // ------------------------------------------------------------------- host static const size_t W_QKV = (size_t)QKVR * HID; static const size_t W_O = (size_t)HID * 2048; static const size_t W_GU = (size_t)6144 * HID; static const size_t W_D = (size_t)HID * INTER; static const size_t W_LAYER = W_QKV + W_O + W_GU + W_D; std::vector scratch_sizes(int64_t nlayer, int64_t smax) { int64_t nf = nlayer * (QKVR + NQ + 2048 + HID + INTER); int64_t nb16 = (nlayer + 1) * HID; int64_t nc = FLAGN; // one grid-barrier counter; the attention combine is // atomic now, so it needs no per-head handshake slot return {nf, nb16, nc}; } int64_t ctx_create(std::vector t, std::vector ii, std::vector ff) { Args* a = new Args(); const int nl = (int)ii[0]; a->nlayer = nl; a->max_seq = (int)ii[1]; a->smax = (int)ii[2]; const bf16* wp = (const bf16*)t[0].data_ptr(); const float* np = (const float*)t[1].data_ptr(); for (int l = 0; l < nl; ++l) { size_t o = (size_t)l * W_LAYER; a->L[l].wqkv = wp + o; a->L[l].wo = wp + o + W_QKV; a->L[l].wgu = wp + o + W_QKV + W_O; a->L[l].wd = wp + o + W_QKV + W_O + W_GU; a->L[l].nrm = np + (size_t)l * (2 * HID + 2 * HD); a->L[l].kv = (bf16*)t[4 + l].data_ptr(); a->L[l].cb = (float)ff[l]; } a->rinv = (const float*)t[2].data_ptr(); float* sf = (float*)t[3].data_ptr(); size_t off = 0; a->qkvb = sf; off += (size_t)nl * QKVR; a->alsum = sf + off; off += (size_t)nl * NQ; a->aout = sf + off; off += (size_t)nl * 2048; a->res2 = sf + off; off += (size_t)nl * HID; a->actb = sf + off; a->xb = (bf16*)t[4 + nl].data_ptr(); a->ctr = (unsigned*)t[5 + nl].data_ptr(); return (int64_t)a; } // Pin as much of the weight pack in L2 as the set-aside allows. The pack is // 125.8 MB and re-read in full every single decode step, so every byte that // stays resident is a byte of DRAM traffic saved per step; the KV stream is // loaded with .cs so it cannot evict what we pinned. // // Two details the standalone probe (dev/l2s.cu, the ctx-2048 byte mix) says // matter, both of which the obvious spelling gets wrong: // * the window must be an 80 MB *subset* of the pack at hitRatio 1.0, not the // whole pack at hitRatio 80/126. The ratio is a per-line lottery and the // losers stay evictable: 44.0 vs 46.8 us. // * missProp must be Normal. Streaming marks the un-pinned remainder // evict-first, and it is still re-read every step: 49.4 vs 44.7 us. // SM120 also refuses .L2::evict_last/evict_first on anything narrower than a // 32 B load (.v8.b32/.v4.b64) and has no .L2::no_allocate for ld at all, so the // stream policy window is the only lever available at this load width. // // And yet: correctly spelled, it does *nothing* for this kernel. Sweeping the // set-aside in situ is flat at every graded context (frac 0 vs 1, interleaved: // 88.63/88.70 at ctx 2048, 155.30/155.37 at 8192, 417.64/417.75 at 32768), and // so is the same window applied inside the dev harness over its own cudaMalloc, // which rules out the plumbing (off/80/64/40 MB: 86.0/86.7/86.6/86.5 at 2048, // 414.3/410.6/413.4/413.8 at 32768). The probe only gains because its working // set is 1.25x L2, where replacement policy decides everything; here ctx 2048 and // 8192 are barrier-bound rather than DRAM-bound, and by ctx 32768 the 80 MB that // can be pinned is only ~12% of the step's DRAM traffic. So this is left off by // default -- kept, documented, and not worth a global device limit. static void* g_pbase = nullptr; static size_t g_pbytes = 0; static float g_phit = 0.f; static cudaStream_t g_pstream = nullptr; static void apply_policy(cudaStream_t s) { if (g_pbase == nullptr) return; if (s == g_pstream) return; g_pstream = s; cudaStreamAttrValue v; memset(&v, 0, sizeof(v)); v.accessPolicyWindow.base_ptr = g_pbase; v.accessPolicyWindow.num_bytes = g_pbytes; v.accessPolicyWindow.hitRatio = g_phit; v.accessPolicyWindow.hitProp = cudaAccessPropertyPersisting; v.accessPolicyWindow.missProp = cudaAccessPropertyNormal; cudaStreamSetAttribute(s, cudaStreamAttributeAccessPolicyWindow, &v); } std::vector set_persist(int64_t ptr, int64_t nbytes, double frac) { cudaDeviceProp prop; cudaGetDeviceProperties(&prop, 0); if (frac <= 0.0) { g_pbase = nullptr; g_pstream = nullptr; cudaDeviceSetLimit(cudaLimitPersistingL2CacheSize, 0); return {0, 0}; } const size_t amax = (size_t)prop.persistingL2CacheMaxSize; size_t aside = (size_t)(prop.persistingL2CacheMaxSize * frac); if (aside > amax) aside = amax; cudaDeviceSetLimit(cudaLimitPersistingL2CacheSize, aside); // window = the front of the pack, exactly as large as the set-aside size_t win = (size_t)nbytes < aside ? (size_t)nbytes : aside; if (win > (size_t)prop.accessPolicyMaxWindowSize) win = prop.accessPolicyMaxWindowSize; g_pbase = (void*)ptr; g_pbytes = win; g_phit = 1.0f; g_pstream = nullptr; apply_policy(at::cuda::getCurrentCUDAStream()); return {(int64_t)win, (int64_t)aside}; } void run_mega(int64_t handle, int64_t start_pos, int64_t n_steps, int64_t rnd, int64_t hin, int64_t hout, int64_t nb, int64_t ph0) { Args* a = (Args*)handle; a->start_pos = (int)start_pos; a->n_steps = (int)n_steps; a->rnd = (const bf16*)rnd; a->hin = (const bf16*)hin; a->hout = (bf16*)hout; a->nb = (int)nb; a->ph0 = (unsigned)ph0; cudaStream_t s = at::cuda::getCurrentCUDAStream(); apply_policy(s); mega<<<(int)nb, TPB, 0, s>>>(*a); } int64_t max_blocks() { int n = 0; cudaOccupancyMaxActiveBlocksPerMultiprocessor(&n, mega, TPB, 0); return (int64_t)n; } """ _ext = None def _get_ext(): global _ext if _ext is None: cap = torch.cuda.get_device_capability(0) os.environ["TORCH_CUDA_ARCH_LIST"] = "%d.%d" % cap from torch.utils.cpp_extension import load_inline _ext = load_inline( name="megaqwen_decode_v1", cpp_sources=[_CPP], cuda_sources=[_CUDA], functions=[ "ctx_create", "run_mega", "scratch_sizes", "max_blocks", "set_persist", ], extra_cflags=["-O3"], extra_cuda_cflags=["-O3", "-lineinfo"], verbose=False, ) return _ext # ---------------------------------------------------------------------------- # Model # ---------------------------------------------------------------------------- class _Block(nn.Module): def __init__(self): super().__init__() H, I, D = HIDDEN, INTERMEDIATE, HEAD_DIM bf = torch.bfloat16 self.input_ln = nn.Parameter(torch.ones(H, dtype=bf)) self.q_proj = nn.Parameter(torch.zeros(NUM_Q * D, H, dtype=bf)) self.k_proj = nn.Parameter(torch.zeros(NUM_KV * D, H, dtype=bf)) self.v_proj = nn.Parameter(torch.zeros(NUM_KV * D, H, dtype=bf)) self.q_norm = nn.Parameter(torch.ones(D, dtype=bf)) self.k_norm = nn.Parameter(torch.ones(D, dtype=bf)) self.o_proj = nn.Parameter(torch.zeros(H, NUM_Q * D, dtype=bf)) self.post_ln = nn.Parameter(torch.ones(H, dtype=bf)) self.gate_proj = nn.Parameter(torch.zeros(I, H, dtype=bf)) self.up_proj = nn.Parameter(torch.zeros(I, H, dtype=bf)) self.down_proj = nn.Parameter(torch.zeros(H, I, dtype=bf)) def _perm(w): """Permute columns inside each 256-wide chunk for conflict-free smem reads.""" m, k = w.shape assert k % 256 == 0 return w.reshape(m, k // 256, 8, 32).transpose(-1, -2).contiguous().reshape(m, k) class Model(nn.Module): def __init__(self, num_layers: int = NUM_LAYERS, max_seq: int = 131072): super().__init__() self.num_layers = num_layers self.max_seq = max_seq self.blocks = nn.ModuleList([_Block() for _ in range(num_layers)]) self._ctx = None self._store = None # any weight change invalidates the packed copy def load_state_dict(self, *a, **kw): self._ctx = None return super().load_state_dict(*a, **kw) def _apply(self, *a, **kw): self._ctx = None return super()._apply(*a, **kw) def _ensure(model: Model): if model._ctx is not None: return model._store ext = _get_ext() dev = next(model.parameters()).device assert dev.type == "cuda", "solution requires CUDA weights" nl = model.num_layers props = torch.cuda.get_device_properties(dev) nb = int(os.environ.get("MQ_NB", props.multi_processor_count)) assert ext.max_blocks() >= 1, "megakernel not resident" assert 32 <= nb <= 256 smax = (nb + NUM_KV - 1) // NUM_KV st = model._store or {} # ---- packed weights: one contiguous 126 MB buffer (L2 resident) ---- wparts, nparts, cbs = [], [], [] for blk in model.blocks: wqkv = torch.cat([blk.q_proj.detach(), blk.k_proj.detach(), blk.v_proj.detach()], 0) wgu = torch.cat([blk.gate_proj.detach(), blk.up_proj.detach()], 0) for w in (wqkv, blk.o_proj.detach(), wgu, blk.down_proj.detach()): wparts.append(_perm(w).reshape(-1)) nparts.append( torch.cat( [ blk.input_ln.detach().float(), blk.post_ln.detach().float(), blk.q_norm.detach().float(), blk.k_norm.detach().float(), ] ) ) qm = blk.q_norm.detach().float().abs().max().item() km = blk.k_norm.detach().float().abs().max().item() cbs.append(min(math.sqrt(HEAD_DIM) * qm * km, 40.0)) st["w"] = torch.cat(wparts) st["n"] = torch.cat(nparts) del wparts, nparts half = HEAD_DIM // 2 st["rinv"] = 1.0 / ( 10000 ** (torch.arange(0, half, device=dev, dtype=torch.float32) / half) ) nf, nb16, nc = [int(x) for x in ext.scratch_sizes(nl, smax)] st["sf"] = torch.zeros(nf, dtype=torch.float32, device=dev) st["xb"] = torch.zeros(nb16, dtype=torch.bfloat16, device=dev) st["ctr"] = torch.zeros(nc, dtype=torch.int32, device=dev) st["ph"] = 0 if "kv" not in st: st["kv"] = [ torch.zeros(NUM_KV, model.max_seq, 2 * HEAD_DIM, dtype=torch.bfloat16, device=dev) for _ in range(nl) ] if "cs" not in st: st["cs"] = torch.cuda.Stream(device=dev) st["ev"] = torch.cuda.Event() st["nb"] = nb st["dev"] = dev # measured neutral on this kernel at every graded context (see set_persist) frac = float(os.environ.get("MQ_PERSIST", 0.0)) ext.set_persist(st["w"].data_ptr(), st["w"].numel() * 2, frac) tens = [st["w"], st["n"], st["rinv"], st["sf"]] + st["kv"] + [st["xb"], st["ctr"]] model._ctx = ext.ctx_create(tens, [nl, model.max_seq, smax], cbs) model._store = st return st _NBUF = 2 def _bufs(st, n): """Ring of (pinned host, device, copy-done event) pairs of >= n rows.""" b = st.get("bufs") if b is None or b[0][0].shape[0] < n: b = [ ( torch.empty(n, HIDDEN, dtype=torch.bfloat16, pin_memory=True), torch.empty(n, HIDDEN, dtype=torch.bfloat16, device=st["dev"]), torch.cuda.Event(), ) for _ in range(_NBUF) ] st["bufs"] = b return b @torch.no_grad() def _run_steps(model, hidden, start_pos, n_steps, rseed): """n_steps fused decode steps. The CPU RNG stream is generated in growing chunks and copied on a side stream so it overlaps with the megakernel.""" ext = _get_ext() st = _ensure(model) nl = model.num_layers dev = st["dev"] g = torch.Generator(device="cpu") g.manual_seed(rseed) out = torch.empty(HIDDEN, dtype=torch.bfloat16, device=dev) hin_ptr = hidden.contiguous().data_ptr() CAP = 1024 bufs = _bufs(st, min(n_steps, CAP)) main = torch.cuda.current_stream(device=dev) cs, ev = st["cs"], st["ev"] done = 0 chunk = 1 ri = 0 while done < n_steps: host, devr, hev = bufs[ri % _NBUF] ri += 1 hev.synchronize() # host rows: last copy out of them has completed cs.wait_stream(main) # device rows: kernels still reading them are done room = min(CAP, n_steps - done) off = 0 while off < room: n = min(chunk, room - off) host[off : off + n].normal_(generator=g) with torch.cuda.stream(cs): devr[off : off + n].copy_(host[off : off + n], non_blocking=True) ev.record(cs) hev.record(cs) main.wait_event(ev) ph0 = st["ph"] nbar = 5 * nl * n * st["nb"] if ph0 + nbar >= 2 ** 31: st["ctr"].zero_() ph0 = 0 st["ph"] = ph0 + nbar ext.run_mega( model._ctx, start_pos + done + off, n, devr[off].data_ptr(), hin_ptr, out.data_ptr(), st["nb"], ph0, ) hin_ptr = st["xb"].data_ptr() + nl * HIDDEN * 2 off += n chunk = min(chunk * 4, CAP) done += room return out def _seeded_hidden(seed: int, device) -> torch.Tensor: g = torch.Generator(device="cpu") g.manual_seed(seed) return torch.randn(HIDDEN, generator=g, dtype=torch.bfloat16).to(device) @torch.no_grad() def prefill(model: Model, ctx_len: int, seed: int, device=None): """Build a KV cache of length ctx_len. NOT timed.""" st = _ensure(model) assert ctx_len <= model.max_seq h = _seeded_hidden(seed, st["dev"]) if ctx_len > 0: h = _run_steps(model, h, 0, ctx_len, seed + 1) return h, st["kv"], st["kv"] @torch.no_grad() def decode_steps(model, hidden, k_caches, v_caches, start_pos, n_steps, seed): """Run n_steps decode steps starting at start_pos. TIMED.""" if n_steps <= 0: return hidden, k_caches, v_caches h = _run_steps(model, hidden, start_pos, n_steps, seed + 2) return h, k_caches, v_caches def run(ctx_len: int, n_decode: int, seed: int, model=None, max_seq=None) -> dict: device = torch.device("cuda:0") max_seq = max_seq or max(ctx_len + n_decode, 512) if model is None: model = Model(NUM_LAYERS, max_seq).to(device).eval() h, k_caches, v_caches = prefill(model, ctx_len, seed, device=device) h, k_caches, v_caches = decode_steps( model, h, k_caches, v_caches, start_pos=ctx_len, n_steps=n_decode, seed=seed ) return {"last_hidden": h.detach(), "ctx_len": ctx_len, "decode_steps": n_decode} def get_init_inputs(): return [NUM_LAYERS, 131072] def get_inputs(): return []