1use std::sync::atomic::{AtomicUsize, Ordering};
12use std::sync::mpsc::{channel, Sender};
13use std::sync::{Arc, Condvar, Mutex};
14
15struct TaskPtr(*const (dyn Fn(usize, usize) + Sync));
19unsafe impl Send for TaskPtr {}
20
21struct Latch {
22 remaining: AtomicUsize,
23 lock: Mutex<()>,
24 cv: Condvar,
25}
26
27impl Latch {
28 fn new(n: usize) -> Arc<Self> {
29 Arc::new(Self {
30 remaining: AtomicUsize::new(n),
31 lock: Mutex::new(()),
32 cv: Condvar::new(),
33 })
34 }
35
36 fn count_down(&self) {
37 if self.remaining.fetch_sub(1, Ordering::AcqRel) == 1 {
38 let _g = self.lock.lock().unwrap();
39 self.cv.notify_all();
40 }
41 }
42
43 fn wait(&self) {
44 let mut g = self.lock.lock().unwrap();
45 while self.remaining.load(Ordering::Acquire) != 0 {
46 g = self.cv.wait(g).unwrap();
47 }
48 }
49}
50
51struct Job {
52 task: TaskPtr,
53 worker_idx: usize,
54 n_workers: usize,
55 latch: Arc<Latch>,
56}
57
58pub struct Pool {
60 txs: Vec<Sender<Job>>,
61}
62
63impl Pool {
64 pub fn new(n_workers: usize) -> Self {
65 let mut txs = Vec::with_capacity(n_workers);
66 for w in 0..n_workers {
67 let (tx, rx) = channel::<Job>();
68 std::thread::Builder::new()
69 .name(format!("cmf-pool-{w}"))
70 .spawn(move || {
71 while let Ok(job) = rx.recv() {
72 let f = unsafe { &*job.task.0 };
75 f(job.worker_idx, job.n_workers);
76 job.latch.count_down();
77 }
78 })
79 .expect("spawn pool worker");
80 txs.push(tx);
81 }
82 Self { txs }
83 }
84
85 pub fn from_env() -> Option<Arc<Self>> {
87 let n = match std::env::var("CMF_THREADS") {
88 Ok(v) => v.parse::<usize>().unwrap_or(0),
89 Err(_) => {
90 let avail = std::thread::available_parallelism()
91 .map(|n| n.get())
92 .unwrap_or(1);
93 avail.saturating_sub(1).min(8)
94 }
95 };
96 if n <= 1 {
97 None
98 } else {
99 Some(Arc::new(Self::new(n)))
100 }
101 }
102
103 pub fn n_workers(&self) -> usize {
104 self.txs.len()
105 }
106
107 pub fn run_rows(&self, rows: usize, f: &(dyn Fn(usize, usize) + Sync)) {
118 let grain = (rows / (self.txs.len() * 8)).max(32);
121 let next = AtomicUsize::new(0);
122 self.run(&|_w, _n| loop {
123 let start = next.fetch_add(grain, Ordering::Relaxed);
124 if start >= rows {
125 break;
126 }
127 f(start, (start + grain).min(rows));
128 });
129 }
130
131 pub fn run(&self, f: &(dyn Fn(usize, usize) + Sync)) {
134 let n = self.txs.len();
135 let latch = Latch::new(n);
136 let ptr: *const (dyn Fn(usize, usize) + Sync) = f;
139 let ptr: *const (dyn Fn(usize, usize) + Sync + 'static) =
140 unsafe { std::mem::transmute(ptr) };
141 for (i, tx) in self.txs.iter().enumerate() {
142 let job = Job {
143 task: TaskPtr(ptr),
144 worker_idx: i,
145 n_workers: n,
146 latch: latch.clone(),
147 };
148 tx.send(job).expect("pool worker died");
149 }
150 latch.wait();
151 }
152}
153
154pub fn matvec_rows(pool: Option<&Pool>, w: &[f32], x: &[f32], out: &mut [f32]) {
157 let in_dim = x.len();
158 let out_dim = out.len();
159 debug_assert!(w.len() >= out_dim * in_dim);
160
161 let row_dot = |o: usize| -> f32 {
162 let row = &w[o * in_dim..(o + 1) * in_dim];
163 let mut sum = 0.0f32;
164 for j in 0..in_dim {
165 sum += row[j] * x[j];
166 }
167 sum
168 };
169
170 match pool {
171 Some(pool) if out_dim >= 256 => {
173 let out_addr = SendMut(out.as_mut_ptr());
174 pool.run(&move |widx, n| {
175 let chunk = out_dim.div_ceil(n);
176 let start = widx * chunk;
177 let end = (start + chunk).min(out_dim);
178 for o in start..end {
179 unsafe { *out_addr.at(o) = row_dot(o) };
181 }
182 });
183 }
184 _ => {
185 for (o, dst) in out.iter_mut().enumerate() {
186 *dst = row_dot(o);
187 }
188 }
189 }
190}
191
192pub fn matvec_rows2(
198 pool: Option<&Pool>,
199 w: &[f32],
200 x1: &[f32],
201 x2: &[f32],
202 out1: &mut [f32],
203 out2: &mut [f32],
204) {
205 let in_dim = x1.len();
206 debug_assert_eq!(x2.len(), in_dim);
207 let out_dim = out1.len();
208 debug_assert_eq!(out2.len(), out_dim);
209 debug_assert!(w.len() >= out_dim * in_dim);
210
211 let row_dots = |o: usize| -> (f32, f32) {
212 let row = &w[o * in_dim..(o + 1) * in_dim];
213 let (mut s1, mut s2) = (0.0f32, 0.0f32);
214 for j in 0..in_dim {
215 s1 += row[j] * x1[j];
216 s2 += row[j] * x2[j];
217 }
218 (s1, s2)
219 };
220
221 match pool {
222 Some(pool) if out_dim >= 256 => {
223 let o1 = SendMut(out1.as_mut_ptr());
224 let o2 = SendMut(out2.as_mut_ptr());
225 pool.run(&move |widx, n| {
226 let chunk = out_dim.div_ceil(n);
227 let start = widx * chunk;
228 let end = (start + chunk).min(out_dim);
229 for o in start..end {
230 let (s1, s2) = row_dots(o);
231 unsafe {
233 *o1.at(o) = s1;
234 *o2.at(o) = s2;
235 }
236 }
237 });
238 }
239 _ => {
240 for o in 0..out_dim {
241 let (s1, s2) = row_dots(o);
242 out1[o] = s1;
243 out2[o] = s2;
244 }
245 }
246 }
247}
248
249#[derive(Clone, Copy)]
250struct SendMut(*mut f32);
251unsafe impl Send for SendMut {}
252unsafe impl Sync for SendMut {}
253
254impl SendMut {
255 #[inline]
258 fn at(self, i: usize) -> *mut f32 {
259 unsafe { self.0.add(i) }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn parallel_matvec_equals_serial_bitexact() {
269 let (out_dim, in_dim) = (512, 64);
270 let w: Vec<f32> = (0..out_dim * in_dim).map(|i| (i as f32 * 0.013).sin()).collect();
271 let x: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.07).cos()).collect();
272
273 let mut serial = vec![0.0f32; out_dim];
274 matvec_rows(None, &w, &x, &mut serial);
275
276 let pool = Pool::new(4);
277 let mut parallel = vec![0.0f32; out_dim];
278 matvec_rows(Some(&pool), &w, &x, &mut parallel);
279
280 assert_eq!(serial, parallel, "row-parallel must be bit-identical");
281 }
282
283 #[test]
284 fn fused_pair_equals_two_singles_bitexact() {
285 let (out_dim, in_dim) = (300, 48);
286 let w: Vec<f32> = (0..out_dim * in_dim).map(|i| (i as f32 * 0.011).sin()).collect();
287 let x1: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.03).cos()).collect();
288 let x2: Vec<f32> = (0..in_dim).map(|i| (i as f32 * 0.09).sin()).collect();
289
290 let mut a1 = vec![0.0f32; out_dim];
291 let mut a2 = vec![0.0f32; out_dim];
292 matvec_rows(None, &w, &x1, &mut a1);
293 matvec_rows(None, &w, &x2, &mut a2);
294
295 for pool in [None, Some(Pool::new(3))] {
296 let mut b1 = vec![0.0f32; out_dim];
297 let mut b2 = vec![0.0f32; out_dim];
298 matvec_rows2(pool.as_ref(), &w, &x1, &x2, &mut b1, &mut b2);
299 assert_eq!(a1, b1, "fused lane 1 must be bit-identical");
300 assert_eq!(a2, b2, "fused lane 2 must be bit-identical");
301 }
302 }
303
304 #[test]
305 fn pool_survives_many_runs() {
306 let pool = Pool::new(3);
307 let counter = AtomicUsize::new(0);
308 for _ in 0..100 {
309 pool.run(&|_, _| {
310 counter.fetch_add(1, Ordering::Relaxed);
311 });
312 }
313 assert_eq!(counter.load(Ordering::Relaxed), 300);
314 }
315}