Skip to main content

kime_cpu/
plan.rs

1//! The CPU backend: graphs lowered to a list of steps over one arena, run on a persistent pool.
2//!
3//! Lowering resolves every value to an arena offset, every weight to its converted tensor and every
4//! RoPE base to its table, and checks every shape, so running a step is a match and a call. The
5//! arena, the per worker attention scratch and the per batch index tables are sized for the bucket
6//! when the plan is built, and nothing on the run path allocates.
7//!
8//! Each step runs on the rows the batch has rather than the bucket's padded count. Padding only
9//! matters to a backend that captures a fixed shape, and on the CPU it would be wasted work. The
10//! kernels are the reference ones, so a plan gives the same bits as [`Compat`](crate::Compat).
11//!
12//! A backend made [`with_int8`](CpuBackend::with_int8) runs the GEMMs over token rows in INT8
13//! instead, which is every GEMM of the encoder and the decision head. The scorer and the act head
14//! run on a row per option or per question and stay in FP32, as spec/10-cpu.md has it.
15
16use 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/// A weight converted to f32.
31#[derive(Debug)]
32pub struct Tensor {
33    /// Shape.
34    pub shape: Vec<usize>,
35    /// Row major values, empty for a weight only GEMMs read.
36    pub data: Vec<f32>,
37    /// The values as [`gemm::pack`] lays them out, for a weight FP32 GEMMs read, and empty
38    /// otherwise.
39    pub packed: Vec<f32>,
40    /// The values rounded to INT8, for a weight INT8 GEMMs read.
41    pub quant: Option<QMatrix>,
42}
43
44/// Every weight of a checkpoint, shared by all the plans built from it.
45#[derive(Debug, Clone)]
46pub struct Weights(Arc<[Tensor]>);
47
48/// The CPU backend, which owns its threads.
49#[derive(Debug)]
50pub struct CpuBackend {
51    pool: Pool,
52    int8: bool,
53}
54
55impl CpuBackend {
56    /// A backend on `threads` threads, the calling thread included.
57    #[must_use]
58    pub fn new(threads: usize) -> Self {
59        Self { pool: Pool::new(threads.max(1)), int8: false }
60    }
61
62    /// The same backend with the GEMMs over token rows in INT8 when `on`: weights rounded per
63    /// output channel at upload, activations per row as they are read, sums in i32. See
64    /// [`qgemm`].
65    #[must_use]
66    pub fn with_int8(mut self, on: bool) -> Self {
67        self.int8 = on;
68        self
69    }
70
71    /// Whether the GEMMs over token rows run in INT8.
72    #[must_use]
73    pub fn int8(&self) -> bool {
74        self.int8
75    }
76
77    /// Whether a GEMM reading `a` runs in INT8.
78    fn int8_rows(&self, rows: Rows) -> bool {
79        self.int8 && rows == Rows::Tokens
80    }
81
82    /// Threads.
83    #[must_use]
84    pub fn threads(&self) -> usize {
85        self.pool.threads()
86    }
87}
88
89/// A value resolved to its place in the arena.
90#[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    MeanPool { h: Loc, out: Loc },
110}
111
112impl Step {
113    fn name(&self) -> &'static str {
114        match self {
115            Step::Embed { .. } => "embed",
116            Step::LayerNorm { .. } => "layer norm",
117            Step::Gemm { .. } => "gemm",
118            Step::Gemm8 { .. } => "gemm int8",
119            Step::Rope { .. } => "rope",
120            Step::Attention { .. } => "attention",
121            Step::GeGlu { .. } => "geglu",
122            Step::AddType { .. } => "type embedding",
123            Step::Gather { .. } => "gather markers",
124            Step::ActFeatures { .. } => "act features",
125            Step::MeanPool { .. } => "mean pool",
126        }
127    }
128
129    /// Where the step writes.
130    fn out(&self) -> Loc {
131        match *self {
132            Step::Embed { out, .. }
133            | Step::LayerNorm { out, .. }
134            | Step::Gemm { out, .. }
135            | Step::Gemm8 { out, .. }
136            | Step::Attention { out, .. }
137            | Step::GeGlu { out, .. }
138            | Step::Gather { out, .. }
139            | Step::ActFeatures { out, .. }
140            | Step::MeanPool { out, .. } => out,
141            Step::Rope { qkv, .. } => qkv,
142            Step::AddType { h, .. } => h,
143        }
144    }
145}
146
147/// What one step wrote in one run, from [`CpuPlan::dumps`].
148#[derive(Debug, Clone)]
149pub struct Dump {
150    /// The kind of step.
151    pub name: &'static str,
152    /// Whether the rows are tokens, sequences or markers.
153    pub rows: Rows,
154    /// Values per row.
155    pub width: usize,
156    /// The live rows, row major.
157    pub data: Vec<f32>,
158}
159
160/// One slot per worker, each touched only by its own worker.
161struct PerWorker<T>(Vec<UnsafeCell<T>>);
162
163// SAFETY: slot w is only reached through `get(w)` from the task running on worker w, and the pool
164// never runs two tasks on one worker at once.
165unsafe impl<T: Send> Sync for PerWorker<T> {}
166
167impl<T> PerWorker<T> {
168    /// # Safety
169    ///
170    /// Only the task running on `worker` may call this, and only for its own worker.
171    #[allow(clippy::mut_from_ref)]
172    unsafe fn get(&self, worker: usize) -> &mut T {
173        // SAFETY: the caller is the only user of slot `worker` right now.
174        unsafe { &mut *self.0[worker].get() }
175    }
176}
177
178/// A graph lowered for one bucket.
179pub struct CpuPlan {
180    w: Weights,
181    bucket: Bucket,
182    steps: Vec<Step>,
183    arena: Vec<f32>,
184    ropes: Vec<Rope>,
185    logits: Option<Loc>,
186    act: Option<Loc>,
187    pooled: Option<Loc>,
188    /// Token starts of each sequence, then marker starts, then the sequence of each token, then
189    /// the query blocks attention runs. Rebuilt per batch in place.
190    cu: Vec<usize>,
191    mcu: Vec<usize>,
192    row_seq: Vec<u32>,
193    blocks: Vec<(u32, u32)>,
194    scratch: PerWorker<Vec<f32>>,
195    /// Nanoseconds per step, summed over runs, when profiling.
196    profile: Option<Vec<u64>>,
197    /// Every step's output from the last run, when dumping.
198    dumps: Option<Vec<Dump>>,
199}
200
201impl std::fmt::Debug for CpuPlan {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        f.debug_struct("CpuPlan")
204            .field("bucket", &self.bucket)
205            .field("steps", &self.steps.len())
206            .field("arena", &self.arena.len())
207            .finish_non_exhaustive()
208    }
209}
210
211impl CpuPlan {
212    /// The bucket it was built for.
213    #[must_use]
214    pub fn bucket(&self) -> Bucket {
215        self.bucket
216    }
217
218    /// Arena size in bytes.
219    #[must_use]
220    pub fn arena_bytes(&self) -> usize {
221        self.arena.len() * 4
222    }
223
224    /// Starts timing every step, which costs two clock reads per step.
225    pub fn profile(&mut self) {
226        self.profile = Some(vec![0; self.steps.len()]);
227    }
228
229    /// Keeps a copy of what every step writes from now on, for finding the first step where two
230    /// runs differ. It allocates on every run, so it is for tests and debugging only.
231    pub fn dump(&mut self) {
232        self.dumps = Some(Vec::new());
233    }
234
235    /// What every step wrote in the last run since [`CpuPlan::dump`], in order.
236    #[must_use]
237    pub fn dumps(&self) -> &[Dump] {
238        self.dumps.as_deref().unwrap_or_default()
239    }
240
241    /// Time per kind of step since [`CpuPlan::profile`], in nanoseconds, largest first.
242    #[must_use]
243    pub fn timings(&self) -> Vec<(&'static str, u64)> {
244        let mut by: Vec<(&'static str, u64)> = Vec::new();
245        for (s, &ns) in self.steps.iter().zip(self.profile.iter().flatten()) {
246            match by.iter_mut().find(|b| b.0 == s.name()) {
247                Some(b) => b.1 += ns,
248                None => by.push((s.name(), ns)),
249            }
250        }
251        by.sort_by_key(|b| std::cmp::Reverse(b.1));
252        by
253    }
254}
255
256/// A pointer to an arena that steps carve disjoint slices from.
257#[derive(Clone, Copy)]
258struct Arena(*mut f32);
259
260// SAFETY: the plan hands out slices of the arena only as the layout allows: values live at the
261// same time never overlap, and within a step each task writes rows no other task touches.
262unsafe impl Send for Arena {}
263// SAFETY: as above.
264unsafe impl Sync for Arena {}
265
266impl Arena {
267    /// # Safety
268    ///
269    /// The range must be in the arena and not written by anyone else while the slice lives.
270    unsafe fn slice<'a>(self, off: usize, len: usize) -> &'a [f32] {
271        // SAFETY: by the caller.
272        unsafe { std::slice::from_raw_parts(self.0.add(off), len) }
273    }
274
275    /// # Safety
276    ///
277    /// The range must be in the arena and not touched by anyone else while the slice lives.
278    #[allow(clippy::mut_from_ref)]
279    unsafe fn slice_mut<'a>(self, off: usize, len: usize) -> &'a mut [f32] {
280        // SAFETY: by the caller.
281        unsafe { std::slice::from_raw_parts_mut(self.0.add(off), len) }
282    }
283}
284
285/// Rows per task for the row wise steps.
286const ROWS: usize = 16;
287
288impl Backend for CpuBackend {
289    type Weights = Weights;
290    type Plan = CpuPlan;
291
292    fn caps(&self) -> Caps {
293        Caps { name: "cpu", threads: self.threads(), graphs: false, unified_memory: true }
294    }
295
296    fn weight_bytes(&self, w: &Weights) -> usize {
297        w.0.iter()
298            .map(|t| {
299                let q = t.quant.as_ref().map_or(0, |q| q.q.len() + 4 * q.scale.len());
300                4 * (t.data.len() + t.packed.len()) + q
301            })
302            .sum()
303    }
304
305    fn plan_bytes(&self, p: &CpuPlan) -> usize {
306        p.arena_bytes()
307    }
308
309    fn upload(&self, tensors: &[HostTensor<'_>], graph: &Graph) -> Result<Weights> {
310        // Weights the GEMMs read are packed or rounded once here, and kept row major only if
311        // something else reads them too.
312        let (mut gemm, mut other) = (vec![false; tensors.len()], vec![false; tensors.len()]);
313        let mut gemm8 = vec![false; tensors.len()];
314        let mark = |flags: &mut Vec<bool>, w: Option<usize>| {
315            if let Some(f) = w.and_then(|w| flags.get_mut(w)) {
316                *f = true;
317            }
318        };
319        for op in &graph.ops {
320            match *op {
321                Op::Gemm { a, w, b, .. } => {
322                    match self.int8_rows(graph.shape(a).rows) {
323                        true => mark(&mut gemm8, Some(w)),
324                        false => mark(&mut gemm, Some(w)),
325                    }
326                    mark(&mut other, b);
327                }
328                Op::Embed { table, .. } | Op::AddType { table, .. } => {
329                    mark(&mut other, Some(table))
330                }
331                Op::LayerNorm { w, b, .. } => {
332                    mark(&mut other, Some(w));
333                    mark(&mut other, b);
334                }
335                Op::Rope { .. }
336                | Op::Attention { .. }
337                | Op::GeGlu { .. }
338                | Op::GatherMarkers { .. }
339                | Op::ActFeatures { .. }
340                | Op::MeanPool { .. } => {}
341            }
342        }
343        let t = par::map(tensors.len(), self.threads(), |i| {
344            let h = &tensors[i];
345            let n = h.bytes.len() / h.dtype.size();
346            if n != h.shape.iter().product::<usize>() {
347                return Err(Error::Unsupported(format!(
348                    "tensor {i} has {n} values for {:?}",
349                    h.shape
350                )));
351            }
352            let data: Vec<f32> = (0..n).map(|j| h.dtype.read_f32(h.bytes, j)).collect();
353            let packed = match (gemm[i], h.shape) {
354                (true, &[rows, cols]) => gemm::pack(&data, rows, cols),
355                _ => Vec::new(),
356            };
357            let quant = match (gemm8[i], h.shape) {
358                (true, &[rows, cols]) => Some(QMatrix::quantize(&data, rows, cols)),
359                _ => None,
360            };
361            // The row major values go when every reader has its own copy.
362            let copied = (!gemm[i] || !packed.is_empty()) && (!gemm8[i] || quant.is_some());
363            let data = if (gemm[i] || gemm8[i]) && !other[i] && copied { Vec::new() } else { data };
364            Ok(Tensor { shape: h.shape.to_vec(), data, packed, quant })
365        });
366        Ok(Weights(t.into_iter().collect::<Result<Vec<_>>>()?.into()))
367    }
368
369    fn lower(&self, w: &Weights, graph: &Graph, bucket: Bucket) -> Result<CpuPlan> {
370        let lay = layout(graph, |r| bucket.rows(r));
371        let loc = |v: Val| {
372            let s = graph.shape(v);
373            Loc { off: lay.offsets[v.0 as usize], rows: s.rows, width: s.width }
374        };
375        let bad = |m: String| Err(Error::Unsupported(m));
376        let shape = |i: usize| -> Result<&[usize]> {
377            match w.0.get(i) {
378                Some(t) => Ok(&t.shape),
379                None => Err(Error::Unsupported(format!("weight {i} is not in the checkpoint"))),
380            }
381        };
382        let mut ropes: Vec<(u64, Rope)> = Vec::new();
383        let mut scratch = attention::scratch_len(bucket.tokens);
384        let mut steps = Vec::with_capacity(graph.ops.len());
385        for (i, op) in graph.ops.iter().enumerate() {
386            let step = match *op {
387                Op::Embed { table, out } => {
388                    let out = loc(out);
389                    if shape(table)?.get(1) != Some(&out.width) || out.rows != Rows::Tokens {
390                        return bad(format!("op {i}: embedding table does not match its output"));
391                    }
392                    Step::Embed { table, out }
393                }
394                Op::LayerNorm { x, w: nw, b, eps, out } => {
395                    let (x, out) = (loc(x), loc(out));
396                    let ok = shape(nw)? == [x.width]
397                        && b.map_or(Ok(true), |b| shape(b).map(|s| s == [x.width]))?
398                        && x.width == out.width
399                        && x.rows == out.rows;
400                    if !ok {
401                        return bad(format!("op {i}: layer norm shapes do not match"));
402                    }
403                    Step::LayerNorm { x, w: nw, b, eps, out }
404                }
405                Op::Gemm { a, w: gw, b, epilogue, out } => {
406                    let (a, out) = (loc(a), loc(out));
407                    let ok = shape(gw)? == [out.width, a.width]
408                        && b.map_or(Ok(true), |b| shape(b).map(|s| s == [out.width]))?
409                        && a.rows == out.rows;
410                    if !ok {
411                        return bad(format!("op {i}: gemm shapes do not match"));
412                    }
413                    if self.int8_rows(a.rows) {
414                        if w.0[gw].quant.is_none() {
415                            return bad(format!("op {i}: gemm weight {gw} was not rounded"));
416                        }
417                        scratch = scratch.max(qgemm::scratch_len(a.width));
418                        Step::Gemm8 { a, w: gw, b, ep: epilogue, out }
419                    } else {
420                        if w.0[gw].packed.is_empty() && a.width * out.width > 0 {
421                            return bad(format!("op {i}: gemm weight {gw} was not packed"));
422                        }
423                        scratch = scratch.max(gemm::scratch_len(a.width, out.width));
424                        Step::Gemm { a, w: gw, b, ep: epilogue, out }
425                    }
426                }
427                Op::Rope { qkv, theta } => {
428                    let qkv = loc(qkv);
429                    if !qkv.width.is_multiple_of(3 * HEAD) || qkv.rows != Rows::Tokens {
430                        return bad(format!("op {i}: rope needs token rows of 3 heads 64"));
431                    }
432                    let at = match ropes.iter().position(|r| r.0 == theta.to_bits()) {
433                        Some(at) => at,
434                        None => {
435                            ropes.push((theta.to_bits(), Rope::new(theta, HEAD, bucket.tokens)));
436                            ropes.len() - 1
437                        }
438                    };
439                    Step::Rope { qkv, rope: at }
440                }
441                Op::Attention { qkv, window, out } => {
442                    let (qkv, out) = (loc(qkv), loc(out));
443                    let ok = qkv.width.is_multiple_of(3 * HEAD)
444                        && out.width * 3 == qkv.width
445                        && qkv.rows == Rows::Tokens
446                        && out.rows == Rows::Tokens;
447                    if !ok {
448                        return bad(format!("op {i}: attention shapes do not match"));
449                    }
450                    Step::Attention { qkv, window, out }
451                }
452                Op::GeGlu { x, out } => {
453                    let (x, out) = (loc(x), loc(out));
454                    if x.width != 2 * out.width || x.rows != out.rows {
455                        return bad(format!("op {i}: geglu input is not twice its output"));
456                    }
457                    Step::GeGlu { x, out }
458                }
459                Op::AddType { h, table } => {
460                    let h = loc(h);
461                    if shape(table)?.get(1) != Some(&h.width) || h.rows != Rows::Tokens {
462                        return bad(format!("op {i}: type table does not match"));
463                    }
464                    Step::AddType { h, table }
465                }
466                Op::GatherMarkers { h, out } => {
467                    let (h, out) = (loc(h), loc(out));
468                    if h.width != out.width || h.rows != Rows::Tokens || out.rows != Rows::Markers {
469                        return bad(format!("op {i}: gather shapes do not match"));
470                    }
471                    Step::Gather { h, out }
472                }
473                Op::ActFeatures { h, logits, out } => {
474                    let (h, logits, out) = (loc(h), loc(logits), loc(out));
475                    let ok = out.width == h.width + 4
476                        && logits.width == 1
477                        && logits.rows == Rows::Markers
478                        && out.rows == Rows::Seqs;
479                    if !ok {
480                        return bad(format!("op {i}: act feature shapes do not match"));
481                    }
482                    Step::ActFeatures { h, logits, out }
483                }
484                Op::MeanPool { h, out } => {
485                    let (h, out) = (loc(h), loc(out));
486                    if h.rows != Rows::Tokens || out.rows != Rows::Seqs || out.width != h.width {
487                        return bad(format!("op {i}: mean pool shapes do not match"));
488                    }
489                    Step::MeanPool { h, out }
490                }
491            };
492            steps.push(step);
493        }
494        let (logits, act, pooled) =
495            (graph.logits.map(loc), graph.act.map(loc), graph.pooled.map(loc));
496        if logits.is_some() != act.is_some() || (logits.is_none() && pooled.is_none()) {
497            return bad("the graph needs logits and act outputs, a pooled output, or both".into());
498        }
499        if logits.is_some_and(|l| l.width != 1 || l.rows != Rows::Markers)
500            || act.is_some_and(|a| a.width != 2 || a.rows != Rows::Seqs)
501        {
502            return bad(
503                "outputs must be one logit per marker and two act logits per sequence".into()
504            );
505        }
506        if pooled.is_some_and(|p| p.rows != Rows::Seqs) {
507            return bad("the pooled output must have one row per sequence".into());
508        }
509        let threads = self.threads();
510        Ok(CpuPlan {
511            w: w.clone(),
512            bucket,
513            steps,
514            arena: vec![0.0; lay.len],
515            ropes: ropes.into_iter().map(|r| r.1).collect(),
516            logits,
517            act,
518            pooled,
519            cu: Vec::with_capacity(bucket.seqs + 1),
520            mcu: Vec::with_capacity(bucket.seqs + 1),
521            row_seq: Vec::with_capacity(bucket.tokens),
522            blocks: Vec::with_capacity(bucket.tokens.div_ceil(QB) + bucket.seqs),
523            scratch: PerWorker(
524                (0..threads).map(|_| UnsafeCell::new(Vec::with_capacity(scratch))).collect(),
525            ),
526            profile: None,
527            dumps: None,
528        })
529    }
530
531    fn run(&self, plan: &mut CpuPlan, batch: &Batch<'_>, out: &mut Outputs) -> Result<()> {
532        let (t, s, m) = (batch.ids.len(), batch.seqs(), batch.markers.len());
533        if !plan.bucket.holds(t, s, m) {
534            return Err(Error::Batch(format!("batch does not fit bucket {}", plan.bucket)));
535        }
536        plan.cu.clear();
537        plan.cu.extend(batch.cu.iter().map(|&c| c as usize));
538        plan.mcu.clear();
539        plan.mcu.extend(batch.mcu.iter().map(|&c| c as usize));
540        plan.row_seq.clear();
541        plan.blocks.clear();
542        for q in 0..s {
543            let (lo, hi) = (plan.cu[q], plan.cu[q + 1]);
544            plan.row_seq.extend(std::iter::repeat_n(q as u32, hi - lo));
545            plan.blocks.extend((lo..hi).step_by(QB).map(|q0| (q as u32, q0 as u32)));
546        }
547        let ctx = Ctx {
548            pool: &self.pool,
549            w: &plan.w.0,
550            arena: Arena(plan.arena.as_mut_ptr()),
551            ropes: &plan.ropes,
552            batch,
553            cu: &plan.cu,
554            mcu: &plan.mcu,
555            row_seq: &plan.row_seq,
556            blocks: &plan.blocks,
557            scratch: &plan.scratch,
558            counts: [t, s, m],
559        };
560        if let Some(d) = plan.dumps.as_mut() {
561            d.clear();
562        }
563        for (i, step) in plan.steps.iter().enumerate() {
564            match plan.profile.as_mut() {
565                None => ctx.step(step),
566                Some(p) => {
567                    let at = Instant::now();
568                    ctx.step(step);
569                    p[i] += u64::try_from(at.elapsed().as_nanos()).unwrap_or(u64::MAX);
570                }
571            }
572            // Copied now, because a later step may reuse the same part of the arena.
573            if let Some(d) = plan.dumps.as_mut() {
574                let l = step.out();
575                // SAFETY: the step is done and the next has not started, so nothing else
576                // touches the arena.
577                let live = unsafe { ctx.arena.slice(l.off, ctx.rows(l.rows) * l.width) };
578                d.push(Dump {
579                    name: step.name(),
580                    rows: l.rows,
581                    width: l.width,
582                    data: live.to_vec(),
583                });
584            }
585        }
586
587        out.logits.clear();
588        out.act.clear();
589        out.pooled.clear();
590        // SAFETY: the steps are done, so nothing else touches the arena.
591        unsafe {
592            if let Some(l) = plan.logits {
593                out.logits.extend_from_slice(ctx.arena.slice(l.off, m));
594            }
595            if let Some(a) = plan.act {
596                out.act.extend_from_slice(ctx.arena.slice(a.off, 2 * s).as_chunks::<2>().0);
597            }
598            if let Some(p) = plan.pooled {
599                out.pooled.extend_from_slice(ctx.arena.slice(p.off, p.width * s));
600            }
601        }
602        Ok(())
603    }
604}
605
606/// Everything a step needs for one batch.
607struct Ctx<'a> {
608    pool: &'a Pool,
609    w: &'a [Tensor],
610    arena: Arena,
611    ropes: &'a [Rope],
612    batch: &'a Batch<'a>,
613    cu: &'a [usize],
614    mcu: &'a [usize],
615    row_seq: &'a [u32],
616    blocks: &'a [(u32, u32)],
617    scratch: &'a PerWorker<Vec<f32>>,
618    counts: [usize; 3],
619}
620
621impl Ctx<'_> {
622    fn rows(&self, r: Rows) -> usize {
623        self.counts[r as usize]
624    }
625
626    fn w(&self, i: usize) -> &[f32] {
627        &self.w[i].data
628    }
629
630    /// The live rows of `l`.
631    ///
632    /// # Safety
633    ///
634    /// Nobody may write `l` while the slice lives.
635    unsafe fn get(&self, l: Loc) -> &[f32] {
636        // SAFETY: in the arena by the layout, and by the caller.
637        unsafe { self.arena.slice(l.off, self.rows(l.rows) * l.width) }
638    }
639
640    /// Runs `f(first row, rows of out)` over the live rows of `out` in blocks of [`ROWS`].
641    ///
642    /// # Safety
643    ///
644    /// Nobody else may touch `out` meanwhile, and `f` must not reach `out` any other way.
645    unsafe fn rows_of(&self, out: Loc, f: &(dyn Fn(usize, &mut [f32]) + Sync)) {
646        let n = self.rows(out.rows);
647        let arena = self.arena;
648        self.pool.run(n.div_ceil(ROWS), &|task, _| {
649            let r0 = task * ROWS;
650            let r1 = (r0 + ROWS).min(n);
651            // SAFETY: rows r0..r1 of out belong to this task alone.
652            let rows = unsafe { arena.slice_mut(out.off + r0 * out.width, (r1 - r0) * out.width) };
653            f(r0, rows);
654        });
655    }
656
657    fn step(&self, step: &Step) {
658        // The layout gives the inputs and the output of a step disjoint arena ranges unless the
659        // step is in place, and an in place step takes one slice only. That is the argument behind
660        // every SAFETY comment below.
661        match *step {
662            Step::Embed { table, out } => {
663                let (tab, ids, d) = (self.w(table), self.batch.ids, out.width);
664                // SAFETY: the layout keeps the inputs and the output of a step apart.
665                unsafe {
666                    self.rows_of(out, &|r0, rows| {
667                        for (i, row) in rows.chunks_exact_mut(d).enumerate() {
668                            let id = ids[r0 + i] as usize;
669                            row.copy_from_slice(&tab[id * d..(id + 1) * d]);
670                        }
671                    });
672                }
673            }
674            Step::LayerNorm { x, w, b, eps, out } => {
675                // SAFETY: the layout keeps the inputs and the output of a step apart.
676                let x = unsafe { self.get(x) };
677                let (nw, nb, d) = (self.w(w), b.map(|b| self.w(b)), out.width);
678                // SAFETY: the layout keeps the inputs and the output of a step apart.
679                unsafe {
680                    self.rows_of(out, &|r0, rows| {
681                        layer_norm(&x[r0 * d..r0 * d + rows.len()], d, nw, nb, eps, rows);
682                    });
683                }
684            }
685            Step::Gemm { a, w, b, ep, out } => {
686                let rows = self.rows(a.rows);
687                // SAFETY: the layout keeps the inputs and the output of a step apart.
688                let x = unsafe { self.get(a) };
689                // SAFETY: the layout keeps the inputs and the output of a step apart.
690                let y = unsafe { self.arena.slice_mut(out.off, rows * out.width) };
691                let g = Gemm {
692                    x,
693                    m: rows,
694                    k: a.width,
695                    w: &self.w[w].packed,
696                    n: out.width,
697                    b: b.map(|b| self.w(b)),
698                    ep,
699                };
700                let scratch = self.scratch;
701                g.run(y, self.pool.threads(), |n, f| {
702                    self.pool.run(n, &|i, worker| {
703                        // SAFETY: the scratch is this worker's, and lowering reserved enough of it
704                        // for every GEMM in the plan, so this does not allocate.
705                        let s = unsafe { scratch.get(worker) };
706                        s.resize(gemm::scratch_len(g.k, g.n), 0.0);
707                        f(i, s);
708                    });
709                });
710            }
711            Step::Gemm8 { a, w, b, ep, out } => {
712                let rows = self.rows(a.rows);
713                // SAFETY: the layout keeps the inputs and the output of a step apart.
714                let x = unsafe { self.get(a) };
715                // SAFETY: the layout keeps the inputs and the output of a step apart.
716                let y = unsafe { self.arena.slice_mut(out.off, rows * out.width) };
717                let q = self.w[w]
718                    .quant
719                    .as_ref()
720                    .unwrap_or_else(|| unreachable!("checked when lowered"));
721                let g = QGemm { x, m: rows, w: q, b: b.map(|b| self.w(b)), ep };
722                let scratch = self.scratch;
723                g.run(y, self.pool.threads(), |n, f| {
724                    self.pool.run(n, &|i, worker| {
725                        // SAFETY: the scratch is this worker's, and lowering reserved enough of it
726                        // for every GEMM in the plan, so this does not allocate.
727                        let s = unsafe { scratch.get(worker) };
728                        s.resize(qgemm::scratch_len(q.k), 0.0);
729                        f(i, s);
730                    });
731                });
732            }
733            Step::Rope { qkv, rope } => {
734                let (rope, d, cu, seq) = (&self.ropes[rope], qkv.width / 3, self.cu, self.row_seq);
735                // SAFETY: the layout keeps the inputs and the output of a step apart.
736                unsafe {
737                    self.rows_of(qkv, &|r0, rows| {
738                        for (i, row) in rows.chunks_exact_mut(qkv.width).enumerate() {
739                            let r = r0 + i;
740                            let pos = r - cu[seq[r] as usize];
741                            for head in row[..2 * d].as_chunks_mut::<HEAD>().0 {
742                                rope.apply(head, pos);
743                            }
744                        }
745                    });
746                }
747            }
748            Step::Attention { qkv, window, out } => {
749                let heads = out.width / HEAD;
750                // SAFETY: the layout keeps the inputs and the output of a step apart.
751                let x = unsafe { self.get(qkv) };
752                // SAFETY: the layout keeps the inputs and the output of a step apart.
753                let y = unsafe { self.arena.slice_mut(out.off, self.rows(out.rows) * out.width) };
754                let shared = Shared::new(y);
755                let (blocks, cu, scratch) = (self.blocks, self.cu, self.scratch);
756                self.pool.run(blocks.len() * heads, &|task, worker| {
757                    let (s, q0) = blocks[task / heads];
758                    let (s, q0, h) = (s as usize, q0 as usize, task % heads);
759                    // SAFETY: the scratch is this worker's, and this task owns rows q0 to q0 + QB
760                    // of head h.
761                    unsafe {
762                        let p = scratch.get(worker);
763                        // Growing it here would allocate on the warm path.
764                        debug_assert!(p.capacity() >= attention::scratch_len(cu[s + 1] - cu[s]));
765                        attention::block(x, heads, (cu[s], cu[s + 1]), q0, h, window, p, &shared);
766                    }
767                });
768            }
769            Step::GeGlu { x, out } => {
770                // SAFETY: the layout keeps the inputs and the output of a step apart.
771                let (x, d) = (unsafe { self.get(x) }, out.width);
772                // SAFETY: the layout keeps the inputs and the output of a step apart.
773                unsafe {
774                    self.rows_of(out, &|r0, rows| {
775                        geglu(&x[2 * r0 * d..2 * (r0 * d + rows.len())], d, rows);
776                    });
777                }
778            }
779            Step::AddType { h, table } => {
780                let (tab, d, seq, qt) = (self.w(table), h.width, self.row_seq, self.batch.qtype);
781                // SAFETY: the layout keeps the inputs and the output of a step apart.
782                unsafe {
783                    self.rows_of(h, &|r0, rows| {
784                        for (i, row) in rows.chunks_exact_mut(d).enumerate() {
785                            let q = usize::from(qt[seq[r0 + i] as usize]);
786                            row.iter_mut().zip(&tab[q * d..(q + 1) * d]).for_each(|(a, b)| *a += b);
787                        }
788                    });
789                }
790            }
791            Step::Gather { h, out } => {
792                // SAFETY: the layout keeps the inputs and the output of a step apart.
793                let (x, d) = (unsafe { self.get(h) }, h.width);
794                // SAFETY: the layout keeps the inputs and the output of a step apart.
795                let y = unsafe { self.arena.slice_mut(out.off, self.rows(out.rows) * d) };
796                let mut at = 0;
797                for s in 0..self.cu.len() - 1 {
798                    for &p in &self.batch.markers[self.mcu[s]..self.mcu[s + 1]] {
799                        let r = self.cu[s] + p as usize;
800                        y[at * d..(at + 1) * d].copy_from_slice(&x[r * d..(r + 1) * d]);
801                        at += 1;
802                    }
803                }
804            }
805            Step::ActFeatures { h, logits, out } => {
806                // SAFETY: the layout keeps the inputs and the output of a step apart.
807                let (x, d) = (unsafe { self.get(h) }, h.width);
808                // SAFETY: the layout keeps the inputs and the output of a step apart.
809                let l = unsafe { self.get(logits) };
810                // SAFETY: the layout keeps the inputs and the output of a step apart.
811                let y = unsafe { self.arena.slice_mut(out.off, self.rows(out.rows) * out.width) };
812                for (s, row) in y.chunks_exact_mut(out.width).enumerate() {
813                    let (lo, hi) = (self.cu[s], self.cu[s + 1]);
814                    if hi > lo {
815                        row[..d].copy_from_slice(&x[lo * d..(lo + 1) * d]);
816                    } else {
817                        row[..d].fill(0.0);
818                    }
819                    row[d..].copy_from_slice(&act_features(&l[self.mcu[s]..self.mcu[s + 1]]));
820                }
821            }
822            Step::MeanPool { h, out } => {
823                // SAFETY: the layout keeps the inputs and the output of a step apart.
824                let (x, d) = (unsafe { self.get(h) }, h.width);
825                // SAFETY: the layout keeps the inputs and the output of a step apart.
826                let y = unsafe { self.arena.slice_mut(out.off, self.rows(out.rows) * d) };
827                for (s, row) in y.chunks_exact_mut(d).enumerate() {
828                    mean_rows(&x[self.cu[s] * d..self.cu[s + 1] * d], row);
829                }
830            }
831        }
832    }
833}
834
835/// The mean of the rows of `x`, each as wide as `out`, summed in f64 a stretch of columns at a
836/// time so nothing is allocated. No rows give zeros.
837fn mean_rows(x: &[f32], out: &mut [f32]) {
838    const C: usize = 64;
839    let d = out.len();
840    let n = x.len() / d.max(1);
841    for c0 in (0..d).step_by(C) {
842        let c1 = (c0 + C).min(d);
843        let mut acc = [0f64; C];
844        for r in x.chunks_exact(d) {
845            acc.iter_mut().zip(&r[c0..c1]).for_each(|(a, &v)| *a += f64::from(v));
846        }
847        for (o, a) in out[c0..c1].iter_mut().zip(acc) {
848            *o = if n == 0 { 0.0 } else { (a / n as f64) as f32 };
849        }
850    }
851}
852
853/// `[top1, top1 - top2, entropy / ln k, k / 255]` over the softmax of the logits, with `k` at
854/// least 2, the same arithmetic as [`crate::compat::act_features`] without its buffers.
855fn act_features(logits: &[f32]) -> [f32; 4] {
856    let kf = logits.len().max(2) as f32;
857    if logits.is_empty() {
858        return [0.0, 0.0, 0.0, kf / 255.0];
859    }
860    let mx = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
861    let sum: f32 = logits.iter().map(|&l| (l - mx).exp()).sum();
862    let (mut top1, mut top2, mut ent) = (f32::NEG_INFINITY, 0f32, 0f32);
863    let mut first = true;
864    for &l in logits {
865        let q = (l - mx).exp() / sum;
866        ent += q * q.max(1e-9).ln();
867        if q > top1 || first {
868            if !first {
869                top2 = top1;
870            }
871            top1 = q;
872            first = false;
873        } else if q > top2 {
874            top2 = q;
875        }
876    }
877    let ent = -ent / kf.ln();
878    [top1, top1 - top2, ent, kf / 255.0]
879}
880
881#[cfg(test)]
882mod tests {
883    use super::*;
884
885    #[test]
886    fn mean_rows_over_wide_and_empty_sequences() {
887        // 70 columns cross the 64 column stretch, and no rows gives zeros.
888        let d = 70;
889        let x: Vec<f32> = (0..3 * d).map(|i| i as f32 * 0.5).collect();
890        let mut out = vec![1.0; d];
891        mean_rows(&x, &mut out);
892        for (c, o) in out.iter().enumerate() {
893            assert!((o - (x[c] + x[d + c] + x[2 * d + c]) / 3.0).abs() < 1e-4, "column {c}");
894        }
895        mean_rows(&[], &mut out);
896        assert!(out.iter().all(|&o| o == 0.0));
897    }
898
899    #[test]
900    fn act_features_match_the_reference() {
901        let cases: [&[f32]; 6] =
902            [&[], &[0.3], &[1.0, 1.0], &[2.0, -1.0, 2.0], &[5.0, 0.1, -3.0, 4.9], &[-1e3, 0.0]];
903        for l in cases {
904            assert_eq!(
905                act_features(l).map(f32::to_bits),
906                crate::compat::act_features(l).map(f32::to_bits),
907                "{l:?}"
908            );
909        }
910    }
911}