mirror of
https://github.com/opencv/opencv.git
synced 2026-09-11 21:01:33 -05:00
SIMD support for FP8 - #29832 ### Pull Request Readiness Checklist See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request - [x] I agree to contribute to the project under Apache 2 License. - [x] To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV - [x] The PR is proposed to the proper branch - [x] There is a reference to the original bug report and related work - [x] There is accuracy test, performance test and test data in opencv_extra repository, if applicable Patch to opencv_extra has the same branch name. - [x] The feature is well documented and sample code can be built with the project CMake
258 lines
9.1 KiB
C++
258 lines
9.1 KiB
C++
// This file is part of OpenCV project.
|
|
// It is subject to the license terms in the LICENSE file found in the top-level directory
|
|
// of this distribution and at http://opencv.org/license.html.
|
|
|
|
#include "test_precomp.hpp"
|
|
|
|
namespace opencv_test { namespace {
|
|
|
|
// The two FP8 depths and their wrapper types share one set of expectations.
|
|
// Values chosen to be exactly representable (so round-trips are bit-exact) plus
|
|
// the special/overflow cases that distinguish the formats.
|
|
|
|
TEST(Core_FP8, type_basics)
|
|
{
|
|
const int depths[] = { CV_8F_E4M3FN, CV_8F_E4M3FNUZ };
|
|
for (int d : depths)
|
|
{
|
|
EXPECT_EQ(CV_ELEM_SIZE1(d), 1) << "depth " << d;
|
|
Mat m(3, 4, CV_MAKETYPE(d, 1));
|
|
EXPECT_EQ(m.depth(), d);
|
|
EXPECT_EQ(m.channels(), 1);
|
|
EXPECT_EQ(m.elemSize(), (size_t)1);
|
|
EXPECT_EQ(m.elemSize1(), (size_t)1);
|
|
EXPECT_EQ(m.total(), (size_t)12);
|
|
// depthToString should not return null for a registered depth
|
|
EXPECT_NE(cv::depthToString(d), (const char*)NULL);
|
|
}
|
|
Mat c3(2, 2, CV_8FC(3));
|
|
EXPECT_EQ(c3.channels(), 3);
|
|
EXPECT_EQ(c3.elemSize(), (size_t)3);
|
|
}
|
|
|
|
TEST(Core_FP8, scalar_roundtrip_exact)
|
|
{
|
|
// {0, .5, 1, 1.5, 2, 3, 4, 6} and negatives are exact in every FP8 format here.
|
|
const float exact[] = { 0.f, 0.5f, 1.f, 1.5f, 2.f, 3.f, 4.f, 6.f, -2.5f, -0.75f };
|
|
for (float v : exact)
|
|
{
|
|
EXPECT_EQ((float)cv::fp8_t(v), v) << v;
|
|
EXPECT_EQ((float)cv::fp8a_t(v), v) << v;
|
|
}
|
|
// round-to-nearest-even onto the grid
|
|
EXPECT_EQ((float)cv::fp8_t(1.234f), 1.25f); // 3 mantissa bits
|
|
}
|
|
|
|
TEST(Core_FP8, format_specific_limits)
|
|
{
|
|
// max finite values
|
|
EXPECT_EQ((float)cv::fp8_t(448.f), 448.f);
|
|
EXPECT_EQ((float)cv::fp8a_t(240.f), 240.f);
|
|
|
|
// overflow: these formats have no inf -> overflow to NaN
|
|
EXPECT_TRUE(cvIsNaN((float)cv::fp8_t(1e6f)));
|
|
EXPECT_TRUE(cvIsNaN((float)cv::fp8a_t(1e6f)));
|
|
// 448 exceeds the FNUZ E4M3 range (max 240) -> NaN
|
|
EXPECT_TRUE(cvIsNaN((float)cv::fp8a_t(448.f)));
|
|
|
|
// NaN propagates
|
|
EXPECT_TRUE(cvIsNaN((float)cv::fp8_t(std::numeric_limits<float>::quiet_NaN())));
|
|
|
|
// smallest E4M3FN subnormal is 2^-9
|
|
EXPECT_EQ((float)cv::fp8_t(0.001953125f), 0.001953125f);
|
|
}
|
|
|
|
TEST(Core_FP8, mat_convert_roundtrip)
|
|
{
|
|
float vals[] = { 0.f, 0.5f, 1.f, 1.5f, 2.f, 3.f, 4.f, 6.f, -1.f, -4.f };
|
|
Mat f(1, 10, CV_32F, vals);
|
|
const int depths[] = { CV_8F_E4M3FN, CV_8F_E4M3FNUZ };
|
|
for (int d : depths)
|
|
{
|
|
Mat q, back;
|
|
f.convertTo(q, d);
|
|
EXPECT_EQ(q.depth(), d);
|
|
EXPECT_EQ(q.elemSize(), (size_t)1);
|
|
q.convertTo(back, CV_32F);
|
|
ASSERT_EQ(back.type(), CV_32FC1);
|
|
for (int i = 0; i < 10; i++)
|
|
EXPECT_EQ(back.at<float>(i), vals[i]) << "depth " << d << " idx " << i;
|
|
}
|
|
}
|
|
|
|
TEST(Core_FP8, convert_from_and_to_other_types)
|
|
{
|
|
// f16 -> fp8 -> f32 (f16 source is lossless into the conversion)
|
|
Mat f32(1, 5, CV_32F);
|
|
float v[] = { 0.5f, 1.f, 2.f, 4.f, -3.f };
|
|
memcpy(f32.data, v, sizeof(v));
|
|
Mat f16; f32.convertTo(f16, CV_16F);
|
|
Mat q; f16.convertTo(q, CV_8F_E4M3FN);
|
|
Mat back; q.convertTo(back, CV_32F);
|
|
for (int i = 0; i < 5; i++)
|
|
EXPECT_EQ(back.at<float>(i), v[i]);
|
|
|
|
// fp8 -> int (saturate_cast rounds to nearest)
|
|
Mat qi; f32.convertTo(qi, CV_8F_E4M3FN);
|
|
Mat i32; qi.convertTo(i32, CV_32S);
|
|
EXPECT_EQ(i32.at<int>(0), 0); // 0.5 -> 0 (round to even)
|
|
EXPECT_EQ(i32.at<int>(1), 1);
|
|
EXPECT_EQ(i32.at<int>(2), 2);
|
|
EXPECT_EQ(i32.at<int>(3), 4);
|
|
EXPECT_EQ(i32.at<int>(4), -3);
|
|
}
|
|
|
|
TEST(Core_FP8, cross_fp8_conversion)
|
|
{
|
|
float v[] = { 0.5f, 1.5f, 6.f, 100.f, -2.f };
|
|
Mat f(1, 5, CV_32F, v);
|
|
Mat e4m3, e4m3u, back;
|
|
f.convertTo(e4m3, CV_8F_E4M3FN);
|
|
e4m3.convertTo(e4m3u, CV_8F_E4M3FNUZ); // FP8 -> FP8
|
|
e4m3u.convertTo(back, CV_32F);
|
|
// values <=6 are representable in both grids -> preserved exactly
|
|
EXPECT_EQ(back.at<float>(0), 0.5f);
|
|
EXPECT_EQ(back.at<float>(1), 1.5f);
|
|
EXPECT_EQ(back.at<float>(2), 6.f);
|
|
EXPECT_EQ(back.at<float>(4), -2.f);
|
|
}
|
|
|
|
TEST(Core_FP8, convert_scale)
|
|
{
|
|
Mat f = (Mat_<float>(1, 4) << 1.f, 2.f, 3.f, 4.f);
|
|
Mat q, back;
|
|
f.convertTo(q, CV_8F_E4M3FN, 2.0, 1.0); // 2x+1 -> {3,5,7,9}
|
|
q.convertTo(back, CV_32F);
|
|
EXPECT_EQ(back.at<float>(0), 3.f); // 1.5*2, exact
|
|
EXPECT_EQ(back.at<float>(1), 5.f); // 1.25*4, exact
|
|
EXPECT_EQ(back.at<float>(2), 7.f); // 1.75*4, exact
|
|
EXPECT_EQ(back.at<float>(3), 9.f); // 9 = 1.125*8 is exact in E4M3 (3 mantissa bits)
|
|
}
|
|
|
|
TEST(Core_FP8, set_scalar)
|
|
{
|
|
Mat m(3, 3, CV_8F_E4M3FN);
|
|
m.setTo(Scalar(2.5));
|
|
Mat back; m.convertTo(back, CV_32F);
|
|
for (int i = 0; i < 9; i++)
|
|
EXPECT_EQ(back.at<float>(i), 2.5f);
|
|
|
|
Mat z = Mat::zeros(2, 2, CV_8F_E4M3FNUZ);
|
|
Mat zf; z.convertTo(zf, CV_32F);
|
|
EXPECT_EQ(countNonZero(zf), 0);
|
|
}
|
|
|
|
// both fp8 flavors <-> every other depth, both directions; values exact in all types
|
|
TEST(Core_FP8, convert_all_depths)
|
|
{
|
|
const int fp8[] = { CV_8F, CV_8F_E4M3FNUZ };
|
|
const int others[] = { CV_8U, CV_8S, CV_16U, CV_16S, CV_32S, CV_32F,
|
|
CV_64F, CV_16F, CV_16BF, CV_64U, CV_64S, CV_32U };
|
|
float vals[] = { 0.f, 1.f, 2.f, 3.f, 4.f, 6.f };
|
|
Mat f(1, 6, CV_32F, vals);
|
|
for (int d : fp8)
|
|
for (int o : others)
|
|
{
|
|
Mat q, viaO, back;
|
|
f.convertTo(q, d); q.convertTo(viaO, o); viaO.convertTo(back, CV_32F);
|
|
Mat so, q2, back2;
|
|
f.convertTo(so, o); so.convertTo(q2, d); q2.convertTo(back2, CV_32F);
|
|
for (int i = 0; i < 6; i++)
|
|
{
|
|
EXPECT_EQ(back.at<float>(i), vals[i]) << "fp8 " << d << " -> " << o << " idx " << i;
|
|
EXPECT_EQ(back2.at<float>(i), vals[i]) << o << " -> fp8 " << d << " idx " << i;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wide stress buffer to exercise the SIMD encode path and its scalar tail.
|
|
static std::vector<float> fp8EncodeStressValues()
|
|
{
|
|
std::vector<float> vals;
|
|
|
|
// every representable FP8 value in both formats, round-tripped through float32
|
|
for (int b = 0; b < 256; b++) vals.push_back(fp8_t::decodeLUT()[b]);
|
|
for (int b = 0; b < 256; b++) vals.push_back(fp8a_t::decodeLUT()[b]);
|
|
|
|
// dense sweep across the normal range, both signs, crossing every rounding boundary
|
|
for (int i = -20000; i <= 20000; i++)
|
|
vals.push_back(i * 0.031f);
|
|
|
|
// geometric sweep from subnormal-FP8 through overflow-to-NaN
|
|
for (int e = -30; e <= 30; e++)
|
|
for (int m = 0; m < 8; m++)
|
|
vals.push_back((float)(std::ldexp(1.0 + m / 8.0, e)));
|
|
|
|
float specials[] = {
|
|
0.f, -0.f,
|
|
std::numeric_limits<float>::infinity(), -std::numeric_limits<float>::infinity(),
|
|
std::numeric_limits<float>::quiet_NaN(),
|
|
std::numeric_limits<float>::denorm_min(), -std::numeric_limits<float>::denorm_min(),
|
|
std::numeric_limits<float>::max(), -std::numeric_limits<float>::max(),
|
|
std::numeric_limits<float>::min(), 1e-40f, -1e-40f,
|
|
};
|
|
vals.insert(vals.end(), std::begin(specials), std::end(specials));
|
|
return vals;
|
|
}
|
|
|
|
template<typename FP8>
|
|
static void checkFp8EncodeMatchesScalar(const std::vector<float>& vals)
|
|
{
|
|
Mat f(1, (int)vals.size(), CV_32F, (void*)vals.data());
|
|
Mat q;
|
|
f.convertTo(q, DataType<FP8>::depth);
|
|
ASSERT_EQ(q.total(), vals.size());
|
|
const uchar* qd = q.ptr<uchar>();
|
|
for (size_t i = 0; i < vals.size(); i++)
|
|
{
|
|
FP8 ref(vals[i]);
|
|
uchar refByte = *reinterpret_cast<const uchar*>(&ref);
|
|
ASSERT_EQ(qd[i], refByte) << "value " << vals[i] << " (idx " << i << ")";
|
|
}
|
|
}
|
|
|
|
TEST(Core_FP8, simd_encode_matches_scalar)
|
|
{
|
|
std::vector<float> vals = fp8EncodeStressValues();
|
|
checkFp8EncodeMatchesScalar<fp8_t>(vals);
|
|
checkFp8EncodeMatchesScalar<fp8a_t>(vals);
|
|
}
|
|
|
|
// Scale path uses a different kernel than the identity path above; check separately.
|
|
template<typename FP8>
|
|
static void checkFp8ScaleEncodeMatchesScalar(const std::vector<float>& vals, double alpha, double beta)
|
|
{
|
|
Mat f(1, (int)vals.size(), CV_32F, (void*)vals.data());
|
|
Mat q;
|
|
f.convertTo(q, DataType<FP8>::depth, alpha, beta);
|
|
ASSERT_EQ(q.total(), vals.size());
|
|
const uchar* qd = q.ptr<uchar>();
|
|
for (size_t i = 0; i < vals.size(); i++)
|
|
{
|
|
float scaled = (float)((double)vals[i]*alpha + beta);
|
|
FP8 ref(scaled);
|
|
uchar refByte = *reinterpret_cast<const uchar*>(&ref);
|
|
ASSERT_EQ(qd[i], refByte) << "value " << vals[i] << " * " << alpha << " + " << beta
|
|
<< " = " << scaled << " (idx " << i << ")";
|
|
}
|
|
}
|
|
|
|
TEST(Core_FP8, simd_scale_encode_matches_scalar)
|
|
{
|
|
std::vector<float> vals = fp8EncodeStressValues();
|
|
// representative quantization scales: shrink, grow, shift-only, negate
|
|
double alphas[] = { 1.0, 0.015625, 64.0, -1.0 };
|
|
double betas[] = { 0.0, 0.5, -3.25 };
|
|
for (double alpha : alphas)
|
|
for (double beta : betas)
|
|
{
|
|
// alpha=1,beta=0 is convertTo's noScale case; skips the scale kernel entirely.
|
|
if (alpha == 1.0 && beta == 0.0)
|
|
continue;
|
|
checkFp8ScaleEncodeMatchesScalar<fp8_t>(vals, alpha, beta);
|
|
checkFp8ScaleEncodeMatchesScalar<fp8a_t>(vals, alpha, beta);
|
|
}
|
|
}
|
|
|
|
}} // namespace
|