PCIe Co-processing
The Neat PCIe host API lets an application on a host machine send tensors or images to a connected Modalix PCIe Card and receive inference results. Use it when the host machine owns application I/O and orchestration while the card runs the compiled model and its configured preprocessing or postprocessing.
This is a separate API from the Neat Library that runs directly on a DevKit.
The public types are in the simaai::neat::pcie C++ namespace and the
pyneatpcie Python package.
Install core/pciehost on the host machine, not inside the Neat SDK
container or on the Modalix PCIe Card. Follow
Install PCIe Host before using this
page.
How co-processing works
One pcie::Model represents one compiled model running on one physical PCIe
queue:
- The constructor reads the local model archive and exposes its input and output contract.
build()uploads the archive to the card over the PCIe virtual network, starts the card-side pipeline, and waits until it is ready.run()orpush()sends input payloads over PCIe.- The card executes preprocessing, inference, and configured postprocessing.
run()orpull()returns the output tensors to the host.close()stops the card-side pipeline and releases the queue.
The model archive is transferred during build(). Inference payloads and
results use the PCIe data transport.
Configure the connection
ConnectionOptions identifies the card and the queue used by this model.
| Field | Default | Purpose |
|---|---|---|
card_host | empty | Explicit SSH/SCP address. When empty, card N uses 10.0.N.2. |
card_id | 0 | Card number passed to the host PCIe plugin. |
user | sima | User for card-side SSH and SCP. |
queue | 0 | Co-processing queue, from 0 through 3. |
max_inflight | 10 | Maximum accepted inputs waiting for results. |
Use the defaults for one card at 10.0.0.2 on queue 0. Set card_host
explicitly when the card uses a different management address.
#include <simaai/neat/pcie/Model.h>
namespace pcie = simaai::neat::pcie;
pcie::ConnectionOptions connection;
connection.card_host = "10.0.0.2";
connection.card_id = 0;
connection.queue = 0;
connection.max_inflight = 10;
Inspect and build a model
Construction is local and does not start the card. Inspect info() before
allocating input, then call build() once to start the co-processing session.
pcie::Model model("model.tar.gz", {}, connection);
const pcie::ModelInfo info = model.info();
for (const auto& input : info.inputs) {
std::cout << input.name << " requires " << input.size_bytes << " bytes\n";
}
model.build(/*readiness_timeout_ms=*/180000);
input_specs() and output_specs() return the same lists individually.
running() becomes true after a successful build and returns to false
after close().
Run synchronous inference
Use run() for the simplest request/response flow. Build the model first and
use a finite timeout so application failures do not wait indefinitely.
The following example constructs an input for a model whose reported input
datatype is FP32.
const auto& input_spec = info.inputs.front();
if (input_spec.dtype != "FP32") {
throw std::runtime_error("this example requires an FP32 model input");
}
std::vector<float> values(input_spec.size_bytes / sizeof(float), 0.0f);
pcie::Tensor input = pcie::Tensor::from_vector(
std::move(values), input_spec.shape, input_spec.name);
pcie::TensorList outputs = model.run(input, /*timeout_ms=*/30000);
model.close();
For a multi-input model, pass one Tensor per logical input in the order and
with the route names reported by info().inputs.
A run() timeout stops waiting but does not cancel an input already accepted
by the card. After catching a timeout, either use pull() to drain that
outstanding result or call close() before starting a new request sequence.
Pipeline requests with push and pull
Use push() and pull() when input preparation should overlap inference.
max_inflight bounds the accepted work that has not yet returned. Pull results
promptly so producers can continue.
std::size_t pushed = 0;
std::size_t pulled = 0;
while (pulled < inputs.size()) {
while (pushed < inputs.size() && pushed - pulled < 10) {
model.push(inputs[pushed++]);
}
auto outputs = model.pull(/*timeout_ms=*/30000);
if (!outputs) {
throw std::runtime_error("PCIe inference timed out");
}
consume(*outputs);
++pulled;
}
push() waits when max_inflight is full, so do not submit more than the
configured window without pulling results. pull() returns the next available
result for this model. Drain all results submitted with push() before calling
run().
Send images and configure preprocessing
Set preprocess.kind to Image when sending decoded image data. The card-side
Neat pipeline can resize, convert color, normalize, and decode supported object
detection outputs.
This example sends a BGR image, letterboxes it to the model input inferred from the model archive, and returns a tensor containing the decoded YOLOv8 BBOX payload.
#include <opencv2/imgcodecs.hpp>
pcie::ModelOptions options;
options.preprocess.kind = pcie::InputKind::Image;
options.preprocess.color_convert.input_format = pcie::ColorFormat::BGR;
options.preprocess.resize.enable = pcie::AutoFlag::On;
options.preprocess.resize.mode = pcie::ResizeMode::Letterbox;
options.decode_type = pcie::BoxDecodeType::YoloV8;
options.score_threshold = 0.25f;
options.nms_iou_threshold = 0.45f;
options.top_k = 100;
pcie::Model detector("yolo_v8n_mpk.tar.gz", options, connection);
detector.build();
cv::Mat image = cv::imread("image.jpg", cv::IMREAD_COLOR);
pcie::TensorList detections = detector.run(image, /*timeout_ms=*/30000);
detector.close();
Do not set input_max_width, input_max_height, or input_max_depth for a
seedless model unless the application needs an explicit input limit. Neat can
infer the model-side resize target from the model archive.
Close reliably
Call close() when the model is no longer needed and before reusing its queue.
It is safe to call more than once:
model = pcie.Model("model.tar.gz", connection=connection)
model.build()
outputs = model.run([input_tensor], timeout_ms=30000)
model.close()
Alternatively, use a context manager to close the model automatically:
with pcie.Model("model.tar.gz", connection=connection) as model:
model.build()
outputs = model.run([input_tensor], timeout_ms=30000)
The context manager calls close() when the block exits, including when an
exception is raised. Do not add another explicit close() inside the with
block.
Build a C++ host application
The development package provides the SimaPCIeHost CMake package:
cmake_minimum_required(VERSION 3.16)
project(pcie_model LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(SimaPCIeHost REQUIRED CONFIG)
add_executable(pcie_model main.cpp)
target_link_libraries(pcie_model PRIVATE SimaPCIeHost::sima_neat_pcie_host)
Build this application natively on the host machine.
The C++ image example also uses OpenCV. Add its headers and libraries to that application target:
find_package(OpenCV REQUIRED COMPONENTS core imgcodecs)
target_include_directories(pcie_model PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries(pcie_model PRIVATE ${OpenCV_LIBS})
Current scope and limits
- One
pcie::Modelowns one PCIe queue. Queues range from0through3. - The Modalix EV74 supports at most four concurrent co-processing pipelines.
- Do not assign two active models to the same queue.
- The host package and the Neat Library installed on the card must be from compatible releases.
- Keep the input media type and geometry stable after the first submitted payload. A later payload larger than the active transport capacity is rejected.
- The PCIe host API supports a focused subset of Neat model preprocessing and object-decode options.
- This co-processing API does not expose host-side
Graph,Node, orRuncomposition. Use the regular Neat Library on a DevKit for native application graphs.
For installation and connectivity checks, return to Install PCIe Host.