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