diff --git a/Cargo.toml b/Cargo.toml index e4ef14f175..7d86beba86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "onnx", "libcli", "cli", + "extra", "tflite", diff --git a/api/ffi/src/lib.rs b/api/ffi/src/lib.rs index de0f2b7560..9c57c0ed81 100644 --- a/api/ffi/src/lib.rs +++ b/api/ffi/src/lib.rs @@ -124,6 +124,14 @@ pub unsafe extern "C" fn tract_nnef_enable_tract_core(nnef: *mut TractNnef) -> T }) } +#[no_mangle] +pub unsafe extern "C" fn tract_nnef_enable_tract_extra(nnef: *mut TractNnef) -> TRACT_RESULT { + wrap(|| unsafe { + check_not_null!(nnef); + (*nnef).0.enable_tract_extra() + }) +} + #[no_mangle] pub unsafe extern "C" fn tract_nnef_enable_onnx(nnef: *mut TractNnef) -> TRACT_RESULT { wrap(|| unsafe { diff --git a/api/generate-tract-h.sh b/api/generate-tract-h.sh new file mode 100755 index 0000000000..3ce87536c5 --- /dev/null +++ b/api/generate-tract-h.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -ex + +cbindgen ffi > tract,h +cp tract.h c +cp tract.h proxy/sys diff --git a/api/proxy/src/lib.rs b/api/proxy/src/lib.rs index c2da4459be..06e6f3b96e 100644 --- a/api/proxy/src/lib.rs +++ b/api/proxy/src/lib.rs @@ -69,6 +69,10 @@ impl NnefInterface for Nnef { check!(sys::tract_nnef_enable_tract_core(self.0)) } + fn enable_tract_extra(&mut self) -> Result<()> { + check!(sys::tract_nnef_enable_tract_extra(self.0)) + } + fn enable_onnx(&mut self) -> Result<()> { check!(sys::tract_nnef_enable_onnx(self.0)) } diff --git a/api/proxy/sys/tract.h b/api/proxy/sys/tract.h index b3c9254ff6..1471d65425 100644 --- a/api/proxy/sys/tract.h +++ b/api/proxy/sys/tract.h @@ -83,7 +83,7 @@ const char *tract_version(void); void tract_free_cstring(char *ptr); /** - * Creates an instance of an NNEF framework and parser that can be used to load models. + * Creates an instance of an NNEF framework and parser that can be used to load and dump NNEF models. * * The returned object should be destroyed with `tract_nnef_destroy` once the model * has been loaded. @@ -92,6 +92,8 @@ enum TRACT_RESULT tract_nnef_create(struct TractNnef **nnef); enum TRACT_RESULT tract_nnef_enable_tract_core(struct TractNnef *nnef); +enum TRACT_RESULT tract_nnef_enable_tract_extra(struct TractNnef *nnef); + enum TRACT_RESULT tract_nnef_enable_onnx(struct TractNnef *nnef); enum TRACT_RESULT tract_nnef_enable_pulse(struct TractNnef *nnef); diff --git a/api/py/tests/mobilenet_onnx_test.py b/api/py/tests/mobilenet_onnx_test.py index 4bd6d7b7b2..b3d5785bd5 100644 --- a/api/py/tests/mobilenet_onnx_test.py +++ b/api/py/tests/mobilenet_onnx_test.py @@ -48,7 +48,7 @@ def test_state(): assert numpy.argmax(confidences) == 652 def test_nnef_register(): - tract.nnef().with_tract_core().with_onnx().with_pulse() + tract.nnef().with_tract_core().with_onnx().with_pulse().with_tract_extra() def test_nnef(): model = ( diff --git a/api/py/tract/nnef.py b/api/py/tract/nnef.py index 074895c3a7..e9d7634b78 100644 --- a/api/py/tract/nnef.py +++ b/api/py/tract/nnef.py @@ -56,6 +56,14 @@ def with_tract_core(self) -> "Nnef": check(lib.tract_nnef_enable_tract_core(self.ptr)) return self + def with_tract_extra(self) -> "Nnef": + """ + Enable tract-extra extensions to NNEF. + """ + self._valid() + check(lib.tract_nnef_enable_tract_extra(self.ptr)) + return self + def with_onnx(self) -> "Nnef": """ Enable tract-opl extensions to NNEF to covers (more or) ONNX operator set diff --git a/api/rs/Cargo.toml b/api/rs/Cargo.toml index 4cb1dd03d5..105921040b 100644 --- a/api/rs/Cargo.toml +++ b/api/rs/Cargo.toml @@ -22,6 +22,7 @@ tract-api = { path = ".." , version = "=0.20.19-pre" } tract-nnef = { path = "../../nnef/" , version = "=0.20.19-pre" } tract-onnx-opl = { path = "../../onnx-opl/" , version = "=0.20.19-pre" } tract-onnx = { path = "../../onnx/" , version = "=0.20.19-pre" } +tract-extra = { path = "../../extra/" , version = "=0.20.19-pre" } tract-pulse = { path = "../../pulse/" , version = "=0.20.19-pre" } tract-libcli = { path = "../../libcli" , version = "=0.20.19-pre" } serde_json.workspace = true diff --git a/api/rs/src/lib.rs b/api/rs/src/lib.rs index ff68712c9e..87ea00c116 100644 --- a/api/rs/src/lib.rs +++ b/api/rs/src/lib.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use anyhow::{Context, Result}; use ndarray::{Data, Dimension, RawData}; +use tract_extra::WithTractExtra; use tract_libcli::annotations::Annotations; use tract_libcli::profile::BenchLimits; use tract_nnef::internal::parse_tdim; @@ -46,6 +47,11 @@ impl NnefInterface for Nnef { Ok(()) } + fn enable_tract_extra(&mut self) -> Result<()> { + self.0.enable_tract_extra(); + Ok(()) + } + fn enable_onnx(&mut self) -> Result<()> { self.0.enable_onnx(); Ok(()) diff --git a/api/src/lib.rs b/api/src/lib.rs index 5e71771411..55dd5ec56b 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -19,6 +19,9 @@ pub trait NnefInterface: Sized { /// Allow the framework to use tract_core extensions instead of a stricter NNEF definition. fn enable_tract_core(&mut self) -> Result<()>; + /// Allow the framework to use tract_extra extensions. + fn enable_tract_extra(&mut self) -> Result<()>; + /// Allow the framework to use tract_onnx extensions to support operators in ONNX that are /// absent from NNEF. fn enable_onnx(&mut self) -> Result<()>; @@ -38,6 +41,12 @@ pub trait NnefInterface: Sized { Ok(self) } + /// Convenience function, similar with enable_tract_core but allowing method chaining. + fn with_tract_extra(mut self) -> Result { + self.enable_tract_extra()?; + Ok(self) + } + /// Convenience function, similar with enable_onnx but allowing method chaining. fn with_onnx(mut self) -> Result { self.enable_onnx()?; diff --git a/api/tract.h b/api/tract.h index b3c9254ff6..1471d65425 100644 --- a/api/tract.h +++ b/api/tract.h @@ -83,7 +83,7 @@ const char *tract_version(void); void tract_free_cstring(char *ptr); /** - * Creates an instance of an NNEF framework and parser that can be used to load models. + * Creates an instance of an NNEF framework and parser that can be used to load and dump NNEF models. * * The returned object should be destroyed with `tract_nnef_destroy` once the model * has been loaded. @@ -92,6 +92,8 @@ enum TRACT_RESULT tract_nnef_create(struct TractNnef **nnef); enum TRACT_RESULT tract_nnef_enable_tract_core(struct TractNnef *nnef); +enum TRACT_RESULT tract_nnef_enable_tract_extra(struct TractNnef *nnef); + enum TRACT_RESULT tract_nnef_enable_onnx(struct TractNnef *nnef); enum TRACT_RESULT tract_nnef_enable_pulse(struct TractNnef *nnef); diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 77c9b35653..cb58dd5940 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -38,6 +38,7 @@ tract-core = { version = "=0.20.19-pre", path = "../core" } tract-hir = { version = "=0.20.19-pre", path = "../hir" } tract-nnef = { version = "=0.20.19-pre", path = "../nnef" } tract-libcli = { version = "=0.20.19-pre", path = "../libcli" } +tract-extra = { optional = true, version = "=0.20.19-pre", path = "../extra" } tract-pulse-opl = { optional = true, version = "=0.20.19-pre", path = "../pulse-opl" } tract-pulse = { optional = true, version = "=0.20.19-pre", path = "../pulse" } tract-onnx = { optional = true, version = "=0.20.19-pre", path = "../onnx" } @@ -45,8 +46,9 @@ tract-tensorflow = { optional = true, version = "=0.20.19-pre", path = "../tenso tract-tflite = { optional = true, version = "=0.20.19-pre", path = "../tflite" } [features] -default = ["onnx", "tf", "pulse", "pulse-opl", "tflite"] +default = ["onnx", "tf", "pulse", "pulse-opl", "tflite", "extra"] onnx = [ "tract-onnx", "tract-libcli/hir", "tract-libcli/onnx" ] +extra = [ "tract-extra" ] pulse-opl = [ "tract-pulse-opl" ] pulse = [ "tract-pulse", "tract-pulse-opl" ] tf = [ "tract-tensorflow", "tract-libcli/hir" ] diff --git a/cli/src/main.rs b/cli/src/main.rs index a8cc6eab82..507e57d6d7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -128,6 +128,7 @@ fn main() -> tract_core::anyhow::Result<()> { .arg(arg!(--"nnef-tract-core" "Allow usage of tract-core extension in NNEF dump and load")) .arg(arg!(--"nnef-tract-onnx" "Allow usage of tract-onnx extension in NNEF dump and load")) .arg(arg!(--"nnef-tract-pulse" "Allow usage of tract-pulse extension in NNEF dump and load")) + .arg(arg!(--"nnef-tract-extra" "Allow usage of tract-extra extension in NNEF dump and load")) .arg(arg!(--"nnef-extended-identifier" "Allow usage of the i\"...\" syntax to escape identifier names")) .arg(arg!(-O --optimize "Optimize before running")) @@ -609,6 +610,17 @@ fn nnef(matches: &clap::ArgMatches) -> tract_nnef::internal::Nnef { panic!("tract is build without pulse-opl support") } } + if matches.is_present("nnef-tract-extra") { + #[cfg(feature = "extra")] + { + use tract_extra::WithTractExtra; + fw = fw.with_tract_extra(); + } + #[cfg(not(feature = "extra"))] + { + panic!("tract is build without tract-extra support") + } + } if matches.is_present("nnef-tract-core") { fw = fw.with_tract_core(); } diff --git a/core/src/model/fact.rs b/core/src/model/fact.rs index aa88506d04..c36176cb30 100644 --- a/core/src/model/fact.rs +++ b/core/src/model/fact.rs @@ -105,7 +105,9 @@ impl ShapeFact { self.dims.remove(axis); if let Some(concrete) = &mut self.concrete { concrete.remove(axis); - } + } else { + self.compute_concrete(); + }; Ok(()) } @@ -124,6 +126,14 @@ impl ShapeFact { let void: &[usize] = &[]; Self::from(void) } + + pub fn consistent(&self) -> TractResult<()> { + ensure!( + self.concrete + == self.dims.iter().map(|d| d.to_usize()).collect::>>().ok() + ); + Ok(()) + } } impl std::ops::Deref for ShapeFact { @@ -241,6 +251,7 @@ impl TypedFact { } pub fn consistent(&self) -> TractResult<()> { + self.shape.consistent()?; if let Some(k) = &self.konst { if !self.matches(k.as_ref(), None)? { bail!("fact says {}, constant is {:?}", self.format_dt_shape_nocheck(), k); diff --git a/extra/Cargo.toml b/extra/Cargo.toml new file mode 100644 index 0000000000..81a5703f02 --- /dev/null +++ b/extra/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tract-extra" +version = "0.20.19-pre" +license = "MIT OR Apache-2.0" +authors = ["Mathieu Poumeyrol "] +description = "Tiny, no-nonsense, self contained, TensorFlow and ONNX inference" +repository = "https://github.com/snipsco/tract" +keywords = [ "TensorFlow", "NeuralNetworks" ] +categories = [ "science" ] +autobenches = false +edition = "2021" +rust-version = "1.65" + +[badges] +maintenance = { status = "actively-developed" } + +[dependencies] +tract-nnef = { version = "=0.20.19-pre", path = "../nnef" } +tract-pulse = { version = "=0.20.19-pre", path = "../pulse" } + +[dev-dependencies] +criterion.workspace = true +env_logger.workspace = true +lazy_static.workspace = true +proptest.workspace = true +approx.workspace = true diff --git a/extra/src/exp_unit_norm.rs b/extra/src/exp_unit_norm.rs new file mode 100644 index 0000000000..981eb75261 --- /dev/null +++ b/extra/src/exp_unit_norm.rs @@ -0,0 +1,159 @@ +use tract_nnef::internal::*; +use tract_nnef::tract_core::trivial_op_state_freeeze; +use tract_pulse::model::PulsedModel; +use tract_pulse::ops::OpPulsifier; +use tract_pulse::PulsedOp; +use tract_pulse::{internal::*, pulsed_op_to_typed_op}; + +pub fn register(registry: &mut Registry) { + registry.register_primitive( + "tract_extra_exp_unit_norm", + &[ + TypeName::Scalar.tensor().named("input"), + TypeName::Scalar.tensor().named("state"), + TypeName::Integer.named("axis"), + TypeName::Integer.named("skip").default(0), + TypeName::Logical.named("stateless").default(false), + TypeName::Scalar.named("alpha"), + TypeName::Scalar.named("epsilon").default(1e-14f32), + ], + &[("output", TypeName::Scalar.tensor())], + de_eun, + ); + + OpPulsifier::register::(pulsify).unwrap(); +} + +fn de_eun(builder: &mut ModelBuilder, invocation: &ResolvedInvocation) -> TractResult { + let wire = invocation.named_arg_as(builder, "input")?; + let state = invocation.named_arg_as(builder, "state")?; + let axis = invocation.named_arg_as::(builder, "axis")? as usize; + let alpha = invocation.named_arg_as(builder, "alpha")?; + let epsilon = invocation.named_arg_as(builder, "epsilon")?; + let stateless = invocation.named_arg_as::(builder, "stateless")?; + let skip = invocation.named_arg_as::(builder, "skip")? as usize; + let op = ExpUnitNorm { alpha, axis, epsilon, stateless, skip }; + builder.wire(op, &[wire, state]) +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ExpUnitNorm { + pub alpha: f32, + pub epsilon: f32, + pub axis: usize, + pub skip: usize, + pub stateless: bool, +} + +#[derive(Clone, Debug, PartialEq, Default)] +pub struct ExpUnitNormState { + hidden: Option, + index: usize, +} +trivial_op_state_freeeze!(ExpUnitNormState); + +impl Op for ExpUnitNorm { + fn name(&self) -> Cow { + "ExpUnitNorm".into() + } + + op_as_typed_op!(); +} + +impl EvalOp for ExpUnitNorm { + fn is_stateless(&self) -> bool { + self.stateless + } + + fn state( + &self, + _session: &mut SessionState, + _node_id: usize, + ) -> TractResult>> { + Ok(Some(Box::::default())) + } + + fn eval(&self, inputs: TVec) -> TractResult> { + ExpUnitNormState::default().eval(self, inputs) + } +} + +impl ExpUnitNormState { + fn eval(&mut self, op: &ExpUnitNorm, inputs: TVec) -> TractResult> { + let (input, state0) = args_2!(inputs); + let mut input = input.into_tensor(); + let mut view = input.to_array_view_mut::()?; + if self.hidden.is_none() || op.stateless { + self.hidden = Some(state0.into_tensor()); + } + let mut state = self.hidden.as_mut().unwrap().to_array_view_mut::()?; + for mut time_slice in view.axis_iter_mut(tract_ndarray::Axis(op.axis)) { + if self.index >= op.skip { + state.zip_mut_with(&time_slice, |s: &mut f32, x: &f32| { + *s = x.max(op.epsilon) * (1f32 - op.alpha) + *s * op.alpha; + }); + } + time_slice.zip_mut_with(&state, |x, s| *x /= s.sqrt()); + self.index += 1; + } + Ok(tvec!(input.into_tvalue())) + } +} + +impl OpState for ExpUnitNormState { + fn eval( + &mut self, + _session: &mut SessionState, + op: &dyn Op, + inputs: TVec, + ) -> TractResult> { + let op = op.downcast_ref::().context("Wrong op")?; + Self::eval(self, op, inputs) + } +} + +impl TypedOp for ExpUnitNorm { + fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult> { + let mut state_shape = inputs[0].shape.clone(); + let _ = state_shape.remove_axis(self.axis); + ensure!(inputs[1].without_value() == inputs[0].datum_type.fact(state_shape)); + Ok(tvec!(inputs[0].without_value())) + } + + as_op!(); +} + +impl PulsedOp for ExpUnitNorm { + fn pulsed_output_facts(&self, inputs: &[&PulsedFact]) -> TractResult> { + Ok(tvec!(inputs[0].clone())) + } + + as_op!(); + pulsed_op_to_typed_op!(); +} + +fn pulsify( + _source: &TypedModel, + node: &TypedNode, + target: &mut PulsedModel, + mapping: &HashMap, + symbol: &Symbol, + _pulse: &TDim, +) -> TractResult>> { + let op = node.op_as::().unwrap(); + let (input, state0) = (mapping[&node.inputs[0]], mapping[&node.inputs[1]]); + let input_fact = target.outlet_fact(input)?; + let pulsing_input_axis = input_fact + .to_streaming_fact() + .shape + .iter() + .position(|dim| dim.symbols().contains(symbol)) + .context("No pulsing axis found")?; + if pulsing_input_axis == op.axis { + let pulsed_op = + ExpUnitNorm { skip: input_fact.stream.as_ref().unwrap().delay, ..op.clone() }; + target.wire_node(&node.name, pulsed_op, &[input, state0]).map(Some) + } else { + target.wire_node(&node.name, op.clone(), &[input, state0]).map(Some) + } +} diff --git a/extra/src/lib.rs b/extra/src/lib.rs new file mode 100644 index 0000000000..dbe46874b3 --- /dev/null +++ b/extra/src/lib.rs @@ -0,0 +1,30 @@ +use tract_nnef::internal::*; + +mod exp_unit_norm; + +pub trait WithTractExtra { + fn enable_tract_extra(&mut self); + fn with_tract_extra(self) -> Self; +} + +impl WithTractExtra for tract_nnef::framework::Nnef { + fn enable_tract_extra(&mut self) { + self.enable_tract_core(); + self.registries.push(tract_extra_registry()); + } + + fn with_tract_extra(mut self) -> Self { + self.enable_tract_extra(); + self + } +} + +pub fn tract_extra_registry() -> Registry { + let mut reg = Registry::new("tract_extra"); + exp_unit_norm::register(&mut reg); + reg +} + +pub fn register_pulsifiers() { + let _ = tract_extra_registry(); +} diff --git a/post-release.sh b/post-release.sh index 0639a2dc55..5aa9cbbc53 100755 --- a/post-release.sh +++ b/post-release.sh @@ -1,7 +1,7 @@ #!/bin/sh VERSION=$1 -ALL_CRATES_PATH="data linalg core nnef nnef/nnef-resources pulse-opl pulse hir tflite tensorflow onnx-opl onnx libcli api api/rs api/ffi api/proxy/sys api/proxy cli" +ALL_CRATES_PATH="data linalg core nnef nnef/nnef-resources pulse-opl pulse extra hir tflite tensorflow onnx-opl onnx libcli api api/rs api/ffi api/proxy/sys api/proxy cli" if [ `uname` = "Darwin" ] then diff --git a/pulse-opl/src/delay.rs b/pulse-opl/src/delay.rs index e762953453..2bc835050b 100644 --- a/pulse-opl/src/delay.rs +++ b/pulse-opl/src/delay.rs @@ -110,8 +110,6 @@ pub struct Delay { pub overlap: usize, } - - impl Delay { pub fn new_typed(input_fact: &TypedFact, axis: usize, delay: usize, overlap: usize) -> Delay { let mut buffer_shape: TVec = input_fact.shape.to_tvec(); diff --git a/pulse/src/model.rs b/pulse/src/model.rs index 72a9ecd280..0b48b4baf9 100644 --- a/pulse/src/model.rs +++ b/pulse/src/model.rs @@ -1,3 +1,5 @@ +use std::sync::Mutex; + use crate::fact::StreamInfo; use crate::{internal::*, ops::sync_inputs}; use tract_core::model::translator::Translate; @@ -107,7 +109,7 @@ impl SpecialOps> for PulsedModel { } } -struct Pulsifier(Symbol, TDim, HashMap); +struct Pulsifier(Symbol, TDim, Arc>>); impl std::fmt::Debug for Pulsifier { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -137,12 +139,10 @@ impl .unwrap()); } - if let Some(pulsifier) = self.2.get(&node.op.type_id()) { - if let Some(pulsified) = - (pulsifier.func)(source, node, target, mapping, &self.0, &self.1)? - { - return Ok(pulsified); - } + if let Some(pulsified) = + OpPulsifier::pulsify(source, node, target, mapping, &self.0, &self.1)? + { + return Ok(pulsified); } let pulse_facts: TVec = diff --git a/pulse/src/ops/mod.rs b/pulse/src/ops/mod.rs index 8806fa8b3f..e80223c936 100644 --- a/pulse/src/ops/mod.rs +++ b/pulse/src/ops/mod.rs @@ -1,4 +1,8 @@ +use std::any::Any; +use std::sync::Mutex; + use crate::internal::*; +use lazy_static::lazy_static; use tract_pulse_opl::ops::Delay; pub mod array; @@ -60,22 +64,53 @@ pub struct OpPulsifier { } impl OpPulsifier { - pub fn inventory() -> HashMap { - let mut inventory = HashMap::default(); - register_all(&mut inventory); - inventory + pub fn inventory() -> Arc>> { + lazy_static! { + static ref INVENTORY: Arc>> = { + let mut it = HashMap::default(); + register_all(&mut it); + Arc::new(Mutex::new(it)) + }; + }; + (*INVENTORY).clone() + } + + pub fn register(func: PulsifierFn) -> TractResult<()> { + let inv = Self::inventory(); + let mut inv = inv.lock().map_err(|e| anyhow!("Fail to lock inventory {e}"))?; + inv.insert( + std::any::TypeId::of::(), + OpPulsifier { + type_id: std::any::TypeId::of::(), + name: std::any::type_name::(), + func, + }, + ); + Ok(()) + } + + pub fn pulsify( + source: &TypedModel, + node: &TypedNode, + target: &mut PulsedModel, + mapping: &HashMap, + symbol: &Symbol, + pulse: &TDim, + ) -> TractResult>> { + let inv = Self::inventory(); + let inv = inv.lock().map_err(|e| anyhow!("Fail to lock inventory {e}"))?; + if let Some(pulsifier) = inv.get(&(*node.op).type_id()) { + if let Some(pulsified) = (pulsifier.func)(source, node, target, mapping, symbol, pulse)? + { + return Ok(Some(pulsified)); + } + } + Ok(None) } } pub trait PulsedOp: - Op - + fmt::Debug - + tract_core::dyn_clone::DynClone - + Send - + Sync - + 'static - + Downcast - + EvalOp + Op + fmt::Debug + tract_core::dyn_clone::DynClone + Send + Sync + 'static + Downcast + EvalOp { /// Reinterpret the PulsedOp as an Op. fn as_op(&self) -> &dyn Op; diff --git a/release.sh b/release.sh index af2f01f0e7..91f30487db 100755 --- a/release.sh +++ b/release.sh @@ -9,7 +9,7 @@ cargo install tomato-toml CRATE_PATH=$1 VERSION=$2 -ALL_CRATES_PATH="data linalg core nnef nnef/nnef-resources pulse-opl pulse hir tflite tensorflow onnx-opl onnx libcli api api/rs api/ffi api/proxy/sys api/proxy cli" +ALL_CRATES_PATH="data linalg core nnef nnef/nnef-resources pulse-opl pulse extra hir tflite tensorflow onnx-opl onnx libcli api api/rs api/ffi api/proxy/sys api/proxy cli" if [ -z "$VERSION" ] then