Skip to main content

ferric_core/
kernels.rs

1//! Ferric L1 kernel set toward a transformer forward pass: elementwise add, SiLU, LayerNorm, Softmax.
2//! Each is one WGSL kernel written once, run on any fabric (native GPU + browser WebGPU), and
3//! validated against the plain-Rust CPU reference in `cpu`. Composed with matmul (in lib.rs), these
4//! cover the bulk of a transformer block; attention is matmul + softmax + matmul.
5
6use crate::{Context, Result, Tensor};
7
8/// On-GPU tensor ops — the L3 graph-executor primitives. Each dispatches and returns a Tensor whose
9/// buffer stays on the device; nothing reads back until `to_vec`, so a whole model runs on-GPU.
10impl Context {
11    /// Upload host data to a GPU tensor.
12    pub fn tensor(&self, data: &[f32], shape: &[usize]) -> Tensor {
13        Tensor { buf: self.storage("t", data), shape: shape.to_vec() }
14    }
15    /// Allocate an empty (device-only) tensor.
16    pub fn empty(&self, shape: &[usize]) -> Tensor {
17        let len: usize = shape.iter().product();
18        Tensor { buf: self.out_buffer(len), shape: shape.to_vec() }
19    }
20    /// Read a tensor back to host (the only sync point).
21    pub async fn to_vec(&self, t: &Tensor) -> Result<Vec<f32>> { self.readback(&t.buf, t.len()).await }
22
23    /// C = A(m×k)·B(k×n), on-GPU.
24    pub fn mm(&self, a: &Tensor, b: &Tensor, m: u32, k: u32, n: u32) -> Tensor {
25        let out = self.empty(&[m as usize, n as usize]);
26        let dims = self.uniform_u32("d", &[m, k, n, 0]);
27        let pipe = self.pipeline("mm", crate::MATMUL_WGSL);
28        self.dispatch(&pipe, &[&a.buf, &b.buf, &out.buf, &dims], ((m + 15) / 16, (n + 15) / 16, 1));
29        out
30    }
31    /// C = scale·(A(m×k)·B(n×k)ᵀ), on-GPU.
32    pub fn mm_bt(&self, a: &Tensor, b: &Tensor, m: u32, n: u32, k: u32, scale: f32) -> Tensor {
33        let out = self.empty(&[m as usize, n as usize]);
34        let dims = self.uniform_u32("d", &[m, n, k, scale.to_bits()]);
35        let pipe = self.pipeline("mm_bt", MATMUL_BT_WGSL);
36        self.dispatch(&pipe, &[&a.buf, &b.buf, &out.buf, &dims], ((m + 15) / 16, (n + 15) / 16, 1));
37        out
38    }
39    pub fn silu_t(&self, x: &Tensor) -> Tensor {
40        let n = x.len();
41        let out = self.empty(&x.shape);
42        let dims = self.uniform_u32("n", &[n as u32, 0, 0, 0]);
43        let pipe = self.pipeline("silu", SILU_WGSL);
44        self.dispatch(&pipe, &[&x.buf, &out.buf, &dims], ((n as u32 + 63) / 64, 1, 1));
45        out
46    }
47    /// Reinterpret a tensor's data under a new shape (contiguous row-major) — Reshape/Cast/Squeeze.
48    pub fn dup(&self, x: &Tensor, shape: Vec<usize>) -> Tensor {
49        Tensor { buf: self.copy_buf(&x.buf, x.len()), shape }
50    }
51    /// Row-gather along axis 0: out[i, :] = data[idx[i], :]. Embedding lookup / index_select.
52    pub fn gather0(&self, data: &Tensor, idx: &[u32], d: usize) -> Tensor {
53        let n = idx.len();
54        let out = self.empty(&[n, d]);
55        let idx_buf = self.storage_u32("idx", idx);
56        let dims = self.uniform_u32("d", &[n as u32, d as u32, 0, 0]);
57        let pipe = self.pipeline("gather", GATHER_WGSL);
58        self.dispatch(&pipe, &[&data.buf, &idx_buf, &out.buf, &dims], ((n * d) as u32 / 64 + 1, 1, 1));
59        out
60    }
61    pub fn sigmoid_t(&self, x: &Tensor) -> Tensor { self.unary(x, SIGMOID_WGSL, "sigmoid") }
62    pub fn sqrt_t(&self, x: &Tensor) -> Tensor { self.unary(x, SQRT_WGSL, "sqrt") }
63    pub fn gelu_t(&self, x: &Tensor) -> Tensor { self.unary(x, GELU_WGSL, "gelu") }
64    pub fn sub_t(&self, a: &Tensor, b: &Tensor) -> Tensor { self.binary(a, b, SUB_WGSL, "sub") }
65    pub fn div_t(&self, a: &Tensor, b: &Tensor) -> Tensor { self.binary(a, b, DIV_WGSL, "div") }
66    fn unary(&self, x: &Tensor, wgsl: &str, name: &str) -> Tensor {
67        let n = x.len();
68        let out = self.empty(&x.shape);
69        let dims = self.uniform_u32("n", &[n as u32, 0, 0, 0]);
70        let pipe = self.pipeline(name, wgsl);
71        self.dispatch(&pipe, &[&x.buf, &out.buf, &dims], ((n as u32 + 63) / 64, 1, 1));
72        out
73    }
74    fn binary(&self, a: &Tensor, b: &Tensor, wgsl: &str, name: &str) -> Tensor {
75        let n = a.len();
76        let out = self.empty(&a.shape);
77        let dims = self.uniform_u32("n", &[n as u32, 0, 0, 0]);
78        let pipe = self.pipeline(name, wgsl);
79        self.dispatch(&pipe, &[&a.buf, &b.buf, &out.buf, &dims], ((n as u32 + 63) / 64, 1, 1));
80        out
81    }
82    /// Elementwise C = A ⊙ B.
83    pub fn mul_t(&self, a: &Tensor, b: &Tensor) -> Tensor {
84        let n = a.len();
85        let out = self.empty(&a.shape);
86        let dims = self.uniform_u32("n", &[n as u32, 0, 0, 0]);
87        let pipe = self.pipeline("mul", MUL_WGSL);
88        self.dispatch(&pipe, &[&a.buf, &b.buf, &out.buf, &dims], ((n as u32 + 63) / 64, 1, 1));
89        out
90    }
91    /// C = A · scalar, where `s` is a 1-element tensor (broadcast).
92    pub fn mul_scalar_t(&self, a: &Tensor, s: &Tensor) -> Tensor {
93        let n = a.len();
94        let out = self.empty(&a.shape);
95        let dims = self.uniform_u32("n", &[n as u32, 0, 0, 0]);
96        let pipe = self.pipeline("mul_scalar", MUL_SCALAR_WGSL);
97        self.dispatch(&pipe, &[&a.buf, &s.buf, &out.buf, &dims], ((n as u32 + 63) / 64, 1, 1));
98        out
99    }
100    /// 2D transpose [rows×cols] → [cols×rows].
101    pub fn transpose2d_t(&self, x: &Tensor, rows: u32, cols: u32) -> Tensor {
102        let out = self.empty(&[cols as usize, rows as usize]);
103        let dims = self.uniform_u32("d", &[rows, cols, 0, 0]);
104        let pipe = self.pipeline("transpose", TRANSPOSE_WGSL);
105        self.dispatch(&pipe, &[&x.buf, &out.buf, &dims], ((rows + 15) / 16, (cols + 15) / 16, 1));
106        out
107    }
108    /// C = A + bias broadcast per row: out[i] = a[i] + bias[i % d]. (a is [.,d], bias is [d].)
109    pub fn add_bias_t(&self, a: &Tensor, bias: &Tensor) -> Tensor {
110        let (n, d) = (a.len(), bias.len());
111        let out = self.empty(&a.shape);
112        let dims = self.uniform_u32("d", &[n as u32, d as u32, 0, 0]);
113        let pipe = self.pipeline("add_bias", ADD_BIAS_WGSL);
114        self.dispatch(&pipe, &[&a.buf, &bias.buf, &out.buf, &dims], ((n as u32 + 63) / 64, 1, 1));
115        out
116    }
117    pub fn relu_t(&self, x: &Tensor) -> Tensor {
118        let n = x.len();
119        let out = self.empty(&x.shape);
120        let dims = self.uniform_u32("n", &[n as u32, 0, 0, 0]);
121        let pipe = self.pipeline("relu", RELU_WGSL);
122        self.dispatch(&pipe, &[&x.buf, &out.buf, &dims], ((n as u32 + 63) / 64, 1, 1));
123        out
124    }
125    pub fn add_t(&self, a: &Tensor, b: &Tensor) -> Tensor {
126        let n = a.len();
127        let out = self.empty(&a.shape);
128        let dims = self.uniform_u32("n", &[n as u32, 0, 0, 0]);
129        let pipe = self.pipeline("add", ADD_WGSL);
130        self.dispatch(&pipe, &[&a.buf, &b.buf, &out.buf, &dims], ((n as u32 + 63) / 64, 1, 1));
131        out
132    }
133    pub fn softmax_t(&self, x: &Tensor, rows: u32, d: u32) -> Tensor {
134        let out = self.empty(&x.shape);
135        let dims = self.uniform_u32("d", &[rows, d, 0, 0]);
136        let pipe = self.pipeline("softmax", SOFTMAX_WGSL);
137        self.dispatch(&pipe, &[&x.buf, &out.buf, &dims], ((rows + 63) / 64, 1, 1));
138        out
139    }
140    /// Rotary position embedding (rotate-half, NeoX/Llama style). x is [T, H·dh]; rotates each head's
141    /// dh-vector by position-dependent angles. Applied to Q and K before attention.
142    pub fn rope_t(&self, x: &Tensor, t: u32, h: u32, dh: u32, base: f32) -> Tensor {
143        self.rope_off_t(x, t, h, dh, base, 0)
144    }
145    /// RoPE with an absolute-position offset: row i is rotated for position (i + offset). Prefill uses
146    /// offset 0; incremental decode of the token at position `pos` uses a 1-row input with offset=pos.
147    pub fn rope_off_t(&self, x: &Tensor, t: u32, h: u32, dh: u32, base: f32, offset: u32) -> Tensor {
148        let out = self.empty(&x.shape);
149        let dims = self.uniform_u32("d", &[t, h, dh, base.to_bits()]);
150        let meta = self.uniform_u32("m", &[offset, 0, 0, 0]);
151        let pipe = self.pipeline("rope", ROPE_WGSL);
152        self.dispatch(&pipe, &[&x.buf, &out.buf, &dims, &meta], (t * h / 64 + 1, 1, 1));
153        out
154    }
155    /// Attention for one query token against a KV cache of `s` past keys/values (incremental decode).
156    /// q is [1, H·dh]; k/v are [s, H·dh]; no mask (all cached keys precede the query). Returns [1, H·dh].
157    pub fn mha_decode_t(&self, q: &Tensor, k: &Tensor, v: &Tensor, hq: u32, hkv: u32, dh: u32, s: u32) -> Tensor {
158        let out = self.empty(&q.shape);
159        let scale = 1.0f32 / (dh as f32).sqrt();
160        let dims = self.uniform_u32("d", &[s, hq, dh, scale.to_bits()]);
161        let gqa = self.uniform_u32("g", &[hkv, 0, 0, 0]);
162        let pipe = self.pipeline("mha_decode", MHA_DECODE_WGSL);
163        self.dispatch(&pipe, &[&q.buf, &k.buf, &v.buf, &out.buf, &dims, &gqa], (hq / 64 + 1, 1, 1));
164        out
165    }
166    /// Causal multi-head attention, single flash-style pass, with grouped-query attention (GQA): Q has
167    /// `hq` heads, K/V have `hkv` heads (hq % hkv == 0); query head h reads kv head h/(hq/hkv). q is
168    /// [T, hq·dh]; k/v are [T, hkv·dh]; scale = 1/√dh; query i attends to keys 0..=i. Returns [T, hq·dh].
169    pub fn mha_causal_t(&self, q: &Tensor, k: &Tensor, v: &Tensor, t: u32, hq: u32, hkv: u32, dh: u32) -> Tensor {
170        let out = self.empty(&q.shape);
171        let scale = 1.0f32 / (dh as f32).sqrt();
172        let dims = self.uniform_u32("d", &[t, hq, dh, scale.to_bits()]);
173        let gqa = self.uniform_u32("g", &[hkv, 0, 0, 0]);
174        let pipe = self.pipeline("mha", MHA_CAUSAL_WGSL);
175        self.dispatch(&pipe, &[&q.buf, &k.buf, &v.buf, &out.buf, &dims, &gqa], (t * hq / 64 + 1, 1, 1));
176        out
177    }
178    /// RMSNorm (Llama/SmolVLA norm): out = x / sqrt(mean(x²)+eps) · weight. No mean-subtraction, no bias.
179    pub fn rmsnorm_t(&self, x: &Tensor, w: &Tensor, rows: u32, d: u32, eps: f32) -> Tensor {
180        let out = self.empty(&x.shape);
181        let dims = self.uniform_u32("d", &[rows, d, eps.to_bits(), 0]);
182        let pipe = self.pipeline("rmsnorm", RMSNORM_WGSL);
183        self.dispatch(&pipe, &[&x.buf, &w.buf, &out.buf, &dims], (rows, 1, 1));
184        out
185    }
186    pub fn layernorm_t(&self, x: &Tensor, w: &Tensor, b: &Tensor, rows: u32, d: u32, eps: f32) -> Tensor {
187        let out = self.empty(&x.shape);
188        let dims = self.uniform_u32("d", &[rows, d, eps.to_bits(), 0]);
189        let pipe = self.pipeline("layernorm", LAYERNORM_WGSL);
190        self.dispatch(&pipe, &[&x.buf, &w.buf, &b.buf, &out.buf, &dims], ((rows + 63) / 64, 1, 1));
191        out
192    }
193    /// Single-head attention on-GPU: softmax(scale·Q·Kᵀ)·V, all buffers stay on device.
194    pub fn attention_t(&self, q: &Tensor, k: &Tensor, v: &Tensor, rq: u32, rk: u32, d: u32, dv: u32, scale: f32) -> Tensor {
195        let scores = self.mm_bt(q, k, rq, rk, d, scale);
196        let probs = self.softmax_t(&scores, rq, rk);
197        self.mm(&probs, v, rq, rk, dv)
198    }
199}
200
201impl Context {
202    /// Elementwise C = A + B (f32).
203    pub async fn add(&self, a: &[f32], b: &[f32]) -> Result<Vec<f32>> {
204        assert_eq!(a.len(), b.len());
205        let n = a.len();
206        let (ab, bb) = (self.storage("a", a), self.storage("b", b));
207        let out = self.out_buffer(n);
208        let dims = self.uniform_u32("n", &[n as u32, 0, 0, 0]);
209        let pipe = self.pipeline("add", ADD_WGSL);
210        self.dispatch(&pipe, &[&ab, &bb, &out, &dims], ((n as u32 + 63) / 64, 1, 1));
211        self.readback(&out, n).await
212    }
213
214    /// SiLU / swish: x · sigmoid(x).
215    pub async fn silu(&self, x: &[f32]) -> Result<Vec<f32>> {
216        let n = x.len();
217        let xb = self.storage("x", x);
218        let out = self.out_buffer(n);
219        let dims = self.uniform_u32("n", &[n as u32, 0, 0, 0]);
220        let pipe = self.pipeline("silu", SILU_WGSL);
221        self.dispatch(&pipe, &[&xb, &out, &dims], ((n as u32 + 63) / 64, 1, 1));
222        self.readback(&out, n).await
223    }
224
225    /// LayerNorm over the last dim `d` for `rows` rows: (x−μ)/√(σ²+eps) · weight + bias.
226    pub async fn layernorm(&self, x: &[f32], weight: &[f32], bias: &[f32], rows: u32, d: u32, eps: f32) -> Result<Vec<f32>> {
227        assert_eq!(x.len(), (rows * d) as usize);
228        assert_eq!(weight.len(), d as usize);
229        assert_eq!(bias.len(), d as usize);
230        let (xb, wb, bb) = (self.storage("x", x), self.storage("w", weight), self.storage("b", bias));
231        let out = self.out_buffer((rows * d) as usize);
232        let dims = self.uniform_u32("dims", &[rows, d, eps.to_bits(), 0]);
233        let pipe = self.pipeline("layernorm", LAYERNORM_WGSL);
234        self.dispatch(&pipe, &[&xb, &wb, &bb, &out, &dims], ((rows + 63) / 64, 1, 1));
235        self.readback(&out, (rows * d) as usize).await
236    }
237
238    /// Row-wise softmax over the last dim `d` for `rows` rows (numerically stable).
239    pub async fn softmax(&self, x: &[f32], rows: u32, d: u32) -> Result<Vec<f32>> {
240        assert_eq!(x.len(), (rows * d) as usize);
241        let xb = self.storage("x", x);
242        let out = self.out_buffer((rows * d) as usize);
243        let dims = self.uniform_u32("dims", &[rows, d, 0, 0]);
244        let pipe = self.pipeline("softmax", SOFTMAX_WGSL);
245        self.dispatch(&pipe, &[&xb, &out, &dims], ((rows + 63) / 64, 1, 1));
246        self.readback(&out, (rows * d) as usize).await
247    }
248
249    /// C = scale · (A(m×k) · B(n×k)ᵀ). Row-major; B is [n×k] (not transposed in memory). This is the
250    /// Q·Kᵀ shape for attention, with the 1/√d scale folded in.
251    pub async fn matmul_bt(&self, a: &[f32], b: &[f32], m: u32, n: u32, k: u32, scale: f32) -> Result<Vec<f32>> {
252        assert_eq!(a.len(), (m * k) as usize);
253        assert_eq!(b.len(), (n * k) as usize);
254        let (ab, bb) = (self.storage("a", a), self.storage("b", b));
255        let out = self.out_buffer((m * n) as usize);
256        let dims = self.uniform_u32("dims", &[m, n, k, scale.to_bits()]);
257        let pipe = self.pipeline("matmul_bt", MATMUL_BT_WGSL);
258        self.dispatch(&pipe, &[&ab, &bb, &out, &dims], ((m + 15) / 16, (n + 15) / 16, 1));
259        self.readback(&out, (m * n) as usize).await
260    }
261
262    /// Single-head scaled dot-product attention: softmax(scale · Q·Kᵀ) · V.
263    /// Q[rows_q×d], K[rows_k×d], V[rows_k×dv] → [rows_q×dv]. Composed from matmul_bt + softmax + matmul.
264    pub async fn attention(&self, q: &[f32], k: &[f32], v: &[f32], rows_q: u32, rows_k: u32, d: u32, dv: u32, scale: f32) -> Result<Vec<f32>> {
265        let scores = self.matmul_bt(q, k, rows_q, rows_k, d, scale).await?; // [rows_q × rows_k], scaled
266        let probs = self.softmax(&scores, rows_q, rows_k).await?;
267        self.matmul(&probs, v, rows_q, rows_k, dv).await                    // [rows_q × dv]
268    }
269}
270
271const MATMUL_BT_WGSL: &str = r#"
272@group(0) @binding(0) var<storage, read>       a: array<f32>;
273@group(0) @binding(1) var<storage, read>       b: array<f32>;
274@group(0) @binding(2) var<storage, read_write> out: array<f32>;
275@group(0) @binding(3) var<uniform>             dims: vec4<u32>; // m, n, k, bitcast(scale)
276@compute @workgroup_size(16, 16, 1)
277fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
278    let m = dims.x; let n = dims.y; let k = dims.z; let scale = bitcast<f32>(dims.w);
279    let row = gid.x; let col = gid.y;
280    if (row >= m || col >= n) { return; }
281    var acc: f32 = 0.0;
282    for (var l: u32 = 0u; l < k; l = l + 1u) {
283        acc = acc + a[row * k + l] * b[col * k + l];   // A · Bᵀ
284    }
285    out[row * n + col] = acc * scale;
286}
287"#;
288
289const ADD_WGSL: &str = r#"
290@group(0) @binding(0) var<storage, read>       a: array<f32>;
291@group(0) @binding(1) var<storage, read>       b: array<f32>;
292@group(0) @binding(2) var<storage, read_write> out: array<f32>;
293@group(0) @binding(3) var<uniform>             dims: vec4<u32>; // n
294@compute @workgroup_size(64)
295fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
296    let i = gid.x;
297    if (i >= dims.x) { return; }
298    out[i] = a[i] + b[i];
299}
300"#;
301
302const ADD_BIAS_WGSL: &str = r#"
303@group(0) @binding(0) var<storage, read>       a: array<f32>;
304@group(0) @binding(1) var<storage, read>       bias: array<f32>;
305@group(0) @binding(2) var<storage, read_write> out: array<f32>;
306@group(0) @binding(3) var<uniform>             dims: vec4<u32>; // n, d
307@compute @workgroup_size(64)
308fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
309    let i = gid.x; if (i >= dims.x) { return; }
310    out[i] = a[i] + bias[i % dims.y];
311}
312"#;
313
314const MUL_WGSL: &str = r#"
315@group(0) @binding(0) var<storage, read>       a: array<f32>;
316@group(0) @binding(1) var<storage, read>       b: array<f32>;
317@group(0) @binding(2) var<storage, read_write> out: array<f32>;
318@group(0) @binding(3) var<uniform>             dims: vec4<u32>;
319@compute @workgroup_size(64)
320fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
321    let i = gid.x; if (i >= dims.x) { return; }
322    out[i] = a[i] * b[i];
323}
324"#;
325
326const MUL_SCALAR_WGSL: &str = r#"
327@group(0) @binding(0) var<storage, read>       a: array<f32>;
328@group(0) @binding(1) var<storage, read>       s: array<f32>;
329@group(0) @binding(2) var<storage, read_write> out: array<f32>;
330@group(0) @binding(3) var<uniform>             dims: vec4<u32>;
331@compute @workgroup_size(64)
332fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
333    let i = gid.x; if (i >= dims.x) { return; }
334    out[i] = a[i] * s[0];
335}
336"#;
337
338const TRANSPOSE_WGSL: &str = r#"
339@group(0) @binding(0) var<storage, read>       x: array<f32>;
340@group(0) @binding(1) var<storage, read_write> out: array<f32>;
341@group(0) @binding(2) var<uniform>             dims: vec4<u32>; // rows, cols
342@compute @workgroup_size(16, 16, 1)
343fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
344    let rows = dims.x; let cols = dims.y;
345    let r = gid.x; let c = gid.y;
346    if (r >= rows || c >= cols) { return; }
347    out[c * rows + r] = x[r * cols + c];   // [rows,cols] → [cols,rows]
348}
349"#;
350
351const RELU_WGSL: &str = r#"
352@group(0) @binding(0) var<storage, read>       x: array<f32>;
353@group(0) @binding(1) var<storage, read_write> out: array<f32>;
354@group(0) @binding(2) var<uniform>             dims: vec4<u32>; // n
355@compute @workgroup_size(64)
356fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
357    let i = gid.x;
358    if (i >= dims.x) { return; }
359    out[i] = max(x[i], 0.0);
360}
361"#;
362
363const SILU_WGSL: &str = r#"
364@group(0) @binding(0) var<storage, read>       x: array<f32>;
365@group(0) @binding(1) var<storage, read_write> out: array<f32>;
366@group(0) @binding(2) var<uniform>             dims: vec4<u32>; // n
367@compute @workgroup_size(64)
368fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
369    let i = gid.x;
370    if (i >= dims.x) { return; }
371    let v = x[i];
372    out[i] = v / (1.0 + exp(-v));
373}
374"#;
375
376const GATHER_WGSL: &str = r#"
377@group(0) @binding(0) var<storage, read>       data: array<f32>;
378@group(0) @binding(1) var<storage, read>       idx:  array<u32>;
379@group(0) @binding(2) var<storage, read_write> out:  array<f32>;
380@group(0) @binding(3) var<uniform>             dims: vec4<u32>; // n, d
381@compute @workgroup_size(64)
382fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
383    let n = dims.x; let d = dims.y;
384    let t = gid.x; if (t >= n * d) { return; }
385    let i = t / d; let j = t % d;
386    out[i * d + j] = data[idx[i] * d + j];
387}
388"#;
389
390const SIGMOID_WGSL: &str = r#"
391@group(0) @binding(0) var<storage, read>       x: array<f32>;
392@group(0) @binding(1) var<storage, read_write> out: array<f32>;
393@group(0) @binding(2) var<uniform>             dims: vec4<u32>; // n
394@compute @workgroup_size(64)
395fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
396    let i = gid.x; if (i >= dims.x) { return; }
397    out[i] = 1.0 / (1.0 + exp(-x[i]));
398}
399"#;
400
401const SQRT_WGSL: &str = r#"
402@group(0) @binding(0) var<storage, read>       x: array<f32>;
403@group(0) @binding(1) var<storage, read_write> out: array<f32>;
404@group(0) @binding(2) var<uniform>             dims: vec4<u32>; // n
405@compute @workgroup_size(64)
406fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
407    let i = gid.x; if (i >= dims.x) { return; }
408    out[i] = sqrt(x[i]);
409}
410"#;
411
412// Exact erf-based GELU: 0.5·x·(1+erf(x/√2)). WGSL has no erf, so use a high-accuracy
413// Abramowitz-Stegun 7.1.26 rational approximation (|err| < 1.5e-7) — matches ONNX Gelu (erf).
414const GELU_WGSL: &str = r#"
415@group(0) @binding(0) var<storage, read>       x: array<f32>;
416@group(0) @binding(1) var<storage, read_write> out: array<f32>;
417@group(0) @binding(2) var<uniform>             dims: vec4<u32>; // n
418fn erf(z: f32) -> f32 {
419    let s = sign(z); let a = abs(z);
420    let t = 1.0 / (1.0 + 0.3275911 * a);
421    let y = 1.0 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * exp(-a * a);
422    return s * y;
423}
424@compute @workgroup_size(64)
425fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
426    let i = gid.x; if (i >= dims.x) { return; }
427    let v = x[i];
428    out[i] = 0.5 * v * (1.0 + erf(v * 0.7071067811865476));
429}
430"#;
431
432const SUB_WGSL: &str = r#"
433@group(0) @binding(0) var<storage, read>       a: array<f32>;
434@group(0) @binding(1) var<storage, read>       b: array<f32>;
435@group(0) @binding(2) var<storage, read_write> out: array<f32>;
436@group(0) @binding(3) var<uniform>             dims: vec4<u32>;
437@compute @workgroup_size(64)
438fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
439    let i = gid.x; if (i >= dims.x) { return; }
440    out[i] = a[i] - b[i];
441}
442"#;
443
444const DIV_WGSL: &str = r#"
445@group(0) @binding(0) var<storage, read>       a: array<f32>;
446@group(0) @binding(1) var<storage, read>       b: array<f32>;
447@group(0) @binding(2) var<storage, read_write> out: array<f32>;
448@group(0) @binding(3) var<uniform>             dims: vec4<u32>;
449@compute @workgroup_size(64)
450fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
451    let i = gid.x; if (i >= dims.x) { return; }
452    out[i] = a[i] / b[i];
453}
454"#;
455
456const ROPE_WGSL: &str = r#"
457@group(0) @binding(0) var<storage, read>       x: array<f32>;
458@group(0) @binding(1) var<storage, read_write> out: array<f32>;
459@group(0) @binding(2) var<uniform>             dims: vec4<u32>; // T, H, dh, bitcast(base)
460@group(0) @binding(3) var<uniform>             rmeta: vec4<u32>; // pos_offset, _, _, _
461@compute @workgroup_size(64)
462fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
463    let t = dims.x; let h = dims.y; let dh = dims.z; let base = bitcast<f32>(dims.w);
464    let id = gid.x; if (id >= t * h) { return; }
465    let i = id / h; let head = id % h;
466    let half = dh / 2u;
467    let o = (i * h + head) * dh;
468    let lb = log(base);
469    for (var c: u32 = 0u; c < half; c = c + 1u) {
470        let inv = exp(-2.0 * f32(c) / f32(dh) * lb);
471        let ang = f32(i + rmeta.x) * inv;
472        let cs = cos(ang); let sn = sin(ang);
473        let x1 = x[o + c]; let x2 = x[o + c + half];
474        out[o + c] = x1 * cs - x2 * sn;
475        out[o + c + half] = x2 * cs + x1 * sn;
476    }
477}
478"#;
479
480const MHA_CAUSAL_WGSL: &str = r#"
481@group(0) @binding(0) var<storage, read>       q: array<f32>;
482@group(0) @binding(1) var<storage, read>       k: array<f32>;
483@group(0) @binding(2) var<storage, read>       v: array<f32>;
484@group(0) @binding(3) var<storage, read_write> out: array<f32>;
485@group(0) @binding(4) var<uniform>             dims: vec4<u32>; // T, Hq, dh, bitcast(scale)
486@group(0) @binding(5) var<uniform>             gqa:  vec4<u32>; // Hkv, _, _, _
487@compute @workgroup_size(64)
488fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
489    let t = dims.x; let hq = dims.y; let dh = dims.z; let scale = bitcast<f32>(dims.w);
490    let hkv = gqa.x;
491    let id = gid.x; if (id >= t * hq) { return; }
492    let i = id / hq; let head = id % hq;
493    let kvhead = head / (hq / hkv);                       // GQA: query head → shared kv head
494    let qo = (i * hq + head) * dh;
495    var acc: array<f32, 128>;
496    for (var c: u32 = 0u; c < dh; c = c + 1u) { acc[c] = 0.0; }
497    var m: f32 = -3.0e38; var l: f32 = 0.0;
498    for (var j: u32 = 0u; j <= i; j = j + 1u) {           // causal: attend to keys 0..=i
499        let ko = (j * hkv + kvhead) * dh;
500        var s: f32 = 0.0;
501        for (var c: u32 = 0u; c < dh; c = c + 1u) { s = s + q[qo + c] * k[ko + c]; }
502        s = s * scale;
503        let mnew = max(m, s);
504        let corr = exp(m - mnew);
505        let p = exp(s - mnew);
506        l = l * corr + p;
507        for (var c: u32 = 0u; c < dh; c = c + 1u) { acc[c] = acc[c] * corr + p * v[ko + c]; }
508        m = mnew;
509    }
510    for (var c: u32 = 0u; c < dh; c = c + 1u) { out[qo + c] = acc[c] / l; }
511}
512"#;
513
514const MHA_DECODE_WGSL: &str = r#"
515@group(0) @binding(0) var<storage, read>       q: array<f32>;   // [1, H*dh]
516@group(0) @binding(1) var<storage, read>       k: array<f32>;   // [S, H*dh]
517@group(0) @binding(2) var<storage, read>       v: array<f32>;   // [S, H*dh]
518@group(0) @binding(3) var<storage, read_write> out: array<f32>; // [1, H*dh]
519@group(0) @binding(4) var<uniform>             dims: vec4<u32>; // S, Hq, dh, bitcast(scale)
520@group(0) @binding(5) var<uniform>             gqa:  vec4<u32>; // Hkv, _, _, _
521@compute @workgroup_size(64)
522fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
523    let s = dims.x; let hq = dims.y; let dh = dims.z; let scale = bitcast<f32>(dims.w);
524    let hkv = gqa.x;
525    let head = gid.x; if (head >= hq) { return; }
526    let kvhead = head / (hq / hkv);           // GQA: query head → shared kv head
527    let qo = head * dh;
528    var acc: array<f32, 128>;
529    for (var c: u32 = 0u; c < dh; c = c + 1u) { acc[c] = 0.0; }
530    var m: f32 = -3.0e38; var l: f32 = 0.0;
531    for (var j: u32 = 0u; j < s; j = j + 1u) {
532        let ko = (j * hkv + kvhead) * dh;
533        var sc: f32 = 0.0;
534        for (var c: u32 = 0u; c < dh; c = c + 1u) { sc = sc + q[qo + c] * k[ko + c]; }
535        sc = sc * scale;
536        let mnew = max(m, sc);
537        let corr = exp(m - mnew);
538        let p = exp(sc - mnew);
539        l = l * corr + p;
540        for (var c: u32 = 0u; c < dh; c = c + 1u) { acc[c] = acc[c] * corr + p * v[ko + c]; }
541        m = mnew;
542    }
543    for (var c: u32 = 0u; c < dh; c = c + 1u) { out[qo + c] = acc[c] / l; }
544}
545"#;
546
547const RMSNORM_WGSL: &str = r#"
548@group(0) @binding(0) var<storage, read>       x: array<f32>;
549@group(0) @binding(1) var<storage, read>       weight: array<f32>;
550@group(0) @binding(2) var<storage, read_write> out: array<f32>;
551@group(0) @binding(3) var<uniform>             dims: vec4<u32>; // rows, d, bitcast(eps), _
552@compute @workgroup_size(64)
553fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
554    let row = gid.x;
555    let rows = dims.x; let d = dims.y; let eps = bitcast<f32>(dims.z);
556    if (row >= rows) { return; }
557    let base = row * d;
558    var ms: f32 = 0.0;
559    for (var j: u32 = 0u; j < d; j = j + 1u) { let v = x[base + j]; ms = ms + v * v; }
560    ms = ms / f32(d);
561    let inv = 1.0 / sqrt(ms + eps);
562    for (var j: u32 = 0u; j < d; j = j + 1u) { out[base + j] = x[base + j] * inv * weight[j]; }
563}
564"#;
565
566const LAYERNORM_WGSL: &str = r#"
567@group(0) @binding(0) var<storage, read>       x: array<f32>;
568@group(0) @binding(1) var<storage, read>       weight: array<f32>;
569@group(0) @binding(2) var<storage, read>       bias: array<f32>;
570@group(0) @binding(3) var<storage, read_write> out: array<f32>;
571@group(0) @binding(4) var<uniform>             dims: vec4<u32>; // rows, d, bitcast(eps), _
572@compute @workgroup_size(64)
573fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
574    let row = gid.x;
575    let rows = dims.x; let d = dims.y; let eps = bitcast<f32>(dims.z);
576    if (row >= rows) { return; }
577    let base = row * d;
578    var mean: f32 = 0.0;
579    for (var j: u32 = 0u; j < d; j = j + 1u) { mean = mean + x[base + j]; }
580    mean = mean / f32(d);
581    var vari: f32 = 0.0;
582    for (var j: u32 = 0u; j < d; j = j + 1u) { let c = x[base + j] - mean; vari = vari + c * c; }
583    vari = vari / f32(d);
584    let inv = 1.0 / sqrt(vari + eps);
585    for (var j: u32 = 0u; j < d; j = j + 1u) {
586        out[base + j] = (x[base + j] - mean) * inv * weight[j] + bias[j];
587    }
588}
589"#;
590
591const SOFTMAX_WGSL: &str = r#"
592@group(0) @binding(0) var<storage, read>       x: array<f32>;
593@group(0) @binding(1) var<storage, read_write> out: array<f32>;
594@group(0) @binding(2) var<uniform>             dims: vec4<u32>; // rows, d
595@compute @workgroup_size(64)
596fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
597    let row = gid.x;
598    let rows = dims.x; let d = dims.y;
599    if (row >= rows) { return; }
600    let base = row * d;
601    var mx: f32 = x[base];
602    for (var j: u32 = 1u; j < d; j = j + 1u) { mx = max(mx, x[base + j]); }
603    var sum: f32 = 0.0;
604    for (var j: u32 = 0u; j < d; j = j + 1u) { let e = exp(x[base + j] - mx); out[base + j] = e; sum = sum + e; }
605    let inv = 1.0 / sum;
606    for (var j: u32 = 0u; j < d; j = j + 1u) { out[base + j] = out[base + j] * inv; }
607}
608"#;
609
610/// Plain-Rust CPU references — the source of truth every kernel is validated against.
611pub mod cpu {
612    pub fn add(a: &[f32], b: &[f32]) -> Vec<f32> { a.iter().zip(b).map(|(x, y)| x + y).collect() }
613    pub fn silu(x: &[f32]) -> Vec<f32> { x.iter().map(|&v| v / (1.0 + (-v).exp())).collect() }
614    pub fn relu(x: &[f32]) -> Vec<f32> { x.iter().map(|&v| v.max(0.0)).collect() }
615    pub fn sigmoid(x: &[f32]) -> Vec<f32> { x.iter().map(|&v| 1.0 / (1.0 + (-v).exp())).collect() }
616    pub fn sqrt(x: &[f32]) -> Vec<f32> { x.iter().map(|&v| v.sqrt()).collect() }
617    pub fn sub(a: &[f32], b: &[f32]) -> Vec<f32> { a.iter().zip(b).map(|(x, y)| x - y).collect() }
618    pub fn div(a: &[f32], b: &[f32]) -> Vec<f32> { a.iter().zip(b).map(|(x, y)| x / y).collect() }
619    pub fn gelu(x: &[f32]) -> Vec<f32> {
620        x.iter().map(|&v| 0.5 * v * (1.0 + libm_erf(v * std::f32::consts::FRAC_1_SQRT_2))).collect()
621    }
622    fn libm_erf(z: f32) -> f32 {
623        let s = z.signum(); let a = z.abs();
624        let t = 1.0 / (1.0 + 0.3275911 * a);
625        let y = 1.0 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * (-a * a).exp();
626        s * y
627    }
628    pub fn layernorm(x: &[f32], w: &[f32], b: &[f32], rows: usize, d: usize, eps: f32) -> Vec<f32> {
629        let mut o = vec![0.0f32; rows * d];
630        for r in 0..rows {
631            let base = r * d;
632            let mean = x[base..base + d].iter().sum::<f32>() / d as f32;
633            let var = x[base..base + d].iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / d as f32;
634            let inv = 1.0 / (var + eps).sqrt();
635            for j in 0..d { o[base + j] = (x[base + j] - mean) * inv * w[j] + b[j]; }
636        }
637        o
638    }
639    pub fn rmsnorm(x: &[f32], w: &[f32], rows: usize, d: usize, eps: f32) -> Vec<f32> {
640        let mut o = vec![0.0f32; rows * d];
641        for r in 0..rows {
642            let base = r * d;
643            let ms = x[base..base + d].iter().map(|v| v * v).sum::<f32>() / d as f32;
644            let inv = 1.0 / (ms + eps).sqrt();
645            for j in 0..d { o[base + j] = x[base + j] * inv * w[j]; }
646        }
647        o
648    }
649    pub fn softmax(x: &[f32], rows: usize, d: usize) -> Vec<f32> {
650        let mut o = vec![0.0f32; rows * d];
651        for r in 0..rows {
652            let base = r * d;
653            let mx = x[base..base + d].iter().cloned().fold(f32::NEG_INFINITY, f32::max);
654            let mut sum = 0.0f32;
655            for j in 0..d { let e = (x[base + j] - mx).exp(); o[base + j] = e; sum += e; }
656            for j in 0..d { o[base + j] /= sum; }
657        }
658        o
659    }
660    pub fn matmul_bt(a: &[f32], b: &[f32], m: usize, n: usize, k: usize, scale: f32) -> Vec<f32> {
661        let mut c = vec![0.0f32; m * n];
662        for i in 0..m {
663            for j in 0..n {
664                let mut acc = 0.0f32;
665                for l in 0..k { acc += a[i * k + l] * b[j * k + l]; }
666                c[i * n + j] = acc * scale;
667            }
668        }
669        c
670    }
671    pub fn rope(x: &[f32], t: usize, h: usize, dh: usize, base: f32) -> Vec<f32> {
672        let mut o = x.to_vec();
673        let half = dh / 2;
674        for i in 0..t {
675            for head in 0..h {
676                let off = (i * h + head) * dh;
677                for c in 0..half {
678                    let inv = (-2.0 * c as f32 / dh as f32 * base.ln()).exp(); // matches the WGSL f32 path
679                    let ang = i as f32 * inv;
680                    let (cs, sn) = (ang.cos(), ang.sin());
681                    let (x1, x2) = (x[off + c], x[off + c + half]);
682                    o[off + c] = x1 * cs - x2 * sn;
683                    o[off + c + half] = x2 * cs + x1 * sn;
684                }
685            }
686        }
687        o
688    }
689    /// Causal attention with grouped-query attention (hq query heads, hkv kv heads). hkv==hq = plain MHA.
690    pub fn mha_causal(q: &[f32], k: &[f32], v: &[f32], t: usize, hq: usize, hkv: usize, dh: usize) -> Vec<f32> {
691        let scale = 1.0 / (dh as f32).sqrt();
692        let mut o = vec![0.0f32; t * hq * dh];
693        for i in 0..t {
694            for head in 0..hq {
695                let kvhead = head / (hq / hkv);
696                let qo = (i * hq + head) * dh;
697                let mut scores = vec![0.0f32; i + 1];
698                for j in 0..=i {
699                    let ko = (j * hkv + kvhead) * dh;
700                    scores[j] = (0..dh).map(|c| q[qo + c] * k[ko + c]).sum::<f32>() * scale;
701                }
702                let mx = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
703                let mut sum = 0.0;
704                for s in scores.iter_mut() { *s = (*s - mx).exp(); sum += *s; }
705                for c in 0..dh {
706                    let mut acc = 0.0;
707                    for j in 0..=i { acc += scores[j] / sum * v[(j * hkv + kvhead) * dh + c]; }
708                    o[qo + c] = acc;
709                }
710            }
711        }
712        o
713    }
714    pub fn attention(q: &[f32], k: &[f32], v: &[f32], rows_q: usize, rows_k: usize, d: usize, dv: usize, scale: f32) -> Vec<f32> {
715        let scores = matmul_bt(q, k, rows_q, rows_k, d, scale);
716        let probs = softmax(&scores, rows_q, rows_k);
717        crate::matmul_cpu(&probs, v, rows_q, rows_k, dv)
718    }
719}