File size: 8,737 Bytes
bbb6388
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// src/core/overlap_main.cpp - `strata-overlap`: does streaming an expert overlap with computing on it?
//
// THE ARCHITECTURE'S CENTRAL PREMISE.  Only (1 - h) of a token's expert weights are in VRAM, so the rest must
// cross the bus while the GPU is working.  If the copy and the compute do not overlap, the per-token cost is
// their SUM; if they do, it is their MAXIMUM.  L3 measured the stream at 4.07 ms/token (design h) and L5 the
// matvec at 27.9 ms/token, so serial would be ~32 ms and overlapped ~28 - but those are the STEADY-STATE
// numbers for a whole token; at one expert's granularity the ratio is completely different and that is what
// decides how the loop must be built.
//
// Measured against a SERIAL baseline in the same process, because the interesting quantity is the ratio and
// not the absolute figure - and L6 established that absolute figures on this machine move by 20%.
#include "strata/core/pinned.hpp"
#include "strata/kernels/s_gemv.hpp"

#include <cuda_runtime.h>

#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>

namespace {

void check(cudaError_t e, const char* what) {
    if (e != cudaSuccess) {
        std::fprintf(stderr, "%s: %s\n", what, cudaGetErrorString(e));
        std::exit(1);
    }
}

struct Timing {
    double seconds = 0;
    const char* name = "";
};

}  // namespace

