|
XArith
|
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.
| 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 |
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.
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.| 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) |
| 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 |
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 |
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 |
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 |
load() / 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 accessdot(), dot2(), and the *Reduce() methods always return float regardless of element type