Skip to content

Commit b543f3e

Browse files
dsharletgxnnpack-bot
authored andcommitted
Add Tensor<T> helper class for tests
This CL does two things: first is adding `Tensor<T>`, a helper class for multi-dimensional arrays that are frequently used in our tests (but currently are duplicating extent/stride computations frequently). Second is modifying two tests (slice and transpose) to make some changes: - Currently, subgraph tests compare subgraph to operator results. This changes tests to directly check the output, without running the operator code. - Currently, subgraph tests run a single random variation, and getting good coverage requires running the test many times. This changes the subgraph tests to test cover many more permutations in a single run. - Currently, subgraph tests dig into the internal implementation details of subgraphs (e.g. checking xnn_node_value state). This makes sense in some cases (e.g. fusion tests), but it is both hard to be certain that this covers real usage, and is brittle. IMO, tests should (as much as possible) attempt to verify the behavior is as expected via the APIs that are visible to the user of the thing they are testing. For the subgraph API, that means we should just make sure the subgraph works as expected. This change required a few minor cleanups: - `xnnpack::Buffer<T>` needs to be able to distinguish between "extra bytes" and real data. - To test things like transpose, concat, slice, etc., I found it helpful to add plain `xnn_datatype_uintX` datatypes. I don't love the idea of adding these to the public API when they don't have a lot of use cases, but I decided this is better than hacking the tests to use whatever datatype is available, which could be complicated (e.g. we'd have to use fp16 or bfloat16 to test transpose of 16-bit elements). - There is now some overlap between `RuntimeTester` and `SubgraphTester`. I think we should deprecate `RuntimeTester` and consolidate everything in `SubgraphTester`, because we can't return `RuntimeTester` from the base class `SubgraphTester` builder methods. This is a minor difficulty, but it also seems like the reason to separate them is minor too. PiperOrigin-RevId: 727983393
1 parent 9d3accb commit b543f3e

15 files changed

+614
-1048
lines changed

BUILD.bazel

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,12 @@ xnnpack_cc_library(
561561
xnnpack_cxx_library(
562562
name = "buffer",
563563
hdrs = ["src/xnnpack/buffer.h"],
564-
deps = [":datatype"],
564+
deps = [
565+
":common",
566+
":datatype",
567+
":math",
568+
":xnnpack_h",
569+
],
565570
)
566571

567572
xnnpack_cc_library(

CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1393,7 +1393,6 @@ IF(XNNPACK_BUILD_TESTS)
13931393
static-reshape
13941394
static-resize-bilinear-2d
13951395
static-transpose
1396-
transpose-reshape
13971396
unary
13981397
unpooling-2d)
13991398
FOREACH(TEST ${LIBRARY_SUBGRAPH_UNIT_TESTS})

include/xnnpack.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@ enum xnn_datatype {
299299
/// Packed quantized 8-bit unsigned integer with shared per-Value quantization
300300
/// parameters.
301301
xnn_datatype_pqint8 = 17,
302+
/// Unsigned integer datatype without quantization parameters.
303+
xnn_datatype_uint8 = 18,
304+
xnn_datatype_uint16 = 19,
305+
xnn_datatype_uint32 = 20,
302306
};
303307

304308
/// Define a tensor-type Value and add it to a Subgraph.

src/datatype.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ bool xnn_datatype_is_real(enum xnn_datatype t) {
1111
switch (t) {
1212
case xnn_datatype_invalid:
1313
case xnn_datatype_int32:
14+
case xnn_datatype_uint8:
15+
case xnn_datatype_uint16:
16+
case xnn_datatype_uint32:
1417
return false;
1518
case xnn_datatype_fp32:
1619
case xnn_datatype_fp16:
@@ -55,6 +58,9 @@ bool xnn_datatype_is_integral(enum xnn_datatype t) {
5558
case xnn_datatype_pfp32:
5659
return false;
5760
case xnn_datatype_int32:
61+
case xnn_datatype_uint8:
62+
case xnn_datatype_uint16:
63+
case xnn_datatype_uint32:
5864
return true;
5965
}
6066
XNN_UNREACHABLE;
@@ -82,6 +88,9 @@ bool xnn_datatype_is_quantized(enum xnn_datatype t) {
8288
case xnn_datatype_int32:
8389
case xnn_datatype_pfp16:
8490
case xnn_datatype_pfp32:
91+
case xnn_datatype_uint8:
92+
case xnn_datatype_uint16:
93+
case xnn_datatype_uint32:
8594
return false;
8695
}
8796
XNN_UNREACHABLE;
@@ -103,16 +112,19 @@ size_t xnn_datatype_log2_size_bits(enum xnn_datatype t) {
103112
case xnn_datatype_qdint8:
104113
case xnn_datatype_qduint8:
105114
case xnn_datatype_qpint8:
115+
case xnn_datatype_uint8:
106116
return 3;
107117
case xnn_datatype_fp16:
108118
case xnn_datatype_bf16:
109119
case xnn_datatype_pfp16:
120+
case xnn_datatype_uint16:
110121
return 4;
111122
case xnn_datatype_qint32:
112123
case xnn_datatype_qcint32:
113124
case xnn_datatype_int32:
114125
case xnn_datatype_fp32:
115126
case xnn_datatype_pfp32:
127+
case xnn_datatype_uint32:
116128
return 5;
117129
}
118130
XNN_UNREACHABLE;
@@ -154,6 +166,9 @@ bool xnn_datatype_is_byte_addressable(enum xnn_datatype t) {
154166
case xnn_datatype_qduint8:
155167
case xnn_datatype_int32:
156168
case xnn_datatype_fp32:
169+
case xnn_datatype_uint8:
170+
case xnn_datatype_uint16:
171+
case xnn_datatype_uint32:
157172
return true;
158173
}
159174
XNN_UNREACHABLE;

src/enums/datatype-strings.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ const char* xnn_datatype_to_string(enum xnn_datatype type) {
5050
return "INT32";
5151
case xnn_datatype_qbint4:
5252
return "QBINT4";
53+
case xnn_datatype_uint8:
54+
return "UINT8";
55+
case xnn_datatype_uint16:
56+
return "UINT16";
57+
case xnn_datatype_uint32:
58+
return "UINT32";
5359
}
5460
XNN_UNREACHABLE;
5561
return NULL;

src/tensor.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ enum xnn_status xnn_define_tensor_value(
128128
case xnn_datatype_int32:
129129
case xnn_datatype_pfp16: // TODO: Does this really belong here?
130130
case xnn_datatype_pfp32: // TODO: Does this really belong here?
131+
case xnn_datatype_uint8:
132+
case xnn_datatype_uint16:
133+
case xnn_datatype_uint32:
131134
break;
132135
default:
133136
xnn_log_error("failed to create Dense Tensor value: unsupported datatype %s (%d)",

src/xnnpack/buffer.h

Lines changed: 210 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
#define __XNNPACK_TEST_BUFFER_H_
88

99
#include <algorithm>
10+
#include <cassert>
1011
#include <cstddef>
1112
#include <cstdint>
1213
#include <cstdlib>
1314
#include <initializer_list>
1415
#include <limits>
16+
#include <memory>
1517
#include <type_traits>
18+
#include <vector>
1619

1720
#include "xnnpack.h"
1821
#include "xnnpack/common.h"
@@ -46,6 +49,10 @@ class NumericLimits<quantized<T>> {
4649
}
4750
};
4851

52+
struct PaddingBytes {
53+
size_t value;
54+
};
55+
4956
// This is a container similar to std::vector, but it leaves the memory
5057
// uninitialized, supports alignment.
5158
// TODO: It would be good if this also managed padding in a way that allowed
@@ -99,8 +106,10 @@ class Buffer {
99106
using const_iterator = const T*;
100107

101108
Buffer() : data_(nullptr), size_(0) {}
102-
explicit Buffer(size_t size)
103-
: data_(reinterpret_cast<T*>(allocate(size * sizeof(T)))), size_(size) {}
109+
explicit Buffer(size_t size, PaddingBytes extra_bytes = {0})
110+
: data_(reinterpret_cast<T*>(
111+
allocate(size * sizeof(T) + extra_bytes.value))),
112+
size_(size) {}
104113
Buffer(size_t size, T value) : Buffer(size) {
105114
std::fill(begin(), end(), value);
106115
}
@@ -165,6 +174,205 @@ void fill_uniform_random_bits(T* data, size_t size, Rng& rng) {
165174
}
166175
}
167176

177+
// Returns {x[i] for i in perm}
178+
template <typename T>
179+
std::vector<T> permute(const std::vector<size_t>& perm,
180+
const std::vector<T>& x) {
181+
std::vector<T> result(perm.size());
182+
for (size_t i = 0; i < perm.size(); ++i) {
183+
result[i] = x[perm[i]];
184+
}
185+
return result;
186+
}
187+
188+
// This stores a multi-dimensional array in a Buffer<T, Alignment> object
189+
// (above). The sizes of dimensions are `extent`s, the distance between elements
190+
// in a dimension in memory are `stride`s.
191+
// This buffer holds an std::shared_ptr to the underlying Buffer<T, Alignment>
192+
// objects, i.e. copies are shallow.
193+
template <typename T, size_t Alignment = alignof(T)>
194+
class Tensor {
195+
public:
196+
using value_type = T;
197+
using iterator = typename xnnpack::Buffer<T>::iterator;
198+
using const_iterator = typename xnnpack::Buffer<T>::const_iterator;
199+
200+
using index_type = std::vector<size_t>;
201+
202+
Tensor() = default;
203+
Tensor(const Tensor& other) = default;
204+
Tensor(Tensor&& other) = default;
205+
// Constructs an array with strides in descending order, with no
206+
// padding/alignment between dimensions.
207+
explicit Tensor(index_type extents, PaddingBytes extra_bytes = {0})
208+
: extents_(std::move(extents)), strides_(extents_.size()) {
209+
size_t stride = 1;
210+
for (size_t i = rank(); i > 0; --i) {
211+
strides_[i - 1] = stride;
212+
stride *= extents_[i - 1];
213+
}
214+
data_ = std::make_shared<Buffer<T, Alignment>>(stride, extra_bytes);
215+
begin_ = data_->begin();
216+
end_ = data_->end();
217+
}
218+
Tensor& operator=(const Tensor& other) = default;
219+
Tensor& operator=(Tensor&& other) = default;
220+
221+
// Returns true if every stride is the product of the following extents, i.e.
222+
// the buffer can be interpreted as a flat array without considering the
223+
// strides.
224+
bool is_contiguous() const {
225+
size_t stride = 1;
226+
for (size_t i = rank(); i > 0; --i) {
227+
if (strides_[i - 1] != stride) {
228+
return false;
229+
}
230+
stride *= extents_[i - 1];
231+
}
232+
return true;
233+
}
234+
235+
const index_type& extents() const { return extents_; }
236+
const index_type& strides() const { return strides_; }
237+
size_t extent(size_t dim) const { return extents_[dim]; }
238+
size_t stride(size_t dim) const { return strides_[dim]; }
239+
240+
size_t rank() const { return extents_.size(); }
241+
bool empty() const { return begin_ >= end_; }
242+
243+
T* base() { return begin_; }
244+
const T* base() const { return begin_; }
245+
246+
// Form a reference to an element at a particular index.
247+
T& operator()(const index_type& indices) {
248+
return *(begin_ + flat_offset(indices));
249+
}
250+
const T& operator()(const index_type& indices) const {
251+
return *(begin_ + flat_offset(indices));
252+
}
253+
254+
template <typename... Args>
255+
T& operator()(Args... args) {
256+
return operator()(index_type{args...});
257+
}
258+
template <typename... Args>
259+
const T& operator()(Args... args) const {
260+
return operator()(index_type{args...});
261+
}
262+
263+
// The following functions can only be used if `is_contiguous` is true.
264+
T* data() {
265+
assert(is_contiguous());
266+
return begin_;
267+
}
268+
const T* data() const {
269+
assert(is_contiguous());
270+
return begin_;
271+
}
272+
size_t size() const {
273+
assert(is_contiguous());
274+
return data_->size();
275+
}
276+
T* begin() { return data(); }
277+
T* end() { return end_; }
278+
const T* begin() const { return data(); }
279+
const T* end() const { return end_; }
280+
const T* cbegin() const { return data(); }
281+
const T* cend() const { return end_; }
282+
T& operator[](size_t index) { return data()[index]; }
283+
const T& operator[](size_t index) const { return data()[index]; }
284+
285+
// This does not actually transpose any data in memory, it just changes the
286+
// strides. To implement the transpose in memory, use this, followed by
287+
// `make_copy` below.
288+
Tensor<T, Alignment> transpose(std::vector<size_t> perm) const {
289+
Tensor<T, Alignment> result(*this);
290+
result.extents_ = permute(perm, extents_);
291+
result.strides_ = permute(perm, strides_);
292+
return result;
293+
}
294+
295+
// This uses the same rules for indexing as numpy, i.e. negative numbers are
296+
// offset are added to the extents.
297+
Tensor<T, Alignment> slice(std::vector<int64_t> begins,
298+
std::vector<int64_t> ends) const {
299+
assert(rank() == begins.size());
300+
assert(rank() == ends.size());
301+
302+
Tensor<T, Alignment> result(*this);
303+
std::vector<size_t> offsets(rank());
304+
for (size_t i = 0; i < rank(); ++i) {
305+
offsets[i] = begins[i] < 0 ? extents_[i] + begins[i] : begins[i];
306+
result.extents_[i] =
307+
(ends[i] <= 0 ? extents_[i] + ends[i] : ends[i]) - offsets[i];
308+
}
309+
310+
result.begin_ = begin_ + flat_offset(offsets);
311+
result.end_ = result.begin_ + result.flat_offset(result.extents_);
312+
313+
return result;
314+
}
315+
316+
// Make a copy of the buffer. The result will be contiguous, i.e. the strides
317+
// of this buffer are lost when copying.
318+
Tensor<T, Alignment> deep_copy(PaddingBytes extra_bytes = {0}) const {
319+
Tensor<T, Alignment> result(extents_, extra_bytes);
320+
copy_impl(rank(), extents_.data(), strides_.data(), base(),
321+
result.strides_.data(), result.base());
322+
return result;
323+
}
324+
325+
private:
326+
static void copy_impl(size_t rank, const size_t* extents,
327+
const size_t* src_strides, const T* src,
328+
const size_t* dst_strides, T* dst) {
329+
if (rank == 0) {
330+
*dst = *src;
331+
return;
332+
} else {
333+
--rank;
334+
size_t extent = *extents++;
335+
size_t src_stride = *src_strides++;
336+
size_t dst_stride = *dst_strides++;
337+
if (rank == 0 && src_stride == 1 && dst_stride == 1) {
338+
std::copy_n(src, extent, dst);
339+
} else {
340+
for (size_t i = 0; i < extent; ++i) {
341+
copy_impl(rank, extents, src_strides, src, dst_strides, dst);
342+
src += src_stride;
343+
dst += dst_stride;
344+
}
345+
}
346+
}
347+
}
348+
349+
// Compute the offset of an index from the pointer to element 0.
350+
size_t flat_offset(const index_type& indices) const {
351+
assert(indices.size() == rank());
352+
size_t result = 0;
353+
for (size_t i = 0; i < rank(); ++i) {
354+
result += strides_[i] * indices[i];
355+
}
356+
return result;
357+
}
358+
359+
index_type extents_;
360+
index_type strides_;
361+
std::shared_ptr<xnnpack::Buffer<T, Alignment>> data_;
362+
T* begin_ = nullptr;
363+
T* end_ = nullptr;
364+
};
365+
366+
template <typename Rng>
367+
std::vector<size_t> random_shape(Rng& rng, size_t rank, size_t min_dim = 1,
368+
size_t max_dim = 9) {
369+
std::vector<size_t> shape(rank);
370+
for (size_t i = 0; i < rank; ++i) {
371+
shape[i] = (rng() % (max_dim - min_dim + 1)) + min_dim;
372+
}
373+
return shape;
374+
}
375+
168376
}; // namespace xnnpack
169377

170378
#endif // __XNNPACK_TEST_BUFFER_H_

src/xnnpack/datatype.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ xnn_datatype xnn_datatype_of() {
105105
return xnn_datatype_fp32;
106106
} else if (std::is_same<T, int32_t>::value) {
107107
return xnn_datatype_int32;
108+
} else if (std::is_same<T, uint8_t>::value) {
109+
return xnn_datatype_uint8;
110+
} else if (std::is_same<T, uint16_t>::value) {
111+
return xnn_datatype_uint16;
112+
} else if (std::is_same<T, uint32_t>::value) {
113+
return xnn_datatype_uint32;
108114
} else {
109115
return xnn_datatype_invalid;
110116
}

0 commit comments

Comments
 (0)