int main(int argc, char** argv) {
    std::string path;
    int experts = 64;
    int threads_per_row = 32;
    for (int i = 1; i < argc; ++i) {
        const std::string a = argv[i];
        auto val = [&]() -> const char* {
            if (i + 1 >= argc) { std::fprintf(stderr, "%s needs a value\n", a.c_str()); std::exit(2); }
            return argv[++i];
        };
        if (a == "--file") path = val();
        else if (a == "--experts") experts = std::atoi(val());
        else if (a == "--tpr") threads_per_row = std::atoi(val());
        else {
            std::fprintf(stderr, "usage: strata-overlap --file experts.bin [--experts N] [--tpr N]\n");
            return 2;
        }
    }
    if (path.empty()) { std::fprintf(stderr, "--file is required\n"); return 2; }

    // One ROLE's worth of S2 weights: gate is [n_embd 2560] x [ff 640] = 1,638,400 weights = 460,800 B of
    // codes at 4 codes per byte, plus 40 scales per row.
    const long long n_in = 2560, n_out = 640;
    const uint64_t role_codes = (uint64_t) n_out * n_in / 4;
    const uint64_t role_bytes = role_codes + (uint64_t) n_out * (n_in / 64) * 4;
    std::printf("per role: %lld x %lld S2 weights, %.1f KB of planes\n", n_in, n_out, role_bytes / 1024.0);

    // host side: `experts` blobs read into a pinned arena, and the S2 planes the GEMV will read
    strata::core::PinnedArena arena((uint64_t) experts * role_codes);
    if (!arena.valid()) { std::fprintf(stderr, "arena allocation failed\n"); return 1; }
    {
        std::ifstream f(path, std::ios::binary);
        if (!f) { std::fprintf(stderr, "cannot open %s\n", path.c_str()); return 1; }
        // take the first `experts` blobs' worth of CODE bytes, pretending each is a role's plane: the bytes
        // are real Q2_0 codes, so the kernel decodes real data and no cache-friendly pattern is invented
        for (int i = 0; i < experts; ++i) {
            f.seekg((std::streamoff) i * 1382400);
            f.read((char*) arena.data() + (uint64_t) i * role_codes, (std::streamsize) role_codes);
            if ((uint64_t) f.gcount() != role_codes) {
                std::fprintf(stderr, "short read on expert %d\n", i);
                return 1;
            }
        }
    }
    std::printf("loaded %d role-sized planes into a %s arena\n", experts, arena.note.c_str());

    // device side: double buffered codes, plus x / scales / y
    uint8_t* d_codes[2] = {nullptr, nullptr};
    for (int b = 0; b < 2; ++b) check(cudaMalloc(&d_codes[b], role_codes), "cudaMalloc codes");
    uint16_t* d_x = nullptr;
    float *d_scales = nullptr, *d_y = nullptr;
    check(cudaMalloc(&d_x, n_in * sizeof(uint16_t)), "cudaMalloc x");
    check(cudaMalloc(&d_scales, (size_t) n_out * (n_in / 64) * sizeof(float)), "cudaMalloc scales");
    check(cudaMalloc(&d_y, n_out * sizeof(float)), "cudaMalloc y");
    std::vector<uint16_t> hx((size_t) n_in, 0x3C00);
    std::vector<float> hs((size_t) n_out * (size_t) (n_in / 64), 0.001f);
    check(cudaMemcpy(d_x, hx.data(), hx.size() * sizeof(uint16_t), cudaMemcpyHostToDevice), "copy x");
    check(cudaMemcpy(d_scales, hs.data(), hs.size() * sizeof(float), cudaMemcpyHostToDevice), "copy scales");

    cudaStream_t s_copy{}, s_comp{};
    check(cudaStreamCreate(&s_copy), "stream copy");
    check(cudaStreamCreate(&s_comp), "stream compute");
    const strata::kernels::SForm form{2, -1, 64, strata::kernels::Codebook::Affine, false};

    // ---- SERIAL: the same operations on the SAME stream, so the GPU runs them one after another ----
    //
    // NOT a sync-per-iteration loop.  The first version waited with `cudaStreamSynchronize` after every copy and
    // measured 234.8 us/expert where a copy plus a GEMV is ~43 us: the baseline was dominated by the CPU
    // round-trip, so the ratio of 0.095 mostly measured "with syncs" against "without syncs".  Putting both
    // operations on one stream expresses the same dependency with NO CPU involvement and ONE sync at the end,
    // which is what makes the comparison about overlap rather than about synchronisation.
    check(cudaStreamSynchronize(0), "pre sync");
    auto t0 = std::chrono::steady_clock::now();
    for (int i = 0; i < experts; ++i) {
        check(cudaMemcpyAsync(d_codes[0], arena.data() + (uint64_t) i * role_codes, role_codes,
                              cudaMemcpyHostToDevice, 0),
              "serial copy");
        strata::kernels::s_gemv_split_async(d_x, d_codes[0], d_scales, nullptr, d_y, n_in, n_out, form,
                                            threads_per_row, (void*) (cudaStream_t) 0);
    }
    check(cudaStreamSynchronize(0), "serial final sync");
    const double serial = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();

    // ---- OVERLAPPED: copy the next one while computing on the current one ----
    check(cudaStreamSynchronize(0), "pre sync 2");
    cudaEvent_t ev[2];
    for (int b = 0; b < 2; ++b) check(cudaEventCreateWithFlags(&ev[b], cudaEventDisableTiming), "event");
    t0 = std::chrono::steady_clock::now();
    check(cudaMemcpyAsync(d_codes[0], arena.data(), role_codes, cudaMemcpyHostToDevice, s_copy), "prologue");
    check(cudaEventRecord(ev[0], s_copy), "prologue event");
    for (int i = 0; i < experts; ++i) {
        const int cur = i & 1, nxt = (i + 1) & 1;
        if (i + 1 < experts) {
            check(cudaMemcpyAsync(d_codes[nxt], arena.data() + (uint64_t) (i + 1) * role_codes, role_codes,
                                  cudaMemcpyHostToDevice, s_copy),
                  "overlap copy");
            check(cudaEventRecord(ev[nxt], s_copy), "overlap event");
        }
        // the compute stream must not start row i until its codes have landed
        check(cudaStreamWaitEvent(s_comp, ev[cur], 0), "wait");
        // a hand-written launch so it goes on s_comp; `s_gemv` synchronises on the default stream
        strata::kernels::s_gemv_split_async(d_x, d_codes[cur], d_scales, nullptr, d_y, n_in, n_out, form,
                                            threads_per_row, (void*) s_comp);
    }
    check(cudaStreamSynchronize(s_comp), "final sync");
    check(cudaStreamSynchronize(s_copy), "final sync 2");
    const double overlap = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();

    std::printf("\n  serial     %8.3f ms for %d experts  (%.1f us/expert)\n", serial * 1000, experts,
                serial * 1e6 / experts);
    std::printf("  overlapped %8.3f ms for %d experts  (%.1f us/expert)\n", overlap * 1000, experts,
                overlap * 1e6 / experts);
    std::printf("  ratio      %.3f  (1.00 = no overlap at all, <1.00 = overlap)\n", overlap / serial);
    std::printf("\nNOTE: the OVERLAPPED loop launches on `s_comp` without synchronising, so the copy and the\n"
                "      compute really are concurrent; the serial loop waits after each step.  What this does NOT\n"
                "      model is the CPU expert path, the router, or the fact that a real token has 1440 roles to\n"
                "      stream rather than %d.\n", experts);

    for (int b = 0; b < 2; ++b) {
        cudaFree(d_codes[b]);
        cudaEventDestroy(ev[b]);
    }
    cudaFree(d_x);
    cudaFree(d_scales);
    cudaFree(d_y);
    cudaStreamDestroy(s_copy);
    cudaStreamDestroy(s_comp);
    return 0;
}