onnxruntime-ep-mlx 0.29.4__tar.gz → 0.29.6.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/PKG-INFO +1 -1
  2. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/pyproject.toml +1 -1
  3. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/Cargo.toml +1 -1
  4. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/compiled.rs +192 -19
  5. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/engine.rs +31 -4
  6. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ep.rs +125 -26
  7. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/attention.rs +137 -21
  8. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/controlflow.rs +92 -5
  9. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/conv.rs +59 -18
  10. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/elementwise.rs +63 -35
  11. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/image.rs +1 -1
  12. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/math.rs +49 -49
  13. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/matmul.rs +2 -2
  14. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/misc.rs +44 -14
  15. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/mod.rs +1 -0
  16. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/norm.rs +44 -13
  17. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/onnx_ml_linear.rs +12 -2
  18. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/onnx_ml_preprocess.rs +13 -7
  19. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/quant.rs +225 -15
  20. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/random.rs +29 -9
  21. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/recurrent.rs +15 -12
  22. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/reduction.rs +106 -19
  23. onnxruntime_ep_mlx-0.29.6.dev1/rust/src/ops/selective_scan.rs +514 -0
  24. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/shape.rs +78 -32
  25. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/signal.rs +40 -8
  26. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/ssm.rs +47 -35
  27. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/stragglers.rs +2 -2
  28. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/ops/vision.rs +59 -9
  29. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/registry.rs +260 -9
  30. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/trace.rs +27 -13
  31. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/.gitignore +0 -0
  32. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/README.md +0 -0
  33. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/hatch_build.py +0 -0
  34. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/build.rs +0 -0
  35. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/factory.rs +0 -0
  36. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/lib.rs +0 -0
  37. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/logging.rs +0 -0
  38. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/mlx.rs +0 -0
  39. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/src/sys.rs +0 -0
  40. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/wrapper_mlx.h +0 -0
  41. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/rust/wrapper_ort.h +0 -0
  42. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/src/onnxruntime_ep_mlx/__init__.py +0 -0
  43. {onnxruntime_ep_mlx-0.29.4 → onnxruntime_ep_mlx-0.29.6.dev1}/src/onnxruntime_ep_mlx/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: onnxruntime-ep-mlx
3
- Version: 0.29.4
3
+ Version: 0.29.6.dev1
4
4
  Summary: MLX-native ONNX Runtime execution provider (plugin EP) for Apple Silicon
5
5
  Project-URL: Homepage, https://github.com/justinchuby/onnxruntime-mlx
6
6
  Project-URL: Repository, https://github.com/justinchuby/onnxruntime-mlx
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "onnxruntime-ep-mlx"
7
- version = "0.29.4"
7
+ version = "0.29.6.dev1"
8
8
  description = "MLX-native ONNX Runtime execution provider (plugin EP) for Apple Silicon"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "onnxruntime-ep-mlx"
3
- version = "0.29.4"
3
+ version = "0.29.6-dev.1"
4
4
  edition = "2024"
5
5
  description = "MLX-native ONNX Runtime execution provider (plugin EP) for Apple Silicon — binds mlx-c directly, no mlx-rs."
6
6
  license = "MIT"
@@ -37,6 +37,7 @@ use crate::engine::{
37
37
  dim_i32, is_separate_qkv_attention, mlx_dtype_from_onnx, read_ctx_input_raw, rope_row_key,
38
38
  };
39
39
  use crate::mlx::{self, Array, Closure, VectorArray};
40
+ use crate::registry::CompileShapeSafety;
40
41
  use crate::sys::mlx as mlxsys;
41
42
  use crate::sys::ort;
42
43
 
@@ -50,6 +51,29 @@ pub fn compile_enabled(has_control_flow: bool) -> bool {
50
51
  !has_control_flow && !killed
51
52
  }
52
53
 
