1use std::sync::Arc;
33
34use vyre_foundation::ir::model::expr::Ident;
35use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
36
37use crate::reduce::multi_block_prefix_scan::multi_block_prefix_scan_sum_u32;
38
39pub const OP_ID_INCLUSIVE_SUM: &str = "vyre-primitives::math::prefix_scan_inclusive_sum";
41pub const OP_ID_EXCLUSIVE_SUM: &str = "vyre-primitives::math::prefix_scan_exclusive_sum";
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ScanKind {
47 InclusiveSum,
49 ExclusiveSum,
51}
52
53#[must_use]
59pub fn prefix_scan(in_buf: &str, out_buf: &str, n: u32, kind: ScanKind) -> Program {
60 let op_id = match kind {
61 ScanKind::InclusiveSum => OP_ID_INCLUSIVE_SUM,
62 ScanKind::ExclusiveSum => OP_ID_EXCLUSIVE_SUM,
63 };
64 prefix_scan_with_op_id(in_buf, out_buf, n, kind, op_id)
65}
66
67#[must_use]
69pub fn prefix_scan_with_op_id(
70 in_buf: &str,
71 out_buf: &str,
72 n: u32,
73 kind: ScanKind,
74 op_id: &'static str,
75) -> Program {
76 if n == 0 || n > 1024 {
77 return crate::invalid_output_program(
78 op_id,
79 out_buf,
80 DataType::U32,
81 format!("Fix: prefix_scan requires n in 1..=1024, got {n}."),
82 );
83 }
84
85 let lanes = n.next_power_of_two();
86 let lane = Expr::InvocationId { axis: 0 };
87 let scratch_a = format!("__{out_buf}_scan_a");
88 let scratch_b = format!("__{out_buf}_scan_b");
89
90 let mut body: Vec<Node> = Vec::new();
91 body.push(Node::store(&scratch_a, lane.clone(), Expr::u32(0)));
92 match kind {
93 ScanKind::InclusiveSum => body.push(Node::if_then(
94 Expr::lt(lane.clone(), Expr::u32(n)),
95 vec![Node::store(
96 &scratch_a,
97 lane.clone(),
98 Expr::load(in_buf, lane.clone()),
99 )],
100 )),
101 ScanKind::ExclusiveSum => body.push(Node::if_then(
102 Expr::and(
103 Expr::lt(Expr::u32(0), lane.clone()),
104 Expr::lt(lane.clone(), Expr::u32(n)),
105 ),
106 vec![Node::store(
107 &scratch_a,
108 lane.clone(),
109 Expr::load(in_buf, Expr::add(lane.clone(), Expr::u32(u32::MAX))),
110 )],
111 )),
112 }
113 body.push(Node::Barrier {
114 ordering: vyre_foundation::MemoryOrdering::SeqCst,
115 });
116
117 let mut stride = 1_u32;
118 while stride < lanes {
119 let previous_lane = Expr::add(lane.clone(), Expr::u32(u32::MAX.wrapping_sub(stride - 1)));
120 body.push(Node::store(
121 &scratch_b,
122 lane.clone(),
123 Expr::load(&scratch_a, lane.clone()),
124 ));
125 body.push(Node::if_then(
126 Expr::lt(Expr::u32(stride - 1), lane.clone()),
127 vec![Node::store(
128 &scratch_b,
129 lane.clone(),
130 Expr::add(
131 Expr::load(&scratch_a, lane.clone()),
132 Expr::load(&scratch_a, previous_lane),
133 ),
134 )],
135 ));
136 body.push(Node::Barrier {
137 ordering: vyre_foundation::MemoryOrdering::SeqCst,
138 });
139 body.push(Node::store(
140 &scratch_a,
141 lane.clone(),
142 Expr::load(&scratch_b, lane.clone()),
143 ));
144 body.push(Node::Barrier {
145 ordering: vyre_foundation::MemoryOrdering::SeqCst,
146 });
147 stride *= 2;
148 }
149
150 body.push(Node::if_then(
151 Expr::lt(lane.clone(), Expr::u32(n)),
152 vec![Node::store(
153 out_buf,
154 lane.clone(),
155 Expr::load(&scratch_a, lane.clone()),
156 )],
157 ));
158
159 let output_bytes = usize::try_from(n).unwrap_or(usize::MAX).saturating_mul(4);
160 let buffers = vec![
161 BufferDecl::storage(in_buf, 0, BufferAccess::ReadOnly, DataType::U32).with_count(n),
162 BufferDecl::output(out_buf, 1, DataType::U32)
163 .with_count(n)
164 .with_output_byte_range(0..output_bytes),
165 BufferDecl::workgroup(&scratch_a, lanes, DataType::U32),
166 BufferDecl::workgroup(&scratch_b, lanes, DataType::U32),
167 ];
168
169 Program::wrapped(
170 buffers,
171 [lanes, 1, 1],
172 vec![Node::Region {
173 generator: Ident::from(op_id),
174 source_region: None,
175 body: Arc::new(body),
176 }],
177 )
178}
179
180#[must_use]
186pub fn prefix_scan_large(in_buf: &str, out_buf: &str, n: u32) -> Program {
187 prefix_scan_large_with_op_id(in_buf, out_buf, n, OP_ID_INCLUSIVE_SUM)
188}
189
190#[must_use]
192pub fn prefix_scan_large_with_op_id(
193 in_buf: &str,
194 out_buf: &str,
195 n: u32,
196 op_id: &'static str,
197) -> Program {
198 if n == 0 {
199 return empty_large_scan_program(in_buf, out_buf, op_id);
200 }
201 if n <= 1024 {
202 return prefix_scan_with_op_id(in_buf, out_buf, n, ScanKind::InclusiveSum, op_id);
203 }
204
205 wrap_large_scan_program(multi_block_prefix_scan_sum_u32(in_buf, out_buf, n), op_id)
206}
207
208fn empty_large_scan_program(in_buf: &str, out_buf: &str, op_id: &'static str) -> Program {
209 let input_decl = BufferDecl::storage(in_buf, 0, BufferAccess::ReadOnly, DataType::U32);
210 let output_decl = BufferDecl::output(out_buf, 1, DataType::U32)
211 .with_count(1)
212 .with_output_byte_range(0..0);
213
214 Program::wrapped(
215 vec![input_decl, output_decl],
216 [1, 1, 1],
217 vec![Node::Region {
218 generator: Ident::from(op_id),
219 source_region: None,
220 body: Arc::new(Vec::new()),
221 }],
222 )
223}
224
225fn wrap_large_scan_program(program: Program, op_id: &'static str) -> Program {
226 let tagged = vec![Node::Region {
229 generator: Ident::from(op_id),
230 source_region: None,
231 body: Arc::new(program.entry().to_vec()),
232 }];
233 program.with_rewritten_wrapped_entry(tagged)
234}
235
236#[must_use]
239#[cfg(any(test, feature = "cpu-parity"))]
240pub fn cpu_ref(input: &[u32], kind: ScanKind) -> Vec<u32> {
241 let mut out = Vec::new();
242 try_cpu_ref_into(input, kind, &mut out)
243 .expect("Fix: replace expect with fallible API or document caller precondition; panic only on programmer error - prefix_scan cpu_ref failed: output allocation failed");
244 out
245}
246
247#[cfg(any(test, feature = "cpu-parity"))]
249pub fn try_cpu_ref(input: &[u32], kind: ScanKind) -> Result<Vec<u32>, String> {
250 let mut out = Vec::new();
251 try_cpu_ref_into(input, kind, &mut out)?;
252 Ok(out)
253}
254
255#[cfg(any(test, feature = "cpu-parity"))]
257pub fn cpu_ref_into(input: &[u32], kind: ScanKind, out: &mut Vec<u32>) {
258 try_cpu_ref_into(input, kind, out)
259 .expect("Fix: replace expect with fallible API or document caller precondition; panic only on programmer error - prefix_scan cpu_ref_into failed: output allocation failed");
260}
261
262#[cfg(any(test, feature = "cpu-parity"))]
264pub fn try_cpu_ref_into(input: &[u32], kind: ScanKind, out: &mut Vec<u32>) -> Result<(), String> {
265 if input.len() > out.capacity() {
266 crate::graph::scratch::reserve_graph_items(
267 out,
268 input.len() - out.len(),
269 "prefix scan CPU oracle",
270 "scan output",
271 )?;
272 }
273 out.clear();
274 let mut acc = 0_u32;
275 match kind {
276 ScanKind::InclusiveSum => {
277 for &x in input {
278 acc = acc.wrapping_add(x);
279 out.push(acc);
280 }
281 }
282 ScanKind::ExclusiveSum => {
283 for &x in input {
284 out.push(acc);
285 acc = acc.wrapping_add(x);
286 }
287 }
288 }
289 Ok(())
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn inclusive_cpu_ref_matches_textbook() {
298 assert_eq!(
299 cpu_ref(&[1, 2, 3, 4], ScanKind::InclusiveSum),
300 vec![1, 3, 6, 10],
301 );
302 }
303
304 #[test]
305 fn exclusive_cpu_ref_matches_textbook() {
306 assert_eq!(
307 cpu_ref(&[1, 2, 3, 4], ScanKind::ExclusiveSum),
308 vec![0, 1, 3, 6],
309 );
310 }
311
312 #[test]
313 fn empty_cpu_ref_returns_empty() {
314 assert_eq!(cpu_ref(&[], ScanKind::InclusiveSum), Vec::<u32>::new());
315 assert_eq!(cpu_ref(&[], ScanKind::ExclusiveSum), Vec::<u32>::new());
316 }
317
318 #[test]
319 fn wrap_on_overflow() {
320 assert_eq!(
322 cpu_ref(&[u32::MAX, 1], ScanKind::InclusiveSum),
323 vec![u32::MAX, 0],
324 );
325 }
326
327 #[test]
328 fn cpu_ref_into_reuses_output_buffer() {
329 let mut out = Vec::with_capacity(16);
330 let ptr = out.as_ptr();
331 cpu_ref_into(&[1, 2, 3, 4], ScanKind::ExclusiveSum, &mut out);
332 assert_eq!(out, vec![0, 1, 3, 6]);
333 assert_eq!(out.as_ptr(), ptr);
334 }
335
336 #[test]
337 fn cpu_ref_into_truncates_stale_tail_without_reallocating() {
338 let mut out = Vec::with_capacity(16);
339 out.extend([99u32; 16]);
340 let ptr = out.as_ptr();
341
342 try_cpu_ref_into(&[1, 2, 3, 4], ScanKind::InclusiveSum, &mut out).unwrap();
343
344 assert_eq!(out, vec![1, 3, 6, 10]);
345 assert_eq!(out.as_ptr(), ptr);
346 }
347
348 #[test]
349 fn generated_cpu_ref_matches_independent_wrapping_scan() {
350 for len in 0..128usize {
351 let input: Vec<u32> = (0..len)
352 .map(|idx| {
353 (idx as u32)
354 .wrapping_mul(0x9E37_79B9)
355 .wrapping_add(len as u32)
356 })
357 .collect();
358 for kind in [ScanKind::InclusiveSum, ScanKind::ExclusiveSum] {
359 let mut out = Vec::with_capacity(len + 3);
360 try_cpu_ref_into(&input, kind, &mut out).unwrap();
361 let mut expected = Vec::with_capacity(len);
362 let mut acc = 0u32;
363 for &value in &input {
364 match kind {
365 ScanKind::InclusiveSum => {
366 acc = acc.wrapping_add(value);
367 expected.push(acc);
368 }
369 ScanKind::ExclusiveSum => {
370 expected.push(acc);
371 acc = acc.wrapping_add(value);
372 }
373 }
374 }
375 assert_eq!(
376 out, expected,
377 "generated prefix scan len={len} kind={kind:?}"
378 );
379 }
380 }
381 }
382
383 #[test]
384 fn emitted_inclusive_program_has_expected_buffers() {
385 let p = prefix_scan("in", "out", 32, ScanKind::InclusiveSum);
386 assert_eq!(p.workgroup_size, [32, 1, 1]);
387 let names: Vec<&str> = p.buffers.iter().map(|b| b.name()).collect();
388 assert_eq!(names, vec!["in", "out", "__out_scan_a", "__out_scan_b"]);
389 }
390
391 #[test]
392 fn emitted_exclusive_program_has_expected_buffers() {
393 let p = prefix_scan("in", "out", 64, ScanKind::ExclusiveSum);
394 assert_eq!(p.workgroup_size, [64, 1, 1]);
395 }
396
397 #[test]
398 fn non_power_of_two_n_pads_to_next_power_of_two() {
399 let p = prefix_scan("in", "out", 5, ScanKind::InclusiveSum);
400 assert_eq!(p.workgroup_size, [8, 1, 1]);
401 }
402
403 #[test]
404 fn zero_n_traps() {
405 let p = prefix_scan("in", "out", 0, ScanKind::InclusiveSum);
406 assert!(p.stats().trap());
407 }
408
409 #[test]
410 fn over_limit_n_traps() {
411 let p = prefix_scan("in", "out", 2048, ScanKind::InclusiveSum);
412 assert!(p.stats().trap());
413 }
414
415 #[test]
416 fn binary_power_of_two_sizes_accepted() {
417 for n in &[1_u32, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024] {
418 let program = prefix_scan("in", "out", *n, ScanKind::InclusiveSum);
419 let names: Vec<&str> = program.buffers().iter().map(|b| b.name()).collect();
420 assert!(
421 names.contains(&"in"),
422 "prefix_scan must declare in for n={n}"
423 );
424 assert!(
425 names.contains(&"out"),
426 "prefix_scan must declare out for n={n}"
427 );
428 }
429 }
430}