Skip to main content

ftts_kernels/
team.rs

1//! KernelTeam v0: the persistent worker pool for int8 GEMV/GEMM output-column partitions.
2//!
3//! This is the doctrine's "persistent, dispatch-free steady state" in its first shippable form:
4//! workers are spawned once per process, parked on a condvar between operations (no busy wait,
5//! no work stealing, no task submission), and each dispatch hands every worker a disjoint
6//! contiguous range of output columns. Integer accumulation makes the parallel result *exactly*
7//! the serial result per element — partitioning never changes a single output bit, so thread
8//! count is a pure speed knob.
9//!
10//! ## The safety argument, in full
11//!
12//! A [`Job`] carries raw pointers into the caller's slices. Three facts make that sound:
13//!
14//! 1. **Lifetime**: [`Team::linear_q8`] does not return until every worker has decremented
15//!    `remaining` to zero, so the pointers outlive every access.
16//! 2. **Aliasing**: workers write only `out[row * n + col]` for `col` inside their own disjoint
17//!    column range; reads (`x_q`, scales, weights, bias) are shared and immutable for the whole
18//!    dispatch because the caller holds the only `&mut` (to `out`) and blocks.
19//! 3. **One parallel owner**: `dispatch_gate` serializes whole dispatches, so a second engine
20//!    thread cannot overwrite the job while workers are mid-partition, and workers themselves
21//!    never dispatch (their compute is a leaf loop).
22//!
23//! A stress test drives thousands of mixed-shape dispatches and a watchdog test bounds wall
24//! time, per the `many_utterances_without_deadlock` policy.
25
26use crate::int8::{Int8Tier, QuantizedMatrix, dot_i32};
27use std::sync::{Condvar, Mutex, OnceLock};
28
29/// One dispatched operation, shared read-only with every worker.
30#[derive(Clone, Copy)]
31enum Job {
32    /// W8A8 linear partitioned over output columns.
33    Linear(LinearJob),
34    /// f32 GQA attention partitioned over query heads.
35    Attention(AttentionJob),
36}
37
38#[derive(Clone, Copy)]
39struct LinearJob {
40    x_q: *const i8,
41    x_scales: *const f32,
42    w_data: *const i8,
43    w_scales: *const f32,
44    /// Null when the projection is bias-free.
45    bias: *const f32,
46    out: *mut f32,
47    m: usize,
48    n: usize,
49    k: usize,
50    tier: Int8Tier,
51    /// Total partitions this dispatch, including the caller's partition 0.
52    partitions: usize,
53}
54
55/// The default-arithmetic GQA attention, partitioned over query heads.
56///
57/// Head independence is the whole safety-and-exactness story: no reduction crosses a head, and
58/// each head writes only its own `head_dim` span of every output row, so any head partition is
59/// bit-identical to the serial full-range call.
60#[derive(Clone, Copy)]
61struct AttentionJob {
62    queries: *const f32,
63    keys: *const f32,
64    values: *const f32,
65    mask: *const f32,
66    query_positions: usize,
67    key_positions: usize,
68    q_heads: usize,
69    kv_heads: usize,
70    head_dim: usize,
71    out: *mut f32,
72    partitions: usize,
73}
74
75// SAFETY: the pointers a Job carries are dereferenced only between dispatch and join (module
76// docs, fact 1), reads are shared-immutable and writes disjoint (fact 2). Sending the
77// descriptor to parked threads is exactly the mechanism those facts govern.
78unsafe impl Send for Job {}
79// SAFETY: workers only read the descriptor fields; interior data races are excluded by the
80// disjoint-write partition argument above.
81unsafe impl Sync for Job {}
82
83struct Control {
84    generation: u64,
85    job: Option<Job>,
86    remaining: usize,
87    /// Set when any partition panicked during the current dispatch, so the caller can
88    /// propagate a loud failure instead of hanging on a worker that will never report done.
89    panicked: bool,
90}
91
92struct Shared {
93    control: Mutex<Control>,
94    go: Condvar,
95    done: Condvar,
96}
97
98/// The process-wide team. Exists only when `FTTS_INT8_THREADS` requests more than one thread.
99pub struct Team {
100    shared: &'static Shared,
101    /// Total partitions per dispatch: spawned workers + the calling thread.
102    partitions: usize,
103    dispatch_gate: Mutex<()>,
104}
105
106thread_local! {
107    static TEAM_BYPASS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
108}
109
110/// Makes THIS thread run its int8 linears serially, never dispatching to the team.
111///
112/// The codec pipeline worker sets this: its work is meant to overlap with the generator's
113/// team dispatches on spare cores, and routing it through the shared team would merely
114/// interleave the two through the dispatch gate instead of running them concurrently.
115pub fn bypass_team_on_this_thread() {
116    TEAM_BYPASS.with(|cell| cell.set(true));
117}
118
119/// Whether the current thread opted out of team dispatch.
120#[must_use]
121pub fn thread_bypassed() -> bool {
122    TEAM_BYPASS.with(std::cell::Cell::get)
123}
124
125/// The team for this process, if parallel execution is enabled.
126///
127/// `FTTS_INT8_THREADS` sets the total partition count (caller included); `1` or unset means
128/// serial (no threads spawned, no team). Values are clamped to the machine's available
129/// parallelism. Read once.
130pub fn armed() -> Option<&'static Team> {
131    // wasm32 has no std threads; the serial path is the only correct one there, and returning
132    // None before the OnceLock keeps the spawn code monomorphized out of wasm binaries.
133    #[cfg(target_arch = "wasm32")]
134    {
135        return None;
136    }
137    #[cfg(not(target_arch = "wasm32"))]
138    armed_native()
139}
140
141#[cfg(not(target_arch = "wasm32"))]
142fn armed_native() -> Option<&'static Team> {
143    static TEAM: OnceLock<Option<Team>> = OnceLock::new();
144    TEAM.get_or_init(|| {
145        let ceiling = std::thread::available_parallelism().map_or(1, usize::from);
146        // Default six ways: the measured knee on M4 Pro (memory-bound beyond it). Partitioning
147        // never changes output bits, so the default applies everywhere, reference route included.
148        let requested: usize = std::env::var("FTTS_INT8_THREADS")
149            .ok()
150            .and_then(|value| value.parse().ok())
151            .unwrap_or(6);
152        let partitions = requested.min(ceiling);
153        if partitions <= 1 {
154            return None;
155        }
156        let shared: &'static Shared = Box::leak(Box::new(Shared {
157            control: Mutex::new(Control {
158                generation: 0,
159                job: None,
160                remaining: 0,
161                panicked: false,
162            }),
163            go: Condvar::new(),
164            done: Condvar::new(),
165        }));
166        // Workers 1..partitions; the caller is partition 0. Threads live for the process and
167        // park on the condvar between dispatches, so leaking their handles is deliberate.
168        for worker in 1..partitions {
169            std::thread::Builder::new()
170                .name(format!("ftts-int8-{worker}"))
171                .spawn(move || worker_loop(shared, worker))
172                .expect("spawn int8 worker");
173        }
174        Some(Team {
175            shared,
176            partitions,
177            dispatch_gate: Mutex::new(()),
178        })
179    })
180    .as_ref()
181}
182
183fn worker_loop(shared: &'static Shared, worker: usize) {
184    let mut seen = 0_u64;
185    loop {
186        let job = {
187            let mut control = lock_control(shared);
188            while control.generation == seen {
189                control = shared
190                    .go
191                    .wait(control)
192                    .unwrap_or_else(std::sync::PoisonError::into_inner);
193            }
194            seen = control.generation;
195            control.job.expect("generation bumped without a job")
196        };
197        // A panicking partition must still report done, or the caller hangs forever waiting
198        // for a decrement that will never come. The panic is recorded and re-raised loudly on
199        // the caller's thread instead.
200        let outcome = std::panic::catch_unwind(|| run_partition(&job, worker));
201        let mut control = lock_control(shared);
202        if outcome.is_err() {
203            control.panicked = true;
204        }
205        control.remaining -= 1;
206        if control.remaining == 0 {
207            shared.done.notify_all();
208        }
209    }
210}
211
212/// Locks team control, tolerating poison: every dispatch re-establishes the full invariant
213/// (job, generation, remaining) from scratch, so a lock poisoned by an earlier panic carries
214/// no state that could mislead the next dispatch.
215fn lock_control(shared: &Shared) -> std::sync::MutexGuard<'_, Control> {
216    shared
217        .control
218        .lock()
219        .unwrap_or_else(std::sync::PoisonError::into_inner)
220}
221
222/// Computes one worker's contiguous column range. Identical arithmetic to the serial
223/// weight-stationary loop in [`crate::int8::linear_q8`], restricted to `[start, end)`.
224fn run_partition(job: &Job, worker: usize) {
225    #[cfg(test)]
226    if worker > 0 && tests::PANIC_INJECT.swap(false, std::sync::atomic::Ordering::SeqCst) {
227        panic!("injected worker panic for the hang-hardening test");
228    }
229    match job {
230        Job::Linear(job) => run_linear_partition(job, worker),
231        Job::Attention(job) => run_attention_partition(job, worker),
232    }
233}
234
235/// One worker's query-head range of an attention job. Same extracted loop the serial reference
236/// runs (`f32ref::gqa_attention_head_range_with_arithmetic` with the default arithmetic).
237fn run_attention_partition(job: &AttentionJob, worker: usize) {
238    let chunk = job.q_heads.div_ceil(job.partitions);
239    let start = (worker * chunk).min(job.q_heads);
240    let end = ((worker + 1) * chunk).min(job.q_heads);
241    if start >= end {
242        return;
243    }
244    // SAFETY: same three facts as the linear job (module docs) — the caller joins before its
245    // slices can die, and reads are shared-immutable for the dispatch. The output stays a raw
246    // pointer: every worker turning it into a whole-buffer `&mut` would put several live `&mut`
247    // on one allocation, which is undefined behaviour even though the writes are disjoint.
248    let (queries, keys, values, mask) = unsafe {
249        (
250            std::slice::from_raw_parts(
251                job.queries,
252                job.query_positions * job.q_heads * job.head_dim,
253            ),
254            std::slice::from_raw_parts(job.keys, job.key_positions * job.kv_heads * job.head_dim),
255            std::slice::from_raw_parts(job.values, job.key_positions * job.kv_heads * job.head_dim),
256            std::slice::from_raw_parts(job.mask, job.query_positions * job.key_positions),
257        )
258    };
259    // SAFETY: `out` is valid for the full [query_positions, q_heads, head_dim] span for this
260    // dispatch, and this worker's `start..end` head range is disjoint from every other
261    // partition's, so no two live borrows ever overlap.
262    unsafe {
263        crate::f32ref::gqa_attention_head_range_into(
264            queries,
265            keys,
266            values,
267            mask,
268            job.query_positions,
269            job.key_positions,
270            job.q_heads,
271            job.kv_heads,
272            job.head_dim,
273            crate::f32ref::F32SoftmaxArithmetic::ReciprocalMultiply,
274            crate::f32ref::F32LinearAccumulation::Scalar,
275            start..end,
276            job.out,
277        );
278    }
279}
280
281fn run_linear_partition(job: &LinearJob, worker: usize) {
282    let chunk = job.n.div_ceil(job.partitions);
283    let start = (worker * chunk).min(job.n);
284    let end = ((worker + 1) * chunk).min(job.n);
285    if start >= end {
286        return;
287    }
288    // SAFETY: module-docs facts 1-3 — pointers outlive the dispatch, reads are shared-immutable,
289    // and this worker writes only columns in its own [start, end) range. The output deliberately
290    // stays a raw pointer: a whole-buffer `&mut` per worker would be several live `&mut` on one
291    // allocation, which is undefined behaviour regardless of the writes being disjoint, and
292    // `rustc` marks `&mut` `noalias` so the optimizer is entitled to act on it.
293    let (x_q, x_scales, w_data, w_scales, bias) = unsafe {
294        (
295            std::slice::from_raw_parts(job.x_q, job.m * job.k),
296            std::slice::from_raw_parts(job.x_scales, job.m),
297            std::slice::from_raw_parts(job.w_data, job.n * job.k),
298            std::slice::from_raw_parts(job.w_scales, job.n),
299            (!job.bias.is_null()).then(|| std::slice::from_raw_parts(job.bias, job.n)),
300        )
301    };
302    for col in start..end {
303        let w_row = &w_data[col * job.k..(col + 1) * job.k];
304        let w_scale = w_scales[col];
305        let bias_term = bias.map(|b| b[col]);
306        for row in 0..job.m {
307            let x_row = &x_q[row * job.k..(row + 1) * job.k];
308            let acc = dot_i32(x_row, w_row, job.tier);
309            let value = acc as f32 * (x_scales[row] * w_scale);
310            // SAFETY: `col` is inside this partition's exclusive range and `row < m`, so this
311            // address is written by no other partition for the duration of the dispatch.
312            unsafe {
313                *job.out.add(row * job.n + col) = bias_term.map_or(value, |b| value + b);
314            }
315        }
316    }
317}
318
319impl Team {
320    /// Runs one W8A8 linear across the team. Bit-identical to the serial path per element.
321    ///
322    /// # Panics
323    ///
324    /// Panics on shape mismatches, exactly as the serial kernel does.
325    #[allow(clippy::too_many_arguments)]
326    pub fn linear_q8(
327        &self,
328        x_q: &[i8],
329        x_scales: &[f32],
330        weight: &QuantizedMatrix,
331        bias: Option<&[f32]>,
332        m: usize,
333        out: &mut [f32],
334        tier: Int8Tier,
335    ) {
336        let (n, k) = (weight.n, weight.k);
337        assert_eq!(x_q.len(), m * k, "x_q must be [m, k]");
338        assert_eq!(x_scales.len(), m, "x_scales must be [m]");
339        assert_eq!(out.len(), m * n, "out must be [m, n]");
340        if let Some(bias) = bias {
341            assert_eq!(bias.len(), n, "bias must be [n]");
342        }
343
344        let job = Job::Linear(LinearJob {
345            x_q: x_q.as_ptr(),
346            x_scales: x_scales.as_ptr(),
347            w_data: weight.data.as_ptr(),
348            w_scales: weight.scales.as_ptr(),
349            bias: bias.map_or(std::ptr::null(), <[f32]>::as_ptr),
350            out: out.as_mut_ptr(),
351            m,
352            n,
353            k,
354            tier,
355            partitions: self.partitions,
356        });
357
358        self.dispatch(job);
359    }
360
361    /// Runs the default-arithmetic GQA attention across the team, partitioned over query
362    /// heads. Bit-identical to the serial `f32ref::gqa_attention` (same extracted loop).
363    ///
364    /// # Panics
365    ///
366    /// Panics on shape mismatches, exactly as the serial reference does.
367    #[allow(clippy::too_many_arguments)]
368    pub fn gqa_attention(
369        &self,
370        queries: &[f32],
371        keys: &[f32],
372        values: &[f32],
373        mask: &[f32],
374        query_positions: usize,
375        key_positions: usize,
376        q_heads: usize,
377        kv_heads: usize,
378        head_dim: usize,
379        out: &mut [f32],
380    ) {
381        assert!(
382            kv_heads > 0 && q_heads.is_multiple_of(kv_heads),
383            "GQA head geometry"
384        );
385        assert_eq!(
386            queries.len(),
387            query_positions * q_heads * head_dim,
388            "queries shape"
389        );
390        assert_eq!(
391            keys.len(),
392            key_positions * kv_heads * head_dim,
393            "keys shape"
394        );
395        assert_eq!(
396            values.len(),
397            key_positions * kv_heads * head_dim,
398            "values shape"
399        );
400        assert_eq!(mask.len(), query_positions * key_positions, "mask shape");
401        assert_eq!(out.len(), query_positions * q_heads * head_dim, "out shape");
402        let job = Job::Attention(AttentionJob {
403            queries: queries.as_ptr(),
404            keys: keys.as_ptr(),
405            values: values.as_ptr(),
406            mask: mask.as_ptr(),
407            query_positions,
408            key_positions,
409            q_heads,
410            kv_heads,
411            head_dim,
412            out: out.as_mut_ptr(),
413            partitions: self.partitions,
414        });
415        self.dispatch(job);
416    }
417
418    /// The shared dispatch/work/join cycle (module-docs facts 1-3).
419    fn dispatch(&self, job: Job) {
420        // One dispatch at a time, held through the join (module-docs fact 3). Poison
421        // tolerance: a prior caller's panic leaves no dispatch state behind — everything is
422        // re-established below.
423        let _gate = self
424            .dispatch_gate
425            .lock()
426            .unwrap_or_else(std::sync::PoisonError::into_inner);
427        {
428            let mut control = lock_control(self.shared);
429            control.job = Some(job);
430            control.generation += 1;
431            control.remaining = self.partitions - 1;
432            control.panicked = false;
433            self.shared.go.notify_all();
434        }
435
436        // The caller is partition 0: it works instead of idling. Its own panic must still
437        // wait out the workers (they hold live pointers into the caller's slices), so the
438        // join below runs before any unwind continues.
439        let caller_outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
440            run_partition(&job, 0);
441        }));
442
443        let mut control = lock_control(self.shared);
444        while control.remaining > 0 {
445            control = self
446                .shared
447                .done
448                .wait(control)
449                .unwrap_or_else(std::sync::PoisonError::into_inner);
450        }
451        control.job = None;
452        let worker_panicked = control.panicked;
453        drop(control);
454
455        if let Err(payload) = caller_outcome {
456            std::panic::resume_unwind(payload);
457        }
458        assert!(
459            !worker_panicked,
460            "a team worker panicked during this dispatch; the output buffer is not fully written"
461        );
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468    use crate::int8::linear_q8;
469
470    /// One-shot fuse consumed by [`run_partition`] on a worker thread.
471    pub(super) static PANIC_INJECT: std::sync::atomic::AtomicBool =
472        std::sync::atomic::AtomicBool::new(false);
473
474    #[test]
475    fn a_panicking_worker_fails_the_dispatch_loudly_instead_of_hanging() {
476        let team = test_team(3);
477        let weight = matrix(64, 32, 5);
478        let x_q = vec![1_i8; 32];
479        let mut out = vec![0.0_f32; 64];
480        PANIC_INJECT.store(true, std::sync::atomic::Ordering::SeqCst);
481        let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
482            team.linear_q8(&x_q, &[1.0], &weight, None, 1, &mut out, Int8Tier::Scalar);
483        }));
484        assert!(
485            outcome.is_err(),
486            "a worker panic must surface at the caller, not hang or pass"
487        );
488        // And the team must still be usable afterwards.
489        team.linear_q8(&x_q, &[1.0], &weight, None, 1, &mut out, Int8Tier::Scalar);
490        assert!(out.iter().all(|value| value.is_finite()));
491    }
492
493    fn matrix(n: usize, k: usize, seed: u64) -> QuantizedMatrix {
494        let mut state = seed;
495        let data: Vec<i8> = (0..n * k)
496            .map(|_| {
497                state = state
498                    .wrapping_mul(6_364_136_223_846_793_005)
499                    .wrapping_add(1);
500                (((state >> 33) % 255) as i32 - 127) as i8
501            })
502            .collect();
503        let scales: Vec<f32> = (0..n).map(|row| 0.001 + (row % 7) as f32 * 0.01).collect();
504        QuantizedMatrix { data, scales, n, k }
505    }
506
507    /// A directly constructed team, so the test controls the partition count regardless of the
508    /// process environment.
509    fn test_team(partitions: usize) -> Team {
510        let shared: &'static Shared = Box::leak(Box::new(Shared {
511            control: Mutex::new(Control {
512                generation: 0,
513                job: None,
514                remaining: 0,
515                panicked: false,
516            }),
517            go: Condvar::new(),
518            done: Condvar::new(),
519        }));
520        for worker in 1..partitions {
521            std::thread::spawn(move || worker_loop(shared, worker));
522        }
523        Team {
524            shared,
525            partitions,
526            dispatch_gate: Mutex::new(()),
527        }
528    }
529
530    #[test]
531    fn every_partition_count_is_bit_identical_to_serial_at_model_shapes() {
532        for &(m, n, k) in &[
533            (1_usize, 2048_usize, 1024_usize),
534            (1, 1024, 3072),
535            (16, 3072, 1024),
536            (2, 517, 129), // deliberately ragged: tail partitions and odd K
537        ] {
538            let weight = matrix(n, k, 42 ^ (n as u64) << 20);
539            let x_q: Vec<i8> = (0..m * k).map(|i| ((i * 31 + 7) % 255) as i8).collect();
540            let x_scales: Vec<f32> = (0..m).map(|row| 0.02 + row as f32 * 0.005).collect();
541            let mut serial = vec![0.0_f32; m * n];
542            linear_q8(
543                &x_q,
544                &x_scales,
545                &weight,
546                None,
547                m,
548                &mut serial,
549                Int8Tier::Scalar,
550            );
551            for partitions in [2_usize, 3, 4, 8] {
552                let team = test_team(partitions);
553                let mut parallel = vec![0.0_f32; m * n];
554                team.linear_q8(
555                    &x_q,
556                    &x_scales,
557                    &weight,
558                    None,
559                    m,
560                    &mut parallel,
561                    Int8Tier::Scalar,
562                );
563                for (index, (a, b)) in serial.iter().zip(&parallel).enumerate() {
564                    assert_eq!(
565                        a.to_bits(),
566                        b.to_bits(),
567                        "partitions={partitions} m={m} n={n} k={k} element {index}"
568                    );
569                }
570            }
571        }
572    }
573
574    #[test]
575    fn thousands_of_mixed_dispatches_complete_without_deadlock() {
576        // The many_utterances_without_deadlock policy at kernel scale: hammer one team with
577        // mixed shapes; a hang here fails by test-harness timeout rather than passing silently.
578        let team = test_team(4);
579        let weight_a = matrix(256, 512, 7);
580        let weight_b = matrix(96, 128, 11);
581        let x_a: Vec<i8> = vec![3; 512];
582        let x_b: Vec<i8> = vec![-5; 2 * 128];
583        let mut out_a = vec![0.0_f32; 256];
584        let mut out_b = vec![0.0_f32; 2 * 96];
585        for _ in 0..2_000 {
586            team.linear_q8(
587                &x_a,
588                &[0.5],
589                &weight_a,
590                None,
591                1,
592                &mut out_a,
593                Int8Tier::Scalar,
594            );
595            team.linear_q8(
596                &x_b,
597                &[0.5, 0.25],
598                &weight_b,
599                None,
600                2,
601                &mut out_b,
602                Int8Tier::Scalar,
603            );
604        }
605        assert!(out_a.iter().all(|value| value.is_finite()));
606        assert!(out_b.iter().all(|value| value.is_finite()));
607    }
608
609    #[test]
610    fn attention_partitioning_is_bit_identical_to_serial_at_talker_geometry() {
611        // Talker decode shape: 16 query heads / 8 KV heads / head_dim 128, growing KV; plus a
612        // prefill-like seq>1 case and a ragged 5-partition split of 16 heads.
613        for &(query_positions, key_positions) in &[(1_usize, 37_usize), (4, 24)] {
614            let (q_heads, kv_heads, head_dim) = (16_usize, 8_usize, 128_usize);
615            let queries = values_of(query_positions * q_heads * head_dim, 21);
616            let keys = values_of(key_positions * kv_heads * head_dim, 22);
617            let values = values_of(key_positions * kv_heads * head_dim, 23);
618            let mut mask = vec![0.0_f32; query_positions * key_positions];
619            for (index, slot) in mask.iter_mut().enumerate() {
620                if index % 11 == 3 {
621                    *slot = f32::NEG_INFINITY;
622                }
623            }
624            let mut serial = vec![0.0_f32; queries.len()];
625            crate::f32ref::gqa_attention(
626                &queries,
627                &keys,
628                &values,
629                &mask,
630                query_positions,
631                key_positions,
632                q_heads,
633                kv_heads,
634                head_dim,
635                &mut serial,
636            );
637            for partitions in [2_usize, 5, 8] {
638                let team = test_team(partitions);
639                let mut parallel = vec![0.0_f32; queries.len()];
640                team.gqa_attention(
641                    &queries,
642                    &keys,
643                    &values,
644                    &mask,
645                    query_positions,
646                    key_positions,
647                    q_heads,
648                    kv_heads,
649                    head_dim,
650                    &mut parallel,
651                );
652                assert_eq!(
653                    serial.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
654                    parallel.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
655                    "partitions={partitions} qp={query_positions}"
656                );
657            }
658        }
659    }
660
661    fn values_of(len: usize, seed: u64) -> Vec<f32> {
662        let mut state = seed;
663        (0..len)
664            .map(|_| {
665                state = state
666                    .wrapping_mul(6_364_136_223_846_793_005)
667                    .wrapping_add(1);
668                ((state >> 33) as f32 / (1u64 << 31) as f32) - 0.5
669            })
670            .collect()
671    }
672
673    #[test]
674    fn bias_reaches_every_partition() {
675        let (m, n, k) = (2_usize, 130_usize, 64_usize);
676        let weight = matrix(n, k, 99);
677        let bias: Vec<f32> = (0..n).map(|i| i as f32).collect();
678        let x_q: Vec<i8> = vec![1; m * k];
679        let x_scales = vec![1.0_f32; m];
680        let mut serial = vec![0.0_f32; m * n];
681        linear_q8(
682            &x_q,
683            &x_scales,
684            &weight,
685            Some(&bias),
686            m,
687            &mut serial,
688            Int8Tier::Scalar,
689        );
690        let team = test_team(3);
691        let mut parallel = vec![0.0_f32; m * n];
692        team.linear_q8(
693            &x_q,
694            &x_scales,
695            &weight,
696            Some(&bias),
697            m,
698            &mut parallel,
699            Int8Tier::Scalar,
700        );
701        assert_eq!(
702            serial.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
703            parallel.iter().map(|v| v.to_bits()).collect::<Vec<_>>()
704        );
705    }
706}