Skip to main content

forge/backend/wgpu/
ops.rs

1//! GPU op wrappers: bind tensors, compute grid sizes, dispatch WGSL kernels.
2//! Semantics mirror `backend::cpu` exactly; see the shader sources for the
3//! kernel-side contracts.
4
5use std::sync::Arc;
6
7use super::{WgpuContext, linear_grid};
8use crate::tensor::WgpuStorage;
9
10pub fn alloc_storage(ctx: &Arc<WgpuContext>, numel: usize) -> WgpuStorage {
11    alloc(ctx, numel)
12}
13
14fn alloc(ctx: &Arc<WgpuContext>, numel: usize) -> WgpuStorage {
15    WgpuStorage {
16        ctx: ctx.clone(),
17        buf: Arc::new(ctx.create_pooled(numel * 4)),
18        offset: 0,
19    }
20}
21
22/// An output buffer guaranteed to be all zeros.
23///
24/// `alloc` recycles, and a recycled buffer holds whatever the last op left in
25/// it. Only for kernels that write part of their output and need the remainder
26/// zero — see `unsplit_head`, the sole caller.
27fn alloc_zeroed(ctx: &Arc<WgpuContext>, numel: usize) -> WgpuStorage {
28    WgpuStorage {
29        ctx: ctx.clone(),
30        buf: Arc::new(ctx.create_zeroed(numel * 4)),
31        offset: 0,
32    }
33}
34
35fn bind(s: &WgpuStorage, numel: usize) -> (&wgpu::Buffer, usize, usize) {
36    (&s.buf, s.offset * 4, numel * 4)
37}
38
39pub fn add(a: &WgpuStorage, b: &WgpuStorage, n: usize) -> WgpuStorage {
40    let out = alloc(&a.ctx, n);
41    a.ctx.dispatch(
42        "add",
43        &[n as u32, 0, 0, 0],
44        &[bind(a, n), bind(b, n), bind(&out, n)],
45        linear_grid(n),
46    );
47    out
48}
49
50pub fn gelu(x: &WgpuStorage, n: usize) -> WgpuStorage {
51    let out = alloc(&x.ctx, n);
52    x.ctx.dispatch(
53        "gelu",
54        &[n as u32, 0, 0, 0],
55        &[bind(x, n), bind(&out, n)],
56        linear_grid(n),
57    );
58    out
59}
60
61#[allow(clippy::too_many_arguments)]
62pub fn matmul(
63    a: &WgpuStorage,
64    b: &WgpuStorage,
65    bias: Option<&WgpuStorage>,
66    m: usize,
67    k: usize,
68    n: usize,
69    batch: usize,
70    a_stride: usize,
71    b_stride: usize,
72    trans_a: bool,
73    trans_b: bool,
74    alpha: f32,
75) -> WgpuStorage {
76    let out = alloc(&a.ctx, batch * m * n);
77    matmul_into(
78        &out, a, b, bias, m, k, n, batch, a_stride, b_stride, trans_a, trans_b, alpha, 0, n,
79    );
80    out
81}
82
83/// Compute `n` columns of a wider [m, n_out] output, writing at column
84/// offset `n_off` — used to keep each weight-chunk binding under
85/// max_storage_buffer_binding_size.
86#[allow(clippy::too_many_arguments)]
87pub fn matmul_into(
88    out: &WgpuStorage,
89    a: &WgpuStorage,
90    b: &WgpuStorage,
91    bias: Option<&WgpuStorage>,
92    m: usize,
93    k: usize,
94    n: usize,
95    batch: usize,
96    a_stride: usize,
97    b_stride: usize,
98    trans_a: bool,
99    trans_b: bool,
100    alpha: f32,
101    n_off: usize,
102    n_out: usize,
103) {
104    let ctx = &a.ctx;
105    // The bias binding must exist even when unused; bind a 1-element dummy.
106    let dummy;
107    let bias_binding = match bias {
108        Some(bias) => bind(bias, n),
109        None => {
110            dummy = alloc(ctx, 1);
111            bind(&dummy, 1)
112        }
113    };
114    let a_numel = if a_stride == 0 {
115        m * k
116    } else {
117        batch * a_stride
118    };
119    let b_numel = if b_stride == 0 {
120        // Physical element count of one B; with b_rows the logical (k or n)
121        // undercounts, but an unbatched B is always bound whole below.
122        k * n
123    } else {
124        batch * b_stride
125    };
126    let params = [
127        m as u32,
128        k as u32,
129        n as u32,
130        batch as u32,
131        a_stride as u32,
132        b_stride as u32,
133        trans_b as u32,
134        bias.is_some() as u32,
135        alpha.to_bits(),
136        n_off as u32,
137        n_out as u32,
138        trans_a as u32,
139    ];
140    ctx.dispatch(
141        "matmul",
142        &params,
143        &[
144            bind(a, a_numel),
145            bind(b, b_numel),
146            bias_binding,
147            bind(out, batch * m * n_out),
148        ],
149        (n.div_ceil(16) as u32, m.div_ceil(16) as u32, batch as u32),
150    );
151}
152
153pub fn softmax(
154    x: &WgpuStorage,
155    rows: usize,
156    cols: usize,
157    q_len: usize,
158    causal: bool,
159    off: usize,
160) -> WgpuStorage {
161    let n = rows * cols;
162    let out = alloc(&x.ctx, n);
163    x.ctx.dispatch(
164        "softmax",
165        &[
166            rows as u32,
167            cols as u32,
168            q_len as u32,
169            causal as u32,
170            off as u32,
171            0,
172            0,
173            0,
174        ],
175        &[bind(x, n), bind(&out, n)],
176        (rows as u32, 1, 1),
177    );
178    out
179}
180
181pub fn layernorm(
182    x: &WgpuStorage,
183    gamma: &WgpuStorage,
184    beta: &WgpuStorage,
185    rows: usize,
186    cols: usize,
187    eps: f32,
188) -> WgpuStorage {
189    let n = rows * cols;
190    let out = alloc(&x.ctx, n);
191    x.ctx.dispatch(
192        "layernorm",
193        &[rows as u32, cols as u32, eps.to_bits(), 0],
194        &[
195            bind(x, n),
196            bind(gamma, cols),
197            bind(beta, cols),
198            bind(&out, n),
199        ],
200        (rows as u32, 1, 1),
201    );
202    out
203}
204
205/// Gather from row-chunked embedding tables. `chunks[i]` holds rows
206/// [i * chunk_rows, ...) of the full table; each token is written by exactly
207/// the dispatch whose chunk owns its id.
208#[allow(clippy::too_many_arguments)]
209pub fn embedding(
210    ids: &WgpuStorage,
211    chunks: &[(&WgpuStorage, usize)], // (buffer, rows in this chunk)
212    chunk_rows: usize,
213    wpe: Option<&WgpuStorage>,
214    t: usize,
215    c: usize,
216    n_ctx: usize,
217    pos: usize,
218) -> WgpuStorage {
219    let ctx = &ids.ctx;
220    let n = t * c;
221    let out = alloc(ctx, n);
222    let dummy;
223    let wpe_binding = match wpe {
224        Some(wpe) => bind(wpe, n_ctx * c),
225        None => {
226            dummy = alloc(ctx, 1);
227            bind(&dummy, 1)
228        }
229    };
230    for (i, (chunk, rows)) in chunks.iter().enumerate() {
231        let row_start = i * chunk_rows;
232        ctx.dispatch(
233            "embedding",
234            &[
235                t as u32,
236                c as u32,
237                pos as u32,
238                wpe.is_some() as u32,
239                row_start as u32,
240                (row_start + rows) as u32,
241                0,
242                0,
243            ],
244            &[
245                bind(ids, t),
246                bind(chunk, rows * c),
247                wpe_binding,
248                bind(&out, n),
249            ],
250            linear_grid(n),
251        );
252    }
253    out
254}
255
256/// Append src [h, t, hd] into cache [h, cap, hd] at row offset `len`.
257pub fn kv_append(
258    cache: &WgpuStorage,
259    src: &WgpuStorage,
260    h: usize,
261    t: usize,
262    hd: usize,
263    cap: usize,
264    len: usize,
265) {
266    let n = h * t * hd;
267    src.ctx.dispatch(
268        "kv_append",
269        &[
270            h as u32, t as u32, hd as u32, cap as u32, len as u32, 0, 0, 0,
271        ],
272        &[bind(src, n), bind(cache, h * cap * hd)],
273        linear_grid(n),
274    );
275}
276
277pub fn split_heads(
278    qkv: &WgpuStorage,
279    t: usize,
280    c: usize,
281    h: usize,
282) -> (WgpuStorage, WgpuStorage, WgpuStorage) {
283    let ctx = &qkv.ctx;
284    let n = t * c;
285    let (q, k, v) = (alloc(ctx, n), alloc(ctx, n), alloc(ctx, n));
286    ctx.dispatch(
287        "split_heads",
288        &[t as u32, c as u32, h as u32, 0],
289        &[bind(qkv, t * 3 * c), bind(&q, n), bind(&k, n), bind(&v, n)],
290        linear_grid(n),
291    );
292    (q, k, v)
293}
294
295pub fn merge_heads(x: &WgpuStorage, t: usize, c: usize, h: usize) -> WgpuStorage {
296    let n = t * c;
297    let out = alloc(&x.ctx, n);
298    x.ctx.dispatch(
299        "merge_heads",
300        &[t as u32, c as u32, h as u32, 0],
301        &[bind(x, n), bind(&out, n)],
302        linear_grid(n),
303    );
304    out
305}
306
307// ---- backward / training kernels (roadmap v4, Stages 8-9) ----
308
309pub fn gelu_bwd(x: &WgpuStorage, dy: &WgpuStorage, n: usize) -> WgpuStorage {
310    let out = alloc(&x.ctx, n);
311    x.ctx.dispatch(
312        "gelu_bwd",
313        &[n as u32, 0, 0, 0],
314        &[bind(x, n), bind(dy, n), bind(&out, n)],
315        linear_grid(n),
316    );
317    out
318}
319
320pub fn softmax_bwd(y: &WgpuStorage, dy: &WgpuStorage, rows: usize, cols: usize) -> WgpuStorage {
321    let n = rows * cols;
322    let out = alloc(&y.ctx, n);
323    y.ctx.dispatch(
324        "softmax_bwd",
325        &[rows as u32, cols as u32, 0, 0],
326        &[bind(y, n), bind(dy, n), bind(&out, n)],
327        (rows as u32, 1, 1),
328    );
329    out
330}
331
332pub fn layernorm_bwd(
333    x: &WgpuStorage,
334    gamma: &WgpuStorage,
335    dy: &WgpuStorage,
336    rows: usize,
337    cols: usize,
338    eps: f32,
339) -> (WgpuStorage, WgpuStorage, WgpuStorage) {
340    let n = rows * cols;
341    let ctx = &x.ctx;
342    let dx = alloc(ctx, n);
343    let stats = alloc(ctx, rows * 2);
344    ctx.dispatch(
345        "layernorm_bwd_dx",
346        &[rows as u32, cols as u32, eps.to_bits(), 0],
347        &[
348            bind(x, n),
349            bind(gamma, cols),
350            bind(dy, n),
351            bind(&dx, n),
352            bind(&stats, rows * 2),
353        ],
354        (rows as u32, 1, 1),
355    );
356    let dgamma = alloc(ctx, cols);
357    let dbeta = alloc(ctx, cols);
358    ctx.dispatch(
359        "layernorm_bwd_dp",
360        &[rows as u32, cols as u32, 0, 0],
361        &[
362            bind(x, n),
363            bind(dy, n),
364            bind(&stats, rows * 2),
365            bind(&dgamma, cols),
366            bind(&dbeta, cols),
367        ],
368        linear_grid(cols),
369    );
370    (dx, dgamma, dbeta)
371}
372
373pub fn sum_rows(x: &WgpuStorage, rows: usize, cols: usize) -> WgpuStorage {
374    let out = alloc(&x.ctx, cols);
375    x.ctx.dispatch(
376        "sum_rows",
377        &[rows as u32, cols as u32, 0, 0],
378        &[bind(x, rows * cols), bind(&out, cols)],
379        linear_grid(cols),
380    );
381    out
382}
383
384/// `dst[ids[r]] += src[r]`, in place (CAS-loop f32 atomics).
385pub fn scatter_add_rows(
386    dst: &WgpuStorage,
387    ids: &WgpuStorage,
388    src: &WgpuStorage,
389    t: usize,
390    c: usize,
391    dst_numel: usize,
392) {
393    src.ctx.dispatch(
394        "scatter_add",
395        &[t as u32, c as u32, 0, 0],
396        &[bind(ids, t), bind(src, t * c), bind(dst, dst_numel)],
397        linear_grid(t * c),
398    );
399}
400
401pub fn gather_nll(probs: &WgpuStorage, ids: &WgpuStorage, rows: usize, cols: usize) -> WgpuStorage {
402    let out = alloc(&probs.ctx, rows);
403    probs.ctx.dispatch(
404        "gather_nll",
405        &[rows as u32, cols as u32, 0, 0],
406        &[bind(probs, rows * cols), bind(ids, rows), bind(&out, rows)],
407        linear_grid(rows),
408    );
409    out
410}
411
412pub fn ce_bwd(
413    probs: &WgpuStorage,
414    ids: &WgpuStorage,
415    rows: usize,
416    cols: usize,
417    scale: f32,
418) -> WgpuStorage {
419    let n = rows * cols;
420    let out = alloc(&probs.ctx, n);
421    probs.ctx.dispatch(
422        "ce_bwd",
423        &[rows as u32, cols as u32, scale.to_bits(), 0],
424        &[bind(probs, n), bind(ids, rows), bind(&out, n)],
425        linear_grid(n),
426    );
427    out
428}
429
430pub fn dropout(x: &WgpuStorage, n: usize, p: f32, scale: f32, seed: u32) -> WgpuStorage {
431    let out = alloc(&x.ctx, n);
432    x.ctx.dispatch(
433        "dropout",
434        &[n as u32, seed, p.to_bits(), scale.to_bits()],
435        &[bind(x, n), bind(&out, n)],
436        linear_grid(n),
437    );
438    out
439}
440
441pub fn unsplit_head(d: &WgpuStorage, t: usize, c: usize, h: usize, which: usize) -> WgpuStorage {
442    let n = t * c;
443    // The kernel writes one third and needs the other two to be zero, so this
444    // is the one op that must not take a recycled buffer. `alloc` here silently
445    // corrupts wte gradients — it did, before `alloc_zeroed` existed.
446    let out = alloc_zeroed(&d.ctx, t * 3 * c);
447    d.ctx.dispatch(
448        "unsplit_heads",
449        &[t as u32, c as u32, h as u32, which as u32],
450        &[bind(d, n), bind(&out, t * 3 * c)],
451        linear_grid(n),
452    );
453    out
454}
455
456pub fn unmerge_heads(dy: &WgpuStorage, t: usize, c: usize, h: usize) -> WgpuStorage {
457    let n = t * c;
458    let out = alloc(&dy.ctx, n);
459    dy.ctx.dispatch(
460        "unmerge_heads",
461        &[t as u32, c as u32, h as u32, 0],
462        &[bind(dy, n), bind(&out, n)],
463        linear_grid(n),
464    );
465    out
466}
467
468/// Per-workgroup partial sums of squares; the caller reads back and sums.
469/// The partial count covers the whole (possibly 2-D) dispatch grid; groups
470/// past the data contribute zeros.
471pub fn sumsq_partials(x: &WgpuStorage, n: usize) -> (WgpuStorage, usize) {
472    let grid = linear_grid(n);
473    let groups = (grid.0 * grid.1) as usize;
474    let out = alloc(&x.ctx, groups);
475    x.ctx.dispatch(
476        "sumsq",
477        &[n as u32, 0, 0, 0],
478        &[bind(x, n), bind(&out, groups)],
479        grid,
480    );
481    (out, groups)
482}
483
484pub fn scale(x: &WgpuStorage, n: usize, alpha: f32) -> WgpuStorage {
485    let out = alloc(&x.ctx, n);
486    x.ctx.dispatch(
487        "scale",
488        &[n as u32, alpha.to_bits(), 0, 0],
489        &[bind(x, n), bind(&out, n)],
490        linear_grid(n),
491    );
492    out
493}
494
495/// AdamW step, updating param/m/v in place.
496#[allow(clippy::too_many_arguments)]
497pub fn adamw(
498    param: &WgpuStorage,
499    grad: &WgpuStorage,
500    m: &WgpuStorage,
501    v: &WgpuStorage,
502    n: usize,
503    lr: f32,
504    beta1: f32,
505    beta2: f32,
506    eps: f32,
507    weight_decay: f32,
508    step: u32,
509) {
510    let bc1 = 1.0 - beta1.powi(step as i32);
511    let bc2 = 1.0 - beta2.powi(step as i32);
512    param.ctx.dispatch(
513        "adamw",
514        &[
515            n as u32,
516            lr.to_bits(),
517            beta1.to_bits(),
518            beta2.to_bits(),
519            eps.to_bits(),
520            weight_decay.to_bits(),
521            bc1.to_bits(),
522            bc2.to_bits(),
523        ],
524        &[bind(grad, n), bind(param, n), bind(m, n), bind(v, n)],
525        linear_grid(n),
526    );
527}