1use std::sync::Arc;
9
10use vyre_foundation::ir::model::expr::{GeneratorRef, Ident};
11use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
12
13pub const SUM_F32_OP_ID: &str = "vyre-primitives::reduce::workgroup_sum_f32";
15pub const SUM_U32_OP_ID: &str = "vyre-primitives::reduce::workgroup_sum_u32";
17pub const MAX_F32_OP_ID: &str = "vyre-primitives::reduce::workgroup_max_f32";
19pub const MAX_U32_OP_ID: &str = "vyre-primitives::reduce::workgroup_max_u32";
21pub const MIN_F32_OP_ID: &str = "vyre-primitives::reduce::workgroup_min_f32";
23pub const MIN_U32_OP_ID: &str = "vyre-primitives::reduce::workgroup_min_u32";
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum WorkgroupReductionScope {
29 EveryWorkgroup,
31 FirstWorkgroup,
33}
34
35impl WorkgroupReductionScope {
36 fn lane_guard(self, lane_expr: Expr) -> Expr {
37 match self {
38 Self::EveryWorkgroup => lane_expr,
39 Self::FirstWorkgroup => Expr::and(Expr::is_first_workgroup(), lane_expr),
40 }
41 }
42}
43
44#[must_use]
46pub fn sum_f32_child(
47 parent_op_id: &str,
48 tile: u32,
49 scratch: &'static str,
50 scope: WorkgroupReductionScope,
51) -> Node {
52 child_region(SUM_F32_OP_ID, parent_op_id, sum_body(tile, scratch, scope))
53}
54
55#[must_use]
57pub fn sum_u32_child(
58 parent_op_id: &str,
59 tile: u32,
60 scratch: &'static str,
61 scope: WorkgroupReductionScope,
62) -> Node {
63 child_region(SUM_U32_OP_ID, parent_op_id, sum_body(tile, scratch, scope))
64}
65
66#[must_use]
68pub fn max_f32_child(
69 parent_op_id: &str,
70 tile: u32,
71 scratch: &'static str,
72 scope: WorkgroupReductionScope,
73) -> Node {
74 child_region(MAX_F32_OP_ID, parent_op_id, max_body(tile, scratch, scope))
75}
76
77#[must_use]
79pub fn max_u32_child(
80 parent_op_id: &str,
81 tile: u32,
82 scratch: &'static str,
83 scope: WorkgroupReductionScope,
84) -> Node {
85 child_region(MAX_U32_OP_ID, parent_op_id, max_body(tile, scratch, scope))
86}
87
88#[must_use]
90pub fn min_f32_child(
91 parent_op_id: &str,
92 tile: u32,
93 scratch: &'static str,
94 scope: WorkgroupReductionScope,
95) -> Node {
96 child_region(MIN_F32_OP_ID, parent_op_id, min_body(tile, scratch, scope))
97}
98
99#[must_use]
101pub fn min_u32_child(
102 parent_op_id: &str,
103 tile: u32,
104 scratch: &'static str,
105 scope: WorkgroupReductionScope,
106) -> Node {
107 child_region(MIN_U32_OP_ID, parent_op_id, min_body(tile, scratch, scope))
108}
109
110#[must_use]
112pub fn workgroup_sum_f32(values: &str, out: &str, count: u32, tile: u32) -> Program {
113 reduction_program(
114 SUM_F32_OP_ID,
115 values,
116 out,
117 count,
118 tile,
119 DataType::F32,
120 Expr::f32(0.0),
121 Expr::add,
122 |tile, scratch| sum_body(tile, scratch, WorkgroupReductionScope::FirstWorkgroup),
123 )
124}
125
126#[must_use]
128pub fn workgroup_sum_u32(values: &str, out: &str, count: u32, tile: u32) -> Program {
129 reduction_program(
130 SUM_U32_OP_ID,
131 values,
132 out,
133 count,
134 tile,
135 DataType::U32,
136 Expr::u32(0),
137 Expr::add,
138 |tile, scratch| sum_body(tile, scratch, WorkgroupReductionScope::FirstWorkgroup),
139 )
140}
141
142#[must_use]
144pub fn workgroup_max_f32(values: &str, out: &str, count: u32, tile: u32) -> Program {
145 reduction_program(
146 MAX_F32_OP_ID,
147 values,
148 out,
149 count,
150 tile,
151 DataType::F32,
152 Expr::f32(f32::MIN),
153 Expr::max,
154 |tile, scratch| max_body(tile, scratch, WorkgroupReductionScope::FirstWorkgroup),
155 )
156}
157
158#[must_use]
166pub fn workgroup_max_u32(values: &str, out: &str, count: u32, tile: u32) -> Program {
167 reduction_program(
168 MAX_U32_OP_ID,
169 values,
170 out,
171 count,
172 tile,
173 DataType::U32,
174 Expr::u32(u32::MIN),
175 Expr::max,
176 |tile, scratch| max_body(tile, scratch, WorkgroupReductionScope::FirstWorkgroup),
177 )
178}
179
180#[must_use]
186pub fn workgroup_min_f32(values: &str, out: &str, count: u32, tile: u32) -> Program {
187 reduction_program(
188 MIN_F32_OP_ID,
189 values,
190 out,
191 count,
192 tile,
193 DataType::F32,
194 Expr::f32(f32::MAX),
195 Expr::min,
196 |tile, scratch| min_body(tile, scratch, WorkgroupReductionScope::FirstWorkgroup),
197 )
198}
199
200#[must_use]
204pub fn workgroup_min_u32(values: &str, out: &str, count: u32, tile: u32) -> Program {
205 reduction_program(
206 MIN_U32_OP_ID,
207 values,
208 out,
209 count,
210 tile,
211 DataType::U32,
212 Expr::u32(u32::MAX),
213 Expr::min,
214 |tile, scratch| min_body(tile, scratch, WorkgroupReductionScope::FirstWorkgroup),
215 )
216}
217
218#[allow(clippy::too_many_arguments)]
219fn reduction_program<F, R>(
220 op_id: &'static str,
221 values: &str,
222 out: &str,
223 count: u32,
224 tile: u32,
225 dtype: DataType,
226 init: Expr,
227 accumulate: F,
228 reduce: R,
229) -> Program
230where
231 F: Fn(Expr, Expr) -> Expr,
232 R: Fn(u32, &'static str) -> Vec<Node>,
233{
234 let tile = tile.max(1);
235 let chunks = count.div_ceil(tile);
236 let scratch = "__workgroup_reduce_scratch";
237 let local = Expr::var("local");
238 let idx = Expr::var("idx");
239 let mut body = vec![
240 Node::let_bind("local", Expr::LocalId { axis: 0 }),
241 Node::if_then(
242 Expr::is_first_workgroup(),
243 vec![
244 Node::let_bind("acc", init),
245 Node::loop_for(
246 "chunk",
247 Expr::u32(0),
248 Expr::u32(chunks),
249 vec![
250 Node::let_bind(
251 "idx",
252 Expr::add(
253 Expr::mul(Expr::var("chunk"), Expr::u32(tile)),
254 local.clone(),
255 ),
256 ),
257 Node::if_then(
258 Expr::lt(idx.clone(), Expr::u32(count)),
259 vec![Node::assign(
260 "acc",
261 accumulate(Expr::var("acc"), Expr::load(values, idx.clone())),
262 )],
263 ),
264 ],
265 ),
266 Node::store(scratch, local.clone(), Expr::var("acc")),
267 ],
268 ),
269 Node::barrier(),
270 ];
271 body.extend(reduce(tile, scratch));
272 body.push(Node::if_then(
273 Expr::and(Expr::is_first_workgroup(), Expr::eq(local, Expr::u32(0))),
274 vec![Node::store(
275 out,
276 Expr::u32(0),
277 Expr::load(scratch, Expr::u32(0)),
278 )],
279 ));
280 Program::wrapped(
281 vec![
282 BufferDecl::storage(values, 0, BufferAccess::ReadOnly, dtype.clone()).with_count(count),
283 BufferDecl::workgroup(scratch, tile, dtype.clone()),
284 BufferDecl::output(out, 1, dtype).with_count(1),
285 ],
286 [tile, 1, 1],
287 vec![Node::Region {
288 generator: Ident::from(op_id),
289 source_region: None,
290 body: Arc::new(body),
291 }],
292 )
293}
294
295fn child_region(generator: &'static str, parent_op_id: &str, body: Vec<Node>) -> Node {
296 Node::Region {
297 generator: Ident::from(generator),
298 source_region: Some(GeneratorRef {
299 name: parent_op_id.to_string(),
300 }),
301 body: Arc::new(body),
302 }
303}
304
305fn sum_body(tile: u32, scratch: &'static str, scope: WorkgroupReductionScope) -> Vec<Node> {
306 tree_body(tile, scratch, scope, Expr::add)
307}
308
309fn max_body(tile: u32, scratch: &'static str, scope: WorkgroupReductionScope) -> Vec<Node> {
310 tree_body(tile, scratch, scope, Expr::max)
311}
312
313fn min_body(tile: u32, scratch: &'static str, scope: WorkgroupReductionScope) -> Vec<Node> {
314 tree_body(tile, scratch, scope, Expr::min)
315}
316
317fn tree_body<F>(
318 tile: u32,
319 scratch: &'static str,
320 scope: WorkgroupReductionScope,
321 combine: F,
322) -> Vec<Node>
323where
324 F: Fn(Expr, Expr) -> Expr,
325{
326 let mut nodes = Vec::new();
327 let mut stride = tile.next_power_of_two() / 2;
328 while stride > 0 {
329 let lhs = Expr::load(scratch, Expr::var("local"));
330 let rhs_index = Expr::add(Expr::var("local"), Expr::u32(stride));
331 let rhs = Expr::load(scratch, rhs_index.clone());
332 nodes.push(Node::if_then(
333 scope.lane_guard(Expr::lt(Expr::var("local"), Expr::u32(stride))),
334 vec![Node::if_then(
335 Expr::lt(rhs_index, Expr::u32(tile)),
336 vec![Node::Store {
337 buffer: scratch.into(),
338 index: Expr::var("local"),
339 value: combine(lhs, rhs),
340 }],
341 )],
342 ));
343 nodes.push(Node::barrier());
344 stride /= 2;
345 }
346 nodes
347}
348
349#[cfg(feature = "inventory-registry")]
350fn fixture_f32(values: &[f32]) -> Vec<u8> {
351 crate::wire::pack_f32_slice(values)
352}
353
354#[cfg(feature = "inventory-registry")]
355fn fixture_u32(values: &[u32]) -> Vec<u8> {
356 crate::wire::pack_u32_slice(values)
357}
358
359#[cfg(feature = "inventory-registry")]
360inventory::submit! {
361 vyre_foundation::operation::OperationRegistration::primitive(
362 SUM_F32_OP_ID,
363 || workgroup_sum_f32("values", "out", 4, 4),
364 Some(|| vec![vec![
365 fixture_f32(&[1.25, -2.0, 5.5, 3.25]),
366 fixture_f32(&[0.0]),
367 ]]),
368 Some(|| vec![vec![fixture_f32(&[8.0])]]),
369 )
370}
371
372#[cfg(feature = "inventory-registry")]
373inventory::submit! {
374 vyre_foundation::operation::OperationRegistration::primitive(
375 SUM_U32_OP_ID,
376 || workgroup_sum_u32("values", "out", 4, 4),
377 Some(|| vec![vec![
378 fixture_u32(&[1, 2, 3, 4]),
379 fixture_u32(&[0]),
380 ]]),
381 Some(|| vec![vec![fixture_u32(&[10])]]),
382 )
383}
384
385#[cfg(feature = "inventory-registry")]
386inventory::submit! {
387 vyre_foundation::operation::OperationRegistration::primitive(
388 MAX_F32_OP_ID,
389 || workgroup_max_f32("values", "out", 4, 4),
390 Some(|| vec![vec![
391 fixture_f32(&[-3.0, 9.5, 4.0, 1.25]),
392 fixture_f32(&[0.0]),
393 ]]),
394 Some(|| vec![vec![fixture_f32(&[9.5])]]),
395 )
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use vyre_reference::value::Value;
402
403 #[test]
404 fn child_region_names_parent_and_primitive() {
405 let node = sum_f32_child(
406 "vyre-libs::math::reduce_mean",
407 256,
408 "scratch",
409 WorkgroupReductionScope::FirstWorkgroup,
410 );
411 let Node::Region {
412 generator,
413 source_region,
414 body,
415 } = node
416 else {
417 panic!("Fix: workgroup tree helper must emit a child Region.");
418 };
419 assert_eq!(generator.as_str(), SUM_F32_OP_ID);
420 assert_eq!(
421 source_region
422 .expect("Fix: child Region must name parent.")
423 .name,
424 "vyre-libs::math::reduce_mean"
425 );
426 assert!(!body.is_empty());
427 }
428
429 #[test]
430 fn standalone_sum_f32_matches_reference_arithmetic() {
431 let values = [1.25_f32, -2.0, 5.5, 3.25, 8.0];
432 let program = workgroup_sum_f32("values", "out", values.len() as u32, 4);
433 let outputs = vyre_reference::reference_eval(
434 &program,
435 &[
436 Value::from(crate::wire::pack_f32_slice(&values)),
437 Value::from(vec![0_u8; core::mem::size_of::<f32>()]),
438 ],
439 )
440 .expect("Fix: workgroup_sum_f32 must execute in the reference interpreter.");
441 assert_eq!(
442 crate::wire::decode_f32_le_bytes_all(&outputs[0].to_bytes())[0],
443 values.iter().copied().sum::<f32>()
444 );
445 }
446
447 #[test]
448 fn standalone_sum_u32_matches_reference_arithmetic() {
449 let values = [1_u32, 2, 3, 4, 5, 6, 7];
450 let program = workgroup_sum_u32("values", "out", values.len() as u32, 4);
451 let outputs = vyre_reference::reference_eval(
452 &program,
453 &[
454 Value::from(crate::wire::pack_u32_slice(&values)),
455 Value::from(vec![0_u8; core::mem::size_of::<u32>()]),
456 ],
457 )
458 .expect("Fix: workgroup_sum_u32 must execute in the reference interpreter.");
459 assert_eq!(
460 crate::wire::decode_u32_le_bytes_all(&outputs[0].to_bytes())[0],
461 values.iter().copied().sum::<u32>()
462 );
463 }
464
465 #[test]
466 fn standalone_max_f32_matches_reference_arithmetic() {
467 let values = [-3.0_f32, 9.5, 4.0, 1.25, 8.75];
468 let program = workgroup_max_f32("values", "out", values.len() as u32, 4);
469 let outputs = vyre_reference::reference_eval(
470 &program,
471 &[
472 Value::from(crate::wire::pack_f32_slice(&values)),
473 Value::from(vec![0_u8; core::mem::size_of::<f32>()]),
474 ],
475 )
476 .expect("Fix: workgroup_max_f32 must execute in the reference interpreter.");
477 assert_eq!(
478 crate::wire::decode_f32_le_bytes_all(&outputs[0].to_bytes())[0],
479 9.5
480 );
481 }
482
483 #[test]
484 fn standalone_max_u32_matches_reference_arithmetic() {
485 let values = [3_u32, 17, 5, 42, 8, 1];
488 let program = workgroup_max_u32("values", "out", values.len() as u32, 4);
489 let outputs = vyre_reference::reference_eval(
490 &program,
491 &[
492 Value::from(crate::wire::pack_u32_slice(&values)),
493 Value::from(vec![0_u8; core::mem::size_of::<u32>()]),
494 ],
495 )
496 .expect("Fix: workgroup_max_u32 must execute in the reference interpreter.");
497 assert_eq!(
498 crate::wire::decode_u32_le_bytes_all(&outputs[0].to_bytes())[0],
499 values.iter().copied().max().expect("non-empty"),
500 "workgroup_max_u32 must compute the unsigned max (42)"
501 );
502 }
503
504 #[test]
505 fn standalone_min_f32_matches_reference_arithmetic() {
506 let values = [3.0_f32, 9.5, -2.5, 1.25, 8.0];
508 let program = workgroup_min_f32("values", "out", values.len() as u32, 4);
509 let outputs = vyre_reference::reference_eval(
510 &program,
511 &[
512 Value::from(crate::wire::pack_f32_slice(&values)),
513 Value::from(vec![0_u8; core::mem::size_of::<f32>()]),
514 ],
515 )
516 .expect("Fix: workgroup_min_f32 must execute in the reference interpreter.");
517 assert_eq!(
518 crate::wire::decode_f32_le_bytes_all(&outputs[0].to_bytes())[0],
519 values.iter().copied().fold(f32::INFINITY, f32::min),
520 "workgroup_min_f32 must compute the min (-2.5)"
521 );
522 }
523
524 #[test]
525 fn standalone_min_u32_matches_reference_arithmetic() {
526 let values = [17_u32, 3, 42, 8, 25];
527 let program = workgroup_min_u32("values", "out", values.len() as u32, 4);
528 let outputs = vyre_reference::reference_eval(
529 &program,
530 &[
531 Value::from(crate::wire::pack_u32_slice(&values)),
532 Value::from(vec![0_u8; core::mem::size_of::<u32>()]),
533 ],
534 )
535 .expect("Fix: workgroup_min_u32 must execute in the reference interpreter.");
536 assert_eq!(
537 crate::wire::decode_u32_le_bytes_all(&outputs[0].to_bytes())[0],
538 values.iter().copied().min().expect("non-empty"),
539 "workgroup_min_u32 must compute the unsigned min (3)"
540 );
541 }
542
543 #[test]
544 fn non_power_of_two_tile_reductions_match_reference_arithmetic() {
545 let values = [4.0_f32, -7.0, 2.5, 9.0, 1.0, 3.25, -2.0];
546 let sum_program = workgroup_sum_f32("values", "out", values.len() as u32, 3);
547 let sum_outputs = vyre_reference::reference_eval(
548 &sum_program,
549 &[
550 Value::from(crate::wire::pack_f32_slice(&values)),
551 Value::from(vec![0_u8; core::mem::size_of::<f32>()]),
552 ],
553 )
554 .expect("Fix: workgroup_sum_f32 must support non-power-of-two tiles.");
555 assert_eq!(
556 crate::wire::decode_f32_le_bytes_all(&sum_outputs[0].to_bytes())[0],
557 values.iter().copied().sum::<f32>()
558 );
559
560 let max_program = workgroup_max_f32("values", "out", values.len() as u32, 3);
561 let max_outputs = vyre_reference::reference_eval(
562 &max_program,
563 &[
564 Value::from(crate::wire::pack_f32_slice(&values)),
565 Value::from(vec![0_u8; core::mem::size_of::<f32>()]),
566 ],
567 )
568 .expect("Fix: workgroup_max_f32 must support non-power-of-two tiles.");
569 assert_eq!(
570 crate::wire::decode_f32_le_bytes_all(&max_outputs[0].to_bytes())[0],
571 9.0
572 );
573 }
574}