Python / C++ API Reference: C7xVirtualMachine / c7x::Module
Arm-side inference API for TVM c_static modules running on the C7x DSP.
Provides a relax.VirtualMachine-compatible interface in both Python and
C++, routing inference to the DSP via the c7x_compute IPC service.
The C++ API is a direct mirror of the Python one, so this single document covers both.
Runs on the board, not the dev host. Both APIs talk to the DSP over the
board's local rpmsg IPC channel, so your Python/C++ process must run on the
ARM Linux side of the target board (AM67A / BeagleY-AI) itself — it cannot
connect to a board remotely over the network. Compile/quantize the model on
your dev host, then copy the compiled artifact (lib0.out) to the board to
run inference.
Prerequisites:
- c7x_compute firmware already running on the DSP (check with
c7x_compute ping on the board)
- libc7x_arm_runtime.so installed on the board — see Getting
Started for the wheel-based build+deploy flow, or
Deploying Firmware for the native
cross-compile/scp flow. Locating Library and Header
Paths below covers resolving where
either flow put the .so/header, for building your own application
against these APIs.
Python API
Import:
from tvm.contrib.c7x import C7xVirtualMachine
Standard usage (copy-based inputs)
Identical syntax to relax.VirtualMachine on CPU:
import tvm, numpy as np
from tvm.contrib.c7x import C7xVirtualMachine
vm = C7xVirtualMachine("/models/resnet18.out")
inp = tvm.nd.array(np.random.randn(1, 3, 224, 224).astype("float32"))
out = vm["main"](inp) # returns tvm.nd.NDArray
print(out.numpy().argmax())
vm.close()
Context manager form (recommended for scripts):
with C7xVirtualMachine("/models/resnet18.out") as vm:
out = vm["main"](inp)
Zero-copy outputs: run_nocopy()
vm["main"]() copies outputs from result DDR to new memory (safe across
multiple calls). run_nocopy() returns numpy views directly into the mmap'd
result buffer — no copy — but they are only valid until the next
run_nocopy() call:
out_np = vm.run_nocopy(data) # numpy array, zero-copy
process(out_np) # must finish before the next run_nocopy() call
Zero-copy inputs: create_input()
Pre-allocate an input tensor inside the staging DDR buffer. Writing to it
skips the staging memcpy on the next inference call:
staging = vm.create_input((1, 3, 224, 224), "float32") # backed by staging DDR
staging.copyfrom(frame) # writes directly to staging buffer
out = vm["main"](staging) # no input copy
The tensor is valid until close() is called. Multiple inputs can be
pre-staged; each call to create_input() advances the allocation offset by the
tensor size (aligned to 64 bytes).
Properties
vm.last_cycles # DSP TSC cycle count from the most recent inference (int)
vm.is_loaded # True if the module is currently loaded on the DSP (bool)
Constructor
C7xVirtualMachine(module_path, so_path="libc7x_arm_runtime.so")
module_path: path tolib0.out(the TVM c_static dynmod)so_path: name or path oflibc7x_arm_runtime.so. A bare name (the default) first checks the wheel's own bundled copy (tvm.data.ti_dsp.paths.find_c7x_arm_runtime_so()), then falls back toctypes.util.find_library+LD_LIBRARY_PATH-- so atvm-ti-c7x-inferencewheel install needs no override at all; only a native/source-tree deploy does (or pass an explicit absolute path to skip the lookup).
Connection to the DSP is established lazily on the first call to vm["main"]
or create_input().
C++ API
Header:
c7x_runtime.h (src/runtime/ti_dsp/firmware/c7x/arm/include/c7x_runtime.h
in the source tree)
— not deployed to /usr/local/include/ by ./build.sh deploy; resolve it
from the tvm-ti-c7x-inference wheel or the source tree instead, see
Locating Library and Header Paths
No TVM runtime dependency — only DLPack is required, so this header can be used from any C++ application on the board.
Standard usage
#include "c7x_runtime.h"
// Load module — throws std::runtime_error on failure
auto vm = c7x::Module::Load("/models/resnet18.out");
// Single-input convenience
c7x::OutputTensor out = vm.Run(&input_dl_tensor);
// out.dl.data → pointer into result DDR, valid until next Run() or Close()
// Multi-input convenience
std::vector<c7x::OutputTensor> outs = vm.Run({&in0, &in1});
Function dispatch (mirrors TVM C++ Module)
c7x::OutputTensor outputs[8];
int num_outputs = 0;
const DLTensor* inputs[] = { &dl_tensor };
auto fn = vm["main"];
int rc = fn(inputs, 1, outputs, &num_outputs);
// rc == 0 on success; outputs[0..num_outputs-1] are valid
Zero-copy inputs
int64_t shape[] = {1, 3, 224, 224};
DLDataType float32 = {kDLFloat, 32, 1};
DLTensor* inp = vm.CreateInput(shape, 4, float32); // data in staging DDR
memcpy(inp->data, my_data, nbytes);
auto out = vm.Run(inp); // no staging memcpy
CreateInput returns nullptr if the staging buffer is full or the module is
not loaded. Supports up to kMaxInputs (128) pre-staged tensors.
Class reference
namespace c7x {
struct OutputTensor {
DLTensor dl; // data → result DDR (zero-copy); valid until next Run/Close
int64_t _shape[6]; // shape storage (dl.shape → this)
size_t data_size; // byte size of the output
};
class Module {
public:
static Module Load(const std::string& lib0_path); // throws on failure
~Module();
Module(Module&&) noexcept;
Module& operator=(Module&&) noexcept;
Module(const Module&) = delete;
struct Function {
int operator()(const DLTensor* const* inputs, int num_inputs,
OutputTensor* outputs, int* num_outputs) const;
};
Function operator[](const std::string& name);
OutputTensor Run(const DLTensor* input);
std::vector<OutputTensor> Run(const std::vector<const DLTensor*>& inputs);
DLTensor* CreateInput(const int64_t* shape, int ndim, DLDataType dtype);
void* StagingBuffer(size_t* size_out = nullptr) const;
void Close();
};
} // namespace c7x
Memory and lifetime rules
Getting these wrong is the most common source of bugs — outputs and pre-staged inputs are views into shared DDR, not independent allocations:
| API | Valid until |
|---|---|
vm["main"](...) / C++ Run(...) |
Not time-limited — output is copied to new memory before return. |
vm.run_nocopy(...) output |
The next run_nocopy() call (no copy — a numpy view of result_buf). |
C++ OutputTensor.dl.data |
The next Run() call, or Close(). |
vm.create_input(...) / C++ CreateInput(...) tensor |
vm.close() / Close(). |
If you need output data to outlive the next inference call, copy it
yourself (e.g. memcpy(my_buf, out.dl.data, out.dl.data_size) in C++, or use
vm["main"](...) instead of run_nocopy() in Python).
Locating Library and Header Paths
libc7x_arm_runtime.so and c7x_runtime.h (used by both APIs above) are
built and deployed to the board by Getting Started's
wheel-based flow, or natively by Deploying Firmware
(arm/build.sh deploy, i.e. cross-compile + scp + ldconfig) -- see
Deploying Firmware -- Testing for how to
confirm either deploy actually works before building against it. This
section is only about resolving where those files ended up afterwards,
for building your own application against the API -- not about the
build/deploy step itself.
Wheel install: pip install tvm-ti-c7x-inference on the board unpacks
libc7x_arm_runtime.so, c7x_runtime.h, and this Python module into
site-packages. The Python API's so_path already resolves this
automatically (see Constructor above); a C++ build, or anything linking
against the .so directly, has to resolve both paths itself instead of
assuming a system install:
python3 -c "from tvm.data.ti_dsp.paths import find_c7x_include_dir; \
print(find_c7x_include_dir())"
python3 -c "from tvm.data.ti_dsp.paths import find_c7x_arm_runtime_so; \
print(find_c7x_arm_runtime_so())"
ctypes.CDLL() (used by the Python API) loads the .so by absolute path,
so no ldconfig step is needed either way. A C++ build should pass the
include dir as -I and either link against the resolved .so path
directly or dlopen() it at runtime. find_c7x_include_dir() returns
None outside a wheel install (e.g. in a source checkout, where the
header and DLPack live in two separate source-tree directories instead
of one merged include/) — callers should fall back to those paths in
that case, as examples/run_resnet18_classification.py
(tests/ti-dsp-runtime/examples/run_resnet18_classification.py in the
source tree) does.
Examples
Examples: YOLO26 & ResNet-18 has full runnable examples for both APIs:
- Python: YOLO26 object detection on BeagleY-AI/AM67A --
run_yolo26_detection.pycompiles and deploys the model from the dev host, andyolo26_board_runner.pyis the board-side script that actually callsC7xVirtualMachine. - C++: ResNet-18 classification --
run_resnet18_classification.pycompiles and cross-compiles from the dev host, andresnet18_board_runner.cppis the board-side program that callsc7x::Moduledirectly, built on the small sharedc7x_infer.h(tests/ti-dsp-runtime/examples/common/c7x_infer.hin the source tree) helper library.