1use crate::backend::{cpu, wgpu as gpu};
6use crate::error::{ForgeError, Result};
7use crate::shape::Shape;
8use crate::tensor::{CpuStorage, Storage, Tensor, WgpuStorage};
9
10fn cpu_f32(t: &Tensor) -> Result<&[f32]> {
11 match t.storage() {
12 Storage::Cpu(CpuStorage::F32(v)) => Ok(v),
13 _ => Err(ForgeError::Device("expected CPU f32 tensor".into())),
14 }
15}
16
17fn cpu_u32(t: &Tensor) -> Result<&[u32]> {
18 match t.storage() {
19 Storage::Cpu(CpuStorage::U32(v)) => Ok(v),
20 _ => Err(ForgeError::Device("expected CPU u32 tensor".into())),
21 }
22}
23
24fn gpu_storage(t: &Tensor) -> Result<&WgpuStorage> {
25 match t.storage() {
26 Storage::Wgpu(s) => Ok(s),
27 _ => Err(ForgeError::Device("expected WGPU tensor".into())),
28 }
29}
30
31fn same_device(tensors: &[&Tensor]) -> Result<()> {
32 let first = tensors[0].device();
33 for t in &tensors[1..] {
34 if !first.same_as(&t.device()) {
35 return Err(ForgeError::Device(format!(
36 "tensors on different devices: {} vs {}",
37 first.describe(),
38 t.device().describe()
39 )));
40 }
41 }
42 Ok(())
43}
44
45fn f32_tensor(storage: Storage, shape: Shape) -> Tensor {
46 Tensor {
47 storage,
48 shape,
49 dtype: crate::dtype::DType::F32,
50 }
51}
52
53pub fn add(a: &Tensor, b: &Tensor) -> Result<Tensor> {
55 same_device(&[a, b])?;
56 if a.shape() != b.shape() {
57 return Err(ForgeError::Shape(format!(
58 "add shape mismatch: {} vs {}",
59 a.shape(),
60 b.shape()
61 )));
62 }
63 let n = a.shape().numel();
64 let storage = match a.storage() {
65 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(cpu::add(cpu_f32(a)?, cpu_f32(b)?).into())),
66 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::add(gpu_storage(a)?, gpu_storage(b)?, n)),
67 };
68 Ok(f32_tensor(storage, a.shape().clone()))
69}
70
71pub fn gelu(x: &Tensor) -> Result<Tensor> {
73 let n = x.shape().numel();
74 let storage = match x.storage() {
75 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(cpu::gelu(cpu_f32(x)?).into())),
76 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::gelu(gpu_storage(x)?, n)),
77 };
78 Ok(f32_tensor(storage, x.shape().clone()))
79}
80
81#[derive(Clone, Copy)]
82pub struct MatmulSpec {
83 pub trans_a: bool,
86 pub trans_b: bool,
88 pub alpha: f32,
90 pub b_rows: Option<usize>,
94}
95
96impl Default for MatmulSpec {
97 fn default() -> Self {
98 MatmulSpec {
99 trans_a: false,
100 trans_b: false,
101 alpha: 1.0,
102 b_rows: None,
103 }
104 }
105}
106
107pub fn matmul(a: &Tensor, b: &Tensor, bias: Option<&Tensor>, spec: MatmulSpec) -> Result<Tensor> {
113 same_device(&[a, b])?;
114 if let Some(bias) = bias {
115 same_device(&[a, bias])?;
116 }
117 let (batch_a, ad0, ad1) = rank23_dims(a, "A")?;
118 let (m, k) = if spec.trans_a { (ad1, ad0) } else { (ad0, ad1) };
119 let (batch_b, bd0_phys, bd1) = rank23_dims(b, "B")?;
120 let bd0 = match spec.b_rows {
121 Some(rows) if rows <= bd0_phys => rows,
122 Some(rows) => {
123 return Err(ForgeError::Shape(format!(
124 "matmul b_rows {rows} exceeds physical rows {bd0_phys}"
125 )));
126 }
127 None => bd0_phys,
128 };
129 let (bk, n) = if spec.trans_b { (bd1, bd0) } else { (bd0, bd1) };
130 if bk != k {
131 return Err(ForgeError::Shape(format!(
132 "matmul inner dim mismatch: A {} vs B {}",
133 a.shape(),
134 b.shape()
135 )));
136 }
137 let batch = match (batch_a, batch_b) {
138 (None, None) => 1,
139 (Some(x), None) | (None, Some(x)) => x,
140 (Some(x), Some(y)) if x == y => x,
141 (Some(x), Some(y)) => {
142 return Err(ForgeError::Shape(format!(
143 "matmul batch mismatch: {x} vs {y}"
144 )));
145 }
146 };
147 let a_stride = if batch_a.is_some() { ad0 * ad1 } else { 0 };
149 let b_stride = if batch_b.is_some() { bd0_phys * bd1 } else { 0 };
150 if let Some(bias) = bias
151 && bias.shape().numel() != n
152 {
153 return Err(ForgeError::Shape(format!(
154 "bias length {} != n {n}",
155 bias.shape().numel()
156 )));
157 }
158 let out_shape = if batch_a.is_some() || batch_b.is_some() {
159 Shape::new(&[batch, m, n])
160 } else {
161 Shape::new(&[m, n])
162 };
163 let storage = match a.storage() {
164 Storage::Cpu(_) => {
165 let bias = bias.map(cpu_f32).transpose()?;
166 Storage::Cpu(CpuStorage::F32(
167 cpu::matmul(
168 cpu_f32(a)?,
169 cpu_f32(b)?,
170 bias,
171 m,
172 k,
173 n,
174 batch,
175 a_stride,
176 b_stride,
177 spec.trans_a,
178 spec.trans_b,
179 spec.alpha,
180 )
181 .into(),
182 ))
183 }
184 Storage::Wgpu(_) => {
185 let bias = bias.map(gpu_storage).transpose()?;
186 Storage::Wgpu(gpu::ops::matmul(
187 gpu_storage(a)?,
188 gpu_storage(b)?,
189 bias,
190 m,
191 k,
192 n,
193 batch,
194 a_stride,
195 b_stride,
196 spec.trans_a,
197 spec.trans_b,
198 spec.alpha,
199 ))
200 }
201 };
202 Ok(f32_tensor(storage, out_shape))
203}
204
205fn rank23_dims(t: &Tensor, which: &str) -> Result<(Option<usize>, usize, usize)> {
206 match t.shape().dims() {
207 [d0, d1] => Ok((None, *d0, *d1)),
208 [bb, d0, d1] => Ok((Some(*bb), *d0, *d1)),
209 d => Err(ForgeError::Shape(format!(
210 "matmul {which} rank {} unsupported",
211 d.len()
212 ))),
213 }
214}
215
216pub fn softmax(x: &Tensor, causal: bool, off: usize) -> Result<Tensor> {
220 let dims = x.shape().dims();
221 if dims.is_empty() {
222 return Err(ForgeError::Shape("softmax on scalar".into()));
223 }
224 let cols = dims[dims.len() - 1];
225 let rows = x.shape().numel() / cols;
226 let q_len = if dims.len() >= 2 {
227 dims[dims.len() - 2]
228 } else {
229 1
230 };
231 let storage = match x.storage() {
232 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
233 cpu::softmax(cpu_f32(x)?, rows, cols, q_len, causal, off).into(),
234 )),
235 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::softmax(
236 gpu_storage(x)?,
237 rows,
238 cols,
239 q_len,
240 causal,
241 off,
242 )),
243 };
244 Ok(f32_tensor(storage, x.shape().clone()))
245}
246
247pub fn layernorm(x: &Tensor, gamma: &Tensor, beta: &Tensor, eps: f32) -> Result<Tensor> {
249 same_device(&[x, gamma, beta])?;
250 let dims = x.shape().dims();
251 let cols = dims[dims.len() - 1];
252 let rows = x.shape().numel() / cols;
253 if gamma.shape().numel() != cols || beta.shape().numel() != cols {
254 return Err(ForgeError::Shape(
255 "layernorm gamma/beta length mismatch".into(),
256 ));
257 }
258 let storage = match x.storage() {
259 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
260 cpu::layernorm(
261 cpu_f32(x)?,
262 cpu_f32(gamma)?,
263 cpu_f32(beta)?,
264 rows,
265 cols,
266 eps,
267 )
268 .into(),
269 )),
270 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::layernorm(
271 gpu_storage(x)?,
272 gpu_storage(gamma)?,
273 gpu_storage(beta)?,
274 rows,
275 cols,
276 eps,
277 )),
278 };
279 Ok(f32_tensor(storage, x.shape().clone()))
280}
281
282pub fn embedding(ids: &Tensor, wte: &Tensor, wpe: Option<&Tensor>, pos: usize) -> Result<Tensor> {
284 let chunk_rows = wte
285 .shape()
286 .dims()
287 .first()
288 .copied()
289 .ok_or_else(|| ForgeError::Shape("wte must be rank 2".into()))?;
290 embedding_chunked(ids, std::slice::from_ref(wte), chunk_rows, wpe, pos)
291}
292
293pub fn embedding_chunked(
298 ids: &Tensor,
299 chunks: &[Tensor],
300 chunk_rows: usize,
301 wpe: Option<&Tensor>,
302 pos: usize,
303) -> Result<Tensor> {
304 if chunks.is_empty() {
305 return Err(ForgeError::Shape(
306 "embedding needs at least one chunk".into(),
307 ));
308 }
309 same_device(&[ids, &chunks[0]])?;
310 let t = ids.shape().numel();
311 let c = match chunks[0].shape().dims() {
312 [_, c] => *c,
313 _ => return Err(ForgeError::Shape("wte chunks must be rank 2".into())),
314 };
315 for (i, ch) in chunks.iter().enumerate() {
316 match ch.shape().dims() {
317 [r, cc] if *cc == c && (*r == chunk_rows || i + 1 == chunks.len()) => {}
318 _ => return Err(ForgeError::Shape("inconsistent wte chunk shapes".into())),
319 }
320 }
321 let n_ctx = match wpe {
322 Some(wpe) => match wpe.shape().dims() {
323 [n, wc] if *wc == c => *n,
324 _ => return Err(ForgeError::Shape("wpe must be [n_ctx, c]".into())),
325 },
326 None => 0,
327 };
328 if let Some(wpe) = wpe {
329 same_device(&[&chunks[0], wpe])?;
330 if t + pos > n_ctx {
331 return Err(ForgeError::Shape(format!(
332 "sequence {t}+{pos} exceeds context length {n_ctx}"
333 )));
334 }
335 }
336 let storage = match ids.storage() {
337 Storage::Cpu(_) => {
338 let ids = cpu_u32(ids)?;
339 let wpe = wpe.map(cpu_f32).transpose()?;
340 let mut out = vec![0.0f32; t * c];
341 for (tt, &id) in ids.iter().enumerate() {
342 let (chunk_i, row) = ((id as usize) / chunk_rows, (id as usize) % chunk_rows);
343 let table = cpu_f32(&chunks[chunk_i])?;
344 let dst = &mut out[tt * c..(tt + 1) * c];
345 dst.copy_from_slice(&table[row * c..(row + 1) * c]);
346 if let Some(wpe) = wpe {
347 for (d, &pv) in dst.iter_mut().zip(&wpe[(tt + pos) * c..(tt + pos + 1) * c]) {
348 *d += pv;
349 }
350 }
351 }
352 Storage::Cpu(CpuStorage::F32(out.into()))
353 }
354 Storage::Wgpu(_) => {
355 let wpe = wpe.map(gpu_storage).transpose()?;
356 let gpu_chunks: Vec<(&crate::tensor::WgpuStorage, usize)> = chunks
357 .iter()
358 .map(|ch| Ok((gpu_storage(ch)?, ch.shape().dim(0))))
359 .collect::<Result<_>>()?;
360 Storage::Wgpu(gpu::ops::embedding(
361 gpu_storage(ids)?,
362 &gpu_chunks,
363 chunk_rows,
364 wpe,
365 t,
366 c,
367 n_ctx,
368 pos,
369 ))
370 }
371 };
372 Ok(f32_tensor(storage, Shape::new(&[t, c])))
373}
374
375pub fn matmul_chunked_transb(a: &Tensor, chunks: &[Tensor], alpha: f32) -> Result<Tensor> {
379 if chunks.is_empty() {
380 return Err(ForgeError::Shape("matmul needs at least one chunk".into()));
381 }
382 same_device(&[a, &chunks[0]])?;
383 let (m, k) = match a.shape().dims() {
384 [m, k] => (*m, *k),
385 _ => return Err(ForgeError::Shape("chunked matmul A must be rank 2".into())),
386 };
387 let mut n_total = 0usize;
388 for ch in chunks {
389 match ch.shape().dims() {
390 [n_i, kk] if *kk == k => n_total += n_i,
391 _ => {
392 return Err(ForgeError::Shape(format!(
393 "chunk shape {} incompatible with k={k}",
394 ch.shape()
395 )));
396 }
397 }
398 }
399 let out_shape = Shape::new(&[m, n_total]);
400 match a.storage() {
401 Storage::Cpu(_) => {
402 let a_data = cpu_f32(a)?;
403 let mut out = vec![0.0f32; m * n_total];
404 let mut n_off = 0usize;
405 for ch in chunks {
406 let n_i = ch.shape().dim(0);
407 let part = cpu::matmul(
408 a_data,
409 cpu_f32(ch)?,
410 None,
411 m,
412 k,
413 n_i,
414 1,
415 0,
416 0,
417 false,
418 true,
419 alpha,
420 );
421 for row in 0..m {
422 out[row * n_total + n_off..row * n_total + n_off + n_i]
423 .copy_from_slice(&part[row * n_i..(row + 1) * n_i]);
424 }
425 n_off += n_i;
426 }
427 Ok(f32_tensor(
428 Storage::Cpu(CpuStorage::F32(out.into())),
429 out_shape,
430 ))
431 }
432 Storage::Wgpu(s) => {
433 let out = gpu::ops::alloc_storage(&s.ctx, m * n_total);
434 let mut n_off = 0usize;
435 for ch in chunks {
436 let n_i = ch.shape().dim(0);
437 gpu::ops::matmul_into(
438 &out,
439 gpu_storage(a)?,
440 gpu_storage(ch)?,
441 None,
442 m,
443 k,
444 n_i,
445 1,
446 0,
447 0,
448 false,
449 true,
450 alpha,
451 n_off,
452 n_total,
453 );
454 n_off += n_i;
455 }
456 Ok(f32_tensor(Storage::Wgpu(out), out_shape))
457 }
458 }
459}
460
461fn last_dim_rows(x: &Tensor) -> Result<(usize, usize)> {
464 let dims = x.shape().dims();
465 if dims.is_empty() {
466 return Err(ForgeError::Shape("op needs rank >= 1".into()));
467 }
468 let cols = dims[dims.len() - 1];
469 Ok((x.shape().numel() / cols, cols))
470}
471
472pub fn gelu_bwd(x: &Tensor, dy: &Tensor) -> Result<Tensor> {
474 same_device(&[x, dy])?;
475 if x.shape() != dy.shape() {
476 return Err(ForgeError::Shape("gelu_bwd shape mismatch".into()));
477 }
478 let n = x.shape().numel();
479 let storage = match x.storage() {
480 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
481 cpu::gelu_bwd(cpu_f32(x)?, cpu_f32(dy)?).into(),
482 )),
483 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::gelu_bwd(gpu_storage(x)?, gpu_storage(dy)?, n)),
484 };
485 Ok(f32_tensor(storage, x.shape().clone()))
486}
487
488pub fn softmax_bwd(y: &Tensor, dy: &Tensor) -> Result<Tensor> {
492 same_device(&[y, dy])?;
493 if y.shape() != dy.shape() {
494 return Err(ForgeError::Shape("softmax_bwd shape mismatch".into()));
495 }
496 let (rows, cols) = last_dim_rows(y)?;
497 let storage = match y.storage() {
498 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
499 cpu::softmax_bwd(cpu_f32(y)?, cpu_f32(dy)?, rows, cols).into(),
500 )),
501 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::softmax_bwd(
502 gpu_storage(y)?,
503 gpu_storage(dy)?,
504 rows,
505 cols,
506 )),
507 };
508 Ok(f32_tensor(storage, y.shape().clone()))
509}
510
511pub fn layernorm_bwd(
513 x: &Tensor,
514 gamma: &Tensor,
515 dy: &Tensor,
516 eps: f32,
517) -> Result<(Tensor, Tensor, Tensor)> {
518 same_device(&[x, gamma, dy])?;
519 if x.shape() != dy.shape() {
520 return Err(ForgeError::Shape("layernorm_bwd shape mismatch".into()));
521 }
522 let (rows, cols) = last_dim_rows(x)?;
523 if gamma.shape().numel() != cols {
524 return Err(ForgeError::Shape("layernorm_bwd gamma length".into()));
525 }
526 let pshape = gamma.shape().clone();
527 match x.storage() {
528 Storage::Cpu(_) => {
529 let dx =
530 cpu::layernorm_bwd_dx(cpu_f32(x)?, cpu_f32(gamma)?, cpu_f32(dy)?, rows, cols, eps);
531 let (dg, db) = cpu::layernorm_bwd_dparams(cpu_f32(x)?, cpu_f32(dy)?, rows, cols, eps);
532 Ok((
533 f32_tensor(Storage::Cpu(CpuStorage::F32(dx.into())), x.shape().clone()),
534 f32_tensor(Storage::Cpu(CpuStorage::F32(dg.into())), pshape.clone()),
535 f32_tensor(Storage::Cpu(CpuStorage::F32(db.into())), pshape),
536 ))
537 }
538 Storage::Wgpu(_) => {
539 let (dx, dg, db) = gpu::ops::layernorm_bwd(
540 gpu_storage(x)?,
541 gpu_storage(gamma)?,
542 gpu_storage(dy)?,
543 rows,
544 cols,
545 eps,
546 );
547 Ok((
548 f32_tensor(Storage::Wgpu(dx), x.shape().clone()),
549 f32_tensor(Storage::Wgpu(dg), pshape.clone()),
550 f32_tensor(Storage::Wgpu(db), pshape),
551 ))
552 }
553 }
554}
555
556pub fn sum_rows(x: &Tensor) -> Result<Tensor> {
558 let (rows, cols) = last_dim_rows(x)?;
559 let storage = match x.storage() {
560 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
561 cpu::sum_rows(cpu_f32(x)?, rows, cols).into(),
562 )),
563 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::sum_rows(gpu_storage(x)?, rows, cols)),
564 };
565 Ok(f32_tensor(storage, Shape::new(&[cols])))
566}
567
568pub fn scatter_add_rows(dst: &mut Tensor, ids: &Tensor, src: &Tensor) -> Result<()> {
570 same_device(&[dst, ids, src])?;
571 let (vocab, c) = match dst.shape().dims() {
572 [v, c] => (*v, *c),
573 _ => return Err(ForgeError::Shape("scatter_add dst must be rank 2".into())),
574 };
575 let t = ids.shape().numel();
576 if src.shape().dims() != [t, c] {
577 return Err(ForgeError::Shape(format!(
578 "scatter_add src {} != [{t}, {c}]",
579 src.shape()
580 )));
581 }
582 match (&mut dst.storage, src.storage()) {
583 (Storage::Cpu(CpuStorage::F32(d)), Storage::Cpu(CpuStorage::F32(s))) => {
584 let ids = cpu_u32(ids)?;
585 if let Some(&bad) = ids.iter().find(|&&id| id as usize >= vocab) {
586 return Err(ForgeError::Shape(format!(
587 "scatter_add id {bad} >= {vocab}"
588 )));
589 }
590 let d: &mut Vec<f32> = std::sync::Arc::make_mut(d);
591 cpu::scatter_add_rows(d, ids, s, c);
592 Ok(())
593 }
594 (Storage::Wgpu(d), Storage::Wgpu(s)) => {
595 gpu::ops::scatter_add_rows(d, gpu_storage(ids)?, s, t, c, vocab * c);
596 Ok(())
597 }
598 _ => Err(ForgeError::Device(
599 "scatter_add expects f32 tensors on one device".into(),
600 )),
601 }
602}
603
604pub fn gather_nll(probs: &Tensor, ids: &Tensor) -> Result<Tensor> {
606 same_device(&[probs, ids])?;
607 let (rows, cols) = last_dim_rows(probs)?;
608 if ids.shape().numel() != rows {
609 return Err(ForgeError::Shape("gather_nll ids length".into()));
610 }
611 let storage = match probs.storage() {
612 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
613 cpu::gather_nll(cpu_f32(probs)?, cpu_u32(ids)?, cols).into(),
614 )),
615 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::gather_nll(
616 gpu_storage(probs)?,
617 gpu_storage(ids)?,
618 rows,
619 cols,
620 )),
621 };
622 Ok(f32_tensor(storage, Shape::new(&[rows])))
623}
624
625pub fn ce_bwd(probs: &Tensor, ids: &Tensor, scale: f32) -> Result<Tensor> {
627 same_device(&[probs, ids])?;
628 let (rows, cols) = last_dim_rows(probs)?;
629 if ids.shape().numel() != rows {
630 return Err(ForgeError::Shape("ce_bwd ids length".into()));
631 }
632 let storage = match probs.storage() {
633 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
634 cpu::ce_bwd(cpu_f32(probs)?, cpu_u32(ids)?, rows, cols, scale).into(),
635 )),
636 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::ce_bwd(
637 gpu_storage(probs)?,
638 gpu_storage(ids)?,
639 rows,
640 cols,
641 scale,
642 )),
643 };
644 Ok(f32_tensor(storage, probs.shape().clone()))
645}
646
647pub fn dropout(x: &Tensor, p: f32, seed: u32) -> Result<Tensor> {
650 if !(0.0..1.0).contains(&p) {
651 return Err(ForgeError::Shape(format!("dropout p {p} outside [0, 1)")));
652 }
653 if p == 0.0 {
654 return Ok(x.clone());
655 }
656 let scale = 1.0f32 / (1.0 - p);
661 let n = x.shape().numel();
662 let storage = match x.storage() {
663 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
664 cpu::dropout(cpu_f32(x)?, p, scale, seed).into(),
665 )),
666 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::dropout(gpu_storage(x)?, n, p, scale, seed)),
667 };
668 Ok(f32_tensor(storage, x.shape().clone()))
669}
670
671pub fn unsplit_head(d: &Tensor, which: usize) -> Result<Tensor> {
674 let (h, t, hd) = match d.shape().dims() {
675 [h, t, hd] => (*h, *t, *hd),
676 _ => return Err(ForgeError::Shape("unsplit_head needs [h, t, hd]".into())),
677 };
678 let c = h * hd;
679 let shape = Shape::new(&[t, 3 * c]);
680 let storage = match d.storage() {
681 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
682 cpu::unsplit_head(cpu_f32(d)?, t, c, h, which).into(),
683 )),
684 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::unsplit_head(gpu_storage(d)?, t, c, h, which)),
685 };
686 Ok(f32_tensor(storage, shape))
687}
688
689pub fn unmerge_heads(dy: &Tensor, h: usize) -> Result<Tensor> {
691 let (t, c) = match dy.shape().dims() {
692 [t, c] => (*t, *c),
693 _ => return Err(ForgeError::Shape("unmerge_heads needs [t, c]".into())),
694 };
695 if c % h != 0 {
696 return Err(ForgeError::Shape(format!("unmerge_heads c={c} % h={h}")));
697 }
698 let shape = Shape::new(&[h, t, c / h]);
699 let storage = match dy.storage() {
700 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
701 cpu::unmerge_heads(cpu_f32(dy)?, t, c, h).into(),
702 )),
703 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::unmerge_heads(gpu_storage(dy)?, t, c, h)),
704 };
705 Ok(f32_tensor(storage, shape))
706}
707
708pub fn sumsq(x: &Tensor) -> Result<f32> {
712 match x.storage() {
713 Storage::Cpu(_) => Ok(cpu_f32(x)?.iter().map(|&v| v * v).sum()),
714 #[cfg(not(target_arch = "wasm32"))]
715 Storage::Wgpu(s) => {
716 let (partials, groups) = gpu::ops::sumsq_partials(s, x.shape().numel());
717 let bytes = partials.ctx.readback(&partials.buf, 0, groups * 4)?;
718 let vals: Vec<f32> = bytemuck::pod_collect_to_vec(&bytes);
719 Ok(vals.iter().sum())
720 }
721 #[cfg(target_arch = "wasm32")]
722 Storage::Wgpu(_) => Err(crate::error::ForgeError::Wgpu(
723 "sumsq readback unavailable on wasm32 (training is native-only)".into(),
724 )),
725 }
726}
727
728pub fn scale(x: &Tensor, alpha: f32) -> Result<Tensor> {
730 let n = x.shape().numel();
731 let storage = match x.storage() {
732 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
733 cpu_f32(x)?
734 .iter()
735 .map(|&v| v * alpha)
736 .collect::<Vec<_>>()
737 .into(),
738 )),
739 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::scale(gpu_storage(x)?, n, alpha)),
740 };
741 Ok(f32_tensor(storage, x.shape().clone()))
742}
743
744#[allow(clippy::too_many_arguments)]
747pub fn adamw(
748 param: &mut Tensor,
749 grad: &Tensor,
750 m: &mut Tensor,
751 v: &mut Tensor,
752 lr: f32,
753 beta1: f32,
754 beta2: f32,
755 eps: f32,
756 weight_decay: f32,
757 step: u32,
758) -> Result<()> {
759 if param.shape() != grad.shape() || param.shape() != m.shape() || param.shape() != v.shape() {
760 return Err(ForgeError::Shape("adamw shape mismatch".into()));
761 }
762 let n = param.shape().numel();
763 match (&mut param.storage, grad.storage()) {
764 (Storage::Cpu(CpuStorage::F32(p)), Storage::Cpu(CpuStorage::F32(g))) => {
765 let (Storage::Cpu(CpuStorage::F32(ms)), Storage::Cpu(CpuStorage::F32(vs))) =
766 (&mut m.storage, &mut v.storage)
767 else {
768 return Err(ForgeError::Device("adamw state device mismatch".into()));
769 };
770 let p: &mut Vec<f32> = std::sync::Arc::make_mut(p);
771 let ms: &mut Vec<f32> = std::sync::Arc::make_mut(ms);
772 let vs: &mut Vec<f32> = std::sync::Arc::make_mut(vs);
773 cpu::adamw(p, g, ms, vs, lr, beta1, beta2, eps, weight_decay, step);
774 Ok(())
775 }
776 (Storage::Wgpu(p), Storage::Wgpu(g)) => {
777 let (Storage::Wgpu(ms), Storage::Wgpu(vs)) = (&m.storage, &v.storage) else {
778 return Err(ForgeError::Device("adamw state device mismatch".into()));
779 };
780 gpu::ops::adamw(p, g, ms, vs, n, lr, beta1, beta2, eps, weight_decay, step);
781 Ok(())
782 }
783 _ => Err(ForgeError::Device(
784 "adamw expects f32 tensors on one device".into(),
785 )),
786 }
787}
788
789pub fn kv_append(cache: &mut Tensor, src: &Tensor, len: usize) -> Result<()> {
792 same_device(&[cache, src])?;
793 let (h, cap, hd) = match cache.shape().dims() {
794 [h, cap, hd] => (*h, *cap, *hd),
795 _ => return Err(ForgeError::Shape("kv_append cache must be rank 3".into())),
796 };
797 let t = match src.shape().dims() {
798 [sh, t, shd] if *sh == h && *shd == hd => *t,
799 _ => {
800 return Err(ForgeError::Shape(format!(
801 "kv_append src {} incompatible with cache {}",
802 src.shape(),
803 cache.shape()
804 )));
805 }
806 };
807 if len + t > cap {
808 return Err(ForgeError::Shape(format!(
809 "kv_append {len}+{t} exceeds cache capacity {cap}"
810 )));
811 }
812 match (&mut cache.storage, src.storage()) {
813 (Storage::Cpu(CpuStorage::F32(dst)), Storage::Cpu(CpuStorage::F32(s))) => {
814 let dst: &mut Vec<f32> = std::sync::Arc::make_mut(dst);
815 cpu::kv_append(dst, s, h, t, hd, cap, len);
816 Ok(())
817 }
818 (Storage::Wgpu(c), Storage::Wgpu(s)) => {
819 gpu::ops::kv_append(c, s, h, t, hd, cap, len);
820 Ok(())
821 }
822 _ => Err(ForgeError::Device(
823 "kv_append expects f32 tensors on one device".into(),
824 )),
825 }
826}
827
828pub fn split_heads(qkv: &Tensor, n_head: usize) -> Result<(Tensor, Tensor, Tensor)> {
830 let (t, c3) = match qkv.shape().dims() {
831 [t, c3] => (*t, *c3),
832 _ => return Err(ForgeError::Shape("split_heads needs [t, 3c]".into())),
833 };
834 let c = c3 / 3;
835 if c3 != 3 * c || c % n_head != 0 {
836 return Err(ForgeError::Shape(format!(
837 "split_heads: dim {c3} not divisible into 3 x {n_head} heads"
838 )));
839 }
840 let shape = Shape::new(&[n_head, t, c / n_head]);
841 match qkv.storage() {
842 Storage::Cpu(_) => {
843 let (q, k, v) = cpu::split_heads(cpu_f32(qkv)?, t, c, n_head);
844 Ok((
845 f32_tensor(Storage::Cpu(CpuStorage::F32(q.into())), shape.clone()),
846 f32_tensor(Storage::Cpu(CpuStorage::F32(k.into())), shape.clone()),
847 f32_tensor(Storage::Cpu(CpuStorage::F32(v.into())), shape),
848 ))
849 }
850 Storage::Wgpu(_) => {
851 let (q, k, v) = gpu::ops::split_heads(gpu_storage(qkv)?, t, c, n_head);
852 Ok((
853 f32_tensor(Storage::Wgpu(q), shape.clone()),
854 f32_tensor(Storage::Wgpu(k), shape.clone()),
855 f32_tensor(Storage::Wgpu(v), shape),
856 ))
857 }
858 }
859}
860
861pub fn merge_heads(x: &Tensor) -> Result<Tensor> {
863 let (h, t, hd) = match x.shape().dims() {
864 [h, t, hd] => (*h, *t, *hd),
865 _ => return Err(ForgeError::Shape("merge_heads needs [h, t, hd]".into())),
866 };
867 let c = h * hd;
868 let shape = Shape::new(&[t, c]);
869 let storage = match x.storage() {
870 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
871 cpu::merge_heads(cpu_f32(x)?, t, c, h).into(),
872 )),
873 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::merge_heads(gpu_storage(x)?, t, c, h)),
874 };
875 Ok(f32_tensor(storage, shape))
876}