1use tract_data::itertools::izip;
2
3use crate::broadcast::multi_broadcast;
4use crate::internal::*;
5use crate::ops::binary::TypedBinOp;
6
7#[derive(Debug, Clone, new, Hash, PartialEq, Eq)]
8pub struct MultiBroadcastTo {
9 pub shape: ShapeFact,
10}
11
12impl Op for MultiBroadcastTo {
13 fn name(&self) -> StaticName {
14 "MultiBroadcastTo".into()
15 }
16
17 op_as_typed_op!();
18}
19
20impl EvalOp for MultiBroadcastTo {
21 op_out_of_plan!();
22
23 fn eval(&self, ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
24 let shape = self.shape.eval_to_usize(ctx.symbols)?;
25 Ok(tvec!(inputs[0].broadcast_to_shape(&shape)?.into_tvalue()))
26 }
27}
28
29impl TypedOp for MultiBroadcastTo {
30 fn axes_mapping(
31 &self,
32 inputs: &[&TypedFact],
33 outputs: &[&TypedFact],
34 ) -> TractResult<AxesMapping> {
35 let in_rank = inputs[0].rank();
41 let out_rank = outputs[0].rank();
42 let leading = out_rank.saturating_sub(in_rank);
43 let mut axes = tvec!();
44 let mut alphabet = 'a'..;
45 for o in 0..leading {
46 axes.push(
47 Axis::new(alphabet.next().unwrap(), inputs.len(), outputs.len()).output(0, o),
48 );
49 }
50 for i in 0..in_rank.min(out_rank) {
51 axes.push(
52 Axis::new(alphabet.next().unwrap(), inputs.len(), outputs.len())
53 .input(0, i)
54 .output(0, leading + i),
55 );
56 }
57 AxesMapping::new(inputs.len(), outputs.len(), axes)
58 }
59
60 fn change_axes(
61 &self,
62 model: &TypedModel,
63 node: &TypedNode,
64 _io: InOut,
65 change: &AxisOp,
66 ) -> TractResult<Option<AxisChangeConsequence>> {
67 let input_shape = &model.outlet_fact(node.inputs[0])?.shape;
72 let canonical = change.canonical();
73 let touched: TVec<usize> = match canonical.as_ref() {
74 AxisOp::Add(ix) | AxisOp::Rm(ix) => tvec![*ix],
75 AxisOp::Move(from, to) => {
76 rule_if!(input_shape.rank() == self.shape.rank());
77 tvec![*from, *to]
78 }
79 _ => return Ok(None),
80 };
81 let passthrough = touched.iter().all(|&ix| {
82 ix < input_shape.rank() && ix < self.shape.rank() && input_shape[ix] == self.shape[ix]
83 });
84 if matches!(canonical.as_ref(), AxisOp::Move(..)) && !passthrough {
88 return Ok(None);
89 }
90
91 let mut shape = self.shape.clone();
92 if change.change_shape(&mut shape, false).is_ok() {
93 let mut wire_changes: TVec<(InOut, AxisOp)> = tvec![(InOut::Out(0), change.clone())];
94 if passthrough {
95 wire_changes.push((InOut::In(0), change.clone()));
96 } else {
97 let offset = self.shape.rank() as isize - input_shape.rank() as isize;
106 let k = touched[0] as isize - offset;
107 let displaced_all_ones =
113 (0..k.max(0) as usize).all(|i| input_shape[i] == 1.to_dim());
114 let window_safe = match canonical.as_ref() {
115 AxisOp::Add(_) => k <= 0,
116 _ => k < 0,
117 } || displaced_all_ones;
118 let broadcastable = multi_broadcast(&[input_shape, &shape])
119 .is_ok_and(|b| b.as_slice() == shape.as_ref());
120 if !(window_safe && broadcastable) {
121 return Ok(None);
122 }
123 }
124 return Ok(Some(AxisChangeConsequence {
125 wire_changes,
126 substitute_op: Some(Box::new(MultiBroadcastTo { shape })),
127 }));
128 }
129 Ok(None)
130 }
131
132 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
133 ensure!(inputs.len() == 1);
134 let mut fact = inputs[0].datum_type.fact(self.shape.clone());
135 fact.uniform.clone_from(&inputs[0].uniform);
136 fact.uniform_tdim = inputs[0].uniform_tdim.clone();
137 Ok(tvec!(fact))
138 }
139
140 fn input_roi(
141 &self,
142 model: &TypedModel,
143 node: &TypedNode,
144 ) -> TractResult<Option<TVec<Option<TDim>>>> {
145 crate::optim::propagate_roi::bubble_roi(model, node)
146 }
147
148 fn set_symbols(
149 &self,
150 _source: &TypedModel,
151 node: &TypedNode,
152 target: &mut TypedModel,
153 mapping: &HashMap<OutletId, OutletId>,
154 subs: &HashMap<Symbol, TDim>,
155 ) -> TractResult<TVec<OutletId>> {
156 let input = mapping[&node.inputs[0]];
157 let shape: TVec<_> =
158 self.shape.iter().map(|d| d.substitute_all(subs)).collect::<TractResult<_>>()?;
159 let op = Self { shape: shape.into() };
160 target.wire_node(&node.name, op, &[input])
161 }
162
163 fn declutter(
164 &self,
165 model: &TypedModel,
166 node: &TypedNode,
167 ) -> TractResult<Option<TypedModelPatch>> {
168 let input_fact = model.outlet_fact(node.inputs[0])?;
169 if input_fact.shape == self.shape {
170 return TypedModelPatch::shunt_one_op(model, node);
171 }
172 for succ in &*node.outputs[0].successors {
178 let succ = model.node(succ.node);
179 let Some(op) = succ.op_as::<AxisOp>() else { continue };
180 if input_fact.rank() != self.shape.rank() {
183 continue;
184 }
185 let mut shape = self.shape.clone();
186 if izip!(0.., &*input_fact.shape, &*self.shape)
187 .filter(|(_, l, r)| l != r)
188 .all(|(axis, _, _)| op.transform_axis(axis).is_some())
189 && op.change_shape(&mut shape, false).is_ok()
190 {
191 let mut patch = TypedModelPatch::default();
192 let mut wire = patch.tap_model(model, node.inputs[0])?;
193 wire = patch.wire_node(&succ.name, op.clone(), &[wire])?[0];
194 wire = patch.wire_node(&node.name, MultiBroadcastTo { shape }, &[wire])?[0];
195 patch.shunt_outside(model, succ.id.into(), wire)?;
196 return Ok(Some(patch));
197 }
198 }
199 if let [succ] = &*node.outputs[0].successors {
200 let succ = model.node(succ.node);
201 if succ.op_is::<TypedBinOp>() {
202 let our_slot = node.outputs[0].successors[0].slot;
203 let other_slot = 1 - our_slot;
204 let other_operand = succ.inputs[other_slot];
205 let other_fact = model.outlet_fact(other_operand)?;
206 let output_fact = model.outlet_fact(succ.id.into())?;
207 if input_fact.rank() == other_fact.rank()
208 && multi_broadcast(&[&input_fact.shape, &other_fact.shape])
209 .is_ok_and(|s| *s == *output_fact.shape)
210 {
211 let mut operands = tvec!(node.inputs[0], other_operand);
212 if our_slot == 1 {
213 operands.swap(0, 1);
214 }
215 return TypedModelPatch::rewire(
216 model,
217 &operands,
218 &[succ.id.into()],
219 &|p, inputs| p.wire_node(&succ.name, succ.op.clone(), inputs),
220 )
221 .map(Some);
222 }
223 }
224 }
225 Ok(None)
226 }
227
228 as_op!();
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::ops::change_axes::AxisOp;
235 use crate::ops::change_axes::{AxisChange, InOut};
236 use crate::ops::logic::And;
237 use proptest::prelude::*;
238
239 #[test]
242 fn broadcast_move_single_successor_swaps() -> TractResult<()> {
243 let mut model = TypedModel::default();
244 let t = model.symbols.sym("T");
245 let pad = model.add_source("pad", bool::fact(&[t.to_dim()]))?;
246 let unsq = model.wire_node("unsq", AxisOp::Add(0), &[pad])?[0];
247 let bcast = model.wire_node(
248 "bcast",
249 MultiBroadcastTo { shape: ShapeFact::from_dims([t.to_dim(), t.to_dim()]) },
250 &[unsq],
251 )?[0];
252 let mv = model.wire_node("move", AxisOp::Move(0, 1), &[bcast])?[0];
253 model.select_output_outlets(&[mv])?;
254
255 let model = model.into_decluttered()?;
256
257 let move_count = model
258 .nodes()
259 .iter()
260 .filter(|n| matches!(n.op_as::<AxisOp>(), Some(AxisOp::Move(0, 1))))
261 .count();
262 assert_eq!(move_count, 0, "Move should have been pushed through Broadcast and absorbed");
263 Ok(())
264 }
265
266 #[test]
271 fn broadcast_move_fanout_pushes_through_one_branch() -> TractResult<()> {
272 let mut model = TypedModel::default();
273 let t = model.symbols.sym("T");
274 let pad = model.add_source("pad", bool::fact(&[t.to_dim()]))?;
275 let unsq = model.wire_node("unsq", AxisOp::Add(0), &[pad])?[0];
276 let bcast = model.wire_node(
277 "bcast",
278 MultiBroadcastTo { shape: ShapeFact::from_dims([t.to_dim(), t.to_dim()]) },
279 &[unsq],
280 )?[0];
281 let mv = model.wire_node("move", AxisOp::Move(0, 1), &[bcast])?[0];
282 let and = model.wire_node("and", TypedBinOp(Box::new(And), None), &[bcast, mv])?[0];
283 model.select_output_outlets(&[and])?;
284
285 let model = model.into_decluttered()?;
286
287 let bcast_count = model.nodes().iter().filter(|n| n.op_is::<MultiBroadcastTo>()).count();
292 assert_eq!(
293 bcast_count, 0,
294 "Both broadcasts should be subsumed into AND's implicit broadcasting"
295 );
296
297 let and_node =
298 model.nodes().iter().find(|n| n.op_is::<TypedBinOp>()).expect("AND should survive");
299 assert_eq!(and_node.inputs.len(), 2);
300 let and_input_shapes: Vec<_> = and_node
301 .inputs
302 .iter()
303 .map(|i| model.outlet_fact(*i).unwrap().shape.to_tvec())
304 .collect();
305 let expected_a = tvec![1.to_dim(), t.to_dim()];
306 let expected_b = tvec![t.to_dim(), 1.to_dim()];
307 let (a, b) = (&and_input_shapes[0], &and_input_shapes[1]);
308 assert!(
309 (a == &expected_a && b == &expected_b) || (a == &expected_b && b == &expected_a),
310 "AND should receive [1, T] and [T, 1]; got {a:?} and {b:?}"
311 );
312 Ok(())
313 }
314
315 #[test]
321 fn broadcast_adding_leading_axis_does_not_swap_with_axis_op() -> TractResult<()> {
322 let mut model = TypedModel::default();
323 let src = model.add_source("src", f32::fact([512, 1]))?;
324 let bcast = model.wire_node(
325 "bcast",
326 MultiBroadcastTo {
327 shape: ShapeFact::from_dims([1.to_dim(), 512.to_dim(), 16.to_dim()]),
328 },
329 &[src],
330 )?[0];
331 let unsq = model.wire_node("unsq", AxisOp::Add(3), &[bcast])?[0];
332 model.select_output_outlets(&[unsq])?;
333
334 let model = model.into_decluttered()?;
335 assert_eq!(
336 model.output_fact(0)?.shape.to_tvec(),
337 tvec![1.to_dim(), 512.to_dim(), 16.to_dim(), 1.to_dim()]
338 );
339 Ok(())
340 }
341
342 #[test]
346 fn broadcast_axis_change_absorbed_not_propagated_to_input() -> TractResult<()> {
347 let mut model = TypedModel::default();
348 let c = model.add_const("c", tensor1(&[0f32]))?;
349 let s = model.symbols.sym("S");
350 let y = model.wire_node(
351 "y",
352 MultiBroadcastTo::new(ShapeFact::from_dims(tvec![s.to_dim(), 1usize.to_dim()])),
353 &[c],
354 )?[0];
355 let node = model.node(y.node);
356 let consequence = node
357 .op
358 .change_axes(&model, node, InOut::Out(0), &AxisOp::Add(2))?
359 .context("expected the broadcast to absorb the change")?;
360 assert_eq!(consequence.wire_changes, tvec![(InOut::Out(0), AxisOp::Add(2))]);
361 Ok(())
362 }
363
364 #[test]
367 fn passthrough_axis_change_propagates_to_input() -> TractResult<()> {
368 let mut model = TypedModel::default();
369 let s = model.symbols.sym("S");
370 let src = model.add_source("src", f32::fact([s.to_dim(), 1usize.into()]))?;
371 let y = model.wire_node(
372 "y",
373 MultiBroadcastTo::new(ShapeFact::from_dims(tvec![s.to_dim(), 1usize.to_dim()])),
374 &[src],
375 )?[0];
376 let node = model.node(y.node);
377 let consequence = node
378 .op
379 .change_axes(&model, node, InOut::Out(0), &AxisOp::Add(1))?
380 .context("expected propagation")?;
381 assert_eq!(
382 consequence.wire_changes,
383 tvec![(InOut::Out(0), AxisOp::Add(1)), (InOut::In(0), AxisOp::Add(1))]
384 );
385 Ok(())
386 }
387
388 #[test]
392 fn axis_change_through_broadcast_with_volume_one_const() -> TractResult<()> {
393 let mut model = TypedModel::default();
394 let s = model.symbols.sym("S");
395 let definer = model.add_source("definer", f32::fact([s.to_dim()]))?;
396 let c = model.add_const("c", tensor1(&[0f32]))?;
397 let y = model.wire_node(
398 "y",
399 MultiBroadcastTo::new(ShapeFact::from_dims(tvec![
400 s.to_dim(),
401 1usize.to_dim(),
402 1usize.to_dim()
403 ])),
404 &[c],
405 )?;
406 let out = model.wire_node("out", AxisOp::Add(0), &[y[0]])?;
407 model.select_output_outlets(&[out[0], definer])?;
408 let change = AxisChange { outlet: out[0], op: AxisOp::Add(3) };
409 let mut explored = Default::default();
410 let (patch, _) =
411 crate::optim::change_axes::change_axes(&model, &change, &[], &[], &mut explored)?
412 .context("axis change through a broadcast should apply")?;
413 patch.apply(&mut model)?;
414 model.compact()?;
415 let found = crate::internal::TypedSimplePlan::new(model)?
416 .run(tvec!(tensor1(&[0f32; 2]).into_tvalue()))?;
417 assert_eq!(found[0].shape(), &[1, 2, 1, 1, 1]);
418 Ok(())
419 }
420
421 #[test]
424 fn rm_of_broadcast_axis_absorbed() -> TractResult<()> {
425 let mut model = TypedModel::default();
426 let src = model.add_source("src", f32::fact([5usize, 7usize]))?;
427 let y = model.wire_node(
428 "y",
429 MultiBroadcastTo::new(ShapeFact::from_dims(tvec![
430 1usize.to_dim(),
431 5usize.to_dim(),
432 7usize.to_dim()
433 ])),
434 &[src],
435 )?[0];
436 let node = model.node(y.node);
437 let consequence = node
438 .op
439 .change_axes(&model, node, InOut::Out(0), &AxisOp::Rm(0))?
440 .context("expected the broadcast to absorb the removal")?;
441 assert_eq!(consequence.wire_changes, tvec![(InOut::Out(0), AxisOp::Rm(0))]);
442 let substitute = consequence
443 .substitute_op
444 .as_deref()
445 .and_then(|op| op.as_op().downcast_ref::<MultiBroadcastTo>())
446 .context("expected a MultiBroadcastTo substitute")?;
447 assert_eq!(substitute.shape.to_tvec(), tvec![5.to_dim(), 7.to_dim()]);
448 Ok(())
449 }
450
451 #[test]
454 fn move_over_broadcast_axis_blocked() -> TractResult<()> {
455 let mut model = TypedModel::default();
456 let src = model.add_source("src", f32::fact([5usize, 1usize]))?;
457 let y = model.wire_node(
458 "y",
459 MultiBroadcastTo::new(ShapeFact::from_dims(tvec![5usize.to_dim(), 7usize.to_dim()])),
460 &[src],
461 )?[0];
462 let node = model.node(y.node);
463 let blocked = node
464 .op
465 .change_axes(&model, node, InOut::Out(0), &AxisOp::Move(0, 1))
466 .map(|r| r.is_none())
467 .unwrap_or(false);
468 assert!(blocked, "expected the broadcast to block the move");
469 Ok(())
470 }
471
472 #[test]
474 fn passthrough_move_propagates_to_input() -> TractResult<()> {
475 let mut model = TypedModel::default();
476 let src = model.add_source("src", f32::fact([5usize, 7usize]))?;
477 let y = model.wire_node(
478 "y",
479 MultiBroadcastTo::new(ShapeFact::from_dims(tvec![5usize.to_dim(), 7usize.to_dim()])),
480 &[src],
481 )?[0];
482 let node = model.node(y.node);
483 let consequence = node
484 .op
485 .change_axes(&model, node, InOut::Out(0), &AxisOp::Move(0, 1))?
486 .context("expected propagation")?;
487 assert_eq!(
488 consequence.wire_changes,
489 tvec![(InOut::Out(0), AxisOp::Move(0, 1)), (InOut::In(0), AxisOp::Move(0, 1))]
490 );
491 Ok(())
492 }
493
494 #[test]
498 fn absorbed_change_keeps_input_broadcastable() -> TractResult<()> {
499 let mut model = TypedModel::default();
500 let src = model.add_source("src", f32::fact([5usize, 1usize]))?;
501 let y = model.wire_node(
502 "y",
503 MultiBroadcastTo::new(ShapeFact::from_dims(tvec![
504 9usize.to_dim(),
505 5usize.to_dim(),
506 1usize.to_dim()
507 ])),
508 &[src],
509 )?[0];
510 let squeezed = model.wire_node("squeezed", AxisOp::Rm(2), &[y])?[0];
511 model.select_output_outlets(&[squeezed])?;
512 let optimized = model.into_optimized()?;
513 let found = crate::internal::TypedSimplePlan::new(optimized)?
514 .run(tvec!(tensor2(&[[0f32; 1]; 5]).into_tvalue()))?;
515 assert_eq!(found[0].shape(), &[9, 5]);
516 Ok(())
517 }
518
519 #[test]
524 fn absorbed_change_does_not_repair_input_axes() -> TractResult<()> {
525 let mut model = TypedModel::default();
526 let src = model.add_source("src", f32::fact([5usize, 1usize]))?;
527 let y = model.wire_node(
528 "y",
529 MultiBroadcastTo::new(ShapeFact::from_dims(tvec![
530 5usize.to_dim(),
531 5usize.to_dim(),
532 1usize.to_dim()
533 ])),
534 &[src],
535 )?[0];
536 let squeezed = model.wire_node("squeezed", AxisOp::Rm(2), &[y])?[0];
537 model.select_output_outlets(&[squeezed])?;
538 let input: Vec<f32> = (0..5).map(|i| i as f32).collect();
539 let input = Tensor::from_shape(&[5, 1], &input)?.into_tvalue();
540 let raw =
541 crate::internal::TypedSimplePlan::new(model.clone())?.run(tvec!(input.clone()))?;
542 let optimized = model.into_optimized()?;
543 let optimized_values =
544 crate::internal::TypedSimplePlan::new(optimized)?.run(tvec!(input))?;
545 assert_eq!(*raw[0], *optimized_values[0]);
546 Ok(())
547 }
548
549 proptest! {
550 #[test]
553 fn prop_axis_changes_preserve_values(
554 input in proptest::collection::vec(1usize..4, 1usize..4),
555 leading in proptest::collection::vec(1usize..4, 0usize..3),
556 bumps in proptest::collection::vec(0usize..4, 0usize..6),
557 selector in 0usize..64,
558 ix in 0usize..8,
559 move_to in 0usize..8,
560 ) {
561 let mut bumps = bumps.into_iter();
562 let mut target: TVec<usize> = leading.clone().into();
563 for dim in &input {
564 let dim = if *dim == 1 { bumps.next().unwrap_or(0).max(1) } else { *dim };
565 target.push(dim);
566 }
567 let op = match selector % 3 {
568 0 => AxisOp::Add(ix.min(target.len())),
569 1 => {
570 if ix < target.len() && target[ix] == 1 {
571 AxisOp::Rm(ix)
572 } else {
573 AxisOp::Add(ix.min(target.len()))
574 }
575 }
576 _ => {
577 let a = ix.min(target.len().saturating_sub(1));
578 let b = (move_to.min(target.len().saturating_sub(1)), a);
579 let b = if b.0 == a { a.saturating_sub(1) } else { b.0 };
580 AxisOp::Move(a, b)
581 }
582 };
583 let mut model = TypedModel::default();
584 let src = model.add_source("src", i32::datum_type().fact(&*input)).unwrap();
585 let bcast = model.wire_node(
586 "bcast",
587 MultiBroadcastTo::new(ShapeFact::from_dims(
588 target.iter().map(|d| d.to_dim()).collect::<TVec<_>>(),
589 )),
590 &[src],
591 ).unwrap()[0];
592 let out = model.wire_node("out", op, &[bcast]).unwrap()[0];
593 model.select_output_outlets(&[out]).unwrap();
594 let volume: usize = input.iter().product();
595 let data: Vec<i32> = (0..volume as i32).collect();
596 let wire = Tensor::from_shape(&input, &data).unwrap().into_tvalue();
597 let raw = crate::internal::TypedSimplePlan::new(model.clone())
598 .unwrap()
599 .run(tvec!(wire.clone()))
600 .unwrap();
601 let optimized = model.into_optimized().unwrap();
602 let optimized_values = crate::internal::TypedSimplePlan::new(optimized)
603 .unwrap()
604 .run(tvec!(wire))
605 .unwrap();
606 prop_assert_eq!(&*raw[0], &*optimized_values[0]);
607 }
608 }
609}