1use std::cell::UnsafeCell;
17use std::sync::Arc;
18use std::time::Instant;
19
20use kime_tensor::plan::{Epilogue, Graph, Op, Rows, Val, layout};
21use kime_tensor::{Backend, Batch, Bucket, Caps, Error, HostTensor, Outputs, Result};
22
23use crate::attention::{self, HEAD, QB};
24use crate::gemm::{self, Gemm};
25use crate::ops::{Rope, geglu, layer_norm};
26use crate::par::{self, Shared};
27use crate::pool::Pool;
28use crate::qgemm::{self, QGemm, QMatrix};
29
30#[derive(Debug)]
32pub struct Tensor {
33 pub shape: Vec<usize>,
35 pub data: Vec<f32>,
37 pub packed: Vec<f32>,
40 pub quant: Option<QMatrix>,
42}
43
44#[derive(Debug, Clone)]
46pub struct Weights(Arc<[Tensor]>);
47
48#[derive(Debug)]
50pub struct CpuBackend {
51 pool: Pool,
52 int8: bool,
53}
54
55impl CpuBackend {
56 #[must_use]
58 pub fn new(threads: usize) -> Self {
59 Self { pool: Pool::new(threads.max(1)), int8: false }
60 }
61
62 #[must_use]
66 pub fn with_int8(mut self, on: bool) -> Self {
67 self.int8 = on;
68 self
69 }
70
71 #[must_use]
73 pub fn int8(&self) -> bool {
74 self.int8
75 }
76
77 fn int8_rows(&self, rows: Rows) -> bool {
79 self.int8 && rows == Rows::Tokens
80 }
81
82 #[must_use]
84 pub fn threads(&self) -> usize {
85 self.pool.threads()
86 }
87}
88
89#[derive(Debug, Clone, Copy)]
91struct Loc {
92 off: usize,
93 rows: Rows,
94 width: usize,
95}
96
97#[derive(Debug, Clone, Copy)]
98enum Step {
99 Embed { table: usize, out: Loc },
100 LayerNorm { x: Loc, w: usize, b: Option<usize>, eps: f64, out: Loc },
101 Gemm { a: Loc, w: usize, b: Option<usize>, ep: Epilogue, out: Loc },
102 Gemm8 { a: Loc, w: usize, b: Option<usize>, ep: Epilogue, out: Loc },
103 Rope { qkv: Loc, rope: usize },
104 Attention { qkv: Loc, window: Option<usize>, out: Loc },
105 GeGlu { x: Loc, out: Loc },
106 AddType { h: Loc, table: usize },
107 Gather { h: Loc, out: Loc },
108 ActFeatures { h: Loc, logits: Loc, out: Loc },
109}
110
111impl Step {
112 fn name(&self) -> &'static str {
113 match self {
114 Step::Embed { .. } => "embed",
115 Step::LayerNorm { .. } => "layer norm",
116 Step::Gemm { .. } => "gemm",
117 Step::Gemm8 { .. } => "gemm int8",
118 Step::Rope { .. } => "rope",
119 Step::Attention { .. } => "attention",
120 Step::GeGlu { .. } => "geglu",
121 Step::AddType { .. } => "type embedding",
122 Step::Gather { .. } => "gather markers",
123 Step::ActFeatures { .. } => "act features",
124 }
125 }
126
127 fn out(&self) -> Loc {
129 match *self {
130 Step::Embed { out, .. }
131 | Step::LayerNorm { out, .. }
132 | Step::Gemm { out, .. }
133 | Step::Gemm8 { out, .. }
134 | Step::Attention { out, .. }
135 | Step::GeGlu { out, .. }
136 | Step::Gather { out, .. }
137 | Step::ActFeatures { out, .. } => out,
138 Step::Rope { qkv, .. } => qkv,
139 Step::AddType { h, .. } => h,
140 }
141 }
142}
143
144#[derive(Debug, Clone)]
146pub struct Dump {
147 pub name: &'static str,
149 pub rows: Rows,
151 pub width: usize,
153 pub data: Vec<f32>,
155}
156
157struct PerWorker<T>(Vec<UnsafeCell<T>>);
159
160unsafe impl<T: Send> Sync for PerWorker<T> {}
163
164impl<T> PerWorker<T> {
165 #[allow(clippy::mut_from_ref)]
169 unsafe fn get(&self, worker: usize) -> &mut T {
170 unsafe { &mut *self.0[worker].get() }
172 }
173}
174
175pub struct CpuPlan {
177 w: Weights,
178 bucket: Bucket,
179 steps: Vec<Step>,
180 arena: Vec<f32>,
181 ropes: Vec<Rope>,
182 logits: Loc,
183 act: Loc,
184 cu: Vec<usize>,
187 mcu: Vec<usize>,
188 row_seq: Vec<u32>,
189 blocks: Vec<(u32, u32)>,
190 scratch: PerWorker<Vec<f32>>,
191 profile: Option<Vec<u64>>,
193 dumps: Option<Vec<Dump>>,
195}
196
197impl std::fmt::Debug for CpuPlan {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 f.debug_struct("CpuPlan")
200 .field("bucket", &self.bucket)
201 .field("steps", &self.steps.len())
202 .field("arena", &self.arena.len())
203 .finish_non_exhaustive()
204 }
205}
206
207impl CpuPlan {
208 #[must_use]
210 pub fn bucket(&self) -> Bucket {
211 self.bucket
212 }
213
214 #[must_use]
216 pub fn arena_bytes(&self) -> usize {
217 self.arena.len() * 4
218 }
219
220 pub fn profile(&mut self) {
222 self.profile = Some(vec![0; self.steps.len()]);
223 }
224
225 pub fn dump(&mut self) {
228 self.dumps = Some(Vec::new());
229 }
230
231 #[must_use]
233 pub fn dumps(&self) -> &[Dump] {
234 self.dumps.as_deref().unwrap_or_default()
235 }
236
237 #[must_use]
239 pub fn timings(&self) -> Vec<(&'static str, u64)> {
240 let mut by: Vec<(&'static str, u64)> = Vec::new();
241 for (s, &ns) in self.steps.iter().zip(self.profile.iter().flatten()) {
242 match by.iter_mut().find(|b| b.0 == s.name()) {
243 Some(b) => b.1 += ns,
244 None => by.push((s.name(), ns)),
245 }
246 }
247 by.sort_by_key(|b| std::cmp::Reverse(b.1));
248 by
249 }
250}
251
252#[derive(Clone, Copy)]
254struct Arena(*mut f32);
255
256unsafe impl Send for Arena {}
259unsafe impl Sync for Arena {}
261
262impl Arena {
263 unsafe fn slice<'a>(self, off: usize, len: usize) -> &'a [f32] {
267 unsafe { std::slice::from_raw_parts(self.0.add(off), len) }
269 }
270
271 #[allow(clippy::mut_from_ref)]
275 unsafe fn slice_mut<'a>(self, off: usize, len: usize) -> &'a mut [f32] {
276 unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
278 }
279}
280
281const ROWS: usize = 16;
283
284impl Backend for CpuBackend {
285 type Weights = Weights;
286 type Plan = CpuPlan;
287
288 fn caps(&self) -> Caps {
289 Caps { name: "cpu", threads: self.threads(), graphs: false, unified_memory: true }
290 }
291
292 fn upload(&self, tensors: &[HostTensor<'_>], graph: &Graph) -> Result<Weights> {
293 let (mut gemm, mut other) = (vec![false; tensors.len()], vec![false; tensors.len()]);
296 let mut gemm8 = vec![false; tensors.len()];
297 let mark = |flags: &mut Vec<bool>, w: Option<usize>| {
298 if let Some(f) = w.and_then(|w| flags.get_mut(w)) {
299 *f = true;
300 }
301 };
302 for op in &graph.ops {
303 match *op {
304 Op::Gemm { a, w, b, .. } => {
305 match self.int8_rows(graph.shape(a).rows) {
306 true => mark(&mut gemm8, Some(w)),
307 false => mark(&mut gemm, Some(w)),
308 }
309 mark(&mut other, b);
310 }
311 Op::Embed { table, .. } | Op::AddType { table, .. } => {
312 mark(&mut other, Some(table))
313 }
314 Op::LayerNorm { w, b, .. } => {
315 mark(&mut other, Some(w));
316 mark(&mut other, b);
317 }
318 Op::Rope { .. }
319 | Op::Attention { .. }
320 | Op::GeGlu { .. }
321 | Op::GatherMarkers { .. }
322 | Op::ActFeatures { .. } => {}
323 }
324 }
325 let t = par::map(tensors.len(), self.threads(), |i| {
326 let h = &tensors[i];
327 let n = h.bytes.len() / h.dtype.size();
328 if n != h.shape.iter().product::<usize>() {
329 return Err(Error::Unsupported(format!(
330 "tensor {i} has {n} values for {:?}",
331 h.shape
332 )));
333 }
334 let data: Vec<f32> = (0..n).map(|j| h.dtype.read_f32(h.bytes, j)).collect();
335 let packed = match (gemm[i], h.shape) {
336 (true, &[rows, cols]) => gemm::pack(&data, rows, cols),
337 _ => Vec::new(),
338 };
339 let quant = match (gemm8[i], h.shape) {
340 (true, &[rows, cols]) => Some(QMatrix::quantize(&data, rows, cols)),
341 _ => None,
342 };
343 let copied = (!gemm[i] || !packed.is_empty()) && (!gemm8[i] || quant.is_some());
345 let data = if (gemm[i] || gemm8[i]) && !other[i] && copied { Vec::new() } else { data };
346 Ok(Tensor { shape: h.shape.to_vec(), data, packed, quant })
347 });
348 Ok(Weights(t.into_iter().collect::<Result<Vec<_>>>()?.into()))
349 }
350
351 fn lower(&self, w: &Weights, graph: &Graph, bucket: Bucket) -> Result<CpuPlan> {
352 let lay = layout(graph, |r| bucket.rows(r));
353 let loc = |v: Val| {
354 let s = graph.shape(v);
355 Loc { off: lay.offsets[v.0 as usize], rows: s.rows, width: s.width }
356 };
357 let bad = |m: String| Err(Error::Unsupported(m));
358 let shape = |i: usize| -> Result<&[usize]> {
359 match w.0.get(i) {
360 Some(t) => Ok(&t.shape),
361 None => Err(Error::Unsupported(format!("weight {i} is not in the checkpoint"))),
362 }
363 };
364 let mut ropes: Vec<(u64, Rope)> = Vec::new();
365 let mut scratch = bucket.tokens;
366 let mut steps = Vec::with_capacity(graph.ops.len());
367 for (i, op) in graph.ops.iter().enumerate() {
368 let step = match *op {
369 Op::Embed { table, out } => {
370 let out = loc(out);
371 if shape(table)?.get(1) != Some(&out.width) || out.rows != Rows::Tokens {
372 return bad(format!("op {i}: embedding table does not match its output"));
373 }
374 Step::Embed { table, out }
375 }
376 Op::LayerNorm { x, w: nw, b, eps, out } => {
377 let (x, out) = (loc(x), loc(out));
378 let ok = shape(nw)? == [x.width]
379 && b.map_or(Ok(true), |b| shape(b).map(|s| s == [x.width]))?
380 && x.width == out.width
381 && x.rows == out.rows;
382 if !ok {
383 return bad(format!("op {i}: layer norm shapes do not match"));
384 }
385 Step::LayerNorm { x, w: nw, b, eps, out }
386 }
387 Op::Gemm { a, w: gw, b, epilogue, out } => {
388 let (a, out) = (loc(a), loc(out));
389 let ok = shape(gw)? == [out.width, a.width]
390 && b.map_or(Ok(true), |b| shape(b).map(|s| s == [out.width]))?
391 && a.rows == out.rows;
392 if !ok {
393 return bad(format!("op {i}: gemm shapes do not match"));
394 }
395 if self.int8_rows(a.rows) {
396 if w.0[gw].quant.is_none() {
397 return bad(format!("op {i}: gemm weight {gw} was not rounded"));
398 }
399 scratch = scratch.max(qgemm::scratch_len(a.width));
400 Step::Gemm8 { a, w: gw, b, ep: epilogue, out }
401 } else {
402 if w.0[gw].packed.is_empty() && a.width * out.width > 0 {
403 return bad(format!("op {i}: gemm weight {gw} was not packed"));
404 }
405 scratch = scratch.max(gemm::scratch_len(a.width, out.width));
406 Step::Gemm { a, w: gw, b, ep: epilogue, out }
407 }
408 }
409 Op::Rope { qkv, theta } => {
410 let qkv = loc(qkv);
411 if !qkv.width.is_multiple_of(3 * HEAD) || qkv.rows != Rows::Tokens {
412 return bad(format!("op {i}: rope needs token rows of 3 heads 64"));
413 }
414 let at = match ropes.iter().position(|r| r.0 == theta.to_bits()) {
415 Some(at) => at,
416 None => {
417 ropes.push((theta.to_bits(), Rope::new(theta, HEAD, bucket.tokens)));
418 ropes.len() - 1
419 }
420 };
421 Step::Rope { qkv, rope: at }
422 }
423 Op::Attention { qkv, window, out } => {
424 let (qkv, out) = (loc(qkv), loc(out));
425 let ok = qkv.width.is_multiple_of(3 * HEAD)
426 && out.width * 3 == qkv.width
427 && qkv.rows == Rows::Tokens
428 && out.rows == Rows::Tokens;
429 if !ok {
430 return bad(format!("op {i}: attention shapes do not match"));
431 }
432 Step::Attention { qkv, window, out }
433 }
434 Op::GeGlu { x, out } => {
435 let (x, out) = (loc(x), loc(out));
436 if x.width != 2 * out.width || x.rows != out.rows {
437 return bad(format!("op {i}: geglu input is not twice its output"));
438 }
439 Step::GeGlu { x, out }
440 }
441 Op::AddType { h, table } => {
442 let h = loc(h);
443 if shape(table)?.get(1) != Some(&h.width) || h.rows != Rows::Tokens {
444 return bad(format!("op {i}: type table does not match"));
445 }
446 Step::AddType { h, table }
447 }
448 Op::GatherMarkers { h, out } => {
449 let (h, out) = (loc(h), loc(out));
450 if h.width != out.width || h.rows != Rows::Tokens || out.rows != Rows::Markers {
451 return bad(format!("op {i}: gather shapes do not match"));
452 }
453 Step::Gather { h, out }
454 }
455 Op::ActFeatures { h, logits, out } => {
456 let (h, logits, out) = (loc(h), loc(logits), loc(out));
457 let ok = out.width == h.width + 4
458 && logits.width == 1
459 && logits.rows == Rows::Markers
460 && out.rows == Rows::Seqs;
461 if !ok {
462 return bad(format!("op {i}: act feature shapes do not match"));
463 }
464 Step::ActFeatures { h, logits, out }
465 }
466 };
467 steps.push(step);
468 }
469 let (Some(logits), Some(act)) = (graph.logits, graph.act) else {
470 return bad("the graph has no logits or act output".into());
471 };
472 let (logits, act) = (loc(logits), loc(act));
473 if logits.width != 1
474 || logits.rows != Rows::Markers
475 || act.width != 2
476 || act.rows != Rows::Seqs
477 {
478 return bad(
479 "outputs must be one logit per marker and two act logits per sequence".into()
480 );
481 }
482 let threads = self.threads();
483 Ok(CpuPlan {
484 w: w.clone(),
485 bucket,
486 steps,
487 arena: vec![0.0; lay.len],
488 ropes: ropes.into_iter().map(|r| r.1).collect(),
489 logits,
490 act,
491 cu: Vec::with_capacity(bucket.seqs + 1),
492 mcu: Vec::with_capacity(bucket.seqs + 1),
493 row_seq: Vec::with_capacity(bucket.tokens),
494 blocks: Vec::with_capacity(bucket.tokens.div_ceil(QB) + bucket.seqs),
495 scratch: PerWorker(
496 (0..threads).map(|_| UnsafeCell::new(Vec::with_capacity(scratch))).collect(),
497 ),
498 profile: None,
499 dumps: None,
500 })
501 }
502
503 fn run(&self, plan: &mut CpuPlan, batch: &Batch<'_>, out: &mut Outputs) -> Result<()> {
504 let (t, s, m) = (batch.ids.len(), batch.seqs(), batch.markers.len());
505 if !plan.bucket.holds(t, s, m) {
506 return Err(Error::Batch(format!("batch does not fit bucket {}", plan.bucket)));
507 }
508 plan.cu.clear();
509 plan.cu.extend(batch.cu.iter().map(|&c| c as usize));
510 plan.mcu.clear();
511 plan.mcu.extend(batch.mcu.iter().map(|&c| c as usize));
512 plan.row_seq.clear();
513 plan.blocks.clear();
514 for q in 0..s {
515 let (lo, hi) = (plan.cu[q], plan.cu[q + 1]);
516 plan.row_seq.extend(std::iter::repeat_n(q as u32, hi - lo));
517 plan.blocks.extend((lo..hi).step_by(QB).map(|q0| (q as u32, q0 as u32)));
518 }
519 let ctx = Ctx {
520 pool: &self.pool,
521 w: &plan.w.0,
522 arena: Arena(plan.arena.as_mut_ptr()),
523 ropes: &plan.ropes,
524 batch,
525 cu: &plan.cu,
526 mcu: &plan.mcu,
527 row_seq: &plan.row_seq,
528 blocks: &plan.blocks,
529 scratch: &plan.scratch,
530 counts: [t, s, m],
531 };
532 if let Some(d) = plan.dumps.as_mut() {
533 d.clear();
534 }
535 for (i, step) in plan.steps.iter().enumerate() {
536 match plan.profile.as_mut() {
537 None => ctx.step(step),
538 Some(p) => {
539 let at = Instant::now();
540 ctx.step(step);
541 p[i] += u64::try_from(at.elapsed().as_nanos()).unwrap_or(u64::MAX);
542 }
543 }
544 if let Some(d) = plan.dumps.as_mut() {
546 let l = step.out();
547 let live = unsafe { ctx.arena.slice(l.off, ctx.rows(l.rows) * l.width) };
550 d.push(Dump {
551 name: step.name(),
552 rows: l.rows,
553 width: l.width,
554 data: live.to_vec(),
555 });
556 }
557 }
558
559 let logits = unsafe { ctx.arena.slice(plan.logits.off, m) };
561 let act = unsafe { ctx.arena.slice(plan.act.off, 2 * s) };
563 out.logits.clear();
564 out.logits.extend_from_slice(logits);
565 out.act.clear();
566 out.act.extend_from_slice(act.as_chunks::<2>().0);
567 Ok(())
568 }
569}
570
571struct Ctx<'a> {
573 pool: &'a Pool,
574 w: &'a [Tensor],
575 arena: Arena,
576 ropes: &'a [Rope],
577 batch: &'a Batch<'a>,
578 cu: &'a [usize],
579 mcu: &'a [usize],
580 row_seq: &'a [u32],
581 blocks: &'a [(u32, u32)],
582 scratch: &'a PerWorker<Vec<f32>>,
583 counts: [usize; 3],
584}
585
586impl Ctx<'_> {
587 fn rows(&self, r: Rows) -> usize {
588 self.counts[r as usize]
589 }
590
591 fn w(&self, i: usize) -> &[f32] {
592 &self.w[i].data
593 }
594
595 unsafe fn get(&self, l: Loc) -> &[f32] {
601 unsafe { self.arena.slice(l.off, self.rows(l.rows) * l.width) }
603 }
604
605 unsafe fn rows_of(&self, out: Loc, f: &(dyn Fn(usize, &mut [f32]) + Sync)) {
611 let n = self.rows(out.rows);
612 let arena = self.arena;
613 self.pool.run(n.div_ceil(ROWS), &|task, _| {
614 let r0 = task * ROWS;
615 let r1 = (r0 + ROWS).min(n);
616 let rows = unsafe { arena.slice_mut(out.off + r0 * out.width, (r1 - r0) * out.width) };
618 f(r0, rows);
619 });
620 }
621
622 fn step(&self, step: &Step) {
623 match *step {
627 Step::Embed { table, out } => {
628 let (tab, ids, d) = (self.w(table), self.batch.ids, out.width);
629 unsafe {
631 self.rows_of(out, &|r0, rows| {
632 for (i, row) in rows.chunks_exact_mut(d).enumerate() {
633 let id = ids[r0 + i] as usize;
634 row.copy_from_slice(&tab[id * d..(id + 1) * d]);
635 }
636 });
637 }
638 }
639 Step::LayerNorm { x, w, b, eps, out } => {
640 let x = unsafe { self.get(x) };
642 let (nw, nb, d) = (self.w(w), b.map(|b| self.w(b)), out.width);
643 unsafe {
645 self.rows_of(out, &|r0, rows| {
646 layer_norm(&x[r0 * d..r0 * d + rows.len()], d, nw, nb, eps, rows);
647 });
648 }
649 }
650 Step::Gemm { a, w, b, ep, out } => {
651 let rows = self.rows(a.rows);
652 let x = unsafe { self.get(a) };
654 let y = unsafe { self.arena.slice_mut(out.off, rows * out.width) };
656 let g = Gemm {
657 x,
658 m: rows,
659 k: a.width,
660 w: &self.w[w].packed,
661 n: out.width,
662 b: b.map(|b| self.w(b)),
663 ep,
664 };
665 let scratch = self.scratch;
666 g.run(y, self.pool.threads(), |n, f| {
667 self.pool.run(n, &|i, worker| {
668 let s = unsafe { scratch.get(worker) };
671 s.resize(gemm::scratch_len(g.k, g.n), 0.0);
672 f(i, s);
673 });
674 });
675 }
676 Step::Gemm8 { a, w, b, ep, out } => {
677 let rows = self.rows(a.rows);
678 let x = unsafe { self.get(a) };
680 let y = unsafe { self.arena.slice_mut(out.off, rows * out.width) };
682 let q = self.w[w]
683 .quant
684 .as_ref()
685 .unwrap_or_else(|| unreachable!("checked when lowered"));
686 let g = QGemm { x, m: rows, w: q, b: b.map(|b| self.w(b)), ep };
687 let scratch = self.scratch;
688 g.run(y, self.pool.threads(), |n, f| {
689 self.pool.run(n, &|i, worker| {
690 let s = unsafe { scratch.get(worker) };
693 s.resize(qgemm::scratch_len(q.k), 0.0);
694 f(i, s);
695 });
696 });
697 }
698 Step::Rope { qkv, rope } => {
699 let (rope, d, cu, seq) = (&self.ropes[rope], qkv.width / 3, self.cu, self.row_seq);
700 unsafe {
702 self.rows_of(qkv, &|r0, rows| {
703 for (i, row) in rows.chunks_exact_mut(qkv.width).enumerate() {
704 let r = r0 + i;
705 let pos = r - cu[seq[r] as usize];
706 for head in row[..2 * d].as_chunks_mut::<HEAD>().0 {
707 rope.apply(head, pos);
708 }
709 }
710 });
711 }
712 }
713 Step::Attention { qkv, window, out } => {
714 let heads = out.width / HEAD;
715 let x = unsafe { self.get(qkv) };
717 let y = unsafe { self.arena.slice_mut(out.off, self.rows(out.rows) * out.width) };
719 let shared = Shared::new(y);
720 let (blocks, cu, scratch) = (self.blocks, self.cu, self.scratch);
721 self.pool.run(blocks.len() * heads, &|task, worker| {
722 let (s, q0) = blocks[task / heads];
723 let (s, q0, h) = (s as usize, q0 as usize, task % heads);
724 unsafe {
727 let p = scratch.get(worker);
728 attention::block(x, heads, (cu[s], cu[s + 1]), q0, h, window, p, &shared);
729 }
730 });
731 }
732 Step::GeGlu { x, out } => {
733 let (x, d) = (unsafe { self.get(x) }, out.width);
735 unsafe {
737 self.rows_of(out, &|r0, rows| {
738 geglu(&x[2 * r0 * d..2 * (r0 * d + rows.len())], d, rows);
739 });
740 }
741 }
742 Step::AddType { h, table } => {
743 let (tab, d, seq, qt) = (self.w(table), h.width, self.row_seq, self.batch.qtype);
744 unsafe {
746 self.rows_of(h, &|r0, rows| {
747 for (i, row) in rows.chunks_exact_mut(d).enumerate() {
748 let q = usize::from(qt[seq[r0 + i] as usize]);
749 row.iter_mut().zip(&tab[q * d..(q + 1) * d]).for_each(|(a, b)| *a += b);
750 }
751 });
752 }
753 }
754 Step::Gather { h, out } => {
755 let (x, d) = (unsafe { self.get(h) }, h.width);
757 let y = unsafe { self.arena.slice_mut(out.off, self.rows(out.rows) * d) };
759 let mut at = 0;
760 for s in 0..self.cu.len() - 1 {
761 for &p in &self.batch.markers[self.mcu[s]..self.mcu[s + 1]] {
762 let r = self.cu[s] + p as usize;
763 y[at * d..(at + 1) * d].copy_from_slice(&x[r * d..(r + 1) * d]);
764 at += 1;
765 }
766 }
767 }
768 Step::ActFeatures { h, logits, out } => {
769 let (x, d) = (unsafe { self.get(h) }, h.width);
771 let l = unsafe { self.get(logits) };
773 let y = unsafe { self.arena.slice_mut(out.off, self.rows(out.rows) * out.width) };
775 for (s, row) in y.chunks_exact_mut(out.width).enumerate() {
776 let (lo, hi) = (self.cu[s], self.cu[s + 1]);
777 if hi > lo {
778 row[..d].copy_from_slice(&x[lo * d..(lo + 1) * d]);
779 } else {
780 row[..d].fill(0.0);
781 }
782 row[d..].copy_from_slice(&act_features(&l[self.mcu[s]..self.mcu[s + 1]]));
783 }
784 }
785 }
786 }
787}
788
789fn act_features(logits: &[f32]) -> [f32; 4] {
792 let kf = logits.len().max(2) as f32;
793 if logits.is_empty() {
794 return [0.0, 0.0, 0.0, kf / 255.0];
795 }
796 let mx = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
797 let sum: f32 = logits.iter().map(|&l| (l - mx).exp()).sum();
798 let (mut top1, mut top2, mut ent) = (f32::NEG_INFINITY, 0f32, 0f32);
799 let mut first = true;
800 for &l in logits {
801 let q = (l - mx).exp() / sum;
802 ent += q * q.max(1e-9).ln();
803 if q > top1 || first {
804 if !first {
805 top2 = top1;
806 }
807 top1 = q;
808 first = false;
809 } else if q > top2 {
810 top2 = q;
811 }
812 }
813 let ent = -ent / kf.ln();
814 [top1, top1 - top2, ent, kf / 255.0]
815}
816
817#[cfg(test)]
818mod tests {
819 use super::*;
820
821 #[test]
822 fn act_features_match_the_reference() {
823 let cases: [&[f32]; 6] =
824 [&[], &[0.3], &[1.0, 1.0], &[2.0, -1.0, 2.0], &[5.0, 0.1, -3.0, 4.9], &[-1e3, 0.0]];
825 for l in cases {
826 assert_eq!(
827 act_features(l).map(f32::to_bits),
828 crate::compat::act_features(l).map(f32::to_bits),
829 "{l:?}"
830 );
831 }
832 }
833}