blob: a85546ee8b0f6ab835fb436c62a4d15111f7ec96 [file] [log] [blame]
// Copyright (c) 2023-2024, ARM Limited.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "verify.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <doctest.h>
#include <array>
#include <iterator>
#include <limits>
#include <numeric>
#include <random>
#include <string>
#include <type_traits>
#include <vector>
namespace
{
void update_json_template(std::string& str, const std::string& find, const std::string& change)
{
// Update the 'str' by looking for instances of 'find' and replacing them with 'change'
auto pos = str.find(find);
while (pos != std::string::npos)
{
str.replace(pos, find.length(), change);
pos = str.find(find);
}
}
class TosaTensor
{
public:
TosaTensor(std::string name, tosa_datatype_t dataType, std::vector<int32_t> shape, uint8_t* data = nullptr)
: _name(std::move(name))
, _shape(std::move(shape))
{
_tensor.name = _name.c_str();
_tensor.data_type = dataType;
_tensor.num_dims = _shape.size();
_tensor.shape = _shape.data();
_tensor.data = data;
_tensor.size =
std::accumulate(_tensor.shape, std::next(_tensor.shape, _tensor.num_dims), 1, std::multiplies<>());
};
const tosa_tensor_t* cTensor() const
{
return &_tensor;
}
private:
std::string _name;
std::vector<int32_t> _shape;
tosa_tensor_t _tensor;
};
template <typename FP>
std::enable_if_t<std::is_floating_point_v<FP>, FP> increment(FP input, uint64_t steps)
{
for (uint64_t step = 0; step < steps; ++step)
input = std::nextafter(input, std::numeric_limits<FP>::infinity());
return input;
}
auto& getRandomGenerator()
{
static std::mt19937 gen(0);
return gen;
}
template <typename FP>
std::enable_if_t<std::is_floating_point_v<FP>, std::add_lvalue_reference_t<std::uniform_real_distribution<FP>>>
getUniformRealDist()
{
// Uniform real distribution generates real values in the range [a, b]
// and requires that b - a <= std::numeric_limits<FP>::max() so here
// we choose some arbitrary values that satisfy that condition.
constexpr auto min = std::numeric_limits<FP>::lowest() / 2;
constexpr auto max = std::numeric_limits<FP>::max() / 2;
static_assert(max <= std::numeric_limits<FP>::max() + min);
static std::uniform_real_distribution<FP> dis(min, max);
return dis;
}
template <typename FP>
std::enable_if_t<std::is_floating_point_v<FP>, FP> getRandomUniformFloat()
{
return getUniformRealDist<FP>()(getRandomGenerator());
}
template <typename FP>
std::enable_if_t<std::is_floating_point_v<FP>, std::vector<FP>> generateRandomTensorData(size_t elementCount,
bool includeNans = false)
{
// Generate some random floats using the full range of fp32.
auto data = std::vector<FP>(elementCount);
std::generate(std::begin(data), std::end(data), []() { return getRandomUniformFloat<FP>(); });
// Include some edge cases.
auto edgeCases = std::vector<float>{ +0.0f, -0.0f, std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::infinity() };
if (includeNans)
{
static const auto nans =
std::vector<float>{ std::numeric_limits<float>::quiet_NaN(), std::numeric_limits<float>::signaling_NaN() };
std::copy(std::begin(nans), std::end(nans), std::back_inserter(edgeCases));
}
if (elementCount >= edgeCases.size())
{
// Evenly distribute the edge cases throughout the data, this way for operations like reductions all edge cases won't
// end up in the same row/column over which a reduction happens.
const auto stride = (data.size() + (edgeCases.size() - 1)) / edgeCases.size();
for (unsigned i = 0; i < edgeCases.size(); ++i)
{
data[i * stride] = edgeCases[i];
}
}
return data;
}
// Calculates the "error" in the tolerance calculation as: E = pow(1 + pow(2, -M-1), N) - 1.
// where M is the number of mantisa bits in the floating point representation and N is the number
// of elements in the product.
constexpr auto reduceProductError(uint64_t M, uint64_t N)
{
return std::pow(1 + std::pow(2, -static_cast<int64_t>(M) - 1), N) - 1;
}
template <typename FP>
auto reduceProductTolerance(uint64_t M, uint64_t N, const std::vector<FP>& results)
{
const auto error = reduceProductError(M, N);
auto tolerances_fp64 = std::vector<FP>(results.size());
for (unsigned i = 0, end = results.size(); i < end; ++i)
{
tolerances_fp64[i] = std::abs(results[i]) * error;
}
return tolerances_fp64;
}
} // namespace
TEST_SUITE_BEGIN("verify");
TEST_CASE("negative - api")
{
std::string jsonCfg = R"({
"tensors" : {
"out1" : {
"mode": "DOT_PRODUCT",
"data_type": "FP32",
"dot_product_info" : {
"s": 2,
"ks": 9
}
}
}
})";
SUBCASE("invalid json")
{
std::string invalidJsonCfg = R"({
"tensors" : {
"out1" : {
"mode": DOT_PRODUCT,
},
}
})";
const TosaTensor ref("out1", tosa_datatype_fp64_t, { 8, 8, 8 });
const TosaTensor refAbs("out1", tosa_datatype_fp64_t, { 8, 8, 8 });
const TosaTensor imp("out1", tosa_datatype_fp32_t, { 8, 8, 8 });
REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), refAbs.cTensor(), imp.cTensor(), invalidJsonCfg.c_str()));
}
SUBCASE("unknown mode")
{
std::string unknownJsonCfg = R"({
"tensors" : {
"out1" : {
"mode": "WIND",
"data_type": "FP32"
}
}
})";
const TosaTensor ref("out1", tosa_datatype_fp64_t, { 8 });
const TosaTensor imp("out1", tosa_datatype_fp32_t, { 8 });
REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), nullptr, imp.cTensor(), unknownJsonCfg.c_str()));
}
SUBCASE("unknown type")
{
std::string unknownJsonCfg = R"({
"tensors" : {
"out1" : {
"mode": "DOT_PRODUCT",
"data_type": "JOULES"
}
}
})";
const TosaTensor ref("out1", tosa_datatype_fp64_t, { 8 });
const TosaTensor imp("out1", tosa_datatype_fp32_t, { 8 });
REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), nullptr, imp.cTensor(), unknownJsonCfg.c_str()));
}
SUBCASE("mismatching dimensions")
{
const TosaTensor ref("out1", tosa_datatype_fp64_t, { 4, 4 });
const TosaTensor refAbs("out1", tosa_datatype_fp64_t, { 4, 4 });
const TosaTensor imp("out1", tosa_datatype_fp32_t, { 8, 8, 8 });
REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), refAbs.cTensor(), imp.cTensor(), jsonCfg.c_str()));
}
SUBCASE("mismatching shapes")
{
const TosaTensor ref("out1", tosa_datatype_fp64_t, { 8, 8, 8 });
const TosaTensor refAbs("out1", tosa_datatype_fp64_t, { 8, 8, 8 });
const TosaTensor imp("out1", tosa_datatype_fp32_t, { 4, 4, 4 });
REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), refAbs.cTensor(), imp.cTensor(), jsonCfg.c_str()));
}
SUBCASE("mismatching data types")
{
const TosaTensor ref("out1", tosa_datatype_fp64_t, { 8, 8, 8 });
const TosaTensor refAbs("out1", tosa_datatype_fp64_t, { 8, 8, 8 });
const TosaTensor imp("out1", tosa_datatype_fp16_t, { 8, 8, 8 });
REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), refAbs.cTensor(), imp.cTensor(), jsonCfg.c_str()));
}
SUBCASE("missing tensor data")
{
const TosaTensor ref("out1", tosa_datatype_fp64_t, { 8, 8, 8 });
const TosaTensor refAbs("out1", tosa_datatype_fp64_t, { 8, 8, 8 });
const TosaTensor imp("out1", tosa_datatype_fp32_t, { 8, 8, 8 });
REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), refAbs.cTensor(), imp.cTensor(), jsonCfg.c_str()));
}
}
TEST_CASE("positive - exact")
{
std::string jsonCfg = R"({
"tensors" : {
"out1" : {
"mode": "EXACT",
"data_type": "FP32"
}
}
})";
const auto shape = std::vector<int32_t>{ 8, 8, 8 };
const auto elementCount = std::accumulate(std::begin(shape), std::end(shape), 1, std::multiplies<>());
// Generate some random floats using the full range of fp32.
auto data_fp32 = generateRandomTensorData<float>(elementCount);
std::vector<double> data_fp64(data_fp32.begin(), data_fp32.end());
SUBCASE("same")
{
const auto referenceTensor =
TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data_fp64.data()));
const auto implementationTensor =
TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(data_fp32.data()));
REQUIRE(tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), jsonCfg.c_str()));
}
SUBCASE("different")
{
// Generate some mismatched tensors by setting every other value to an incrementing counter.
// In theory this could be the same, but the probability is tiny.
auto otherData_fp32 = std::vector<float>(elementCount);
std::generate(std::begin(otherData_fp32), std::end(otherData_fp32), [&, i = 0]() mutable {
auto oldIndex = i++;
return oldIndex % 2 ? data_fp32[oldIndex] : static_cast<float>(oldIndex);
});
const auto referenceTensor =
TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data_fp64.data()));
const auto implementationTensor =
TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(otherData_fp32.data()));
REQUIRE_FALSE(
tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), jsonCfg.c_str()));
}
}
TEST_CASE("positive - reduce product")
{
std::string jsonCfg = R"({
"tensors" : {
"out1" : {
"mode": "REDUCE_PRODUCT",
"data_type": "FP32",
"reduce_product_info": {
"n": 8
}
}
}
})";
const auto inputShape = std::vector<int32_t>{ 8, 8, 8 };
const auto outputShape = std::vector<int32_t>{ 8, 8, 1 };
const auto reductionSize = inputShape[2];
const auto elementCount = std::accumulate(std::begin(inputShape), std::end(inputShape), 1, std::multiplies<>());
// Generate some random floats using the full range of fp32. This will be the "result" of our
// dot product. Here we "reduced" over the z-axis of our shape.
auto data_fp32 = generateRandomTensorData<float>(elementCount / reductionSize, false);
std::vector<double> data_fp64(data_fp32.begin(), data_fp32.end());
// Calculate the tolerances_fp64 for each element in the result.
// A float has 23 bit dedicated to the fraction.
constexpr uint64_t mantisa_count = 23;
const auto tolerances_fp64 = reduceProductTolerance(mantisa_count, reductionSize, data_fp64);
SUBCASE("same")
{
// Generate some new floats that are as far away as possible from each result without
// exceeding the tolerance.
auto otherData_fp32 = std::vector<float>(elementCount / reductionSize);
for (unsigned i = 0; i < data_fp32.size(); ++i)
{
auto newValue = data_fp32[i];
const double target = tolerances_fp64[i] + newValue;
// Here we just increment the value until we exceed the tolerance. For simplicity we go up.
auto previousValue = newValue;
while (newValue < target)
{
previousValue = newValue;
newValue = std::nextafter(newValue, std::numeric_limits<float>::infinity());
}
otherData_fp32[i] = previousValue;
}
const auto referenceTensor =
TosaTensor("out1", tosa_datatype_fp64_t, outputShape, reinterpret_cast<uint8_t*>(data_fp64.data()));
const auto implementationTensor =
TosaTensor("out1", tosa_datatype_fp32_t, outputShape, reinterpret_cast<uint8_t*>(otherData_fp32.data()));
REQUIRE(tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), jsonCfg.c_str()));
}
SUBCASE("different")
{
// Generate some new floats that exceed the tolerance.
auto otherData_fp32 = std::vector<float>(elementCount / reductionSize);
for (unsigned i = 0; i < data_fp32.size(); ++i)
{
auto newValue = data_fp32[i];
const double target = tolerances_fp64[i] + newValue;
// Here we just increment the value until we exceed the tolerance. For simplicity we go up.
while (newValue < target)
{
newValue = std::nextafter(newValue, std::numeric_limits<float>::infinity());
}
otherData_fp32[i] = newValue;
}
const auto referenceTensor =
TosaTensor("out1", tosa_datatype_fp64_t, outputShape, reinterpret_cast<uint8_t*>(data_fp64.data()));
const auto implementationTensor =
TosaTensor("out1", tosa_datatype_fp32_t, outputShape, reinterpret_cast<uint8_t*>(otherData_fp32.data()));
REQUIRE_FALSE(
tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), jsonCfg.c_str()));
}
}
TEST_CASE("positive - ulp")
{
std::string jsonCfg = R"({
"tensors" : {
"out1" : {
"mode": "ULP",
"data_type": "FP32",
"ulp_info": {
"ulp": 5
}
}
}
})";
const auto shape = std::vector<int32_t>{ 8, 8, 8 };
const auto elementCount = std::accumulate(std::begin(shape), std::end(shape), 1, std::multiplies<>());
// Generate some random floats using the full range of fp32.
auto data_fp32 = generateRandomTensorData<float>(elementCount, true);
std::vector<double> data_fp64(data_fp32.begin(), data_fp32.end());
SUBCASE("same")
{
// Generate some data that meets the ULP requirements of the result.
auto otherData_fp32 = data_fp32;
std::for_each(std::begin(otherData_fp32), std::end(otherData_fp32), [](auto& value) {
if (std::abs(value) != 0.0 && !std::isinf(value) && !std::isnan(value))
value = increment(value, 5);
});
const auto referenceTensor =
TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data_fp64.data()));
const auto implementationTensor =
TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(otherData_fp32.data()));
REQUIRE(tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), jsonCfg.c_str()));
}
SUBCASE("different")
{
// Generate some data that exceeds a specified number of ULP for each value in the tensor.
auto otherData_fp32 = data_fp32;
std::for_each(std::begin(otherData_fp32), std::end(otherData_fp32), [](auto& value) {
if (std::abs(value) != 0.0 && !std::isinf(value) && !std::isnan(value))
value = increment(value, 6);
});
const auto referenceTensor =
TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data_fp64.data()));
const auto implementationTensor =
TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(otherData_fp32.data()));
REQUIRE_FALSE(
tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), jsonCfg.c_str()));
}
}
TEST_CASE("positive - abs error")
{
std::string jsonCfg = R"({
"tensors" : {
"out1" : {
"mode": "ABS_ERROR",
"data_type": "FP32"
}
}
})";
const auto shape = std::vector<int32_t>{ 4, 4, 4 };
const auto elementCount = std::accumulate(std::begin(shape), std::end(shape), 1, std::multiplies<>());
// Generate some random floats using the full range of fp32.
auto data_fp32 = generateRandomTensorData<float>(elementCount, true);
std::vector<double> data_fp64(data_fp32.begin(), data_fp32.end());
// Set up simple bounds of the input to 2.0
std::vector<double> bounds_fp64(elementCount);
std::for_each(std::begin(bounds_fp64), std::end(bounds_fp64), [](auto& value) { value = 2.0; });
constexpr float insideErrBound = 1.0e-7 * 2; // v.approx exp2(-23) * bounds[]
constexpr float outsideErrBound = 1.0e-7 * 3;
SUBCASE("inside")
{
// Generate some data that meets the ABS_ERROR requirements of the result.
auto otherData_fp32 = data_fp32;
std::for_each(std::begin(otherData_fp32), std::end(otherData_fp32), [insideErrBound](auto& value) {
if (std::abs(value) != 0.0 && !std::isinf(value) && !std::isnan(value))
value += value * insideErrBound;
});
const auto referenceTensor =
TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data_fp64.data()));
const auto boundsTensor =
TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(bounds_fp64.data()));
const auto implementationTensor =
TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(otherData_fp32.data()));
REQUIRE(tvf_verify_data(referenceTensor.cTensor(), boundsTensor.cTensor(), implementationTensor.cTensor(),
jsonCfg.c_str()));
}
SUBCASE("outside")
{
// Generate some data that exceeds a requirements for each value in the tensor.
auto otherData_fp32 = data_fp32;
std::for_each(std::begin(otherData_fp32), std::end(otherData_fp32), [outsideErrBound](auto& value) {
if (std::abs(value) != 0.0 && !std::isinf(value) && !std::isnan(value))
value += value * outsideErrBound;
});
const auto referenceTensor =
TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data_fp64.data()));
const auto boundsTensor =
TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(bounds_fp64.data()));
const auto implementationTensor =
TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(otherData_fp32.data()));
REQUIRE_FALSE(tvf_verify_data(referenceTensor.cTensor(), boundsTensor.cTensor(), implementationTensor.cTensor(),
jsonCfg.c_str()));
}
}
TEST_CASE("positive - relative")
{
std::string templateJsonCfg = R"({
"tensors" : {
"out1" : {
"mode": "RELATIVE",
"data_type": "FP32",
"relative_info": {
"max": _MAXIMUM_,
"scale": _SCALE_
}
}
}
})";
const auto shape = std::vector<int32_t>{ 3, 3, 3 };
const auto elementCount = std::accumulate(std::begin(shape), std::end(shape), 1, std::multiplies<>());
// Generate some random floats using the full range of fp32.
auto data_fp32 = generateRandomTensorData<float>(elementCount, true);
std::vector<double> data_fp64(data_fp32.begin(), data_fp32.end());
float scale = 0.0006;
float max = 0.0;
std::for_each(std::begin(data_fp32), std::end(data_fp32), [&max](auto& value) {
if (!std::isinf(value) && !std::isnan(value))
{
max = std::max(max, std::abs(value));
}
});
std::string jsonCfg = templateJsonCfg;
update_json_template(jsonCfg, "_MAXIMUM_", std::to_string(max));
update_json_template(jsonCfg, "_SCALE_", std::to_string(scale));
float errBound = max * scale;
// Use 10% error margin to test due to using v.large values in our random data
float insideErrBound = errBound * 0.9;
float outsideErrBound = errBound * 1.1;
SUBCASE("inside")
{
// Generate some data that meets the requirements of the result.
auto otherData_fp32 = data_fp32;
std::for_each(std::begin(otherData_fp32), std::end(otherData_fp32), [insideErrBound](auto& value) {
if (std::abs(value) != 0.0 && !std::isinf(value) && !std::isnan(value))
value += insideErrBound;
});
const auto referenceTensor =
TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data_fp64.data()));
const auto implementationTensor =
TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(otherData_fp32.data()));
REQUIRE(tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), jsonCfg.c_str()));
}
SUBCASE("outside")
{
// Generate some data that exceeds the requirements for each value in the tensor.
auto otherData_fp32 = data_fp32;
std::for_each(std::begin(otherData_fp32), std::end(otherData_fp32), [outsideErrBound](auto& value) {
if (std::abs(value) != 0.0 && !std::isinf(value) && !std::isnan(value))
value += outsideErrBound;
});
const auto referenceTensor =
TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data_fp64.data()));
const auto implementationTensor =
TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(otherData_fp32.data()));
REQUIRE_FALSE(
tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), jsonCfg.c_str()));
}
}
TEST_SUITE_END(); // verify