// src/kernels/native_grouped_parity.cpp - native_expert_grouped's group stride (grid_groups) and its fused SwiGLU + // q8_1 pass, against the launches before them (native_grouped_set_v1: a block row per possible group, SwiGLU and the // q8_1 quantization as two kernels over all cap_entries). Synthetic blobs, no model. // // build/native_grouped_parity the checks (a ctest; needs the GPU) // build/native_grouped_parity --bench the checks, then v1/new timings of 48 calls in a graph (a window's layers) // // Every gate/up format with every down format; calls of 0..cap groups of 1..T entries each (an empty call is what the // verify window's PCIe call usually is), entries from 0 and from past 0 (the PCIe call's follow the VRAM call's), // scattered destinations, stale scratch, grid_groups 0 (= cap), 1, 2, 3, 4 and above cap: the output must be BITWISE // equal to v1's, the rows the call must not write included (both start from the same NaN pattern). // // Random bytes are valid codes for every format here (every grid index is in range); only the fp16 block scales are // set, small enough that the SwiGLU outputs keep a finite fp16 q8_1 scale. #include "strata/kernels/iq_kernels.hpp" #include #include #include #include #include #include #include #include #include #include namespace k = strata::kernels; namespace { int g_fail = 0; void ck(cudaError_t e, const char* what) { if (e != cudaSuccess) { std::fprintf(stderr, "CUDA error in %s: %s\n", what, cudaGetErrorString(e)); std::exit(2); } } template T* dalloc(size_t n) { T* p = nullptr; ck(cudaMalloc(&p, n * sizeof(T) + 256), "malloc"); ck(cudaMemset(p, 0, n * sizeof(T) + 256), "memset"); return p; } const char* name_of(int t) { switch (t) { case 16: return "IQ2_XXS"; case 17: return "IQ2_XS"; case 18: return "IQ3_XXS"; case 20: return "IQ4_NL"; case 21: return "IQ3_S"; case 22: return "IQ2_S"; case 23: return "IQ4_XS"; case 29: return "IQ1_M"; case 42: return "Q2_0"; case 12: return "Q4_K"; case 13: return "Q5_K"; case 7: return "Q5_1"; case 8: return "Q8_0"; default: return "?"; } } int block_values(int t) { return t == 20 || t == 7 || t == 8 ? 32 : t == 42 ? 64 : 256; } // `rows` rows of `n` values of format t: random bytes, then a finite fp16 scale in every block std::vector random_rows(int t, int64_t rows, int64_t n, std::mt19937& rng) { const size_t rb = k::iq_row_bytes(t, n), bs = k::iq_row_bytes(t, block_values(t)); std::vector w((size_t) rows * rb); std::uniform_int_distribution byte(0, 255), ex(2, 8), man(0, 1023), sgn(0, 1); for (auto& b : w) b = (uint8_t) byte(rng); for (size_t o = 0; o < w.size(); o += bs) { if (t == 29) { // IQ1_M: the fp16 scale is the top nibbles of its four scale words (bytes 48..55), the sign and the // exponent's top bits in the last: 0x1 / 0x2 there (0x9 / 0xA negative) keeps it in 2^-11 .. 2^-3 const uint8_t nib = (uint8_t) ((sgn(rng) ? 0x8 : 0x0) | (1 + (byte(rng) & 1))); w[o + 55] = (uint8_t) ((w[o + 55] & 0x0F) | (nib << 4)); } else { // the other formats start their block with the fp16 scale: 2^-13 .. 2^-6 // (Q4_K / Q5_K: 2^-14 .. 2^-11, their 6-bit sub-scales multiply it by up to 63) const int e = (t == 12 || t == 13) ? 1 + ex(rng) % 4 : ex(rng); const uint16_t h = (uint16_t) ((sgn(rng) ? 0x8000 : 0) | (e << 10) | man(rng)); std::memcpy(&w[o], &h, 2); if (t == 12 || t == 13 || t == 7) { // Q4_K / Q5_K: dmin, Q5_1: m - the second fp16, as small const int em = (t == 7) ? ex(rng) : 1 + ex(rng) % 4; const uint16_t m = (uint16_t) ((sgn(rng) ? 0x8000 : 0) | (em << 10) | man(rng)); std::memcpy(&w[o + 2], &m, 2); } } } return w; } // one window's worth of expert calls on one format pair: blobs, T tokens' q8_1 activations, cap = T * K entries struct Setup { k::NativeExpertLayout L; int T = 0, K = 0, cap = 0, n_blobs = 0; size_t slot = 0; uint8_t* blobs = nullptr; uint8_t* xq = nullptr; uint8_t* scr = nullptr; size_t scr_bytes = 0; float* out = nullptr; size_t out_floats = 0; unsigned long long* ptr = nullptr; int32_t *start = nullptr, *n = nullptr, *dst = nullptr, *tok = nullptr; Setup(int gu, int dt, int64_t H, int64_t FF, int tokens, int k_per_token, int blobs_n, std::mt19937& rng, cudaStream_t s) { L = k::native_expert_layout(gu, dt, H, FF); T = tokens; K = k_per_token; cap = T * K; n_blobs = blobs_n; slot = (L.bytes + 255) & ~(size_t) 255; blobs = dalloc(slot * (size_t) n_blobs); for (int b = 0; b < n_blobs; ++b) { // gate rows | up rows (format gu, H values each) | down rows (format dt, FF values each) auto blob = random_rows(gu, 2 * FF, H, rng); const auto down = random_rows(dt, H, FF, rng); blob.insert(blob.end(), down.begin(), down.end()); if (blob.size() != L.bytes) { std::fprintf(stderr, "blob %zu != %zu bytes\n", blob.size(), L.bytes); std::exit(2); } ck(cudaMemcpy(blobs + slot * (size_t) b, blob.data(), blob.size(), cudaMemcpyHostToDevice), "blob"); } std::normal_distribution nd(0.f, 1.f); std::vector x((size_t) T * H); for (auto& v : x) v = nd(rng); float* dx = dalloc(x.size()); ck(cudaMemcpy(dx, x.data(), x.size() * 4, cudaMemcpyHostToDevice), "x"); xq = dalloc((size_t) T * (H / 32) * 36); k::quantize_q8_1_rows(dx, T, H, xq, s); ck(cudaStreamSynchronize(s), "quantize"); cudaFree(dx); scr_bytes = k::native_expert_scratch_bytes(cap, FF); scr = dalloc(scr_bytes); out_floats = (size_t) cap * H; out = dalloc(out_floats); ptr = dalloc(cap); start = dalloc(cap + 1); n = dalloc(1); dst = dalloc(cap); tok = dalloc(cap); } ~Setup() { cudaFree(blobs); cudaFree(xq); cudaFree(scr); cudaFree(out); cudaFree(ptr); cudaFree(start); cudaFree(n); cudaFree(dst); cudaFree(tok); } // a plan: `groups` groups of 1..T entries from entry `base` on (as many as fit in cap) int plan(int groups, int base, std::mt19937& rng) { std::uniform_int_distribution size(1, T); std::vector sizes; int used = base; for (int g = 0; g < groups; ++g) { const int m = std::min(size(rng), cap - used); if (m <= 0) break; sizes.push_back(m); used += m; } return plan_sizes(sizes, base, rng); } // groups of these sizes from entry `base` on, each its own blob (as long as there are enough: a window's groups // are different experts); scattered destinations, random tokens int plan_sizes(const std::vector& sizes, int base, std::mt19937& rng) { std::uniform_int_distribution tk(0, T - 1); std::vector order(n_blobs); std::iota(order.begin(), order.end(), 0); std::shuffle(order.begin(), order.end(), rng); std::vector p; std::vector st(1, base), tk_v, ds(cap); for (const int m : sizes) { p.push_back((unsigned long long) (blobs + slot * (size_t) order[p.size() % order.size()])); st.push_back(st.back() + m); } if (st.back() > cap) { std::fprintf(stderr, "plan: %d entries > cap %d\n", st.back(), cap); std::exit(2); } const int ng = (int) p.size(); std::iota(ds.begin(), ds.end(), 0); std::shuffle(ds.begin(), ds.end(), rng); // scattered rows of `out` for (int e = 0; e < cap; ++e) tk_v.push_back(tk(rng)); if (ng) ck(cudaMemcpy(ptr, p.data(), p.size() * 8, cudaMemcpyHostToDevice), "ptr"); ck(cudaMemcpy(start, st.data(), st.size() * 4, cudaMemcpyHostToDevice), "start"); ck(cudaMemcpy(n, &ng, 4, cudaMemcpyHostToDevice), "n"); ck(cudaMemcpy(dst, ds.data(), ds.size() * 4, cudaMemcpyHostToDevice), "dst"); ck(cudaMemcpy(tok, tk_v.data(), tk_v.size() * 4, cudaMemcpyHostToDevice), "tok"); return ng; } void call(int64_t grid_groups, cudaStream_t s) { k::native_expert_grouped(L, ptr, start, n, dst, tok, cap, cap, xq, scr, out, s, grid_groups); } // `out` after one call from the NaN pattern, the scratch holding `junk` (what earlier calls left there) std::vector result(bool v1, int64_t grid_groups, int junk, cudaStream_t s) { ck(cudaMemset(out, 0xFF, out_floats * 4), "memset"); ck(cudaMemset(scr, junk, scr_bytes), "memset"); k::native_grouped_set_v1(v1); call(grid_groups, s); k::native_grouped_set_v1(false); ck(cudaStreamSynchronize(s), "sync"); std::vector o(out_floats); ck(cudaMemcpy(o.data(), out, o.size() * 4, cudaMemcpyDeviceToHost), "out"); return o; } }; void check(int gu, int dt, int64_t H, int64_t FF, cudaStream_t s, std::mt19937& rng) { Setup S(gu, dt, H, FF, 6, 4, 9, rng, s); // 6 tokens x 4 experts: cap 24 entries int calls = 0, bad = 0; size_t written = 0; for (const int base : {0, 7}) { for (const int groups : {0, 1, 2, 3, 5, 8, 24}) { const int ng = S.plan(groups, base, rng); const auto ref = S.result(true, 0, 0x00, s); size_t w = 0; for (const uint32_t v : ref) w += v != 0xFFFFFFFFu; written += w; for (const int64_t gy : {(int64_t) 0, (int64_t) 1, (int64_t) 2, (int64_t) 3, (int64_t) 4, (int64_t) S.cap + 3}) { const auto got = S.result(false, gy, (calls & 1) ? 0x7F : 0x00, s); ++calls; size_t diff = 0; for (size_t i = 0; i < ref.size(); ++i) diff += ref[i] != got[i]; if (diff) { std::printf(" %-8s/%-7s base %d, %d groups, grid_groups %lld: %zu of %zu floats differ FAIL\n", name_of(gu), name_of(dt), base, ng, (long long) gy, diff, ref.size()); ++bad; } } } } std::printf("%-8s/%-7s %5lld x %4lld %d calls: %s (%zu output floats written in all)\n", name_of(gu), name_of(dt), (long long) H, (long long) FF, calls, bad ? "FAIL" : "bitwise equal to v1", written); if (written == 0) { std::printf(" nothing was written: the check checked nothing FAIL\n"); ++bad; } g_fail += bad; } // ------------------------------------------------------------------------------------------------ --bench // A window's 48 calls, one per layer, each with ITS OWN experts as the layers have: copies of the Setup's blobs in one // arena, so the weights stream from DRAM as in the engine. (48 calls re-reading the same 28 experts - 73 MB, about an // Ada L2 - would read part of them from L2; the engine's layers share none.) struct Layers { uint8_t* arena = nullptr; std::vector ptr; // per layer: its groups' blobs (device) Layers(Setup& S, int ng) { const int per = std::max(ng, 1); arena = dalloc(S.slot * (size_t) (48 * per)); for (int l = 0; l < 48; ++l) { std::vector p; for (int j = 0; j < per; ++j) { uint8_t* d = arena + S.slot * (size_t) (l * per + j); ck(cudaMemcpy(d, S.blobs + S.slot * (size_t) ((l * per + j) % S.n_blobs), S.L.bytes, cudaMemcpyDeviceToDevice), "blob copy"); p.push_back((unsigned long long) d); } unsigned long long* dp = dalloc(p.size()); ck(cudaMemcpy(dp, p.data(), p.size() * 8, cudaMemcpyHostToDevice), "ptr"); ptr.push_back(dp); } } Layers(const Layers&) = delete; Layers& operator=(const Layers&) = delete; ~Layers() { for (auto* p : ptr) cudaFree(p); cudaFree(arena); } }; // the 48 calls of one variant captured in one graph cudaGraphExec_t make_graph(cudaStream_t s, Setup& S, const Layers& Ls, bool v1, int64_t gy) { k::native_grouped_set_v1(v1); cudaGraph_t g; cudaGraphExec_t ge; ck(cudaStreamBeginCapture(s, cudaStreamCaptureModeThreadLocal), "capture"); for (int l = 0; l < 48; ++l) k::native_expert_grouped(S.L, Ls.ptr[l], S.start, S.n, S.dst, S.tok, S.cap, S.cap, S.xq, S.scr, S.out, s, gy); ck(cudaStreamEndCapture(s, &g), "capture"); ck(cudaGraphInstantiate(&ge, g, 0), "instantiate"); ck(cudaGraphDestroy(g), "graph"); k::native_grouped_set_v1(false); return ge; } // one launch of the graph: microseconds per call float launch_us(cudaStream_t s, cudaGraphExec_t ge, cudaEvent_t e0, cudaEvent_t e1) { ck(cudaEventRecord(e0, s), "record"); ck(cudaGraphLaunch(ge, s), "launch"); ck(cudaEventRecord(e1, s), "record"); ck(cudaEventSynchronize(e1), "bench"); float ms = 0; ck(cudaEventElapsedTime(&ms, e0, e1), "elapsed"); return 1e3f * ms / 48.0f; } float median(std::vector v) { std::sort(v.begin(), v.end()); return v[v.size() / 2]; } // The two variants' launches ALTERNATE (v1-new, new-v1, ...) after a warm-up, and each reports its median: a card // under sustained load steps its clock down after a fraction of a second (an RTX 4080 SUPER went from 80 to 89 us // per gate/up call within one run), and whichever variant ran before the step would look faster. constexpr int kWarmPairs = 20, kPairs = 40; void bench(cudaStream_t s, std::mt19937& rng) { std::printf("\n--bench: microseconds per call, a window's 48 calls in a graph, each layer with its own experts (the\n" "weights stream from DRAM as in the engine); v1 / new (grid_groups): medians of %d launches each,\n" "alternating, after %d warm-up pairs - idle GPU assumed\n", kPairs, kWarmPairs); // a verify window's shape: 4 tokens x 10 experts, 2560 x 768 IQ3_S / IQ4_XS experts Setup S(21, 23, 2560, 768, 4, 10, 32, rng, s); std::vector vram(16, 1); vram.insert(vram.end(), 12, 2); // 28 groups, 40 entries const struct { const char* what; std::vector sizes; int64_t gy; } rows[] = { {"no group (the PCIe call at pcie_frac 0)", {}, 4}, {"1 group of 2 entries (a PCIe call)", {2}, 4}, {"28 groups, 40 entries (a VRAM call)", vram, 0}, }; cudaEvent_t e0, e1; ck(cudaEventCreate(&e0), "event"); ck(cudaEventCreate(&e1), "event"); for (const auto& r : rows) { const int ng = S.plan_sizes(r.sizes, 0, rng); const Layers Ls(S, ng); // 48 x 28 experts: 3.7 GB for the VRAM call cudaGraphExec_t ga = make_graph(s, S, Ls, true, 0), gb = make_graph(s, S, Ls, false, r.gy); std::vector ta, tb; for (int i = 0; i < kWarmPairs + kPairs; ++i) { const bool a_first = i % 2 == 0; const float x = launch_us(s, a_first ? ga : gb, e0, e1), y = launch_us(s, a_first ? gb : ga, e0, e1); if (i < kWarmPairs) continue; ta.push_back(a_first ? x : y); tb.push_back(a_first ? y : x); } ck(cudaGraphExecDestroy(ga), "graph"); ck(cudaGraphExecDestroy(gb), "graph"); std::printf(" %-42s %8.2f / %8.2f us (grid_groups %lld)\n", r.what, median(ta), median(tb), (long long) r.gy); } cudaEventDestroy(e0); cudaEventDestroy(e1); } } // namespace int main(int argc, char** argv) { const bool do_bench = argc > 1 && std::string(argv[1]) == "--bench"; cudaStream_t s; ck(cudaStreamCreate(&s), "stream"); std::mt19937 rng(316); for (int gu : {16, 17, 18, 21, 22, 23, 29, 42, 12, 13, 8}) // STRATA_GU_FMTS for (int dt : {20, 23, 42, 7, 8}) check(gu, dt, 512, 256, s, rng); // STRATA_D_FMTS; IQ4_XS: n_ff % 256 check(21, 20, 2560, 640, s, rng); // a model's shapes check(21, 23, 2560, 768, s, rng); if (do_bench) bench(s, rng); std::printf("native_grouped_parity: %d failures\n", g_fail); cudaStreamDestroy(s); return g_fail ? 1 : 0; }