Uni20
Asynchronous tensor and runtime library.
Loading...
Searching...
No Matches
iteration_plan.hpp
Go to the documentation of this file.
1#pragma once
2
12
13#include <algorithm>
14#include <array>
15#include <cstddef>
16#include <cstdlib>
17#include <limits>
18#include <tuple>
19#include <utility>
20
21namespace uni20
22{
23
28template <typename ExtentT = std::size_t, typename StrideT = std::ptrdiff_t> struct extent_stride
29{
30 ExtentT extent{};
31 StrideT stride{};
32
33 constexpr extent_stride() = default;
34 constexpr extent_stride(ExtentT extent_, StrideT stride_) : extent(extent_), stride(stride_) {}
35
41 [[nodiscard]] constexpr bool can_merge_with_inner(extent_stride inner) const noexcept
42 {
43 if (extent == ExtentT{1} || inner.extent == ExtentT{1}) return true;
44 return stride == inner.stride * static_cast<StrideT>(inner.extent);
45 }
46
52 constexpr void merge_with_inner(extent_stride inner) noexcept
53 {
54 if (extent == ExtentT{1})
55 {
56 extent = inner.extent;
57 stride = inner.extent == ExtentT{1} ? StrideT{1} : inner.stride;
58 return;
59 }
60 if (inner.extent == ExtentT{1}) return;
61
62 extent *= inner.extent;
63 stride = inner.stride;
64 }
65};
66
71{
72 std::ptrdiff_t minimum = 0;
73 std::ptrdiff_t maximum = 0;
74
76 [[nodiscard]] constexpr bool empty() const noexcept { return minimum > maximum; }
77};
78
83template <std::size_t OperandCount, std::size_t MaximumRank> struct multi_iteration_plan
84{
85 static constexpr std::size_t operand_count = OperandCount;
86 static constexpr std::size_t maximum_rank = MaximumRank;
87
89 std::array<std::ptrdiff_t, OperandCount> base_offsets{};
90 std::array<iteration_offset_range, OperandCount> reachable_offsets{};
91 std::size_t element_count = 1;
92 bool representable = true;
93
95 [[nodiscard]] std::size_t rank() const noexcept { return dimensions.size(); }
96
98 [[nodiscard]] bool empty() const noexcept { return element_count == 0; }
99};
100
101namespace detail
102{
103
104[[nodiscard]] constexpr bool checked_iteration_multiply(std::ptrdiff_t lhs, std::ptrdiff_t rhs,
105 std::ptrdiff_t& result) noexcept
106{
107 if (lhs == 0 || rhs == 0)
108 {
109 result = 0;
110 return true;
111 }
112
113 constexpr auto minimum = std::numeric_limits<std::ptrdiff_t>::min();
114 constexpr auto maximum = std::numeric_limits<std::ptrdiff_t>::max();
115 if ((lhs == -1 && rhs == minimum) || (rhs == -1 && lhs == minimum)) return false;
116
117 if (lhs > 0)
118 {
119 if ((rhs > 0 && lhs > maximum / rhs) || (rhs < 0 && rhs < minimum / lhs)) return false;
120 }
121 else
122 {
123 if ((rhs > 0 && lhs < minimum / rhs) || (rhs < 0 && lhs < maximum / rhs)) return false;
124 }
125
126 result = lhs * rhs;
127 return true;
128}
129
130[[nodiscard]] constexpr bool checked_iteration_add(std::ptrdiff_t lhs, std::ptrdiff_t rhs,
131 std::ptrdiff_t& result) noexcept
132{
133 constexpr auto minimum = std::numeric_limits<std::ptrdiff_t>::min();
134 constexpr auto maximum = std::numeric_limits<std::ptrdiff_t>::max();
135 if ((rhs > 0 && lhs > maximum - rhs) || (rhs < 0 && lhs < minimum - rhs)) return false;
136 result = lhs + rhs;
137 return true;
138}
139
140template <std::size_t OperandCount, std::size_t MaximumRank>
141void finish_iteration_plan(multi_iteration_plan<OperandCount, MaximumRank>& plan)
142{
143 if (plan.dimensions.size() == 1 && plan.dimensions[0].extent == 0)
144 {
145 plan.element_count = 0;
146 for (auto& range : plan.reachable_offsets)
147 range = iteration_offset_range{.minimum = 0, .maximum = -1};
148 return;
149 }
150
151 plan.element_count = 1;
152 for (auto const& dimension : plan.dimensions)
153 {
154 auto const extent = static_cast<std::size_t>(dimension.extent);
155 if (extent != 0 && plan.element_count > std::numeric_limits<std::size_t>::max() / extent)
156 {
157 plan.representable = false;
158 return;
159 }
160 plan.element_count *= extent;
161 }
162
163 for (std::size_t operand = 0; operand < OperandCount; ++operand)
164 {
165 auto minimum = plan.base_offsets[operand];
166 auto maximum = plan.base_offsets[operand];
167 for (auto const& dimension : plan.dimensions)
168 {
169 std::ptrdiff_t delta = 0;
170 if (!checked_iteration_multiply(static_cast<std::ptrdiff_t>(dimension.extent - 1), dimension.strides[operand],
171 delta))
172 {
173 plan.representable = false;
174 return;
175 }
176
177 auto& bound = delta < 0 ? minimum : maximum;
178 if (!checked_iteration_add(bound, delta, bound))
179 {
180 plan.representable = false;
181 return;
182 }
183 }
184 plan.reachable_offsets[operand] = iteration_offset_range{.minimum = minimum, .maximum = maximum};
185 }
186}
187
188} // namespace detail
189
195template <typename Mapping> auto make_iteration_plan_with_offset(Mapping const& mapping)
196{
197 using size_type = typename Mapping::size_type;
198 using index_type = typename Mapping::index_type;
199 static constexpr size_type Rank = Mapping::extents_type::rank();
200
201 static_vector<extent_strides<1>, Rank> raw_plan;
203 index_type offset = 0;
204
205 for (size_type i = 0; i < Rank; ++i)
206 {
207 size_type const extent = mapping.extents().extent(i);
208
209 // A zero-extent dimension makes the whole iteration empty (0 elements).
210 // Encode that as a single retained extent-0 dim so the loop nest runs it
211 // zero times; no other dimension's stride matters. This is deliberately
212 // distinct from an *empty* plan, which denotes a single element at the base
213 // offset (a rank-0 scalar) -- see the empty-plan return below.
214 if (extent == 0)
215 {
216 plan.emplace_back(size_type{0}, index_type{0});
217 return std::pair{plan, index_type{0}};
218 }
219
220 // A size-1 dimension spans index 0 only: it adds 0*stride to every offset
221 // and a factor of 1 to the element count, so it coalesces with anything by
222 // simply vanishing. Drop it here rather than leaving it to the arithmetic
223 // merge, whose merge_with_inner would otherwise adopt this dim's
224 // meaningless stride.
225 if (extent == 1) continue;
226
227 index_type stride = mapping.stride(i);
228 if (stride < 0)
229 {
230 offset += stride * static_cast<index_type>(extent - 1);
231 stride = -stride;
232 }
233
234 raw_plan.emplace_back(extent, std::array<index_type, 1>{stride});
235 }
236
237 // An empty plan denotes a single element at the base offset (rank-0 scalar):
238 // every dimension was size-1, or the mapping is rank-0, so there is exactly
239 // one element to visit.
240 if (raw_plan.empty()) return std::pair{plan, offset};
241
242 std::sort(raw_plan.begin(), raw_plan.end(),
243 [](auto const& lhs, auto const& rhs) { return std::abs(lhs.strides[0]) > std::abs(rhs.strides[0]); });
244
245 merge_strides_right(raw_plan);
246
247 for (auto const& dim : raw_plan)
248 {
249 plan.emplace_back(static_cast<size_type>(dim.extent), static_cast<index_type>(dim.strides[0]));
250 }
251
252 return std::pair{plan, offset};
253}
254
263template <typename... Mappings> auto make_multi_iteration_plan(std::tuple<Mappings...> const& mappings)
264{
265 static_assert(sizeof...(Mappings) >= 1, "At least one mapping is required");
266
267 using first_mapping = std::tuple_element_t<0, std::tuple<Mappings...>>;
268 using size_type = typename first_mapping::size_type;
269 using index_type = std::ptrdiff_t;
270 static constexpr std::size_t N = sizeof...(Mappings);
271 static constexpr size_type Rank = first_mapping::extents_type::rank();
272
273 static_assert(((Mappings::extents_type::rank() == Rank) && ...),
274 "All iteration-plan mappings must have the same rank");
275
276 auto const& base_mapping = std::get<0>(mappings);
277 auto const& base_extents = base_mapping.extents();
278 std::size_t logical_element_count = 1;
279
280 auto check_extents = [&](auto const& mapping) {
281 for (size_type axis = 0; axis < Rank; ++axis)
282 PRECONDITION_EQUAL(mapping.extents().extent(axis), base_extents.extent(axis),
283 "elementwise iteration requires equal extents");
284 };
285 std::apply([&](auto const&... mapping) { (check_extents(mapping), ...); }, mappings);
286
288
289 if constexpr (Rank > 0)
290 {
291 auto make_empty_plan = [&] {
292 plan.dimensions.clear();
293 plan.dimensions.emplace_back(size_type{0}, std::array<index_type, N>{});
294 plan.base_offsets = std::array<index_type, N>{};
295 detail::finish_iteration_plan(plan);
296 };
297
298 for (size_type i = 0; i < Rank; ++i)
299 {
300 size_type const extent = base_extents.extent(i);
301
302 // A zero-extent dimension makes the whole iteration empty (0 elements):
303 // retain a single extent-0 dim so the loop nest runs zero times, and reset
304 // the offset corrections (unused once nothing is visited). Distinct from an
305 // empty plan, which denotes a single element (rank-0 scalar).
306 if (extent == 0)
307 {
308 make_empty_plan();
309 return plan;
310 }
311
312 if (std::cmp_greater(extent, std::numeric_limits<std::ptrdiff_t>::max()) ||
313 logical_element_count > std::numeric_limits<std::ptrdiff_t>::max() / static_cast<std::size_t>(extent))
314 {
315 // Only the exceptional overflow path scans ahead. A later zero extent
316 // still makes the complete logical iteration space empty.
317 for (size_type remaining = i + 1; remaining < Rank; ++remaining)
318 {
319 if (base_extents.extent(remaining) == 0)
320 {
321 make_empty_plan();
322 return plan;
323 }
324 }
325 plan.representable = false;
326 return plan;
327 }
328 logical_element_count *= static_cast<std::size_t>(extent);
329
330 // Size-1 dimensions span index 0 only (0*stride, count factor 1), so drop
331 // them; they coalesce with anything by vanishing.
332 if (extent == 1) continue;
333
334 auto strides = std::apply(
335 [i](auto const&... mapping) {
336 return std::array<index_type, N>{static_cast<index_type>(mapping.stride(i))...};
337 },
338 mappings);
339
340 if (strides[0] < 0)
341 {
342 for (std::size_t k = 0; k < N; ++k)
343 {
344 index_type delta = 0;
345 if (!detail::checked_iteration_multiply(strides[k], static_cast<index_type>(extent - 1), delta) ||
346 !detail::checked_iteration_add(plan.base_offsets[k], delta, plan.base_offsets[k]))
347 {
348 plan.representable = false;
349 return plan;
350 }
351 if (strides[k] == std::numeric_limits<index_type>::min())
352 {
353 plan.representable = false;
354 return plan;
355 }
356 strides[k] = -strides[k];
357 }
358 }
359
360 plan.dimensions.emplace_back(extent, strides);
361 }
362 }
363
364 // Empty plan => single element at the base offset (rank-0 scalar).
365 if (!plan.dimensions.empty())
366 {
367 std::sort(plan.dimensions.begin(), plan.dimensions.end(),
368 [](auto const& lhs, auto const& rhs) { return std::abs(lhs.strides[0]) > std::abs(rhs.strides[0]); });
369 merge_strides_right(plan.dimensions);
370 }
371
372 detail::finish_iteration_plan(plan);
373 return plan;
374}
375
376} // namespace uni20
A fixed-capacity vector with stack storage and std::vector-like API.
Definition static_vector.hpp:24
iterator end() noexcept
Definition static_vector.hpp:198
bool empty() const noexcept
Definition static_vector.hpp:210
void emplace_back(Args &&... args)
Construct a new element in-place at the end.
Definition static_vector.hpp:242
iterator begin() noexcept
Definition static_vector.hpp:194
auto make_iteration_plan_with_offset(Mapping const &mapping)
Construct a merged iteration plan and offset for a single mapping.
Definition iteration_plan.hpp:195
auto make_multi_iteration_plan(std::tuple< Mappings... > const &mappings)
Build a merged iteration plan for multiple tensors sharing the same extents.
Definition iteration_plan.hpp:263
constexpr void merge_with_inner(extent_stride inner) noexcept
Merge an inner dimension into this one.
Definition iteration_plan.hpp:52
constexpr bool can_merge_with_inner(extent_stride inner) const noexcept
Returns true when two adjacent dimensions can be merged.
Definition iteration_plan.hpp:41
auto strides(S const &s)
Retrieve the strides associated with a strided mdspec-like type.
Definition mdspan/concepts.hpp:558
static_vector< extent_strides< 2 >, R > merge_strides_right(Extents const &ext, std::array< std::ptrdiff_t, R > const &Stride1, std::array< std::ptrdiff_t, R > const &Stride2)
Build and merge stride metadata from two stride arrays favouring row-major (right) order.
Definition strides.hpp:240
Mdspan concept and accessor extensions for Uni20.
Definition async/async.hpp:26
std::ptrdiff_t size_type
Signed size and index type, used for tensor extents and indexing.
Definition types.hpp:18
std::ptrdiff_t index_type
Definition types.hpp:19
Defines static_vector, a fixed-capacity, stack-allocated vector with a std::vector-like API.
Stride utilities for mdspan-like tensors.
Represents a single dimension's extent and stride for iteration planning.
Definition iteration_plan.hpp:29
StrideT stride
Definition iteration_plan.hpp:31
constexpr extent_stride()=default
ExtentT extent
Definition iteration_plan.hpp:30
constexpr extent_stride(ExtentT extent_, StrideT stride_)
Definition iteration_plan.hpp:34
Inclusive range of handle-relative offsets visited by an iteration plan.
Definition iteration_plan.hpp:71
std::ptrdiff_t maximum
Definition iteration_plan.hpp:73
constexpr bool empty() const noexcept
Report whether the iteration visits no offsets.
Definition iteration_plan.hpp:76
std::ptrdiff_t minimum
Definition iteration_plan.hpp:72
Compact affine iteration plan shared by host and device backends.
Definition iteration_plan.hpp:84
std::array< std::ptrdiff_t, OperandCount > base_offsets
Definition iteration_plan.hpp:89
static_vector< extent_strides< OperandCount >, MaximumRank > dimensions
Definition iteration_plan.hpp:88
std::array< iteration_offset_range, OperandCount > reachable_offsets
Definition iteration_plan.hpp:90
static constexpr std::size_t operand_count
Definition iteration_plan.hpp:85
std::size_t rank() const noexcept
Report the compact rank after size-one elimination and coalescing.
Definition iteration_plan.hpp:95
bool representable
Definition iteration_plan.hpp:92
bool empty() const noexcept
Report whether the logical iteration space is empty.
Definition iteration_plan.hpp:98
std::size_t element_count
Definition iteration_plan.hpp:91
static constexpr std::size_t maximum_rank
Definition iteration_plan.hpp:86
#define PRECONDITION_EQUAL(a, b,...)
Definition trace.hpp:351