54
+ fn node_compile_shape_safety(node: &NodeDesc) -> CompileShapeSafety {
55
+ node.compile_shape_safety
56
+ }
57
+
58
+ fn compile_shape_mode_supported(nodes: &[NodeDesc], shapeless: bool) -> bool {
59
+ nodes.iter().all(|node| {
60
+ (if shapeless {
61
+ node_compile_shape_safety(node).allows_shapeless()
62
+ } else {
63
+ node_compile_shape_safety(node).allows_shape_keyed()
64
+ }) && node
65
+ .subgraphs
66
+ .iter()
67
+ .all(|subgraph| compile_shape_mode_supported(&subgraph.nodes, shapeless))
68
+ })
69
+ }
70
+
71
+ /// Decode uses `mlx_compile(..., shapeless=true)`, so plans containing a primitive without MLX
72
+ /// output-shape inference must use their shape-keyed/eager route instead.
73
+ pub fn decode_enabled(has_control_flow: bool, nodes: &[NodeDesc]) -> bool {
74
+ compile_enabled(has_control_flow) && compile_shape_mode_supported(nodes, true)
75
+ }
76
+
53
77
  /// Op types whose subgraphs keep their existing (eager / compiled-decode) routes because they are
54
78
  /// incompatible with a single static-shape fused trace:
55
79
  /// * attention ops carry KV-cache aliasing + delta copy-out semantics not modelled by the general
