Georgios Pinitas | 41df428 | 2023-05-30 12:20:31 +0100 | [diff] [blame] | 1 | // Copyright (c) 2023, ARM Limited. |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | #include "verify.h" |
| 15 | |
Jack Frankland | aafc850 | 2023-09-13 11:03:50 +0100 | [diff] [blame] | 16 | #include <algorithm> |
Jack Frankland | 62737b1 | 2023-09-13 15:47:48 +0100 | [diff] [blame] | 17 | #include <cmath> |
| 18 | #include <cstdint> |
Georgios Pinitas | 41df428 | 2023-05-30 12:20:31 +0100 | [diff] [blame] | 19 | #include <doctest.h> |
| 20 | |
| 21 | #include <array> |
Jack Frankland | aafc850 | 2023-09-13 11:03:50 +0100 | [diff] [blame] | 22 | #include <iterator> |
| 23 | #include <limits> |
| 24 | #include <numeric> |
| 25 | #include <random> |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 26 | #include <string> |
Jack Frankland | aafc850 | 2023-09-13 11:03:50 +0100 | [diff] [blame] | 27 | #include <type_traits> |
Georgios Pinitas | 41df428 | 2023-05-30 12:20:31 +0100 | [diff] [blame] | 28 | #include <vector> |
| 29 | |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 30 | namespace |
Georgios Pinitas | 41df428 | 2023-05-30 12:20:31 +0100 | [diff] [blame] | 31 | { |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 32 | |
| 33 | class TosaTensor |
| 34 | { |
| 35 | public: |
Jack Frankland | aafc850 | 2023-09-13 11:03:50 +0100 | [diff] [blame] | 36 | TosaTensor(std::string name, tosa_datatype_t dataType, std::vector<int32_t> shape, uint8_t* data = nullptr) |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 37 | : _name(std::move(name)) |
| 38 | , _shape(std::move(shape)) |
Georgios Pinitas | 41df428 | 2023-05-30 12:20:31 +0100 | [diff] [blame] | 39 | { |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 40 | _tensor.name = _name.c_str(); |
| 41 | _tensor.data_type = dataType; |
| 42 | _tensor.num_dims = _shape.size(); |
| 43 | _tensor.shape = _shape.data(); |
Jack Frankland | aafc850 | 2023-09-13 11:03:50 +0100 | [diff] [blame] | 44 | _tensor.data = data; |
| 45 | _tensor.size = |
| 46 | std::accumulate(_tensor.shape, std::next(_tensor.shape, _tensor.num_dims), 1, std::multiplies<>()); |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 47 | }; |
Georgios Pinitas | 41df428 | 2023-05-30 12:20:31 +0100 | [diff] [blame] | 48 | |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 49 | const tosa_tensor_t* cTensor() const |
| 50 | { |
| 51 | return &_tensor; |
Georgios Pinitas | 41df428 | 2023-05-30 12:20:31 +0100 | [diff] [blame] | 52 | } |
Georgios Pinitas | 41df428 | 2023-05-30 12:20:31 +0100 | [diff] [blame] | 53 | |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 54 | private: |
| 55 | std::string _name; |
| 56 | std::vector<int32_t> _shape; |
| 57 | tosa_tensor_t _tensor; |
| 58 | }; |
| 59 | |
Jack Frankland | 62737b1 | 2023-09-13 15:47:48 +0100 | [diff] [blame] | 60 | template <typename FP> |
| 61 | std::enable_if_t<std::is_floating_point_v<FP>, FP> increment(FP input, uint64_t steps) |
| 62 | { |
| 63 | for (uint64_t step = 0; step < steps; ++step) |
| 64 | input = std::nextafter(input, std::numeric_limits<FP>::infinity()); |
| 65 | return input; |
| 66 | } |
| 67 | |
Jack Frankland | aafc850 | 2023-09-13 11:03:50 +0100 | [diff] [blame] | 68 | auto& getRandomGenerator() |
| 69 | { |
| 70 | static std::mt19937 gen(0); |
| 71 | return gen; |
| 72 | } |
| 73 | |
| 74 | template <typename FP> |
| 75 | std::enable_if_t<std::is_floating_point_v<FP>, std::add_lvalue_reference_t<std::uniform_real_distribution<FP>>> |
| 76 | getUniformRealDist() |
| 77 | { |
| 78 | // Uniform real distribution generates real values in the range [a, b) |
| 79 | // and requires that b - a <= std::numeric_limits<FP>::max() so here |
| 80 | // we choose some arbitrary values that satisfy that condition. |
| 81 | constexpr auto min = std::numeric_limits<FP>::lowest() / 2; |
| 82 | constexpr auto max = std::numeric_limits<FP>::max() / 2; |
| 83 | static_assert(max <= std::numeric_limits<FP>::max() + min); |
| 84 | |
| 85 | static std::uniform_real_distribution<FP> dis(min, max); |
| 86 | return dis; |
| 87 | } |
| 88 | |
| 89 | template <typename FP> |
| 90 | std::enable_if_t<std::is_floating_point_v<FP>, FP> getRandomUniformFloat() |
| 91 | { |
| 92 | return getUniformRealDist<FP>()(getRandomGenerator()); |
| 93 | } |
| 94 | |
| 95 | template <typename FP> |
| 96 | std::enable_if_t<std::is_floating_point_v<FP>, std::vector<FP>> generateRandomTensorData(size_t elementCount, |
| 97 | bool includeNans = false) |
| 98 | { |
| 99 | // Generate some random floats using the full range of fp32. |
| 100 | auto data = std::vector<FP>(elementCount); |
| 101 | std::generate(std::begin(data), std::end(data), []() { return getRandomUniformFloat<FP>(); }); |
| 102 | |
| 103 | // Include some edge cases. |
| 104 | auto edgeCases = std::vector<float>{ +0.0f, -0.0f, std::numeric_limits<float>::infinity(), |
| 105 | -std::numeric_limits<float>::infinity() }; |
| 106 | if (includeNans) |
| 107 | { |
| 108 | static const auto nans = |
| 109 | std::vector<float>{ std::numeric_limits<float>::quiet_NaN(), std::numeric_limits<float>::signaling_NaN() }; |
| 110 | |
| 111 | std::copy(std::begin(nans), std::end(nans), std::back_inserter(edgeCases)); |
| 112 | } |
| 113 | |
| 114 | if (elementCount >= edgeCases.size()) |
| 115 | { |
| 116 | // Evenly distribute the edge cases throughout the data, this way for operations like reductions all edge cases won't |
| 117 | // end up in the same row/column over which a reduction happens. |
| 118 | const auto stride = (data.size() + (edgeCases.size() - 1)) / edgeCases.size(); |
| 119 | for (unsigned i = 0; i < edgeCases.size(); ++i) |
| 120 | { |
| 121 | data[i * stride] = edgeCases[i]; |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | return data; |
| 126 | } |
| 127 | |
Jack Frankland | 12ee1a7 | 2023-09-20 09:08:34 +0100 | [diff] [blame^] | 128 | // Calculates the "error" in the tolerance calculation as: E = pow(1 + pow(2, -M-1), N) - 1. |
| 129 | // where M is the number of mantisa bits in the floating point representation and N is the number |
| 130 | // of elements in the product. |
| 131 | constexpr auto reduceProductError(uint64_t M, uint64_t N) |
| 132 | { |
| 133 | return std::pow(1 + std::pow(2, -static_cast<int64_t>(M) - 1), N) - 1; |
| 134 | } |
| 135 | |
| 136 | template <typename FP> |
| 137 | auto reduceProductTolerance(uint64_t M, uint64_t N, const std::vector<FP>& results) |
| 138 | { |
| 139 | const auto error = reduceProductError(M, N); |
| 140 | auto tolerances = std::vector<FP>(results.size()); |
| 141 | for (unsigned i = 0, end = results.size(); i < end; ++i) |
| 142 | { |
| 143 | tolerances[i] = std::abs(results[i]) * error; |
| 144 | } |
| 145 | return tolerances; |
| 146 | } |
| 147 | |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 148 | } // namespace |
| 149 | |
| 150 | TEST_SUITE_BEGIN("verify"); |
| 151 | |
| 152 | TEST_CASE("negative - api") |
| 153 | { |
| 154 | std::string json_cfg = R"({ |
| 155 | "tensors" : { |
| 156 | "out1" : { |
| 157 | "mode": "DOT_PRODUCT", |
Jeremy Johnson | bb0935f | 2023-09-14 16:43:48 +0100 | [diff] [blame] | 158 | "data_type": "FP32", |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 159 | "dot_product_info" : { |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 160 | "s": 2, |
| 161 | "ks": 9 |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | })"; |
| 166 | |
| 167 | SUBCASE("invalid json") |
| 168 | { |
| 169 | std::string invalid_json_cfg = R"({ |
| 170 | "tensors" : { |
| 171 | "out1" : { |
| 172 | "mode": DOT_PRODUCT, |
| 173 | }, |
| 174 | } |
| 175 | })"; |
| 176 | |
| 177 | const TosaTensor ref("out1", tosa_datatype_fp64_t, { 8, 8, 8 }); |
| 178 | const TosaTensor refAbs("out1", tosa_datatype_fp64_t, { 8, 8, 8 }); |
| 179 | const TosaTensor imp("out1", tosa_datatype_fp32_t, { 8, 8, 8 }); |
| 180 | |
| 181 | REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), refAbs.cTensor(), imp.cTensor(), invalid_json_cfg.c_str())); |
| 182 | } |
| 183 | SUBCASE("mismatching dimensions") |
| 184 | { |
| 185 | const TosaTensor ref("out1", tosa_datatype_fp64_t, { 4, 4 }); |
| 186 | const TosaTensor refAbs("out1", tosa_datatype_fp64_t, { 4, 4 }); |
| 187 | const TosaTensor imp("out1", tosa_datatype_fp32_t, { 8, 8, 8 }); |
| 188 | |
| 189 | REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), refAbs.cTensor(), imp.cTensor(), json_cfg.c_str())); |
| 190 | } |
| 191 | SUBCASE("mismatching shapes") |
| 192 | { |
| 193 | const TosaTensor ref("out1", tosa_datatype_fp64_t, { 8, 8, 8 }); |
| 194 | const TosaTensor refAbs("out1", tosa_datatype_fp64_t, { 8, 8, 8 }); |
| 195 | const TosaTensor imp("out1", tosa_datatype_fp32_t, { 4, 4, 4 }); |
| 196 | |
| 197 | REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), refAbs.cTensor(), imp.cTensor(), json_cfg.c_str())); |
| 198 | } |
| 199 | SUBCASE("mismatching data types") |
| 200 | { |
| 201 | const TosaTensor ref("out1", tosa_datatype_fp64_t, { 8, 8, 8 }); |
| 202 | const TosaTensor refAbs("out1", tosa_datatype_fp64_t, { 8, 8, 8 }); |
| 203 | const TosaTensor imp("out1", tosa_datatype_fp16_t, { 8, 8, 8 }); |
| 204 | |
| 205 | REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), refAbs.cTensor(), imp.cTensor(), json_cfg.c_str())); |
| 206 | } |
| 207 | SUBCASE("missing tensor data") |
| 208 | { |
| 209 | const TosaTensor ref("out1", tosa_datatype_fp64_t, { 8, 8, 8 }); |
| 210 | const TosaTensor refAbs("out1", tosa_datatype_fp64_t, { 8, 8, 8 }); |
| 211 | const TosaTensor imp("out1", tosa_datatype_fp32_t, { 8, 8, 8 }); |
| 212 | |
| 213 | REQUIRE_FALSE(tvf_verify_data(ref.cTensor(), refAbs.cTensor(), imp.cTensor(), json_cfg.c_str())); |
Georgios Pinitas | 41df428 | 2023-05-30 12:20:31 +0100 | [diff] [blame] | 214 | } |
| 215 | } |
Georgios Pinitas | 7021ef0 | 2023-08-22 08:25:57 +0100 | [diff] [blame] | 216 | |
Jack Frankland | aafc850 | 2023-09-13 11:03:50 +0100 | [diff] [blame] | 217 | TEST_CASE("positive - exact") |
| 218 | { |
| 219 | std::string json_cfg = R"({ |
| 220 | "tensors" : { |
| 221 | "out1" : { |
Jeremy Johnson | bb0935f | 2023-09-14 16:43:48 +0100 | [diff] [blame] | 222 | "mode": "EXACT", |
| 223 | "data_type": "FP32" |
Jack Frankland | aafc850 | 2023-09-13 11:03:50 +0100 | [diff] [blame] | 224 | } |
| 225 | } |
| 226 | })"; |
| 227 | |
| 228 | const auto shape = std::vector<int32_t>{ 8, 8, 8 }; |
| 229 | const auto elementCount = std::accumulate(std::begin(shape), std::end(shape), 1, std::multiplies<>()); |
| 230 | |
| 231 | // Generate some random floats using the full range of fp32. |
| 232 | auto data = generateRandomTensorData<float>(elementCount); |
| 233 | SUBCASE("same") |
| 234 | { |
| 235 | const auto referenceTensor = |
| 236 | TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data.data())); |
| 237 | const auto implementationTensor = |
| 238 | TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(data.data())); |
| 239 | REQUIRE(tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), json_cfg.c_str())); |
| 240 | } |
| 241 | |
| 242 | SUBCASE("different") |
| 243 | { |
| 244 | // Generate some mismatched tensors by setting every other value to an incrementing counter. |
| 245 | // In theory this could be the same, but the probability is tiny. |
| 246 | auto otherData = std::vector<float>(elementCount); |
| 247 | std::generate(std::begin(otherData), std::end(otherData), [&, i = 0]() mutable { |
| 248 | auto oldIndex = i++; |
| 249 | return oldIndex % 2 ? data[oldIndex] : static_cast<float>(oldIndex); |
| 250 | }); |
| 251 | |
| 252 | const auto referenceTensor = |
| 253 | TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data.data())); |
| 254 | const auto implementationTensor = |
| 255 | TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(otherData.data())); |
| 256 | REQUIRE_FALSE( |
| 257 | tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), json_cfg.c_str())); |
| 258 | } |
| 259 | } |
| 260 | |
Jack Frankland | 12ee1a7 | 2023-09-20 09:08:34 +0100 | [diff] [blame^] | 261 | TEST_CASE("positive - reduce product") |
| 262 | { |
| 263 | std::string json_cfg = R"({ |
| 264 | "tensors" : { |
| 265 | "out1" : { |
| 266 | "mode": "REDUCE_PRODUCT", |
| 267 | "reduce_product_info": { |
| 268 | "m": 23, |
| 269 | "n": 8 |
| 270 | } |
| 271 | } |
| 272 | } |
| 273 | })"; |
| 274 | |
| 275 | const auto inputShape = std::vector<int32_t>{ 8, 8, 8 }; |
| 276 | const auto outputShape = std::vector<int32_t>{ 8, 8, 1 }; |
| 277 | const auto reductionSize = inputShape[2]; |
| 278 | const auto elementCount = std::accumulate(std::begin(inputShape), std::end(inputShape), 1, std::multiplies<>()); |
| 279 | |
| 280 | // Generate some random floats using the full range of fp32. This will be the "result" of our |
| 281 | // dot product. Here we "reduced" over the z-axis of our shape. |
| 282 | auto data = generateRandomTensorData<float>(elementCount / reductionSize, false); |
| 283 | // Calculate the tolerances for each element in the result. |
| 284 | // A float has 23 bit dedicated to the fraction. |
| 285 | constexpr uint64_t mantisa_count = 23; |
| 286 | const auto tolerances = reduceProductTolerance(mantisa_count, reductionSize, data); |
| 287 | |
| 288 | SUBCASE("same") |
| 289 | { |
| 290 | // TODO: Generate some new floats that are as far away as possible from each result without |
| 291 | // exceeding the tolerance. |
| 292 | auto otherData = std::vector<float>(elementCount / reductionSize); |
| 293 | for (unsigned i = 0; i < data.size(); ++i) |
| 294 | { |
| 295 | auto newValue = data[i]; |
| 296 | auto oldValue = newValue; |
| 297 | const auto target = tolerances[i] + newValue; |
| 298 | |
| 299 | // Here we just increment the value until we exceed the tolerance. For simplicity we go up. |
| 300 | while (newValue < target) |
| 301 | { |
| 302 | oldValue = newValue; |
| 303 | newValue = std::nextafter(newValue, std::numeric_limits<float>::infinity()); |
| 304 | } |
| 305 | |
| 306 | otherData[i] = oldValue; |
| 307 | } |
| 308 | |
| 309 | const auto referenceTensor = |
| 310 | TosaTensor("out1", tosa_datatype_fp64_t, outputShape, reinterpret_cast<uint8_t*>(data.data())); |
| 311 | const auto implementationTensor = |
| 312 | TosaTensor("out1", tosa_datatype_fp32_t, outputShape, reinterpret_cast<uint8_t*>(otherData.data())); |
| 313 | REQUIRE(tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), json_cfg.c_str())); |
| 314 | } |
| 315 | |
| 316 | SUBCASE("different") |
| 317 | { |
| 318 | // TODO: Generate some new floats that exceed the tolerance. |
| 319 | auto otherData = std::vector<float>(elementCount / reductionSize); |
| 320 | for (unsigned i = 0; i < data.size(); ++i) |
| 321 | { |
| 322 | auto newValue = data[i]; |
| 323 | const auto target = tolerances[i] + newValue; |
| 324 | |
| 325 | // Here we just increment the value until we exceed the tolerance. For simplicity we go up. |
| 326 | while (newValue < target) |
| 327 | { |
| 328 | newValue = std::nextafter(newValue, std::numeric_limits<float>::infinity()); |
| 329 | } |
| 330 | |
| 331 | otherData[i] = newValue; |
| 332 | } |
| 333 | |
| 334 | const auto referenceTensor = |
| 335 | TosaTensor("out1", tosa_datatype_fp64_t, outputShape, reinterpret_cast<uint8_t*>(data.data())); |
| 336 | const auto implementationTensor = |
| 337 | TosaTensor("out1", tosa_datatype_fp32_t, outputShape, reinterpret_cast<uint8_t*>(otherData.data())); |
| 338 | REQUIRE_FALSE( |
| 339 | tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), json_cfg.c_str())); |
| 340 | } |
| 341 | } |
| 342 | |
Jack Frankland | 62737b1 | 2023-09-13 15:47:48 +0100 | [diff] [blame] | 343 | TEST_CASE("positive - ulp") |
| 344 | { |
| 345 | std::string json_cfg = R"({ |
| 346 | "tensors" : { |
| 347 | "out1" : { |
| 348 | "mode": "ULP", |
Jeremy Johnson | bb0935f | 2023-09-14 16:43:48 +0100 | [diff] [blame] | 349 | "data_type": "FP32", |
Jack Frankland | 62737b1 | 2023-09-13 15:47:48 +0100 | [diff] [blame] | 350 | "ulp_info": { |
| 351 | "ulp": 5 |
| 352 | } |
| 353 | } |
| 354 | } |
| 355 | })"; |
| 356 | |
| 357 | const auto shape = std::vector<int32_t>{ 8, 8, 8 }; |
| 358 | const auto elementCount = std::accumulate(std::begin(shape), std::end(shape), 1, std::multiplies<>()); |
| 359 | |
| 360 | // Generate some random floats using the full range of fp32. |
| 361 | auto data = generateRandomTensorData<float>(elementCount, false); |
| 362 | SUBCASE("same") |
| 363 | { |
| 364 | // Generate some data that meets the ULP requirements of the result. |
| 365 | auto otherData = data; |
| 366 | std::for_each(std::begin(otherData), std::end(otherData), [](auto& value) { value = increment(value, 5); }); |
| 367 | const auto referenceTensor = |
| 368 | TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data.data())); |
| 369 | const auto implementationTensor = |
| 370 | TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(otherData.data())); |
| 371 | REQUIRE(tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), json_cfg.c_str())); |
| 372 | } |
| 373 | |
| 374 | SUBCASE("different") |
| 375 | { |
| 376 | // Generate some data that exceeds a specified number of ULP for each value in the tensor. |
| 377 | auto otherData = std::vector<float>(elementCount); |
| 378 | std::for_each(std::begin(otherData), std::end(otherData), [](auto& value) { value = increment(value, 6); }); |
| 379 | |
| 380 | const auto referenceTensor = |
| 381 | TosaTensor("out1", tosa_datatype_fp64_t, shape, reinterpret_cast<uint8_t*>(data.data())); |
| 382 | const auto implementationTensor = |
| 383 | TosaTensor("out1", tosa_datatype_fp32_t, shape, reinterpret_cast<uint8_t*>(otherData.data())); |
| 384 | REQUIRE_FALSE( |
| 385 | tvf_verify_data(referenceTensor.cTensor(), nullptr, implementationTensor.cTensor(), json_cfg.c_str())); |
| 386 | } |
| 387 | } |
| 388 | |
Jack Frankland | aafc850 | 2023-09-13 11:03:50 +0100 | [diff] [blame] | 389 | TEST_SUITE_END(); // verify |