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.
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.