@@ -82,14 +106,17 @@ fn is_general_compile_unsafe(n: &NodeDesc) -> bool {
82
106
  /// containing an op that is unsafe to trace (see [`is_general_compile_unsafe`]). An
83
107
  /// extra kill-switch `ONNXRUNTIME_EP_MLX_NO_GENERAL_COMPILE` forces eager for A/B numerical validation without
84
108
  /// touching the decode path.
85
- /// A node that would read a DATA-DEPENDENT runtime shape/scalar (a reshape/expand/slice/range target
86
- /// that is neither constant nor shape-const). Reading it needs a mid-graph eval that is illegal in a
87
- /// shapeless `mlx_compile` trace, so such a subgraph must run eager. Shape-const runtime targets
88
- /// (derived from `Shape`/`Size`) are compile-safe and do NOT trip this.
89
- fn reads_data_dependent_shape(n: &NodeDesc) -> bool {
109
+ /// A node that would read a DATA-DEPENDENT runtime shape/scalar while translating the graph.
110
+ ///
111
+ /// Shape-keyed MLX caches observe tensor shapes/dtypes, not tensor values. A direct runtime
112
+ /// Reshape/Expand/Slice/Range parameter or CumSum axis would otherwise be read from the live
113
+ /// `OrtKernelContext` during the first trace and silently baked into every later call with the same
114
+ /// input shapes. Initializers and shape-const intermediates (derived from `Shape`/`Size`) are safe:
115
+ /// their value is fixed, or changes exactly when the shape key changes.
116
+ fn reads_data_dependent_parameter(n: &NodeDesc) -> bool {
90
117
  if n.subgraphs
91
118
  .iter()
92
- .any(|sg| sg.nodes.iter().any(reads_data_dependent_shape))
119
+ .any(|sg| sg.nodes.iter().any(reads_data_dependent_parameter))
93
120
  {
94
121
  return true;
95
122
  }
@@ -97,12 +124,13 @@ fn reads_data_dependent_shape(n: &NodeDesc) -> bool {
97
124
  "Reshape" | "Expand" => &[1],
98
125
  "Slice" => &[1, 2, 3, 4],
99
126
  "Range" => &[0, 1, 2],
127
+ "CumSum" => &[1],
100
128
  _ => return false,
101
129
  };
102
130
  idx.iter().any(|&i| {
103
- n.inputs
104
- .get(i)
105
- .is_some_and(|tr| matches!(tr.source, Src::Intermediate) && !tr.shape_const)
131
+ n.inputs.get(i).is_some_and(|tr| {
132
+ !matches!(tr.source, Src::Initializer | Src::Absent) && !tr.constant && !tr.shape_const
133
+ })
106
134
  })
107
135
  }
108
136
 
@@ -118,7 +146,8 @@ pub fn general_enabled(has_control_flow: bool, nodes: &[NodeDesc]) -> bool {
118
146
  }
119
147
  !nodes
120
148
  .iter()
121
- .any(|n| is_general_compile_unsafe(n) || reads_data_dependent_shape(n))
149
+ .any(|n| is_general_compile_unsafe(n) || reads_data_dependent_parameter(n))
150
+ && compile_shape_mode_supported(nodes, false)
122
151
  }
123
152
 
124
153
  fn host_scalar_i64(
@@ -262,10 +291,9 @@ pub fn prefill_enabled(has_control_flow: bool, nodes: &[NodeDesc]) -> bool {
262
291
  {
263
292
  return false;
264
293
  }
265
- nodes.iter().any(|n| {
266
- n.op_type == "GroupQueryAttention"
267
- && n.ints.get("sliding_window_cache").copied().unwrap_or(0) == 0
268
- })
294
+ compile_shape_mode_supported(nodes, false)
295
+ && !nodes.iter().any(reads_data_dependent_parameter)
296
+ && nodes.iter().any(|n| n.op_type == "GroupQueryAttention")
269
297
  }
270
298
 
271
299
  /// Query sequence length S = trailing dim of the `input_ids` dynamic ctx input (decode => 1, prefill
@@ -837,13 +865,13 @@ fn build_closure(
837
865
  kctx: *mut ort::OrtKernelContext,
838
866
  stream: mlxsys::mlx_stream,
839
867
  ) -> bool {
840
- if unsafe { &*plan_ptr }.nodes.iter().any(|n| {
841
- n.op_type == "GroupQueryAttention"
842
- && n.ints.get("sliding_window_cache").copied().unwrap_or(0) != 0
843
- }) {
868
+ let cfg = slot.get(unsafe { &*plan_ptr }).config;
869
+ if !compile_shape_mode_supported(
870
+ &unsafe { &*plan_ptr }.nodes,
871
+ cfg.shape_mode == ShapeMode::Shapeless,
872
+ ) {
844
873
  return true;
845
874
  }
846
- let cfg = slot.get(unsafe { &*plan_ptr }).config;
847
875
 
848
876
  let mut dyn_inputs: Vec<DynInput> = Vec::new();
849
877
  let mut ext_outputs: Vec<OutRef> = Vec::new();
@@ -1201,3 +1229,148 @@ fn trace_body(
1201
1229
  }
1202
1230
  Ok(())
1203
1231
  }
1232
+
1233
+ #[cfg(test)]
1234
+ mod shape_inference_tests {
1235
+ use super::*;
1236
+ use crate::engine::TensorRef;
1237
+
1238
+ fn input(name: &str, source: Src) -> TensorRef {
1239
+ TensorRef {
1240
+ name: name.to_string(),
1241
+ source,
1242
+ ctx_index: 0,
1243
+ constant: false,
1244
+ shape_const: false,
1245
+ init: None,
1246
+ }
1247
+ }
1248
+
1249
+ fn cumsum(axis_source: Src) -> NodeDesc {
1250
+ let mut node = NodeDesc::new(
1251
+ "CumSum".to_string(),
1252
+ String::new(),
1253
+ 14,
1254
+ CompileShapeSafety::ShapeKeyedOnly {
1255
+ reason: crate::registry::MLX_SCAN_SHAPE_REASON,
1256
+ },
1257
+ );
1258
+ node.inputs = vec![input("data", Src::CtxInput), input("axis", axis_source)];
1259
+ node
1260
+ }
1261
+
1262
+ #[test]
1263
+ fn missing_output_shape_emitters_require_shape_specialized_compile() {
1264
+ for node in [
1265
+ NodeDesc::new(
1266
+ "CumSum".to_string(),
1267
+ String::new(),
1268
+ 14,
1269
+ CompileShapeSafety::ShapeKeyedOnly {
1270
+ reason: crate::registry::MLX_SCAN_SHAPE_REASON,
1271
+ },
1272
+ ),
1273
+ NodeDesc::new(
1274
+ "Slice".to_string(),
1275
+ String::new(),
1276
+ 14,
1277
+ CompileShapeSafety::ShapeKeyedOnly {
1278
+ reason: crate::registry::MLX_SLICE_SHAPE_REASON,
1279
+ },
1280
+ ),
1281
+ NodeDesc::new(
1282
+ "LinearAttention".to_string(),
1283
+ "com.microsoft".to_string(),
1284
+ 1,
1285
+ CompileShapeSafety::ShapeKeyedOnly {
1286
+ reason: "emits MLX Slice, Pad, and Scan",
1287
+ },
1288
+ ),
1289
+ NodeDesc::new(
1290
+ "QMoE".to_string(),
1291
+ "com.microsoft".to_string(),
1292
+ 1,
1293
+ CompileShapeSafety::ShapeKeyedOnly {
1294
+ reason: crate::registry::MLX_SLICE_SHAPE_REASON,
1295
+ },
1296
+ ),
1297
+ ] {
1298
+ assert!(!node_compile_shape_safety(&node).allows_shapeless());
1299
+ }
1300
+ assert!(
1301
+ node_compile_shape_safety(&NodeDesc::new(
1302
+ "Add".to_string(),
1303
+ String::new(),
1304
+ 14,
1305
+ CompileShapeSafety::Shapeless,
1306
+ ))
1307
+ .allows_shapeless()
1308
+ );
1309
+ assert!(compile_shape_mode_supported(
1310
+ &[NodeDesc::new(
1311
+ "CumSum".to_string(),
1312
+ String::new(),
1313
+ 14,
1314
+ CompileShapeSafety::ShapeKeyedOnly {
1315
+ reason: crate::registry::MLX_SCAN_SHAPE_REASON,
1316
+ },
1317
+ )],
1318
+ false
1319
+ ));
1320
+ assert!(!compile_shape_mode_supported(
1321
+ &[NodeDesc::new(
1322
+ "CumSum".to_string(),
1323
+ String::new(),
1324
+ 14,
1325
+ CompileShapeSafety::ShapeKeyedOnly {
1326
+ reason: crate::registry::MLX_SCAN_SHAPE_REASON,
1327
+ },
1328
+ )],
1329
+ true
1330
+ ));
1331
+ let eager_only_gqa = NodeDesc::new(
1332
+ "GroupQueryAttention".to_string(),
1333
+ "com.microsoft".to_string(),
1334
+ 1,
1335
+ CompileShapeSafety::EagerOnly {
1336
+ reason: "attention-bias form",
1337
+ },
1338
+ );
1339
+ assert!(!compile_shape_mode_supported(
1340
+ std::slice::from_ref(&eager_only_gqa),
1341
+ false
1342
+ ));
1343
+ assert!(!compile_shape_mode_supported(&[eager_only_gqa], true));
1344
+ }
1345
+
1346
+ #[test]
1347
+ fn runtime_cumsum_axis_is_not_a_shape_key() {
1348
+ assert!(
1349
+ reads_data_dependent_parameter(&cumsum(Src::CtxInput)),
1350
+ "a runtime axis value must not be baked into a shape-keyed closure"
1351
+ );
1352
+ assert!(
1353
+ !reads_data_dependent_parameter(&cumsum(Src::Initializer)),
1354
+ "an initializer axis is compile-time data"
1355
+ );
1356
+ }
1357
+
1358
+ #[test]
1359
+ fn direct_runtime_shape_parameter_is_data_dependent() {
1360
+ let mut reshape = NodeDesc::new(
1361
+ "Reshape".to_string(),
1362
+ String::new(),
1363
+ 14,
1364
+ CompileShapeSafety::Shapeless,
1365
+ );
1366
+ reshape.inputs = vec![input("data", Src::CtxInput), input("shape", Src::CtxInput)];
1367
+ assert!(reads_data_dependent_parameter(&reshape));
1368
+
1369
+ reshape.inputs[1].source = Src::Intermediate;
1370
+ reshape.inputs[1].shape_const = true;
1371
+ assert!(
1372
+ !reads_data_dependent_parameter(&reshape),
1373
+ "Shape/Size-derived parameters change with the shape-keyed cache key"
1374
+ );
1375
+ }
1376
+ }
@@ -27,8 +27,7 @@ pub struct InitData {
27
27
  pub data: *const c_void,
28
28
  pub shape: Vec<i64>,
29
29
  pub dtype: ort::ONNXTensorElementDataType,
30
- /// Element count of the initializer (kept for weight-repack handlers in the next wave).
31
- #[allow(dead_code)]
30
+ /// ORT-reported element count, used to bound typed initializer reads safely.
32
31
  pub count: usize,
33
32
  /// Owned copy of the bytes (control-flow body initializers), keeping `data` valid.
34
33
  #[allow(dead_code)]
@@ -137,6 +136,7 @@ pub struct NodeDesc {
137
136
  pub op_type: String,
138
137
  pub domain: String,
139
138
  pub since_version: i32,
139
+ pub compile_shape_safety: crate::registry::CompileShapeSafety,
140
140
  pub ints: HashMap<String, i64>,
141
141
  pub floats: HashMap<String, f32>,
142
142
  pub int_arrays: HashMap<String, Vec<i64>>,
@@ -152,13 +152,19 @@ pub struct NodeDesc {
152
152
  }
153
153
 
154
154
  impl NodeDesc {
155
- pub fn new(op_type: String, domain: String, since_version: i32) -> Self {
155
+ pub fn new(
156
+ op_type: String,
157
+ domain: String,
158
+ since_version: i32,
159
+ compile_shape_safety: crate::registry::CompileShapeSafety,
160
+ ) -> Self {
156
161
  NodeDesc {
157
162
  node_id: 0,
158
163
  name: String::new(),
159
164
  op_type,
160
165
  domain,
161
166
  since_version,
167
+ compile_shape_safety,
162
168
  ints: HashMap::new(),
163
169
  floats: HashMap::new(),
164
170
  int_arrays: HashMap::new(),
@@ -1217,6 +1223,22 @@ impl<'a> TranslationContext<'a> {
1217
1223
  Ok(self.keep(res))
1218
1224
  }
1219
1225
 
1226
+ /// Shape-preserving cumulative sum.
1227
+ ///
1228
+ /// MLX 0.32's backing `Scan` primitive lacks `Primitive::output_shapes`, so every registered
1229
+ /// handler that can reach this helper must declare `CompileShapeSafety::ShapeKeyedOnly`.
1230
+ pub fn cumsum(
1231
+ &mut self,
1232
+ a: mlxsys::mlx_array,
1233
+ axis: i32,
1234
+ reverse: bool,
1235
+ inclusive: bool,
1236
+ ) -> Result<mlxsys::mlx_array, MlxError> {
1237
+ self.emit(|res, stream| unsafe {
1238
+ mlxsys::mlx_cumsum(res, a, axis, reverse, inclusive, stream)
1239
+ })
1240
+ }
1241
+
1220
1242
  // ---- array introspection (borrowed raw handles; ownership stays with the arena/cache) ---------
1221
1243
 
1222
1244
  pub fn shape_of(&self, a: mlxsys::mlx_array) -> Vec<i32> {
@@ -2191,7 +2213,12 @@ mod tests {
2191
2213
  }
2192
2214
 
2193
2215
  fn node(op_type: &str, inputs: &[&str], output_name: &str) -> NodeDesc {
2194
- let mut node = NodeDesc::new(op_type.to_string(), String::new(), 17);
2216
+ let mut node = NodeDesc::new(
2217
+ op_type.to_string(),
2218
+ String::new(),
2219
+ 17,
2220
+ crate::registry::CompileShapeSafety::Shapeless,
2221
+ );
2195
2222
  node.inputs = inputs.iter().map(|name| input(name)).collect();
2196
2223
  node.outputs = vec![output(output_name)];
2197
2224
  node