1use std::collections::HashSet;
33
34use crate::internal::*;
35use crate::ops::array::{MultiBroadcastTo, TypedConcat};
36use crate::ops::change_axes::AxisOp;
37use crate::ops::einsum::EinSum;
38use crate::ops::source::TypedSource;
39use crate::transform::ModelTransform;
40
41#[derive(Debug, Default, serde::Deserialize)]
42pub struct BatchifyConfig {
43 pub symbol: Option<String>,
45 #[serde(default)]
47 pub shared: Option<Vec<String>>,
48}
49
50#[derive(Debug)]
51pub struct Batchify(pub BatchifyConfig);
52
53impl ModelTransform for Batchify {
54 fn name(&self) -> StaticName {
55 "batchify".into()
56 }
57
58 fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
59 let name = self.0.symbol.as_deref().unwrap_or("B");
60 let symbol = model.symbols.sym(name);
61 let shared = self.0.shared.as_deref().unwrap_or(&[]);
62 *model = batchify(model, &symbol, shared)?;
63 Ok(())
64 }
65}
66
67struct Hosted {
71 op: Option<Box<dyn TypedOp>>,
72 pad: TVec<usize>,
73 broadcast: bool,
77}
78
79pub fn batchify(model: &TypedModel, batch: &Symbol, shared: &[String]) -> TractResult<TypedModel> {
81 for name in shared {
82 ensure!(
83 model.input_outlets()?.iter().any(|o| &model.node(o.node).name == name),
84 "{name} is listed as a shared input but is not a model input"
85 );
86 }
87 let mut join = false;
92 for outlet in model.input_outlets()? {
93 let name = &model.node(outlet.node).name;
94 join |= !shared.contains(name) && carrier(model.outlet_fact(*outlet)?, batch).is_some();
95 }
96 let mut carried: HashSet<OutletId> = Default::default();
97 let mut hosts: HashMap<usize, Hosted> = Default::default();
98 for id in model.eval_order()? {
99 let node = model.node(id);
100 if let Some(source) = node.op_as::<TypedSource>() {
101 if !shared.contains(&node.name)
102 && carrier(&source.fact, batch).is_none()
103 && !collapsed(&source.fact, batch, join)
104 {
105 carried.insert(id.into());
106 }
107 continue;
108 }
109 let batched: TVec<usize> = node
110 .inputs
111 .iter()
112 .enumerate()
113 .filter(|(_, input)| carried.contains(input))
114 .map(|(slot, _)| slot)
115 .collect();
116 if batched.is_empty() {
117 continue;
118 }
119 let mut hosted = host(model, node, &batched, &carried)
120 .with_context(|| format!("Giving {node} a batch axis"))?;
121 hosted.broadcast = node.op_is::<TypedConcat>();
122 hosts.insert(id, hosted);
123 carried.extend((0..node.outputs.len()).map(|slot| OutletId::new(id, slot)));
124 }
125 let wired = wire(model, batch, shared, join, &hosts)?;
126 check(wired, batch)
127}
128
129fn carrier(fact: &TypedFact, batch: &Symbol) -> Option<usize> {
132 fact.shape.iter().position(|dim| dim.symbols().contains(batch))
133}
134
135fn collapsed(fact: &TypedFact, batch: &Symbol, join: bool) -> bool {
140 join && carrier(fact, batch).is_none() && fact.shape.first() == Some(&1.to_dim())
141}
142
143fn host(
146 model: &TypedModel,
147 node: &TypedNode,
148 batched: &[usize],
149 carried: &HashSet<OutletId>,
150) -> TractResult<Hosted> {
151 if let Some(einsum) = node.op_as::<EinSum>() {
152 return batched_einsum(einsum, batched);
153 }
154 if let Some(axis_op) = node.op_as::<AxisOp>() {
155 return Ok(Hosted {
156 op: Some(Box::new(shifted_axis_op(axis_op))),
157 pad: tvec!(),
158 broadcast: false,
159 });
160 }
161 let change = AxisOp::Add(0);
162 let consequence = node
163 .op
164 .change_axes(model, node, InOut::In(batched[0]), &change)?
165 .context("the op takes no extra leading axis at all")?;
166 for slot in 0..node.outputs.len() {
167 ensure!(
168 consequence.wire_changes.contains(&(InOut::Out(slot), change.clone())),
169 "the op takes an extra leading axis on an input but does not carry it to output {slot}, \
170 so the seats would be folded into one another"
171 );
172 }
173 let pad = consequence
174 .wire_changes
175 .iter()
176 .filter_map(|(io, _)| match io {
177 InOut::In(slot) if !carried.contains(&node.inputs[*slot]) => Some(*slot),
178 _ => None,
179 })
180 .collect();
181 Ok(Hosted { op: consequence.substitute_op, pad, broadcast: false })
182}
183
184fn batched_einsum(einsum: &EinSum, batched: &[usize]) -> TractResult<Hosted> {
188 let label = einsum.axes.available_label();
189 let mut axes = einsum.axes.clone().with_extra_axis(label, InOut::In(batched[0]), 0)?;
190 for slot in &batched[1..] {
191 axes = axes.with_extra_axis_occurency(label, InOut::In(*slot), 0)?;
192 }
193 for slot in 0..axes.output_count() {
194 axes = axes.with_extra_axis_occurency(label, InOut::Out(slot), 0)?;
195 }
196 let op = EinSum { axes, ..einsum.clone() };
197 Ok(Hosted { op: Some(Box::new(op)), pad: tvec!(), broadcast: false })
198}
199
200fn shifted_axis_op(op: &AxisOp) -> AxisOp {
206 match op {
207 AxisOp::Add(a) => AxisOp::Add(a + 1),
208 AxisOp::Rm(a) => AxisOp::Rm(a + 1),
209 AxisOp::Move(from, to) => AxisOp::Move(from + 1, to + 1),
210 AxisOp::Reshape(at, from, to) => AxisOp::Reshape(at + 1, from.clone(), to.clone()),
211 }
212}
213
214fn shift_pulse_axes(model: &mut TypedModel, key: &str, widened: &[bool]) -> TractResult<()> {
219 let Some(axes) = model.properties.get(key) else { return Ok(()) };
220 let mut axes: Vec<i64> = axes.cast_to::<i64>()?.try_as_plain_ram()?.as_slice::<i64>()?.to_vec();
221 ensure!(
222 axes.len() == widened.len(),
223 "{key} holds {} axes for {} interfaces",
224 axes.len(),
225 widened.len()
226 );
227 for (axis, widened) in axes.iter_mut().zip(widened) {
228 if *widened && *axis >= 0 {
229 *axis += 1;
230 }
231 }
232 model.properties.insert(key.to_string(), tensor1(&axes).into_arc_tensor());
233 Ok(())
234}
235
236fn wire(
239 model: &TypedModel,
240 batch: &Symbol,
241 shared: &[String],
242 join: bool,
243 hosts: &HashMap<usize, Hosted>,
244) -> TractResult<TypedModel> {
245 let mut target = TypedModel { symbols: model.symbols.clone(), ..TypedModel::default() };
246 let mut mapping: HashMap<OutletId, OutletId> = Default::default();
247 let mut interface: HashMap<OutletId, OutletId> = Default::default();
248 for id in model.eval_order()? {
249 let node = model.node(id);
250 let wires = if let Some(source) = node.op_as::<TypedSource>() {
251 let (source, interior) = batched_source(
252 &mut target,
253 node,
254 source,
255 batch,
256 !shared.contains(&node.name),
257 join,
258 )?;
259 interface.insert(id.into(), source);
260 tvec!(interior)
261 } else {
262 let hosted = hosts.get(&id);
263 let mut inputs: TVec<OutletId> =
264 node.inputs.iter().map(|input| mapping[input]).collect();
265 for slot in hosted.map(|h| &*h.pad).unwrap_or(&[]) {
266 let mut wire = target.wire_node(
267 format!("{}.batchify.rank.{slot}", node.name),
268 AxisOp::Add(0),
269 &[inputs[*slot]],
270 )?[0];
271 if hosted.is_some_and(|h| h.broadcast) {
272 let mut shape: TVec<TDim> = target.outlet_fact(wire)?.shape.to_tvec();
273 shape[0] = batch.to_dim();
274 wire = target.wire_node(
275 format!("{}.batchify.broadcast.{slot}", node.name),
276 MultiBroadcastTo { shape: shape.into() },
277 &[wire],
278 )?[0];
279 }
280 inputs[*slot] = wire;
281 }
282 let op = hosted.and_then(|h| h.op.clone()).unwrap_or_else(|| node.op.clone());
283 target
284 .wire_node(&node.name, op, &inputs)
285 .with_context(|| format!("Wiring {node} with a batch axis"))?
286 };
287 for (slot, wire) in wires.into_iter().enumerate() {
288 let outlet = OutletId::new(id, slot);
289 if let Some(label) = model.outlet_label(outlet) {
290 target.set_outlet_label(wire, label.to_string())?;
291 }
292 mapping.insert(outlet, wire);
293 }
294 }
295 let inputs: Vec<OutletId> = model.input_outlets()?.iter().map(|i| interface[i]).collect();
296 let mut outputs: Vec<OutletId> = model.output_outlets()?.iter().map(|o| mapping[o]).collect();
297 for (ix, output) in outputs.iter_mut().enumerate() {
298 let fact = target.outlet_fact(*output)?.clone();
299 if let Some(axis) = carrier(&fact, batch).filter(|axis| *axis > 0) {
300 let label = target.outlet_label(*output).map(|l| l.to_string());
301 *output = target.wire_node(
302 format!("batchify.move.output.{ix}"),
303 AxisOp::Move(axis, 0),
304 &[*output],
305 )?[0];
306 if let Some(label) = label {
307 target.set_outlet_label(*output, label)?;
308 }
309 }
310 }
311 target.set_input_outlets(&inputs)?;
312 target.select_output_outlets(&outputs)?;
313 target.properties.clone_from(&model.properties);
318 if !join {
319 let batched_inputs: Vec<bool> = model
320 .input_outlets()?
321 .iter()
322 .map(|o| !shared.contains(&model.node(o.node).name))
323 .collect();
324 shift_pulse_axes(&mut target, "pulse.input_axes", &batched_inputs)?;
325 let batched_outputs = vec![true; model.output_outlets()?.len()];
326 shift_pulse_axes(&mut target, "pulse.output_axes", &batched_outputs)?;
327 }
328 Ok(target)
329}
330
331fn batched_source(
336 target: &mut TypedModel,
337 node: &TypedNode,
338 source: &TypedSource,
339 batch: &Symbol,
340 batched: bool,
341 join: bool,
342) -> TractResult<(OutletId, OutletId)> {
343 let carrier = carrier(&source.fact, batch);
344 if !batched || carrier == Some(0) {
345 let wire = target.wire_node(&node.name, source.clone(), &[])?[0];
346 return Ok((wire, wire));
347 }
348 if collapsed(&source.fact, batch, join) {
349 let mut fact = source.fact.clone();
352 let mut shape = fact.shape.to_tvec();
353 shape[0] = batch.to_dim();
354 fact.shape = shape.into();
355 let wire = target.wire_node(&node.name, TypedSource::new(fact), &[])?[0];
356 return Ok((wire, wire));
357 }
358 let mut shape = source.fact.shape.to_tvec();
359 let mut restore = None;
360 match carrier {
361 Some(axis) => {
362 let dim = shape.remove(axis);
363 shape.insert(0, dim);
364 restore = Some(AxisOp::Move(0, axis));
365 }
366 None => shape.insert(0, batch.to_dim()),
367 }
368 let mut fact = source.fact.clone();
369 fact.shape = shape.into();
370 let wire = target.wire_node(&node.name, TypedSource::new(fact), &[])?[0];
371 match restore {
372 Some(restore) => {
373 let interior =
374 target.wire_node(format!("{}.batchify.move", node.name), restore, &[wire])?[0];
375 Ok((wire, interior))
376 }
377 None => Ok((wire, wire)),
378 }
379}
380
381fn check(model: TypedModel, batch: &Symbol) -> TractResult<TypedModel> {
384 let batch = batch.to_dim();
385 let mut shared = vec![];
386 for (ix, outlet) in model.output_outlets()?.iter().enumerate() {
387 let fact = model.outlet_fact(*outlet)?;
388 if fact.shape.first() == Some(&batch) {
389 return Ok(model);
390 }
391 shared.push(format!("output {ix} is {:?}", fact.shape));
392 }
393 bail!("No output carries the batch axis on axis 0: {}", shared.join(", "))
394}
395
396register_model_transform!("batchify", BatchifyConfig, |config| Ok(Box::new(Batchify(config))));