跳至主要内容

在單個樣本中傳送多個輸入

欄位
類別模型與推論
難度中級
預估閱讀時間15 minutes
標籤multi-input, samples, sync

許多實際應用程式在每次推論事件中會處理多個輸入。Neat 將其表示為一個「組合樣本」(bundle sample):一個單一的 Sample,其 fields 列表包含多個具名稱的張量有效載體,每個有效載體都可以透過一個 port_name 來存取。執行階段將具名稱的欄位組合在一起,作為一個邏輯事件,因此 leftright(或影像和中繼資料)在整個管線中保持對齊。

本章將建立一個張量輸入/張量輸出的圖,將兩個具名稱的浮點張量組合在一起,將組合推送到管線中,然後讀取具名稱的欄位。在本章結束時,您將建立一個多欄位樣本,並確認兩個欄位都已完整地通過了整個流程,且其連接埠名稱保持不變。

操作指南

設定張量輸入

此圖處理原始張量,而不是已解碼的影像,因此輸入合約宣告為張量有效載體(FP32,具有 width/height/depth),而不是像素格式。這會告訴輸入節點直接接受張量緩衝區。

C++: 設定 in.payload_type = PayloadType::Tensor

Python: 設定 inp.payload_type = pyneat.PayloadType.Tensorinp.format = pyneat.Format.FP32

tutorials/010_feed_multi_input_model/feed_multi_input_model.cpp
simaai::neat::InputOptions in;
in.payload_type = simaai::neat::PayloadType::Tensor;
in.format = "FP32";
in.width = w;
in.height = h;
in.depth = c;

建立圖和種子執行

我們使用來自第 004 章的相同最小 Input -> Output 拓撲結構,並將其 build() 到一個 Run 中。build() 需要一個具有代表性的樣本來鎖定已協商的形狀,因此我們傳遞一個單一的種子張量(所有值都為零),其形狀與實際欄位將使用的形狀相同。種子僅用於形狀協商——實際資料稍後會傳遞。

tutorials/010_feed_multi_input_model/feed_multi_input_model.cpp
// 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_tensor_sample(port_name, tensor) 獲得一個名稱,並且模型會透過連接埠來存取這些具名稱的欄位。在這裡,left 填充了 1.0,而 right 填充了 2.0,以便您可以區分它們在輸出時的內容。

C++: make_bundle_sample({...}) 將具名稱的欄位包裝到一個 Sample 中,其 kindBundle

Python: 具名稱的樣本列表會直接傳遞到 push(...);pyneat 會為您建立組合封套。

tutorials/010_feed_multi_input_model/feed_multi_input_model.cpp
// 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)),
});

推送組合並讀取它

最後,將組合推送到管線中並檢查結果。輸出本身也是一個組合 Sample,因此我們讀取 out.fields,而不是將其視為單一的張量——out.fields.size() 應該是 2,並且每個欄位都包含 port_name 和一個張量有效載體。

C++:run.run(Sample{bundle}, timeout_ms) 會傳回一個 Sample。由於邏輯結果具有多個欄位,因此傳回的 Sample 本身就是一個 Bundle,因此我們檢查 out.kind == SampleKind::Bundle 並迭代 out.fields,而不是 front()(這會表示「bundle 內的第一個欄位」)。

Python:run.push(fields),然後 run.pull(timeout_ms=...) 會傳回輸出樣本;迭代 out.fields 並讀取每個 field.port_namefield.tensor

tutorials/010_feed_multi_input_model/feed_multi_input_model.cpp
auto outs = run.run(simaai::neat::Sample{bundle}, /*timeout_ms=*/1000);

執行

Neat 安裝根目錄(包含 share/lib/ 的目錄)執行 PythonC++(預先建置) 命令;從 原始碼儲存庫根目錄 執行 從原始碼建置 命令。本章不需要模型封存檔。

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

預期的輸出(C++):

bundle_fields=2
field=left has_tensor=yes
field=right has_tensor=yes
[OK] 010_feed_multi_input_model

(Python 建置會以 port=left has_tensor=True 行的形式輸出相同的欄位計數。)若要將本章的 C++ 原始碼整合到您自己的專案中,並使用自訂的 CMakeLists.txt(不需要額外的資料夾),請參閱登陸頁面上的 如何執行教學

實務應用

如何將 bundle 模式應用於此兩個欄位的示範之外。

命名和路由

  • port_name 是接線合約:這是多輸入模型用來尋址每個欄位的方式。將名稱與模型宣告的輸入連接埠相符。
  • 輸出 bundle 會保留欄位結構,因此您可以根據名稱而不是位置將結果與輸入進行比對。

檢查輸出 bundle

  • 始終首先根據 kind 進行分支:多欄位結果是 SampleKind.Bundle,將其作為單一張量讀取將無法運作。
  • 在觸及有效載荷之前,請檢查每個欄位中是否存在張量(field.tensor is not None / field.tensor.has_value())——欄位可能攜帶中繼資料,而不是張量。

完整原始碼

顯示完整原始碼程式
tutorials/010_feed_multi_input_model/feed_multi_input_model.cpp
// 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;
}
}

來源