use std::collections::HashSet;
use crate::internal::*;
use crate::ops::array::{MultiBroadcastTo, TypedConcat};
use crate::ops::change_axes::AxisOp;
use crate::ops::einsum::EinSum;
use crate::ops::source::TypedSource;
use crate::transform::ModelTransform;
#[derive(Debug, Default, serde::Deserialize)]
pub struct BatchifyConfig {
pub symbol: Option<String>,
#[serde(default)]
pub shared: Option<Vec<String>>,
}
#[derive(Debug)]
pub struct Batchify(pub BatchifyConfig);
impl ModelTransform for Batchify {
fn name(&self) -> StaticName {
"batchify".into()
}
fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
let name = self.0.symbol.as_deref().unwrap_or("B");
let symbol = model.symbols.sym(name);
let shared = self.0.shared.as_deref().unwrap_or(&[]);
*model = batchify(model, &symbol, shared)?;
Ok(())
}
}
struct Hosted {
op: Option<Box<dyn TypedOp>>,
pad: TVec<usize>,
broadcast: bool,
}
pub fn batchify(model: &TypedModel, batch: &Symbol, shared: &[String]) -> TractResult<TypedModel> {
for name in shared {
ensure!(
model.input_outlets()?.iter().any(|o| &model.node(o.node).name == name),
"{name} is listed as a shared input but is not a model input"
);
}
let mut join = false;
for outlet in model.input_outlets()? {
let name = &model.node(outlet.node).name;
join |= !shared.contains(name) && carrier(model.outlet_fact(*outlet)?, batch).is_some();
}
let mut carried: HashSet<OutletId> = Default::default();
let mut hosts: HashMap<usize, Hosted> = Default::default();
for id in model.eval_order()? {
let node = model.node(id);
if let Some(source) = node.op_as::<TypedSource>() {
if !shared.contains(&node.name)
&& carrier(&source.fact, batch).is_none()
&& !collapsed(&source.fact, batch, join)
{
carried.insert(id.into());
}
continue;
}
let batched: TVec<usize> = node
.inputs
.iter()
.enumerate()
.filter(|(_, input)| carried.contains(input))
.map(|(slot, _)| slot)
.collect();
if batched.is_empty() {
continue;
}
let mut hosted = host(model, node, &batched, &carried)
.with_context(|| format!("Giving {node} a batch axis"))?;
hosted.broadcast = node.op_is::<TypedConcat>();
hosts.insert(id, hosted);
carried.extend((0..node.outputs.len()).map(|slot| OutletId::new(id, slot)));
}
let wired = wire(model, batch, shared, join, &hosts)?;
check(wired, batch)
}
fn carrier(fact: &TypedFact, batch: &Symbol) -> Option<usize> {
fact.shape.iter().position(|dim| dim.symbols().contains(batch))
}
fn collapsed(fact: &TypedFact, batch: &Symbol, join: bool) -> bool {
join && carrier(fact, batch).is_none() && fact.shape.first() == Some(&1.to_dim())
}
fn host(
model: &TypedModel,
node: &TypedNode,
batched: &[usize],
carried: &HashSet<OutletId>,
) -> TractResult<Hosted> {
if let Some(einsum) = node.op_as::<EinSum>() {
return batched_einsum(einsum, batched);
}
if let Some(axis_op) = node.op_as::<AxisOp>() {
return Ok(Hosted {
op: Some(Box::new(shifted_axis_op(axis_op))),
pad: tvec!(),
broadcast: false,
});
}
let change = AxisOp::Add(0);
let consequence = node
.op
.change_axes(model, node, InOut::In(batched[0]), &change)?
.context("the op takes no extra leading axis at all")?;
for slot in 0..node.outputs.len() {
ensure!(
consequence.wire_changes.contains(&(InOut::Out(slot), change.clone())),
"the op takes an extra leading axis on an input but does not carry it to output {slot}, \
so the seats would be folded into one another"
);
}
let pad = consequence
.wire_changes
.iter()
.filter_map(|(io, _)| match io {
InOut::In(slot) if !carried.contains(&node.inputs[*slot]) => Some(*slot),
_ => None,
})
.collect();
Ok(Hosted { op: consequence.substitute_op, pad, broadcast: false })
}
fn batched_einsum(einsum: &EinSum, batched: &[usize]) -> TractResult<Hosted> {
let label = einsum.axes.available_label();
let mut axes = einsum.axes.clone().with_extra_axis(label, InOut::In(batched[0]), 0)?;
for slot in &batched[1..] {
axes = axes.with_extra_axis_occurency(label, InOut::In(*slot), 0)?;
}
for slot in 0..axes.output_count() {
axes = axes.with_extra_axis_occurency(label, InOut::Out(slot), 0)?;
}
let op = EinSum { axes, ..einsum.clone() };
Ok(Hosted { op: Some(Box::new(op)), pad: tvec!(), broadcast: false })
}
fn shifted_axis_op(op: &AxisOp) -> AxisOp {
match op {
AxisOp::Add(a) => AxisOp::Add(a + 1),
AxisOp::Rm(a) => AxisOp::Rm(a + 1),
AxisOp::Move(from, to) => AxisOp::Move(from + 1, to + 1),
AxisOp::Reshape(at, from, to) => AxisOp::Reshape(at + 1, from.clone(), to.clone()),
}
}
fn shift_pulse_axes(model: &mut TypedModel, key: &str, widened: &[bool]) -> TractResult<()> {
let Some(axes) = model.properties.get(key) else { return Ok(()) };
let mut axes: Vec<i64> = axes.cast_to::<i64>()?.try_as_plain_ram()?.as_slice::<i64>()?.to_vec();
ensure!(
axes.len() == widened.len(),
"{key} holds {} axes for {} interfaces",
axes.len(),
widened.len()
);
for (axis, widened) in axes.iter_mut().zip(widened) {
if *widened && *axis >= 0 {
*axis += 1;
}
}
model.properties.insert(key.to_string(), tensor1(&axes).into_arc_tensor());
Ok(())
}
fn wire(
model: &TypedModel,
batch: &Symbol,
shared: &[String],
join: bool,
hosts: &HashMap<usize, Hosted>,
) -> TractResult<TypedModel> {
let mut target = TypedModel { symbols: model.symbols.clone(), ..TypedModel::default() };
let mut mapping: HashMap<OutletId, OutletId> = Default::default();
let mut interface: HashMap<OutletId, OutletId> = Default::default();
for id in model.eval_order()? {
let node = model.node(id);
let wires = if let Some(source) = node.op_as::<TypedSource>() {
let (source, interior) = batched_source(
&mut target,
node,
source,
batch,
!shared.contains(&node.name),
join,
)?;
interface.insert(id.into(), source);
tvec!(interior)
} else {
let hosted = hosts.get(&id);
let mut inputs: TVec<OutletId> =
node.inputs.iter().map(|input| mapping[input]).collect();
for slot in hosted.map(|h| &*h.pad).unwrap_or(&[]) {
let mut wire = target.wire_node(
format!("{}.batchify.rank.{slot}", node.name),
AxisOp::Add(0),
&[inputs[*slot]],
)?[0];
if hosted.is_some_and(|h| h.broadcast) {
let mut shape: TVec<TDim> = target.outlet_fact(wire)?.shape.to_tvec();
shape[0] = batch.to_dim();
wire = target.wire_node(
format!("{}.batchify.broadcast.{slot}", node.name),
MultiBroadcastTo { shape: shape.into() },
&[wire],
)?[0];
}
inputs[*slot] = wire;
}
let op = hosted.and_then(|h| h.op.clone()).unwrap_or_else(|| node.op.clone());
target
.wire_node(&node.name, op, &inputs)
.with_context(|| format!("Wiring {node} with a batch axis"))?
};
for (slot, wire) in wires.into_iter().enumerate() {
let outlet = OutletId::new(id, slot);
if let Some(label) = model.outlet_label(outlet) {
target.set_outlet_label(wire, label.to_string())?;
}
mapping.insert(outlet, wire);
}
}
let inputs: Vec<OutletId> = model.input_outlets()?.iter().map(|i| interface[i]).collect();
let mut outputs: Vec<OutletId> = model.output_outlets()?.iter().map(|o| mapping[o]).collect();
for (ix, output) in outputs.iter_mut().enumerate() {
let fact = target.outlet_fact(*output)?.clone();
if let Some(axis) = carrier(&fact, batch).filter(|axis| *axis > 0) {
let label = target.outlet_label(*output).map(|l| l.to_string());
*output = target.wire_node(
format!("batchify.move.output.{ix}"),
AxisOp::Move(axis, 0),
&[*output],
)?[0];
if let Some(label) = label {
target.set_outlet_label(*output, label)?;
}
}
}
target.set_input_outlets(&inputs)?;
target.select_output_outlets(&outputs)?;
target.properties.clone_from(&model.properties);
if !join {
let batched_inputs: Vec<bool> = model
.input_outlets()?
.iter()
.map(|o| !shared.contains(&model.node(o.node).name))
.collect();
shift_pulse_axes(&mut target, "pulse.input_axes", &batched_inputs)?;
let batched_outputs = vec![true; model.output_outlets()?.len()];
shift_pulse_axes(&mut target, "pulse.output_axes", &batched_outputs)?;
}
Ok(target)
}
fn batched_source(
target: &mut TypedModel,
node: &TypedNode,
source: &TypedSource,
batch: &Symbol,
batched: bool,
join: bool,
) -> TractResult<(OutletId, OutletId)> {
let carrier = carrier(&source.fact, batch);
if !batched || carrier == Some(0) {
let wire = target.wire_node(&node.name, source.clone(), &[])?[0];
return Ok((wire, wire));
}
if collapsed(&source.fact, batch, join) {
let mut fact = source.fact.clone();
let mut shape = fact.shape.to_tvec();
shape[0] = batch.to_dim();
fact.shape = shape.into();
let wire = target.wire_node(&node.name, TypedSource::new(fact), &[])?[0];
return Ok((wire, wire));
}
let mut shape = source.fact.shape.to_tvec();
let mut restore = None;
match carrier {
Some(axis) => {
let dim = shape.remove(axis);
shape.insert(0, dim);
restore = Some(AxisOp::Move(0, axis));
}
None => shape.insert(0, batch.to_dim()),
}
let mut fact = source.fact.clone();
fact.shape = shape.into();
let wire = target.wire_node(&node.name, TypedSource::new(fact), &[])?[0];
match restore {
Some(restore) => {
let interior =
target.wire_node(format!("{}.batchify.move", node.name), restore, &[wire])?[0];
Ok((wire, interior))
}
None => Ok((wire, wire)),
}
}
fn check(model: TypedModel, batch: &Symbol) -> TractResult<TypedModel> {
let batch = batch.to_dim();
let mut shared = vec![];
for (ix, outlet) in model.output_outlets()?.iter().enumerate() {
let fact = model.outlet_fact(*outlet)?;
if fact.shape.first() == Some(&batch) {
return Ok(model);
}
shared.push(format!("output {ix} is {:?}", fact.shape));
}
bail!("No output carries the batch axis on axis 0: {}", shared.join(", "))
}
register_model_transform!("batchify", BatchifyConfig, |config| Ok(Box::new(Batchify(config))));