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 fn is_stateless(&self) -> bool {
22 true
23 }
24
25 fn eval_with_session(
26 &self,
27 _node_id: usize,
28 session: &TurnState,
29 inputs: TVec<TValue>,
30 ) -> TractResult<TVec<TValue>> {
31 let shape = self.shape.eval_to_usize(&session.resolved_symbols)?;
32 Ok(tvec!(inputs[0].broadcast_to_shape(&shape)?.into_tvalue()))
33 }
34}
35
36impl TypedOp for MultiBroadcastTo {
37 fn axes_mapping(
38 &self,
39 inputs: &[&TypedFact],
40 outputs: &[&TypedFact],
41 ) -> TractResult<AxesMapping> {
42 let in_rank = inputs[0].rank();
48 let out_rank = outputs[0].rank();
49 let leading = out_rank.saturating_sub(in_rank);
50 let mut axes = tvec!();
51 let mut alphabet = 'a'..;
52 for o in 0..leading {
53 axes.push(
54 Axis::new(alphabet.next().unwrap(), inputs.len(), outputs.len()).output(0, o),
55 );
56 }
57 for i in 0..in_rank.min(out_rank) {
58 axes.push(
59 Axis::new(alphabet.next().unwrap(), inputs.len(), outputs.len())
60 .input(0, i)
61 .output(0, leading + i),
62 );
63 }
64 AxesMapping::new(inputs.len(), outputs.len(), axes)
65 }
66
67 fn change_axes(
68 &self,
69 model: &TypedModel,
70 node: &TypedNode,
71 _io: InOut,
72 change: &AxisOp,
73 ) -> TractResult<Option<AxisChangeConsequence>> {
74 let input_shape = &model.outlet_fact(node.inputs[0])?.shape;
81 let canonical = change.canonical();
82 let touched: TVec<usize> = match canonical.as_ref() {
83 AxisOp::Add(ix) | AxisOp::Rm(ix) => tvec![*ix],
84 AxisOp::Move(from, to) => {
85 rule_if!(input_shape.rank() == self.shape.rank());
86 tvec![*from, *to]
87 }
88 _ => return Ok(None),
89 };
90 for &ix in &touched {
91 if ix < self.shape.rank()
92 && ix < input_shape.rank()
93 && input_shape[ix] != self.shape[ix]
94 {
95 return Ok(None);
96 }
97 }
98
99 let mut shape = self.shape.clone();
100 if change.change_shape(&mut shape, false).is_ok() {
101 return Ok(Some(AxisChangeConsequence::new(
102 model,
103 node,
104 Some(Box::new(MultiBroadcastTo { shape })),
105 change,
106 )));
107 }
108 Ok(None)
109 }
110
111 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
112 ensure!(inputs.len() == 1);
113 let mut fact = inputs[0].datum_type.fact(self.shape.clone());
114 fact.uniform.clone_from(&inputs[0].uniform);
115 fact.uniform_tdim = inputs[0].uniform_tdim.clone();
116 Ok(tvec!(fact))
117 }
118
119 fn input_roi(
120 &self,
121 model: &TypedModel,
122 node: &TypedNode,
123 ) -> TractResult<Option<TVec<Option<TDim>>>> {
124 crate::optim::propagate_roi::bubble_roi(model, node)
125 }
126
127 fn set_symbols(
128 &self,
129 _source: &TypedModel,
130 node: &TypedNode,
131 target: &mut TypedModel,
132 mapping: &HashMap<OutletId, OutletId>,
133 subs: &HashMap<Symbol, TDim>,
134 ) -> TractResult<TVec<OutletId>> {
135 let input = mapping[&node.inputs[0]];
136 let shape: TVec<_> =
137 self.shape.iter().map(|d| d.substitute_all(subs)).collect::<TractResult<_>>()?;
138 let op = Self { shape: shape.into() };
139 target.wire_node(&node.name, op, &[input])
140 }
141
142 fn declutter(
143 &self,
144 model: &TypedModel,
145 node: &TypedNode,
146 ) -> TractResult<Option<TypedModelPatch>> {
147 let input_fact = model.outlet_fact(node.inputs[0])?;
148 if input_fact.shape == self.shape {
149 return TypedModelPatch::shunt_one_op(model, node);
150 }
151 for succ in &*node.outputs[0].successors {
157 let succ = model.node(succ.node);
158 let Some(op) = succ.op_as::<AxisOp>() else { continue };
159 if input_fact.rank() != self.shape.rank() {
162 continue;
163 }
164 let mut shape = self.shape.clone();
165 if izip!(0.., &*input_fact.shape, &*self.shape)
166 .filter(|(_, l, r)| l != r)
167 .all(|(axis, _, _)| op.transform_axis(axis).is_some())
168 && op.change_shape(&mut shape, false).is_ok()
169 {
170 let mut patch = TypedModelPatch::default();
171 let mut wire = patch.tap_model(model, node.inputs[0])?;
172 wire = patch.wire_node(&succ.name, op.clone(), &[wire])?[0];
173 wire = patch.wire_node(&node.name, MultiBroadcastTo { shape }, &[wire])?[0];
174 patch.shunt_outside(model, succ.id.into(), wire)?;
175 return Ok(Some(patch));
176 }
177 }
178 if let [succ] = &*node.outputs[0].successors {
179 let succ = model.node(succ.node);
180 if succ.op_is::<TypedBinOp>() {
181 let our_slot = node.outputs[0].successors[0].slot;
182 let other_slot = 1 - our_slot;
183 let other_operand = succ.inputs[other_slot];
184 let other_fact = model.outlet_fact(other_operand)?;
185 let output_fact = model.outlet_fact(succ.id.into())?;
186 if input_fact.rank() == other_fact.rank()
187 && multi_broadcast(&[&input_fact.shape, &other_fact.shape])
188 .is_ok_and(|s| *s == *output_fact.shape)
189 {
190 let mut operands = tvec!(node.inputs[0], other_operand);
191 if our_slot == 1 {
192 operands.swap(0, 1);
193 }
194 return TypedModelPatch::rewire(
195 model,
196 &operands,
197 &[succ.id.into()],
198 &|p, inputs| p.wire_node(&succ.name, succ.op.clone(), inputs),
199 )
200 .map(Some);
201 }
202 }
203 }
204 Ok(None)
205 }
206
207 as_op!();
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::ops::change_axes::AxisOp;
214 use crate::ops::logic::And;
215
216 #[test]
219 fn broadcast_move_single_successor_swaps() -> TractResult<()> {
220 let mut model = TypedModel::default();
221 let t = model.symbols.sym("T");
222 let pad = model.add_source("pad", bool::fact(&[t.to_dim()]))?;
223 let unsq = model.wire_node("unsq", AxisOp::Add(0), &[pad])?[0];
224 let bcast = model.wire_node(
225 "bcast",
226 MultiBroadcastTo { shape: ShapeFact::from_dims([t.to_dim(), t.to_dim()]) },
227 &[unsq],
228 )?[0];
229 let mv = model.wire_node("move", AxisOp::Move(0, 1), &[bcast])?[0];
230 model.select_output_outlets(&[mv])?;
231
232 let model = model.into_decluttered()?;
233
234 let move_count = model
235 .nodes()
236 .iter()
237 .filter(|n| matches!(n.op_as::<AxisOp>(), Some(AxisOp::Move(0, 1))))
238 .count();
239 assert_eq!(move_count, 0, "Move should have been pushed through Broadcast and absorbed");
240 Ok(())
241 }
242
243 #[test]
248 fn broadcast_move_fanout_pushes_through_one_branch() -> TractResult<()> {
249 let mut model = TypedModel::default();
250 let t = model.symbols.sym("T");
251 let pad = model.add_source("pad", bool::fact(&[t.to_dim()]))?;
252 let unsq = model.wire_node("unsq", AxisOp::Add(0), &[pad])?[0];
253 let bcast = model.wire_node(
254 "bcast",
255 MultiBroadcastTo { shape: ShapeFact::from_dims([t.to_dim(), t.to_dim()]) },
256 &[unsq],
257 )?[0];
258 let mv = model.wire_node("move", AxisOp::Move(0, 1), &[bcast])?[0];
259 let and = model.wire_node("and", TypedBinOp(Box::new(And), None), &[bcast, mv])?[0];
260 model.select_output_outlets(&[and])?;
261
262 let model = model.into_decluttered()?;
263
264 let bcast_count = model.nodes().iter().filter(|n| n.op_is::<MultiBroadcastTo>()).count();
269 assert_eq!(
270 bcast_count, 0,
271 "Both broadcasts should be subsumed into AND's implicit broadcasting"
272 );
273
274 let and_node =
275 model.nodes().iter().find(|n| n.op_is::<TypedBinOp>()).expect("AND should survive");
276 assert_eq!(and_node.inputs.len(), 2);
277 let and_input_shapes: Vec<_> = and_node
278 .inputs
279 .iter()
280 .map(|i| model.outlet_fact(*i).unwrap().shape.to_tvec())
281 .collect();
282 let expected_a = tvec![1.to_dim(), t.to_dim()];
283 let expected_b = tvec![t.to_dim(), 1.to_dim()];
284 let (a, b) = (&and_input_shapes[0], &and_input_shapes[1]);
285 assert!(
286 (a == &expected_a && b == &expected_b) || (a == &expected_b && b == &expected_a),
287 "AND should receive [1, T] and [T, 1]; got {a:?} and {b:?}"
288 );
289 Ok(())
290 }
291
292 #[test]
298 fn broadcast_adding_leading_axis_does_not_swap_with_axis_op() -> TractResult<()> {
299 let mut model = TypedModel::default();
300 let src = model.add_source("src", f32::fact([512, 1]))?;
301 let bcast = model.wire_node(
302 "bcast",
303 MultiBroadcastTo {
304 shape: ShapeFact::from_dims([1.to_dim(), 512.to_dim(), 16.to_dim()]),
305 },
306 &[src],
307 )?[0];
308 let unsq = model.wire_node("unsq", AxisOp::Add(3), &[bcast])?[0];
309 model.select_output_outlets(&[unsq])?;
310
311 let model = model.into_decluttered()?;
312 assert_eq!(
313 model.output_fact(0)?.shape.to_tvec(),
314 tvec![1.to_dim(), 512.to_dim(), 16.to_dim(), 1.to_dim()]
315 );
316 Ok(())
317 }
318}