use std::collections::HashSet;
use crate::internal::*;
use tract_core::model::translator::Translate;
use tract_core::ops::array::MultiBroadcastTo;
use tract_core::ops::binary::TypedBinOp;
use tract_core::ops::change_axes::wire_rank_broadcast;
use tract_core::ops::konst::Const;
use tract_core::ops::source::TypedSource;
use tract_core::transform::ModelTransform;
#[derive(Debug, Default, serde::Deserialize)]
pub struct BatchifyDataFreeConfig {
pub symbol: Option<String>,
}
#[derive(Debug)]
pub struct BatchifyDataFree(pub BatchifyDataFreeConfig);
impl ModelTransform for BatchifyDataFree {
fn name(&self) -> std::borrow::Cow<'static, str> {
"batchify_data_free".into()
}
fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
let name = self.0.symbol.as_deref().unwrap_or("BATCH");
let symbol = model.symbols.sym(name);
let mut carried = false;
for outlet in model.input_outlets()? {
let fact = model.outlet_fact(*outlet)?;
carried |= fact.shape.iter().any(|d| d.symbols().contains(&symbol));
}
ensure!(carried, "No model input is sized by {name}, so there is no batch axis to join");
*model = batchify_data_free(model, &symbol.to_dim())?;
Ok(())
}
}
fn data_free_nodes(model: &TypedModel) -> TractResult<HashSet<usize>> {
let mut fed: HashSet<usize> = Default::default();
let order = model.eval_order()?;
for &id in &order {
let node = model.node(id);
if node.op_is::<TypedSource>() || node.inputs.iter().any(|i| fed.contains(&i.node)) {
fed.insert(id);
}
}
Ok(order
.into_iter()
.filter(|id| !fed.contains(id) && !model.node(*id).op_is::<Const>())
.collect())
}
fn data_free_facts(
model: &TypedModel,
data_free: &HashSet<usize>,
) -> TractResult<HashMap<OutletId, TypedFact>> {
let mut facts: HashMap<OutletId, TypedFact> = Default::default();
for id in model.eval_order()? {
if !data_free.contains(&id) {
continue;
}
let node = model.node(id);
let mut inputs: TVec<TypedFact> = tvec!();
for input in &node.inputs {
inputs.push(match facts.get(input) {
Some(fact) => fact.clone(),
None => model.outlet_fact(*input)?.clone(),
});
}
let inputs: TVec<&TypedFact> = inputs.iter().collect();
for (slot, fact) in node.op.output_facts(&inputs)?.into_iter().enumerate() {
facts.insert(OutletId::new(id, slot), fact);
}
}
Ok(facts)
}
fn quadratic(fact: &TypedFact) -> bool {
let mut per_symbol: HashMap<Symbol, usize> = Default::default();
for dim in fact.shape.iter() {
for symbol in dim.symbols() {
*per_symbol.entry(symbol).or_default() += 1;
}
}
per_symbol.values().any(|&axes| axes >= 2)
}
fn exits(
model: &TypedModel,
data_free: &HashSet<usize>,
facts: &HashMap<OutletId, TypedFact>,
batch: &TDim,
) -> TractResult<Vec<OutletId>> {
let outputs = model.output_outlets()?;
let mut exits = vec![];
for &id in data_free {
for slot in 0..model.node(id).outputs.len() {
let outlet = OutletId::new(id, slot);
let fact = &facts[&outlet];
if !quadratic(fact)
|| fact.shape.first().is_some_and(|d| d == batch)
|| !model.node(id).outputs[slot]
.successors
.iter()
.any(|s| !data_free.contains(&s.node))
{
continue;
}
ensure!(
!outputs.contains(&outlet),
"{} is a model output and has no data input, batchifying it would change the model interface",
model.node(id)
);
exits.push(outlet);
}
}
Ok(exits)
}
fn find_placeholder(
model: &TypedModel,
data_free: &HashSet<usize>,
exits: &[OutletId],
) -> TractResult<usize> {
let mut todo: Vec<usize> = exits.iter().map(|o| o.node).collect();
let mut seen: HashSet<usize> = Default::default();
let mut found: HashSet<usize> = Default::default();
while let Some(id) = todo.pop() {
if !seen.insert(id) {
continue;
}
let node = model.node(id);
if matches!(node.op_as::<AxisOp>(), Some(AxisOp::Add(0))) {
found.insert(id);
continue;
}
let upstream: Vec<usize> =
node.inputs.iter().map(|i| i.node).filter(|n| data_free.contains(n)).collect();
ensure!(
!upstream.is_empty(),
"No batch axis placeholder between {node} and the data-free subgraph's exits"
);
todo.extend(upstream);
}
ensure!(
found.len() == 1,
"Expected one batch axis placeholder, found {:?}",
found.iter().map(|&id| model.node(id).name.as_str()).collect::<Vec<_>>()
);
Ok(found.into_iter().next().unwrap())
}
fn nodes_to_widen(model: &TypedModel, data_free: &HashSet<usize>, node: usize) -> HashSet<usize> {
let mut widened: HashSet<usize> = Default::default();
let mut todo: Vec<usize> =
model.node(node).inputs.iter().map(|i| i.node).filter(|n| data_free.contains(n)).collect();
while let Some(id) = todo.pop() {
if !widened.insert(id) {
continue;
}
todo.extend(model.node(id).inputs.iter().map(|i| i.node).filter(|n| data_free.contains(n)));
}
widened
}
pub fn batchify_data_free(model: &TypedModel, batch: &TDim) -> TractResult<TypedModel> {
let data_free = data_free_nodes(model)?;
let facts = data_free_facts(model, &data_free)?;
let exits = exits(model, &data_free, &facts, batch)?;
if exits.is_empty() {
return Ok(model.clone());
}
let placeholder = find_placeholder(model, &data_free, &exits)?;
let widened = nodes_to_widen(model, &data_free, placeholder);
Widen { batch: batch.clone(), nodes: widened, placeholder, facts }.translate_model(model)
}
struct Widen {
batch: TDim,
nodes: HashSet<usize>,
placeholder: usize,
facts: HashMap<OutletId, TypedFact>,
}
impl std::fmt::Debug for Widen {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Widen({}, {} wires)", self.batch, self.nodes.len())
}
}
impl Translate<TypedFact, Box<dyn TypedOp>, TypedFact, Box<dyn TypedOp>> for Widen {
fn translate_node(
&self,
_source: &TypedModel,
node: &TypedNode,
target: &mut TypedModel,
mapping: &HashMap<OutletId, OutletId>,
) -> TractResult<TVec<OutletId>> {
let inputs: TVec<OutletId> = node.inputs.iter().map(|i| mapping[i]).collect();
if node.id == self.placeholder {
return Ok(inputs);
}
if !self.nodes.contains(&node.id) {
return target.wire_node(&node.name, node.op.clone(), &inputs);
}
ensure!(node.outputs.len() == 1, "{node} has several outputs, which batchify cannot widen");
let op: Box<dyn TypedOp> = match node.op_as::<AxisOp>() {
Some(axis_op) => Box::new(axis_op.pad_left(1)),
None => node.op.clone(),
};
let inputs = if node.op_is::<TypedBinOp>() {
wire_rank_broadcast(&node.name, target, &inputs)?
} else {
inputs
};
let mut wire = target.wire_node(&node.name, op, &inputs)?;
if node.inputs.iter().all(|i| !self.nodes.contains(&i.node)) {
wire =
target.wire_node(format!("{}.batchify.axis", node.name), AxisOp::Add(0), &wire)?;
let mut shape = target.outlet_fact(wire[0])?.shape.to_tvec();
shape[0] = self.batch.clone();
wire = target.wire_node(
format!("{}.batchify.extent", node.name),
MultiBroadcastTo { shape: shape.into() },
&wire,
)?;
}
let expected: TVec<TDim> = std::iter::once(self.batch.clone())
.chain(self.facts[&node.id.into()].shape.iter().cloned())
.collect();
let got = target.outlet_fact(wire[0])?.shape.to_tvec();
ensure!(got == expected, "Batchifying {node} gave shape {got:?}, expected {expected:?}");
Ok(wire)
}
}