XArith
Loading...
Searching...
No Matches
XArith API Overview

XArith is a device-side C++ library for XCENA's Computational Memory (MX1), providing high-performance vector computation primitives for VPE (Vector Processing Engine). Used with the MU library (mu/mu.hpp), it enables developers to build high-performance MU kernels running on the MX1's vector processing hardware.

Key Characteristics

  • Device-only library: Runs exclusively on MX1, not on the host
  • Thread-safe: Safe for concurrent access from multiple threads. For best performance, each thread should use its own VpeContext
  • Synchronous operations: All vector operations block until completion. Asynchronous operations will be available in a future release

API Reference

Context

Method Description
VpeContext(strategy, dataType) Create context with a VPE strategy and element data type (strategy defaults to ByThreadId, dataType to Fp32). The element type is fixed for the context's lifetime
getDataType() Get the element data type fixed at construction
bytesForDimension(dimension) Bytes occupied by dimension elements of the context's DataType; pass the result to waitAllocateBuffers() / allocateBuffer() to size a buffer

Buffer Management

SRAM is managed by a per-VPE allocator with a fixed 512 B chunk granularity. Allocation works in bytes: a buffer of b bytes occupies ceil(b / 512) contiguous chunks, so up to 160 concurrent buffers fit when the payload is ≤ 512 B and fewer for larger payloads. Buffers of different sizes coexist freely, and the context tracks each buffer's size so freeBuffer() needs no size argument. Use bytesForDimension() to size a buffer for an element count. The multi-buffer variants are atomic (all-or-nothing), so a caller never holds a partial reservation. There is no count-query API — allocate and act on the result rather than checking availability up front.

Each call comes in a blocking and a non-blocking form:

  • waitAllocate* waits until the buffers fit. Prefer this when the task cannot proceed without the buffers, which is the usual case.
  • allocateBuffer / tryAllocate* return immediately, reporting failure when the pool is momentarily full, for callers with something else to do meanwhile.

Do not hand-roll while (!tryAllocateBuffers(...)) {}. On MX1 every failed attempt issues allocator atomics, and those are resolved by the same L3 ALUs that the tasks holding the buffers need in order to run and free. A flat-out retry loop therefore throttles the very peers it is waiting on: under an oversubscribed pool it measurably starves them, turning a ~14 ms kernel launch into multi-second outliers. waitAllocate* paces its retries instead.

Note
waitAllocate* does not time out. It rejects (returns false / INVALID_BUFFER) only for a request that could not fit even an empty pool — a zero byte size, or a rounded-up chunk total above the 160-chunk (80 KB) per-VPE pool. Those are decided from the arguments alone, so a currently full pool is a reason to wait, never to fail. count == 0 is a vacuous success (returns true, nothing reserved), matching tryAllocateBuffers(). Size your task count against the pool, or use the non-blocking form, if unbounded waiting is not acceptable.
Warning
Check the result whenever the size derives from a runtime value. On the rejection path the output array is left untouched, so an unchecked call proceeds to use uninitialised stack values as SRAM offsets — silent corruption rather than a visible failure.
Method Description
waitAllocateBuffer(byteSize) Allocate one buffer of byteSize bytes, waiting until it fits; returns the offset, or INVALID_BUFFER for a request that can never fit
waitAllocateBuffers(count, byteSize, outBuffers) Atomically allocate count equally-sized buffers (all-or-nothing), waiting until they fit; returns false only for a request that can never fit
waitAllocateBuffersWithSizes(count, byteSizes, outBuffers) As above, with per-buffer byte sizes
allocateBuffer(byteSize) Non-blocking: allocate one SRAM buffer of byteSize bytes (rounded up to whole 512 B chunks); returns the offset or INVALID_BUFFER if no free run is available
tryAllocateBuffers(count, byteSize, outBuffers) Non-blocking: atomically allocate count equally-sized buffers (all-or-nothing); preferred over multiple allocateBuffer() calls to avoid partial-allocation deadlock
tryAllocateBuffersWithSizes(count, byteSizes, outBuffers) Non-blocking: atomically allocate count buffers of per-buffer byte sizes (all-or-nothing)
freeBuffer(buffer) Free a previously allocated buffer
freeBuffers(count, buffers) Free count buffers under a single lock acquisition (more efficient than repeated freeBuffer())
load(srcDram, dstBuffer, dimension) Load dimension elements from DRAM to SRAM; srcDram is a raw byte pointer, transfer size and element width come from the context DataType
store(srcBuffer, dstDram, dimension) Store dimension elements from SRAM to DRAM; dstDram is a raw byte pointer, transfer size and element width come from the context DataType
copy(src, dst, dimension) Copy dimension elements SRAM-to-SRAM; source buffer is left unchanged
fillZero(dst, dimension) Fill dimension elements with the value 0 (0.0 in the context DataType)
fillOne(dst, dimension) Fill dimension elements with the value 1 (1.0 in the context DataType: 0x3F800000 for FP32, 0x3C00 for FP16)

