basic — Fundamental Vector Operations
basic/main.cpp holds five device kernels — L2 distance, inner product, a fused dot2, vector add, and vector sub — alongside the host driver that launches them. Each kernel is a small VpeContext program: allocate SRAM buffers, load from DRAM, run the op, then return a scalar or store a vector back.
L2 distance — the canonical distance kernel. Allocate three buffers all-or-nothing (the deadlock-free spin from Buffer Allocation), load both vectors, then L2² = (target − query)·(target − query):
__pxl_kernel__ void computeExampleL2Distance(const float* query, const float* target,
float* result, uint64_t dimension)
{
mu::vdma::VpeContext ctx(mu::vdma::VpeIdStrategy::ByThreadId);
uint64_t bufs[3];
// false only if the request could never fit an empty pool -- with a
// runtime dimension that is worth checking; the output array is untouched.
if (!ctx.waitAllocateBuffers(3, ctx.bytesForDimension(dimension), bufs))
return;
ctx.load(query, bufs[0], dimension);
ctx.load(target, bufs[1], dimension);
ctx.sub(bufs[1], bufs[0], bufs[2], dimension); // diff = target - query
*result = ctx.dot(bufs[2], bufs[2], dimension); // diff . diff
}
Inner product drops the sub and returns ctx.dot(buf1, buf2, dimension) directly. Vector add / sub write a vector result instead of a scalar — the only difference from L2 is the final step:
ctx.add(buf1, buf2, bufResult, dimension); // (or ctx.sub for vector subtraction)
ctx.store(bufResult, result, dimension); // write the result vector back to DRAM
dot2 is the distinctive one: two dot products that share an operand, fused into a single DPT2 op — e.g. scoring one query against two candidates in one pass:
__pxl_kernel__ void computeExampleDot2(const float* shared, const float* src1, const float* src2,
float* results, uint64_t dimension)
{
mu::vdma::VpeContext ctx(mu::vdma::VpeIdStrategy::ByClusterId);
uint64_t bufs[3];
// false only if the request could never fit an empty pool -- with a
// runtime dimension that is worth checking; the output array is untouched.
if (!ctx.waitAllocateBuffers(3, ctx.bytesForDimension(dimension), bufs))
return;
ctx.load(shared, bufs[0], dimension);
ctx.load(src1, bufs[1], dimension);
ctx.load(src2, bufs[2], dimension);
auto r = ctx.dot2(bufs[0], bufs[1], bufs[2], dimension);
results[0] = r.r1; // shared . src1
results[1] = r.r2; // shared . src2
}
Sharing one operand is what lets the two products fuse into a single operation, roughly doubling FLOPs/cycle versus two separate dot() calls. The host drives all five kernels and checks each result against a CPU reference.
./run.sh basic # or, from the build/ directory:
./bin/xarith_basic