Verifying Your Deployment
Two tools confirm a deployment works before you run a full model: the
firmware's own hardware test suite (test_dynmod.sh, checks firmware
boot and the DLOAD/IPC path directly) and test_c7x_runtime (checks the
c7x::Module C++ API on top of a working firmware).
Firmware Hardware Test Suite
# From the firmware/c7x directory:
test/test_dynmod.sh --deploy
# Or with options:
test/test_dynmod.sh --target root@am67a --module /path/to/lib0.out
The test script covers 6 milestones: 1. Firmware boots via remoteproc 2. Basic IPC (ping, status) 3. Dynamic module load via DLOAD 4. Inference execution (cg_main_dsp with trace verification) 5. Module unload 6. Load-infer-unload stability cycle (5 iterations)
test_c7x_runtime (C++ API)
test_c7x_runtime is a C++ test binary for the c7x::Module ARM inference
API. It exercises libc7x_arm_runtime.so end-to-end on an AM67A/BeagleY-AI
ARM board against a live C7x DSP firmware instance -- a good way to confirm
your deployment actually works before running a full model.
This is the C++ counterpart to the Python integration tests in
tests/ti-dsp-runtime/dsp-tests/test_c7x_vm_dsp.py. Both test the
same underlying c7x_compute_client IPC path; this binary tests the
C++ c7x::Module wrapper directly, without Python or TVM overhead.
Test cases
| # | Name | What it checks |
|---|---|---|
| 1 | LOAD/CLOSE | Module::Load() connects to the DSP and loads the ELF; Close() is idempotent (safe to call twice) |
| 2 | INFERENCE | Run() returns a non-degenerate output tensor (ndim > 0, data_size > 0); prints shape and dtype |
| 3 | REFERENCE | If --ref is supplied: max|out - ref| < atol; skipped otherwise |
| 4 | CREATE_INPUT | CreateInput() pointer lies within [StagingBuffer(), StagingBuffer()+size) (pre-staged DDR); inference result matches standard-path result |
| 5 | REPEATED_INFER | Three consecutive Run() calls with identical input produce bit-identical output |
Test 1 is a prerequisite: if the module cannot be loaded, the remaining tests are skipped and the binary exits immediately.
Building
test_c7x_runtime is built automatically alongside libc7x_arm_runtime.so
when the arm/ CMakeLists detects the source file:
cd src/runtime/ti_dsp/firmware/c7x/arm
# Cross-compile for ARM64 (default)
./build.sh --board j722s-evm
# Or natively on the AM67A board
./build.sh --board j722s-evm native
Outputs written to arm/build/:
libc7x_arm_runtime.so — shared library (required at runtime)
c7x_compute — CLI tool
test_c7x_runtime — this test binary
Deploying to AM67A
cd src/runtime/ti_dsp/firmware/c7x/arm
./build.sh --board j722s-evm deploy
This SCPs all three binaries plus c7x_runtime.h to the board and
runs ldconfig so the shared library is found by the dynamic linker:
am67a:/usr/local/bin/c7x_compute
am67a:/usr/local/bin/test_c7x_runtime
am67a:/usr/local/lib/libc7x_arm_runtime.so
am67a:/usr/local/include/c7x_runtime.h
The deploy hostname is a deterministic function of --board
(beagley-ai -> beagley-ai, else am67a) -- add an SSH-config alias if
your board is reachable under a different name.
Preparing test inputs
The test binary takes a raw flat binary input.bin (contiguous, row-major).
Use Python/numpy on the development machine to generate it alongside a
CPU reference output ref.bin:
import numpy as np
# Generate a random input matching your model's expected shape
inp = np.random.randn(1, 64).astype("float32")
inp.tofile("/tmp/input.bin")
# Run on CPU to get the reference output
import sys
sys.path.insert(0, "tests/ti-dsp-runtime/dsp-tests")
from test_c7x_vm_dsp import _cpu_reference_mlp
ref = _cpu_reference_mlp(inp)
ref.tofile("/tmp/ref.bin")
Or use the pytest fixture directly — test_c7x_vm_dsp.py::TestC7xCpp
generates and transfers the files automatically.
Running on AM67A
SSH into the board and run:
# Minimal: load + infer + repeatability only (no reference check)
test_c7x_runtime /path/to/lib0.out /path/to/input.bin \
--shape 1,64 --dtype float32
# With reference comparison (max |out - ref| < 1e-3)
test_c7x_runtime /path/to/lib0.out /path/to/input.bin \
--shape 1,64 --dtype float32 \
--ref /path/to/ref.bin --atol 1e-3
# Classification model example (ResNet-18 style, 1000 classes)
test_c7x_runtime resnet18.out input_1x3x224x224.bin \
--shape 1,3,224,224 --dtype float32 \
--ref cpu_output_1x1000.bin --atol 5e-3
CLI reference
test_c7x_runtime <lib0.out> <input.bin>
[--shape D0,D1,...] [--dtype TYPE]
[--ref ref.bin] [--atol TOL]
Required:
lib0.out TVM c_static_lib DLOAD module (output of build_dsp_dynmod())
input.bin Raw binary input tensor, flat row-major, no header
Optional:
--shape Comma-separated dimensions matching the model input
(default: 1,64)
--dtype Element type: float32 float16 int32 int8 uint8
(default: float32)
--ref CPU reference output for numerical comparison
--atol Absolute tolerance for --ref comparison (default: 1e-3)
Exit code: 0 = all run tests passed; N = N failures
Expected output (all tests pass)
test_c7x_runtime: /tmp/mlp_lib0.out
input: /tmp/input.bin shape: 1,64 dtype: float32 (256 bytes)
--- Test 1: LOAD/CLOSE
PASS load_close
--- Test 2: INFERENCE
Output: ndim=2 data_size=32 dtype=2.32
shape[0]=1
shape[1]=8
PASS inference
--- Test 3: REFERENCE COMPARISON
max|out - ref| = 1.23e-07 (atol=1.00e-03)
PASS reference
--- Test 4: CREATE_INPUT
PASS create_input_range
PASS create_input_result
--- Test 5: REPEATED_INFER
PASS repeated_infer
Results: 5 passed, 0 failed
Prerequisites
| Requirement | Notes |
|---|---|
| AM67A (J722S) board | Running Linux (Yocto/Ubuntu) |
| c7x_compute firmware | Running on DSP; check with c7x_compute ping |
libc7x_arm_runtime.so |
Installed via ./build.sh deploy |
lib0.out |
TVM c_static_lib module for C7x (DLOAD-compatible ELF) |
aarch64-linux-gnu-g++ |
Cross-compiler, for building on dev PC |
DSP C++ Harness Build Reference
For developing custom DSP executables that run TVM-generated code directly
via the DSP runtime (bypassing the firmware/IPC layer), see the
tests/ti-dsp-runtime/dsp-cpp/ infrastructure.
Quick Start (Python)
import sys
sys.path.insert(0, "path/to/dsp-cpp")
from dsp_utils import compile_and_run_dsp, compare_results
# Compile and run on DSP (mod is a TVM IRModule with parameters bound)
results = compile_and_run_dsp(
mod=mod,
input_data=input_data, # numpy array or tuple of arrays
target_string="c_static_lib -mcpu=c66x",
execution_mode="both", # "host", "c66x", or "both"
build_type="Release", # "Release" or "Debug"
)
The workflow uses file-based I/O:
1. Python writes input tensors to input.bin
2. DSP executable reads input.bin, runs inference, writes output.bin
3. Python reads output.bin and compares against reference
See ../dsp-tests/test_clista_dsp.py for a complete example.
Key Files
| File | Description |
|---|---|
main_dsp.cpp |
Main entry point - reads input.bin, runs inference, writes output.bin |
CMakeLists.txt |
Build configuration for host, C66x, and C7x targets |
dsp_utils.py |
Python utilities for DSP compilation and execution |
io/tensor_file.cpp, io/tensor_file.h |
Tensor file I/O (input.bin/output.bin) |
io/weights_loader.cpp, io/weights_loader.h |
Weights source (filesystem or linker-embedded) |
io/tensor_file_format.md |
Binary tensor file format specification |
Prerequisites
For Host Emulation:
- CMake 3.16+
- C++11 compiler (GCC, Clang, or AppleClang)
- TVM DSP runtime library (libtvm_dsp_runtime_host.a)
For C66x Hardware:
- TI C6000 Compiler v8.5.0+ (part of CCS)
- MMWAVE-L-SDK-6 v6.1.0.05
- AWRL6844 evaluation board with XDS110 debug probe
- TVM DSP runtime library (libtvm_dsp_runtime_c66x.a)
For C7x Hardware:
- TI C7000 Compiler v5.0.0+ (part of CCS)
- J722S evaluation board with XDS110 debug probe
- Code Composer Studio 12.0+
- TVM DSP runtime library (libtvm_dsp_runtime_c7x.a)
Building
Step 1: Build the DSP Runtime
# Host emulation runtime
cd $TVM_HOME/src/runtime/ti_dsp
mkdir -p build && cd build
cmake ..
cmake --build .
# C66x runtime (optional, for hardware deployment)
cd $TVM_HOME/src/runtime/ti_dsp
mkdir -p build-c66x && cd build-c66x
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain-awrl6844.cmake ..
cmake --build .
# C7x runtime (optional, for J722S hardware deployment)
cd $TVM_HOME/src/runtime/ti_dsp
mkdir -p build-c7x && cd build-c7x
cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain-j722s-c7x.cmake ..
cmake --build .
Step 2: Generate TVM Model Code
import tvm
from tvm import relax
# Load/compile your model to get an IRModule
mod = ...
# Build with c_static_lib target for C66x
target = tvm.target.Target("c_static_lib -mcpu=c66x")
with tvm.transform.PassContext(opt_level=3):
ex = relax.build(mod, target, exec_mode="compiled", system_lib=True)
# Export generated code to a directory
ex.export_library("model_dir/model_library.tar", target=target)
# Extract to get lib0.c and weights.bin
Or use dsp_utils.py which handles this automatically:
from dsp_utils import compile_for_dsp
generated_dir = compile_for_dsp(mod, "c_static_lib -mcpu=c66x")
Step 3: Build Host Emulation
cd tests/ti-dsp-runtime/dsp-cpp
# Debug build (default)
mkdir -p build-debug && cd build-debug
cmake -DGENERATED_CODE_DIR=/path/to/model_dir ..
cmake --build .
# Release build
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release -DGENERATED_CODE_DIR=/path/to/model_dir ..
cmake --build .
Step 4: Build for C66x Hardware
cd tests/ti-dsp-runtime/dsp-cpp
# Debug build (default)
mkdir -p build-awrl6844-debug && cd build-awrl6844-debug
cmake \
-DCMAKE_TOOLCHAIN_FILE=$TVM_HOME/src/runtime/ti_dsp/cmake/toolchain-awrl6844.cmake \
-DGENERATED_CODE_DIR=/path/to/model_dir \
..
cmake --build .
# Release build
mkdir -p build-awrl6844 && cd build-awrl6844
cmake \
-DCMAKE_TOOLCHAIN_FILE=$TVM_HOME/src/runtime/ti_dsp/cmake/toolchain-awrl6844.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DGENERATED_CODE_DIR=/path/to/model_dir \
..
cmake --build .
Step 5: Build for C7x Hardware (J722S)
cd tests/ti-dsp-runtime/dsp-cpp
# Debug build (default)
mkdir -p build-j722s-debug && cd build-j722s-debug
cmake \
-DCMAKE_TOOLCHAIN_FILE=$TVM_HOME/src/runtime/ti_dsp/cmake/toolchain-j722s-c7x.cmake \
-DGENERATED_CODE_DIR=/path/to/model_dir \
..
cmake --build .
# Release build
mkdir -p build-j722s && cd build-j722s
cmake \
-DCMAKE_TOOLCHAIN_FILE=$TVM_HOME/src/runtime/ti_dsp/cmake/toolchain-j722s-c7x.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DGENERATED_CODE_DIR=/path/to/model_dir \
..
cmake --build .
Configuration Options
CMake Variables:
| Variable | Default | Description |
|---|---|---|
CMAKE_BUILD_TYPE |
Debug |
Build type: Debug or Release |
TVM_DSP_TARGET |
host |
Target: host, c66x, c7x_host, or c7x-dynmod |
TVM_DSP_DEVICE |
(none) | Device variant: awrl6844 or j722s |
GENERATED_CODE_DIR |
../cstatic-tests |
Directory containing lib0.c and weights.bin |
TVM_HOME |
Auto-detect | Path to TVM repository |
WEIGHTS_FILE |
${GENERATED_CODE_DIR}/weights.bin |
Path to weights file |
Model Configuration:
Set via CMake, compiled in as preprocessor definitions consumed by the Model API:
cmake -DMODEL_ENTRY_FUNCTION=main -DMODEL_NUM_INPUTS=1 \
-DMODEL_RETURNS_TUPLE=ON -DGENERATED_CODE_DIR=/path/to/model_dir ..
Input shape and data are provided via input.bin at runtime (no recompilation needed).
Generated Code Notes
When using -mcpu=c66x target, the generated lib0.c:
- Includes DSP-specific headers directly (ffi_types.h, etc.)
- Uses TVM_DSP_SKIP_CG_MAIN to exclude exception-based wrapper
- Is compiled as C++ to support the DSP runtime API
The CMakeLists.txt handles this automatically:
set_source_files_properties(${GENERATED_SOURCES} PROPERTIES LANGUAGE CXX)
Troubleshooting
"Failed to load weights"
- Verify weights.bin exists at the configured path
- Check file permissions
- For C66x: ensure weights are properly embedded
"TVMFFIFunctionCall with NULL function"
- Ensure InitVMBuiltins() is called before __vmtir__main
- Verify VM builtins are registered with TVMDSPRegisterVMBuiltins()
Inference timeout on C66x
- Check memory usage in the .map file
- Verify L2/L3 pool sizes are sufficient
- Try increasing timeout: --timeout 300000
Linker errors about LLVM paths
- The toolchain file should clear host linker flags
- If not, manually set: -DCMAKE_EXE_LINKER_FLAGS=""
Dynamic Shape Limitation
The c_static_lib backend does not support Relax loops (tail-recursive
functions requiring vm.builtin.invoke_closure with a runtime function
dispatch table). The static C codegen generates standalone C functions
with no inter-function call mechanism. Models needing iterative computation
(autoregressive LLMs, RNNs, iterative refinement) must unroll the loop at
the graph level or use the VM backend instead.
See tests/ti-dsp-runtime/dynamic-tests/ for tests exercising supported
dynamic features (If expressions, symbolic batch dimensions).