Vector Operations

Method Description
dot(src1, src2, dimension) Compute dot product; returns float (FP16 inputs accumulate in FP32)
dot2(shared, src1, src2, dimension) Compute two dot products sharing one operand in a single fused operation; both results are float (FP16 accumulates in FP32)
add(src1, src2, dst, dimension) Element-wise addition: dst = src1 + src2
sub(src1, src2, dst, dimension) Element-wise subtraction: dst = src1 - src2
mul(src1, src2, dst, dimension) Element-wise multiplication: dst = src1 * src2
div(src1, src2, dst, dimension) Element-wise division: dst = src1 / src2
square(src, dst, dimension) Element-wise square: dst = src * src
bitwiseXor(src1, src2, dst, dimension) Element-wise XOR: dst = src1 ^ src2
addReduce(src1, src2, dimension) Sum of element-wise addition; returns float (FP16 accumulates in FP32)
subReduce(src1, src2, dimension) Sum of element-wise subtraction; returns float (FP16 accumulates in FP32)
divReduce(src1, src2, dimension) Sum of element-wise division; returns float (FP16 accumulates in FP32)
equals(src1, src2, dimension) Check if vectors are equal
isAllZero(src, dimension) Check if all elements are zero

Scalar-Vector Operations

Element-wise ops between a broadcast scalar and a vector, in the hardware's native order dst[i] = scalar OP src[i] (the scalar is the left operand). FP32 and FP16 contexts are both supported.

You supply the scalar in SPB. The mx1p VPER has no scalar-immediate field – it reads the scalar from the first element of an SPB buffer. So before the call, load the scalar into a one-element buffer yourself: allocateBuffer(bytesForDimension(1)), then load(scalarDram, scalar, 1) from your own DRAM (or point scalar at a prior op's SPB output). The op does no staging, conversion, or allocation. scalar must hold the value in the context's element type (a half for an FP16 context; raw mask bits for scalarXor()) and must not alias dst.

Scalar-LHS only. Only scalar OP src[i] is offered. For the reversed src[i] OP scalar order, pre-transform the scalar you load: src - s = load -s then scalarAdd(); src / s = load 1/s then scalarMul().

Method Description
scalarAdd(scalar, src, dst, dimension) Element-wise add: dst = scalar + src
scalarSub(scalar, src, dst, dimension) Element-wise subtract: dst = scalar - src
scalarMul(scalar, src, dst, dimension) Element-wise multiply: dst = scalar * src
scalarDiv(scalar, src, dst, dimension) Element-wise divide (true per-element divide): dst = scalar / src (src[i] != 0)
scalarXor(scalar, src, dst, dimension) Element-wise XOR of the scalar's raw bits: dst = scalar ^ src
scalarDot(scalar, src, dimension) Scalar-weighted sum Σ(scalar · src[i]); returns float

VpeIdStrategy Options

The strategy selects how a context is assigned to a VPE so that concurrent work is distributed across the available engines.

Strategy Value Description
ByThreadId Default Spreads concurrent threads in the same execution unit across engines
ByClusterId Alternative Spreads work from different clusters across engines

DataType Options

The element data type is fixed at construction. It sets the per-buffer SRAM payload (dimension × elementSize bytes, allocated in 512 B chunks) and the element type used by every operation issued by the context. Query it at runtime with getDataType().

DataType Value Bytes/element Description
Fp32 Default 4 32-bit IEEE 754 single precision
Fp16 Alternative 2 16-bit IEEE 754 half precision; halves the per-buffer payload, so a buffer needs at most half as many 512 B chunks and more concurrent buffers can fit
  • Byte-based load/storeload() / store() take a raw void* DRAM endpoint and use the context DataType as the single source of truth for element width (transfer size = dimension × elementSize); a host buffer whose elements differ from getDataType() is a data-interpretation error, not an out-of-bounds access
  • Reductions stay FP32 – the hardware accumulates FP16 reductions in FP32, so dot(), dot2(), and the *Reduce() methods always return float regardless of element type
  • No mixing on one VPE – contexts of differing data type or dimension that share a VPE collide on the shared allocator lock; do not create such mixed contexts on the same VPE