Files
TheRock/rocm-systems/projects/hip-tests/catch/unit/deviceLib/ext_ocp_fp8.cc
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

308 lines
10 KiB
C++

/*
* Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
*
* SPDX-License-Identifier: MIT
*/
#include <hip_test_common.hh>
#include <hip/hip_ext_ocp.h>
#include <cmath>
#include <utility>
#include <vector>
/*
* Unit tests for the OCP FP8 (E4M3 / E5M2) types defined in hip_ext_ocp.h:
* __hipext_ocp_fp8_e4m3, __hipext_ocp_fp8_e5m2,
* __hipext_ocp_fp8x2_e4m3, __hipext_ocp_fp8x2_e5m2
*
* These tests are hardware agnostic: they only assert that "value in ->
* expected value out". They cover round-trip idempotence over all representable
* bit patterns, documented known values, scaled conversions, stochastic
* rounding bounds, and fp16/bf16 sources. A final section runs the same
* conversion on device and requires it to match the host result.
*/
namespace {
// Is a raw fp8 bit pattern NaN / Inf for the given interpretation?
bool is_e4m3_special(unsigned i) { return (i & 0x7fu) == 0x7fu; } // 0x7f, 0xff are NaN
bool is_e5m2_special(unsigned i) { return (i & 0x7fu) >= 0x7bu; } // sat E5M2: 0x7b inf, 0x7c overflow, 0x7d-7f NaN
// All representable (finite, non-NaN) fp8 values for an interpretation.
template <bool is_e4m3> std::vector<float> all_representable_fp8() {
std::vector<float> ret;
for (unsigned i = 0; i <= 0xffu; ++i) {
if (is_e4m3 ? is_e4m3_special(i) : is_e5m2_special(i)) continue;
if (is_e4m3) {
__hipext_ocp_fp8_e4m3 t;
t.__x = static_cast<__amd_fp8_storage_t>(i);
ret.push_back(static_cast<float>(t));
} else {
__hipext_ocp_fp8_e5m2 t;
t.__x = static_cast<__amd_fp8_storage_t>(i);
ret.push_back(static_cast<float>(t));
}
}
return ret;
}
// Nearest representable values bracketing v: {lo <= v, hi >= v}.
std::pair<float, float> bracket(const std::vector<float>& reps, float v) {
float lo = -INFINITY, hi = INFINITY;
for (float r : reps) {
if (r <= v && r > lo) lo = r;
if (r >= v && r < hi) hi = r;
}
return {lo, hi};
}
} // namespace
/**
* Test Description
* ------------------------
* - Every representable fp8 bit pattern must survive a fp8 -> float -> fp8
* round-trip unchanged (encode is the exact inverse of decode).
* Test source
* ------------------------
* - /unit/deviceLib/ext_ocp_fp8.cc
*/
HIP_TEST_CASE(Unit_ext_ocp_fp8_roundtrip_host) {
SECTION("e4m3") {
for (unsigned i = 0; i <= 0xffu; ++i) {
if (is_e4m3_special(i)) continue;
__hipext_ocp_fp8_e4m3 in;
in.__x = static_cast<__amd_fp8_storage_t>(i);
float f = in;
__hipext_ocp_fp8_e4m3 back(f);
INFO("pattern 0x" << std::hex << i << " -> " << f << " -> 0x" << (unsigned)back.__x);
REQUIRE(back.__x == in.__x);
}
}
SECTION("e5m2") {
for (unsigned i = 0; i <= 0xffu; ++i) {
if (is_e5m2_special(i)) continue;
__hipext_ocp_fp8_e5m2 in;
in.__x = static_cast<__amd_fp8_storage_t>(i);
float f = in;
__hipext_ocp_fp8_e5m2 back(f);
INFO("pattern 0x" << std::hex << i << " -> " << f << " -> 0x" << (unsigned)back.__x);
REQUIRE(back.__x == in.__x);
}
}
}
/**
* Test Description
* ------------------------
* - Assert documented reference values (min/max normal, unit, halves) from the
* format tables in amd_hip_ocp_fp.hpp decode to exact floats.
* Test source
* ------------------------
* - /unit/deviceLib/ext_ocp_fp8.cc
*/
HIP_TEST_CASE(Unit_ext_ocp_fp8_known_values_host) {
SECTION("e4m3") {
// {bit pattern, expected float}
std::vector<std::pair<unsigned, float>> table = {
{0x00, 0.0f}, {0x38, 1.0f}, {0x40, 2.0f},
{0x30, 0.5f}, {0x7E, 448.0f} /*max normal*/,
{0x08, 0.015625f} /*min normal 2^-6*/};
for (auto [pat, exp] : table) {
__hipext_ocp_fp8_e4m3 t;
t.__x = static_cast<__amd_fp8_storage_t>(pat);
float f = t;
INFO("e4m3 pattern 0x" << std::hex << pat);
REQUIRE(f == exp);
}
}
SECTION("e5m2") {
// NOTE: 0x7B (E5M2 max normal, 57344) is intentionally excluded here. Under
// the saturating decode (sat=true) the reference classifies 0x7B as the
// saturated-inf pattern, so operator float() returns inf rather than 57344.
// Re-add {0x7B, 57344.0f} once the E5M2 max-normal/inf aliasing is resolved.
std::vector<std::pair<unsigned, float>> table = {
{0x3C, 1.0f}, {0x40, 2.0f}, {0x38, 0.5f}, {0x04, 6.10352e-05f} /*min normal 2^-14*/};
for (auto [pat, exp] : table) {
__hipext_ocp_fp8_e5m2 t;
t.__x = static_cast<__amd_fp8_storage_t>(pat);
float f = t;
INFO("e5m2 pattern 0x" << std::hex << pat);
REQUIRE(f == Catch::Approx(exp));
}
}
}
/**
* Test Description
* ------------------------
* - Decoding with an E8M0 scale multiplies the base value by 2^scale.
* get_scaled_float/fp16/bf16 must reflect that exactly.
* Test source
* ------------------------
* - /unit/deviceLib/ext_ocp_fp8.cc
*/
HIP_TEST_CASE(Unit_ext_ocp_fp8_scaled_decode_host) {
// e4m3 pattern 0x38 == 1.0
__hipext_ocp_fp8_e4m3 one;
one.__x = 0x38;
for (int s : {-2, -1, 0, 1, 2, 3}) {
float expected = std::ldexp(1.0f, s); // 1.0 * 2^s
INFO("scale " << s);
REQUIRE(one.get_scaled_float(static_cast<__amd_scale_t>(s)) == expected);
REQUIRE(static_cast<float>(one.get_scaled_fp16(static_cast<__amd_scale_t>(s))) == expected);
REQUIRE(static_cast<float>(one.get_scaled_bf16(static_cast<__amd_scale_t>(s))) == expected);
}
}
/**
* Test Description
* ------------------------
* - fp8x2 packs two lanes and supports a non-SR scaled encode. Encoding
* (v0,v1) with scale s and decoding with the same scale is a round-trip when
* v/2^s is representable.
* Test source
* ------------------------
* - /unit/deviceLib/ext_ocp_fp8.cc
*/
HIP_TEST_CASE(Unit_ext_ocp_fp8x2_scaled_roundtrip_host) {
const int s = 1;
// base values are representable in e4m3/e5m2; v = base * 2^s.
std::vector<float2> vals = {float2(2.0f, 4.0f), float2(0.5f, 1.0f), float2(8.0f, 16.0f)};
SECTION("e4m3") {
for (auto v : vals) {
__hipext_ocp_fp8x2_e4m3 packed(__amd_floatx2_storage_t{v.x, v.y},
static_cast<__amd_scale_t>(s));
__amd_floatx2_storage_t out = packed.get_scaled_floatx2(static_cast<__amd_scale_t>(s));
INFO("in (" << v.x << "," << v.y << ") out (" << out[0] << "," << out[1] << ")");
REQUIRE(out[0] == v.x);
REQUIRE(out[1] == v.y);
}
}
SECTION("e5m2") {
for (auto v : vals) {
__hipext_ocp_fp8x2_e5m2 packed(__amd_floatx2_storage_t{v.x, v.y},
static_cast<__amd_scale_t>(s));
__amd_floatx2_storage_t out = packed.get_scaled_floatx2(static_cast<__amd_scale_t>(s));
INFO("in (" << v.x << "," << v.y << ") out (" << out[0] << "," << out[1] << ")");
REQUIRE(out[0] == v.x);
REQUIRE(out[1] == v.y);
}
}
}
/**
* Test Description
* ------------------------
* - Stochastic rounding: exactly representable inputs must be unchanged for any
* seed; non-representable inputs must round to one of the two nearest
* representable neighbours.
* Test source
* ------------------------
* - /unit/deviceLib/ext_ocp_fp8.cc
*/
HIP_TEST_CASE(Unit_ext_ocp_fp8_stochastic_rounding_host) {
std::vector<unsigned> seeds = {0u, 1u, 0x1234u, 0x80000000u, 0xdeadbeefu, 0xffffffffu};
auto reps = all_representable_fp8<true>(); // e4m3
SECTION("representable is stable under SR") {
for (float v : {0.5f, 1.0f, 2.0f, 4.0f}) {
__hipext_ocp_fp8_e4m3 exact(v);
for (unsigned seed : seeds) {
__hipext_ocp_fp8_e4m3 sr(v, seed);
INFO("v " << v << " seed 0x" << std::hex << seed);
REQUIRE(sr.__x == exact.__x);
}
}
}
SECTION("non-representable stays within neighbours") {
for (float v : {1.0625f, 3.3f, 5.7f, 0.6f}) {
auto [lo, hi] = bracket(reps, v);
for (unsigned seed : seeds) {
__hipext_ocp_fp8_e4m3 sr(v, seed);
float f = sr;
INFO("v " << v << " seed 0x" << std::hex << seed << " -> " << f << " lo " << lo << " hi "
<< hi);
REQUIRE((f == lo || f == hi));
}
}
}
}
/**
* Test Description
* ------------------------
* - Constructing an fp8 from fp16/bf16 sources yields the same encoding as the
* float source for representable values (SR is a no-op there).
* Test source
* ------------------------
* - /unit/deviceLib/ext_ocp_fp8.cc
*/
HIP_TEST_CASE(Unit_ext_ocp_fp8_fp16_bf16_source_host) {
const unsigned seed = 0u;
const __amd_scale_t scale = 0;
std::vector<float> vals = {0.5f, 1.0f, 1.5f, 2.0f, 4.0f};
for (float v : vals) {
__hipext_ocp_fp8_e4m3 from_float_v(v);
__amd_fp16_storage_t hv = static_cast<__amd_fp16_storage_t>(v);
__amd_bf16_storage_t bv = static_cast<__amd_bf16_storage_t>(v);
__hipext_ocp_fp8_e4m3 from_fp16(hv, seed, scale);
__hipext_ocp_fp8_e4m3 from_bf16(bv, seed, scale);
INFO("v " << v);
REQUIRE(from_fp16.__x == from_float_v.__x);
REQUIRE(from_bf16.__x == from_float_v.__x);
}
}
// ---- Device consistency -----------------------------------------------------
__global__ void fp8_e4m3_roundtrip_kernel(const float* in, float* out, size_t n) {
size_t i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
__hipext_ocp_fp8_e4m3 t(in[i]);
out[i] = t;
}
}
/**
* Test Description
* ------------------------
* - The device conversion path must return the same result as the host for the
* full set of representable e4m3 values.
* Test source
* ------------------------
* - /unit/deviceLib/ext_ocp_fp8.cc
*/
HIP_TEST_CASE(Unit_ext_ocp_fp8_device_matches_host) {
auto reps = all_representable_fp8<true>(); // decoded floats, all representable
size_t n = reps.size();
float* d_in = nullptr;
float* d_out = nullptr;
HIP_CHECK(hipMalloc(&d_in, sizeof(float) * n));
HIP_CHECK(hipMalloc(&d_out, sizeof(float) * n));
HIP_CHECK(hipMemcpy(d_in, reps.data(), sizeof(float) * n, hipMemcpyHostToDevice));
fp8_e4m3_roundtrip_kernel<<<(n / 256) + 1, 256>>>(d_in, d_out, n);
HIP_CHECK(hipGetLastError());
std::vector<float> dev(n, 0.0f);
HIP_CHECK(hipMemcpy(dev.data(), d_out, sizeof(float) * n, hipMemcpyDeviceToHost));
HIP_CHECK(hipDeviceSynchronize());
for (size_t i = 0; i < n; ++i) {
__hipext_ocp_fp8_e4m3 h(reps[i]);
float host = h;
INFO("in " << reps[i] << " host " << host << " dev " << dev[i]);
REQUIRE(dev[i] == host);
REQUIRE(dev[i] == reps[i]); // representable -> exact
}
HIP_CHECK(hipFree(d_in));
HIP_CHECK(hipFree(d_out));
}