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