Feed Models That Take Multiple Inputs
| Field | Value |
|---|---|
| Difficulty | Intermediate |
| Estimated Read Time | 15 minutes |
| Labels | multi-input, samples, sync |
Many real applications carry more than one input per inference event. Neat represents this as a bundle sample: a single Sample whose fields list holds multiple named tensor payloads, each addressable by a port_name. The runtime keeps the named fields together as one logical event, so left and right (or image and metadata) stay aligned through the pipeline.
This chapter builds a tensor-in/tensor-out graph, bundles two named float tensors, pushes the bundle through, and reads the named fields back out. By the end you will have constructed a multi-field sample and confirmed both fields survived the round trip with their port names intact.
Walkthrough
Configure a tensor input
This graph consumes raw tensors, not decoded images, so the input contract is declared as a tensor payload (FP32, with width/height/depth) rather than a pixel format. That tells the input node to accept tensor buffers directly.
Set in.payload_type = PayloadType::Tensor.
simaai::neat::InputOptions in;
in.payload_type = simaai::neat::PayloadType::Tensor;
in.format = "FP32";
in.width = w;
in.height = h;
in.depth = c;
Build the graph and a seed run
We compose the same minimal Input -> Output topology from chapter 004 and build() it into a Run. build() needs a representative sample to lock in negotiated shapes, so we pass a single seed tensor (all zeros) of the same shape the real fields will use. The seed is only for shape negotiation — the real data comes next.
// Graph accepting fp32 tensors as input.
simaai::neat::Graph graph;
graph.add(simaai::neat::nodes::Input(in));
graph.add(simaai::neat::nodes::Output());
auto run = graph.build(simaai::neat::TensorList{seed});
Build the bundle
Now assemble the multi-input event. Each input gets a name via make_tensor_sample(port_name, tensor), and those named fields are what the model addresses by port. Here left is filled with 1.0 and right with 2.0 so you can tell them apart on the way out.
make_bundle_sample({...}) wraps the named fields into one Sample whose kind is Bundle.
// make_bundle_sample packs multiple named tensors into one Sample.
simaai::neat::Sample bundle = simaai::neat::make_bundle_sample({
simaai::neat::make_tensor_sample("left", make_fp32_tensor(w, h, c, 1.0f)),
simaai::neat::make_tensor_sample("right", make_fp32_tensor(w, h, c, 2.0f)),
});
Push the bundle and read it back
Finally, send the bundle through and inspect the result. The output is itself a bundle Sample, so we read out.fields rather than treating it as a single tensor — out.fields.size() should be 2, and each field carries the port_name and a tensor payload.
run.run(Sample{bundle}, timeout_ms) returns one Sample. Because the logical result has multiple fields, that returned Sample is itself a Bundle — so we check out.kind == SampleKind::Bundle and iterate out.fields, not front() (which would mean "first field inside the bundle").
auto outs = run.run(simaai::neat::Sample{bundle}, /*timeout_ms=*/1000);
Run
Run the Python and C++ (prebuilt) commands from the Neat install root (the directory that contains share/ and lib/); run the build from source commands from the repo root. This chapter needs no model archive.
C++ (prebuilt):
./lib/sima-neat/tutorials/tutorial_010_feed_multi_input_model \
--width 64 --height 48
C++ (build from source):
./build.sh --target tutorial_010_feed_multi_input_model
./build/tutorials-standalone/tutorial_010_feed_multi_input_model \
--width 64 --height 48
Expected output (C++):
bundle_fields=2
field=left has_tensor=yes
field=right has_tensor=yes
[OK] 010_feed_multi_input_model
(The Python build prints the same field count with port=left has_tensor=True lines.) To integrate this chapter's C++ source into your own project with a custom CMakeLists.txt (no extras folder required), see How to Run Tutorials on the landing page.
In Practice
How to apply the bundle pattern beyond this two-field demo.
Naming and routing
port_nameis the wiring contract: it is how a multi-input model addresses each field. Match the names to the model's declared input ports.- The output bundle preserves field structure, so you can match results back to the inputs by name rather than position.
Inspecting output bundles
- Always branch on
kindfirst: a multi-field result isSampleKind.Bundle, and reading it as a single tensor will not work. - Check tensor presence per field (
field.tensor is not None/field.tensor.has_value()) before touching the payload — a field may carry metadata rather than a tensor.
Full source
Show the complete C++ and Python programs
// Build a multi-port bundle Sample and push it through a tensor-in/tensor-out Graph.
//
// Usage:
// tutorial_010_feed_multi_input_model [--width 64] [--height 48]
#include "neat.h"
#include <cstddef>
#include <iostream>
#include <stdexcept>
#include <string>
namespace {
bool get_arg(int argc, char** argv, const std::string& key, std::string& out) {
for (int i = 1; i + 1 < argc; ++i) {
if (key == argv[i]) {
out = argv[i + 1];
return true;
}
}
return false;
}
int parse_int_arg(int argc, char** argv, const std::string& key, int def) {
std::string value;
if (!get_arg(argc, argv, key, value))
return def;
return std::stoi(value);
}
simaai::neat::Tensor make_fp32_tensor(int w, int h, int c, float fill) {
const std::size_t bytes = static_cast<std::size_t>(w) * h * c * sizeof(float);
auto storage = simaai::neat::make_cpu_owned_storage(bytes);
auto map = storage->map(simaai::neat::MapMode::Write);
auto* p = static_cast<float*>(map.data);
const std::size_t n = static_cast<std::size_t>(w) * h * c;
for (std::size_t i = 0; i < n; ++i)
p[i] = fill;
simaai::neat::Tensor t;
t.storage = storage;
t.dtype = simaai::neat::TensorDType::Float32;
t.layout = simaai::neat::TensorLayout::HWC;
t.shape = {h, w, c};
t.device = {simaai::neat::DeviceType::CPU, 0};
t.read_only = true;
return t;
}
} // namespace
int main(int argc, char** argv) {
try {
const int w = parse_int_arg(argc, argv, "--width", 64);
const int h = parse_int_arg(argc, argv, "--height", 48);
const int c = 3;
simaai::neat::InputOptions in;
in.payload_type = simaai::neat::PayloadType::Tensor;
in.format = "FP32";
in.width = w;
in.height = h;
in.depth = c;
simaai::neat::Tensor seed = make_fp32_tensor(w, h, c, 0.0f);
// CORE LOGIC
// Graph accepting fp32 tensors as input.
simaai::neat::Graph graph;
graph.add(simaai::neat::nodes::Input(in));
graph.add(simaai::neat::nodes::Output());
auto run = graph.build(simaai::neat::TensorList{seed});
// make_bundle_sample packs multiple named tensors into one Sample.
simaai::neat::Sample bundle = simaai::neat::make_bundle_sample({
simaai::neat::make_tensor_sample("left", make_fp32_tensor(w, h, c, 1.0f)),
simaai::neat::make_tensor_sample("right", make_fp32_tensor(w, h, c, 2.0f)),
});
auto outs = run.run(simaai::neat::Sample{bundle}, /*timeout_ms=*/1000);
if (outs.empty())
throw std::runtime_error("bundle output missing");
// `Run::run(Sample)` returns one Sample. When the logical result has multiple fields,
// that Sample is itself a Bundle; `front()` would mean "first field inside the bundle",
// not "first output sample".
const simaai::neat::Sample& out = outs;
if (out.kind != simaai::neat::SampleKind::Bundle)
throw std::runtime_error("expected bundle output");
if (out.fields.size() != 2U)
throw std::runtime_error("expected two bundle fields");
std::cout << "bundle_fields=" << out.fields.size() << "\n";
for (std::size_t i = 0; i < out.fields.size(); ++i) {
const auto& field = out.fields[i];
const bool has_tensor = field.tensor.has_value() || !field.tensors.empty();
const std::string label =
!field.port_name.empty()
? field.port_name
: (!field.stream_label.empty() ? field.stream_label : ("field_" + std::to_string(i)));
std::cout << " field=" << label << " has_tensor=" << (has_tensor ? "yes" : "no") << "\n";
}
std::cout << "[OK] 010_feed_multi_input_model\n";
return 0;
} catch (const std::exception& e) {
std::cerr << "[FAIL] " << e.what() << "\n";
return 1;
}
}