Download include/strata/kernels/elementwise.hpp from WineryLabs/Winery-Strata: direct link, hf CLI and curl.
- Browser
- Download file 8.8 kB
-
https://huggingface.co/WineryLabs/Winery-Strata/resolve/main/include/strata/kernels/elementwise.hpp
- Command line
-
hf download hf://WineryLabs/Winery-Strata/include/strata/kernels/elementwise.hpp
-
curl -L -o elementwise.hpp https://huggingface.co/WineryLabs/Winery-Strata/resolve/main/include/strata/kernels/elementwise.hpp
8.8 kB
| // include/strata/kernels/elementwise.hpp - the small ops a LAYER needs between its GEMVs, P2.S5. | |
| // | |
| // These are not interesting kernels and that is why they are in one file with one parity test: they are the | |
| // glue in the per-layer graph, and glue that is written inline at each call site is glue with several | |
| // slightly different float conventions. Three of them exist because a projection returns a DIFFERENT | |
| // quantity from the one the next kernel wants: | |
| // | |
| // * `gdn_gate` turns (alpha, dt, ssm_a) into the recurrence's `gate`. `ref/gdn.py` L265: | |
| // `gate = softplus(w_alpha . x + dt) * ssm_a`, with `softplus(x) = log1p(exp(x))` and the large-x branch | |
| // `ggml_compute_softplus_f32` uses. `ssm_a` is NEGATIVE, which is what puts `exp(gate)` in (0,1). | |
| // * `scale_inplace` applies the delta net's `1/sqrt(S)` to q. It is a separate op because | |
| // `ref/gdn.py` says the two llama.cpp paths apply it in DIFFERENT places, so the caller has to own the | |
| // choice rather than have it buried in a kernel. | |
| // * `f32_to_f16_bulk` is what lets the f32 arithmetic of the mixer feed the fp16-activation GEMVs. | |
| // `docs/activation-contract.md` is explicit that this is NOT the contract for a quantized weight - it is | |
| // the bridge between two stages that both already exist, and the contract decision is a separate one. | |
| // | |
| // `silu_inplace` is here because `shared_expert` has its own copy inline and `gdn` needs one for the conv | |
| // output; a third copy would be two too many. | |
| namespace strata::kernels { | |
| /// Decode one canonical embedding row already selected by the caller. Codes are | |
| /// packed low bits first (2, 4 or 8 bits); scales/offsets are FP32 per group. | |
| /// The optional offset defaults to +0. Multiplication and addition round separately, | |
| /// matching the former CPU decoder. Launches asynchronously on the supplied stream. | |
| /// The caller validates positive, divisible dimensions and row/plane bounds. | |
| void embedding_gather(const uint8_t* codes, const float* scales, const float* offsets, | |
| int64_t n, int code_bits, int code_bias, int group_elems, | |
| float* out, void* stream); | |
| /// `y[t][h] = softplus(alpha[t][h] + dt[h]) * ssm_a[h]`, one thread per (t, h). In place is not possible: | |
| /// `alpha` is read and `gate` is written, and they are different buffers by construction. | |
| /// | |
| /// `h_v` is the number of v heads (48) and `n_tokens` the leading dimension; the real call is (1, 48). | |
| void gdn_gate(const float* alpha, const float* dt, const float* ssm_a, float* gate, int64_t n_tokens, | |
| int64_t h_v, void* stream); | |
| /// `x[i] *= s`, in place. | |
| void scale_inplace(float* x, int64_t n, float s, void* stream); | |
| /// `dst[i] += src[i]`. **ADDED FOR R4's HIT/MISS SPLIT, WHERE THE TWO HALVES COME FROM DIFFERENT | |
| /// ENGINES.** The CPU writes the misses into `parts` and the GPU writes the resident experts into a | |
| /// buffer of their own, so that the GPU's work can run CONCURRENTLY with the CPU's instead of after it - | |
| /// and the two are summed here, on the stream, immediately before `moe_combine` reads the result. | |
| /// | |
| /// The alternative - having the hit kernel write straight into `parts` - costs the whole overlap, because | |
| /// the copy of the misses into `parts` cannot happen until the pool has produced them, and the kernel is | |
| /// then ordered behind that copy. Measured: the drain fell 18.2 -> 10.2 ms and the token did not move. | |
| void add_inplace(float* dst, const float* src, int64_t n, void* stream); | |
| /// `y[i] = f16(x[i])`, round-to-nearest-even, using the shared conversion in `f16_bits.hpp`. | |
| void f32_to_f16_bulk(const float* x, uint16_t* y, int64_t n, void* stream); | |
| /// `y[i] = bf16(x[i])`, for the weights whose contract is a bf16 activation - the BF16 `hc_*`, `ssm_alpha`, | |
| /// `ssm_beta`, `indexer.*` and `ple_value` tensors. | |
| /// | |
| /// A SEPARATE ENTRY POINT FROM `f32_to_f16_bulk` and not a flag on it, because the two conversions are not | |
| /// variants of one idea: fp16 has 5 exponent bits and 10 mantissa bits, bf16 has 8 and 7. A caller that | |
| /// picked the wrong one would get a plausible tensor at the wrong precision - 2^-11 against 2^-9, a factor of | |
| /// four - and nothing downstream could tell which was intended. | |
| void f32_to_bf16_bulk(const float* x, uint16_t* y, int64_t n, void* stream); | |
| /// `x[i] = x[i] / (1 + exp(-x[i]))`, in place. | |
| void silu_inplace(float* x, int64_t n, void* stream); | |
| /// `build_norm`: `y[r][c] = x[r][c] / sqrt(MEAN_c(x[r]^2) + eps) * w[c]`, over the LAST axis. | |
| /// | |
| /// QSA's norm, used on `attn_q` (24x256), `attn_k` (2x256) and `indexer.q_proj` (4x128). `w` may be null. | |
| /// | |
| /// **IT IS NOT `gdn_l2_norm` AND THE DIFFERENCE IS A FACTOR OF sqrt(cols) = 16 HERE.** `gdn_l2_norm` divides by | |
| /// `sqrt(SUM(x^2) + eps)` and applies no weight; this divides by `sqrt(MEAN(x^2) + eps)` and multiplies by `w`. | |
| /// Two kernels whose names both say "norm" and whose bodies differ in one `/ cols` is exactly the pair that | |
| /// gets swapped, and swapping them produces a well-scaled, plausible tensor - so they are separate entry | |
| /// points and the parity test asserts the two readings are OBSERVABLE apart rather than trusting the names. | |
| /// | |
| /// There is NO `(1 + w)` folding: `ref/qsa.py::rms_norm` is `y * w`, and `build_norm` in the source is | |
| /// `ggml_rms_norm` followed by a plain multiply. Some norms in this artifact DO fold (the GDN path records | |
| /// which); this one does not, and the same `sqrt(cols)`-sized argument applies to getting it wrong. | |
| void rms_norm_weighted(float* x, const float* w, int64_t rows, int64_t cols, float eps, void* stream); | |
| /// *seq = v in MAPPED PINNED memory - THE DOORBELL. A KERNEL and not cudaEventRecord, because round 199 | |
| /// found that an event record inside a capture is SILENTLY DROPPED while a kernel is captured normally, and | |
| /// round 209 measured the host seeing this one's result 0.050 ms into a 39.8 ms graph. | |
| /// | |
| /// It lives here because the caller that needs it - moe_layer - is a HOST translation unit, where | |
| /// __global__ and <<<>>> do not exist. | |
| /// *seq += 1 - AN INCREMENT, NOT A STORE OF A CALLER-SUPPLIED VALUE. A value passed in is evaluated on the | |
| /// HOST at CAPTURE time, so a captured graph would ring the same number on every replay. Reading and | |
| /// incrementing in memory is what makes the count advance under cudaGraphLaunch. | |
| void doorbell_ring(uint32_t* d_seq, void* stream); | |
| /// Plan v0.3 P3 token graph: one thread spins until the host-written `*d_flag` equals the ring value `*d_seq` | |
| /// (both mapped pinned memory), then fences so everything the host wrote before the flag is visible to the | |
| /// nodes that follow. Captured once per layer between `pre[l]` and the parts copy; it is what lets one graph | |
| /// hold the whole token (bench/micro/device_wait.cu: 5.2 us per handoff, no driver call). | |
| void doorbell_wait(const uint32_t* d_flag, const uint32_t* d_seq, void* stream); | |
| /// Plan v0.3 P3 token graph: copy `n` floats from MAPPED pinned host memory (`src` is its device pointer) into | |
| /// device memory with a kernel, so the handoff stays on the compute queue (a memcpy node is a copy-engine | |
| /// operation, which WDDM submits separately and which measured 67 flushes per token). | |
| void copy_from_mapped(float* dst, const float* src, int64_t n, void* stream); | |
| /// `rows` rows of `width` floats from mapped memory, except the rows listed in hit_rows[0, *count) (device), | |
| /// which are written +0.0 instead (a verify window's GPU-computed entries: the pool leaves zeros there). | |
| /// multi-GPU: dst[rows[r] * width ..] = src[r * width ..] for r < n (float4; dst may be mapped host memory). | |
| void scatter_rows_f32(const float* src, float* dst, const int32_t* rows, int64_t n, int64_t width, void* stream); | |
| void copy_rows_from_mapped(float* dst, const float* src, int64_t rows, int64_t width, const int32_t* hit_rows, | |
| const int32_t* count, void* stream); | |
| /// Plan v0.3 P3: the doorbell's payload and its ring in ONE kernel. Copies `x` (n floats), `ids` and `weights` | |
| /// (k each) into the mapped host regions, fences, and increments the mapped sequence number - replacing three | |
| /// device-to-host memcpy nodes (copy-engine operations in the middle of the layer chain) and the ring kernel. | |
| void doorbell_publish(const float* x, const int32_t* ids, const float* weights, int64_t n, int64_t k, float* x_out, | |
| int32_t* ids_out, float* weights_out, uint32_t* d_seq, void* stream); | |
| /// Plan v0.3 P3: copy `n` int32 from mapped pinned host memory into device memory with a kernel (the QSA | |
| /// per-token step and positions), instead of a host-to-device memcpy node in the middle of a layer. | |
| void copy_i32_from_mapped(int32_t* dst, const int32_t* src, int64_t n, void* stream); | |
| } // namespace strata::kernels | |