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.