fp16 — FP16 Data Type & Conversion

Minimal FP16 demo built on mu::vdma::VpeContext constructed with DataType::Fp16. fp16/main.cpp holds four device kernels next to the host driver: four host-side tests (element-wise add, element-wise mul, dot product, and FP16 overflow accumulation) drive them.

./run.sh fp16           # or, from the build/ directory:
./fp16/xarith_fp16

Selecting the data type

A context’s precision is fixed when it is constructed — pass the data type to the VpeContext constructor:

// FP32 (the implicit choice in the other examples)
mu::vdma::VpeContext ctx(mu::vdma::VpeIdStrategy::ByThreadId);

// FP16 — operands and element-wise results are 16-bit
mu::vdma::VpeContext ctx(mu::vdma::VpeIdStrategy::ByThreadId, mu::vdma::DataType::Fp16);

FP16 buffers store mu::vdma::half elements (2 bytes each), so the kernel takes half* operands and sizes buffers with ctx.bytesForDimension(dimension) (which counts 2 bytes per element for Fp16). dot() always returns float — the hardware accumulates FP16 reductions in FP32, so a reduction keeps FP32 range even with FP16 inputs.

Converting fp32 ↔ fp16

Conversion is done host-side with the header-only mu::vdma::half type from <xarith/half.hpp>:

#include <xarith/half.hpp>
using mu::vdma::half;

half h  = half(3.14f);             // fp32 -> fp16 (constructor)
float f = static_cast<float>(h);   // fp16 -> fp32

// Pack an fp32 source array `src` into an fp16 device buffer `buf`
// (both DIMENSION elements):
for (uint64_t i = 0; i < DIMENSION; ++i)
    buf[i] = half(src[i]);

The kernel side

A kernel takes half* operands and constructs its context with the FP16 data type; otherwise it is identical to an FP32 kernel. From fp16/main.cpp:

__pxl_kernel__ void fp16ElementwiseAdd(const mu::vdma::half* a, const mu::vdma::half* b,
                                       mu::vdma::half* dst, uint64_t dim)
{
    mu::vdma::VpeContext ctx(mu::vdma::VpeIdStrategy::ByThreadId, mu::vdma::DataType::Fp16);

    uint64_t buf[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(dim), buf))
        return;

    ctx.load(a, buf[0], dim);
    ctx.load(b, buf[1], dim);
    ctx.add(buf[0], buf[1], buf[2], dim);
    ctx.store(buf[2], dst, dim);
}

fp16DotProduct is the same shape but its result is float*dot() returns FP32 even for half inputs.

The host writes half values straight into the device buffer and checks the FP16 outputs bit-exact against a half reference. The overflow-accumulation test exercises the FP16 range limit: the largest finite FP16 value is 65504, so a doubling step past it rounds to +Inf.