1use crate::backend::{cpu, wgpu as gpu};
6use crate::error::{ForgeError, Result};
7use crate::shape::Shape;
8use crate::tensor::{CpuStorage, Storage, Tensor, WgpuStorage};
9
10pub(crate) fn 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
17pub(crate) fn 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
24pub(crate) fn 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
31pub(crate) fn 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
45pub(crate) fn 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
461pub fn kv_append(cache: &mut Tensor, src: &Tensor, len: usize) -> Result<()> {
464 same_device(&[cache, src])?;
465 let (h, cap, hd) = match cache.shape().dims() {
466 [h, cap, hd] => (*h, *cap, *hd),
467 _ => return Err(ForgeError::Shape("kv_append cache must be rank 3".into())),
468 };
469 let t = match src.shape().dims() {
470 [sh, t, shd] if *sh == h && *shd == hd => *t,
471 _ => {
472 return Err(ForgeError::Shape(format!(
473 "kv_append src {} incompatible with cache {}",
474 src.shape(),
475 cache.shape()
476 )));
477 }
478 };
479 if len + t > cap {
480 return Err(ForgeError::Shape(format!(
481 "kv_append {len}+{t} exceeds cache capacity {cap}"
482 )));
483 }
484 match (&mut cache.storage, src.storage()) {
485 (Storage::Cpu(CpuStorage::F32(dst)), Storage::Cpu(CpuStorage::F32(s))) => {
486 let dst: &mut Vec<f32> = std::sync::Arc::make_mut(dst);
487 cpu::kv_append(dst, s, h, t, hd, cap, len);
488 Ok(())
489 }
490 (Storage::Wgpu(c), Storage::Wgpu(s)) => {
491 gpu::ops::kv_append(c, s, h, t, hd, cap, len);
492 Ok(())
493 }
494 _ => Err(ForgeError::Device(
495 "kv_append expects f32 tensors on one device".into(),
496 )),
497 }
498}
499
500pub fn split_heads(qkv: &Tensor, n_head: usize) -> Result<(Tensor, Tensor, Tensor)> {
502 let (t, c3) = match qkv.shape().dims() {
503 [t, c3] => (*t, *c3),
504 _ => return Err(ForgeError::Shape("split_heads needs [t, 3c]".into())),
505 };
506 let c = c3 / 3;
507 if c3 != 3 * c || c % n_head != 0 {
508 return Err(ForgeError::Shape(format!(
509 "split_heads: dim {c3} not divisible into 3 x {n_head} heads"
510 )));
511 }
512 let shape = Shape::new(&[n_head, t, c / n_head]);
513 match qkv.storage() {
514 Storage::Cpu(_) => {
515 let (q, k, v) = cpu::split_heads(cpu_f32(qkv)?, t, c, n_head);
516 Ok((
517 f32_tensor(Storage::Cpu(CpuStorage::F32(q.into())), shape.clone()),
518 f32_tensor(Storage::Cpu(CpuStorage::F32(k.into())), shape.clone()),
519 f32_tensor(Storage::Cpu(CpuStorage::F32(v.into())), shape),
520 ))
521 }
522 Storage::Wgpu(_) => {
523 let (q, k, v) = gpu::ops::split_heads(gpu_storage(qkv)?, t, c, n_head);
524 Ok((
525 f32_tensor(Storage::Wgpu(q), shape.clone()),
526 f32_tensor(Storage::Wgpu(k), shape.clone()),
527 f32_tensor(Storage::Wgpu(v), shape),
528 ))
529 }
530 }
531}
532
533pub fn merge_heads(x: &Tensor) -> Result<Tensor> {
535 let (h, t, hd) = match x.shape().dims() {
536 [h, t, hd] => (*h, *t, *hd),
537 _ => return Err(ForgeError::Shape("merge_heads needs [h, t, hd]".into())),
538 };
539 let c = h * hd;
540 let shape = Shape::new(&[t, c]);
541 let storage = match x.storage() {
542 Storage::Cpu(_) => Storage::Cpu(CpuStorage::F32(
543 cpu::merge_heads(cpu_f32(x)?, t, c, h).into(),
544 )),
545 Storage::Wgpu(_) => Storage::Wgpu(gpu::ops::merge_heads(gpu_storage(x)?, t, c, h)),
546 };
547 Ok(f32_tensor(storage, shape))
548}
549
550#[cfg(feature = "train")]
553#[cfg_attr(docsrs, doc(cfg(feature = "train")))]
554mod train;
555#[cfg(feature = "train")]
556#[cfg_attr(docsrs, doc(cfg(feature = "train")))]
557pub use train::*;