Skip to main content

tract_core/
transform.rs

1use std::borrow::Cow;
2
3use crate::internal::*;
4use crate::ops::matmul::de_block_quant::BlockQuantTransform;
5use std::fmt::Debug;
6
7use tract_data::TractResult;
8
9use crate::floats::FloatPrecisionTranslator;
10use crate::ops::nn::TypedModel;
11
12#[macro_export]
13macro_rules! rule_if {
14    ($cond:expr) => {
15        if !$cond {
16            return Ok(None);
17        }
18    };
19}
20
21#[macro_export]
22macro_rules! rule_if_let {
23    ($pat:pat = $expr:expr) => {
24        let $pat = $expr else {
25            return Ok(None);
26        };
27    };
28}
29
30#[macro_export]
31macro_rules! rule_if_some {
32    ($pat:pat = $expr:expr) => {
33        let Some($pat) = $expr else {
34            return Ok(None);
35        };
36    };
37}
38
39/// Structured include/exclude filter for node names.
40///
41/// If `include` is `None`, all nodes are candidates; if `Some`, only nodes matching
42/// at least one pattern are included. `exclude` then removes from that set.
43#[derive(Debug, Clone, Default)]
44pub struct NodeFilter {
45    pub include: Option<Vec<String>>,
46    pub exclude: Option<Vec<String>>,
47}
48
49impl NodeFilter {
50    /// Returns `true` if the given node name passes the filter.
51    pub fn matches(&self, name: &str) -> bool {
52        let dominated = match &self.include {
53            Some(patterns) => patterns.iter().any(|p| name.contains(p)),
54            None => true,
55        };
56        if !dominated {
57            return false;
58        }
59        match &self.exclude {
60            Some(patterns) => !patterns.iter().any(|p| name.contains(p)),
61            None => true,
62        }
63    }
64
65    /// Returns `true` when neither include nor exclude is set.
66    pub fn is_pass_through(&self) -> bool {
67        self.include.is_none() && self.exclude.is_none()
68    }
69}
70
71/// Parse a legacy filter string (`"!=..."` / `"==..."`) into a `NodeFilter`.
72pub fn parse_legacy_filter(filter: Option<&str>) -> TractResult<NodeFilter> {
73    let Some(filter) = filter.filter(|f| !f.is_empty()) else {
74        return Ok(NodeFilter::default());
75    };
76    if let Some(patterns) = filter.strip_prefix("!=") {
77        let patterns = patterns.split(',').map(|it| it.trim().to_string()).collect();
78        Ok(NodeFilter { exclude: Some(patterns), ..Default::default() })
79    } else if let Some(patterns) = filter.strip_prefix("==") {
80        let patterns = patterns.split(',').map(|it| it.trim().to_string()).collect();
81        Ok(NodeFilter { include: Some(patterns), ..Default::default() })
82    } else {
83        Ok(NodeFilter::default())
84    }
85}
86
87/// Build Float precision translator given a `NodeFilter`. If the filter is pass-through,
88/// all nodes will be translated during the transformation.
89pub fn build_float_translator(
90    from_dt: DatumType,
91    to_dt: DatumType,
92    filter: NodeFilter,
93) -> Box<dyn ModelTransform> {
94    if filter.is_pass_through() {
95        return Box::new(FloatPrecisionTranslator::new(from_dt, to_dt));
96    }
97    Box::new(FloatPrecisionTranslator::with_filter(from_dt, to_dt, move |node| {
98        filter.matches(&node.name)
99    }))
100}
101
102pub trait ModelTransform: Debug {
103    fn name(&self) -> StaticName;
104    fn transform(&self, model: &mut TypedModel) -> TractResult<()>;
105    fn transform_into(&self, mut model: TypedModel) -> TractResult<TypedModel> {
106        self.transform(&mut model)?;
107        Ok(model)
108    }
109}
110
111/// Config for float precision transforms (f32_to_f16, f16_to_f32).
112#[derive(Debug, Default, serde::Deserialize)]
113pub struct FloatTranslatorConfig {
114    /// Legacy filter string (`"!=..."` / `"==..."`).
115    #[serde(default)]
116    pub filter: Option<String>,
117    /// Include patterns — only nodes matching at least one pattern are translated.
118    #[serde(default)]
119    pub include: Option<Vec<String>>,
120    /// Exclude patterns — matching nodes are excluded from translation.
121    #[serde(default)]
122    pub exclude: Option<Vec<String>>,
123}
124
125impl FloatTranslatorConfig {
126    pub fn into_node_filter(self) -> TractResult<NodeFilter> {
127        if self.include.is_some() || self.exclude.is_some() {
128            Ok(NodeFilter { include: self.include, exclude: self.exclude })
129        } else {
130            parse_legacy_filter(self.filter.as_deref())
131        }
132    }
133}
134
135/// Config for the `float_precision` transform.
136#[derive(Debug, serde::Deserialize)]
137pub struct FloatPrecisionConfig {
138    pub from: String,
139    pub to: String,
140    /// Include patterns — only nodes matching at least one pattern are translated.
141    #[serde(default)]
142    pub include: Option<Vec<String>>,
143    /// Exclude patterns — matching nodes are excluded from translation.
144    #[serde(default)]
145    pub exclude: Option<Vec<String>>,
146}
147
148pub struct ModelTransformFactory {
149    pub name: &'static str,
150    /// Build with default config (no params).
151    pub build_default: fn() -> TractResult<Box<dyn ModelTransform>>,
152    /// Build from a type-erased deserializer.
153    pub build: fn(&mut dyn erased_serde::Deserializer) -> TractResult<Box<dyn ModelTransform>>,
154}
155
156inventory::collect!(ModelTransformFactory);
157
158#[macro_export]
159macro_rules! register_simple_model_transform {
160    ($name: expr, $type: expr) => {
161        $crate::internal::inventory::submit! {
162            $crate::transform::ModelTransformFactory {
163                name: $name,
164                build_default: || Ok(Box::new($type)),
165                build: |_de| Ok(Box::new($type)),
166            }
167        }
168    };
169}
170
171#[macro_export]
172macro_rules! register_model_transform {
173    ($name:expr, $config:ty, $builder:expr) => {
174        $crate::internal::inventory::submit! {
175            $crate::transform::ModelTransformFactory {
176                name: $name,
177                build_default: || {
178                    let config = <$config>::default();
179                    let builder: fn($config) -> $crate::prelude::TractResult<Box<dyn $crate::transform::ModelTransform>> = $builder;
180                    builder(config)
181                },
182                build: |de: &mut dyn erased_serde::Deserializer| {
183                    let config: $config = erased_serde::deserialize(de)
184                        .map_err(|e| $crate::internal::anyhow!("deserializing transform config: {e}"))?;
185                    let builder: fn($config) -> $crate::prelude::TractResult<Box<dyn $crate::transform::ModelTransform>> = $builder;
186                    builder(config)
187                },
188            }
189        }
190    };
191}
192
193/// Split a transform spec like `"f32_to_f16(filter: \"!=layer.norm\")"` into name and params.
194pub fn split_spec(spec: &str) -> (Cow<'_, str>, &str) {
195    if let Some(pos) = spec.find('(') {
196        (Cow::Borrowed(&spec[..pos]), &spec[pos..])
197    } else if spec.contains('-') {
198        // Backward compat: simple name with no params, convert kebab→snake
199        (Cow::Owned(spec.replace('-', "_")), "")
200    } else {
201        (Cow::Borrowed(spec), "")
202    }
203}
204
205/// Look up a transform by name, using default config.
206pub fn get_transform(name: &str) -> TractResult<Option<Box<dyn ModelTransform>>> {
207    let (name, _) = split_spec(name);
208    for factory in inventory::iter::<ModelTransformFactory>() {
209        if factory.name == &*name {
210            return Ok(Some((factory.build_default)()?));
211        }
212    }
213    Ok(None)
214}
215
216/// Look up a transform by name, deserializing config from the given deserializer.
217pub fn get_transform_with_params(
218    name: &str,
219    de: &mut dyn erased_serde::Deserializer,
220) -> TractResult<Option<Box<dyn ModelTransform>>> {
221    for factory in inventory::iter::<ModelTransformFactory>() {
222        if factory.name == name {
223            return Ok(Some((factory.build)(de)?));
224        }
225    }
226    Ok(None)
227}
228
229/// Per-symbol substitution: either a concrete integer or a TDim
230/// expression string parsed against the model's symbol scope.
231#[derive(Debug, serde::Deserialize)]
232#[serde(untagged)]
233pub enum SymbolValueSpec {
234    Int(i64),
235    Expr(String),
236}
237
238#[derive(Debug, Default, serde::Deserialize)]
239pub struct SetSymbolsConfig {
240    pub values: std::collections::HashMap<String, SymbolValueSpec>,
241}
242
243#[derive(Debug)]
244struct SetSymbolsTransform(SetSymbolsConfig);
245
246impl ModelTransform for SetSymbolsTransform {
247    fn name(&self) -> StaticName {
248        "set_symbols".into()
249    }
250
251    fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
252        let mut subs = std::collections::HashMap::new();
253        for (k, spec) in &self.0.values {
254            let sym = model.symbols.sym(k);
255            let dim = match spec {
256                SymbolValueSpec::Int(v) => TDim::Val(*v),
257                SymbolValueSpec::Expr(s) => model
258                    .symbols
259                    .parse_tdim(s)
260                    .with_context(|| format!("Parsing TDim expression {s:?} for symbol {k}"))?,
261            };
262            subs.insert(sym, dim);
263        }
264        *model = model.set_symbols(&subs)?;
265        Ok(())
266    }
267}
268
269register_model_transform!("set_symbols", SetSymbolsConfig, |config| Ok(Box::new(
270    SetSymbolsTransform(config)
271)));
272
273/// Ad-hoc fix-up for NNEF artifacts exported before Scan grew the
274/// `external_state` flag (issue #2157). Sets `external_state = true` on every
275/// Scan, asserting that the caller plumbs initial state in and reads final
276/// state out each call. Apply only when the loaded model is known to use
277/// external state management, e.g. the parakeet decoder. Cheaper than
278/// re-exporting cached NNEF.
279///
280/// This does *not* touch the sequence dimension. Inlining the Scan body via
281/// `declutter_single_loop` additionally requires `iters == 1`, which is the
282/// caller's per-call contract — concretize it explicitly (e.g. `--set
283/// TARGETS__TIME=1`), separately from this flag.
284#[derive(Debug)]
285struct ForceScanExternalState;
286
287impl ModelTransform for ForceScanExternalState {
288    fn name(&self) -> StaticName {
289        "force_scan_external_state".into()
290    }
291
292    fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
293        use crate::ops::scan::Scan;
294        for node in &mut model.nodes {
295            if let Some(scan) = node.op_as_mut::<Scan>() {
296                scan.external_state = true;
297            }
298        }
299        Ok(())
300    }
301}
302
303register_simple_model_transform!("force_scan_external_state", ForceScanExternalState);
304
305/// Hands a single-iteration Scan's recurrent state to the caller, so
306/// `declutter_single_loop` can inline the body.
307///
308/// A Scan whose state is seeded by a constant carries that state inside tract
309/// across calls, which `declutter_single_loop` refuses to inline because
310/// inlining would rewire the body's state input back to the seed. This rewires
311/// the state input to a fresh model input and publishes the scan output — which
312/// is the final state when the loop runs once — as a model output, then asserts
313/// `external_state`. The caller must thread the added state pair: pass the
314/// previous value in, feed the returned value back on the next call.
315///
316/// Only Scans that run exactly one iteration, with no warm-up (`skip == 0`),
317/// and are seeded by a constant are touched. A state already fed by a `Source`
318/// belongs to the caller; past one iteration the scan output is the whole
319/// sequence rather than a final value; and a Scan still inside its warm-up
320/// window suppresses body execution in a way an inlined body cannot reproduce.
321/// Model inputs and outputs are appended in Scan node order.
322///
323/// Output is bit-identical to the untransformed model, but the state round-trip
324/// through model I/O is not free: it pays off only where the Scan scaffolding it
325/// removes costs more than the state copy it adds. The profile that benefits is
326/// a pulse-1 streaming graph whose recurrent state is small relative to the
327/// per-frame work — DeepFilterNet3's constant-seeded GRUs are the canonical
328/// case. Models with large recurrent state, or whose remaining Scans are
329/// already cheap, can regress. Measure before adopting.
330#[derive(Debug)]
331struct ExportScanState;
332
333impl ModelTransform for ExportScanState {
334    fn name(&self) -> StaticName {
335        "export_scan_state".into()
336    }
337
338    fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
339        use crate::ops::konst::Const;
340        use crate::ops::scan::{InputMapping, Scan};
341
342        let scans: Vec<usize> =
343            model.nodes().iter().filter(|n| n.op_is::<Scan>()).map(|n| n.id).collect();
344
345        for id in scans {
346            let eligible = {
347                let inputs = model.node_input_facts(id)?;
348                let scan = model.node(id).op_as::<Scan>().unwrap();
349                scan.skip == 0 && scan.iteration_count(&inputs).map(|i| i.is_one()).unwrap_or(false)
350            };
351            if !eligible {
352                continue;
353            }
354
355            let scan = model.node(id).op_as::<Scan>().unwrap();
356            let Some(state_output) =
357                scan.output_mapping.iter().find(|om| om.state).and_then(|om| om.scan.map(|s| s.0))
358            else {
359                continue;
360            };
361            let seeded: Vec<usize> = scan
362                .input_mapping
363                .iter()
364                .enumerate()
365                .filter(|(_, im)| matches!(im, InputMapping::State))
366                .map(|(slot, _)| slot)
367                .filter(|&slot| model.node(model.node(id).inputs[slot].node).op_is::<Const>())
368                .collect();
369            if seeded.is_empty() {
370                continue;
371            }
372
373            for slot in seeded {
374                let fact = model.outlet_fact(model.node(id).inputs[slot])?.clone().without_value();
375                let name = format!("{}.state_in", model.node(id).name);
376                let source = model.add_source(name, fact)?;
377                model.add_edge(source, InletId::new(id, slot))?;
378            }
379            model.outputs.push(OutletId::new(id, state_output));
380            model.node_mut(id).op_as_mut::<Scan>().unwrap().external_state = true;
381        }
382        Ok(())
383    }
384}
385
386register_simple_model_transform!("export_scan_state", ExportScanState);
387
388register_simple_model_transform!("block_quant", BlockQuantTransform);
389
390#[derive(Debug, serde::Deserialize, Default)]
391pub struct SelectOutputsConfig {
392    pub outputs: Vec<String>,
393}
394
395#[derive(Debug)]
396struct SelectOutputsTransform(SelectOutputsConfig);
397
398impl ModelTransform for SelectOutputsTransform {
399    fn name(&self) -> StaticName {
400        "select_outputs".into()
401    }
402
403    fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
404        model.select_outputs_by_name(self.0.outputs.iter())
405    }
406}
407
408register_model_transform!("select_outputs", SelectOutputsConfig, |config| Ok(Box::new(
409    SelectOutputsTransform(config)
410)));
411
412#[derive(Debug, serde::Deserialize, Default)]
413pub struct SelectInputsConfig {
414    pub inputs: Vec<String>,
415}
416
417#[derive(Debug)]
418struct SelectInputsTransform(SelectInputsConfig);
419
420impl ModelTransform for SelectInputsTransform {
421    fn name(&self) -> StaticName {
422        "select_inputs".into()
423    }
424
425    fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
426        model.select_inputs_by_name(self.0.inputs.iter())
427    }
428}
429
430register_model_transform!("select_inputs", SelectInputsConfig, |config| Ok(Box::new(
431    SelectInputsTransform(config)
432)));
433
434inventory::submit! {
435    ModelTransformFactory {
436        name: "f32_to_f16",
437        build_default: || Ok(build_float_translator(DatumType::F32, DatumType::F16, NodeFilter::default())),
438        build: |de| {
439            let config: FloatTranslatorConfig = erased_serde::deserialize(de)
440                .map_err(|e| anyhow::anyhow!("deserializing f32_to_f16 config: {e}"))?;
441            Ok(build_float_translator(DatumType::F32, DatumType::F16, config.into_node_filter()?))
442        },
443    }
444}
445
446inventory::submit! {
447    ModelTransformFactory {
448        name: "f16_to_f32",
449        build_default: || Ok(build_float_translator(DatumType::F16, DatumType::F32, NodeFilter::default())),
450        build: |de| {
451            let config: FloatTranslatorConfig = erased_serde::deserialize(de)
452                .map_err(|e| anyhow::anyhow!("deserializing f16_to_f32 config: {e}"))?;
453            Ok(build_float_translator(DatumType::F16, DatumType::F32, config.into_node_filter()?))
454        },
455    }
456}
457
458inventory::submit! {
459    ModelTransformFactory {
460        name: "float_precision",
461        build_default: || {
462            anyhow::bail!("float_precision transform requires 'from' and 'to' parameters")
463        },
464        build: |de| {
465            let config: FloatPrecisionConfig = erased_serde::deserialize(de)
466                .map_err(|e| anyhow::anyhow!("deserializing float_precision config: {e}"))?;
467            let from_dt: DatumType = config.from.parse()
468                .map_err(|e| anyhow::anyhow!("parsing 'from' datum type: {e}"))?;
469            let to_dt: DatumType = config.to.parse()
470                .map_err(|e| anyhow::anyhow!("parsing 'to' datum type: {e}"))?;
471            let filter = NodeFilter { include: config.include, exclude: config.exclude };
472            Ok(build_float_translator(from_dt, to_dt, filter))
473        },
474    }
475}