blob: b75ddecc21507eefb0e12865f1cd623f7b64bcd6 [file] [log] [blame]
Georgios Pinitas41df4282023-05-30 12:20:31 +01001// 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 Franklandaafc8502023-09-13 11:03:50 +010016#include <algorithm>
Jack Frankland62737b12023-09-13 15:47:48 +010017#include <cmath>
18#include <cstdint>
Georgios Pinitas41df4282023-05-30 12:20:31 +010019#include <doctest.h>
20
21#include <array>
Jack Franklandaafc8502023-09-13 11:03:50 +010022#include <iterator>
23#include <limits>
24#include <numeric>
25#include <random>
Georgios Pinitas7021ef02023-08-22 08:25:57 +010026#include <string>
Jack Franklandaafc8502023-09-13 11:03:50 +010027#include <type_traits>
Georgios Pinitas41df4282023-05-30 12:20:31 +010028#include <vector>
29
Georgios Pinitas7021ef02023-08-22 08:25:57 +010030namespace
Georgios Pinitas41df4282023-05-30 12:20:31 +010031{
Georgios Pinitas7021ef02023-08-22 08:25:57 +010032
33class TosaTensor
34{
35public:
Jack Franklandaafc8502023-09-13 11:03:50 +010036 TosaTensor(std::string name, tosa_datatype_t dataType, std::vector<int32_t> shape, uint8_t* data = nullptr)
Georgios Pinitas7021ef02023-08-22 08:25:57 +010037 : _name(std::move(name))
38 , _shape(std::move(shape))
Georgios Pinitas41df4282023-05-30 12:20:31 +010039 {
Georgios Pinitas7021ef02023-08-22 08:25:57 +010040 _tensor.name = _name.c_str();
41 _tensor.data_type = dataType;
42 _tensor.num_dims = _shape.size();
43 _tensor.shape = _shape.data();
Jack Franklandaafc8502023-09-13 11:03:50 +010044 _tensor.data = data;
45 _tensor.size =
46 std::accumulate(_tensor.shape, std::next(_tensor.shape, _tensor.num_dims), 1, std::multiplies<>());
Georgios Pinitas7021ef02023-08-22 08:25:57 +010047 };
Georgios Pinitas41df4282023-05-30 12:20:31 +010048
Georgios Pinitas7021ef02023-08-22 08:25:57 +010049 const tosa_tensor_t* cTensor() const
50 {
51 return &_tensor;
Georgios Pinitas41df4282023-05-30 12:20:31 +010052 }
Georgios Pinitas41df4282023-05-30 12:20:31 +010053
Georgios Pinitas7021ef02023-08-22 08:25:57 +010054private:
55 std::string _name;
56 std::vector<int32_t> _shape;
57 tosa_tensor_t _tensor;
58};
59
Jack Frankland62737b12023-09-13 15:47:48 +010060template <typename FP>
61std::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 Franklandaafc8502023-09-13 11:03:50 +010068auto& getRandomGenerator()
69{
70 static std::mt19937 gen(0);
71 return gen;
72}
73
74template <typename FP>
75std::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
89template <typename FP>
90std::enable_if_t<std::is_floating_point_v<FP>, FP> getRandomUniformFloat()
91{
92 return getUniformRealDist<FP>()(getRandomGenerator());
93}
94
95template <typename FP>
96std::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 Frankland12ee1a72023-09-20 09:08:34 +0100128// 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.
131constexpr 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
136template <typename FP>
137auto 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 Pinitas7021ef02023-08-22 08:25:57 +0100148} // namespace
149
150TEST_SUITE_BEGIN("verify");
151
152TEST_CASE("negative - api")
153{
154 std::string json_cfg = R"({
155 "tensors" : {
156 "out1" : {
157 "mode": "DOT_PRODUCT",
Jeremy Johnsonbb0935f2023-09-14 16:43:48 +0100158 "data_type": "FP32",
Georgios Pinitas7021ef02023-08-22 08:25:57 +0100159 "dot_product_info" : {
Georgios Pinitas7021ef02023-08-22 08:25:57 +0100160 "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 Pinitas41df4282023-05-30 12:20:31 +0100214 }
215}
Georgios Pinitas7021ef02023-08-22 08:25:57 +0100216
Jack Franklandaafc8502023-09-13 11:03:50 +0100217TEST_CASE("positive - exact")
218{
219 std::string json_cfg = R"({
220 "tensors" : {
221 "out1" : {
Jeremy Johnsonbb0935f2023-09-14 16:43:48 +0100222 "mode": "EXACT",
223 "data_type": "FP32"
Jack Franklandaafc8502023-09-13 11:03:50 +0100224 }
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 Frankland12ee1a72023-09-20 09:08:34 +0100261TEST_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 Frankland62737b12023-09-13 15:47:48 +0100343TEST_CASE("positive - ulp")
344{
345 std::string json_cfg = R"({
346 "tensors" : {
347 "out1" : {
348 "mode": "ULP",
Jeremy Johnsonbb0935f2023-09-14 16:43:48 +0100349 "data_type": "FP32",
Jack Frankland62737b12023-09-13 15:47:48 +0100350 "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 Franklandaafc8502023-09-13 11:03:50 +0100389TEST_SUITE_END(); // verify