1use crate::int8::{Int8Tier, QuantizedMatrix, dot_i32};
27use std::sync::{Condvar, Mutex, OnceLock};
28
29#[derive(Clone, Copy)]
31struct Job {
32 x_q: *const i8,
33 x_scales: *const f32,
34 w_data: *const i8,
35 w_scales: *const f32,
36 bias: *const f32,
38 out: *mut f32,
39 m: usize,
40 n: usize,
41 k: usize,
42 tier: Int8Tier,
43 partitions: usize,
45}
46
47unsafe impl Send for Job {}
51unsafe impl Sync for Job {}
54
55struct Control {
56 generation: u64,
57 job: Option<Job>,
58 remaining: usize,
59}
60
61struct Shared {
62 control: Mutex<Control>,
63 go: Condvar,
64 done: Condvar,
65}
66
67pub struct Team {
69 shared: &'static Shared,
70 partitions: usize,
72 dispatch_gate: Mutex<()>,
73}
74
75pub fn armed() -> Option<&'static Team> {
81 static TEAM: OnceLock<Option<Team>> = OnceLock::new();
82 TEAM.get_or_init(|| {
83 let ceiling = std::thread::available_parallelism().map_or(1, usize::from);
84 let requested: usize = std::env::var("FTTS_INT8_THREADS")
87 .ok()
88 .and_then(|value| value.parse().ok())
89 .unwrap_or(6);
90 let partitions = requested.min(ceiling);
91 if partitions <= 1 {
92 return None;
93 }
94 let shared: &'static Shared = Box::leak(Box::new(Shared {
95 control: Mutex::new(Control {
96 generation: 0,
97 job: None,
98 remaining: 0,
99 }),
100 go: Condvar::new(),
101 done: Condvar::new(),
102 }));
103 for worker in 1..partitions {
106 std::thread::Builder::new()
107 .name(format!("ftts-int8-{worker}"))
108 .spawn(move || worker_loop(shared, worker))
109 .expect("spawn int8 worker");
110 }
111 Some(Team {
112 shared,
113 partitions,
114 dispatch_gate: Mutex::new(()),
115 })
116 })
117 .as_ref()
118}
119
120fn worker_loop(shared: &'static Shared, worker: usize) {
121 let mut seen = 0_u64;
122 loop {
123 let job = {
124 let mut control = shared.control.lock().expect("team control poisoned");
125 while control.generation == seen {
126 control = shared.go.wait(control).expect("team control poisoned");
127 }
128 seen = control.generation;
129 control.job.expect("generation bumped without a job")
130 };
131 run_partition(&job, worker);
132 let mut control = shared.control.lock().expect("team control poisoned");
133 control.remaining -= 1;
134 if control.remaining == 0 {
135 shared.done.notify_all();
136 }
137 }
138}
139
140fn run_partition(job: &Job, worker: usize) {
143 let chunk = job.n.div_ceil(job.partitions);
144 let start = (worker * chunk).min(job.n);
145 let end = ((worker + 1) * chunk).min(job.n);
146 if start >= end {
147 return;
148 }
149 let (x_q, x_scales, w_data, w_scales, bias, out) = unsafe {
152 (
153 std::slice::from_raw_parts(job.x_q, job.m * job.k),
154 std::slice::from_raw_parts(job.x_scales, job.m),
155 std::slice::from_raw_parts(job.w_data, job.n * job.k),
156 std::slice::from_raw_parts(job.w_scales, job.n),
157 (!job.bias.is_null()).then(|| std::slice::from_raw_parts(job.bias, job.n)),
158 std::slice::from_raw_parts_mut(job.out, job.m * job.n),
159 )
160 };
161 for col in start..end {
162 let w_row = &w_data[col * job.k..(col + 1) * job.k];
163 let w_scale = w_scales[col];
164 let bias_term = bias.map(|b| b[col]);
165 for row in 0..job.m {
166 let x_row = &x_q[row * job.k..(row + 1) * job.k];
167 let acc = dot_i32(x_row, w_row, job.tier);
168 let value = acc as f32 * (x_scales[row] * w_scale);
169 out[row * job.n + col] = bias_term.map_or(value, |b| value + b);
170 }
171 }
172}
173
174impl Team {
175 #[allow(clippy::too_many_arguments)]
181 pub fn linear_q8(
182 &self,
183 x_q: &[i8],
184 x_scales: &[f32],
185 weight: &QuantizedMatrix,
186 bias: Option<&[f32]>,
187 m: usize,
188 out: &mut [f32],
189 tier: Int8Tier,
190 ) {
191 let (n, k) = (weight.n, weight.k);
192 assert_eq!(x_q.len(), m * k, "x_q must be [m, k]");
193 assert_eq!(x_scales.len(), m, "x_scales must be [m]");
194 assert_eq!(out.len(), m * n, "out must be [m, n]");
195 if let Some(bias) = bias {
196 assert_eq!(bias.len(), n, "bias must be [n]");
197 }
198
199 let job = Job {
200 x_q: x_q.as_ptr(),
201 x_scales: x_scales.as_ptr(),
202 w_data: weight.data.as_ptr(),
203 w_scales: weight.scales.as_ptr(),
204 bias: bias.map_or(std::ptr::null(), <[f32]>::as_ptr),
205 out: out.as_mut_ptr(),
206 m,
207 n,
208 k,
209 tier,
210 partitions: self.partitions,
211 };
212
213 let _gate = self.dispatch_gate.lock().expect("dispatch gate poisoned");
215 {
216 let mut control = self.shared.control.lock().expect("team control poisoned");
217 control.job = Some(job);
218 control.generation += 1;
219 control.remaining = self.partitions - 1;
220 self.shared.go.notify_all();
221 }
222
223 run_partition(&job, 0);
225
226 let mut control = self.shared.control.lock().expect("team control poisoned");
227 while control.remaining > 0 {
228 control = self
229 .shared
230 .done
231 .wait(control)
232 .expect("team control poisoned");
233 }
234 control.job = None;
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::int8::linear_q8;
242
243 fn matrix(n: usize, k: usize, seed: u64) -> QuantizedMatrix {
244 let mut state = seed;
245 let data: Vec<i8> = (0..n * k)
246 .map(|_| {
247 state = state
248 .wrapping_mul(6_364_136_223_846_793_005)
249 .wrapping_add(1);
250 (((state >> 33) % 255) as i32 - 127) as i8
251 })
252 .collect();
253 let scales: Vec<f32> = (0..n).map(|row| 0.001 + (row % 7) as f32 * 0.01).collect();
254 QuantizedMatrix { data, scales, n, k }
255 }
256
257 fn test_team(partitions: usize) -> Team {
260 let shared: &'static Shared = Box::leak(Box::new(Shared {
261 control: Mutex::new(Control {
262 generation: 0,
263 job: None,
264 remaining: 0,
265 }),
266 go: Condvar::new(),
267 done: Condvar::new(),
268 }));
269 for worker in 1..partitions {
270 std::thread::spawn(move || worker_loop(shared, worker));
271 }
272 Team {
273 shared,
274 partitions,
275 dispatch_gate: Mutex::new(()),
276 }
277 }
278
279 #[test]
280 fn every_partition_count_is_bit_identical_to_serial_at_model_shapes() {
281 for &(m, n, k) in &[
282 (1_usize, 2048_usize, 1024_usize),
283 (1, 1024, 3072),
284 (16, 3072, 1024),
285 (2, 517, 129), ] {
287 let weight = matrix(n, k, 42 ^ (n as u64) << 20);
288 let x_q: Vec<i8> = (0..m * k).map(|i| ((i * 31 + 7) % 255) as i8).collect();
289 let x_scales: Vec<f32> = (0..m).map(|row| 0.02 + row as f32 * 0.005).collect();
290 let mut serial = vec![0.0_f32; m * n];
291 linear_q8(
292 &x_q,
293 &x_scales,
294 &weight,
295 None,
296 m,
297 &mut serial,
298 Int8Tier::Scalar,
299 );
300 for partitions in [2_usize, 3, 4, 8] {
301 let team = test_team(partitions);
302 let mut parallel = vec![0.0_f32; m * n];
303 team.linear_q8(
304 &x_q,
305 &x_scales,
306 &weight,
307 None,
308 m,
309 &mut parallel,
310 Int8Tier::Scalar,
311 );
312 for (index, (a, b)) in serial.iter().zip(¶llel).enumerate() {
313 assert_eq!(
314 a.to_bits(),
315 b.to_bits(),
316 "partitions={partitions} m={m} n={n} k={k} element {index}"
317 );
318 }
319 }
320 }
321 }
322
323 #[test]
324 fn thousands_of_mixed_dispatches_complete_without_deadlock() {
325 let team = test_team(4);
328 let weight_a = matrix(256, 512, 7);
329 let weight_b = matrix(96, 128, 11);
330 let x_a: Vec<i8> = vec![3; 512];
331 let x_b: Vec<i8> = vec![-5; 2 * 128];
332 let mut out_a = vec![0.0_f32; 256];
333 let mut out_b = vec![0.0_f32; 2 * 96];
334 for _ in 0..2_000 {
335 team.linear_q8(
336 &x_a,
337 &[0.5],
338 &weight_a,
339 None,
340 1,
341 &mut out_a,
342 Int8Tier::Scalar,
343 );
344 team.linear_q8(
345 &x_b,
346 &[0.5, 0.25],
347 &weight_b,
348 None,
349 2,
350 &mut out_b,
351 Int8Tier::Scalar,
352 );
353 }
354 assert!(out_a.iter().all(|value| value.is_finite()));
355 assert!(out_b.iter().all(|value| value.is_finite()));
356 }
357
358 #[test]
359 fn bias_reaches_every_partition() {
360 let (m, n, k) = (2_usize, 130_usize, 64_usize);
361 let weight = matrix(n, k, 99);
362 let bias: Vec<f32> = (0..n).map(|i| i as f32).collect();
363 let x_q: Vec<i8> = vec![1; m * k];
364 let x_scales = vec![1.0_f32; m];
365 let mut serial = vec![0.0_f32; m * n];
366 linear_q8(
367 &x_q,
368 &x_scales,
369 &weight,
370 Some(&bias),
371 m,
372 &mut serial,
373 Int8Tier::Scalar,
374 );
375 let team = test_team(3);
376 let mut parallel = vec![0.0_f32; m * n];
377 team.linear_q8(
378 &x_q,
379 &x_scales,
380 &weight,
381 Some(&bias),
382 m,
383 &mut parallel,
384 Int8Tier::Scalar,
385 );
386 assert_eq!(
387 serial.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
388 parallel.iter().map(|v| v.to_bits()).collect::<Vec<_>>()
389 );
390 }
391}