edgefirst-tensor
Zero-copy tensor memory management for edge AI applications.
This crate provides a unified interface for managing multi-dimensional arrays (tensors) with support for different memory backends optimized for ML inference pipelines.
Role in edgefirst-hal
edgefirst-tensor is the foundation of the data-plane crates in the
EdgeFirst HAL workspace. The image, decoder, capi, and gpu-probe crates
all depend on it; the tracker and bench crates are independent and
operate on their own types.
edgefirst-imageconsumesTensor<u8>/TensorDynfor image processor input/output buffers and provides thePboOpstrait impl that backsPboTensor.edgefirst-decoderreads model output viaTensor<T>andTensorMap.edgefirst-halre-exports this crate asedgefirst_hal::tensor.edgefirst-hal-capicrosses the FFI boundary usingfrom_fd,clone_fd, andfrom_planes.gpu-probeuses it to allocate the DMA-BUF round-trip buffer the probe verifies.edgefirst-trackerandedgefirst-benchdo not depend on this crate — tracker works againstDetectionBoxandnalgebra, bench wrapsserde_jsonfor benchmark IO.
This crate has no internal edgefirst-* dependencies.
Memory Types
| Type | Description | Use Case |
|---|---|---|
| DMA | Linux DMA-BUF allocation | Hardware accelerators (GPU, NPU, video codecs) |
| SHM | POSIX shared memory | Inter-process communication, zero-copy IPC |
| Mem | Standard heap allocation | General purpose, maximum compatibility |
| PBO | OpenGL Pixel Buffer Object | GPU-accelerated image processing (created by ImageProcessor) |
Features
- Automatic memory selection - Tries DMA → SHM → Mem based on availability
- Zero-copy sharing - Share tensors between processes via file descriptors
- Memory mapping - Efficient CPU access to tensor data
- ndarray integration - Optional conversion to/from
ndarray::Array(feature:ndarray)
Quick Start
use ;
// Create a tensor with automatic memory selection
let tensor = new?;
println!;
// Create with explicit memory type
let dma_tensor = new?;
// Map tensor for CPU access
let mut map = tensor.map?;
map.as_mut_slice.fill;
// Share via file descriptor (Unix only)
let fd = tensor.clone_fd?;
Platform Support
| Platform | DMA | SHM | Mem | PBO |
|---|---|---|---|---|
| Linux | Yes | Yes | Yes | Yes (with OpenGL) |
| macOS | No | Yes | Yes | No |
| Other Unix | No | Yes | Yes | No |
| Windows | No | No | Yes | No |
Feature Flags
ndarray(default) - Enablendarrayintegration for array conversions
Environment Variables
EDGEFIRST_TENSOR_FORCE_MEM- Set to1ortrueto force heap allocation
PlaneDescriptor
PlaneDescriptor wraps a duplicated file descriptor for use with
ImageProcessor::import_image(). It captures optional stride and offset
metadata alongside the fd so that the importer gets a complete picture of the
plane layout without additional out-of-band parameters.
use PlaneDescriptor;
use BorrowedFd;
// SAFETY: replace 42 with a real, valid fd from a DMA-BUF allocation.
let pd = unsafe
.expect
.with_stride // optional: row stride in bytes
.with_offset; // optional: plane offset in bytes
The fd is duplicated eagerly in new() — a bad fd fails immediately rather
than inside import_image. The caller retains ownership of the original fd.
DMA-BUF fd Accessors
TensorDyn exposes two fd accessors for DMA-backed tensors (Linux only):
dmabuf(&self) -> Result<BorrowedFd<'_>>— Borrow the DMA-BUF fd tied to the tensor's lifetime.dmabuf_clone(&self) -> Result<OwnedFd>— Duplicate the DMA-BUF fd. Fails withError::NotImplementedif the tensor is not DMA-backed.
// Share the buffer with an external consumer (e.g. NPU delegate)
let fd = tensor.dmabuf_clone?;
delegate.register_buffer?;
Pixel Format Metadata
Attach a PixelFormat to any tensor for image processing:
set_format(format: PixelFormat) -> Result<()>— Validates shape compatibility and stores the format.with_format(format: PixelFormat) -> Result<Self>— Builder-style consuming variant.
let mut t = new?;
t.set_format?;
Row Stride
For externally allocated buffers with row padding (e.g. V4L2 camera frames):
row_stride(&self) -> Option<usize>— Stored stride,Noneif tightly packed.effective_row_stride(&self) -> Option<usize>— Stored stride, or computed from format and width if not set.set_row_stride(stride: usize) -> Result<()>— Set stride in bytes. Format must be set first.with_row_stride(stride: usize) -> Result<Self>— Builder-style consuming variant.
Plane Offset
For buffers where image data does not start at byte 0 of the fd:
plane_offset(&self) -> Option<usize>— Offset in bytes,Noneif zero.set_plane_offset(offset: usize)— Set byte offset.with_plane_offset(offset: usize) -> Self— Builder-style consuming variant.
BufferIdentity
BufferIdentity provides a stable cache key for a tensor's underlying buffer.
It is created fresh on every allocation or import and carries:
id() -> u64— Monotonically increasing integer. Changes whenever the buffer changes. Suitable as a HashMap key or EGL image cache key.weak() -> Weak<()>— Goes dead when the owning tensor (and all clones) are dropped, allowing caches to detect stale entries without holding a strong reference.
buffer_identity() is accessible on typed tensors via TensorTrait:
use ;
let t = new?;
let key = t.buffer_identity.id;
let guard = t.buffer_identity.weak;
// Later: guard.upgrade().is_none() means the tensor was dropped.
BufferIdentity is used internally by the image processing backends as an EGL
image cache key to avoid redundant GPU texture imports across frames.
CUDA Tensor Mapping
On CUDA-capable devices (e.g. Jetson Orin-series) the float PBO produced
by ImageProcessor::convert() can be mapped directly to a CUDA device
pointer. No link-time dependency on libcudart — the symbols are resolved
at runtime via dlopen.
Availability probe
use is_cuda_available;
if is_cuda_available
Usage — try CUDA map, fall back to host
use ;
// Per-frame: prefer zero-copy CUDA, fall back to host map
if let Some = dst.cuda_map else
cuda_map() returns None when:
libcudartis not present at runtime.- The tensor is not PBO- or DMA-BUF-backed.
- CUDA registration of the backing buffer failed (logged at
warn).
The CudaMap guard must be dropped before the next ImageProcessor::convert()
call that writes into the same tensor — the GL pipeline must not touch a
PBO while CUDA has it mapped. See
ARCHITECTURE.md § Zero-copy CUDA tensor mapping
for the full aliasing rules, DMA-BUF import path, and drop-order contract.
C API
if
Python
= # returns CudaMap or None
# context manager — unmap on __exit__
Documentation
- Architecture overview: ARCHITECTURE.md
- Testing guide: TESTING.md
- Full API reference: docs.rs/edgefirst-tensor
- Project README: ../../README.md
License
Licensed under the Apache License, Version 2.0. See LICENSE for details.