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;
74 let canonical = change.canonical();
75 let touched: TVec<usize> = match canonical.as_ref() {
76 AxisOp::Add(ix) | AxisOp::Rm(ix) => tvec![*ix],
77 AxisOp::Move(from, to) => {
78 rule_if!(input_shape.rank() == self.shape.rank());
79 tvec![*from, *to]
80 }
81 _ => return Ok(None),
82 };
83 for &ix in &touched {
84 if ix < self.shape.rank()
85 && ix < input_shape.rank()
86 && input_shape[ix] != self.shape[ix]
87 {
88 return Ok(None);
89 }
90 }
91
92 let mut shape = self.shape.clone();
93 if change.change_shape(&mut shape, false).is_ok() {
94 return Ok(Some(AxisChangeConsequence::new(
95 model,
96 node,
97 Some(Box::new(MultiBroadcastTo { shape })),
98 change,
99 )));
100 }
101 Ok(None)
102 }
103
104 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
105 ensure!(inputs.len() == 1);
106 let mut fact = inputs[0].datum_type.fact(self.shape.clone());
107 fact.uniform.clone_from(&inputs[0].uniform);
108 fact.uniform_tdim = inputs[0].uniform_tdim.clone();
109 Ok(tvec!(fact))
110 }
111
112 fn input_roi(
113 &self,
114 model: &TypedModel,
115 node: &TypedNode,
116 ) -> TractResult<Option<TVec<Option<TDim>>>> {
117 crate::optim::propagate_roi::bubble_roi(model, node)
118 }
119
120 fn set_symbols(
121 &self,
122 _source: &TypedModel,
123 node: &TypedNode,
124 target: &mut TypedModel,
125 mapping: &HashMap<OutletId, OutletId>,
126 subs: &HashMap<Symbol, TDim>,
127 ) -> TractResult<TVec<OutletId>> {
128 let input = mapping[&node.inputs[0]];
129 let shape: TVec<_> =
130 self.shape.iter().map(|d| d.substitute_all(subs)).collect::<TractResult<_>>()?;
131 let op = Self { shape: shape.into() };
132 target.wire_node(&node.name, op, &[input])
133 }
134
135 fn declutter(
136 &self,
137 model: &TypedModel,
138 node: &TypedNode,
139 ) -> TractResult<Option<TypedModelPatch>> {
140 let input_fact = model.outlet_fact(node.inputs[0])?;
141 if input_fact.shape == self.shape {
142 return TypedModelPatch::shunt_one_op(model, node);
143 }
144 for succ in &*node.outputs[0].successors {
150 let succ = model.node(succ.node);
151 let Some(op) = succ.op_as::<AxisOp>() else { continue };
152 if input_fact.rank() != self.shape.rank() {
155 continue;
156 }
157 let mut shape = self.shape.clone();
158 if izip!(0.., &*input_fact.shape, &*self.shape)
159 .filter(|(_, l, r)| l != r)
160 .all(|(axis, _, _)| op.transform_axis(axis).is_some())
161 && op.change_shape(&mut shape, false).is_ok()
162 {
163 let mut patch = TypedModelPatch::default();
164 let mut wire = patch.tap_model(model, node.inputs[0])?;
165 wire = patch.wire_node(&succ.name, op.clone(), &[wire])?[0];
166 wire = patch.wire_node(&node.name, MultiBroadcastTo { shape }, &[wire])?[0];
167 patch.shunt_outside(model, succ.id.into(), wire)?;
168 return Ok(Some(patch));
169 }
170 }
171 if let [succ] = &*node.outputs[0].successors {
172 let succ = model.node(succ.node);
173 if succ.op_is::<TypedBinOp>() {
174 let our_slot = node.outputs[0].successors[0].slot;
175 let other_slot = 1 - our_slot;
176 let other_operand = succ.inputs[other_slot];
177 let other_fact = model.outlet_fact(other_operand)?;
178 let output_fact = model.outlet_fact(succ.id.into())?;
179 if input_fact.rank() == other_fact.rank()
180 && multi_broadcast(&[&input_fact.shape, &other_fact.shape])
181 .is_ok_and(|s| *s == *output_fact.shape)
182 {
183 let mut operands = tvec!(node.inputs[0], other_operand);
184 if our_slot == 1 {
185 operands.swap(0, 1);
186 }
187 return TypedModelPatch::rewire(
188 model,
189 &operands,
190 &[succ.id.into()],
191 &|p, inputs| p.wire_node(&succ.name, succ.op.clone(), inputs),
192 )
193 .map(Some);
194 }
195 }
196 }
197 Ok(None)
198 }
199
200 as_op!();
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::ops::change_axes::AxisOp;
207 use crate::ops::logic::And;
208
209 #[test]
212 fn broadcast_move_single_successor_swaps() -> TractResult<()> {
213 let mut model = TypedModel::default();
214 let t = model.symbols.sym("T");
215 let pad = model.add_source("pad", bool::fact(&[t.to_dim()]))?;
216 let unsq = model.wire_node("unsq", AxisOp::Add(0), &[pad])?[0];
217 let bcast = model.wire_node(
218 "bcast",
219 MultiBroadcastTo { shape: ShapeFact::from_dims([t.to_dim(), t.to_dim()]) },
220 &[unsq],
221 )?[0];
222 let mv = model.wire_node("move", AxisOp::Move(0, 1), &[bcast])?[0];
223 model.select_output_outlets(&[mv])?;
224
225 let model = model.into_decluttered()?;
226
227 let move_count = model
228 .nodes()
229 .iter()
230 .filter(|n| matches!(n.op_as::<AxisOp>(), Some(AxisOp::Move(0, 1))))
231 .count();
232 assert_eq!(move_count, 0, "Move should have been pushed through Broadcast and absorbed");
233 Ok(())
234 }
235
236 #[test]
241 fn broadcast_move_fanout_pushes_through_one_branch() -> TractResult<()> {
242 let mut model = TypedModel::default();
243 let t = model.symbols.sym("T");
244 let pad = model.add_source("pad", bool::fact(&[t.to_dim()]))?;
245 let unsq = model.wire_node("unsq", AxisOp::Add(0), &[pad])?[0];
246 let bcast = model.wire_node(
247 "bcast",
248 MultiBroadcastTo { shape: ShapeFact::from_dims([t.to_dim(), t.to_dim()]) },
249 &[unsq],
250 )?[0];
251 let mv = model.wire_node("move", AxisOp::Move(0, 1), &[bcast])?[0];
252 let and = model.wire_node("and", TypedBinOp(Box::new(And), None), &[bcast, mv])?[0];
253 model.select_output_outlets(&[and])?;
254
255 let model = model.into_decluttered()?;
256
257 let bcast_count = model.nodes().iter().filter(|n| n.op_is::<MultiBroadcastTo>()).count();
262 assert_eq!(
263 bcast_count, 0,
264 "Both broadcasts should be subsumed into AND's implicit broadcasting"
265 );
266
267 let and_node =
268 model.nodes().iter().find(|n| n.op_is::<TypedBinOp>()).expect("AND should survive");
269 assert_eq!(and_node.inputs.len(), 2);
270 let and_input_shapes: Vec<_> = and_node
271 .inputs
272 .iter()
273 .map(|i| model.outlet_fact(*i).unwrap().shape.to_tvec())
274 .collect();
275 let expected_a = tvec![1.to_dim(), t.to_dim()];
276 let expected_b = tvec![t.to_dim(), 1.to_dim()];
277 let (a, b) = (&and_input_shapes[0], &and_input_shapes[1]);
278 assert!(
279 (a == &expected_a && b == &expected_b) || (a == &expected_b && b == &expected_a),
280 "AND should receive [1, T] and [T, 1]; got {a:?} and {b:?}"
281 );
282 Ok(())
283 }
284
285 #[test]
291 fn broadcast_adding_leading_axis_does_not_swap_with_axis_op() -> TractResult<()> {
292 let mut model = TypedModel::default();
293 let src = model.add_source("src", f32::fact([512, 1]))?;
294 let bcast = model.wire_node(
295 "bcast",
296 MultiBroadcastTo {
297 shape: ShapeFact::from_dims([1.to_dim(), 512.to_dim(), 16.to_dim()]),
298 },
299 &[src],
300 )?[0];
301 let unsq = model.wire_node("unsq", AxisOp::Add(3), &[bcast])?[0];
302 model.select_output_outlets(&[unsq])?;
303
304 let model = model.into_decluttered()?;
305 assert_eq!(
306 model.output_fact(0)?.shape.to_tvec(),
307 tvec![1.to_dim(), 512.to_dim(), 16.to_dim(), 1.to_dim()]
308 );
309 Ok(())
310 }
311}