diff --git a/docs/api/core-types.md b/docs/api/core-types.md index 61fd307..a58b1a7 100644 --- a/docs/api/core-types.md +++ b/docs/api/core-types.md @@ -41,7 +41,8 @@ floating point, and standard floating point types: ## TensorView -`infini::rt::TensorView` is a non-owning description of tensor memory. +`infini::rt::TensorView` is a non-owning description of tensor memory that owns +its shape and stride metadata. ```cpp std::vector data(16); @@ -66,4 +67,12 @@ auto contiguous = tensor.IsContiguous(); - device - strides -It does not own the memory it references. +It does not own the tensor data it references. Shape and strides are stored +inline for ranks 0 through 8; rank 9 and above use owned heap fallback storage. +Construction from `std::vector` and other compatible contiguous ranges remains +supported. + +On an lvalue `TensorView`, `shape()` and `strides()` return lightweight +contiguous views by value. Those views borrow metadata from the `TensorView` and +must not outlive it. Calling the accessors on an rvalue returns owning metadata +so a view cannot dangle from a temporary. diff --git a/docs/compatibility.md b/docs/compatibility.md index c700c95..b933fe5 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -42,3 +42,17 @@ by the same configured build. InfiniRT currently exposes a C++ API. Consumers should treat the installed headers and `libinfinirt.so` as a matching pair from the same build or release. +`TensorView::Shape` and `TensorView::Strides` are concrete vector-like C++ +aliases using inline capacity 8. `TensorView` stores ranks 0 through 8 inline +and uses owned heap fallback at rank 9 and above. This representation changes +`TensorView` layout and is an API/ABI compatibility break from the previous +`std::vector` aliases. Consumers must rebuild after this alias or layout change +and must not mix headers and libraries from different builds. + +Existing construction from `std::vector` remains supported, but code that +requires the exact `std::vector` alias must adapt. On lvalues, `shape()` and +`strides()` now return typed borrowed contiguous views by value; callers that +need ownership should explicitly materialize `TensorView::Shape` or +`TensorView::Strides`. The owning aliases support the common +`Strides(count, value)` construction used by downstream metadata code. + diff --git a/scripts/run_performance_tests.py b/scripts/run_performance_tests.py index b1cc101..d0b3776 100644 --- a/scripts/run_performance_tests.py +++ b/scripts/run_performance_tests.py @@ -146,6 +146,7 @@ def main(): "perf_runtime_dispatch", "perf_memory", "perf_tensor_view", + "perf_tensor_view_footprint", ] metadata = { diff --git a/src/common/metadata_view.h b/src/common/metadata_view.h new file mode 100644 index 0000000..727e9f3 --- /dev/null +++ b/src/common/metadata_view.h @@ -0,0 +1,169 @@ +#ifndef INFINI_RT_COMMON_METADATA_VIEW_H_ +#define INFINI_RT_COMMON_METADATA_VIEW_H_ + +#include +#include +#include +#include + +namespace infini::rt::detail { + +template +class SmallVector; + +template +class MetadataView; + +template +struct IsMetadataView : std::false_type {}; + +template +struct IsMetadataView> : std::true_type {}; + +template +struct IsMetadataViewSmallVector : std::false_type {}; + +template +struct IsMetadataViewSmallVector> + : std::true_type {}; + +template +struct IsMetadataViewComparableRange : std::false_type {}; + +template +struct IsMetadataViewComparableRange< + Range, T, + std::void_t())), + decltype(std::end(std::declval())), + decltype(std::size(std::declval())), + decltype(static_cast( + std::declval() == + *std::begin(std::declval())))>> + : std::true_type {}; + +template +class MetadataView { + public: + using value_type = T; + + using size_type = std::size_t; + + using reference = const T&; + + using const_reference = const T&; + + using pointer = const T*; + + using const_pointer = const T*; + + using iterator = const T*; + + using const_iterator = const T*; + + constexpr MetadataView() noexcept = default; + + constexpr MetadataView(const_pointer data, size_type size) noexcept + : data_{data}, size_{size} {} + + constexpr size_type size() const noexcept { return size_; } + + constexpr bool empty() const noexcept { return size_ == 0; } + + constexpr const_pointer data() const noexcept { return data_; } + + constexpr const_reference front() const noexcept { return data_[0]; } + + constexpr const_reference back() const noexcept { return data_[size_ - 1]; } + + constexpr const_reference operator[](size_type index) const noexcept { + return data_[index]; + } + + constexpr const_iterator begin() const noexcept { return data_; } + + constexpr const_iterator end() const noexcept { + return empty() ? data_ : data_ + size_; + } + + constexpr const_iterator cbegin() const noexcept { return begin(); } + + constexpr const_iterator cend() const noexcept { return end(); } + + private: + const_pointer data_{nullptr}; + + size_type size_{0}; +}; + +template , Left>::value, + int> = 0> +constexpr bool operator==(MetadataView left, MetadataView right) { + if (left.size() != right.size()) return false; + + for (std::size_t index = 0; index < left.size(); ++index) { + if (!(left[index] == right[index])) return false; + } + + return true; +} + +template , Left>::value, + int> = 0> +constexpr bool operator!=(MetadataView left, MetadataView right) { + return !(left == right); +} + +template >::value && + !IsMetadataViewSmallVector>::value && + IsMetadataViewComparableRange::value, + int> = 0> +constexpr bool operator==(MetadataView left, const Range& right) { + if (left.size() != static_cast(std::size(right))) return false; + + auto right_iterator = std::begin(right); + for (std::size_t index = 0; index < left.size(); ++index, ++right_iterator) { + if (!(left[index] == *right_iterator)) return false; + } + + return true; +} + +template >::value && + !IsMetadataViewSmallVector>::value && + IsMetadataViewComparableRange::value, + int> = 0> +constexpr bool operator==(const Range& left, MetadataView right) { + return right == left; +} + +template >::value && + !IsMetadataViewSmallVector>::value && + IsMetadataViewComparableRange::value, + int> = 0> +constexpr bool operator!=(MetadataView left, const Range& right) { + return !(left == right); +} + +template >::value && + !IsMetadataViewSmallVector>::value && + IsMetadataViewComparableRange::value, + int> = 0> +constexpr bool operator!=(const Range& left, MetadataView right) { + return !(right == left); +} + +} // namespace infini::rt::detail + +#endif diff --git a/src/common/shape_strides_storage.h b/src/common/shape_strides_storage.h new file mode 100644 index 0000000..a693c31 --- /dev/null +++ b/src/common/shape_strides_storage.h @@ -0,0 +1,494 @@ +#ifndef INFINI_RT_COMMON_SHAPE_STRIDES_STORAGE_H_ +#define INFINI_RT_COMMON_SHAPE_STRIDES_STORAGE_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/metadata_view.h" +#include "common/small_vector.h" + +namespace infini::rt::detail { + +struct DefaultStridesTag {}; + +template +using RangeIterator = decltype(std::begin(std::declval())); + +template +struct IsForwardRange : std::false_type {}; + +template +struct IsForwardRange>::iterator_category>> + : std::is_base_of>::iterator_category> {}; + +template +class ShapeStridesStorage { + static_assert(inline_capacity > 0, + "Shape/strides storage requires a positive inline capacity."); + + static_assert(std::is_trivially_copyable_v && + std::is_trivially_destructible_v, + "Shape/strides storage requires a trivial size type."); + + static_assert(std::is_trivially_copyable_v && + std::is_trivially_destructible_v, + "Shape/strides storage requires a trivial stride type."); + + static_assert(alignof(Size) <= alignof(std::max_align_t) && + alignof(Stride) <= alignof(std::max_align_t), + "Shape/strides storage does not support over-aligned types."); + + public: + using Shape = SmallVector; + + using Strides = SmallVector; + + using ShapeView = MetadataView; + + using StridesView = MetadataView; + + ShapeStridesStorage() = default; + + ShapeStridesStorage(const Shape& shape, const Strides& strides) { + InitializeRanges(shape, strides); + } + + ShapeStridesStorage(Shape&& shape, Strides&& strides) { + InitializeOwned(std::move(shape), std::move(strides)); + } + + ShapeStridesStorage(Shape&& shape, const Strides& strides) { + InitializeMixed(std::move(shape), strides); + } + + ShapeStridesStorage(const Shape& shape, Strides&& strides) { + InitializeMixed(shape, std::move(strides)); + } + + template + ShapeStridesStorage(const ShapeRange& shape, const StridesRange& strides) { + InitializeRanges(shape, strides); + } + + ShapeStridesStorage(const Shape& shape, DefaultStridesTag) { + InitializeDefaultStrides(shape); + } + + ShapeStridesStorage(Shape&& shape, DefaultStridesTag) { + InitializeDefaultStrides(std::move(shape)); + } + + template + ShapeStridesStorage(const ShapeRange& shape, DefaultStridesTag) { + InitializeDefaultStrides(shape); + } + + ShapeStridesStorage(const ShapeStridesStorage& other) { + InitializeRanges(other.shape(), other.strides()); + } + + ShapeStridesStorage(ShapeStridesStorage&& other) noexcept { + MoveConstructFrom(other); + } + + ShapeStridesStorage& operator=(const ShapeStridesStorage&) = delete; + + ShapeStridesStorage& operator=(ShapeStridesStorage&&) = delete; + + ~ShapeStridesStorage() { ReleaseStorage(); } + + ShapeView shape() const noexcept { + return ShapeView{ShapeData(), shape_size_}; + } + + StridesView strides() const noexcept { + return StridesView{StridesData(), strides_size_}; + } + + std::size_t shape_size() const noexcept { return shape_size_; } + + std::size_t strides_size() const noexcept { return strides_size_; } + + const Size* shape_data() const noexcept { return ShapeData(); } + + const Stride* strides_data() const noexcept { return StridesData(); } + + private: + using ShapeAllocation = typename Shape::HeapAllocation; + + using StridesAllocation = typename Strides::HeapAllocation; + + struct InlineStorage { + Size shape[inline_capacity]; + + Stride strides[inline_capacity]; + + InlineStorage() noexcept {} + }; + + struct HeapStorage { + void* allocation; + + Size* shape; + + Stride* strides; + + std::size_t shape_capacity; + + std::size_t strides_capacity; + }; + + union Storage { + InlineStorage inline_storage; + + HeapStorage heap_storage; + + Storage() noexcept : inline_storage{} {} + + ~Storage() {} + }; + + class CombinedAllocation { + public: + CombinedAllocation(std::size_t shape_size, std::size_t strides_size) { + const std::size_t shape_bytes = CheckedMultiply(shape_size, sizeof(Size)); + const std::size_t strides_bytes = + CheckedMultiply(strides_size, sizeof(Stride)); + const std::size_t padding = alignof(Stride) - 1; + const std::size_t bytes = + CheckedAdd(CheckedAdd(shape_bytes, padding), strides_bytes); + + RawAllocation allocation{::operator new(bytes)}; + void* stride_storage = static_cast( + static_cast(allocation.get()) + shape_bytes); + std::size_t stride_space = bytes - shape_bytes; + + const void* const aligned_stride_storage = std::align( + alignof(Stride), strides_bytes, stride_storage, stride_space); + if (aligned_stride_storage == nullptr) { + std::abort(); + } + + shape_ = shape_size == 0 ? static_cast(allocation.get()) + : ::new (allocation.get()) Size[shape_size]; + strides_ = strides_size == 0 ? static_cast(stride_storage) + : ::new (stride_storage) + Stride[strides_size]; + allocation_ = allocation.release(); + } + + CombinedAllocation(const CombinedAllocation&) = delete; + + CombinedAllocation& operator=(const CombinedAllocation&) = delete; + + ~CombinedAllocation() { ::operator delete(allocation_); } + + void* allocation() const noexcept { return allocation_; } + + Size* shape() const noexcept { return shape_; } + + Stride* strides() const noexcept { return strides_; } + + void release() noexcept { allocation_ = nullptr; } + + private: + struct RawDeleter { + void operator()(void* allocation) const noexcept { + ::operator delete(allocation); + } + }; + + using RawAllocation = std::unique_ptr; + + static std::size_t CheckedMultiply(std::size_t left, std::size_t right) { + if (right != 0 && + left > std::numeric_limits::max() / right) { + std::abort(); + } + + return left * right; + } + + static std::size_t CheckedAdd(std::size_t left, std::size_t right) { + if (left > std::numeric_limits::max() - right) { + std::abort(); + } + + return left + right; + } + + void* allocation_{nullptr}; + + Size* shape_{nullptr}; + + Stride* strides_{nullptr}; + }; + + bool IsInline() const noexcept { + return shape_size_ <= inline_capacity && strides_size_ <= inline_capacity; + } + + bool IsCombined() const noexcept { + return !IsInline() && storage_.heap_storage.allocation != nullptr; + } + + static std::uint32_t NarrowSize(std::size_t size) { + if (size > std::numeric_limits::max()) { + std::abort(); + } + + return static_cast(size); + } + + template + static std::size_t RangeSize(const Range& range) { + const auto first = std::begin(range); + const auto last = std::end(range); + if (first == last) return 0; + + const auto distance = std::distance(first, last); + assert(distance >= 0); + + return static_cast(distance); + } + + template + static void CopyRange(const Range& range, T* destination) { + for (const auto& value : range) { + *destination++ = static_cast(value); + } + } + + template + void InitializeRanges(const ShapeRange& shape, const StridesRange& strides) { + if constexpr (IsForwardRange::value && + IsForwardRange::value) { + const std::size_t shape_size = RangeSize(shape); + const std::size_t strides_size = RangeSize(strides); + Initialize(shape_size, strides_size, + [&](Size* shape_destination, Stride* strides_destination) { + CopyRange(shape, shape_destination); + CopyRange(strides, strides_destination); + }); + } else { + Shape owned_shape{std::begin(shape), std::end(shape)}; + Strides owned_strides{std::begin(strides), std::end(strides)}; + InitializeOwned(std::move(owned_shape), std::move(owned_strides)); + } + } + + template + void Initialize(std::size_t shape_size, std::size_t strides_size, + Writer&& writer) { + const std::uint32_t narrowed_shape_size = NarrowSize(shape_size); + const std::uint32_t narrowed_strides_size = NarrowSize(strides_size); + + if (shape_size <= inline_capacity && strides_size <= inline_capacity) { + std::forward(writer)(storage_.inline_storage.shape, + storage_.inline_storage.strides); + shape_size_ = narrowed_shape_size; + strides_size_ = narrowed_strides_size; + + return; + } + + CombinedAllocation allocation{shape_size, strides_size}; + std::forward(writer)(allocation.shape(), allocation.strides()); + ActivateCombined(allocation, narrowed_shape_size, narrowed_strides_size); + } + + void InitializeOwned(Shape&& shape, Strides&& strides) { + const std::size_t shape_size = shape.size(); + const std::size_t strides_size = strides.size(); + + if (shape_size <= inline_capacity && strides_size <= inline_capacity) { + InitializeRanges(shape, strides); + + return; + } + + if (shape.capacity() > inline_capacity && + strides.capacity() > inline_capacity) { + const std::uint32_t narrowed_shape_size = NarrowSize(shape_size); + const std::uint32_t narrowed_strides_size = NarrowSize(strides_size); + ShapeAllocation shape_allocation = shape.ReleaseHeap(); + StridesAllocation strides_allocation = strides.ReleaseHeap(); + ActivateSplit(std::move(shape_allocation), std::move(strides_allocation), + narrowed_shape_size, narrowed_strides_size); + + return; + } + + InitializeRanges(shape, strides); + } + + void InitializeMixed(Shape&& shape, const Strides& strides) { + if (shape.size() > inline_capacity && strides.size() > inline_capacity && + shape.capacity() > inline_capacity) { + Strides owned_strides{strides}; + InitializeOwned(std::move(shape), std::move(owned_strides)); + + return; + } + + InitializeRanges(shape, strides); + } + + void InitializeMixed(const Shape& shape, Strides&& strides) { + if (shape.size() > inline_capacity && strides.size() > inline_capacity && + strides.capacity() > inline_capacity) { + Shape owned_shape{shape}; + InitializeOwned(std::move(owned_shape), std::move(strides)); + + return; + } + + InitializeRanges(shape, strides); + } + + template + void InitializeDefaultStrides(const ShapeRange& shape) { + if constexpr (IsForwardRange::value) { + const std::size_t shape_size = RangeSize(shape); + Initialize(shape_size, shape_size, + [&](Size* shape_destination, Stride* strides_destination) { + CopyRange(shape, shape_destination); + FillDefaultStrides(shape_destination, shape_size, + strides_destination); + }); + } else { + Shape owned_shape{std::begin(shape), std::end(shape)}; + InitializeDefaultStrides(std::move(owned_shape)); + } + } + + void InitializeDefaultStrides(Shape&& shape) { + if (shape.size() <= inline_capacity || + shape.capacity() <= inline_capacity) { + InitializeDefaultStrides(static_cast(shape)); + + return; + } + + Strides strides(shape.size()); + FillDefaultStrides(shape.data(), shape.size(), strides.data()); + InitializeOwned(std::move(shape), std::move(strides)); + } + + static void FillDefaultStrides(const Size* shape, std::size_t shape_size, + Stride* strides) { + if (shape_size == 0) return; + + strides[shape_size - 1] = 1; + + for (std::size_t index = shape_size - 1; index > 0; --index) { + strides[index - 1] = strides[index] * shape[index]; + } + } + + void ActivateCombined(CombinedAllocation& allocation, + std::uint32_t shape_size, + std::uint32_t strides_size) noexcept { + storage_.inline_storage.~InlineStorage(); + ::new (static_cast(&storage_.heap_storage)) + HeapStorage{allocation.allocation(), allocation.shape(), + allocation.strides(), 0, 0}; + shape_size_ = shape_size; + strides_size_ = strides_size; + allocation.release(); + } + + void ActivateSplit(ShapeAllocation&& shape, StridesAllocation&& strides, + std::uint32_t shape_size, + std::uint32_t strides_size) noexcept { + const std::size_t shape_capacity = shape.capacity(); + const std::size_t strides_capacity = strides.capacity(); + Size* shape_data = shape.release(); + Stride* strides_data = strides.release(); + + storage_.inline_storage.~InlineStorage(); + ::new (static_cast(&storage_.heap_storage)) HeapStorage{ + nullptr, shape_data, strides_data, shape_capacity, strides_capacity}; + shape_size_ = shape_size; + strides_size_ = strides_size; + } + + void MoveConstructFrom(ShapeStridesStorage& other) noexcept { + if (other.IsInline()) { + Initialize( + other.shape_size_, other.strides_size_, + [&](Size* shape_destination, Stride* strides_destination) { + for (std::size_t index = 0; index < other.shape_size_; ++index) { + shape_destination[index] = other.ShapeData()[index]; + } + for (std::size_t index = 0; index < other.strides_size_; ++index) { + strides_destination[index] = other.StridesData()[index]; + } + }); + other.shape_size_ = 0; + other.strides_size_ = 0; + + return; + } + + storage_.inline_storage.~InlineStorage(); + ::new (static_cast(&storage_.heap_storage)) + HeapStorage{other.storage_.heap_storage}; + shape_size_ = other.shape_size_; + strides_size_ = other.strides_size_; + other.storage_.heap_storage.~HeapStorage(); + ::new (static_cast(&other.storage_.inline_storage)) InlineStorage{}; + other.shape_size_ = 0; + other.strides_size_ = 0; + } + + const Size* ShapeData() const noexcept { + return IsInline() ? storage_.inline_storage.shape + : storage_.heap_storage.shape; + } + + const Stride* StridesData() const noexcept { + return IsInline() ? storage_.inline_storage.strides + : storage_.heap_storage.strides; + } + + void ReleaseStorage() noexcept { + if (IsInline()) return; + + if (IsCombined()) { + ::operator delete(storage_.heap_storage.allocation); + + return; + } + + std::allocator shape_allocator; + std::allocator_traits>::deallocate( + shape_allocator, storage_.heap_storage.shape, + storage_.heap_storage.shape_capacity); + std::allocator strides_allocator; + std::allocator_traits>::deallocate( + strides_allocator, storage_.heap_storage.strides, + storage_.heap_storage.strides_capacity); + } + + Storage storage_; + + std::uint32_t shape_size_{0}; + + std::uint32_t strides_size_{0}; +}; + +} // namespace infini::rt::detail + +#endif diff --git a/src/common/small_vector.h b/src/common/small_vector.h new file mode 100644 index 0000000..30c74e5 --- /dev/null +++ b/src/common/small_vector.h @@ -0,0 +1,615 @@ +#ifndef INFINI_RT_COMMON_SMALL_VECTOR_H_ +#define INFINI_RT_COMMON_SMALL_VECTOR_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace infini::rt::detail { + +template +class SmallVector; + +template +struct IsSmallVector : std::false_type {}; + +template +struct IsSmallVector> : std::true_type {}; + +template +struct IsCompatibleContainer : std::false_type {}; + +template +struct IsCompatibleContainer< + Range, T, + std::void_t())), + decltype(std::end(std::declval())), + decltype(static_cast(*std::begin( + std::declval())))>> : std::true_type {}; + +template +struct IsEqualityComparableRange : std::false_type {}; + +template +struct IsEqualityComparableRange< + Range, T, + std::void_t())), + decltype(std::end(std::declval())), + decltype(std::size(std::declval())), + decltype(static_cast( + std::declval() == + *std::begin(std::declval())))>> + : std::true_type {}; + +template +class SmallVector { + static_assert(inline_capacity > 0, + "`SmallVector` requires a positive inline capacity."); + + static_assert(std::is_trivially_copyable_v, + "`SmallVector` requires `T` to be trivially copyable."); + + static_assert(std::is_trivially_destructible_v, + "`SmallVector` requires `T` to be trivially destructible."); + + static_assert(std::is_nothrow_default_constructible_v, + "`SmallVector` requires `T` to be nothrow default " + "constructible."); + + static_assert(std::is_nothrow_copy_constructible_v, + "`SmallVector` requires `T` to be nothrow copy " + "constructible."); + + static_assert(std::is_nothrow_copy_assignable_v, + "`SmallVector` requires `T` to be nothrow copy assignable."); + + public: + using value_type = T; + + using size_type = std::size_t; + + using iterator = T*; + + using const_iterator = const T*; + + class HeapAllocation { + public: + HeapAllocation() = default; + + HeapAllocation(const HeapAllocation&) = delete; + + HeapAllocation& operator=(const HeapAllocation&) = delete; + + HeapAllocation(HeapAllocation&& other) noexcept + : data_(other.data_), size_(other.size_), capacity_(other.capacity_) { + other.Clear(); + } + + HeapAllocation& operator=(HeapAllocation&& other) noexcept { + if (this == &other) return *this; + + Reset(); + data_ = other.data_; + size_ = other.size_; + capacity_ = other.capacity_; + other.Clear(); + return *this; + } + + ~HeapAllocation() { Reset(); } + + T* data() noexcept { return data_; } + + const T* data() const noexcept { return data_; } + + size_type size() const noexcept { return size_; } + + size_type capacity() const noexcept { return capacity_; } + + bool empty() const noexcept { return data_ == nullptr; } + + T* release() noexcept { + T* data = data_; + Clear(); + return data; + } + + private: + friend class SmallVector; + + HeapAllocation(T* data, size_type size, size_type capacity) noexcept + : data_(data), size_(size), capacity_(capacity) {} + + void Clear() noexcept { + data_ = nullptr; + size_ = 0; + capacity_ = 0; + } + + void Reset() noexcept { + if (data_ != nullptr) Deallocate(data_, capacity_); + Clear(); + } + + T* data_{nullptr}; + + size_type size_{0}; + + size_type capacity_{0}; + }; + + SmallVector() = default; + + explicit SmallVector(size_type count) { InitializeCount(count); } + + SmallVector(size_type count, const T& value) { InitializeFill(count, value); } + + SmallVector(std::initializer_list values) + : SmallVector(values.begin(), values.end()) {} + + template , int> = 0> + SmallVector(InputIt first, InputIt last) { + using IteratorCategory = + typename std::iterator_traits::iterator_category; + + if constexpr (std::is_base_of_v) { + InitializeForwardRange(first, last); + } else { + SmallVector replacement; + replacement.InitializeInputRange(first, last); + MoveConstructFrom(replacement); + } + } + + template < + typename Container, + std::enable_if_t, SmallVector> && + IsCompatibleContainer::value, + int> = 0> + explicit SmallVector(const Container& container) + : SmallVector(std::begin(container), std::end(container)) {} + + SmallVector(const SmallVector& other) { CopyConstructFrom(other); } + + SmallVector(SmallVector&& other) noexcept { MoveConstructFrom(other); } + + SmallVector& operator=(const SmallVector& other) { + if (this == &other) return *this; + + if (other.IsHeap()) { + CopyAssignHeap(other); + } else { + AssignInline(other.data(), other.size_); + } + + return *this; + } + + SmallVector& operator=(SmallVector&& other) noexcept { + if (this == &other) return *this; + + if (other.IsHeap()) { + MoveAssignHeap(other); + } else { + AssignInline(other.data(), other.size_); + other.clear(); + } + + return *this; + } + + ~SmallVector() { + if (IsHeap()) Deallocate(storage_.heap_data, capacity_); + } + + size_type size() const noexcept { return size_; } + + size_type capacity() const noexcept { return capacity_; } + + bool empty() const noexcept { return size_ == 0; } + + T* data() noexcept { + return IsHeap() ? storage_.heap_data : storage_.inline_storage.data; + } + + const T* data() const noexcept { + return IsHeap() ? storage_.heap_data : storage_.inline_storage.data; + } + + T& front() noexcept { return data()[0]; } + + const T& front() const noexcept { return data()[0]; } + + T& back() noexcept { return data()[size_ - 1]; } + + const T& back() const noexcept { return data()[size_ - 1]; } + + T& operator[](size_type index) noexcept { return data()[index]; } + + const T& operator[](size_type index) const noexcept { return data()[index]; } + + iterator begin() noexcept { return data(); } + + const_iterator begin() const noexcept { return data(); } + + const_iterator cbegin() const noexcept { return data(); } + + iterator end() noexcept { return data() + size_; } + + const_iterator end() const noexcept { return data() + size_; } + + const_iterator cend() const noexcept { return data() + size_; } + + void clear() noexcept { size_ = 0; } + + void reserve(size_type requested_capacity) { + if (requested_capacity <= capacity_) return; + + Reallocate(requested_capacity); + } + + void resize(size_type count) { + if (count <= size_) { + size_ = count; + return; + } + + if (count > capacity_) Reallocate(count); + + while (size_ < count) { + ConstructValue(data() + size_); + ++size_; + } + } + + void push_back(const T& value) { + if (size_ == capacity_) { + T saved_value(value); + GrowForAppend(); + Construct(data() + size_, saved_value); + } else { + Construct(data() + size_, value); + } + + ++size_; + } + + template , int> = 0> + void assign(InputIt first, InputIt last) { + SmallVector replacement(first, last); + *this = std::move(replacement); + } + + void assign(std::initializer_list values) { + assign(values.begin(), values.end()); + } + + HeapAllocation ReleaseHeap() noexcept { + if (!IsHeap()) return {}; + + HeapAllocation allocation{storage_.heap_data, size_, capacity_}; + ReconstructInline(); + return allocation; + } + + private: + using Allocator = std::allocator; + + using AllocatorTraits = std::allocator_traits; + + struct InlineStorage { + T data[inline_capacity]; + + InlineStorage() noexcept {} + }; + + union Storage { + InlineStorage inline_storage; + + T* heap_data; + + Storage() noexcept : inline_storage() {} + }; + + bool IsHeap() const noexcept { return capacity_ > inline_capacity; } + + static void Construct(T* destination, const T& value) { + ::new (static_cast(destination)) T(value); + } + + static void ConstructValue(T* destination) { + ::new (static_cast(destination)) T{}; + } + + static void Deallocate(T* pointer, size_type capacity) noexcept { + Allocator allocator; + AllocatorTraits::deallocate(allocator, pointer, capacity); + } + + void InitializeCount(size_type count) { + if (count <= inline_capacity) { + std::fill_n(storage_.inline_storage.data, count, T{}); + size_ = count; + + return; + } + + Allocator allocator; + HeapAllocation allocation{AllocatorTraits::allocate(allocator, count), + count, count}; + ::new (static_cast(allocation.data())) T[count]{}; + ReplaceWithHeap(allocation.release(), count, count); + } + + void InitializeFill(size_type count, const T& value) { + if (count <= inline_capacity) { + std::fill_n(storage_.inline_storage.data, count, value); + size_ = count; + + return; + } + + Allocator allocator; + HeapAllocation allocation{AllocatorTraits::allocate(allocator, count), + count, count}; + std::uninitialized_fill_n(allocation.data(), count, value); + ReplaceWithHeap(allocation.release(), count, count); + } + + template + void InitializeForwardRange(ForwardIt first, ForwardIt last) { + if (first == last) return; + + const size_type count = static_cast(std::distance(first, last)); + + if (count <= inline_capacity) { + CopyToInline(first, last, storage_.inline_storage.data); + size_ = count; + + return; + } + + Allocator allocator; + + if constexpr (IsSamePointerRange()) { + HeapAllocation allocation{AllocatorTraits::allocate(allocator, count), + count, count}; + ::new (static_cast(allocation.data())) T[count]; + std::copy(first, last, allocation.data()); + ReplaceWithHeap(allocation.release(), count, count); + + return; + } + + HeapAllocation allocation{AllocatorTraits::allocate(allocator, count), + count, count}; + UninitializedCopy(first, last, allocation.data()); + ReplaceWithHeap(allocation.release(), count, count); + } + + template + void InitializeInputRange(InputIt first, InputIt last) { + for (; first != last; ++first) push_back(static_cast(*first)); + } + + template + static constexpr bool IsSamePointerRange() { + return std::is_pointer_v && + std::is_same_v>, + T>; + } + + template + static void CopyToInline(ForwardIt first, ForwardIt last, T* destination) { + if constexpr (IsSamePointerRange()) { + std::copy(first, last, destination); + } else { + for (; first != last; ++first, ++destination) { + *destination = static_cast(*first); + } + } + } + + template + static void UninitializedCopy(ForwardIt first, ForwardIt last, + T* destination) { + using Source = + std::remove_cv_t::value_type>; + + if constexpr (std::is_same_v) { + std::uninitialized_copy(first, last, destination); + } else { + for (; first != last; ++first, ++destination) { + Construct(destination, static_cast(*first)); + } + } + } + + void CopyConstructFrom(const SmallVector& other) { + if (other.IsHeap()) Reallocate(other.capacity_); + + for (; size_ < other.size_; ++size_) { + Construct(data() + size_, other.data()[size_]); + } + } + + void MoveConstructFrom(SmallVector& other) noexcept { + if (other.IsHeap()) { + T* heap_data = other.storage_.heap_data; + ::new (static_cast(&storage_.heap_data)) T*(heap_data); + size_ = other.size_; + capacity_ = other.capacity_; + other.ReconstructInline(); + return; + } + + for (; size_ < other.size_; ++size_) { + Construct(data() + size_, other.data()[size_]); + } + other.clear(); + } + + void CopyAssignHeap(const SmallVector& other) { + Allocator allocator; + T* new_data = AllocatorTraits::allocate(allocator, other.capacity_); + for (size_type index = 0; index < other.size_; ++index) { + Construct(new_data + index, other.data()[index]); + } + + ReplaceWithHeap(new_data, other.size_, other.capacity_); + } + + void MoveAssignHeap(SmallVector& other) { + T* heap_data = other.storage_.heap_data; + ReplaceWithHeap(heap_data, other.size_, other.capacity_); + other.ReconstructInline(); + } + + void AssignInline(const T* values, size_type count) { + if (IsHeap()) SwitchToInline(); + + for (size_type index = 0; index < count; ++index) { + Construct(storage_.inline_storage.data + index, values[index]); + } + size_ = count; + } + + void ReplaceWithHeap(T* new_data, size_type new_size, + size_type new_capacity) { + T* old_data = nullptr; + size_type old_capacity = 0; + + if (IsHeap()) { + old_data = storage_.heap_data; + old_capacity = capacity_; + storage_.heap_data = new_data; + } else { + ::new (static_cast(&storage_.heap_data)) T*(new_data); + } + + size_ = new_size; + capacity_ = new_capacity; + if (old_data != nullptr) Deallocate(old_data, old_capacity); + } + + void SwitchToInline() { + T* old_data = storage_.heap_data; + const size_type old_capacity = capacity_; + ReconstructInline(); + Deallocate(old_data, old_capacity); + } + + void ReconstructInline() { + storage_.~Storage(); + ::new (static_cast(&storage_)) Storage(); + size_ = 0; + capacity_ = inline_capacity; + } + + void Reallocate(size_type new_capacity) { + Allocator allocator; + T* new_data = AllocatorTraits::allocate(allocator, new_capacity); + const T* old_data = data(); + for (size_type index = 0; index < size_; ++index) { + Construct(new_data + index, old_data[index]); + } + + ReplaceWithHeap(new_data, size_, new_capacity); + } + + void GrowForAppend() { + Allocator allocator; + const size_type max_size = AllocatorTraits::max_size(allocator); + size_type increment = capacity_ / 2; + if (increment == 0) increment = 1; + + const size_type new_capacity = + increment > max_size - capacity_ ? max_size : capacity_ + increment; + Reallocate(new_capacity); + } + + Storage storage_; + + size_type size_{0}; + + size_type capacity_{inline_capacity}; +}; + +template , T>::value, + int> = 0> +bool operator==(const SmallVector& left, + const SmallVector& right) { + if (left.size() != right.size()) return false; + + for (std::size_t index = 0; index < left.size(); ++index) { + if (!(left[index] == right[index])) return false; + } + + return true; +} + +template , T>::value, + int> = 0> +bool operator!=(const SmallVector& left, + const SmallVector& right) { + return !(left == right); +} + +template >::value && + IsEqualityComparableRange::value, + int> = 0> +bool operator==(const SmallVector& left, + const Range& right) { + if (left.size() != static_cast(std::size(right))) return false; + + auto right_iterator = std::begin(right); + for (std::size_t index = 0; index < left.size(); ++index, ++right_iterator) { + if (!(left[index] == *right_iterator)) return false; + } + + return true; +} + +template >::value && + IsEqualityComparableRange::value, + int> = 0> +bool operator==(const Range& left, + const SmallVector& right) { + return right == left; +} + +template >::value && + IsEqualityComparableRange::value, + int> = 0> +bool operator!=(const SmallVector& left, + const Range& right) { + return !(left == right); +} + +template >::value && + IsEqualityComparableRange::value, + int> = 0> +bool operator!=(const Range& left, + const SmallVector& right) { + return !(right == left); +} + +} // namespace infini::rt::detail + +#endif diff --git a/src/tensor_view.cc b/src/tensor_view.cc index a19fbaa..0a1e1d1 100644 --- a/src/tensor_view.cc +++ b/src/tensor_view.cc @@ -2,73 +2,77 @@ #include #include -#include #include "dispatcher.h" namespace infini::rt { -static TensorView::Index GetEffectiveIndex(TensorView::Index index, - TensorView::Size size) { - return index < 0 ? index + size : index; -} - TensorView::TensorView(void* data, std::initializer_list shape, const DataType& dtype, const Device& device, std::initializer_list strides) : data_{data}, - shape_{shape}, + shape_strides_storage_{shape, strides}, dtype_{dtype}, - device_{device}, - strides_{strides} {} + device_{device} {} TensorView TensorView::operator[](const Index& index) const { - return { - reinterpret_cast( - reinterpret_cast(data_) + - GetEffectiveIndex(index, shape_[0]) * strides_[0] * element_size()), - Shape{shape_.cbegin() + 1, shape_.cend()}, dtype_, device_, - Strides{strides_.cbegin() + 1, strides_.cend()}}; + const ShapeView shape_view = shape(); + const StridesView strides_view = strides(); + + return {reinterpret_cast( + reinterpret_cast(data_) + + GetEffectiveIndex(index, shape_view[0]) * strides_view[0] * + element_size()), + ShapeView{shape_view.data() + 1, shape_view.size() - 1}, dtype_, + device_, + StridesView{strides_view.data() + 1, strides_view.size() - 1}}; } void*& TensorView::data() { return data_; } const void* TensorView::data() const { return data_; } -const TensorView::Shape& TensorView::shape() const { return shape_; } - const DataType& TensorView::dtype() const { return dtype_; } const Device& TensorView::device() const { return device_; } -const TensorView::Strides& TensorView::strides() const { return strides_; } +TensorView::Shape TensorView::shape() && { + const ShapeView view = shape_strides_storage_.shape(); -TensorView::Size TensorView::size(const Index& index) const { - return shape_[GetEffectiveIndex(index, shape_.size())]; + return Shape{view.begin(), view.end()}; } -TensorView::Stride TensorView::stride(const Index& index) const { - return strides_[GetEffectiveIndex(index, strides_.size())]; +TensorView::Shape TensorView::shape() const&& { + const ShapeView view = shape_strides_storage_.shape(); + + return Shape{view.begin(), view.end()}; } -TensorView::Size TensorView::ndim() const { return shape_.size(); } +TensorView::Strides TensorView::strides() && { + const StridesView view = shape_strides_storage_.strides(); -TensorView::Size TensorView::element_size() const { - return kDataTypeToSize.at(dtype_); + return Strides{view.begin(), view.end()}; } -TensorView::Size TensorView::numel() const { - return std::accumulate( - shape_.begin(), shape_.end(), static_cast(1), - [](TensorView::Size a, TensorView::Size b) { return a * b; }); +TensorView::Strides TensorView::strides() const&& { + const StridesView view = shape_strides_storage_.strides(); + + return Strides{view.begin(), view.end()}; +} + +TensorView::Size TensorView::element_size() const { + return kDataTypeToSize.at(dtype_); } TensorView TensorView::T() const { + const ShapeView shape_view = shape(); + const StridesView strides_view = strides(); + return {data_, - {shape_[1], shape_[0]}, + {shape_view[1], shape_view[0]}, dtype_, device_, - {strides_[1], strides_[0]}}; + {strides_view[1], strides_view[0]}}; } std::string TensorView::ToString() const { @@ -78,9 +82,12 @@ std::string TensorView::ToString() const { } bool TensorView::HasBroadcastDim() const { - return std::any_of(shape_.begin(), shape_.end(), + const ShapeView shape_view = shape(); + const StridesView strides_view = strides(); + + return std::any_of(shape_view.begin(), shape_view.end(), [&, i = 0](const auto&) mutable { - return shape_[i] != 1 && strides_[i++] == 0; + return shape_view[i] != 1 && strides_view[i++] == 0; }); } @@ -100,22 +107,6 @@ const DataType TensorView::DefaultDataType() { return DataType::kFloat32; } Device TensorView::DefaultDevice() { return Device{Device::Type::kCpu}; } -TensorView::Strides TensorView::DefaultStrides(const Shape& shape) { - if (shape.empty()) { - return {}; - } - - Strides strides(shape.size()); - - strides.back() = 1; - - for (auto i{shape.size() - 2}; i != -1; --i) { - strides[i] = strides[i + 1] * shape[i + 1]; - } - - return strides; -} - std::string TensorView::ToStringHelper() const { if (ndim() == 0) { return DispatchFunc #include +#include #include #include #include -#include +#include "common/shape_strides_storage.h" #include "data_type.h" #include "device.h" #include "hash.h" @@ -15,6 +17,8 @@ namespace infini::rt { namespace tensor_view_detail { +inline constexpr std::size_t kInlineMetadataCapacity = 8; + template struct IsTensorLike : std::false_type {}; @@ -37,98 +41,143 @@ class TensorView { using Index = Stride; - using Shape = std::vector; + private: + using ShapeStridesStorage = + detail::ShapeStridesStorage; + + public: + using Shape = typename ShapeStridesStorage::Shape; - using Strides = std::vector; + using Strides = typename ShapeStridesStorage::Strides; + + using ShapeView = typename ShapeStridesStorage::ShapeView; + + using StridesView = typename ShapeStridesStorage::StridesView; template ::value>> TensorView(const TensorLike& tensor) : data_{const_cast(static_cast(tensor.data()))}, - shape_{tensor.shape()}, + shape_strides_storage_{tensor.shape(), tensor.strides()}, dtype_{tensor.dtype()}, - device_{tensor.device()}, - strides_{tensor.strides()} {} + device_{tensor.device()} {} + + TensorView(void* data, const Shape& shape) + : data_{data}, + shape_strides_storage_{shape, detail::DefaultStridesTag{}}, + dtype_{DefaultDataType()}, + device_{DefaultDevice()} {} - TensorView(void* data, Shape shape) + TensorView(void* data, Shape&& shape) : data_{data}, - shape_{std::move(shape)}, + shape_strides_storage_{std::move(shape), detail::DefaultStridesTag{}}, dtype_{DefaultDataType()}, - device_{DefaultDevice()}, - strides_{DefaultStrides(shape_)} {} + device_{DefaultDevice()} {} - template - TensorView(void* data, const Shape& shape) + template + TensorView(void* data, const ShapeLike& shape) : data_{data}, - shape_{shape}, + shape_strides_storage_{shape, detail::DefaultStridesTag{}}, dtype_{DefaultDataType()}, - device_{DefaultDevice()}, - strides_{DefaultStrides(shape)} {} + device_{DefaultDevice()} {} - TensorView(void* data, Shape shape, const DataType& dtype) + TensorView(void* data, const Shape& shape, const DataType& dtype) : data_{data}, - shape_{std::move(shape)}, + shape_strides_storage_{shape, detail::DefaultStridesTag{}}, dtype_{dtype}, - device_{DefaultDevice()}, - strides_{DefaultStrides(shape_)} {} + device_{DefaultDevice()} {} - template - TensorView(void* data, const Shape& shape, const DataType& dtype) + TensorView(void* data, Shape&& shape, const DataType& dtype) : data_{data}, - shape_{shape}, + shape_strides_storage_{std::move(shape), detail::DefaultStridesTag{}}, dtype_{dtype}, - device_{DefaultDevice()}, - strides_{DefaultStrides(shape)} {} + device_{DefaultDevice()} {} - TensorView(void* data, Shape shape, const Device& device) + template + TensorView(void* data, const ShapeLike& shape, const DataType& dtype) : data_{data}, - shape_{std::move(shape)}, - dtype_{DefaultDataType()}, - device_{device}, - strides_{DefaultStrides(shape_)} {} + shape_strides_storage_{shape, detail::DefaultStridesTag{}}, + dtype_{dtype}, + device_{DefaultDevice()} {} - template TensorView(void* data, const Shape& shape, const Device& device) : data_{data}, - shape_{shape}, + shape_strides_storage_{shape, detail::DefaultStridesTag{}}, + dtype_{DefaultDataType()}, + device_{device} {} + + TensorView(void* data, Shape&& shape, const Device& device) + : data_{data}, + shape_strides_storage_{std::move(shape), detail::DefaultStridesTag{}}, + dtype_{DefaultDataType()}, + device_{device} {} + + template + TensorView(void* data, const ShapeLike& shape, const Device& device) + : data_{data}, + shape_strides_storage_{shape, detail::DefaultStridesTag{}}, dtype_{DefaultDataType()}, - device_{device}, - strides_{DefaultStrides(shape)} {} + device_{device} {} - TensorView(void* data, Shape shape, const DataType& dtype, + TensorView(void* data, const Shape& shape, const DataType& dtype, const Device& device) : data_{data}, - shape_{std::move(shape)}, + shape_strides_storage_{shape, detail::DefaultStridesTag{}}, dtype_{dtype}, - device_{device}, - strides_{DefaultStrides(shape_)} {} + device_{device} {} - template - TensorView(void* data, const Shape& shape, const DataType& dtype, + TensorView(void* data, Shape&& shape, const DataType& dtype, const Device& device) : data_{data}, - shape_{shape}, + shape_strides_storage_{std::move(shape), detail::DefaultStridesTag{}}, dtype_{dtype}, - device_{device}, - strides_{DefaultStrides(shape)} {} + device_{device} {} - TensorView(void* data, Shape shape, const DataType& dtype, - const Device& device, Strides strides) + template + TensorView(void* data, const ShapeLike& shape, const DataType& dtype, + const Device& device) : data_{data}, - shape_{std::move(shape)}, + shape_strides_storage_{shape, detail::DefaultStridesTag{}}, dtype_{dtype}, - device_{device}, - strides_{std::move(strides)} {} + device_{device} {} - template TensorView(void* data, const Shape& shape, const DataType& dtype, const Device& device, const Strides& strides) : data_{data}, - shape_{shape}, + shape_strides_storage_{shape, strides}, dtype_{dtype}, - device_{device}, - strides_{strides} {} + device_{device} {} + + TensorView(void* data, Shape&& shape, const DataType& dtype, + const Device& device, Strides&& strides) + : data_{data}, + shape_strides_storage_{std::move(shape), std::move(strides)}, + dtype_{dtype}, + device_{device} {} + + TensorView(void* data, Shape&& shape, const DataType& dtype, + const Device& device, const Strides& strides) + : data_{data}, + shape_strides_storage_{std::move(shape), strides}, + dtype_{dtype}, + device_{device} {} + + TensorView(void* data, const Shape& shape, const DataType& dtype, + const Device& device, Strides&& strides) + : data_{data}, + shape_strides_storage_{shape, std::move(strides)}, + dtype_{dtype}, + device_{device} {} + + template + TensorView(void* data, const ShapeLike& shape, const DataType& dtype, + const Device& device, const StridesLike& strides) + : data_{data}, + shape_strides_storage_{shape, strides}, + dtype_{dtype}, + device_{device} {} TensorView(void* data, std::initializer_list shape, const DataType& dtype, const Device& device, @@ -144,19 +193,49 @@ class TensorView { const Device& device() const; - const Shape& shape() const; + ShapeView shape() const& noexcept { return shape_strides_storage_.shape(); } - const Strides& strides() const; + Shape shape() &&; - Size size(const Index& index) const; + Shape shape() const&&; - Stride stride(const Index& index) const; + StridesView strides() const& noexcept { + return shape_strides_storage_.strides(); + } - Size ndim() const; + Strides strides() &&; + + Strides strides() const&&; + + Size size(const Index& index) const noexcept { + const Size rank = shape_strides_storage_.shape_size(); + + return shape_strides_storage_ + .shape_data()[static_cast(GetEffectiveIndex(index, rank))]; + } + + Stride stride(const Index& index) const noexcept { + const Size rank = shape_strides_storage_.strides_size(); + + return shape_strides_storage_ + .strides_data()[static_cast(GetEffectiveIndex(index, rank))]; + } + + Size ndim() const noexcept { return shape_strides_storage_.shape_size(); } Size element_size() const; - Size numel() const; + Size numel() const noexcept { + const Size rank = shape_strides_storage_.shape_size(); + const Size* const shape_data = shape_strides_storage_.shape_data(); + Size result = 1; + + for (Size axis = 0; axis < rank; ++axis) { + result *= shape_data[axis]; + } + + return result; + } TensorView T() const; @@ -167,25 +246,25 @@ class TensorView { bool IsContiguous() const; private: + static constexpr Index GetEffectiveIndex(Index index, Size size) noexcept { + return index < 0 ? index + static_cast(size) : index; + } + static const DataType DefaultDataType(); static Device DefaultDevice(); - static Strides DefaultStrides(const Shape& shape); - std::string ToStringHelper() const; bool IsMergeable(Size dim_start, Size dim_end) const; void* data_{nullptr}; - Shape shape_; + ShapeStridesStorage shape_strides_storage_; const DataType dtype_; Device device_; - - Strides strides_; }; } // namespace infini::rt diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c6a84dc..bb05ed2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -41,6 +41,9 @@ endfunction() add_infini_rt_test(test_smoke test_smoke.cc) add_infini_rt_test(test_core test_core.cc) +add_infini_rt_test(test_small_vector test_small_vector.cc) +add_infini_rt_test(test_metadata_view test_metadata_view.cc) +add_infini_rt_test(test_shape_strides_storage test_shape_strides_storage.cc) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_infini_rt_test(test_tensor_view_allocations test_tensor_view_allocations.cc) diff --git a/tests/install_consumer_smoke.cc b/tests/install_consumer_smoke.cc index 9a540f3..968c144 100644 --- a/tests/install_consumer_smoke.cc +++ b/tests/install_consumer_smoke.cc @@ -8,14 +8,36 @@ int main() { std::vector data{1.0f, 2.0f, 3.0f, 4.0f}; const infini::rt::Device device{infini::rt::Device::Type::kCpu}; - const infini::rt::TensorView tensor{data.data(), std::vector{4}, - infini::rt::DataType::kFloat32, device}; + const std::vector shape{2, 2}; + const std::vector default_strides{2, 1}; + const std::vector explicit_strides{1, 2}; + const infini::rt::TensorView::Strides filled_strides(shape.size(), 0); + const infini::rt::TensorView default_view{ + data.data(), shape, infini::rt::DataType::kFloat32, device}; + const infini::rt::TensorView explicit_view{data.data(), shape, + infini::rt::DataType::kFloat32, + device, explicit_strides}; if (device.ToString() != "cpu:0") { return 1; } - if (tensor.numel() != 4 || !tensor.IsContiguous()) { + if (filled_strides.size() != shape.size() || filled_strides[0] != 0 || + filled_strides[1] != 0) { + return 1; + } + + if (default_view.numel() != 4 || !default_view.IsContiguous() || + default_view.shape() != shape || + default_view.strides() != default_strides || default_view.size(-1) != 2 || + default_view.stride(-1) != 1) { + return 1; + } + + if (explicit_view.numel() != 4 || explicit_view.IsContiguous() || + explicit_view.shape() != shape || + explicit_view.strides() != explicit_strides || + explicit_view.size(0) != 2 || explicit_view.stride(0) != 1) { return 1; } diff --git a/tests/performance/CMakeLists.txt b/tests/performance/CMakeLists.txt index ad69724..7db86de 100644 --- a/tests/performance/CMakeLists.txt +++ b/tests/performance/CMakeLists.txt @@ -30,3 +30,5 @@ endfunction() add_infini_rt_performance_test(perf_runtime_dispatch perf_runtime_dispatch.cc) add_infini_rt_performance_test(perf_memory perf_memory.cc) add_infini_rt_performance_test(perf_tensor_view perf_tensor_view.cc) +add_infini_rt_performance_test(perf_tensor_view_footprint + perf_tensor_view_footprint.cc) diff --git a/tests/performance/perf_common.h b/tests/performance/perf_common.h index 5fc5a9e..cb617ee 100644 --- a/tests/performance/perf_common.h +++ b/tests/performance/perf_common.h @@ -15,6 +15,14 @@ #include #include +#if defined(_MSC_VER) +#define INFINI_RT_NOINLINE __declspec(noinline) +#elif defined(__GNUC__) || defined(__clang__) +#define INFINI_RT_NOINLINE __attribute__((noinline)) +#else +#define INFINI_RT_NOINLINE +#endif + #ifndef INFINI_RT_PERF_BACKEND_NAME #define INFINI_RT_PERF_BACKEND_NAME "unknown" #endif diff --git a/tests/performance/perf_tensor_view.cc b/tests/performance/perf_tensor_view.cc index d9e9b87..9329d2a 100644 --- a/tests/performance/perf_tensor_view.cc +++ b/tests/performance/perf_tensor_view.cc @@ -1,8 +1,20 @@ #include +#if defined(__has_include) +#if __has_include() +#include +#define INFINI_RT_HAS_SMALL_VECTOR 1 +#endif +#endif + +#ifndef INFINI_RT_HAS_SMALL_VECTOR +#define INFINI_RT_HAS_SMALL_VECTOR 0 +#endif + #include #include #include +#include #include #include "perf_common.h" @@ -15,86 +27,265 @@ using infini::rt::DataType; using infini::rt::Device; using infini::rt::TensorView; -} // namespace +constexpr std::size_t kIterations = 200000; -int main() { - constexpr std::size_t kIterations = 200000; - std::array data{}; - const TensorView::Shape shape{32, 64}; - const TensorView::Strides contiguous_strides{64, 1}; - const TensorView::Strides transposed_strides{1, 32}; - const Device cpu_device{Device::Type::kCpu}; +struct VectorTensorLike { + void* data_value; - perf::RunBenchmark("perf_tensor_view.construct_contiguous", - {perf::NumberParam("ndim", 2)}, kIterations, "ns", [&] { - TensorView tensor{data.data(), shape, DataType::kFloat32, - cpu_device, contiguous_strides}; - perf::DoNotOptimize(tensor); - }); + std::vector shape_value; - perf::RunBenchmark("perf_tensor_view.construct_rvalue_full_metadata", - {perf::NumberParam("ndim", 2)}, kIterations, "ns", [&] { - TensorView tensor{data.data(), TensorView::Shape{32, 64}, - DataType::kFloat32, cpu_device, - TensorView::Strides{64, 1}}; - perf::DoNotOptimize(tensor); - }); + DataType dtype_value; + + Device device_value; + + std::vector strides_value; + + void* data() const { return data_value; } + + const std::vector& shape() const { return shape_value; } + + DataType dtype() const { return dtype_value; } + + Device device() const { return device_value; } + + const std::vector& strides() const { return strides_value; } +}; + +INFINI_RT_NOINLINE std::size_t ConsumeTensorView(TensorView tensor) { + perf::DoNotOptimize(tensor.data()); + return tensor.ndim() + tensor.size(0) + + static_cast(tensor.stride(0)); +} + +INFINI_RT_NOINLINE std::size_t ConsumeTensorMetadata(const TensorView& tensor) { + const auto rank = tensor.ndim(); + std::size_t result = rank; - perf::RunBenchmark("perf_tensor_view.construct_rvalue_default_strides", - {perf::NumberParam("ndim", 2)}, kIterations, "ns", [&] { - TensorView tensor{data.data(), TensorView::Shape{32, 64}, - DataType::kFloat32, cpu_device}; + for (TensorView::Index axis = 0; axis < static_cast(rank); + ++axis) { + result += tensor.size(axis); + result += static_cast(tensor.stride(axis)); + } + + return result; +} + +template +std::array MakeShape() { + std::array shape{}; + shape.fill(2); + return shape; +} + +template +std::array MakeStrides( + const std::array& shape) { + std::array strides{}; + TensorView::Stride stride = 1; + + for (std::size_t i = rank; i > 0; --i) { + strides[i - 1] = stride; + stride *= static_cast(shape[i - 1]); + } + + return strides; +} + +template +TensorView MakeInitializerListTensor(float* data, const Device& device); + +template <> +TensorView MakeInitializerListTensor<1>(float* data, const Device& device) { + return TensorView{data, {2}, DataType::kFloat32, device, {1}}; +} + +template <> +TensorView MakeInitializerListTensor<2>(float* data, const Device& device) { + return TensorView{data, {2, 2}, DataType::kFloat32, device, {2, 1}}; +} + +template <> +TensorView MakeInitializerListTensor<4>(float* data, const Device& device) { + return TensorView{ + data, {2, 2, 2, 2}, DataType::kFloat32, device, {8, 4, 2, 1}}; +} + +template <> +TensorView MakeInitializerListTensor<5>(float* data, const Device& device) { + return TensorView{ + data, {2, 2, 2, 2, 2}, DataType::kFloat32, device, {16, 8, 4, 2, 1}}; +} + +template <> +TensorView MakeInitializerListTensor<8>(float* data, const Device& device) { + return TensorView{data, + {2, 2, 2, 2, 2, 2, 2, 2}, + DataType::kFloat32, + device, + {128, 64, 32, 16, 8, 4, 2, 1}}; +} + +template <> +TensorView MakeInitializerListTensor<9>(float* data, const Device& device) { + return TensorView{data, + {2, 2, 2, 2, 2, 2, 2, 2, 2}, + DataType::kFloat32, + device, + {256, 128, 64, 32, 16, 8, 4, 2, 1}}; +} + +template +void RunRankBenchmarks(float* data, const Device& device) { + const auto shape_values = MakeShape(); + const auto stride_values = MakeStrides(shape_values); + const TensorView::Shape shape{shape_values.begin(), shape_values.end()}; + const TensorView::Strides strides{stride_values.begin(), stride_values.end()}; + const VectorTensorLike tensor_like{ + data, + {shape_values.begin(), shape_values.end()}, + DataType::kFloat32, + device, + {stride_values.begin(), stride_values.end()}}; + const TensorView source{data, shape, DataType::kFloat32, device, strides}; + const auto params = std::vector{perf::NumberParam("ndim", rank)}; + + perf::RunBenchmark("perf_tensor_view.construct_lvalue_explicit", params, + kIterations, "ns", [&] { + TensorView tensor{data, shape, DataType::kFloat32, + device, strides}; perf::DoNotOptimize(tensor); }); perf::RunBenchmark( - "perf_tensor_view.construct_initializer_list", - {perf::NumberParam("ndim", 2)}, kIterations, "ns", [&] { + "perf_tensor_view.construct_rvalue_explicit", params, kIterations, "ns", + [&] { TensorView tensor{ - data.data(), {32, 64}, DataType::kFloat32, cpu_device, {64, 1}}; + data, TensorView::Shape{shape_values.begin(), shape_values.end()}, + DataType::kFloat32, device, + TensorView::Strides{stride_values.begin(), stride_values.end()}}; perf::DoNotOptimize(tensor); }); - TensorView contiguous{data.data(), shape, DataType::kFloat32, cpu_device, - contiguous_strides}; - TensorView transposed{data.data(), shape, DataType::kFloat32, cpu_device, - transposed_strides}; + perf::RunBenchmark( + "perf_tensor_view.construct_default_strides", params, kIterations, "ns", + [&] { + TensorView tensor{ + data, TensorView::Shape{shape_values.begin(), shape_values.end()}, + DataType::kFloat32, device}; + perf::DoNotOptimize(tensor); + }); - perf::RunBenchmark("perf_tensor_view.transpose", - {perf::NumberParam("ndim", 2)}, kIterations, "ns", [&] { - const auto result = contiguous.T(); - perf::DoNotOptimize(result); + perf::RunBenchmark("perf_tensor_view.construct_initializer_list", params, + kIterations, "ns", [&] { + const auto tensor = + MakeInitializerListTensor(data, device); + perf::DoNotOptimize(tensor); }); - perf::RunBenchmark("perf_tensor_view.operator_index", - {perf::NumberParam("ndim", 2)}, kIterations, "ns", [&] { - const auto slice = contiguous[1]; - perf::DoNotOptimize(slice); + perf::RunBenchmark("perf_tensor_view.construct_tensor_like", params, + kIterations, "ns", [&] { + TensorView tensor{tensor_like}; + perf::DoNotOptimize(tensor); }); - perf::RunBenchmark("perf_tensor_view.numel", {perf::NumberParam("ndim", 2)}, - kIterations, "ns", [&] { - const auto count = contiguous.numel(); - perf::DoNotOptimize(count); + perf::RunBenchmark("perf_tensor_view.copy", params, kIterations, "ns", [&] { + TensorView tensor{source}; + perf::DoNotOptimize(tensor); + }); + + perf::RunBenchmark("perf_tensor_view.operator_index", params, kIterations, + "ns", [&] { + const auto tensor = source[0]; + perf::DoNotOptimize(tensor); }); - perf::RunBenchmark("perf_tensor_view.is_contiguous_true", - {perf::NumberParam("ndim", 2)}, kIterations, "ns", [&] { - const auto result = contiguous.IsContiguous(); - perf::DoNotOptimize(result); + perf::RunBenchmark("perf_tensor_view.pass_by_value", params, kIterations, + "ns", [&] { + const auto value = ConsumeTensorView(source); + perf::DoNotOptimize(value); }); - perf::RunBenchmark("perf_tensor_view.is_contiguous_false", - {perf::NumberParam("ndim", 2)}, kIterations, "ns", [&] { - const auto result = transposed.IsContiguous(); - perf::DoNotOptimize(result); + perf::RunBenchmark("perf_tensor_view.metadata_access", params, kIterations, + "ns", [&] { + const auto value = ConsumeTensorMetadata(source); + perf::DoNotOptimize(value); }); - perf::RunBenchmark("perf_tensor_view.hash", {perf::NumberParam("ndim", 2)}, + perf::RunBenchmark("perf_tensor_view.numel", params, kIterations, "ns", [&] { + const auto value = source.numel(); + perf::DoNotOptimize(value); + }); +} + +void RunRank2Controls(float* data, const Device& device) { + const TensorView::Shape shape{2, 2}; + const TensorView::Strides contiguous_strides{2, 1}; + const TensorView::Strides transposed_strides{1, 2}; + const TensorView contiguous{data, shape, DataType::kFloat32, device, + contiguous_strides}; + const TensorView transposed{data, shape, DataType::kFloat32, device, + transposed_strides}; + const auto params = std::vector{perf::NumberParam("ndim", 2)}; + + perf::RunBenchmark("perf_tensor_view.transpose", params, kIterations, "ns", + [&] { + const auto tensor = contiguous.T(); + perf::DoNotOptimize(tensor); + }); + + perf::RunBenchmark("perf_tensor_view.is_contiguous_true", params, kIterations, + "ns", [&] { + const auto value = contiguous.IsContiguous(); + perf::DoNotOptimize(value); + }); + + perf::RunBenchmark("perf_tensor_view.is_contiguous_false", params, kIterations, "ns", [&] { - const auto value = std::hash{}(contiguous); + const auto value = transposed.IsContiguous(); perf::DoNotOptimize(value); }); + perf::RunBenchmark("perf_tensor_view.hash", params, kIterations, "ns", [&] { + const auto value = std::hash{}(contiguous); + perf::DoNotOptimize(value); + }); +} + +} // namespace + +int main() { + std::cerr << "sizeof(TensorView)=" << sizeof(TensorView) + << " sizeof(Shape)=" << sizeof(TensorView::Shape) + << " sizeof(Strides)=" << sizeof(TensorView::Strides) + << " sizeof(ShapeView)=" << sizeof(TensorView::ShapeView) + << " sizeof(StridesView)=" << sizeof(TensorView::StridesView) + << '\n'; + +#if INFINI_RT_HAS_SMALL_VECTOR + std::cerr + << "sizeof(SmallVector)=" + << sizeof(infini::rt::detail::SmallVector) + << " sizeof(SmallVector)=" + << sizeof(infini::rt::detail::SmallVector) + << " sizeof(ShapeStridesStorage<4>)=" + << sizeof(infini::rt::detail::ShapeStridesStorage) + << " sizeof(ShapeStridesStorage<8>)=" + << sizeof(infini::rt::detail::ShapeStridesStorage) + << '\n'; +#endif + + std::array data{}; + const Device cpu_device{Device::Type::kCpu}; + + RunRankBenchmarks<1>(data.data(), cpu_device); + RunRankBenchmarks<2>(data.data(), cpu_device); + RunRankBenchmarks<4>(data.data(), cpu_device); + RunRankBenchmarks<5>(data.data(), cpu_device); + RunRankBenchmarks<8>(data.data(), cpu_device); + RunRankBenchmarks<9>(data.data(), cpu_device); + RunRank2Controls(data.data(), cpu_device); + return 0; } diff --git a/tests/performance/perf_tensor_view_footprint.cc b/tests/performance/perf_tensor_view_footprint.cc new file mode 100644 index 0000000..174c9f8 --- /dev/null +++ b/tests/performance/perf_tensor_view_footprint.cc @@ -0,0 +1,143 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "perf_common.h" + +namespace { + +namespace perf = infini::rt::perf; + +using infini::rt::DataType; +using infini::rt::Device; +using infini::rt::TensorView; + +constexpr std::size_t kTensorVisitsPerSample = 262144; +constexpr std::array kTensorCounts = {8, 256}; + +volatile std::uintptr_t g_benchmark_sink = 0; + +struct CacheKeyLike { + std::size_t hash{0}; + + std::vector tensors; + + std::size_t scalar_hash{0}; +}; + +void HashCombine(std::size_t& seed, std::size_t value) { + seed ^= std::hash{}(value) + + static_cast(0x9e3779b9) + (seed << 6) + (seed >> 2); +} + +void HashCombine(std::size_t& seed, const TensorView& value) { + seed ^= std::hash{}(value) + + static_cast(0x9e3779b9) + (seed << 6) + (seed >> 2); +} + +INFINI_RT_NOINLINE CacheKeyLike +BuildCacheKeyLike(const std::vector& inputs) { + CacheKeyLike key; + HashCombine(key.hash, inputs.size()); + for (const auto& input : inputs) { + HashCombine(key.hash, input); + key.tensors.push_back(input); + } + return key; +} + +INFINI_RT_NOINLINE bool EqualCacheKeys(const CacheKeyLike& lhs, + const CacheKeyLike& rhs) { + if (lhs.scalar_hash != rhs.scalar_hash || + lhs.tensors.size() != rhs.tensors.size()) { + return false; + } + + const std::equal_to equal; + for (std::size_t i = 0; i < lhs.tensors.size(); ++i) { + if (!equal(lhs.tensors[i], rhs.tensors[i])) { + return false; + } + } + return true; +} + +template +TensorView::Shape MakeShape() { + TensorView::Shape shape(rank); + for (auto& size : shape) { + size = 2; + } + return shape; +} + +TensorView::Strides MakeStrides(const TensorView::Shape& shape) { + TensorView::Strides strides(shape.size()); + TensorView::Stride stride = 1; + + for (std::size_t i = shape.size(); i > 0; --i) { + strides[i - 1] = stride; + stride *= static_cast(shape[i - 1]); + } + + return strides; +} + +template +std::vector MakeInputs(float* data, const Device& device, + std::size_t tensor_count) { + std::vector inputs; + inputs.reserve(tensor_count); + + for (std::size_t i = 0; i < tensor_count; ++i) { + auto shape = MakeShape(); + shape[0] += (i & 3); + const auto strides = MakeStrides(shape); + inputs.emplace_back(data, shape, DataType::kFloat32, device, strides); + } + return inputs; +} + +template +void RunFootprintBenchmarks(float* data, const Device& device) { + for (const auto tensor_count : kTensorCounts) { + const auto inputs = MakeInputs(data, device, tensor_count); + const auto reference = BuildCacheKeyLike(inputs); + const auto iterations = kTensorVisitsPerSample / tensor_count; + const auto params = std::vector{ + perf::NumberParam("ndim", rank), + perf::NumberParam("tensor_count", tensor_count)}; + + perf::RunBenchmark( + "perf_tensor_view_footprint.cache_key_build_hit", params, iterations, + "ns", [&] { + const auto candidate = BuildCacheKeyLike(inputs); + const bool equal = EqualCacheKeys(candidate, reference); + g_benchmark_sink = + static_cast(candidate.hash) ^ + reinterpret_cast(candidate.tensors.data()) ^ + static_cast(equal); + }); + } +} + +} // namespace + +int main() { + std::cerr << "sizeof(TensorView)=" << sizeof(TensorView) + << " sizeof(Shape)=" << sizeof(TensorView::Shape) + << " sizeof(Strides)=" << sizeof(TensorView::Strides) << '\n'; + + std::array data{}; + const Device cpu_device{Device::Type::kCpu}; + + RunFootprintBenchmarks<4>(data.data(), cpu_device); + RunFootprintBenchmarks<8>(data.data(), cpu_device); + + return 0; +} diff --git a/tests/test_core.cc b/tests/test_core.cc index 273ecef..4582ffd 100644 --- a/tests/test_core.cc +++ b/tests/test_core.cc @@ -1,9 +1,12 @@ #include +#include #include #include +#include #include #include +#include #include #include "test_helper.h" @@ -14,8 +17,120 @@ using infini::rt::DataType; using infini::rt::Device; using infini::rt::TensorView; -static_assert(!std::is_constructible_v>, - "TensorView should not treat tensor containers as tensor-like."); +static_assert(std::is_copy_constructible_v, + "`TensorView` should remain copy constructible."); +static_assert(std::is_move_constructible_v, + "`TensorView` should remain move constructible."); +static_assert(!std::is_copy_assignable_v, + "`TensorView` should not become copy assignable."); +static_assert(!std::is_move_assignable_v, + "`TensorView` should not become move assignable."); +static_assert( + !std::is_constructible_v>, + "`TensorView` should not treat tensor containers as tensor-like."); +static_assert( + std::is_same_v().shape()), + TensorView::ShapeView>, + "`TensorView` lvalues should expose a borrowed shape view."); +static_assert( + std::is_same_v().strides()), + TensorView::StridesView>, + "`TensorView` lvalues should expose a borrowed strides view."); +static_assert(std::is_same_v().shape()), + TensorView::Shape>, + "`TensorView` rvalues should return an owning shape."); +static_assert(std::is_same_v().strides()), + TensorView::Strides>, + "`TensorView` rvalues should return owning strides."); +static_assert( + std::is_same_v().shape()), + TensorView::Shape>, + "Const `TensorView` rvalues should return an owning shape."); +static_assert( + std::is_same_v().strides()), + TensorView::Strides>, + "Const `TensorView` rvalues should return owning strides."); +static_assert(!std::is_convertible_v, + "A borrowed shape should not hide an owning allocation."); +static_assert( + !std::is_convertible_v, + "Borrowed strides should not hide an owning allocation."); +static_assert(noexcept(std::declval().shape()), + "Borrowed shape access should be `noexcept`."); +static_assert(noexcept(std::declval().strides()), + "Borrowed strides access should be `noexcept`."); +static_assert(noexcept(std::declval().size(0)), + "Scalar size access should be `noexcept`."); +static_assert(noexcept(std::declval().stride(0)), + "Scalar stride access should be `noexcept`."); +static_assert(noexcept(std::declval().ndim()), + "Rank access should be `noexcept`."); +static_assert(noexcept(std::declval().numel()), + "Element-count access should be `noexcept`."); + +struct VectorTensorLike { + void* data_value; + + std::vector shape_value; + + DataType dtype_value; + + Device device_value; + + std::vector strides_value; + + mutable std::size_t data_call_count{0}; + + mutable std::size_t shape_call_count{0}; + + mutable std::size_t dtype_call_count{0}; + + mutable std::size_t device_call_count{0}; + + mutable std::size_t strides_call_count{0}; + + void* data() const { + ++data_call_count; + return data_value; + } + + std::vector shape() const { + ++shape_call_count; + return shape_value; + } + + DataType dtype() const { + ++dtype_call_count; + return dtype_value; + } + + Device device() const { + ++device_call_count; + return device_value; + } + + std::vector strides() const { + ++strides_call_count; + return strides_value; + } +}; + +std::vector MakeShape(std::size_t rank) { + return std::vector(rank, 2); +} + +std::vector MakeContiguousStrides( + const std::vector& shape) { + std::vector strides(shape.size()); + std::ptrdiff_t stride = 1; + + for (std::size_t index = shape.size(); index > 0; --index) { + strides[index - 1] = stride; + stride *= static_cast(shape[index - 1]); + } + + return strides; +} void TestDevice(infini::rt::test::TestContext* context) { const Device cpu{Device::Type::kCpu}; @@ -46,41 +161,244 @@ void TestDataType(infini::rt::test::TestContext* context) { DataType::kUInt16, "uint16 should parse by name."); } -void TestTensorView(infini::rt::test::TestContext* context) { - std::vector data{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; - TensorView tensor{data.data(), std::vector{2, 3}, - DataType::kFloat32, Device{Device::Type::kCpu}}; +void TestTensorViewRanks(infini::rt::test::TestContext* context) { + std::array data{}; + const Device cpu{Device::Type::kCpu}; + const std::array ranks{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + + for (const std::size_t rank : ranks) { + const std::vector shape = MakeShape(rank); + const std::vector strides = MakeContiguousStrides(shape); + const TensorView tensor{data.data(), shape, DataType::kFloat32, cpu}; + const std::string rank_prefix = "Rank " + std::to_string(rank) + ": "; + std::size_t expected_numel = 1; + + for (const std::size_t size : shape) { + expected_numel *= size; + } + + context->ExpectEqual( + tensor.ndim(), rank, + rank_prefix + "`TensorView` should preserve the tested rank."); + context->ExpectEqual( + tensor.shape(), shape, + rank_prefix + "`TensorView` should preserve the complete shape."); + context->ExpectEqual( + tensor.strides(), strides, + rank_prefix + + "`TensorView` should generate complete contiguous strides."); + context->ExpectEqual( + tensor.numel(), expected_numel, + rank_prefix + "`TensorView` should compute the element count."); - context->ExpectEqual(tensor.ndim(), std::size_t{2}, - "TensorView should keep its rank."); + if (rank > 0) { + context->ExpectEqual( + tensor.size(-1), shape.back(), + rank_prefix + "Negative size access should preserve the last axis."); + context->ExpectEqual( + tensor.stride(-1), strides.back(), + rank_prefix + + "Negative stride access should preserve the last axis."); + } + + context->Expect( + tensor.IsContiguous(), + rank_prefix + "`TensorView` should report contiguous metadata."); + } +} + +void TestTensorLikeValueAccessors(infini::rt::test::TestContext* context) { + std::array data{}; + const VectorTensorLike tensor_like{data.data(), + {2, 3}, + DataType::kFloat64, + Device{Device::Type::kCpu, 1}, + {3, 1}}; + const TensorView tensor{tensor_like}; + + context->ExpectEqual( + tensor.data(), static_cast(tensor_like.data_value), + "`TensorView` should preserve `TensorLike` data returned by value."); + context->ExpectEqual( + tensor.shape(), tensor_like.shape_value, + "`TensorView` should own shape metadata returned by value."); + context->ExpectEqual( + tensor.dtype(), tensor_like.dtype_value, + "`TensorView` should preserve `TensorLike` dtype returned by value."); + context->ExpectEqual( + tensor.device(), tensor_like.device_value, + "`TensorView` should preserve `TensorLike` device returned by value."); + context->ExpectEqual( + tensor.strides(), tensor_like.strides_value, + "`TensorView` should own stride metadata returned by value."); context->ExpectEqual(tensor.numel(), std::size_t{6}, - "TensorView should compute element count."); + "`TensorLike` construction should preserve the shape."); + context->Expect(tensor.IsContiguous(), + "`TensorLike` construction should preserve contiguity."); + context->ExpectEqual(tensor_like.data_call_count, std::size_t{1}, + "`TensorView` should evaluate data exactly once."); + context->ExpectEqual(tensor_like.shape_call_count, std::size_t{1}, + "`TensorView` should evaluate shape exactly once."); + context->ExpectEqual(tensor_like.dtype_call_count, std::size_t{1}, + "`TensorView` should evaluate dtype exactly once."); + context->ExpectEqual(tensor_like.device_call_count, std::size_t{1}, + "`TensorView` should evaluate device exactly once."); + context->ExpectEqual(tensor_like.strides_call_count, std::size_t{1}, + "`TensorView` should evaluate strides exactly once."); +} + +void TestTensorViewOperations(infini::rt::test::TestContext* context) { + std::array data{1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f}; + std::array equal_data{}; + const Device cpu{Device::Type::kCpu}; + const std::vector shape{2, 3}; + const std::vector strides{3, 1}; + const TensorView tensor{data.data(), shape, DataType::kFloat32, cpu}; + context->ExpectEqual(tensor.element_size(), std::size_t{4}, - "TensorView should compute element size."); + "`TensorView` should compute element size."); context->ExpectEqual(tensor.size(0), std::size_t{2}, - "TensorView should expose dimension sizes."); + "`TensorView` should expose dimension sizes."); context->ExpectEqual(tensor.size(-1), std::size_t{3}, - "TensorView should support negative dimension sizes."); + "`TensorView` should support negative dimension sizes."); context->ExpectEqual(tensor.stride(0), std::ptrdiff_t{3}, - "TensorView should compute default row-major strides."); - context->ExpectEqual(tensor.stride(1), std::ptrdiff_t{1}, - "TensorView should compute default innermost stride."); - context->Expect(tensor.IsContiguous(), - "Default TensorView strides should be contiguous."); + "`TensorView` should expose dimension strides."); + context->ExpectEqual( + tensor.stride(-1), std::ptrdiff_t{1}, + "`TensorView` should support negative dimension strides."); + + const TensorView indexed = tensor[1]; + const TensorView negative_indexed = tensor[-1]; + const std::vector indexed_shape{3}; + const std::vector indexed_strides{1}; + context->ExpectEqual(indexed.shape(), indexed_shape, + "Indexing should remove the leading dimension."); + context->ExpectEqual(indexed.strides(), indexed_strides, + "Indexing should remove the leading stride."); + context->ExpectEqual( + indexed.data(), static_cast(data.data() + 3), + "Indexing should offset the data pointer by the leading stride."); + context->ExpectEqual( + negative_indexed.data(), indexed.data(), + "Negative indexing should select the matching leading element."); + + const TensorView reverse_strided{data.data() + 3, shape, DataType::kFloat32, + cpu, std::vector{-3, 1}}; + const TensorView reverse_indexed = reverse_strided[1]; + context->ExpectEqual( + reverse_indexed.data(), static_cast(data.data()), + "Indexing should preserve a negative leading-stride offset."); - TensorView transposed = tensor.T(); - context->ExpectEqual(transposed.shape(), TensorView::Shape({3, 2}), - "Transposed TensorView should swap shape."); - context->ExpectEqual(transposed.strides(), TensorView::Strides({1, 3}), - "Transposed TensorView should swap strides."); + const TensorView transposed = tensor.T(); + const std::vector transposed_shape{3, 2}; + const std::vector transposed_strides{1, 3}; + context->ExpectEqual(transposed.shape(), transposed_shape, + "Transposing should swap the complete shape."); + context->ExpectEqual(transposed.strides(), transposed_strides, + "Transposing should swap the complete strides."); context->Expect(!transposed.IsContiguous(), - "Transposed TensorView should not be contiguous."); + "A transposed matrix should not be contiguous."); - TensorView strided{data.data(), std::vector{2, 3}, - DataType::kFloat32, Device{Device::Type::kCpu}, - std::vector{4, 1}}; + const TensorView equal_tensor{equal_data.data(), shape, DataType::kFloat32, + cpu, strides}; + const TensorView strided{data.data(), shape, DataType::kFloat32, cpu, + std::vector{4, 1}}; + context->Expect(std::equal_to{}(tensor, equal_tensor), + "Equivalent `TensorView` objects should compare equal."); + context->Expect( + !std::equal_to{}(tensor, strided), + "Different strides should make `TensorView` objects unequal."); + context->ExpectEqual( + std::hash{}(tensor), std::hash{}(equal_tensor), + "Equivalent `TensorView` objects should have equal hashes."); context->Expect(!strided.IsContiguous(), - "TensorView with row padding should not be contiguous."); + "`TensorView` with row padding should not be contiguous."); + + const auto first_shape_view = tensor.shape(); + const auto second_shape_view = tensor.shape(); + const auto first_strides_view = tensor.strides(); + const auto second_strides_view = tensor.strides(); + context->Expect( + first_shape_view.data() == second_shape_view.data(), + "Repeated shape access should reference the same owned metadata."); + context->Expect( + first_strides_view.data() == second_strides_view.data(), + "Repeated stride access should reference the same owned metadata."); + + const TensorView copied{tensor}; + context->Expect(copied.shape().data() != tensor.shape().data(), + "A `TensorView` copy should own independent shape metadata."); + context->Expect( + copied.strides().data() != tensor.strides().data(), + "A `TensorView` copy should own independent stride metadata."); + + TensorView::Shape owned_temporary_shape = + TensorView{data.data(), shape}.shape(); + context->ExpectEqual( + owned_temporary_shape, shape, + "Shape access on a temporary `TensorView` should return owned metadata."); +} + +void TestTensorViewHeapRepresentations(infini::rt::test::TestContext* context) { + std::array data{}; + const Device cpu{Device::Type::kCpu}; + const TensorView::Shape shape{2, 2, 2, 2, 2, 2, 2, 2, 2}; + const TensorView::Strides strides{256, 128, 64, 32, 16, 8, 4, 2, 1}; + const TensorView combined{data.data(), shape, DataType::kFloat32, cpu, + strides}; + const TensorView split{data.data(), + TensorView::Shape{shape.begin(), shape.end()}, + DataType::kFloat32, cpu, + TensorView::Strides{strides.begin(), strides.end()}}; + + context->ExpectEqual(split.ndim(), std::size_t{9}, + "Split metadata should preserve its rank."); + context->ExpectEqual(split.size(8), std::size_t{2}, + "Split metadata should support scalar shape access."); + context->ExpectEqual(split.stride(8), std::ptrdiff_t{1}, + "Split metadata should support scalar stride access."); + context->ExpectEqual(split.numel(), std::size_t{512}, + "Split metadata should preserve its element count."); + + context->Expect(std::equal_to{}(combined, split), + "Combined and split metadata should compare equal."); + context->ExpectEqual( + std::hash{}(combined), std::hash{}(split), + "Combined and split metadata should have the same hash."); + + const TensorView indexed = split[1]; + const std::vector indexed_shape(8, 2); + const std::vector indexed_strides{128, 64, 32, 16, + 8, 4, 2, 1}; + context->ExpectEqual(indexed.shape(), indexed_shape, + "High-rank indexing should preserve the shape suffix."); + context->ExpectEqual(indexed.strides(), indexed_strides, + "High-rank indexing should preserve the stride suffix."); + context->ExpectEqual(indexed.data(), + static_cast(data.data() + 256), + "High-rank indexing should preserve the data offset."); + + const TensorView copied{split}; + context->ExpectEqual(copied.shape(), shape, + "Copying split metadata should preserve its shape."); + context->ExpectEqual(copied.strides(), strides, + "Copying split metadata should preserve its strides."); + + TensorView split_move_source{ + data.data(), TensorView::Shape{shape.begin(), shape.end()}, + DataType::kFloat32, cpu, + TensorView::Strides{strides.begin(), strides.end()}}; + TensorView moved{std::move(split_move_source)}; + context->ExpectEqual(moved.shape(), shape, + "Moving split metadata should preserve its shape."); + context->ExpectEqual(moved.strides(), strides, + "Moving split metadata should preserve its strides."); + + TensorView::Strides owned_temporary_strides = + TensorView{data.data(), shape}.strides(); + context->ExpectEqual(owned_temporary_strides, strides, + "Stride access on a temporary `TensorView` should " + "return owned metadata."); } } // namespace @@ -90,7 +408,10 @@ int main() { TestDevice(&context); TestDataType(&context); - TestTensorView(&context); + TestTensorViewRanks(&context); + TestTensorLikeValueAccessors(&context); + TestTensorViewOperations(&context); + TestTensorViewHeapRepresentations(&context); return context.ExitCode(); } diff --git a/tests/test_metadata_view.cc b/tests/test_metadata_view.cc new file mode 100644 index 0000000..d3e4bda --- /dev/null +++ b/tests/test_metadata_view.cc @@ -0,0 +1,141 @@ +#include +#include +#include +#include +#include + +#include "common/metadata_view.h" +#include "common/small_vector.h" +#include "test_helper.h" + +namespace { + +using MetadataView = infini::rt::detail::MetadataView; +using SmallVector = infini::rt::detail::SmallVector; +using infini::rt::test::TestContext; + +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_nothrow_copy_constructible_v); +static_assert(std::is_nothrow_copy_assignable_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v); +static_assert(std::is_same_v().data()), + const std::size_t*>); +static_assert(std::is_same_v().front()), + const std::size_t&>); +static_assert(std::is_same_v().back()), + const std::size_t&>); +static_assert(std::is_same_v()[0]), + const std::size_t&>); +static_assert(std::is_same_v().begin()), + const std::size_t*>); + +void TestEmptyView(TestContext* context) { + const MetadataView empty; + context->Expect(empty.empty(), "A default `MetadataView` should be empty."); + context->ExpectEqual(empty.size(), std::size_t{0}, + "A default `MetadataView` should have size zero."); + context->Expect(empty.data() == nullptr, + "A default `MetadataView` should have null data."); + context->Expect(empty.begin() == nullptr, + "A default `MetadataView` should have a null begin."); + context->Expect(empty.end() == nullptr, + "A default `MetadataView` should have a null end."); + context->Expect(empty.cbegin() == empty.begin(), + "Empty `begin()` and `cbegin()` should agree."); + context->Expect(empty.cend() == empty.end(), + "Empty `end()` and `cend()` should agree."); + + const std::array storage{7}; + const MetadataView empty_at_data{storage.data(), 0}; + context->Expect(empty_at_data.begin() == storage.data(), + "An empty `MetadataView` should preserve non-null data."); + context->Expect(empty_at_data.end() == storage.data(), + "An empty `MetadataView` should end at its data pointer."); +} + +void TestAccessors(TestContext* context) { + std::array storage{2, 4, 6}; + const MetadataView view{storage.data(), storage.size()}; + + context->Expect(!view.empty(), + "A `MetadataView` with values should not be empty."); + context->ExpectEqual(view.size(), storage.size(), + "`MetadataView` should report its size."); + context->Expect(view.data() == storage.data(), + "`MetadataView` should preserve its data pointer."); + context->ExpectEqual(view.front(), std::size_t{2}, + "`front()` should expose the first value."); + context->ExpectEqual(view[1], std::size_t{4}, + "`operator[]` should expose the selected value."); + context->ExpectEqual(view.back(), std::size_t{6}, + "`back()` should expose the final value."); + context->Expect(view.begin() == view.cbegin(), + "`begin()` and `cbegin()` should agree."); + context->Expect(view.end() == view.cend(), + "`end()` and `cend()` should agree."); + context->Expect(view.end() == storage.data() + storage.size(), + "`end()` should follow the final value."); + + storage[1] = 8; + context->ExpectEqual(view[1], std::size_t{8}, + "`MetadataView` should observe its referenced storage."); +} + +void TestViewEquality(TestContext* context) { + const std::array values{1, 2, 3}; + const std::array equal_values{1, 2, 3}; + const std::array different_values{1, 2, 4}; + const MetadataView view{values.data(), values.size()}; + const infini::rt::detail::MetadataView equal_view{ + equal_values.data(), equal_values.size()}; + const infini::rt::detail::MetadataView different_view{ + different_values.data(), different_values.size()}; + + context->Expect(view == equal_view && equal_view == view, + "Compatible `MetadataView` types should compare by value."); + context->Expect(view != different_view && different_view != view, + "`MetadataView` should detect unequal values."); +} + +void TestRangeEquality(TestContext* context) { + const std::array values{1, 2, 3}; + const MetadataView view{values.data(), values.size()}; + + const std::array equal_array{1, 2, 3}; + const std::array different_array{1, 2, 4}; + context->Expect(view == equal_array && equal_array == view, + "`MetadataView` and `std::array` should compare by value."); + context->Expect( + view != different_array && different_array != view, + "`MetadataView` and `std::array` should detect unequal values."); + + const std::vector equal_vector{1, 2, 3}; + const std::vector shorter_vector{1, 2}; + context->Expect(view == equal_vector && equal_vector == view, + "`MetadataView` and `std::vector` should compare by value."); + context->Expect(view != shorter_vector && shorter_vector != view, + "`MetadataView` should detect a different range size."); + + const SmallVector equal_small_vector{1, 2, 3}; + const SmallVector different_small_vector{1, 2, 4}; + context->Expect(view == equal_small_vector && equal_small_vector == view, + "`MetadataView` and `SmallVector` should compare by value."); + context->Expect( + view != different_small_vector && different_small_vector != view, + "`MetadataView` and `SmallVector` should detect unequal values."); +} + +} // namespace + +int main() { + TestContext context; + + TestEmptyView(&context); + TestAccessors(&context); + TestViewEquality(&context); + TestRangeEquality(&context); + + return context.ExitCode(); +} diff --git a/tests/test_shape_strides_storage.cc b/tests/test_shape_strides_storage.cc new file mode 100644 index 0000000..6d54966 --- /dev/null +++ b/tests/test_shape_strides_storage.cc @@ -0,0 +1,293 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/shape_strides_storage.h" +#include "test_helper.h" + +namespace { + +using ShapeStridesStorage = + infini::rt::detail::ShapeStridesStorage; +using DefaultStridesTag = infini::rt::detail::DefaultStridesTag; +using Shape = ShapeStridesStorage::Shape; +using Strides = ShapeStridesStorage::Strides; +using infini::rt::test::TestContext; + +static_assert(std::is_copy_constructible_v); +static_assert(std::is_nothrow_move_constructible_v); +static_assert(!std::is_copy_assignable_v); +static_assert(!std::is_move_assignable_v); + +template +void ExpectView(TestContext* context, + infini::rt::detail::MetadataView actual, + std::initializer_list expected, + std::string_view message) { + context->ExpectEqual(actual, std::vector(expected.begin(), expected.end()), + message); +} + +template +void ExpectContiguous(TestContext* context, + infini::rt::detail::MetadataView view, + std::string_view message) { + context->Expect(view.data() == view.begin(), message); + context->Expect(view.end() == view.data() + view.size(), message); + + for (std::size_t index = 0; index < view.size(); ++index) { + context->Expect(&view[index] == view.data() + index, message); + } +} + +template +class InputRange { + public: + explicit InputRange(std::istream* stream) : stream_(stream) {} + + std::istream_iterator begin() const { + return std::istream_iterator{*stream_}; + } + + std::istream_iterator end() const { return std::istream_iterator{}; } + + private: + std::istream* stream_; +}; + +void TestEmptyMetadata(TestContext* context) { + const ShapeStridesStorage metadata; + + context->Expect(metadata.shape().empty(), + "Default tensor metadata should have an empty shape."); + context->Expect(metadata.strides().empty(), + "Default tensor metadata should have empty strides."); + context->ExpectEqual(metadata.shape().size(), std::size_t{0}, + "Default shape size should be zero."); + context->ExpectEqual(metadata.strides().size(), std::size_t{0}, + "Default strides size should be zero."); + + const Shape shape; + const Strides strides; + const ShapeStridesStorage explicit_empty{shape, strides}; + context->Expect(explicit_empty.shape().empty(), + "Explicit rank-zero metadata should have an empty shape."); + context->Expect(explicit_empty.strides().empty(), + "Explicit rank-zero metadata should have empty strides."); + + const std::array empty_shape{}; + const std::array empty_strides{}; + const ShapeStridesStorage empty_array_metadata{empty_shape, empty_strides}; + context->Expect(empty_array_metadata.shape().empty() && + empty_array_metadata.strides().empty(), + "Empty standard arrays should construct rank-zero metadata."); +} + +void TestInlineMetadata(TestContext* context) { + const Shape shape{2, 3, 4, 5}; + const Strides strides{60, 20, 5, 1}; + const ShapeStridesStorage metadata{shape, strides}; + + ExpectView(context, metadata.shape(), {2, 3, 4, 5}, + "Rank-four inline metadata should preserve shape values."); + ExpectView(context, metadata.strides(), {60, 20, 5, 1}, + "Rank-four inline metadata should preserve stride values."); + ExpectContiguous(context, metadata.shape(), + "Inline shape values should be contiguous."); + ExpectContiguous(context, metadata.strides(), + "Inline stride values should be contiguous."); + context->ExpectEqual(metadata.shape_size(), std::size_t{4}, + "Direct inline shape size should match the view."); + context->ExpectEqual(metadata.strides_size(), std::size_t{4}, + "Direct inline strides size should match the view."); + context->Expect(metadata.shape_data() == metadata.shape().data(), + "Direct inline shape data should match the view."); + context->Expect(metadata.strides_data() == metadata.strides().data(), + "Direct inline strides data should match the view."); +} + +void TestCombinedMetadata(TestContext* context) { + const Shape shape{2, 3, 4, 5, 6}; + const Strides strides{360, 120, 30, 6, 1}; + const ShapeStridesStorage exact_lvalue{shape, strides}; + + ExpectView(context, exact_lvalue.shape(), {2, 3, 4, 5, 6}, + "Rank-five lvalue metadata should preserve shape values."); + ExpectView(context, exact_lvalue.strides(), {360, 120, 30, 6, 1}, + "Rank-five lvalue metadata should preserve stride values."); + ExpectContiguous(context, exact_lvalue.shape(), + "Combined shape values should be contiguous."); + ExpectContiguous(context, exact_lvalue.strides(), + "Combined stride values should be contiguous."); + context->ExpectEqual(exact_lvalue.shape_size(), std::size_t{5}, + "Direct heap shape size should match the view."); + context->ExpectEqual(exact_lvalue.strides_size(), std::size_t{5}, + "Direct heap strides size should match the view."); + context->Expect(exact_lvalue.shape_data() == exact_lvalue.shape().data(), + "Direct heap shape data should match the view."); + context->Expect(exact_lvalue.strides_data() == exact_lvalue.strides().data(), + "Direct heap strides data should match the view."); + + const std::array generic_shape{7, 8, 9, 10, 11}; + const std::array generic_strides{7920, 990, 110, 11, 1}; + const ShapeStridesStorage generic{generic_shape, generic_strides}; + ExpectView(context, generic.shape(), {7, 8, 9, 10, 11}, + "Generic rank-five metadata should convert shape values."); + ExpectView(context, generic.strides(), {7920, 990, 110, 11, 1}, + "Generic rank-five metadata should convert stride values."); +} + +void TestSplitRvalueMetadata(TestContext* context) { + const ShapeStridesStorage temporary_values{Shape{2, 3, 4, 5, 6}, + Strides{360, 120, 30, 6, 1}}; + ExpectView(context, temporary_values.shape(), {2, 3, 4, 5, 6}, + "Exact rvalue metadata should preserve shape values."); + ExpectView(context, temporary_values.strides(), {360, 120, 30, 6, 1}, + "Exact rvalue metadata should preserve stride values."); + + Shape shape{3, 4, 5, 6, 7}; + Strides strides{840, 210, 42, 7, 1}; + const ShapeStridesStorage pre_moved{std::move(shape), std::move(strides)}; + ExpectView(context, pre_moved.shape(), {3, 4, 5, 6, 7}, + "Pre-moved metadata should preserve shape values."); + ExpectView(context, pre_moved.strides(), {840, 210, 42, 7, 1}, + "Pre-moved metadata should preserve stride values."); + ExpectContiguous(context, pre_moved.shape(), + "Split shape values should be contiguous."); + ExpectContiguous(context, pre_moved.strides(), + "Split stride values should be contiguous."); +} + +ShapeStridesStorage CopyPastSourceLifetime(TestContext* context) { + const ShapeStridesStorage source{Shape{2, 3, 4, 5, 6}, + Strides{360, 120, 30, 6, 1}}; + ShapeStridesStorage copy{source}; + + context->Expect(copy.shape().data() != source.shape().data(), + "A metadata copy should own separate shape storage."); + context->Expect(copy.strides().data() != source.strides().data(), + "A metadata copy should own separate stride storage."); + + return copy; +} + +ShapeStridesStorage MovePastSourceLifetime() { + ShapeStridesStorage source{Shape{3, 4, 5, 6, 7}, Strides{840, 210, 42, 7, 1}}; + ShapeStridesStorage moved{std::move(source)}; + + return moved; +} + +void TestCopyAndMoveOwnership(TestContext* context) { + const ShapeStridesStorage copy = CopyPastSourceLifetime(context); + ExpectView(context, copy.shape(), {2, 3, 4, 5, 6}, + "A copy should remain valid after its source is destroyed."); + ExpectView(context, copy.strides(), {360, 120, 30, 6, 1}, + "Copied strides should survive source destruction."); + + const ShapeStridesStorage moved = MovePastSourceLifetime(); + ExpectView(context, moved.shape(), {3, 4, 5, 6, 7}, + "Moved metadata should survive source destruction."); + ExpectView(context, moved.strides(), {840, 210, 42, 7, 1}, + "Moved strides should survive source destruction."); +} + +void TestMixedOwnership(TestContext* context) { + Shape moved_shape{2, 3, 4, 5, 6}; + const Strides borrowed_strides{360, 120, 30, 6, 1}; + const ShapeStridesStorage shape_rvalue{std::move(moved_shape), + borrowed_strides}; + ExpectView(context, shape_rvalue.shape(), {2, 3, 4, 5, 6}, + "A moved shape with lvalue strides should preserve shape."); + ExpectView(context, shape_rvalue.strides(), {360, 120, 30, 6, 1}, + "A moved shape with lvalue strides should preserve strides."); + + const Shape borrowed_shape{3, 4, 5, 6, 7}; + Strides moved_strides{840, 210, 42, 7, 1}; + const ShapeStridesStorage strides_rvalue{borrowed_shape, + std::move(moved_strides)}; + ExpectView(context, strides_rvalue.shape(), {3, 4, 5, 6, 7}, + "An lvalue shape with moved strides should preserve shape."); + ExpectView(context, strides_rvalue.strides(), {840, 210, 42, 7, 1}, + "An lvalue shape with moved strides should preserve strides."); +} + +void TestDefaultStrides(TestContext* context) { + const ShapeStridesStorage inline_metadata{Shape{2, 3, 4, 5}, + DefaultStridesTag{}}; + ExpectView(context, inline_metadata.strides(), {60, 20, 5, 1}, + "Default inline strides should be row-major."); + + Shape shape{2, 3, 4, 5, 6}; + const ShapeStridesStorage heap_metadata{std::move(shape), + DefaultStridesTag{}}; + ExpectView(context, heap_metadata.shape(), {2, 3, 4, 5, 6}, + "Default-stride construction should preserve shape."); + ExpectView(context, heap_metadata.strides(), {360, 120, 30, 6, 1}, + "Default rank-five strides should be row-major."); +} + +void TestIndependentViewLengths(TestContext* context) { + const ShapeStridesStorage longer_shape{Shape{2, 3, 4, 5, 6}, + Strides{20, 5, 1}}; + context->ExpectEqual(longer_shape.shape().size(), std::size_t{5}, + "Shape length should be preserved independently."); + context->ExpectEqual(longer_shape.strides().size(), std::size_t{3}, + "Stride length should be preserved independently."); + context->ExpectEqual(longer_shape.shape_size(), std::size_t{5}, + "Direct shape size should remain independent."); + context->ExpectEqual(longer_shape.strides_size(), std::size_t{3}, + "Direct strides size should remain independent."); + ExpectView(context, longer_shape.shape(), {2, 3, 4, 5, 6}, + "A longer shape should preserve all shape values."); + ExpectView(context, longer_shape.strides(), {20, 5, 1}, + "A shorter stride range should preserve all stride values."); + + const ShapeStridesStorage longer_strides{Shape{2, 3, 4}, + Strides{360, 120, 30, 6, 1}}; + context->ExpectEqual(longer_strides.shape().size(), std::size_t{3}, + "Shorter shape length should be preserved."); + context->ExpectEqual(longer_strides.strides().size(), std::size_t{5}, + "Longer stride length should be preserved."); + context->ExpectEqual(longer_strides.shape_size(), std::size_t{3}, + "Direct shorter shape size should be preserved."); + context->ExpectEqual(longer_strides.strides_size(), std::size_t{5}, + "Direct longer strides size should be preserved."); +} + +void TestInputRanges(TestContext* context) { + std::istringstream shape_stream{"2 3 4 5 6"}; + std::istringstream strides_stream{"360 120 30 6 1"}; + const InputRange shape{&shape_stream}; + const InputRange strides{&strides_stream}; + const ShapeStridesStorage metadata{shape, strides}; + + ExpectView(context, metadata.shape(), {2, 3, 4, 5, 6}, + "Input ranges should be consumed once for shape values."); + ExpectView(context, metadata.strides(), {360, 120, 30, 6, 1}, + "Input ranges should be consumed once for stride values."); +} + +} // namespace + +int main() { + TestContext context; + + TestEmptyMetadata(&context); + TestInlineMetadata(&context); + TestCombinedMetadata(&context); + TestSplitRvalueMetadata(&context); + TestCopyAndMoveOwnership(&context); + TestMixedOwnership(&context); + TestDefaultStrides(&context); + TestIndependentViewLengths(&context); + TestInputRanges(&context); + + return context.ExitCode(); +} diff --git a/tests/test_small_vector.cc b/tests/test_small_vector.cc new file mode 100644 index 0000000..0a2b7d3 --- /dev/null +++ b/tests/test_small_vector.cc @@ -0,0 +1,534 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/small_vector.h" +#include "test_helper.h" + +namespace { + +thread_local bool count_allocations = false; +thread_local std::size_t allocation_count = 0; +thread_local bool count_deallocations = false; +thread_local std::size_t deallocation_count = 0; + +class AllocationScope { + public: + AllocationScope() { + allocation_count = 0; + count_allocations = true; + } + + AllocationScope(const AllocationScope&) = delete; + + AllocationScope& operator=(const AllocationScope&) = delete; + + ~AllocationScope() { count_allocations = false; } + + std::size_t count() const { return allocation_count; } +}; + +class DeallocationScope { + public: + DeallocationScope() { + deallocation_count = 0; + count_deallocations = true; + } + + DeallocationScope(const DeallocationScope&) = delete; + + DeallocationScope& operator=(const DeallocationScope&) = delete; + + ~DeallocationScope() { count_deallocations = false; } + + std::size_t count() const { return deallocation_count; } +}; + +template +std::size_t CountAllocations(Function&& function) { + AllocationScope scope; + std::forward(function)(); + return scope.count(); +} + +template +std::size_t CountDeallocations(Function&& function) { + DeallocationScope scope; + std::forward(function)(); + return scope.count(); +} + +} // namespace + +void* operator new(std::size_t size) { + if (void* pointer = std::malloc(size == 0 ? 1 : size)) { + if (count_allocations) ++allocation_count; + return pointer; + } + throw std::bad_alloc{}; +} + +void* operator new[](std::size_t size) { return ::operator new(size); } + +void operator delete(void* pointer) noexcept { + if (pointer != nullptr && count_deallocations) ++deallocation_count; + std::free(pointer); +} + +void operator delete[](void* pointer) noexcept { ::operator delete(pointer); } + +void operator delete(void* pointer, std::size_t) noexcept { + ::operator delete(pointer); +} + +void operator delete[](void* pointer, std::size_t) noexcept { + ::operator delete[](pointer); +} + +namespace { + +using Inline4 = infini::rt::detail::SmallVector; +using Inline8 = infini::rt::detail::SmallVector; +using HeapAllocation = Inline4::HeapAllocation; +using infini::rt::test::TestContext; + +static_assert(std::is_copy_constructible_v); +static_assert(std::is_move_constructible_v); +static_assert(std::is_copy_assignable_v); +static_assert(std::is_move_assignable_v); +static_assert( + std::is_constructible_v); +static_assert(!std::is_copy_constructible_v); +static_assert(!std::is_copy_assignable_v); +static_assert(std::is_nothrow_move_constructible_v); +static_assert(std::is_nothrow_move_assignable_v); + +template +void ExpectValues( + TestContext* context, + const infini::rt::detail::SmallVector& actual, + std::initializer_list expected, std::string_view message) { + context->ExpectEqual(std::vector(actual.begin(), actual.end()), + std::vector(expected), message); +} + +void ExpectHeapValues(TestContext* context, const HeapAllocation& actual, + std::initializer_list expected, + std::string_view message) { + context->ExpectEqual( + std::vector(actual.data(), actual.data() + actual.size()), + std::vector(expected), message); +} + +void TestConstruction(TestContext* context) { + Inline4 empty; + context->Expect(empty.empty(), "A default `SmallVector` should be empty."); + context->ExpectEqual(empty.size(), std::size_t{0}, + "A default `SmallVector` should have size zero."); + context->ExpectEqual( + empty.capacity(), std::size_t{4}, + "A default `SmallVector` should expose inline capacity."); + + const std::size_t* null_range = nullptr; + Inline4 empty_pointer_range{null_range, null_range}; + context->Expect( + empty_pointer_range.empty(), + "An empty null pointer range should construct an empty `SmallVector`."); + + Inline4 counted(3); + ExpectValues(context, counted, {0, 0, 0}, + "The count constructor should value-initialize elements."); + + Inline4 empty_filled(0, 7); + context->Expect(empty_filled.empty(), + "A zero-count fill constructor should be empty."); + + Inline4 inline_filled; + const std::size_t inline_fill_allocations = + CountAllocations([&] { inline_filled = Inline4(4, 7); }); + ExpectValues(context, inline_filled, {7, 7, 7, 7}, + "The fill constructor should initialize inline elements."); + context->ExpectEqual(inline_fill_allocations, std::size_t{0}, + "The inline fill constructor should not allocate."); + context->ExpectEqual( + inline_filled.capacity(), std::size_t{4}, + "The inline fill constructor should preserve inline storage."); + + Inline4 overflow_filled; + const std::size_t overflow_fill_allocations = + CountAllocations([&] { overflow_filled = Inline4(5, 9); }); + ExpectValues(context, overflow_filled, {9, 9, 9, 9, 9}, + "The fill constructor should initialize overflow elements."); + context->ExpectEqual(overflow_fill_allocations, std::size_t{1}, + "The overflow fill constructor should allocate once."); + context->ExpectEqual( + overflow_filled.capacity(), std::size_t{5}, + "The overflow fill constructor should allocate exact storage."); + + Inline4 inline_values{1, 2, 3, 4}; + context->ExpectEqual(inline_values.capacity(), std::size_t{4}, + "Inline values should keep inline storage."); + + Inline4 overflow_values{1, 2, 3, 4, 5}; + context->Expect(overflow_values.capacity() >= 5, + "Overflow values should use sufficient heap storage."); + + const std::array iterator_source{2, 4, 6}; + Inline4 iterator_range(iterator_source.begin(), iterator_source.end()); + ExpectValues(context, iterator_range, {2, 4, 6}, + "The iterator-range constructor should preserve values."); + + std::istringstream input_stream{"5 7 9"}; + Inline4 input_range(std::istream_iterator{input_stream}, + std::istream_iterator{}); + ExpectValues(context, input_range, {5, 7, 9}, + "The input-iterator constructor should consume the range " + "once."); + + const std::vector container_source{3, 6, 9}; + Inline4 container_range(container_source); + ExpectValues(context, container_range, {3, 6, 9}, + "The container constructor should preserve values."); + + Inline8 wider_inline{1, 2, 3, 4, 5, 6, 7, 8}; + context->ExpectEqual(wider_inline.capacity(), std::size_t{8}, + "`Inline8` should expose and use its inline capacity."); + ExpectValues(context, wider_inline, {1, 2, 3, 4, 5, 6, 7, 8}, + "`Inline8` should preserve inline values."); +} + +void TestAccessorsAndIterators(TestContext* context) { + Inline4 values{1, 2, 3}; + context->ExpectEqual(values.size(), std::size_t{3}, + "`SmallVector` should report its size."); + context->Expect(!values.empty(), + "`SmallVector` with values should not be empty."); + context->Expect( + values.data() == values.begin(), + "Mutable `data()` and `begin()` should identify the first value."); + context->Expect(values.end() == values.data() + values.size(), + "Mutable `end()` should follow the final value."); + + values.front() = 4; + values[1] = 5; + values.back() = 6; + context->ExpectEqual(*values.begin(), std::size_t{4}, + "Mutable `begin()` should expose the first value."); + context->ExpectEqual(values.data()[1], std::size_t{5}, + "Mutable `data()` should expose indexed values."); + context->ExpectEqual(*(values.end() - 1), std::size_t{6}, + "Mutable `end()` should delimit the final value."); + + const Inline4& const_values = values; + context->Expect( + const_values.data() == const_values.begin(), + "Const `data()` and `begin()` should identify the first value."); + context->Expect(const_values.begin() == const_values.cbegin(), + "Const `begin()` and `cbegin()` should agree."); + context->Expect(const_values.end() == const_values.cend(), + "Const `end()` and `cend()` should agree."); + context->ExpectEqual(const_values.front(), std::size_t{4}, + "Const `front()` should expose the first value."); + context->ExpectEqual(const_values[1], std::size_t{5}, + "Const indexing should expose indexed values."); + context->ExpectEqual(const_values.back(), std::size_t{6}, + "Const `back()` should expose the final value."); +} + +void TestEquality(TestContext* context) { + const Inline4 values{1, 2, 3}; + const std::vector equal_values{1, 2, 3}; + const std::vector different_values{1, 2, 4}; + + context->Expect(values == equal_values, + "`SmallVector` should compare equal to `std::vector`."); + context->Expect(equal_values == values, + "`std::vector` should compare equal to `SmallVector`."); + context->Expect(values != different_values, + "`SmallVector` should compare unequal to `std::vector`."); + context->Expect(different_values != values, + "`std::vector` should compare unequal to `SmallVector`."); + + const Inline8 wider_equal{1, 2, 3}; + const Inline8 wider_different{1, 2, 4}; + context->Expect(values == wider_equal && wider_equal == values, + "Different inline capacities should compare by value."); + context->Expect(values != wider_different && wider_different != values, + "Different inline capacities should detect unequal values."); +} + +void TestMutation(TestContext* context) { + Inline4 cleared{1, 2, 3, 4, 5}; + cleared.clear(); + context->Expect(cleared.empty(), "`clear()` should remove every value."); + context->ExpectEqual(cleared.size(), std::size_t{0}, + "`clear()` should reset the size to zero."); + + Inline4 reserved{1, 2, 3}; + reserved.reserve(12); + context->Expect(reserved.capacity() >= 12, + "`reserve()` should provide the requested capacity."); + ExpectValues(context, reserved, {1, 2, 3}, + "`reserve()` should preserve existing values."); + const std::size_t reserved_capacity = reserved.capacity(); + reserved.reserve(6); + context->ExpectEqual(reserved.capacity(), reserved_capacity, + "`reserve()` should not shrink existing capacity."); + + Inline4 resized{1, 2}; + resized.resize(5); + ExpectValues(context, resized, {1, 2, 0, 0, 0}, + "Growing `resize()` should value-initialize new elements."); + context->Expect(resized.capacity() >= 5, + "Growing `resize()` should provide sufficient capacity."); + resized.resize(1); + ExpectValues(context, resized, {1}, + "Shrinking `resize()` should preserve the retained prefix."); + + Inline4 pushed; + std::vector pushed_expected; + std::size_t previous_capacity = pushed.capacity(); + std::size_t growth_count = 0; + for (std::size_t value = 0; value < 32; ++value) { + pushed.push_back(value); + pushed_expected.push_back(value); + if (pushed.capacity() != previous_capacity) { + const std::size_t new_capacity = pushed.capacity(); + context->Expect(new_capacity > previous_capacity, + "Repeated `push_back()` should increase capacity."); + if (new_capacity > previous_capacity) { + context->Expect( + new_capacity - previous_capacity >= previous_capacity / 2, + "Repeated `push_back()` should grow capacity multiplicatively."); + } + previous_capacity = new_capacity; + ++growth_count; + } + } + context->ExpectEqual(pushed.size(), pushed_expected.size(), + "Repeated `push_back()` should update the size."); + context->Expect(pushed.capacity() >= pushed.size(), + "Repeated `push_back()` should provide sufficient capacity."); + context->Expect(growth_count >= 2, + "Repeated `push_back()` should exercise multiple heap growth " + "steps."); + context->Expect(pushed == pushed_expected, + "Repeated `push_back()` should preserve every value."); + + Inline4 assigned; + assigned.assign({4, 5, 6}); + ExpectValues(context, assigned, {4, 5, 6}, + "Initializer-list `assign()` should replace values."); + const std::vector range_values{8, 6, 4, 2, 0}; + assigned.assign(range_values.begin(), range_values.end()); + context->Expect(assigned == range_values, + "Iterator-range `assign()` should replace values."); + + std::istringstream assign_input_stream{"9 7 5"}; + Inline4 input_assigned; + input_assigned.assign(std::istream_iterator{assign_input_stream}, + std::istream_iterator{}); + ExpectValues(context, input_assigned, {9, 7, 5}, + "Input-iterator `assign()` should consume the range once."); + + Inline4 heap_to_inline{1, 2, 3, 4, 5}; + heap_to_inline.assign({7, 8}); + ExpectValues(context, heap_to_inline, {7, 8}, + "A small assignment should replace overflow values."); + context->ExpectEqual(heap_to_inline.capacity(), std::size_t{4}, + "Assigning a small range should restore inline " + "storage."); + + Inline4 inline_to_heap{1, 2}; + inline_to_heap.assign({1, 2, 3, 4, 5}); + ExpectValues(context, inline_to_heap, {1, 2, 3, 4, 5}, + "An overflow assignment should replace inline values."); + context->Expect(inline_to_heap.capacity() >= 5, + "Assigning an overflow range should use heap storage."); +} + +void TestHeapRelease(TestContext* context) { + Inline4 inline_values{1, 2, 3}; + std::size_t* const inline_data = inline_values.data(); + HeapAllocation inline_allocation; + const std::size_t inline_release_allocations = CountAllocations( + [&] { inline_allocation = inline_values.ReleaseHeap(); }); + context->ExpectEqual(inline_release_allocations, std::size_t{0}, + "Releasing inline storage should not allocate."); + context->Expect(inline_allocation.empty(), + "Releasing inline storage should return an empty owner."); + context->Expect(inline_values.data() == inline_data, + "Releasing inline storage should preserve its address."); + ExpectValues(context, inline_values, {1, 2, 3}, + "Releasing inline storage should preserve its values."); + + Inline4 overflow_values{1, 2, 3, 4, 5}; + overflow_values.reserve(12); + std::size_t* const overflow_data = overflow_values.data(); + const std::size_t overflow_size = overflow_values.size(); + const std::size_t overflow_capacity = overflow_values.capacity(); + context->Expect(overflow_capacity > overflow_size, + "The release test should cover spare heap capacity."); + + HeapAllocation allocation; + const std::size_t overflow_release_allocations = + CountAllocations([&] { allocation = overflow_values.ReleaseHeap(); }); + context->ExpectEqual(overflow_release_allocations, std::size_t{0}, + "Releasing heap storage should not allocate."); + context->Expect(allocation.data() == overflow_data, + "Heap release should transfer the original allocation."); + context->ExpectEqual(allocation.size(), overflow_size, + "Heap release should preserve the logical size."); + context->ExpectEqual(allocation.capacity(), overflow_capacity, + "Heap release should preserve the allocation capacity."); + ExpectHeapValues(context, allocation, {1, 2, 3, 4, 5}, + "Heap release should preserve every value."); + context->Expect(overflow_values.empty(), + "A heap release source should become empty."); + context->ExpectEqual(overflow_values.capacity(), std::size_t{4}, + "A heap release source should restore inline capacity."); + + HeapAllocation second_allocation = overflow_values.ReleaseHeap(); + context->Expect(second_allocation.empty(), + "Releasing the same source twice should return no heap."); + context->Expect(overflow_values.empty(), + "A second heap release should leave the source empty."); + overflow_values.assign({9, 8}); + ExpectValues(context, overflow_values, {9, 8}, + "A heap release source should remain reusable."); + + HeapAllocation moved_allocation; + moved_allocation = std::move(allocation); + context->Expect(allocation.empty(), + "Moving a heap owner should empty the source owner."); + context->Expect(moved_allocation.data() == overflow_data, + "Moving a heap owner should preserve its allocation."); + const std::size_t owner_deallocations = CountDeallocations( + [&] { HeapAllocation final_allocation{std::move(moved_allocation)}; }); + context->ExpectEqual( + owner_deallocations, std::size_t{1}, + "A moved heap owner should deallocate its allocation exactly once."); + context->Expect(moved_allocation.empty(), + "Moving a heap owner should leave it non-owning."); + + Inline4 released_values{4, 3, 2, 1, 0}; + released_values.reserve(10); + HeapAllocation released_allocation = released_values.ReleaseHeap(); + const std::size_t released_capacity = released_allocation.capacity(); + std::size_t* const released_data = released_allocation.release(); + context->Expect(released_allocation.empty(), + "Explicit release should empty the heap owner."); + context->Expect(released_allocation.release() == nullptr, + "Explicit release should return the allocation only once."); + context->ExpectEqual(released_data[0], std::size_t{4}, + "Explicit release should return the owned values."); + std::allocator allocator; + allocator.deallocate(released_data, released_capacity); +} + +void TestCopySemantics(TestContext* context) { + Inline4 inline_source{1, 2, 3}; + Inline4 inline_copy{inline_source}; + inline_source[0] = 9; + ExpectValues(context, inline_copy, {1, 2, 3}, + "An inline copy should own independent values."); + + Inline4 overflow_source{1, 2, 3, 4, 5}; + Inline4 overflow_copy{overflow_source}; + overflow_source[0] = 9; + ExpectValues(context, overflow_copy, {1, 2, 3, 4, 5}, + "An overflow copy should own independent values."); + + Inline4 copy_assignment_source{4, 5, 6, 7, 8}; + Inline4 copy_assigned{0}; + copy_assigned = copy_assignment_source; + copy_assignment_source[1] = 0; + ExpectValues(context, copy_assigned, {4, 5, 6, 7, 8}, + "Copy assignment should own independent values."); + + Inline4 inline_assignment_source{4, 5, 6}; + Inline4 heap_copy_assigned{0, 1, 2, 3, 4}; + heap_copy_assigned = inline_assignment_source; + inline_assignment_source[0] = 0; + ExpectValues(context, heap_copy_assigned, {4, 5, 6}, + "Copy assignment should replace heap values with an " + "independent inline copy."); + context->ExpectEqual( + heap_copy_assigned.capacity(), std::size_t{4}, + "Copy assignment from inline values should restore inline storage."); + + Inline4 self_assigned{1, 2, 3}; + self_assigned = self_assigned; + context->Expect(self_assigned == std::vector({1, 2, 3}), + "Self-assignment should preserve values."); +} + +void TestMoveSemantics(TestContext* context) { + Inline4 inline_construct_source{1, 2, 3}; + Inline4 inline_constructed{std::move(inline_construct_source)}; + ExpectValues(context, inline_constructed, {1, 2, 3}, + "Moving inline values should preserve them in the destination."); + inline_construct_source = Inline4{9}; + ExpectValues(context, inline_construct_source, {9}, + "An inline move source should remain assignable."); + + Inline4 overflow_construct_source{1, 2, 3, 4, 5}; + Inline4 overflow_constructed{std::move(overflow_construct_source)}; + ExpectValues( + context, overflow_constructed, {1, 2, 3, 4, 5}, + "Moving overflow values should preserve them in the destination."); + overflow_construct_source = Inline4{9}; + ExpectValues(context, overflow_construct_source, {9}, + "An overflow move source should remain assignable."); + + Inline4 inline_assignment_source{4, 5, 6}; + Inline4 inline_assigned{0, 1, 2, 3, 4}; + inline_assigned = std::move(inline_assignment_source); + ExpectValues(context, inline_assigned, {4, 5, 6}, + "Move assignment should preserve inline values."); + context->ExpectEqual( + inline_assigned.capacity(), std::size_t{4}, + "Move assignment from inline values should restore inline storage."); + inline_assignment_source = Inline4{9}; + ExpectValues(context, inline_assignment_source, {9}, + "An inline move-assignment source should remain assignable."); + + Inline4 overflow_assignment_source{4, 5, 6, 7, 8}; + Inline4 overflow_assigned{0}; + overflow_assigned = std::move(overflow_assignment_source); + ExpectValues(context, overflow_assigned, {4, 5, 6, 7, 8}, + "Move assignment should preserve overflow values."); + overflow_assignment_source = Inline4{9}; + ExpectValues(context, overflow_assignment_source, {9}, + "An overflow move-assignment source should remain assignable."); + + Inline4 self_moved{1, 2, 3}; + self_moved = std::move(self_moved); + ExpectValues(context, self_moved, {1, 2, 3}, + "Self-move assignment should preserve values."); +} + +} // namespace + +int main() { + TestContext context; + + TestConstruction(&context); + TestAccessorsAndIterators(&context); + TestEquality(&context); + TestMutation(&context); + TestHeapRelease(&context); + TestCopySemantics(&context); + TestMoveSemantics(&context); + + return context.ExitCode(); +} diff --git a/tests/test_tensor_view_allocations.cc b/tests/test_tensor_view_allocations.cc index b76c682..ff73f24 100644 --- a/tests/test_tensor_view_allocations.cc +++ b/tests/test_tensor_view_allocations.cc @@ -1,9 +1,13 @@ #include +#include #include #include +#include #include +#include #include +#include #include "test_helper.h" @@ -72,112 +76,282 @@ using infini::rt::DataType; using infini::rt::Device; using infini::rt::TensorView; -void TestTensorViewAllocations(infini::rt::test::TestContext* context) { - alignas(float) std::byte data[6 * sizeof(float)]{}; - const Device cpu{Device::Type::kCpu}; - const Device indexed_cpu{Device::Type::kCpu, 1}; - const TensorView::Shape shape{2, 3}; - const TensorView::Strides strides{3, 1}; +struct VectorTensorLike { + void* data_value; - bool shape_only_metadata_is_default = false; - ExpectAllocationCount( + std::vector shape_value; + + DataType dtype_value; + + Device device_value; + + std::vector strides_value; + + void* data() const { return data_value; } + + const std::vector& shape() const { return shape_value; } + + DataType dtype() const { return dtype_value; } + + Device device() const { return device_value; } + + const std::vector& strides() const { return strides_value; } +}; + +void ExpectRankAllocationCount(infini::rt::test::TestContext* context, + std::size_t actual, std::size_t expected, + std::size_t rank, const char* message) { + std::string full_message = "Rank-" + std::to_string(rank) + " "; + full_message += message; + context->ExpectEqual(actual, expected, full_message); +} + +template +std::array MakeShapeValues() { + std::array shape{}; + shape.fill(2); + return shape; +} + +template +std::array MakeStrideValues() { + std::array strides{}; + TensorView::Stride stride = 1; + for (std::size_t index = rank; index > 0; --index) { + strides[index - 1] = stride; + stride *= 2; + } + return strides; +} + +template +std::size_t CountInitializerListConstructionAllocations( + void* data, const std::array& shape, + const std::array& strides, const Device& device, + std::index_sequence) { + return CountAllocations([&] { + TensorView tensor{ + data, std::initializer_list{shape[indices]...}, + DataType::kFloat32, device, + std::initializer_list{strides[indices]...}}; + (void)tensor; + }); +} + +template +void TestConstructionAllocationsForRank(infini::rt::test::TestContext* context, + void* data, const Device& device) { + constexpr std::size_t kCombinedMetadataAllocationCount = rank <= 8 ? 0 : 1; + constexpr std::size_t kRvalueMetadataAllocationCount = rank <= 8 ? 0 : 2; + constexpr std::size_t kGeneratedMetadataAllocationCount = rank <= 8 ? 0 : 1; + + const auto shape_values = MakeShapeValues(); + const auto stride_values = MakeStrideValues(); + const TensorView::Shape shape{shape_values.begin(), shape_values.end()}; + const TensorView::Strides strides{stride_values.begin(), stride_values.end()}; + const VectorTensorLike tensor_like{ + data, std::vector{shape_values.begin(), shape_values.end()}, + DataType::kFloat32, device, + std::vector{stride_values.begin(), stride_values.end()}}; + + ExpectRankAllocationCount( context, CountAllocations([&] { - TensorView tensor{data, TensorView::Shape{2, 3}}; - shape_only_metadata_is_default = tensor.dtype() == DataType::kFloat32 && - tensor.device() == cpu && - tensor.strides() == strides; + TensorView tensor{data, shape, DataType::kFloat32, device, strides}; + (void)tensor; }), - 2, "Rvalue shape construction should use default metadata directly."); - context->Expect(shape_only_metadata_is_default, - "Shape-only construction should keep default metadata."); + kCombinedMetadataAllocationCount, rank, + "lvalue shape and strides should have the expected allocation count."); - bool dtype_only_metadata_is_default = false; - ExpectAllocationCount( + ExpectRankAllocationCount( context, CountAllocations([&] { - TensorView tensor{data, TensorView::Shape{2, 3}, DataType::kFloat64}; - dtype_only_metadata_is_default = tensor.dtype() == DataType::kFloat64 && - tensor.device() == cpu && - tensor.strides() == strides; + TensorView tensor{ + data, TensorView::Shape{shape_values.begin(), shape_values.end()}, + DataType::kFloat32, device, + TensorView::Strides{stride_values.begin(), stride_values.end()}}; + (void)tensor; }), - 2, "Rvalue shape and dtype should use default device and strides."); - context->Expect( - dtype_only_metadata_is_default, - "Shape and dtype construction should keep default device and strides."); - - bool device_only_metadata_is_default = false; - ExpectAllocationCount( + kRvalueMetadataAllocationCount, rank, + "exact-type rvalue shape and strides should have the expected allocation " + "count."); + + ExpectRankAllocationCount( + context, + CountInitializerListConstructionAllocations( + data, shape_values, stride_values, device, + std::make_index_sequence{}), + kCombinedMetadataAllocationCount, rank, + "initializer-list overload should have the expected allocation count."); + + ExpectRankAllocationCount( context, CountAllocations([&] { - TensorView tensor{data, TensorView::Shape{2, 3}, indexed_cpu}; - device_only_metadata_is_default = - tensor.dtype() == DataType::kFloat32 && - tensor.device() == indexed_cpu && tensor.strides() == strides; + TensorView tensor{tensor_like}; + (void)tensor; }), - 2, "Rvalue shape and device should use default dtype and strides."); - context->Expect( - device_only_metadata_is_default, - "Shape and device construction should keep default dtype and strides."); + kCombinedMetadataAllocationCount, rank, + "vector-backed `TensorLike` should have the expected allocation count."); - ExpectAllocationCount( + ExpectRankAllocationCount( context, CountAllocations([&] { - TensorView tensor{data, shape, DataType::kFloat32, cpu, strides}; + TensorView tensor{data, shape, DataType::kFloat32, device}; (void)tensor; }), - 2, "Lvalue shape and strides should allocate only their owned copies."); - - ExpectAllocationCount( + kCombinedMetadataAllocationCount, rank, + "ordinary default-stride construction should have the expected " + "allocation count."); + + TensorView::Shape explicit_move_shape{shape_values.begin(), + shape_values.end()}; + TensorView::Strides explicit_move_strides{stride_values.begin(), + stride_values.end()}; + ExpectRankAllocationCount( context, CountAllocations([&] { - TensorView tensor{data, TensorView::Shape{2, 3}, DataType::kFloat32, - cpu, TensorView::Strides{3, 1}}; + TensorView tensor{data, std::move(explicit_move_shape), + DataType::kFloat32, device, + std::move(explicit_move_strides)}; (void)tensor; }), - 2, "Rvalue shape and strides should transfer their allocations."); + 0, rank, "moved exact explicit metadata should not allocate."); - ExpectAllocationCount( + TensorView::Shape default_move_shape{shape_values.begin(), + shape_values.end()}; + ExpectRankAllocationCount( context, CountAllocations([&] { - TensorView tensor{data, TensorView::Shape{2, 3}, DataType::kFloat32, - cpu}; + TensorView tensor{data, std::move(default_move_shape), + DataType::kFloat32, device}; (void)tensor; }), - 2, - "Rvalue shape construction should allocate shape and default strides."); + kGeneratedMetadataAllocationCount, rank, + "moved shape with generated default strides should have the expected " + "allocation count."); +} + +template +void TestConstructionAllocationsForRanks(infini::rt::test::TestContext* context, + void* data, const Device& device, + std::index_sequence) { + (TestConstructionAllocationsForRank(context, data, device), ...); +} + +void TestConstructionAllocationMatrix(infini::rt::test::TestContext* context) { + std::array data{}; + const Device cpu{Device::Type::kCpu}; + TestConstructionAllocationsForRanks(context, data.data(), cpu, + std::make_index_sequence<10>{}); +} + +void TestValueAndDerivedViewAllocations( + infini::rt::test::TestContext* context) { + std::array data{}; + const Device cpu{Device::Type::kCpu}; + const TensorView::Shape shape8{2, 2, 2, 2, 2, 2, 2, 2}; + const TensorView::Strides strides8{128, 64, 32, 16, 8, 4, 2, 1}; + const TensorView::Shape shape9{2, 2, 2, 2, 2, 2, 2, 2, 2}; + const TensorView::Strides strides9{256, 128, 64, 32, 16, 8, 4, 2, 1}; + const TensorView source8{data.data(), shape8, DataType::kFloat32, cpu, + strides8}; + const TensorView source9{data.data(), shape9, DataType::kFloat32, cpu, + strides9}; + + ExpectAllocationCount(context, CountAllocations([&] { + TensorView copied{source8}; + (void)copied; + }), + 0, "Copying rank-8 metadata should stay inline."); ExpectAllocationCount( context, CountAllocations([&] { - TensorView tensor{data, {2, 3}, DataType::kFloat32, cpu, {3, 1}}; - (void)tensor; + TensorView copied{source9}; + (void)copied; }), - 2, "Initializer lists should construct owned metadata directly."); + 1, "Copying rank-9 metadata should use one combined allocation."); - const TensorView source{data, shape, DataType::kFloat32, cpu, strides}; + TensorView move_source8{data.data(), shape8, DataType::kFloat32, cpu, + strides8}; + TensorView move_source9{data.data(), shape9, DataType::kFloat32, cpu, + strides9}; ExpectAllocationCount(context, CountAllocations([&] { - TensorView indexed = source[0]; + TensorView moved{std::move(move_source8)}; + (void)moved; + }), + 0, "Moving rank-8 metadata should not allocate."); + ExpectAllocationCount(context, CountAllocations([&] { + TensorView moved{std::move(move_source9)}; + (void)moved; + }), + 0, + "Moving rank-9 metadata should transfer heap storage."); + + ExpectAllocationCount(context, CountAllocations([&] { + TensorView indexed = source8[0]; (void)indexed; }), - 2, - "Indexing should allocate only the result metadata."); + 0, "Indexing rank-8 to rank-7 should stay inline."); + ExpectAllocationCount(context, CountAllocations([&] { + TensorView indexed = source9[0]; + (void)indexed; + }), + 0, "Indexing rank-9 to rank-8 should stay inline."); + const TensorView transpose_source{data.data(), TensorView::Shape{2, 2}, + DataType::kFloat32, cpu, + TensorView::Strides{2, 1}}; + ExpectAllocationCount(context, CountAllocations([&] { + TensorView transposed = transpose_source.T(); + (void)transposed; + }), + 0, "Transposing rank-2 metadata should stay inline."); +} + +void TestDefaultMetadataAllocations(infini::rt::test::TestContext* context) { + std::array data{}; + const Device cpu{Device::Type::kCpu}; + const Device indexed_cpu{Device::Type::kCpu, 1}; + const TensorView::Shape expected_shape{2, 3}; + const TensorView::Strides expected_strides{3, 1}; + + bool shape_only_metadata_is_default = false; ExpectAllocationCount( context, CountAllocations([&] { - TensorView transposed = source.T(); - (void)transposed; + TensorView tensor{data.data(), TensorView::Shape{2, 3}}; + shape_only_metadata_is_default = tensor.shape() == expected_shape && + tensor.dtype() == DataType::kFloat32 && + tensor.device() == cpu && + tensor.strides() == expected_strides; }), - 2, "Transposing should allocate only the result metadata."); + 0, "Rank-2 shape-only construction should stay inline."); + context->Expect(shape_only_metadata_is_default, + "Shape-only construction should keep default metadata."); + bool dtype_only_metadata_is_default = false; ExpectAllocationCount( context, CountAllocations([&] { - TensorView copied{source}; - (void)copied; + TensorView tensor{data.data(), TensorView::Shape{2, 3}, + DataType::kFloat64}; + dtype_only_metadata_is_default = tensor.shape() == expected_shape && + tensor.dtype() == DataType::kFloat64 && + tensor.device() == cpu && + tensor.strides() == expected_strides; }), - 2, "Copying should allocate one owned shape and one owned stride array."); + 0, "Rank-2 shape and dtype construction should stay inline."); + context->Expect( + dtype_only_metadata_is_default, + "Shape and dtype construction should keep default device and strides."); - TensorView move_source{data, shape, DataType::kFloat32, cpu, strides}; + bool device_only_metadata_is_default = false; ExpectAllocationCount( context, CountAllocations([&] { - TensorView moved{std::move(move_source)}; - (void)moved; + TensorView tensor{data.data(), TensorView::Shape{2, 3}, indexed_cpu}; + device_only_metadata_is_default = + tensor.shape() == expected_shape && + tensor.dtype() == DataType::kFloat32 && + tensor.device() == indexed_cpu && + tensor.strides() == expected_strides; }), - 0, "Moving should transfer owned metadata without allocating."); + 0, "Rank-2 shape and device construction should stay inline."); + context->Expect( + device_only_metadata_is_default, + "Shape and device construction should keep default dtype and strides."); } } // namespace @@ -185,7 +359,9 @@ void TestTensorViewAllocations(infini::rt::test::TestContext* context) { int main() { infini::rt::test::TestContext context; - TestTensorViewAllocations(&context); + TestConstructionAllocationMatrix(&context); + TestValueAndDerivedViewAllocations(&context); + TestDefaultMetadataAllocations(&context); return context.ExitCode(); }