1use std::collections::HashSet;
33
34use crate::internal::*;
35use crate::ops::change_axes::AxisOp;
36use crate::ops::einsum::EinSum;
37use crate::ops::source::TypedSource;
38use crate::transform::ModelTransform;
39
40#[derive(Debug, Default, serde::Deserialize)]
41pub struct BatchifyConfig {
42 pub symbol: Option<String>,
44 #[serde(default)]
46 pub shared: Option<Vec<String>>,
47}
48
49#[derive(Debug)]
50pub struct Batchify(pub BatchifyConfig);
51
52impl ModelTransform for Batchify {
53 fn name(&self) -> StaticName {
54 "batchify".into()
55 }
56
57 fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
58 let name = self.0.symbol.as_deref().unwrap_or("B");
59 let symbol = model.symbols.sym(name);
60 let shared = self.0.shared.as_deref().unwrap_or(&[]);
61 *model = batchify(model, &symbol, shared)?;
62 Ok(())
63 }
64}
65
66struct Hosted {
70 op: Option<Box<dyn TypedOp>>,
71 pad: TVec<usize>,
72}
73
74pub fn batchify(model: &TypedModel, batch: &Symbol, shared: &[String]) -> TractResult<TypedModel> {
76 for name in shared {
77 ensure!(
78 model.input_outlets()?.iter().any(|o| &model.node(o.node).name == name),
79 "{name} is listed as a shared input but is not a model input"
80 );
81 }
82 let mut carried: HashSet<OutletId> = Default::default();
83 let mut hosts: HashMap<usize, Hosted> = Default::default();
84 for id in model.eval_order()? {
85 let node = model.node(id);
86 if let Some(source) = node.op_as::<TypedSource>() {
87 if !shared.contains(&node.name) && carrier(&source.fact, batch).is_none() {
88 carried.insert(id.into());
89 }
90 continue;
91 }
92 let batched: TVec<usize> = node
93 .inputs
94 .iter()
95 .enumerate()
96 .filter(|(_, input)| carried.contains(input))
97 .map(|(slot, _)| slot)
98 .collect();
99 if batched.is_empty() {
100 continue;
101 }
102 let hosted = host(model, node, &batched, &carried)
103 .with_context(|| format!("Giving {node} a batch axis"))?;
104 hosts.insert(id, hosted);
105 carried.extend((0..node.outputs.len()).map(|slot| OutletId::new(id, slot)));
106 }
107 let wired = wire(model, batch, shared, &hosts)?;
108 check(wired, batch)
109}
110
111fn carrier(fact: &TypedFact, batch: &Symbol) -> Option<usize> {
114 fact.shape.iter().position(|dim| dim.symbols().contains(batch))
115}
116
117fn host(
120 model: &TypedModel,
121 node: &TypedNode,
122 batched: &[usize],
123 carried: &HashSet<OutletId>,
124) -> TractResult<Hosted> {
125 if let Some(einsum) = node.op_as::<EinSum>() {
126 return batched_einsum(einsum, batched);
127 }
128 let change = AxisOp::Add(0);
129 let consequence = node
130 .op
131 .change_axes(model, node, InOut::In(batched[0]), &change)?
132 .context("the op takes no extra leading axis at all")?;
133 for slot in 0..node.outputs.len() {
134 ensure!(
135 consequence.wire_changes.contains(&(InOut::Out(slot), change.clone())),
136 "the op takes an extra leading axis on an input but does not carry it to output {slot}, \
137 so the seats would be folded into one another"
138 );
139 }
140 let pad = consequence
141 .wire_changes
142 .iter()
143 .filter_map(|(io, _)| match io {
144 InOut::In(slot) if !carried.contains(&node.inputs[*slot]) => Some(*slot),
145 _ => None,
146 })
147 .collect();
148 Ok(Hosted { op: consequence.substitute_op, pad })
149}
150
151fn batched_einsum(einsum: &EinSum, batched: &[usize]) -> TractResult<Hosted> {
155 let label = einsum.axes.available_label();
156 let mut axes = einsum.axes.clone().with_extra_axis(label, InOut::In(batched[0]), 0)?;
157 for slot in &batched[1..] {
158 axes = axes.with_extra_axis_occurency(label, InOut::In(*slot), 0)?;
159 }
160 for slot in 0..axes.output_count() {
161 axes = axes.with_extra_axis_occurency(label, InOut::Out(slot), 0)?;
162 }
163 let op = EinSum { axes, ..einsum.clone() };
164 Ok(Hosted { op: Some(Box::new(op)), pad: tvec!() })
165}
166
167fn wire(
171 model: &TypedModel,
172 batch: &Symbol,
173 shared: &[String],
174 hosts: &HashMap<usize, Hosted>,
175) -> TractResult<TypedModel> {
176 let mut target = TypedModel { symbols: model.symbols.clone(), ..TypedModel::default() };
177 let mut mapping: HashMap<OutletId, OutletId> = Default::default();
178 let mut interface: HashMap<OutletId, OutletId> = Default::default();
179 for id in model.eval_order()? {
180 let node = model.node(id);
181 let wires = if let Some(source) = node.op_as::<TypedSource>() {
182 let (source, interior) =
183 batched_source(&mut target, node, source, batch, !shared.contains(&node.name))?;
184 interface.insert(id.into(), source);
185 tvec!(interior)
186 } else {
187 let hosted = hosts.get(&id);
188 let mut inputs: TVec<OutletId> =
189 node.inputs.iter().map(|input| mapping[input]).collect();
190 for slot in hosted.map(|h| &*h.pad).unwrap_or(&[]) {
191 inputs[*slot] = target.wire_node(
192 format!("{}.batchify.rank.{slot}", node.name),
193 AxisOp::Add(0),
194 &[inputs[*slot]],
195 )?[0];
196 }
197 let op = hosted.and_then(|h| h.op.clone()).unwrap_or_else(|| node.op.clone());
198 target
199 .wire_node(&node.name, op, &inputs)
200 .with_context(|| format!("Wiring {node} with a batch axis"))?
201 };
202 for (slot, wire) in wires.into_iter().enumerate() {
203 let outlet = OutletId::new(id, slot);
204 if let Some(label) = model.outlet_label(outlet) {
205 target.set_outlet_label(wire, label.to_string())?;
206 }
207 mapping.insert(outlet, wire);
208 }
209 }
210 let inputs: Vec<OutletId> = model.input_outlets()?.iter().map(|i| interface[i]).collect();
211 let mut outputs: Vec<OutletId> = model.output_outlets()?.iter().map(|o| mapping[o]).collect();
212 for (ix, output) in outputs.iter_mut().enumerate() {
213 let fact = target.outlet_fact(*output)?.clone();
214 if let Some(axis) = carrier(&fact, batch).filter(|axis| *axis > 0) {
215 let label = target.outlet_label(*output).map(|l| l.to_string());
216 *output = target.wire_node(
217 format!("batchify.move.output.{ix}"),
218 AxisOp::Move(axis, 0),
219 &[*output],
220 )?[0];
221 if let Some(label) = label {
222 target.set_outlet_label(*output, label)?;
223 }
224 }
225 }
226 target.set_input_outlets(&inputs)?;
227 target.select_output_outlets(&outputs)?;
228 Ok(target)
229}
230
231fn batched_source(
236 target: &mut TypedModel,
237 node: &TypedNode,
238 source: &TypedSource,
239 batch: &Symbol,
240 batched: bool,
241) -> TractResult<(OutletId, OutletId)> {
242 let carrier = carrier(&source.fact, batch);
243 if !batched || carrier == Some(0) {
244 let wire = target.wire_node(&node.name, source.clone(), &[])?[0];
245 return Ok((wire, wire));
246 }
247 let mut shape = source.fact.shape.to_tvec();
248 let mut restore = None;
249 match carrier {
250 Some(axis) => {
251 let dim = shape.remove(axis);
252 shape.insert(0, dim);
253 restore = Some(AxisOp::Move(0, axis));
254 }
255 None => shape.insert(0, batch.to_dim()),
256 }
257 let mut fact = source.fact.clone();
258 fact.shape = shape.into();
259 let wire = target.wire_node(&node.name, TypedSource::new(fact), &[])?[0];
260 match restore {
261 Some(restore) => {
262 let interior =
263 target.wire_node(format!("{}.batchify.move", node.name), restore, &[wire])?[0];
264 Ok((wire, interior))
265 }
266 None => Ok((wire, wire)),
267 }
268}
269
270fn check(model: TypedModel, batch: &Symbol) -> TractResult<TypedModel> {
273 let batch = batch.to_dim();
274 let mut shared = vec![];
275 for (ix, outlet) in model.output_outlets()?.iter().enumerate() {
276 let fact = model.outlet_fact(*outlet)?;
277 if fact.shape.first() == Some(&batch) {
278 return Ok(model);
279 }
280 shared.push(format!("output {ix} is {:?}", fact.shape));
281 }
282 bail!("No output carries the batch axis on axis 0: {}", shared.join(", "))
283}
284
285register_model_transform!("batchify", BatchifyConfig, |config| Ok(Box::new(Batchify(config))));