1use std::collections::HashMap;
32use std::sync::Mutex;
33
34use crate::ComputeBackend;
35use crate::capabilities::{Capabilities, DeviceInfo, MemoryKind};
36use crate::error::{BackendError, BackendResult};
37use crate::ops::{BackendTranspose, BinaryOp, MixedPrecision, ReduceOp, UnaryOp};
38use crate::precision::{round_to_bf16, round_to_f16};
39
40#[derive(Debug)]
42pub struct CpuBackend {
43 initialized: bool,
44 allocations: Mutex<HashMap<u64, Vec<u8>>>,
46 next_ptr: Mutex<u64>,
49}
50
51impl Default for CpuBackend {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl CpuBackend {
58 #[must_use]
60 pub fn new() -> Self {
61 Self {
62 initialized: false,
63 allocations: Mutex::new(HashMap::new()),
64 next_ptr: Mutex::new(0x1000),
67 }
68 }
69
70 #[must_use]
72 pub fn live_allocations(&self) -> usize {
73 self.allocations.lock().map(|t| t.len()).unwrap_or_default()
74 }
75
76 fn read_f32(&self, ptr: u64, len: usize) -> BackendResult<Vec<f32>> {
78 let table = self
79 .allocations
80 .lock()
81 .map_err(|_| BackendError::DeviceError("allocation table poisoned".into()))?;
82 let buf = table.get(&ptr).ok_or_else(|| {
83 BackendError::InvalidArgument(format!("unknown device pointer {ptr:#x}"))
84 })?;
85 let need = len * 4;
86 if buf.len() < need {
87 return Err(BackendError::InvalidArgument(format!(
88 "buffer at {ptr:#x} holds {} bytes, need {need}",
89 buf.len()
90 )));
91 }
92 let mut out = Vec::with_capacity(len);
93 for chunk in buf[..need].chunks_exact(4) {
94 out.push(f32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
95 }
96 Ok(out)
97 }
98
99 fn write_f32(&self, ptr: u64, data: &[f32]) -> BackendResult<()> {
101 let mut table = self
102 .allocations
103 .lock()
104 .map_err(|_| BackendError::DeviceError("allocation table poisoned".into()))?;
105 let buf = table.get_mut(&ptr).ok_or_else(|| {
106 BackendError::InvalidArgument(format!("unknown device pointer {ptr:#x}"))
107 })?;
108 let need = data.len() * 4;
109 if buf.len() < need {
110 return Err(BackendError::InvalidArgument(format!(
111 "buffer at {ptr:#x} holds {} bytes, need {need}",
112 buf.len()
113 )));
114 }
115 for (slot, &v) in buf[..need].chunks_exact_mut(4).zip(data.iter()) {
116 slot.copy_from_slice(&v.to_ne_bytes());
117 }
118 Ok(())
119 }
120
121 fn read_f64(&self, ptr: u64, len: usize) -> BackendResult<Vec<f64>> {
123 let table = self
124 .allocations
125 .lock()
126 .map_err(|_| BackendError::DeviceError("allocation table poisoned".into()))?;
127 let buf = table.get(&ptr).ok_or_else(|| {
128 BackendError::InvalidArgument(format!("unknown device pointer {ptr:#x}"))
129 })?;
130 let need = len * 8;
131 if buf.len() < need {
132 return Err(BackendError::InvalidArgument(format!(
133 "buffer at {ptr:#x} holds {} bytes, need {need}",
134 buf.len()
135 )));
136 }
137 let mut out = Vec::with_capacity(len);
138 for chunk in buf[..need].chunks_exact(8) {
139 let mut b = [0u8; 8];
140 b.copy_from_slice(chunk);
141 out.push(f64::from_ne_bytes(b));
142 }
143 Ok(out)
144 }
145
146 fn write_f64(&self, ptr: u64, data: &[f64]) -> BackendResult<()> {
148 let mut table = self
149 .allocations
150 .lock()
151 .map_err(|_| BackendError::DeviceError("allocation table poisoned".into()))?;
152 let buf = table.get_mut(&ptr).ok_or_else(|| {
153 BackendError::InvalidArgument(format!("unknown device pointer {ptr:#x}"))
154 })?;
155 let need = data.len() * 8;
156 if buf.len() < need {
157 return Err(BackendError::InvalidArgument(format!(
158 "buffer at {ptr:#x} holds {} bytes, need {need}",
159 buf.len()
160 )));
161 }
162 for (slot, &v) in buf[..need].chunks_exact_mut(8).zip(data.iter()) {
163 slot.copy_from_slice(&v.to_ne_bytes());
164 }
165 Ok(())
166 }
167}
168
169#[inline]
171const fn col_major(row: usize, col: usize, ld: usize) -> usize {
172 col * ld + row
173}
174
175#[inline]
179fn at(m: &[f64], trans: BackendTranspose, row: usize, col: usize, ld: usize) -> f64 {
180 match trans {
181 BackendTranspose::NoTrans => m[col_major(row, col, ld)],
182 BackendTranspose::Trans | BackendTranspose::ConjTrans => m[col_major(col, row, ld)],
183 }
184}
185
186#[inline]
189fn at_f32(m: &[f32], trans: BackendTranspose, row: usize, col: usize, ld: usize) -> f32 {
190 match trans {
191 BackendTranspose::NoTrans => m[col_major(row, col, ld)],
192 BackendTranspose::Trans | BackendTranspose::ConjTrans => m[col_major(col, row, ld)],
193 }
194}
195
196#[inline]
201fn round_store(prec: MixedPrecision, x: f32) -> f32 {
202 match prec {
203 MixedPrecision::F16 => round_to_f16(x),
204 MixedPrecision::Bf16 => round_to_bf16(x),
205 }
206}
207
208impl ComputeBackend for CpuBackend {
209 fn name(&self) -> &str {
210 "cpu"
211 }
212
213 fn init(&mut self) -> BackendResult<()> {
214 self.initialized = true;
215 Ok(())
216 }
217
218 fn is_initialized(&self) -> bool {
219 self.initialized
220 }
221
222 fn capabilities(&self) -> Capabilities {
223 Capabilities::cpu()
224 }
225
226 fn available_devices(&self) -> BackendResult<Vec<DeviceInfo>> {
227 Ok(vec![DeviceInfo {
231 ordinal: 0,
232 name: "CPU (reference)".to_string(),
233 compute_capability: (0, 0),
234 total_memory_bytes: 0,
235 memory_kind: MemoryKind::Host,
236 capabilities: Capabilities::cpu(),
237 }])
238 }
239
240 fn gemm(
241 &self,
242 trans_a: BackendTranspose,
243 trans_b: BackendTranspose,
244 m: usize,
245 n: usize,
246 k: usize,
247 alpha: f64,
248 a_ptr: u64,
249 lda: usize,
250 b_ptr: u64,
251 ldb: usize,
252 beta: f64,
253 c_ptr: u64,
254 ldc: usize,
255 ) -> BackendResult<()> {
256 if m == 0 || n == 0 {
257 return Ok(());
258 }
259 let a_rows = if trans_a == BackendTranspose::NoTrans {
262 m
263 } else {
264 k
265 };
266 let a_cols = if trans_a == BackendTranspose::NoTrans {
267 k
268 } else {
269 m
270 };
271 let b_rows = if trans_b == BackendTranspose::NoTrans {
272 k
273 } else {
274 n
275 };
276 let b_cols = if trans_b == BackendTranspose::NoTrans {
277 n
278 } else {
279 k
280 };
281 if lda < a_rows || ldb < b_rows || ldc < m {
282 return Err(BackendError::InvalidArgument(
283 "leading dimension smaller than matrix extent".into(),
284 ));
285 }
286 let a = if k == 0 {
287 Vec::new()
288 } else {
289 self.read_f64(a_ptr, lda * a_cols)?
290 };
291 let b = if k == 0 {
292 Vec::new()
293 } else {
294 self.read_f64(b_ptr, ldb * b_cols)?
295 };
296 let mut c = self.read_f64(c_ptr, ldc * n)?;
297
298 for j in 0..n {
299 for i in 0..m {
300 let mut acc = 0.0f64;
301 for p in 0..k {
302 acc += at(&a, trans_a, i, p, lda) * at(&b, trans_b, p, j, ldb);
303 }
304 let dst = &mut c[col_major(i, j, ldc)];
305 *dst = alpha * acc + beta * *dst;
306 }
307 }
308 self.write_f64(c_ptr, &c)
309 }
310
311 fn conv2d_forward(
312 &self,
313 input_ptr: u64,
314 input_shape: &[usize],
315 filter_ptr: u64,
316 filter_shape: &[usize],
317 output_ptr: u64,
318 output_shape: &[usize],
319 stride: &[usize],
320 padding: &[usize],
321 ) -> BackendResult<()> {
322 if input_shape.len() != 4 || filter_shape.len() != 4 || output_shape.len() != 4 {
323 return Err(BackendError::InvalidArgument(
324 "conv2d expects 4-D NCHW shapes".into(),
325 ));
326 }
327 if stride.len() != 2 || padding.len() != 2 {
328 return Err(BackendError::InvalidArgument(
329 "conv2d expects 2-element stride and padding".into(),
330 ));
331 }
332 let (n, c_in, h, w) = (
333 input_shape[0],
334 input_shape[1],
335 input_shape[2],
336 input_shape[3],
337 );
338 let (k_out, c_f, fh, fw) = (
339 filter_shape[0],
340 filter_shape[1],
341 filter_shape[2],
342 filter_shape[3],
343 );
344 let (on, ok, oh, ow) = (
345 output_shape[0],
346 output_shape[1],
347 output_shape[2],
348 output_shape[3],
349 );
350 if c_f != c_in || k_out != ok || on != n {
351 return Err(BackendError::InvalidArgument(
352 "conv2d shape mismatch between input/filter/output".into(),
353 ));
354 }
355 let (sh, sw) = (stride[0], stride[1]);
356 let (ph, pw) = (padding[0], padding[1]);
357 let exp_oh = (h + 2 * ph).saturating_sub(fh) / sh.max(1) + 1;
359 let exp_ow = (w + 2 * pw).saturating_sub(fw) / sw.max(1) + 1;
360 if oh != exp_oh || ow != exp_ow {
361 return Err(BackendError::InvalidArgument(format!(
362 "conv2d output spatial size {oh}x{ow} != expected {exp_oh}x{exp_ow}"
363 )));
364 }
365
366 let input = self.read_f32(input_ptr, n * c_in * h * w)?;
367 let filter = self.read_f32(filter_ptr, k_out * c_in * fh * fw)?;
368 let mut output = vec![0.0f32; n * k_out * oh * ow];
369
370 let in_idx =
371 |ni: usize, ci: usize, hi: usize, wi: usize| ((ni * c_in + ci) * h + hi) * w + wi;
372 let f_idx =
373 |ko: usize, ci: usize, fhi: usize, fwi: usize| ((ko * c_in + ci) * fh + fhi) * fw + fwi;
374 let out_idx = |ni: usize, ko: usize, ohi: usize, owi: usize| {
375 ((ni * k_out + ko) * oh + ohi) * ow + owi
376 };
377
378 for ni in 0..n {
379 for ko in 0..k_out {
380 for ohi in 0..oh {
381 for owi in 0..ow {
382 let mut acc = 0.0f32;
383 for ci in 0..c_in {
384 for fhi in 0..fh {
385 let src_h = ohi * sh + fhi;
387 if src_h < ph || src_h >= h + ph {
388 continue;
389 }
390 let ih = src_h - ph;
391 for fwi in 0..fw {
392 let src_w = owi * sw + fwi;
393 if src_w < pw || src_w >= w + pw {
394 continue;
395 }
396 let iw = src_w - pw;
397 acc += input[in_idx(ni, ci, ih, iw)]
398 * filter[f_idx(ko, ci, fhi, fwi)];
399 }
400 }
401 }
402 output[out_idx(ni, ko, ohi, owi)] = acc;
403 }
404 }
405 }
406 }
407 self.write_f32(output_ptr, &output)
408 }
409
410 fn gemm_mixed_precision(
411 &self,
412 prec: MixedPrecision,
413 trans_a: BackendTranspose,
414 trans_b: BackendTranspose,
415 m: usize,
416 n: usize,
417 k: usize,
418 alpha: f32,
419 a_ptr: u64,
420 lda: usize,
421 b_ptr: u64,
422 ldb: usize,
423 beta: f32,
424 c_ptr: u64,
425 ldc: usize,
426 ) -> BackendResult<()> {
427 if m == 0 || n == 0 {
428 return Ok(());
429 }
430 let a_rows = if trans_a == BackendTranspose::NoTrans {
431 m
432 } else {
433 k
434 };
435 let a_cols = if trans_a == BackendTranspose::NoTrans {
436 k
437 } else {
438 m
439 };
440 let b_rows = if trans_b == BackendTranspose::NoTrans {
441 k
442 } else {
443 n
444 };
445 let b_cols = if trans_b == BackendTranspose::NoTrans {
446 n
447 } else {
448 k
449 };
450 if lda < a_rows || ldb < b_rows || ldc < m {
451 return Err(BackendError::InvalidArgument(
452 "leading dimension smaller than matrix extent".into(),
453 ));
454 }
455 let a_raw = if k == 0 {
460 Vec::new()
461 } else {
462 self.read_f32(a_ptr, lda * a_cols)?
463 };
464 let b_raw = if k == 0 {
465 Vec::new()
466 } else {
467 self.read_f32(b_ptr, ldb * b_cols)?
468 };
469 let a: Vec<f32> = a_raw.iter().map(|&v| round_store(prec, v)).collect();
470 let b: Vec<f32> = b_raw.iter().map(|&v| round_store(prec, v)).collect();
471 let mut c = self.read_f32(c_ptr, ldc * n)?;
472
473 for j in 0..n {
474 for i in 0..m {
475 let mut acc = 0.0f32;
479 for p in 0..k {
480 acc += at_f32(&a, trans_a, i, p, lda) * at_f32(&b, trans_b, p, j, ldb);
481 }
482 let dst = &mut c[col_major(i, j, ldc)];
483 *dst = alpha * acc + beta * *dst;
484 }
485 }
486 self.write_f32(c_ptr, &c)
487 }
488
489 fn conv2d_backward_data(
490 &self,
491 grad_output_ptr: u64,
492 grad_output_shape: &[usize],
493 filter_ptr: u64,
494 filter_shape: &[usize],
495 grad_input_ptr: u64,
496 grad_input_shape: &[usize],
497 stride: &[usize],
498 padding: &[usize],
499 ) -> BackendResult<()> {
500 if grad_output_shape.len() != 4 || filter_shape.len() != 4 || grad_input_shape.len() != 4 {
501 return Err(BackendError::InvalidArgument(
502 "conv2d_backward_data expects 4-D NCHW shapes".into(),
503 ));
504 }
505 if stride.len() != 2 || padding.len() != 2 {
506 return Err(BackendError::InvalidArgument(
507 "conv2d_backward_data expects 2-element stride and padding".into(),
508 ));
509 }
510 let (n, c_in, h, w) = (
511 grad_input_shape[0],
512 grad_input_shape[1],
513 grad_input_shape[2],
514 grad_input_shape[3],
515 );
516 let (k_out, c_f, fh, fw) = (
517 filter_shape[0],
518 filter_shape[1],
519 filter_shape[2],
520 filter_shape[3],
521 );
522 let (gn, gk, oh, ow) = (
523 grad_output_shape[0],
524 grad_output_shape[1],
525 grad_output_shape[2],
526 grad_output_shape[3],
527 );
528 if c_f != c_in || gk != k_out || gn != n {
529 return Err(BackendError::InvalidArgument(
530 "conv2d_backward_data shape mismatch between grad_output/filter/grad_input".into(),
531 ));
532 }
533 let (sh, sw) = (stride[0], stride[1]);
534 let (ph, pw) = (padding[0], padding[1]);
535 let exp_oh = (h + 2 * ph).saturating_sub(fh) / sh.max(1) + 1;
538 let exp_ow = (w + 2 * pw).saturating_sub(fw) / sw.max(1) + 1;
539 if oh != exp_oh || ow != exp_ow {
540 return Err(BackendError::InvalidArgument(format!(
541 "conv2d_backward_data grad_output spatial size {oh}x{ow} != expected {exp_oh}x{exp_ow}"
542 )));
543 }
544
545 let grad_output = self.read_f32(grad_output_ptr, n * k_out * oh * ow)?;
546 let filter = self.read_f32(filter_ptr, k_out * c_in * fh * fw)?;
547 let mut grad_input = vec![0.0f32; n * c_in * h * w];
548
549 let in_idx =
550 |ni: usize, ci: usize, hi: usize, wi: usize| ((ni * c_in + ci) * h + hi) * w + wi;
551 let f_idx =
552 |ko: usize, ci: usize, fhi: usize, fwi: usize| ((ko * c_in + ci) * fh + fhi) * fw + fwi;
553 let go_idx = |ni: usize, ko: usize, ohi: usize, owi: usize| {
554 ((ni * k_out + ko) * oh + ohi) * ow + owi
555 };
556
557 for ni in 0..n {
564 for ko in 0..k_out {
565 for ohi in 0..oh {
566 for owi in 0..ow {
567 let g = grad_output[go_idx(ni, ko, ohi, owi)];
568 if g == 0.0 {
569 continue;
570 }
571 for ci in 0..c_in {
572 for fhi in 0..fh {
573 let src_h = ohi * sh + fhi;
574 if src_h < ph || src_h >= h + ph {
575 continue;
576 }
577 let ih = src_h - ph;
578 for fwi in 0..fw {
579 let src_w = owi * sw + fwi;
580 if src_w < pw || src_w >= w + pw {
581 continue;
582 }
583 let iw = src_w - pw;
584 grad_input[in_idx(ni, ci, ih, iw)] +=
585 g * filter[f_idx(ko, ci, fhi, fwi)];
586 }
587 }
588 }
589 }
590 }
591 }
592 }
593 self.write_f32(grad_input_ptr, &grad_input)
594 }
595
596 fn conv2d_backward_filter(
597 &self,
598 input_ptr: u64,
599 input_shape: &[usize],
600 grad_output_ptr: u64,
601 grad_output_shape: &[usize],
602 grad_filter_ptr: u64,
603 grad_filter_shape: &[usize],
604 stride: &[usize],
605 padding: &[usize],
606 ) -> BackendResult<()> {
607 if input_shape.len() != 4 || grad_output_shape.len() != 4 || grad_filter_shape.len() != 4 {
608 return Err(BackendError::InvalidArgument(
609 "conv2d_backward_filter expects 4-D NCHW shapes".into(),
610 ));
611 }
612 if stride.len() != 2 || padding.len() != 2 {
613 return Err(BackendError::InvalidArgument(
614 "conv2d_backward_filter expects 2-element stride and padding".into(),
615 ));
616 }
617 let (n, c_in, h, w) = (
618 input_shape[0],
619 input_shape[1],
620 input_shape[2],
621 input_shape[3],
622 );
623 let (k_out, c_f, fh, fw) = (
624 grad_filter_shape[0],
625 grad_filter_shape[1],
626 grad_filter_shape[2],
627 grad_filter_shape[3],
628 );
629 let (gn, gk, oh, ow) = (
630 grad_output_shape[0],
631 grad_output_shape[1],
632 grad_output_shape[2],
633 grad_output_shape[3],
634 );
635 if c_f != c_in || gk != k_out || gn != n {
636 return Err(BackendError::InvalidArgument(
637 "conv2d_backward_filter shape mismatch between input/grad_output/grad_filter"
638 .into(),
639 ));
640 }
641 let (sh, sw) = (stride[0], stride[1]);
642 let (ph, pw) = (padding[0], padding[1]);
643 let exp_oh = (h + 2 * ph).saturating_sub(fh) / sh.max(1) + 1;
644 let exp_ow = (w + 2 * pw).saturating_sub(fw) / sw.max(1) + 1;
645 if oh != exp_oh || ow != exp_ow {
646 return Err(BackendError::InvalidArgument(format!(
647 "conv2d_backward_filter grad_output spatial size {oh}x{ow} != expected {exp_oh}x{exp_ow}"
648 )));
649 }
650
651 let input = self.read_f32(input_ptr, n * c_in * h * w)?;
652 let grad_output = self.read_f32(grad_output_ptr, n * k_out * oh * ow)?;
653 let mut grad_filter = vec![0.0f32; k_out * c_in * fh * fw];
654
655 let in_idx =
656 |ni: usize, ci: usize, hi: usize, wi: usize| ((ni * c_in + ci) * h + hi) * w + wi;
657 let f_idx =
658 |ko: usize, ci: usize, fhi: usize, fwi: usize| ((ko * c_in + ci) * fh + fhi) * fw + fwi;
659 let go_idx = |ni: usize, ko: usize, ohi: usize, owi: usize| {
660 ((ni * k_out + ko) * oh + ohi) * ow + owi
661 };
662
663 for ko in 0..k_out {
669 for ci in 0..c_in {
670 for fhi in 0..fh {
671 for fwi in 0..fw {
672 let mut acc = 0.0f32;
673 for ni in 0..n {
674 for ohi in 0..oh {
675 let src_h = ohi * sh + fhi;
676 if src_h < ph || src_h >= h + ph {
677 continue;
678 }
679 let ih = src_h - ph;
680 for owi in 0..ow {
681 let src_w = owi * sw + fwi;
682 if src_w < pw || src_w >= w + pw {
683 continue;
684 }
685 let iw = src_w - pw;
686 acc += input[in_idx(ni, ci, ih, iw)]
687 * grad_output[go_idx(ni, ko, ohi, owi)];
688 }
689 }
690 }
691 grad_filter[f_idx(ko, ci, fhi, fwi)] = acc;
692 }
693 }
694 }
695 }
696 self.write_f32(grad_filter_ptr, &grad_filter)
697 }
698
699 fn attention(
700 &self,
701 q_ptr: u64,
702 k_ptr: u64,
703 v_ptr: u64,
704 o_ptr: u64,
705 batch: usize,
706 heads: usize,
707 seq_q: usize,
708 seq_kv: usize,
709 head_dim: usize,
710 scale: f64,
711 causal: bool,
712 ) -> BackendResult<()> {
713 let total_q = batch * heads * seq_q * head_dim;
714 let total_kv = batch * heads * seq_kv * head_dim;
715 let q = self.read_f32(q_ptr, total_q)?;
716 let k = self.read_f32(k_ptr, total_kv)?;
717 let v = self.read_f32(v_ptr, total_kv)?;
718 let mut o = vec![0.0f32; total_q];
719
720 let scale = scale as f32;
721 for b in 0..batch {
722 for h in 0..heads {
723 let base_q = ((b * heads + h) * seq_q) * head_dim;
724 let base_kv = ((b * heads + h) * seq_kv) * head_dim;
725 for iq in 0..seq_q {
726 let q_off = base_q + iq * head_dim;
727 let valid = if causal {
729 (iq + seq_kv).saturating_sub(seq_q) + 1
732 } else {
733 seq_kv
734 }
735 .min(seq_kv);
736 let mut scores = vec![f32::NEG_INFINITY; seq_kv];
737 let mut max_s = f32::NEG_INFINITY;
738 for (jk, score) in scores.iter_mut().enumerate().take(valid) {
739 let k_off = base_kv + jk * head_dim;
740 let mut dot = 0.0f32;
741 for d in 0..head_dim {
742 dot += q[q_off + d] * k[k_off + d];
743 }
744 let s = dot * scale;
745 *score = s;
746 if s > max_s {
747 max_s = s;
748 }
749 }
750 let mut denom = 0.0f32;
752 for score in scores.iter_mut().take(valid) {
753 let e = (*score - max_s).exp();
754 *score = e;
755 denom += e;
756 }
757 let inv = if denom > 0.0 { 1.0 / denom } else { 0.0 };
758 let o_off = q_off;
760 for d in 0..head_dim {
761 let mut acc = 0.0f32;
762 for (jk, &score) in scores.iter().enumerate().take(valid) {
763 let v_off = base_kv + jk * head_dim;
764 acc += score * inv * v[v_off + d];
765 }
766 o[o_off + d] = acc;
767 }
768 }
769 }
770 }
771 self.write_f32(o_ptr, &o)
772 }
773
774 fn reduce(
775 &self,
776 op: ReduceOp,
777 input_ptr: u64,
778 output_ptr: u64,
779 shape: &[usize],
780 axis: usize,
781 ) -> BackendResult<()> {
782 if axis >= shape.len() {
783 return Err(BackendError::InvalidArgument(format!(
784 "reduce axis {axis} out of bounds for {}-D shape",
785 shape.len()
786 )));
787 }
788 let total: usize = shape.iter().product();
789 let input = self.read_f32(input_ptr, total)?;
790 let axis_len = shape[axis];
791 let outer: usize = shape[..axis].iter().product();
793 let inner: usize = shape[axis + 1..].iter().product();
794 let mut out = vec![0.0f32; outer * inner];
795
796 for o in 0..outer {
797 for i in 0..inner {
798 let mut acc = match op {
799 ReduceOp::Sum | ReduceOp::Mean => 0.0f32,
800 ReduceOp::Max => f32::NEG_INFINITY,
801 ReduceOp::Min => f32::INFINITY,
802 };
803 for a in 0..axis_len {
804 let idx = (o * axis_len + a) * inner + i;
805 let v = input[idx];
806 acc = match op {
807 ReduceOp::Sum | ReduceOp::Mean => acc + v,
808 ReduceOp::Max => acc.max(v),
809 ReduceOp::Min => acc.min(v),
810 };
811 }
812 if op == ReduceOp::Mean && axis_len > 0 {
813 acc /= axis_len as f32;
814 }
815 out[o * inner + i] = acc;
816 }
817 }
818 self.write_f32(output_ptr, &out)
819 }
820
821 fn unary(&self, op: UnaryOp, input_ptr: u64, output_ptr: u64, n: usize) -> BackendResult<()> {
822 let input = self.read_f32(input_ptr, n)?;
823 let mut out = Vec::with_capacity(n);
824 for &x in &input {
825 out.push(match op {
826 UnaryOp::Relu => x.max(0.0),
827 UnaryOp::Sigmoid => 1.0 / (1.0 + (-x).exp()),
828 UnaryOp::Tanh => x.tanh(),
829 UnaryOp::Exp => x.exp(),
830 UnaryOp::Log => x.ln(),
831 UnaryOp::Sqrt => x.sqrt(),
832 UnaryOp::Abs => x.abs(),
833 UnaryOp::Neg => -x,
834 });
835 }
836 self.write_f32(output_ptr, &out)
837 }
838
839 fn binary(
840 &self,
841 op: BinaryOp,
842 a_ptr: u64,
843 b_ptr: u64,
844 output_ptr: u64,
845 n: usize,
846 ) -> BackendResult<()> {
847 let a = self.read_f32(a_ptr, n)?;
848 let b = self.read_f32(b_ptr, n)?;
849 let mut out = Vec::with_capacity(n);
850 for i in 0..n {
851 out.push(match op {
852 BinaryOp::Add => a[i] + b[i],
853 BinaryOp::Sub => a[i] - b[i],
854 BinaryOp::Mul => a[i] * b[i],
855 BinaryOp::Div => a[i] / b[i],
856 BinaryOp::Max => a[i].max(b[i]),
857 BinaryOp::Min => a[i].min(b[i]),
858 });
859 }
860 self.write_f32(output_ptr, &out)
861 }
862
863 fn softmax(
864 &self,
865 input_ptr: u64,
866 output_ptr: u64,
867 shape: &[usize],
868 axis: usize,
869 ) -> BackendResult<()> {
870 if axis >= shape.len() {
871 return Err(BackendError::InvalidArgument(format!(
872 "softmax axis {axis} out of bounds for {}-D shape",
873 shape.len()
874 )));
875 }
876 let total: usize = shape.iter().product();
877 let input = self.read_f32(input_ptr, total)?;
878 let axis_len = shape[axis];
879 let outer: usize = shape[..axis].iter().product();
880 let inner: usize = shape[axis + 1..].iter().product();
881 let mut out = vec![0.0f32; total];
882
883 for o in 0..outer {
884 for i in 0..inner {
885 let mut max_v = f32::NEG_INFINITY;
887 for a in 0..axis_len {
888 let idx = (o * axis_len + a) * inner + i;
889 max_v = max_v.max(input[idx]);
890 }
891 let mut denom = 0.0f32;
893 for a in 0..axis_len {
894 let idx = (o * axis_len + a) * inner + i;
895 let e = (input[idx] - max_v).exp();
896 out[idx] = e;
897 denom += e;
898 }
899 let inv = if denom > 0.0 { 1.0 / denom } else { 0.0 };
901 for a in 0..axis_len {
902 let idx = (o * axis_len + a) * inner + i;
903 out[idx] *= inv;
904 }
905 }
906 }
907 self.write_f32(output_ptr, &out)
908 }
909
910 fn gather(
911 &self,
912 input_ptr: u64,
913 indices: &[usize],
914 output_ptr: u64,
915 rows: usize,
916 cols: usize,
917 ) -> BackendResult<()> {
918 let table = self.read_f32(input_ptr, rows * cols)?;
919 let mut out = Vec::with_capacity(indices.len() * cols);
920 for &row in indices {
921 if row >= rows {
922 return Err(BackendError::InvalidArgument(format!(
923 "gather index {row} out of bounds for {rows} rows"
924 )));
925 }
926 out.extend_from_slice(&table[row * cols..(row + 1) * cols]);
927 }
928 self.write_f32(output_ptr, &out)
929 }
930
931 fn scatter(
932 &self,
933 input_ptr: u64,
934 indices: &[usize],
935 output_ptr: u64,
936 rows: usize,
937 cols: usize,
938 ) -> BackendResult<()> {
939 let src = self.read_f32(input_ptr, indices.len() * cols)?;
943 let mut dst = self.read_f32(output_ptr, rows * cols)?;
944 for (slot, &row) in indices.iter().enumerate() {
945 if row >= rows {
946 return Err(BackendError::InvalidArgument(format!(
947 "scatter index {row} out of bounds for {rows} rows"
948 )));
949 }
950 dst[row * cols..(row + 1) * cols].copy_from_slice(&src[slot * cols..(slot + 1) * cols]);
951 }
952 self.write_f32(output_ptr, &dst)
953 }
954
955 fn synchronize(&self) -> BackendResult<()> {
956 Ok(())
958 }
959
960 fn alloc(&self, bytes: usize) -> BackendResult<u64> {
961 if bytes == 0 {
962 return Err(BackendError::InvalidArgument(
963 "cannot allocate 0 bytes".into(),
964 ));
965 }
966 let mut next = self
967 .next_ptr
968 .lock()
969 .map_err(|_| BackendError::DeviceError("pointer counter poisoned".into()))?;
970 let ptr = *next;
971 let advance = (bytes as u64).div_ceil(16) * 16;
974 *next = next
975 .checked_add(advance.max(16))
976 .ok_or(BackendError::OutOfMemory)?;
977 drop(next);
978
979 let mut table = self
980 .allocations
981 .lock()
982 .map_err(|_| BackendError::DeviceError("allocation table poisoned".into()))?;
983 table.insert(ptr, vec![0u8; bytes]);
984 Ok(ptr)
985 }
986
987 fn free(&self, ptr: u64) -> BackendResult<()> {
988 let mut table = self
989 .allocations
990 .lock()
991 .map_err(|_| BackendError::DeviceError("allocation table poisoned".into()))?;
992 if table.remove(&ptr).is_none() {
993 return Err(BackendError::InvalidArgument(format!(
994 "free of unknown device pointer {ptr:#x}"
995 )));
996 }
997 Ok(())
998 }
999
1000 fn copy_htod(&self, dst: u64, src: &[u8]) -> BackendResult<()> {
1001 let mut table = self
1002 .allocations
1003 .lock()
1004 .map_err(|_| BackendError::DeviceError("allocation table poisoned".into()))?;
1005 let buf = table.get_mut(&dst).ok_or_else(|| {
1006 BackendError::InvalidArgument(format!("unknown device pointer {dst:#x}"))
1007 })?;
1008 if src.len() > buf.len() {
1009 return Err(BackendError::InvalidArgument(format!(
1010 "copy_htod of {} bytes into {}-byte buffer",
1011 src.len(),
1012 buf.len()
1013 )));
1014 }
1015 buf[..src.len()].copy_from_slice(src);
1016 Ok(())
1017 }
1018
1019 fn copy_dtoh(&self, dst: &mut [u8], src: u64) -> BackendResult<()> {
1020 let table = self
1021 .allocations
1022 .lock()
1023 .map_err(|_| BackendError::DeviceError("allocation table poisoned".into()))?;
1024 let buf = table.get(&src).ok_or_else(|| {
1025 BackendError::InvalidArgument(format!("unknown device pointer {src:#x}"))
1026 })?;
1027 if dst.len() > buf.len() {
1028 return Err(BackendError::InvalidArgument(format!(
1029 "copy_dtoh of {} bytes from {}-byte buffer",
1030 dst.len(),
1031 buf.len()
1032 )));
1033 }
1034 dst.copy_from_slice(&buf[..dst.len()]);
1035 Ok(())
1036 }
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041 use super::*;
1042
1043 fn upload_f32(be: &CpuBackend, data: &[f32]) -> u64 {
1046 let ptr = be.alloc(data.len() * 4).expect("alloc");
1047 let mut bytes = Vec::with_capacity(data.len() * 4);
1048 for &v in data {
1049 bytes.extend_from_slice(&v.to_ne_bytes());
1050 }
1051 be.copy_htod(ptr, &bytes).expect("htod");
1052 ptr
1053 }
1054
1055 fn download_f32(be: &CpuBackend, ptr: u64, len: usize) -> Vec<f32> {
1056 let mut bytes = vec![0u8; len * 4];
1057 be.copy_dtoh(&mut bytes, ptr).expect("dtoh");
1058 bytes
1059 .chunks_exact(4)
1060 .map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
1061 .collect()
1062 }
1063
1064 fn upload_f64(be: &CpuBackend, data: &[f64]) -> u64 {
1065 let ptr = be.alloc(data.len() * 8).expect("alloc");
1066 let mut bytes = Vec::with_capacity(data.len() * 8);
1067 for &v in data {
1068 bytes.extend_from_slice(&v.to_ne_bytes());
1069 }
1070 be.copy_htod(ptr, &bytes).expect("htod");
1071 ptr
1072 }
1073
1074 fn download_f64(be: &CpuBackend, ptr: u64, len: usize) -> Vec<f64> {
1075 let mut bytes = vec![0u8; len * 8];
1076 be.copy_dtoh(&mut bytes, ptr).expect("dtoh");
1077 bytes
1078 .chunks_exact(8)
1079 .map(|c| {
1080 let mut b = [0u8; 8];
1081 b.copy_from_slice(c);
1082 f64::from_ne_bytes(b)
1083 })
1084 .collect()
1085 }
1086
1087 #[test]
1088 fn init_and_name() {
1089 let mut be = CpuBackend::new();
1090 assert_eq!(be.name(), "cpu");
1091 assert!(!be.is_initialized());
1092 be.init().unwrap();
1093 assert!(be.is_initialized());
1094 }
1095
1096 #[test]
1097 fn alloc_copy_roundtrip_and_free() {
1098 let be = CpuBackend::new();
1099 let data = [1.0f32, 2.0, 3.0, 4.0];
1100 let ptr = upload_f32(&be, &data);
1101 assert_eq!(be.live_allocations(), 1);
1102 let back = download_f32(&be, ptr, 4);
1103 assert_eq!(back, data);
1104 be.free(ptr).unwrap();
1105 assert_eq!(be.live_allocations(), 0);
1106 assert!(be.free(ptr).is_err());
1108 }
1109
1110 #[test]
1111 fn alloc_never_reuses_pointer() {
1112 let be = CpuBackend::new();
1113 let p1 = be.alloc(64).unwrap();
1114 be.free(p1).unwrap();
1115 let p2 = be.alloc(64).unwrap();
1116 assert_ne!(p1, p2, "freed address must not be handed out again");
1117 }
1118
1119 #[test]
1120 fn zero_byte_alloc_is_error() {
1121 let be = CpuBackend::new();
1122 assert!(matches!(be.alloc(0), Err(BackendError::InvalidArgument(_))));
1123 }
1124
1125 #[test]
1126 fn gemm_identity_times_matrix() {
1127 let be = CpuBackend::new();
1128 let a = [1.0f64, 0.0, 0.0, 1.0]; let b = [1.0f64, 3.0, 2.0, 4.0]; let a_ptr = upload_f64(&be, &a);
1132 let b_ptr = upload_f64(&be, &b);
1133 let c_ptr = upload_f64(&be, &[0.0f64; 4]);
1134 be.gemm(
1135 BackendTranspose::NoTrans,
1136 BackendTranspose::NoTrans,
1137 2,
1138 2,
1139 2,
1140 1.0,
1141 a_ptr,
1142 2,
1143 b_ptr,
1144 2,
1145 0.0,
1146 c_ptr,
1147 2,
1148 )
1149 .unwrap();
1150 let c = download_f64(&be, c_ptr, 4);
1151 assert_eq!(c, b);
1152 }
1153
1154 #[test]
1155 fn gemm_alpha_beta_and_known_product() {
1156 let be = CpuBackend::new();
1157 let a = [1.0f64, 3.0, 2.0, 4.0];
1160 let b = [5.0f64, 7.0, 6.0, 8.0];
1161 let c0 = [10.0f64, 10.0, 10.0, 10.0]; let a_ptr = upload_f64(&be, &a);
1163 let b_ptr = upload_f64(&be, &b);
1164 let c_ptr = upload_f64(&be, &c0);
1165 be.gemm(
1167 BackendTranspose::NoTrans,
1168 BackendTranspose::NoTrans,
1169 2,
1170 2,
1171 2,
1172 2.0,
1173 a_ptr,
1174 2,
1175 b_ptr,
1176 2,
1177 3.0,
1178 c_ptr,
1179 2,
1180 )
1181 .unwrap();
1182 let c = download_f64(&be, c_ptr, 4);
1183 let expected = [
1185 2.0 * 19.0 + 3.0 * 10.0,
1186 2.0 * 43.0 + 3.0 * 10.0,
1187 2.0 * 22.0 + 3.0 * 10.0,
1188 2.0 * 50.0 + 3.0 * 10.0,
1189 ];
1190 assert_eq!(c, expected);
1191 }
1192
1193 #[test]
1194 fn gemm_transpose_a() {
1195 let be = CpuBackend::new();
1196 let a = [1.0f64, 3.0, 2.0, 4.0];
1199 let b = [1.0f64, 0.0, 0.0, 1.0];
1200 let a_ptr = upload_f64(&be, &a);
1201 let b_ptr = upload_f64(&be, &b);
1202 let c_ptr = upload_f64(&be, &[0.0f64; 4]);
1203 be.gemm(
1204 BackendTranspose::Trans,
1205 BackendTranspose::NoTrans,
1206 2,
1207 2,
1208 2,
1209 1.0,
1210 a_ptr,
1211 2,
1212 b_ptr,
1213 2,
1214 0.0,
1215 c_ptr,
1216 2,
1217 )
1218 .unwrap();
1219 let c = download_f64(&be, c_ptr, 4);
1220 assert_eq!(c, [1.0, 2.0, 3.0, 4.0]);
1222 }
1223
1224 #[test]
1225 fn unary_relu_and_neg() {
1226 let be = CpuBackend::new();
1227 let data = [-2.0f32, -0.5, 0.0, 1.5];
1228 let ip = upload_f32(&be, &data);
1229 let op = be.alloc(4 * 4).unwrap();
1230 be.unary(UnaryOp::Relu, ip, op, 4).unwrap();
1231 assert_eq!(download_f32(&be, op, 4), [0.0, 0.0, 0.0, 1.5]);
1232 be.unary(UnaryOp::Neg, ip, op, 4).unwrap();
1233 assert_eq!(download_f32(&be, op, 4), [2.0, 0.5, 0.0, -1.5]);
1234 }
1235
1236 #[test]
1237 fn binary_ops() {
1238 let be = CpuBackend::new();
1239 let a = [1.0f32, 5.0, 3.0];
1240 let b = [4.0f32, 2.0, 3.0];
1241 let ap = upload_f32(&be, &a);
1242 let bp = upload_f32(&be, &b);
1243 let op = be.alloc(3 * 4).unwrap();
1244 be.binary(BinaryOp::Add, ap, bp, op, 3).unwrap();
1245 assert_eq!(download_f32(&be, op, 3), [5.0, 7.0, 6.0]);
1246 be.binary(BinaryOp::Max, ap, bp, op, 3).unwrap();
1247 assert_eq!(download_f32(&be, op, 3), [4.0, 5.0, 3.0]);
1248 }
1249
1250 #[test]
1251 fn reduce_sum_and_mean_over_axis() {
1252 let be = CpuBackend::new();
1253 let data = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
1255 let ip = upload_f32(&be, &data);
1256 let op = be.alloc(2 * 4).unwrap();
1258 be.reduce(ReduceOp::Sum, ip, op, &[2, 3], 1).unwrap();
1259 assert_eq!(download_f32(&be, op, 2), [6.0, 15.0]);
1260 let op2 = be.alloc(3 * 4).unwrap();
1262 be.reduce(ReduceOp::Sum, ip, op2, &[2, 3], 0).unwrap();
1263 assert_eq!(download_f32(&be, op2, 3), [5.0, 7.0, 9.0]);
1264 let op3 = be.alloc(2 * 4).unwrap();
1266 be.reduce(ReduceOp::Mean, ip, op3, &[2, 3], 1).unwrap();
1267 assert_eq!(download_f32(&be, op3, 2), [2.0, 5.0]);
1268 }
1269
1270 #[test]
1271 fn softmax_axis_sums_to_one() {
1272 let be = CpuBackend::new();
1273 let data = [1.0f32, 2.0, 3.0, 1.0, 1.0, 1.0];
1274 let ip = upload_f32(&be, &data);
1275 let op = be.alloc(6 * 4).unwrap();
1276 be.softmax(ip, op, &[2, 3], 1).unwrap();
1277 let out = download_f32(&be, op, 6);
1278 let row0: f32 = out[..3].iter().sum();
1279 let row1: f32 = out[3..].iter().sum();
1280 assert!((row0 - 1.0).abs() < 1e-6);
1281 assert!((row1 - 1.0).abs() < 1e-6);
1282 for &p in &out[3..] {
1284 assert!((p - 1.0 / 3.0).abs() < 1e-6);
1285 }
1286 }
1287
1288 #[test]
1289 fn gather_selects_rows() {
1290 let be = CpuBackend::new();
1291 let table = [10.0f32, 11.0, 20.0, 21.0, 30.0, 31.0];
1293 let ip = upload_f32(&be, &table);
1294 let op = be.alloc(2 * 2 * 4).unwrap();
1295 be.gather(ip, &[2, 0], op, 3, 2).unwrap();
1296 assert_eq!(download_f32(&be, op, 4), [30.0, 31.0, 10.0, 11.0]);
1297 assert!(be.gather(ip, &[5], op, 3, 2).is_err());
1299 }
1300
1301 #[test]
1302 fn scatter_writes_rows_preserving_others() {
1303 let be = CpuBackend::new();
1304 let dst0 = [0.0f32; 6]; let op = upload_f32(&be, &dst0);
1306 let src = [99.0f32, 98.0]; let ip = upload_f32(&be, &src);
1308 be.scatter(ip, &[1], op, 3, 2).unwrap();
1309 assert_eq!(download_f32(&be, op, 6), [0.0, 0.0, 99.0, 98.0, 0.0, 0.0]);
1311 }
1312
1313 #[test]
1314 fn conv2d_identity_filter() {
1315 let be = CpuBackend::new();
1316 let input: Vec<f32> = (1..=9).map(|x| x as f32).collect();
1319 let ip = upload_f32(&be, &input);
1320 let fp = upload_f32(&be, &[2.0f32]);
1321 let op = be.alloc(9 * 4).unwrap();
1322 be.conv2d_forward(
1323 ip,
1324 &[1, 1, 3, 3],
1325 fp,
1326 &[1, 1, 1, 1],
1327 op,
1328 &[1, 1, 3, 3],
1329 &[1, 1],
1330 &[0, 0],
1331 )
1332 .unwrap();
1333 let out = download_f32(&be, op, 9);
1334 let expected: Vec<f32> = input.iter().map(|x| x * 2.0).collect();
1335 assert_eq!(out, expected);
1336 }
1337
1338 #[test]
1339 fn conv2d_rejects_wrong_output_size() {
1340 let be = CpuBackend::new();
1341 let ip = be.alloc(9 * 4).unwrap();
1342 let fp = be.alloc(4 * 4).unwrap();
1343 let op = be.alloc(9 * 4).unwrap();
1344 let err = be.conv2d_forward(
1347 ip,
1348 &[1, 1, 3, 3],
1349 fp,
1350 &[1, 1, 2, 2],
1351 op,
1352 &[1, 1, 3, 3],
1353 &[1, 1],
1354 &[0, 0],
1355 );
1356 assert!(matches!(err, Err(BackendError::InvalidArgument(_))));
1357 }
1358
1359 #[test]
1360 fn attention_uniform_keys_averages_values() {
1361 let be = CpuBackend::new();
1362 let q = [0.0f32, 0.0];
1365 let k = [1.0f32, 1.0, 2.0, 2.0];
1366 let v = [10.0f32, 20.0, 30.0, 40.0];
1367 let qp = upload_f32(&be, &q);
1368 let kp = upload_f32(&be, &k);
1369 let vp = upload_f32(&be, &v);
1370 let op = be.alloc(2 * 4).unwrap();
1371 be.attention(qp, kp, vp, op, 1, 1, 1, 2, 2, 1.0, false)
1372 .unwrap();
1373 let out = download_f32(&be, op, 2);
1374 assert!((out[0] - 20.0).abs() < 1e-5);
1376 assert!((out[1] - 30.0).abs() < 1e-5);
1377 }
1378
1379 #[test]
1380 fn attention_causal_first_query_sees_only_first_key() {
1381 let be = CpuBackend::new();
1382 let q = [0.0f32, 0.0, 0.0, 0.0]; let k = [0.0f32, 0.0, 0.0, 0.0];
1385 let v = [1.0f32, 1.0, 5.0, 5.0]; let qp = upload_f32(&be, &q);
1387 let kp = upload_f32(&be, &k);
1388 let vp = upload_f32(&be, &v);
1389 let op = be.alloc(4 * 4).unwrap();
1390 be.attention(qp, kp, vp, op, 1, 1, 2, 2, 2, 1.0, true)
1391 .unwrap();
1392 let out = download_f32(&be, op, 4);
1393 assert!((out[0] - 1.0).abs() < 1e-5);
1395 assert!((out[1] - 1.0).abs() < 1e-5);
1396 assert!((out[2] - 3.0).abs() < 1e-5);
1398 assert!((out[3] - 3.0).abs() < 1e-5);
1399 }
1400
1401 #[test]
1402 fn batched_gemm_default_runs_on_cpu() {
1403 let be = CpuBackend::new();
1408 let a = [1.0f64, 0.0, 0.0, 1.0];
1409 let b = [2.0f64, 3.0, 4.0, 5.0];
1410 let a_ptr = upload_f64(&be, &a);
1411 let b_ptr = upload_f64(&be, &b);
1412 let c_ptr = upload_f64(&be, &[0.0f64; 4]);
1413 be.batched_gemm(
1414 BackendTranspose::NoTrans,
1415 BackendTranspose::NoTrans,
1416 2,
1417 2,
1418 2,
1419 1.0,
1420 a_ptr,
1421 2,
1422 0,
1423 b_ptr,
1424 2,
1425 0,
1426 0.0,
1427 c_ptr,
1428 2,
1429 0,
1430 1,
1431 )
1432 .unwrap();
1433 assert_eq!(download_f64(&be, c_ptr, 4), b);
1434 }
1435
1436 #[test]
1437 fn unknown_pointer_errors() {
1438 let be = CpuBackend::new();
1439 let mut dst = [0u8; 4];
1440 assert!(be.copy_dtoh(&mut dst, 0xDEAD).is_err());
1441 assert!(be.copy_htod(0xDEAD, &[0u8; 4]).is_err());
1442 }
1443
1444 #[test]
1445 fn capabilities_and_devices() {
1446 let be = CpuBackend::new();
1447 assert_eq!(be.capabilities(), Capabilities::cpu());
1448 let devs = be.available_devices().unwrap();
1449 assert_eq!(devs.len(), 1);
1450 assert_eq!(devs[0].memory_kind, MemoryKind::Host);
1451 }
1452
1453 use crate::ops::MixedPrecision;
1456 use crate::precision::{round_to_bf16, round_to_f16};
1457
1458 fn ref_gemm_f32(m: usize, n: usize, k: usize, a: &[f32], b: &[f32]) -> Vec<f32> {
1460 let mut c = vec![0.0f32; m * n];
1461 for j in 0..n {
1462 for i in 0..m {
1463 let mut acc = 0.0f32;
1464 for p in 0..k {
1465 acc += a[p * m + i] * b[j * k + p];
1466 }
1467 c[j * m + i] = acc;
1468 }
1469 }
1470 c
1471 }
1472
1473 #[test]
1474 fn mixed_precision_bf16_matches_f32_within_rounding_tolerance() {
1475 let be = CpuBackend::new();
1476 let (m, n, k) = (4, 3, 5);
1477 let a: Vec<f32> = (0..m * k)
1479 .map(|i| ((i * 7 % 11) as f32) * 0.25 - 1.0)
1480 .collect();
1481 let b: Vec<f32> = (0..k * n)
1482 .map(|i| ((i * 5 % 13) as f32) * 0.125 - 0.5)
1483 .collect();
1484
1485 let a_ptr = upload_f32(&be, &a);
1486 let b_ptr = upload_f32(&be, &b);
1487 let c_ptr = upload_f32(&be, &vec![0.0f32; m * n]);
1488 be.gemm_mixed_precision(
1489 MixedPrecision::Bf16,
1490 BackendTranspose::NoTrans,
1491 BackendTranspose::NoTrans,
1492 m,
1493 n,
1494 k,
1495 1.0,
1496 a_ptr,
1497 m,
1498 b_ptr,
1499 k,
1500 0.0,
1501 c_ptr,
1502 m,
1503 )
1504 .unwrap();
1505 let got = download_f32(&be, c_ptr, m * n);
1506
1507 let a_r: Vec<f32> = a.iter().map(|&v| round_to_bf16(v)).collect();
1510 let b_r: Vec<f32> = b.iter().map(|&v| round_to_bf16(v)).collect();
1511 let want = ref_gemm_f32(m, n, k, &a_r, &b_r);
1512 for (g, w) in got.iter().zip(want.iter()) {
1513 assert!((g - w).abs() < 1e-6, "bf16 gemm {g} vs {w}");
1514 }
1515
1516 let exact = ref_gemm_f32(m, n, k, &a, &b);
1519 for (g, e) in got.iter().zip(exact.iter()) {
1520 let tol = 1e-2 * (1.0 + e.abs());
1521 assert!((g - e).abs() < tol, "bf16 gemm {g} vs exact {e}");
1522 }
1523 }
1524
1525 #[test]
1526 fn mixed_precision_f16_matches_f32_within_rounding_tolerance() {
1527 let be = CpuBackend::new();
1528 let (m, n, k) = (3, 4, 6);
1529 let a: Vec<f32> = (0..m * k)
1530 .map(|i| ((i * 3 % 7) as f32) * 0.5 - 1.5)
1531 .collect();
1532 let b: Vec<f32> = (0..k * n)
1533 .map(|i| ((i * 9 % 5) as f32) * 0.25 - 0.5)
1534 .collect();
1535
1536 let a_ptr = upload_f32(&be, &a);
1537 let b_ptr = upload_f32(&be, &b);
1538 let c_ptr = upload_f32(&be, &vec![0.0f32; m * n]);
1539 be.gemm_mixed_precision(
1540 MixedPrecision::F16,
1541 BackendTranspose::NoTrans,
1542 BackendTranspose::NoTrans,
1543 m,
1544 n,
1545 k,
1546 1.0,
1547 a_ptr,
1548 m,
1549 b_ptr,
1550 k,
1551 0.0,
1552 c_ptr,
1553 m,
1554 )
1555 .unwrap();
1556 let got = download_f32(&be, c_ptr, m * n);
1557
1558 let a_r: Vec<f32> = a.iter().map(|&v| round_to_f16(v)).collect();
1559 let b_r: Vec<f32> = b.iter().map(|&v| round_to_f16(v)).collect();
1560 let want = ref_gemm_f32(m, n, k, &a_r, &b_r);
1561 for (g, w) in got.iter().zip(want.iter()) {
1562 assert!((g - w).abs() < 1e-6, "f16 gemm {g} vs {w}");
1563 }
1564 }
1565
1566 #[test]
1567 fn mixed_precision_bf16_exact_for_representable_operands() {
1568 let be = CpuBackend::new();
1572 let (m, n, k) = (2, 2, 3);
1573 let a = [1.0f32, 2.0, -1.0, 0.5, 4.0, -2.0]; let b = [0.5f32, 1.0, 2.0, -1.0, 0.25, 8.0]; let a_ptr = upload_f32(&be, &a);
1577 let b_ptr = upload_f32(&be, &b);
1578 let c_ptr = upload_f32(&be, &vec![0.0f32; m * n]);
1579 be.gemm_mixed_precision(
1580 MixedPrecision::Bf16,
1581 BackendTranspose::NoTrans,
1582 BackendTranspose::NoTrans,
1583 m,
1584 n,
1585 k,
1586 1.0,
1587 a_ptr,
1588 m,
1589 b_ptr,
1590 k,
1591 0.0,
1592 c_ptr,
1593 m,
1594 )
1595 .unwrap();
1596 let got = download_f32(&be, c_ptr, m * n);
1597 let want = ref_gemm_f32(m, n, k, &a, &b);
1598 assert_eq!(got, want, "exact bf16 operands must match f32 exactly");
1599 }
1600
1601 #[test]
1602 fn mixed_precision_accumulates_in_f32_not_f16() {
1603 let be = CpuBackend::new();
1611 let k = 512usize;
1612 let (m, n) = (1, 1);
1613 let a = vec![1.0f32; k]; let inc = 1.0f32 / 256.0; let b = vec![inc; k]; let a_ptr = upload_f32(&be, &a);
1617 let b_ptr = upload_f32(&be, &b);
1618 let c_ptr = upload_f32(&be, &[0.0f32]);
1619 be.gemm_mixed_precision(
1620 MixedPrecision::Bf16,
1621 BackendTranspose::NoTrans,
1622 BackendTranspose::NoTrans,
1623 m,
1624 n,
1625 k,
1626 1.0,
1627 a_ptr,
1628 m,
1629 b_ptr,
1630 k,
1631 0.0,
1632 c_ptr,
1633 m,
1634 )
1635 .unwrap();
1636 let got = download_f32(&be, c_ptr, 1)[0];
1637 let expected = k as f32 * inc; assert!((got - expected).abs() < 1e-5, "f32-accumulated dot = {got}");
1639
1640 let mut bf16_acc = 0.0f32;
1644 for _ in 0..k {
1645 bf16_acc = round_to_bf16(bf16_acc + inc);
1646 }
1647 assert!(
1648 bf16_acc < expected - 0.1,
1649 "bf16 accumulation should stall ({bf16_acc} < {expected})"
1650 );
1651 assert!(got > bf16_acc + 0.1);
1653 }
1654
1655 #[test]
1656 fn mixed_precision_alpha_beta_and_transpose() {
1657 let be = CpuBackend::new();
1658 let a = [1.0f32, 3.0, 2.0, 4.0]; let b = [1.0f32, 0.0, 0.0, 1.0];
1661 let c0 = [1.0f32, 1.0, 1.0, 1.0];
1662 let a_ptr = upload_f32(&be, &a);
1663 let b_ptr = upload_f32(&be, &b);
1664 let c_ptr = upload_f32(&be, &c0);
1665 be.gemm_mixed_precision(
1666 MixedPrecision::Bf16,
1667 BackendTranspose::Trans,
1668 BackendTranspose::NoTrans,
1669 2,
1670 2,
1671 2,
1672 2.0,
1673 a_ptr,
1674 2,
1675 b_ptr,
1676 2,
1677 3.0,
1678 c_ptr,
1679 2,
1680 )
1681 .unwrap();
1682 let got = download_f32(&be, c_ptr, 4);
1683 assert_eq!(got, [5.0, 7.0, 9.0, 11.0]);
1685 }
1686
1687 #[test]
1688 fn mixed_precision_rejects_bad_leading_dim() {
1689 let be = CpuBackend::new();
1690 let a_ptr = be.alloc(4 * 4).unwrap();
1691 let b_ptr = be.alloc(4 * 4).unwrap();
1692 let c_ptr = be.alloc(4 * 4).unwrap();
1693 let err = be.gemm_mixed_precision(
1694 MixedPrecision::F16,
1695 BackendTranspose::NoTrans,
1696 BackendTranspose::NoTrans,
1697 2,
1698 2,
1699 2,
1700 1.0,
1701 a_ptr,
1702 1, b_ptr,
1704 2,
1705 0.0,
1706 c_ptr,
1707 2,
1708 );
1709 assert!(matches!(err, Err(BackendError::InvalidArgument(_))));
1710 }
1711
1712 fn forward_conv(
1716 be: &CpuBackend,
1717 input: &[f32],
1718 in_shape: [usize; 4],
1719 filter: &[f32],
1720 f_shape: [usize; 4],
1721 out_shape: [usize; 4],
1722 stride: [usize; 2],
1723 pad: [usize; 2],
1724 ) -> Vec<f32> {
1725 let ip = upload_f32(be, input);
1726 let fp = upload_f32(be, filter);
1727 let out_len: usize = out_shape.iter().product();
1728 let op = be.alloc(out_len * 4).unwrap();
1729 be.conv2d_forward(ip, &in_shape, fp, &f_shape, op, &out_shape, &stride, &pad)
1730 .unwrap();
1731 let out = download_f32(be, op, out_len);
1732 be.free(ip).unwrap();
1733 be.free(fp).unwrap();
1734 be.free(op).unwrap();
1735 out
1736 }
1737
1738 #[allow(clippy::too_many_arguments)]
1742 fn conv_loss(
1743 be: &CpuBackend,
1744 input: &[f32],
1745 in_shape: [usize; 4],
1746 filter: &[f32],
1747 f_shape: [usize; 4],
1748 out_shape: [usize; 4],
1749 stride: [usize; 2],
1750 pad: [usize; 2],
1751 grad_output: &[f32],
1752 ) -> f32 {
1753 let y = forward_conv(be, input, in_shape, filter, f_shape, out_shape, stride, pad);
1754 y.iter().zip(grad_output.iter()).map(|(a, b)| a * b).sum()
1755 }
1756
1757 #[test]
1758 fn conv2d_backward_data_matches_finite_difference() {
1759 let be = CpuBackend::new();
1760 let in_shape = [1, 2, 4, 4];
1762 let f_shape = [3, 2, 3, 3];
1763 let out_shape = [1, 3, 4, 4];
1764 let stride = [1, 1];
1765 let pad = [1, 1];
1766 let in_len: usize = in_shape.iter().product();
1767 let f_len: usize = f_shape.iter().product();
1768 let out_len: usize = out_shape.iter().product();
1769
1770 let input: Vec<f32> = (0..in_len)
1772 .map(|i| ((i * 13 % 17) as f32) * 0.1 - 0.8)
1773 .collect();
1774 let filter: Vec<f32> = (0..f_len)
1775 .map(|i| ((i * 7 % 11) as f32) * 0.1 - 0.5)
1776 .collect();
1777 let grad_output: Vec<f32> = (0..out_len)
1778 .map(|i| ((i * 5 % 9) as f32) * 0.2 - 0.8)
1779 .collect();
1780
1781 let gop = upload_f32(&be, &grad_output);
1783 let fp = upload_f32(&be, &filter);
1784 let gip = be.alloc(in_len * 4).unwrap();
1785 be.conv2d_backward_data(gop, &out_shape, fp, &f_shape, gip, &in_shape, &stride, &pad)
1786 .unwrap();
1787 let analytic = download_f32(&be, gip, in_len);
1788
1789 let eps = 1e-2f32;
1791 for idx in 0..in_len {
1792 let mut plus = input.clone();
1793 let mut minus = input.clone();
1794 plus[idx] += eps;
1795 minus[idx] -= eps;
1796 let lp = conv_loss(
1797 &be,
1798 &plus,
1799 in_shape,
1800 &filter,
1801 f_shape,
1802 out_shape,
1803 stride,
1804 pad,
1805 &grad_output,
1806 );
1807 let lm = conv_loss(
1808 &be,
1809 &minus,
1810 in_shape,
1811 &filter,
1812 f_shape,
1813 out_shape,
1814 stride,
1815 pad,
1816 &grad_output,
1817 );
1818 let fd = (lp - lm) / (2.0 * eps);
1819 assert!(
1820 (analytic[idx] - fd).abs() < 1e-2,
1821 "grad_input[{idx}] analytic {} vs finite-diff {fd}",
1822 analytic[idx]
1823 );
1824 }
1825 }
1826
1827 #[test]
1828 fn conv2d_backward_filter_matches_finite_difference() {
1829 let be = CpuBackend::new();
1830 let in_shape = [2, 2, 5, 5];
1833 let f_shape = [2, 2, 3, 3];
1834 let out_shape = [2, 2, 3, 3];
1835 let stride = [2, 2];
1836 let pad = [1, 1];
1837 let in_len: usize = in_shape.iter().product();
1838 let f_len: usize = f_shape.iter().product();
1839 let out_len: usize = out_shape.iter().product();
1840
1841 let input: Vec<f32> = (0..in_len)
1842 .map(|i| ((i * 11 % 19) as f32) * 0.07 - 0.6)
1843 .collect();
1844 let filter: Vec<f32> = (0..f_len)
1845 .map(|i| ((i * 3 % 13) as f32) * 0.1 - 0.6)
1846 .collect();
1847 let grad_output: Vec<f32> = (0..out_len)
1848 .map(|i| ((i * 17 % 7) as f32) * 0.15 - 0.4)
1849 .collect();
1850
1851 let ip = upload_f32(&be, &input);
1853 let gop = upload_f32(&be, &grad_output);
1854 let gfp = be.alloc(f_len * 4).unwrap();
1855 be.conv2d_backward_filter(ip, &in_shape, gop, &out_shape, gfp, &f_shape, &stride, &pad)
1856 .unwrap();
1857 let analytic = download_f32(&be, gfp, f_len);
1858
1859 let eps = 1e-2f32;
1861 for idx in 0..f_len {
1862 let mut plus = filter.clone();
1863 let mut minus = filter.clone();
1864 plus[idx] += eps;
1865 minus[idx] -= eps;
1866 let lp = conv_loss(
1867 &be,
1868 &input,
1869 in_shape,
1870 &plus,
1871 f_shape,
1872 out_shape,
1873 stride,
1874 pad,
1875 &grad_output,
1876 );
1877 let lm = conv_loss(
1878 &be,
1879 &input,
1880 in_shape,
1881 &minus,
1882 f_shape,
1883 out_shape,
1884 stride,
1885 pad,
1886 &grad_output,
1887 );
1888 let fd = (lp - lm) / (2.0 * eps);
1889 assert!(
1890 (analytic[idx] - fd).abs() < 1e-2,
1891 "grad_filter[{idx}] analytic {} vs finite-diff {fd}",
1892 analytic[idx]
1893 );
1894 }
1895 }
1896
1897 #[test]
1898 fn conv2d_backward_data_known_1x1_filter() {
1899 let be = CpuBackend::new();
1902 let grad_output: Vec<f32> = (1..=9).map(|x| x as f32).collect();
1903 let gop = upload_f32(&be, &grad_output);
1904 let fp = upload_f32(&be, &[3.0f32]);
1905 let gip = be.alloc(9 * 4).unwrap();
1906 be.conv2d_backward_data(
1907 gop,
1908 &[1, 1, 3, 3],
1909 fp,
1910 &[1, 1, 1, 1],
1911 gip,
1912 &[1, 1, 3, 3],
1913 &[1, 1],
1914 &[0, 0],
1915 )
1916 .unwrap();
1917 let got = download_f32(&be, gip, 9);
1918 let want: Vec<f32> = grad_output.iter().map(|g| g * 3.0).collect();
1919 assert_eq!(got, want);
1920 }
1921
1922 #[test]
1923 fn conv2d_backward_rejects_shape_mismatch() {
1924 let be = CpuBackend::new();
1925 let gop = be.alloc(9 * 4).unwrap();
1926 let fp = be.alloc(4 * 4).unwrap();
1927 let gip = be.alloc(9 * 4).unwrap();
1928 let err = be.conv2d_backward_data(
1931 gop,
1932 &[1, 1, 3, 3],
1933 fp,
1934 &[1, 1, 2, 2],
1935 gip,
1936 &[1, 1, 3, 3],
1937 &[1, 1],
1938 &[0, 0],
1939 );
1940 assert!(matches!(err, Err(BackendError::InvalidArgument(_))));
1941
1942 let ip = be.alloc(9 * 4).unwrap();
1944 let err2 = be.conv2d_backward_filter(
1945 ip,
1946 &[1, 1, 3, 3],
1947 gop,
1948 &[1, 1, 3, 3],
1949 fp,
1950 &[1, 1, 2, 2],
1951 &[1, 1],
1952 &[0, 0],
1953 );
1954 assert!(matches!(err2, Err(BackendError::InvalidArgument(_))));
1955 }
1956}