1use std::rc::Rc;
8
9use gwr_engine::sim_error;
10use gwr_engine::types::{SimError, SimResult};
11use rand::RngExt;
12
13use super::{Operator, Shape, Tensor, TensorPartition};
14use crate::processing_element::operators::{
15 HasShape, TensorView, apply_dim_partitions, partition_across_dimensions,
16};
17use crate::processing_element::{ComputeCapabilities, MachineOp, MachineOpCounts};
18
19const NAME: &str = "Add";
20
21fn choose_partition_dims<T: HasShape>(output: &T) -> Vec<usize> {
22 let dims = output.shape().get_dims();
23 let mut candidate_dims = dims
24 .iter()
25 .enumerate()
26 .filter_map(|(dim, size)| (*size > 1).then_some(dim))
27 .collect::<Vec<_>>();
28
29 if candidate_dims.is_empty() {
30 candidate_dims.push(output.num_dims().saturating_sub(1));
31 }
32
33 candidate_dims
34}
35
36pub struct OperatorAdd {}
37
38fn broadcast_shapes(a: &Shape, b: &Shape) -> Result<Shape, SimError> {
39 let rank_a = a.num_dims();
40 let rank_b = b.num_dims();
41 let rank = rank_a.max(rank_b);
42 let mut result = vec![1; rank];
43
44 for (i, result_i) in result.iter_mut().enumerate() {
45 let a_dim = a.get_dim(rank, i);
46 let b_dim = b.get_dim(rank, i);
47
48 *result_i = if a_dim == b_dim {
49 a_dim
50 } else if a_dim == 1 {
51 b_dim
52 } else if b_dim == 1 {
53 a_dim
54 } else {
55 return sim_error!("{NAME}: cannot broadcast shapes {:?} and {:?}", a, b);
56 };
57 }
58
59 Ok(Shape(result))
60}
61
62fn choose_input_shape(output: &Tensor, rng: &mut impl RngExt, expand_ratio: f64) -> Shape {
63 let keep_prob = expand_ratio.clamp(0.0, 1.0);
64 let mut dims = output.shape.0.clone();
65
66 for dim in &mut dims {
67 if *dim > 1 && rng.random_bool(1.0 - keep_prob) {
68 *dim = 1;
69 }
70 }
71
72 while dims.len() > 1 && dims[0] == 1 && rng.random_bool(1.0 - keep_prob) {
73 dims.remove(0);
74 }
75
76 Shape(dims)
77}
78
79fn validate_inputs<T: HasShape>(inputs: &[Option<T>]) -> Result<(&T, &T), SimError> {
80 if inputs.len() != 2 {
81 return sim_error!("{NAME}: {} inputs found - expected 2", inputs.len());
82 }
83 let input_a = inputs[0]
84 .as_ref()
85 .ok_or(SimError(format!("{NAME}: missing input 0")))?;
86 let input_b = inputs[1]
87 .as_ref()
88 .ok_or(SimError(format!("{NAME}: missing input 1")))?;
89 Ok((input_a, input_b))
90}
91
92fn validate_outputs<T: HasShape>(outputs: &[Option<T>]) -> Result<&T, SimError> {
93 if outputs.len() != 1 {
94 return sim_error!("{NAME}: {} outputs found - expected 1", outputs.len());
95 }
96 outputs[0]
97 .as_ref()
98 .ok_or(SimError(format!("{NAME}: missing output")))
99}
100
101fn validate_input_outputs<'a, 'b, T: HasShape>(
102 inputs: &'a [Option<T>],
103 outputs: &'b [Option<T>],
104) -> Result<(&'a T, &'a T, &'b T), SimError> {
105 let (input_a, input_b) = validate_inputs(inputs)?;
106 let output = validate_outputs(outputs)?;
107
108 let expected_shape = broadcast_shapes(input_a.shape(), input_b.shape())?;
109 if expected_shape != *output.shape() {
110 return sim_error!(
111 "{NAME}: Invalid output shape - expected {:?}, found {:?}",
112 expected_shape,
113 output.shape()
114 );
115 }
116 Ok((input_a, input_b, output))
117}
118
119fn num_add_flops<T: HasShape>(
120 inputs: &[Option<T>],
121 outputs: &[Option<T>],
122) -> Result<usize, SimError> {
123 let (_, _, output) = validate_input_outputs(inputs, outputs)?;
124 Ok(output.num_elements())
125}
126
127impl OperatorAdd {
128 pub fn create_outputs(
129 &self,
130 inputs: &[Option<Tensor>],
131 _expand_ratio: f64,
132 _rng: &mut impl RngExt,
133 ) -> Result<Vec<Option<Tensor>>, gwr_engine::types::SimError> {
134 let (input_a, input_b) = validate_inputs(inputs)?;
135
136 let output_shape = broadcast_shapes(&input_a.shape, &input_b.shape)?;
137 let output_dtype = if input_a.dtype > input_b.dtype {
138 input_a.dtype
139 } else {
140 input_b.dtype
141 };
142
143 Ok(vec![Some(Tensor {
144 id: None,
145 shape: output_shape,
146 dtype: output_dtype,
147 addr: 0,
148 })])
149 }
150
151 pub fn create_inputs(
152 &self,
153 outputs: &[Option<Tensor>],
154 expand_ratio: f64,
155 rng: &mut impl RngExt,
156 ) -> Result<Vec<Option<Tensor>>, gwr_engine::types::SimError> {
157 let output = validate_outputs(outputs)?;
158
159 let (input_a_shape, input_b_shape) = if rng.random_bool(0.5) {
163 (
164 choose_input_shape(output, rng, expand_ratio),
165 output.shape.clone(),
166 )
167 } else {
168 (
169 output.shape.clone(),
170 choose_input_shape(output, rng, expand_ratio),
171 )
172 };
173
174 Ok(vec![
175 Some(Tensor {
176 id: None,
177 shape: input_a_shape,
178 dtype: output.dtype,
179 addr: 0,
180 }),
181 Some(Tensor {
182 id: None,
183 shape: input_b_shape,
184 dtype: output.dtype,
185 addr: 0,
186 }),
187 ])
188 }
189}
190
191impl Operator for OperatorAdd {
192 fn validate_tensors(&self, inputs: &[Option<Tensor>], outputs: &[Option<Tensor>]) -> SimResult {
193 validate_input_outputs(inputs, outputs)?;
194 Ok(())
195 }
196
197 fn compute_delay_ticks(
198 &self,
199 compute_capabilities: &Rc<ComputeCapabilities>,
200 inputs: &[Option<TensorView>],
201 outputs: &[Option<TensorView>],
202 ) -> Result<usize, SimError> {
203 let num_adds = num_add_flops(inputs, outputs)?;
204 compute_capabilities.cycles_for_ops(num_adds, MachineOp::Add)
205 }
206
207 fn compute_machine_ops(
208 &self,
209 inputs: &[Option<TensorView>],
210 outputs: &[Option<TensorView>],
211 ) -> Result<MachineOpCounts, SimError> {
212 Ok(MachineOpCounts {
213 adds: num_add_flops(inputs, outputs)?,
214 ..MachineOpCounts::default()
215 })
216 }
217
218 fn partition_views(
219 &self,
220 input_views: &[Option<TensorView>],
221 output_views: &[Option<TensorView>],
222 num_partitions: usize,
223 ) -> Result<Vec<TensorPartition>, SimError> {
224 let (_, _, output_view) = validate_input_outputs(input_views, output_views)?;
225
226 let partition_dims = choose_partition_dims(&output_view);
227 let output_view_dims = output_view.shape().get_dims();
228 let partition_specs =
229 partition_across_dimensions(output_view_dims, &partition_dims, num_partitions);
230 let output_rank = output_view.num_dims();
231
232 let mut partitions = Vec::with_capacity(partition_specs.len());
233 for spec in partition_specs {
234 let (output_shape, partition_offsets) = apply_dim_partitions(output_view_dims, &spec);
235 let output_offsets = output_view
236 .offsets()
237 .get_dims()
238 .iter()
239 .zip(partition_offsets.iter())
240 .map(|(base, offset)| base + offset)
241 .collect::<Vec<_>>();
242 let output_view =
243 TensorView::new(output_view.tensor().clone(), &output_shape, &output_offsets);
244
245 let input_views = input_views
246 .iter()
247 .map(|maybe_input_view| {
248 maybe_input_view.as_ref().map(|input_view| {
249 TensorView::from_output_partitions_on_view(input_view, output_rank, &spec)
250 })
251 })
252 .collect::<Vec<_>>();
253
254 partitions.push(TensorPartition {
255 inputs: input_views,
256 outputs: vec![Some(output_view)],
257 });
258 }
259
260 Ok(partitions)
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use std::convert::Infallible;
267
268 use rand::TryRng;
269
270 use super::*;
271 use crate::processing_element::operators::dtype::DataType;
272 use crate::processing_element::operators::partition_tensors;
273
274 fn tensor(dims: &[usize]) -> Option<Tensor> {
275 Some(Tensor::new(dims, &DataType::Bf16, 0))
276 }
277
278 fn tensor_view(dims: &[usize]) -> Option<TensorView> {
279 let tensor = Tensor::new(dims, &DataType::Bf16, 0);
280 Some(TensorView::new_full(tensor))
281 }
282
283 struct FixedBoolRng {
284 values: Vec<bool>,
285 }
286
287 impl FixedBoolRng {
288 fn with_bool_values(values: impl IntoIterator<Item = bool>) -> Self {
289 Self {
290 values: values.into_iter().collect(),
291 }
292 }
293
294 fn next_bool(&mut self) -> bool {
295 if self.values.is_empty() {
296 true
297 } else {
298 self.values.remove(0)
299 }
300 }
301 }
302
303 impl TryRng for FixedBoolRng {
304 type Error = Infallible;
305
306 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
307 Ok(if self.next_bool() { 0 } else { u32::MAX })
308 }
309
310 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
311 Ok(if self.next_bool() { 0 } else { u64::MAX })
312 }
313
314 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
315 for chunk in dst.chunks_mut(size_of::<u64>()) {
316 let bytes = self.try_next_u64()?.to_le_bytes();
317 chunk.copy_from_slice(&bytes[..chunk.len()]);
318 }
319 Ok(())
320 }
321 }
322
323 #[test]
324 fn create_outputs_broadcasts_same_rank_inputs() {
325 let op = OperatorAdd {};
326 let inputs = vec![tensor(&[2, 3, 4]), tensor(&[1, 3, 1])];
327 let mut rng = rand::rng();
328
329 let outputs = op.create_outputs(&inputs, 1.0, &mut rng).unwrap();
330
331 assert_eq!(outputs.len(), 1);
332 assert_eq!(outputs[0].as_ref().unwrap().shape, Shape(vec![2, 3, 4]));
333 }
334
335 #[test]
336 fn create_outputs_broadcasts_different_rank_inputs() {
337 let op = OperatorAdd {};
338 let inputs = vec![tensor(&[3, 4]), tensor(&[2, 1, 4])];
339 let mut rng = rand::rng();
340
341 let outputs = op.create_outputs(&inputs, 1.0, &mut rng).unwrap();
342
343 assert_eq!(outputs.len(), 1);
344 assert_eq!(outputs[0].as_ref().unwrap().shape, Shape(vec![2, 3, 4]));
345 }
346
347 #[test]
348 fn create_outputs_rejects_non_broadcastable_inputs() {
349 let op = OperatorAdd {};
350 let inputs = vec![tensor(&[2, 3]), tensor(&[4, 3])];
351 let mut rng = rand::rng();
352
353 let err = op.create_outputs(&inputs, 1.0, &mut rng).unwrap_err();
354
355 assert!(format!("{err}").contains("cannot broadcast shapes"));
356 }
357
358 #[test]
359 fn validate_tensors_accepts_broadcasted_output_shape() {
360 let op = OperatorAdd {};
361 let inputs = vec![tensor(&[3, 4]), tensor(&[2, 1, 4])];
362 let outputs = vec![tensor(&[2, 3, 4])];
363
364 op.validate_tensors(&inputs, &outputs).unwrap();
365 }
366
367 #[test]
368 fn validate_tensors_rejects_wrong_output_shape() {
369 let op = OperatorAdd {};
370 let inputs = vec![tensor(&[3, 4]), tensor(&[2, 1, 4])];
371 let outputs = vec![tensor(&[3, 4])];
372
373 let err = op.validate_tensors(&inputs, &outputs).unwrap_err();
374
375 assert!(format!("{err}").contains("Invalid output shape"));
376 }
377
378 #[test]
379 fn create_inputs_with_expand_ratio_one_preserves_output_shape() {
380 let op = OperatorAdd {};
381 let outputs = vec![tensor(&[2, 3, 4])];
382
383 for shrink_input_a in [false, true] {
384 let mut rng = FixedBoolRng::with_bool_values([shrink_input_a]);
385
386 let inputs = op.create_inputs(&outputs, 1.0, &mut rng).unwrap();
387
388 assert_eq!(inputs.len(), 2);
389 assert_eq!(inputs[0].as_ref().unwrap().shape, Shape(vec![2, 3, 4]));
390 assert_eq!(inputs[1].as_ref().unwrap().shape, Shape(vec![2, 3, 4]));
391 op.validate_tensors(&inputs, &outputs).unwrap();
392 }
393 }
394
395 #[test]
396 fn create_inputs_with_expand_ratio_zero_creates_broadcastable_inputs() {
397 let op = OperatorAdd {};
398 let outputs = vec![tensor(&[2, 3, 4])];
399
400 for shrink_input_a in [false, true] {
401 let mut rng = FixedBoolRng::with_bool_values([shrink_input_a]);
402
403 let inputs = op.create_inputs(&outputs, 0.0, &mut rng).unwrap();
404
405 assert_eq!(inputs.len(), 2);
406 op.validate_tensors(&inputs, &outputs).unwrap();
407
408 let inputs: Vec<Tensor> = inputs.into_iter().map(|input| input.unwrap()).collect();
409 let outputs: Vec<Tensor> = outputs
410 .iter()
411 .map(|output| output.clone().unwrap())
412 .collect();
413 assert_eq!(inputs[usize::from(shrink_input_a)].shape, outputs[0].shape);
414 assert!(inputs[0].num_dims() <= outputs[0].num_dims());
415 assert!(inputs[1].num_dims() <= outputs[0].num_dims());
416 assert!(
417 inputs[0]
418 .shape
419 .0
420 .iter()
421 .all(|dim| *dim == 1 || outputs[0].shape.0.contains(dim))
422 );
423 assert!(
424 inputs[1]
425 .shape
426 .0
427 .iter()
428 .all(|dim| *dim == 1 || outputs[0].shape.0.contains(dim))
429 );
430 }
431 }
432
433 #[test]
434 fn delay_ticks() {
435 let compute_capabilities = Rc::new(ComputeCapabilities {
436 adds_per_tick: 1.0,
437 muls_per_tick: 100.0,
438 compares_per_tick: 200.0,
439 sram_bytes: 1024,
440 });
441 let operator = OperatorAdd {};
442 let delay_ticks = operator
443 .compute_delay_ticks(
444 &compute_capabilities,
445 &[tensor_view(&[4, 5]), tensor_view(&[4, 5])],
446 &[tensor_view(&[4, 5])],
447 )
448 .unwrap();
449 assert_eq!(delay_ticks, 20);
450
451 let compute_capabilities = Rc::new(ComputeCapabilities {
452 adds_per_tick: 2.0,
453 muls_per_tick: 100.0,
454 compares_per_tick: 100.0,
455 sram_bytes: 1024,
456 });
457 let delay_ticks = operator
458 .compute_delay_ticks(
459 &compute_capabilities,
460 &[tensor_view(&[4, 5]), tensor_view(&[4, 5])],
461 &[tensor_view(&[4, 5])],
462 )
463 .unwrap();
464 assert_eq!(delay_ticks, 10);
465
466 let delay_ticks = operator
467 .compute_delay_ticks(
468 &compute_capabilities,
469 &[tensor_view(&[10, 4, 5]), tensor_view(&[10, 4, 5])],
470 &[tensor_view(&[10, 4, 5])],
471 )
472 .unwrap();
473 assert_eq!(delay_ticks, 100);
474 }
475
476 #[test]
477 fn flop_count_matches_output_elements() {
478 let operator = OperatorAdd {};
479 assert_eq!(
480 operator
481 .compute_flops(
482 &[tensor_view(&[2, 3, 4]), tensor_view(&[1, 3, 1])],
483 &[tensor_view(&[2, 3, 4])],
484 )
485 .unwrap(),
486 24
487 );
488 }
489
490 type OffsetsShapes = (&'static [usize], &'static [usize]);
491
492 fn check_partitions(partitions: &[TensorPartition], expected: &[OffsetsShapes]) {
495 assert_eq!(partitions.len(), expected.len());
496
497 for (partition, (expected_offsets, expected_shape)) in
498 partitions.iter().zip(expected.iter())
499 {
500 let views = partition
501 .inputs
502 .iter()
503 .chain(partition.outputs.iter())
504 .map(|view| view.as_ref().unwrap());
505
506 for view in views {
507 assert_eq!(view.offsets().get_dims().as_slice(), *expected_offsets);
508 assert_eq!(view.shape().get_dims().as_slice(), *expected_shape);
509 }
510 }
511 }
512
513 #[test]
514 fn can_partition_across_one_dimension() {
515 let op = OperatorAdd {};
516 let inputs = vec![tensor(&[1, 5, 3, 4]), tensor(&[1, 5, 3, 4])];
517 let outputs = vec![tensor(&[1, 5, 3, 4])];
518
519 let partitions = partition_tensors(&op, &inputs, &outputs, 5).unwrap();
520 assert_eq!(partitions.len(), 5);
521
522 let expected: &[OffsetsShapes] = &[
523 (&[0, 0, 0, 0], &[1, 1, 3, 4]),
524 (&[0, 1, 0, 0], &[1, 1, 3, 4]),
525 (&[0, 2, 0, 0], &[1, 1, 3, 4]),
526 (&[0, 3, 0, 0], &[1, 1, 3, 4]),
527 (&[0, 4, 0, 0], &[1, 1, 3, 4]),
528 ];
529 check_partitions(&partitions, expected);
530 }
531
532 #[test]
533 fn can_partition_across_multiple_dimensions() {
534 let op = OperatorAdd {};
535 let inputs = vec![tensor(&[2, 3, 4]), tensor(&[2, 3, 4])];
536 let outputs = vec![tensor(&[2, 3, 4])];
537
538 let partitions = partition_tensors(&op, &inputs, &outputs, 5).unwrap();
539 assert_eq!(partitions.len(), 6);
540
541 let expected: &[OffsetsShapes] = &[
542 (&[0, 0, 0], &[1, 1, 4]),
543 (&[0, 1, 0], &[1, 1, 4]),
544 (&[0, 2, 0], &[1, 1, 4]),
545 (&[1, 0, 0], &[1, 1, 4]),
546 (&[1, 1, 0], &[1, 1, 4]),
547 (&[1, 2, 0], &[1, 1, 4]),
548 ];
549 check_partitions(&partitions, expected);
550 }
551
552 #[test]
553 fn partitions_preserve_subset_view_offsets() {
554 let op = OperatorAdd {};
555 let input_a = Tensor::new(&[4, 5, 4], &DataType::Bf16, 0);
556 let input_b = Tensor::new(&[4, 5, 4], &DataType::Bf16, 0);
557 let output = Tensor::new(&[4, 5, 4], &DataType::Bf16, 0);
558
559 let input_views = vec![
560 Some(TensorView::new(input_a, &[2, 2, 4], &[1, 2, 0])),
561 Some(TensorView::new(input_b, &[2, 2, 4], &[1, 2, 0])),
562 ];
563 let output_views = vec![Some(TensorView::new(output, &[2, 2, 4], &[1, 2, 0]))];
564
565 let partitions = op.partition_views(&input_views, &output_views, 2).unwrap();
566 assert_eq!(partitions.len(), 2);
567
568 let expected: &[OffsetsShapes] = &[(&[1, 2, 0], &[1, 2, 4]), (&[2, 2, 0], &[1, 2, 4])];
569 check_partitions(&partitions, expected);
570 }
571}