vpe — VPE Operation Benchmark

vpe/main.cpp provides each VPE operation (dot, dot2, L2, add, sub, mul, div) from a single templated device body instantiated for both element types — float (FP32) and mu::vdma::half (FP16). The only per-type difference is the DataType bit passed to VpeContext; the loop shape is identical, so the two precisions are measured at matched configurations.

template <typename T>
void dotBody(const T* vec1, const T* vec2, uint64_t dimension,
             uint64_t numIterations, float* results, float* checksums)
{
    uint64_t taskIdx = mu::getTaskIdx();
    VpeContext ctx(VpeIdStrategy::ByClusterId, VpeDataType<T>::value);  // Fp32 or Fp16

    uint64_t bufs[2];
    allocBuffersBlocking(ctx, bufs, dimension);
    ctx.load(vec1, bufs[0], dimension);
    ctx.load(vec2, bufs[1], dimension);

    float sum = 0.0f, lastResult = 0.0f;
    for (uint64_t i = 0; i < numIterations; ++i)
    {
        lastResult = ctx.dot(bufs[0], bufs[1], dimension);
        sum += lastResult;          // checksum keeps the compiler from hoisting the loop
    }
    results[taskIdx]   = lastResult;
    checksums[taskIdx] = sum;
}

VpeDataType<T> maps the host element type to the DataType bit (floatFp32, halfFp16). pxcc launches kernels by symbol, so each (op, dtype) pair is a concrete __pxl_kernel__ wrapper that instantiates the matching body; the host picks the right wrapper at compile time (via a Kernels<T> traits struct) rather than a name lookup:

__pxl_kernel__ void vpeDotF32(const float* v1, const float* v2, uint64_t dim, uint64_t iters,
                              float* results, float* checksums)
{
    dotBody<float>(v1, v2, dim, iters, results, checksums);
}
__pxl_kernel__ void vpeDotF16(const half* v1, const half* v2, uint64_t dim, uint64_t iters,
                              float* results, float* checksums)
{
    dotBody<half>(v1, v2, dim, iters, results, checksums);
}
// ... l2, dot2, and element-wise add/sub/mul/div, each in F32 and F16

The element-wise ops share one body too, selecting add/sub/mul/div via a pointer-to-member template parameter. Reductions (dot/dot2/L2) always return FP32 — the hardware accumulates FP16 reductions in FP32.

The host sweeps dot/dot2/L2/add/sub/mul over numSub ∈ {16, the device’s own Sub count — read at startup, since parts differ}, a range of taskCount values, and dim ∈ {128, 256, 512, 1024, 2048, 4096}; div is far slower than the rest, so it runs only a minimal smoke point (dim 1024, SRAM-saturating taskCount) instead of the full grid. It reports throughput in GFLOPS and prints a best-configuration-per-op summary for FP32 then FP16. Grid points whose worst-case VPE would exceed the SRAM budget are skipped silently; every kept point is checked against a host reference (relErr ≤ 1%).

./run.sh vpe            # or, from the build/ directory:
./bin/xarith_vpe