NumericAL
A header-only C++20 lexer built on REAL
Loading...
Searching...
No Matches
tensor.hpp
Go to the documentation of this file.
1
20#ifndef NUMERICAL_TENSOR_HPP
21#define NUMERICAL_TENSOR_HPP
22
23#include <algorithm>
24#include <array>
25#include <cctype>
26#include <cmath>
27#include <complex> // so std::abs resolves for a complex scalar, regardless of include order
28#include <cstddef>
29#include <deque>
30#include <initializer_list>
31#include <map>
32#include <optional>
33#include <stdexcept>
34#include <string>
35#include <string_view>
36#include <utility>
37#include <vector>
38
39#include "gemm.hpp"
40#include "matrix.hpp"
41#include "parallel.hpp"
42#include "vector.hpp"
43
44namespace numerical {
45
47 class einsum_error : public std::runtime_error
48 {
49 public:
50
52 explicit einsum_error(const std::string& message)
53 : std::runtime_error(message)
54 {}
55 };
56
61 template <typename T>
62 class tensor
63 {
64 public:
65
72 tensor(std::vector<std::size_t> shape,
73 buffer<T> elements)
74 : shape_(std::move(shape)), data_(std::move(elements))
75 {
76 finalize();
77 }
78
85 tensor(std::vector<std::size_t> shape,
86 const std::vector<T>& elements)
87 : tensor(std::move(shape),
88 buffer<T>(elements.begin(), elements.end()))
89 {}
90
92 tensor(std::vector<std::size_t> shape,
93 std::initializer_list<T> elements)
94 : tensor(std::move(shape),
95 buffer<T>(elements))
96 {}
97
99 explicit tensor(const vector<T>& v)
100 : shape_ {v.size()}
101 {
102 data_.reserve(v.size());
103 for (std::size_t i {0}; i < v.size(); ++i) {
104 data_.push_back(v[i]);
105 }
106 finalize();
107 }
108
110 explicit tensor(matrix<T>&& m)
111 : shape_ {m.rows(), m.cols()}, data_ {std::move(m).elements()}
112 {
113 finalize();
114 }
115
117 explicit tensor(const matrix<T>& m)
118 : shape_ {m.rows(), m.cols()}, data_(m.begin(), m.end())
119 {
120 finalize();
121 }
122
124 [[nodiscard]] std::size_t rank() const noexcept
125 {
126 return shape_.size();
127 }
128
130 [[nodiscard]] std::size_t size() const noexcept
131 {
132 return data_.size();
133 }
134
136 [[nodiscard]] const std::vector<std::size_t>& shape() const noexcept
137 {
138 return shape_;
139 }
140
143 {
144 return data_;
145 }
146
148 [[nodiscard]] const T& operator()(const std::vector<std::size_t>& index) const
149 {
150 std::size_t flat {0};
151 for (std::size_t axis {0}; axis < index.size(); ++axis) {
152 flat += strides_[axis] * index[axis];
153 }
154 return data_[flat];
155 }
156
158 [[nodiscard]] bool operator==(const tensor& other) const
159 {
160 return shape_ == other.shape_ && data_ == other.data_;
161 }
162
163 private:
164
166 void finalize()
167 {
168 strides_.assign(shape_.size(), 1);
169 std::size_t acc {1};
170 for (std::size_t axis {shape_.size()}; axis-- > 0;) {
171 strides_[axis] = acc;
172 acc = detail::checked_mul(acc, shape_[axis]); // guard the element-count product
173 }
174 if (data_.size() != acc) {
175 throw dimension_error("tensor: data size does not match the shape");
176 }
177 }
178
179 std::vector<std::size_t> shape_;
180 std::vector<std::size_t> strides_;
182 };
183
191 template <typename T>
193 const tensor<T>& b,
194 double tolerance = 1e-9)
195 {
196 if (a.shape() != b.shape()) {
197 return false;
198 }
199 for (std::size_t i {0}; i < a.size(); ++i) {
200 // Equal, or within the tolerance: a NaN is neither, so it is never close, and two equal
201 // infinities are equal although their difference is NaN.
202 if (!(a.data()[i] == b.data()[i]) && !(std::abs(a.data()[i] - b.data()[i]) <= tolerance)) {
203 return false;
204 }
205 }
206 return true;
207 }
208
209 namespace detail {
215 template <typename T>
216 [[nodiscard]] T pairwise_sum(const T* first,
217 std::size_t count,
218 std::size_t stride)
219 {
220 constexpr std::size_t lanes {8};
221 constexpr std::size_t block {128};
222 if (count < lanes) {
223 T sum {};
224 for (std::size_t i {0}; i < count; ++i) {
225 sum += first[i * stride];
226 }
227 return sum;
228 }
229 if (count <= block) {
230 std::array<T, lanes> partial {};
231 for (std::size_t j {0}; j < lanes; ++j) {
232 partial[j] = first[j * stride];
233 }
234 std::size_t i {lanes};
235 for (; i < count - (count % lanes); i += lanes) {
236 for (std::size_t j {0}; j < lanes; ++j) {
237 partial[j] += first[(i + j) * stride];
238 }
239 }
240 T sum {((partial[0] + partial[1]) + (partial[2] + partial[3])) + ((partial[4] + partial[5]) + (partial[6] + partial[7]))};
241 for (; i < count; ++i) {
242 sum += first[i * stride];
243 }
244 return sum;
245 }
246 std::size_t half {count / 2};
247 half -= half % lanes;
248 return pairwise_sum(first, half, stride) + pairwise_sum(first + (half * stride), count - half, stride);
249 }
250
256 template <typename T>
257 void pairwise_sum_rows(const T* first,
258 std::size_t count,
259 std::size_t stride,
260 std::size_t width,
261 T* out)
262 {
263 constexpr std::size_t lanes {8};
264 constexpr std::size_t block {128};
265 const auto row = [&](std::size_t i) { return first + (i * stride); };
266 if (count < lanes) {
267 std::fill_n(out, width, T {});
268 for (std::size_t i {0}; i < count; ++i) {
269 for (std::size_t q {0}; q < width; ++q) {
270 out[q] += row(i)[q];
271 }
272 }
273 return;
274 }
275 if (count <= block) {
276 std::vector<T> partial(lanes * width);
277 for (std::size_t j {0}; j < lanes; ++j) {
278 std::copy_n(row(j), width, partial.data() + (j * width));
279 }
280 std::size_t i {lanes};
281 for (; i < count - (count % lanes); i += lanes) {
282 for (std::size_t j {0}; j < lanes; ++j) {
283 T* const sums {partial.data() + (j * width)};
284 const T* const next {row(i + j)};
285 for (std::size_t q {0}; q < width; ++q) {
286 sums[q] += next[q];
287 }
288 }
289 }
290 const T* const p {partial.data()};
291 for (std::size_t q {0}; q < width; ++q) {
292 out[q] = ((p[q] + p[width + q]) + (p[(2 * width) + q] + p[(3 * width) + q]))
293 + ((p[(4 * width) + q] + p[(5 * width) + q]) + (p[(6 * width) + q] + p[(7 * width) + q]));
294 }
295 for (; i < count; ++i) {
296 for (std::size_t q {0}; q < width; ++q) {
297 out[q] += row(i)[q];
298 }
299 }
300 return;
301 }
302 std::size_t half {count / 2};
303 half -= half % lanes;
304 std::vector<T> second(width);
305 pairwise_sum_rows(first, half, stride, width, out);
306 pairwise_sum_rows(row(half), count - half, stride, width, second.data());
307 for (std::size_t q {0}; q < width; ++q) {
308 out[q] = out[q] + second[q];
309 }
310 }
311
314 inline constexpr std::size_t pairwise_columns {512};
315
321 template <typename T>
322 void pairwise_sums(const T* data,
323 std::size_t outer,
324 std::size_t count,
325 std::size_t inner,
326 T* out)
327 {
328 for (std::size_t o {0}; o < outer; ++o) {
329 const T* const base {data + (o * count * inner)};
330 if (inner == 1) {
331 out[o] = pairwise_sum(base, count, 1);
332 continue;
333 }
334 for (std::size_t q {0}; q < inner; q += pairwise_columns) {
335 pairwise_sum_rows(base + q, count, inner, std::min(pairwise_columns, inner - q), out + (o * inner) + q);
336 }
337 }
338 }
339
341 [[nodiscard]] inline bool is_label(char ch)
342 {
343 return std::isalpha(static_cast<unsigned char>(ch)) != 0;
344 }
345
347 [[nodiscard]] inline std::vector<std::string> split_inputs(const std::string& lhs)
348 {
349 std::vector<std::string> groups;
350 std::string current;
351 for (const char ch : lhs) {
352 if (ch == ',') {
353 groups.push_back(current);
354 current.clear();
355 }
356 else if (is_label(ch)) {
357 current.push_back(ch);
358 }
359 else {
360 throw einsum_error("einsum: invalid character in input subscripts");
361 }
362 }
363 groups.push_back(current);
364 return groups;
365 }
366
374 template <typename T>
376 const T* b,
377 std::size_t rows,
378 std::size_t inner,
379 std::size_t cols,
380 std::vector<std::size_t> out_shape)
381 {
382 buffer<T> c(rows * cols); // the caller routes only an output of two elements or more here
383 parallel_zero(c.data(), c.size()); // gemm adds to C
384 if (inner != 0) {
385 gemm(rows, cols, inner, a, inner, b, cols, c.data(), cols);
386 }
387 return tensor<T> {std::move(out_shape), std::move(c)};
388 }
389
391 inline constexpr std::size_t vector_chunk {128};
392
398 template <typename T>
400 const T* b,
401 std::size_t rows,
402 std::size_t inner,
403 std::size_t cols,
404 std::vector<std::size_t> out_shape)
405 {
406 if (cols == 1 && rows > 1 && inner != 0) { // a matrix times a vector: each row's dot product, as "i,i->"
407 buffer<T> y(rows);
408 matrix_times_vector(a, rows, inner, b, y.data());
409 return tensor<T> {std::move(out_shape), std::move(y)};
410 }
411 if (rows == 1 && cols > 1 && inner != 0) { // a vector times a matrix: its rows scaled and added in order
412 buffer<T> y(cols, T {});
413 parallel_for((cols + vector_chunk - 1) / vector_chunk, inner * vector_chunk, [&](std::size_t chunk) {
414 const std::size_t first {chunk * vector_chunk};
415 const std::size_t count {std::min(vector_chunk, cols - first)};
416 for (std::size_t k {0}; k < inner; ++k) {
417 add_product(y.data() + first, a[k], b + (k * cols) + first, count);
418 }
419 });
420 return tensor<T> {std::move(out_shape), std::move(y)};
421 }
422 if (rows * cols != 1) { // the product, an empty one included; a single element is one dot product
423 return tile_gemm<T>(a, b, rows, inner, cols, std::move(out_shape));
424 }
425 return tensor<T> {std::move(out_shape), buffer<T> {chunked_dot(a, b, inner)}};
426 }
427
430 inline constexpr std::size_t transposed_columns {8};
431
438 template <typename T>
440 const T* bt,
441 std::size_t ldbt,
442 std::size_t inner,
443 std::size_t cols)
444 {
445 buffer<T> y(cols, T {});
446 parallel_for((cols + vector_chunk - 1) / vector_chunk, inner * vector_chunk, [&](std::size_t chunk) {
447 const std::size_t last {std::min((chunk + 1) * vector_chunk, cols)};
448 std::size_t j {chunk * vector_chunk};
449 for (; j + transposed_columns <= last; j += transposed_columns) { // a whole group: its sums in registers
450 std::array<T, transposed_columns> sums {};
451 for (std::size_t k {0}; k < inner; ++k) {
452 const T f {a[k]};
453 for (std::size_t c {0}; c < transposed_columns; ++c) {
454 add_one_product(sums[c], f, bt[((j + c) * ldbt) + k]);
455 }
456 }
457 std::copy(sums.begin(), sums.end(), y.data() + j);
458 }
459 for (; j < last; ++j) {
460 T sum {};
461 for (std::size_t k {0}; k < inner; ++k) {
462 add_one_product(sum, a[k], bt[(j * ldbt) + k]);
463 }
464 y[j] = sum;
465 }
466 });
467 return y;
468 }
469
470#ifdef NUMERICAL_X86_COMPILED
473 [[nodiscard]] inline buffer<double> vector_times_transposed(const double* a,
474 const double* bt,
475 std::size_t ldbt,
476 std::size_t inner,
477 std::size_t cols)
478 {
481 return vector_times_transposed<double>(a, bt, ldbt, inner, cols);
482 }
483 buffer<double> y(cols);
484 parallel_for((cols + vector_chunk - 1) / vector_chunk, inner * vector_chunk, [&](std::size_t chunk) {
485 avx2_transposed_columns(a, bt, ldbt, inner, chunk * vector_chunk, std::min((chunk + 1) * vector_chunk, cols), y.data());
486 });
487 return y;
488 }
489
490#endif
491
499 template <typename T>
500 [[nodiscard]] std::optional<tensor<T>> sum_over_run(const std::string& in,
501 const std::string& output,
502 const std::vector<std::size_t>& shape,
503 const T* data)
504 {
505 const std::size_t first {in.find_first_not_of(output)};
506 if (first == std::string::npos) {
507 return std::nullopt; // nothing summed: a permutation, or the operand itself
508 }
509 const std::size_t next {in.find_first_of(output, first)};
510 const std::size_t past {next == std::string::npos ? in.size() : next};
511 if (in.substr(0, first) + in.substr(past) != output) {
512 return std::nullopt;
513 }
514 std::vector<std::size_t> out_shape;
515 for (std::size_t k {0}; k < in.size(); ++k) {
516 if (k < first || k >= past) {
517 out_shape.push_back(shape[k]);
518 }
519 }
520 // The result's elements: none where a kept extent is 0, whatever the others (each part then 0); else their
521 // checked product, which bounds the parts, and the run's with them the operand's.
522 const std::size_t kept {element_count(out_shape.begin(), out_shape.end())};
523 if (kept == 0) {
524 return tensor<T> {std::move(out_shape), buffer<T> {}};
525 }
526 const auto begin {shape.begin()};
527 const std::size_t outer {element_count(begin, begin + static_cast<std::ptrdiff_t>(first))};
528 const std::size_t run {element_count(begin + static_cast<std::ptrdiff_t>(first), begin + static_cast<std::ptrdiff_t>(past))};
529 const std::size_t inner {element_count(begin + static_cast<std::ptrdiff_t>(past), shape.end())};
530 std::vector<T> sums(kept);
531 pairwise_sums(data, outer, run, inner, sums.data());
532 return tensor<T> {std::move(out_shape), std::move(sums)};
533 }
534
540 template <typename T>
542 const std::string& from,
543 const std::string& to,
544 const std::map<char, std::size_t>& sizes)
545 {
546 buffer<T> out(data.size()); // every element written below
547 if (data.empty()) {
548 return out;
549 }
550 if (to.size() == 2) { // the one permutation of two axes, a transpose: by tiles, each read and written in the cache
551 const std::size_t rows {sizes.at(from[0])};
552 const std::size_t cols {sizes.at(from[1])};
553 parallel_for((rows + transpose_tile - 1) / transpose_tile, transpose_tile * cols, [&](std::size_t block) {
554 const std::size_t r0 {block * transpose_tile};
555 const std::size_t r1 {std::min(r0 + transpose_tile, rows)};
556 for (std::size_t c0 {0}; c0 < cols; c0 += transpose_tile) {
557 const std::size_t c1 {std::min(c0 + transpose_tile, cols)};
558 for (std::size_t r {r0}; r < r1; ++r) {
559 for (std::size_t c {c0}; c < c1; ++c) {
560 out[(c * rows) + r] = data[(r * cols) + c];
561 }
562 }
563 }
564 });
565 return out;
566 }
567 const std::size_t rank {to.size()};
568 std::map<char, std::size_t> source; // each label's stride in data
569 std::size_t step {1};
570 for (std::size_t axis {from.size()}; axis-- > 0;) {
571 source[from[axis]] = step;
572 step *= sizes.at(from[axis]);
573 }
574 std::vector<std::size_t> stride(rank);
575 std::vector<std::size_t> extent(rank);
576 for (std::size_t axis {0}; axis < rank; ++axis) {
577 stride[axis] = source.at(to[axis]);
578 extent[axis] = sizes.at(to[axis]);
579 }
580 const std::size_t inner {extent[rank - 1]};
581 const std::size_t sweep {stride[rank - 1]};
582 std::vector<std::size_t> index(rank - 1, 0);
583 std::size_t offset {0};
584 for (std::size_t row {0}; row < data.size() / inner; ++row) {
585 for (std::size_t i {0}; i < inner; ++i) {
586 out[(row * inner) + i] = data[offset + (i * sweep)];
587 }
588 for (std::size_t axis {rank - 1}; axis-- > 0;) {
589 offset += stride[axis];
590 if (++index[axis] < extent[axis]) {
591 break;
592 }
593 offset -= stride[axis] * extent[axis];
594 index[axis] = 0;
595 }
596 }
597 return out;
598 }
599
609 template <typename T>
610 [[nodiscard]] std::optional<tensor<T>> contract_pair(const std::string& sa,
611 const tensor<T>& a,
612 const std::string& sb,
613 const tensor<T>& b,
614 const std::string& output,
615 const std::string& summed,
616 const std::map<char, std::size_t>& sizes)
617 {
618 const auto repeats {[](const std::string& labels) {
619 for (std::size_t i {0}; i < labels.size(); ++i) {
620 if (labels.find(labels[i], i + 1) != std::string::npos) {
621 return true;
622 }
623 }
624 return false;
625 }};
626 if (repeats(sa) || repeats(sb)) {
627 return std::nullopt;
628 }
629 std::string batch;
630 std::string free_a;
631 std::string free_b;
632 for (const char label : output) {
633 const bool in_a {sa.find(label) != std::string::npos};
634 const bool in_b {sb.find(label) != std::string::npos};
635 (in_a && in_b ? batch : (in_a ? free_a : free_b)).push_back(label);
636 }
637 if (batch + free_a + free_b != output && batch + free_b + free_a == output) {
638 // The other operand first lays the result out in the output's order: the same terms in the same order
639 // (each a product of the same two numbers), with no permutation of the result.
640 return contract_pair(sb, b, sa, a, output, summed, sizes);
641 }
642 std::size_t inner {1};
643 for (const char label : summed) {
644 if (sa.find(label) == std::string::npos || sb.find(label) == std::string::npos) {
645 return std::nullopt;
646 }
647 inner *= sizes.at(label);
648 }
649 const auto extent_of {[&sizes](const std::string& labels) {
650 std::size_t product {1};
651 for (const char label : labels) {
653 }
654 return product;
655 }};
656 const std::size_t batches {extent_of(batch)};
657 const std::size_t rows {extent_of(free_a)};
658 const std::size_t cols {extent_of(free_b)};
659 const std::string left_order {batch + free_a + summed};
660 const std::string right_order {batch + summed + free_b};
661 const buffer<T> left {sa == left_order ? buffer<T> {} : permuted(a.data(), sa, left_order, sizes)};
662 const buffer<T> right {sb == right_order ? buffer<T> {} : permuted(b.data(), sb, right_order, sizes)};
663 const T* const left_data {sa == left_order ? a.data().data() : left.data()};
664 const T* const right_data {sb == right_order ? b.data().data() : right.data()};
665 buffer<T> c(checked_mul(batches, checked_mul(rows, cols)));
666 if (rows == 1 && cols == 1) { // a dot product per batch, as `chunked_dot` sums each chunk
667 parallel_for(batches, inner, [&](std::size_t p) {
668 c[p] = dot_product(left_data + (p * inner), right_data + (p * inner), inner);
669 });
670 }
671 else {
672 parallel_zero(c.data(), c.size()); // gemm adds to C
673 parallel_for(batches, rows * cols * inner / gemm_kernel_speedup, [&](std::size_t p) {
674 gemm(rows, cols, inner, left_data + (p * rows * inner), inner, right_data + (p * inner * cols), cols,
675 c.data() + (p * rows * cols), cols);
676 });
677 }
678 std::vector<std::size_t> shape;
679 for (const char label : output) {
680 shape.push_back(sizes.at(label));
681 }
682 const std::string order {batch + free_a + free_b};
683 return tensor<T> {std::move(shape), order == output ? std::move(c) : permuted(c, order, output, sizes)};
684 }
685
688 inline constexpr std::size_t exhaustive_path_operands {4};
689
693 {
694 std::size_t first;
695 std::size_t second;
696 };
697
700 [[nodiscard]] inline std::string pair_keeps(const std::vector<std::string>& labels,
701 std::size_t i,
702 std::size_t j,
703 const std::string& output)
704 {
705 std::string elsewhere {output};
706 for (std::size_t k {0}; k < labels.size(); ++k) {
707 elsewhere += k == i || k == j ? std::string {} : labels[k];
708 }
709 std::string keep;
710 for (const char label : labels[i] + labels[j]) {
711 if (elsewhere.find(label) != std::string::npos && keep.find(label) == std::string::npos) {
712 keep.push_back(label);
713 }
714 }
715 return keep;
716 }
717
719 [[nodiscard]] inline double extent_product(const std::string& labels,
720 const std::map<char, std::size_t>& sizes)
721 {
722 double product {1.0};
723 std::string seen;
724 for (const char label : labels) {
725 if (seen.find(label) == std::string::npos) {
726 seen.push_back(label);
727 product *= static_cast<double>(sizes.at(label));
728 }
729 }
730 return product;
731 }
732
734 [[nodiscard]] inline std::vector<std::string> after_step(const std::vector<std::string>& labels,
736 const std::string& output)
737 {
738 std::vector<std::string> next;
739 for (std::size_t k {0}; k < labels.size(); ++k) {
740 if (k != step.first && k != step.second) {
741 next.push_back(labels[k]);
742 }
743 }
744 next.push_back(pair_keeps(labels, step.first, step.second, output));
745 return next;
746 }
747
750 inline void cheapest_path(const std::vector<std::string>& labels,
751 const std::string& output,
752 const std::map<char, std::size_t>& sizes,
753 std::vector<contraction_step>& path,
754 double& cost,
755 double& largest)
756 {
757 if (labels.size() < 2) {
758 path.clear();
759 cost = 0.0;
760 largest = 0.0;
761 return;
762 }
763 bool found {false};
764 for (std::size_t i {0}; i < labels.size(); ++i) {
765 for (std::size_t j {i + 1}; j < labels.size(); ++j) {
766 const contraction_step step {.first = i, .second = j};
767 std::vector<contraction_step> rest;
768 double rest_cost {0.0};
769 double rest_largest {0.0};
771 const double here {extent_product(labels[i] + labels[j], sizes)};
772 const double size {extent_product(pair_keeps(labels, i, j, output), sizes)};
773 const double total {here + rest_cost};
774 const double peak {std::max(size, rest_largest)};
775 if (!found || total < cost || (total == cost && peak < largest)) {
776 found = true;
777 cost = total;
778 largest = peak;
779 path.assign(1, step);
780 path.insert(path.end(), rest.begin(), rest.end());
781 }
782 }
783 }
784 }
785
794 [[nodiscard]] inline std::vector<contraction_step> contraction_path(std::vector<std::string> labels,
795 const std::string& output,
796 const std::map<char, std::size_t>& sizes)
797 {
798 std::vector<contraction_step> path;
799 if (labels.size() <= exhaustive_path_operands) {
800 double cost {0.0};
801 double largest {0.0};
803 return path;
804 }
805 const auto shares {[&labels](std::size_t i, std::size_t j) {
806 return labels[i].find_first_of(labels[j]) != std::string::npos;
807 }};
808 while (labels.size() > 1) {
809 contraction_step best {.first = 0, .second = 1};
810 double best_cost {-1.0};
811 double best_size {0.0};
812 bool connected {false}; // a pair sharing a label exists: an outer product waits
813 for (std::size_t i {0}; i < labels.size(); ++i) {
814 for (std::size_t j {i + 1}; j < labels.size(); ++j) {
815 connected = connected || shares(i, j);
816 }
817 }
818 for (std::size_t i {0}; i < labels.size(); ++i) {
819 for (std::size_t j {i + 1}; j < labels.size(); ++j) {
820 if (connected && !shares(i, j)) {
821 continue;
822 }
823 const double here {extent_product(labels[i] + labels[j], sizes)};
824 const double size {extent_product(pair_keeps(labels, i, j, output), sizes)};
825 if (best_cost < 0.0 || here < best_cost || (here == best_cost && size < best_size)) {
826 best = {.first = i, .second = j};
827 best_cost = here;
828 best_size = size;
829 }
830 }
831 }
832 path.push_back(best);
834 }
835 return path;
836 }
837
851 [[nodiscard]] inline std::size_t ellipsis_in(const std::string& subscripts)
852 {
853 const std::size_t at {subscripts.find("...")};
854 const auto dots {static_cast<std::size_t>(std::count(subscripts.begin(), subscripts.end(), '.'))};
855 if (dots != (at == std::string::npos ? 0U : 3U)) {
856 throw einsum_error("einsum: '.' stands only as one ellipsis, '...', per subscript");
857 }
858 return at;
859 }
860
861 template <typename T>
862 [[nodiscard]] tensor<T> einsum_impl(const std::string& spec_in,
863 const std::vector<const tensor<T>*>& operands);
864
873 template <typename T>
874 [[nodiscard]] tensor<T> einsum_ellipsis(const std::string& spec,
875 const std::vector<const tensor<T>*>& operands)
876 {
877 const std::size_t arrow {spec.find("->")};
878 std::vector<std::string> inputs {""};
879 for (const char ch : spec.substr(0, arrow)) {
880 if (ch == ',') {
881 inputs.emplace_back();
882 }
883 else {
884 inputs.back().push_back(ch);
885 }
886 }
887 if (inputs.size() != operands.size()) {
888 throw einsum_error("einsum: number of operands does not match the spec");
889 }
890 std::vector<std::size_t> at(inputs.size());
891 std::vector<std::size_t> under(inputs.size(), 0); // the axes each operand's ellipsis stands for
892 std::size_t widest {0};
893 for (std::size_t i {0}; i < inputs.size(); ++i) {
894 at[i] = ellipsis_in(inputs[i]);
895 if (at[i] != std::string::npos) {
896 const std::size_t letters {inputs[i].size() - 3};
897 if (operands[i]->rank() < letters) {
898 throw einsum_error("einsum: operand rank does not match its subscripts");
899 }
900 under[i] = operands[i]->rank() - letters;
901 widest = std::max(widest, under[i]);
902 }
903 }
904 static constexpr std::string_view alphabet {"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"};
905 std::string fresh;
906 for (const char letter : alphabet) {
907 if (fresh.size() < widest && spec.find(letter) == std::string::npos) {
908 fresh.push_back(letter);
909 }
910 }
911 if (fresh.size() < widest) {
912 throw einsum_error("einsum: too many axes under the ellipsis for the letters left");
913 }
914 std::vector<std::size_t> extent(widest, 1);
915 for (std::size_t i {0}; i < inputs.size(); ++i) {
916 for (std::size_t d {0}; d < under[i]; ++d) {
917 const std::size_t dim {operands[i]->shape()[at[i] + d]};
918 std::size_t& broadcast {extent[widest - under[i] + d]};
919 if (dim != 1 && broadcast != 1 && dim != broadcast) {
920 throw dimension_error("einsum: the axes under the ellipsis do not broadcast");
921 }
922 broadcast = dim == 1 ? broadcast : dim;
923 }
924 }
925 std::deque<tensor<T>> owned; // the operands with axes dropped (a deque keeps their addresses)
926 std::vector<const tensor<T>*> next;
927 std::string rewritten;
928 for (std::size_t i {0}; i < inputs.size(); ++i) {
929 rewritten += i == 0 ? "" : ",";
930 if (at[i] == std::string::npos) {
931 rewritten += inputs[i];
932 next.push_back(operands[i]);
933 continue;
934 }
935 std::string labels {inputs[i].substr(0, at[i])};
936 std::vector<std::size_t> shape(operands[i]->shape().begin(), operands[i]->shape().begin() + static_cast<std::ptrdiff_t>(at[i]));
937 for (std::size_t d {0}; d < under[i]; ++d) {
938 const std::size_t axis {widest - under[i] + d};
939 const std::size_t dim {operands[i]->shape()[at[i] + d]};
940 if (dim != extent[axis]) { // 1 where another operand's is longer: repeated, its letter dropped
941 continue;
942 }
943 labels.push_back(fresh[axis]);
944 shape.push_back(dim);
945 }
946 labels += inputs[i].substr(at[i] + 3);
947 shape.insert(shape.end(), operands[i]->shape().begin() + static_cast<std::ptrdiff_t>(at[i] + under[i]), operands[i]->shape().end());
948 rewritten += labels;
949 if (shape.size() == operands[i]->rank()) {
950 next.push_back(operands[i]);
951 }
952 else {
953 owned.emplace_back(std::move(shape), buffer<T>(operands[i]->data().begin(), operands[i]->data().end()));
954 next.push_back(&owned.back());
955 }
956 }
957 std::string output;
958 if (arrow != std::string::npos) {
959 output = spec.substr(arrow + 2);
960 const std::size_t where {ellipsis_in(output)};
961 if (where == std::string::npos && widest > 0) {
962 throw einsum_error("einsum: the output keeps the axes under the ellipsis: '...' in it, as NumPy requires");
963 }
964 if (where != std::string::npos) {
965 output.replace(where, 3, fresh);
966 }
967 }
968 else { // implicit: the broadcast axes, then the letters that appear once, in order
969 std::map<char, std::size_t> seen;
970 for (const char label : spec) {
971 if (is_label(label)) {
972 ++seen[label];
973 }
974 }
975 output = fresh;
976 for (const auto& [label, count] : seen) {
977 if (count == 1) {
978 output.push_back(label);
979 }
980 }
981 }
982 return einsum_impl<T>(rewritten + "->" + output, next);
983 }
984
985 template <typename T>
986 [[nodiscard]] tensor<T> einsum_impl(const std::string& spec_in,
987 const std::vector<const tensor<T>*>& operands)
988 {
989 // Tolerate whitespace anywhere in the spec, the way NumPy does ("ij, jk -> ik").
990 std::string spec {spec_in};
991 spec.erase(std::remove_if(spec.begin(), spec.end(),
992 [](unsigned char ch) { return std::isspace(ch) != 0; }),
993 spec.end());
994 if (spec.find('.') != std::string::npos) {
996 }
997 if (spec.find("->") == std::string::npos) {
998 // Implicit mode, as NumPy reads it: the output is every label that appears once, in ASCII order (`"ij,jk"`
999 // is `"ij,jk->ik"`, `"ii"` the trace, `"ba"` the transpose).
1000 std::map<char, std::size_t> seen;
1001 for (const char label : spec) {
1002 if (label != ',') {
1003 ++seen[label];
1004 }
1005 }
1006 spec += "->";
1007 for (const auto& [label, count] : seen) {
1008 if (count == 1) {
1009 spec += label;
1010 }
1011 }
1012 }
1013 const std::size_t arrow {spec.find("->")};
1014 const std::vector<std::string> inputs {split_inputs(spec.substr(0, arrow))};
1015 const std::string rhs {spec.substr(arrow + 2)};
1016
1017 if (operands.size() != inputs.size()) {
1018 throw einsum_error("einsum: number of operands does not match the spec");
1019 }
1020
1021 // Each label's extent, checked for consistency across every axis it labels.
1022 std::map<char, std::size_t> sizes;
1023 for (std::size_t i {0}; i < operands.size(); ++i) {
1024 if (inputs[i].size() != operands[i]->rank()) {
1025 throw einsum_error("einsum: operand rank does not match its subscripts");
1026 }
1027 for (std::size_t axis {0}; axis < inputs[i].size(); ++axis) {
1028 const char label {inputs[i][axis]};
1029 const std::size_t dim {operands[i]->shape()[axis]};
1030 const auto found {sizes.find(label)};
1031 if (found == sizes.end()) {
1032 sizes.emplace(label, dim);
1033 }
1034 else if (found->second != dim) {
1035 throw dimension_error("einsum: inconsistent extent for a repeated label");
1036 }
1037 }
1038 }
1039
1040 // Output (free) labels: each must be present in the inputs and appear once.
1041 std::string output;
1042 for (const char ch : rhs) {
1043 if (!is_label(ch)) {
1044 throw einsum_error("einsum: invalid character in output subscripts");
1045 }
1046 if (sizes.find(ch) == sizes.end()) {
1047 throw einsum_error("einsum: output label not present in any input");
1048 }
1049 if (output.find(ch) != std::string::npos) {
1050 throw einsum_error("einsum: repeated output label");
1051 }
1052 output.push_back(ch);
1053 }
1054
1055 // Summed labels: those that do not survive into the output (sorted, by map).
1056 std::string summed;
1057 for (const auto& entry : sizes) {
1058 if (output.find(entry.first) == std::string::npos) {
1059 summed.push_back(entry.first);
1060 }
1061 }
1062
1063 // Three operands or more: first sum each one over the labels no other operand and not the output
1064 // carries, then contract them two at a time in the cheapest order (\ref contraction_path), each
1065 // intermediate keeping only the labels still to come. One loop over every label of `ij,jk,kl->il` runs n⁴
1066 // terms; pairs run 2·n³, and `i,j,k,l,m->` over 65 536 ones each runs five sums of 65 536 instead of 2⁸⁰.
1067 if (operands.size() > 2) {
1068 std::deque<tensor<T>> owned; // the reductions and intermediates (a deque keeps their addresses)
1069 std::vector<const tensor<T>*> current;
1070 std::vector<std::string> kept;
1071 for (std::size_t i {0}; i < operands.size(); ++i) {
1072 std::string elsewhere {output};
1073 for (std::size_t j {0}; j < operands.size(); ++j) {
1074 elsewhere += j == i ? std::string {} : inputs[j];
1075 }
1076 std::string keep;
1077 for (const char label : inputs[i]) {
1078 if (elsewhere.find(label) != std::string::npos && keep.find(label) == std::string::npos) {
1079 keep.push_back(label);
1080 }
1081 }
1082 if (keep == inputs[i]) { // nothing of its own to sum: the operand itself
1083 current.push_back(operands[i]);
1084 }
1085 else {
1086 const std::vector<const tensor<T>*> alone {operands[i]};
1087 owned.push_back(einsum_impl<T>(inputs[i] + "->" + keep, alone));
1088 current.push_back(&owned.back());
1089 }
1090 kept.push_back(std::move(keep));
1091 }
1092 for (const contraction_step step : contraction_path(kept, output, sizes)) {
1093 const std::string keep {kept.size() == 2 ? output : pair_keeps(kept, step.first, step.second, output)};
1094 const std::vector<const tensor<T>*> pair {current[step.first], current[step.second]};
1095 owned.push_back(einsum_impl<T>(kept[step.first] + "," + kept[step.second] + "->" + keep, pair));
1096 current.erase(current.begin() + static_cast<std::ptrdiff_t>(step.second));
1097 current.erase(current.begin() + static_cast<std::ptrdiff_t>(step.first));
1098 kept.erase(kept.begin() + static_cast<std::ptrdiff_t>(step.second));
1099 kept.erase(kept.begin() + static_cast<std::ptrdiff_t>(step.first));
1100 current.push_back(&owned.back());
1101 kept.push_back(keep);
1102 }
1103 return std::move(owned.back()); // the last contraction's, the output
1104 }
1105
1106 // Fast path: one operand, nothing summed, each label once: a permutation of its axes (a transpose), a copy.
1107 if (operands.size() == 1 && summed.empty() && inputs[0].size() == output.size()) {
1108 std::vector<std::size_t> out_shape;
1109 for (const char label : output) {
1110 out_shape.push_back(sizes.at(label));
1111 }
1112 return tensor<T> {std::move(out_shape), inputs[0] == output ? operands[0]->data() : permuted(operands[0]->data(), inputs[0], output, sizes)};
1113 }
1114
1115 // Fast path: one operand, each label once, its summed labels one run of its axes and the output the others in
1116 // order (ij->i, ij->, ij->j, ijk->ik): sum_over_run.
1117 if (operands.size() == 1 && sizes.size() == inputs[0].size()) {
1118 if (std::optional<tensor<T>> sums {sum_over_run(inputs[0], output, operands[0]->shape(), operands[0]->data().data())}) {
1119 return *std::move(sums);
1120 }
1121 }
1122
1123 // Fast path: a plain two-operand single contraction in canonical layout —
1124 // `A` ending with the contracted label, `B` starting with it, and the
1125 // output being A's leading free labels then B's trailing ones — is exactly
1126 // a matrix product, routed to the packed kernel (gemm.hpp). (Any stray copy of
1127 // the contracted label leaves it in a "free" part, so the output would
1128 // carry a summed label and this equality fails — correctly declining it.)
1129 if (operands.size() == 2 && summed.size() == 1) {
1130 const char contracted {summed.front()};
1131 const std::string& s0 {inputs[0]};
1132 const std::string& s1 {inputs[1]};
1133 if (!s0.empty() && !s1.empty() && s0.back() == contracted && s1.front() == contracted
1134 && output == s0.substr(0, s0.size() - 1) + s1.substr(1)) {
1135 const std::size_t inner {sizes.at(contracted)};
1136 std::vector<std::size_t> out_shape;
1137 for (std::size_t k {0}; k + 1 < s0.size(); ++k) {
1138 out_shape.push_back(sizes.at(s0[k]));
1139 }
1140 for (std::size_t k {1}; k < s1.size(); ++k) {
1141 out_shape.push_back(sizes.at(s1[k]));
1142 }
1143 // The result's elements, 0 where one of its extents is 0 whatever the others (an empty contraction's
1144 // operands may hold any number along their other axes); else checked, and the rows and columns within.
1145 if (element_count(out_shape.begin(), out_shape.end()) == 0) {
1146 return tensor<T> {std::move(out_shape), buffer<T> {}};
1147 }
1148 const auto split {out_shape.begin() + static_cast<std::ptrdiff_t>(s0.size() - 1)};
1149 const std::size_t rows {element_count(out_shape.begin(), split)};
1150 const std::size_t cols {element_count(split, out_shape.end())};
1151 return canonical_product<T>(operands[0]->data().data(), operands[1]->data().data(), rows, inner, cols, std::move(out_shape));
1152 }
1153 }
1154 if (operands.size() == 2) {
1155 if (std::optional<tensor<T>> paired {contract_pair<T>(inputs[0], *operands[0], inputs[1], *operands[1], output, summed, sizes)}) {
1156 return *std::move(paired);
1157 }
1158 }
1159
1160 // Every distinct label, output (free) first then summed: this fixes the
1161 // index space and the row-major order of the result.
1162 const std::string labels {output + summed};
1163 const std::size_t n_labels {labels.size()};
1164 const std::size_t n_free {output.size()};
1165 const std::size_t n_ops {operands.size()};
1166
1167 std::vector<std::size_t> extent(n_labels);
1168 for (std::size_t k {0}; k < n_labels; ++k) {
1169 extent[k] = sizes.at(labels[k]);
1170 }
1171
1172 // coef[i][k]: operand i's flat-offset change per unit of label k — the sum
1173 // of its row-major strides over the axes carrying that label (so a label
1174 // repeated within one operand, e.g. "ii", walks that operand's diagonal).
1175 std::vector<std::vector<std::size_t>> coef(n_ops, std::vector<std::size_t>(n_labels, 0));
1176 for (std::size_t i {0}; i < n_ops; ++i) {
1177 const std::vector<std::size_t>& shape {operands[i]->shape()};
1178 std::size_t stride {1};
1179 for (std::size_t axis {inputs[i].size()}; axis-- > 0;) {
1180 coef[i][labels.find(inputs[i][axis])] += stride;
1181 stride *= shape[axis];
1182 }
1183 }
1184
1185 // Fast path: peel the innermost label of each group — the last free label
1186 // and the last summed label — into a tight unit-of-stride inner loop, and
1187 // run the odometer only over the *remaining* (outer) labels. The innermost
1188 // axis is then a contiguous sweep amortising one odometer step over its
1189 // whole extent: a strided write for a transpose/broadcast, a strided
1190 // multiply-accumulate for a contraction — both auto-vectoriser-friendly.
1191 const bool has_free_inner {n_free > 0};
1192 const bool has_sum_inner {n_labels > n_free};
1193 const std::size_t inner_free {has_free_inner ? extent[n_free - 1] : 1};
1194 const std::size_t inner_sum {has_sum_inner ? extent[n_labels - 1] : 1};
1195 const std::size_t free_outer_end {has_free_inner ? n_free - 1 : 0};
1196 const std::size_t sum_outer_end {has_sum_inner ? n_labels - 1 : n_free};
1197
1198 // Per-operand stride of each peeled axis (0 when that axis is absent).
1199 std::vector<std::size_t> free_step(n_ops, 0);
1200 std::vector<std::size_t> sum_step(n_ops, 0);
1201 for (std::size_t i {0}; i < n_ops; ++i) {
1202 if (has_free_inner) {
1203 free_step[i] = coef[i][n_free - 1];
1204 }
1205 if (has_sum_inner) {
1206 sum_step[i] = coef[i][n_labels - 1];
1207 }
1208 }
1209
1210 std::size_t out_size {1};
1211 for (std::size_t k {0}; k < n_free; ++k) {
1212 out_size = detail::checked_mul(out_size, extent[k]); // guard the result allocation size
1213 }
1214 std::vector<std::size_t> out_shape(extent.begin(),
1215 extent.begin() + static_cast<std::ptrdiff_t>(n_free));
1216 if (out_size == 0) { // an empty result computes nothing, whatever its other extents multiply to
1217 return tensor<T> {std::move(out_shape), buffer<T> {}};
1218 }
1219 std::size_t out_outer {1}; // bounded by out_size
1220 for (std::size_t k {0}; k < free_outer_end; ++k) {
1221 out_outer *= extent[k];
1222 }
1223 std::size_t sum_outer {1};
1224 for (std::size_t k {n_free}; k < sum_outer_end; ++k) {
1225 sum_outer = detail::checked_mul(sum_outer, extent[k]); // a wrapped count would sum nothing, silently
1226 }
1227
1228 std::vector<std::size_t> index(n_labels, 0); // value of each outer label
1229 std::vector<std::size_t> offset(n_ops, 0); // each operand's flat offset (outer labels only)
1231 result.reserve(out_size);
1232
1233 for (std::size_t of {0}; of < out_outer; ++of) {
1234 for (std::size_t fe {0}; fe < inner_free; ++fe) {
1235 T acc {};
1236 for (std::size_t os {0}; os < sum_outer; ++os) {
1237 for (std::size_t se {0}; se < inner_sum; ++se) {
1238 // The first factor itself, then the others: a complex 1 times an element was no identity (0 · inf
1239 // is a NaN, and −0 − (−0) a +0), so a trace or a sum of complex infinities or signed zeros went wrong.
1240 T prod {operands[0]->data()[offset[0] + (fe * free_step[0]) + (se * sum_step[0])]};
1241 for (std::size_t i {1}; i < n_ops; ++i) {
1242 prod *= operands[i]->data()[offset[i] + (fe * free_step[i]) + (se * sum_step[i])];
1243 }
1244 acc += prod;
1245 }
1246 // Step the outer-summed odometer (labels [n_free, sum_outer_end)).
1247 for (std::size_t k {sum_outer_end}; k-- > n_free;) {
1248 if (++index[k] < extent[k]) {
1249 for (std::size_t i {0}; i < n_ops; ++i) {
1250 offset[i] += coef[i][k];
1251 }
1252 break;
1253 }
1254 index[k] = 0;
1255 for (std::size_t i {0}; i < n_ops; ++i) {
1256 offset[i] -= coef[i][k] * (extent[k] - 1);
1257 }
1258 }
1259 }
1260 result.push_back(acc);
1261 }
1262 // Step the outer-free odometer (labels [0, free_outer_end)).
1263 for (std::size_t k {free_outer_end}; k-- > 0;) {
1264 if (++index[k] < extent[k]) {
1265 for (std::size_t i {0}; i < n_ops; ++i) {
1266 offset[i] += coef[i][k];
1267 }
1268 break;
1269 }
1270 index[k] = 0;
1271 for (std::size_t i {0}; i < n_ops; ++i) {
1272 offset[i] -= coef[i][k] * (extent[k] - 1);
1273 }
1274 }
1275 }
1276 return tensor<T> {std::move(out_shape), std::move(result)};
1277 }
1278 } // namespace detail
1279
1296 template <typename T, typename ... Rest>
1297 [[nodiscard]] tensor<T> einsum(const std::string& spec,
1298 const tensor<T>& first,
1299 const Rest& ... rest)
1300 {
1301 const std::vector<const tensor<T>*> operands {&first, (&rest)...};
1303 }
1304
1314 template <typename T>
1315 [[nodiscard]] tensor<T> einsum(const std::string& spec,
1316 const std::vector<const tensor<T>*>& operands)
1317 {
1319 }
1320
1331 template <typename T>
1333 const tensor<T>& b)
1334 {
1335 const std::vector<std::size_t>& sa {a.shape()};
1336 const std::vector<std::size_t>& sb {b.shape()};
1337 if (sa.empty() || sb.empty()) {
1338 throw dimension_error("matmul: a rank-0 operand has no matrix to multiply");
1339 }
1340 const bool row {sa.size() == 1};
1341 const bool column {sb.size() == 1};
1342 const std::size_t m {row ? 1 : sa[sa.size() - 2]};
1343 const std::size_t k {sa.back()};
1344 const std::size_t n {column ? 1 : sb.back()};
1345 if ((column ? sb[0] : sb[sb.size() - 2]) != k) {
1346 throw dimension_error("matmul: the inner extents of the operands differ");
1347 }
1348 const std::size_t lead_a {sa.size() - (row ? 1 : 2)};
1349 const std::size_t lead_b {sb.size() - (column ? 1 : 2)};
1350 const std::size_t rank {std::max(lead_a, lead_b)};
1351 std::vector<std::size_t> batch(rank);
1352 std::vector<std::size_t> stride_a(rank, 0); // 0 along an axis the operand repeats
1353 std::vector<std::size_t> stride_b(rank, 0);
1354 std::size_t step_a {m * k};
1355 std::size_t step_b {k * n};
1356 for (std::size_t d {rank}; d-- > 0;) {
1357 const std::size_t da {d + lead_a >= rank ? sa[d + lead_a - rank] : 1};
1358 const std::size_t db {d + lead_b >= rank ? sb[d + lead_b - rank] : 1};
1359 if (da != db && da != 1 && db != 1) {
1360 throw dimension_error("matmul: the leading axes of the operands do not broadcast");
1361 }
1362 batch[d] = da == 1 ? db : da;
1363 stride_a[d] = da == 1 ? 0 : step_a;
1364 stride_b[d] = db == 1 ? 0 : step_b;
1367 }
1368 std::size_t count {1};
1369 for (const std::size_t extent : batch) {
1370 count = detail::checked_mul(count, extent);
1371 }
1372 std::vector<std::size_t> shape {batch};
1373 if (!row) {
1374 shape.push_back(m);
1375 }
1376 if (!column) {
1377 shape.push_back(n);
1378 }
1379 const std::array<std::size_t, 3> elements {count, m, n};
1380 buffer<T> c(detail::element_count(elements.begin(), elements.end()));
1381 detail::parallel_zero(c.data(), c.size()); // gemm adds to C
1382 if (rank == 0) { // no stack: one product
1383 detail::gemm(m, n, k, a.data().data(), k, b.data().data(), n, c.data(), n);
1384 return tensor<T> {std::move(shape), std::move(c)};
1385 }
1386 const auto product {[&](std::size_t p) {
1387 std::size_t rest {p};
1388 std::size_t offset_a {0};
1389 std::size_t offset_b {0};
1390 for (std::size_t d {batch.size()}; d-- > 0;) {
1391 const std::size_t index {rest % batch[d]};
1392 rest /= batch[d];
1393 offset_a += index * stride_a[d];
1394 offset_b += index * stride_b[d];
1395 }
1396 detail::gemm(m, n, k, a.data().data() + offset_a, k, b.data().data() + offset_b, n, c.data() + (p * m * n), n);
1397 }};
1398 detail::parallel_for(count, m * n * k / detail::gemm_kernel_speedup, product); // the stack's products in parallel
1399 return tensor<T> {std::move(shape), std::move(c)};
1400 }
1401} // namespace numerical
1402
1403#endif // NUMERICAL_TENSOR_HPP
Thrown when an operation's operands have incompatible shapes.
Definition vector.hpp:35
Thrown when an einsum specification is malformed or inconsistent.
Definition tensor.hpp:48
einsum_error(const std::string &message)
Builds the error.
Definition tensor.hpp:52
A dense, row-major matrix of T with value semantics.
Definition matrix.hpp:35
A dense, row-major, N-dimensional tensor of T with value semantics.
Definition tensor.hpp:63
const std::vector< std::size_t > & shape() const noexcept
The size of each axis.
Definition tensor.hpp:136
std::vector< std::size_t > strides_
Row-major strides.
Definition tensor.hpp:180
tensor(std::vector< std::size_t > shape, std::initializer_list< T > elements)
From a shape and a braced list of the elements, row-major.
Definition tensor.hpp:92
const T & operator()(const std::vector< std::size_t > &index) const
The element at a multi-index.
Definition tensor.hpp:148
tensor(std::vector< std::size_t > shape, buffer< T > elements)
From a shape and a flat row-major buffer.
Definition tensor.hpp:72
tensor(const matrix< T > &m)
A rank-2 tensor from a matrix.
Definition tensor.hpp:117
void finalize()
Computes row-major strides and checks the buffer against the shape.
Definition tensor.hpp:166
bool operator==(const tensor &other) const
Exact equality (same shape and equal elements).
Definition tensor.hpp:158
buffer< T > data_
The elements, row-major.
Definition tensor.hpp:181
std::size_t rank() const noexcept
The number of axes (0 for a scalar).
Definition tensor.hpp:124
tensor(matrix< T > &&m)
A rank-2 tensor taking over an expiring matrix's elements, without a copy.
Definition tensor.hpp:110
tensor(const vector< T > &v)
A rank-1 tensor from a vector.
Definition tensor.hpp:99
std::size_t size() const noexcept
The total number of elements.
Definition tensor.hpp:130
tensor(std::vector< std::size_t > shape, const std::vector< T > &elements)
From a shape and a flat row-major std::vector, copied into the storage (a buffer moves in instead).
Definition tensor.hpp:85
const buffer< T > & data() const noexcept
The elements, row-major.
Definition tensor.hpp:142
std::vector< std::size_t > shape_
The size of each axis.
Definition tensor.hpp:179
A dense vector of T with value semantics.
Definition vector.hpp:86
std::size_t size() const noexcept
The number of elements.
Definition vector.hpp:109
The matrix product kernel behind matrix * matrix and the einsum matmul path.
A dense, row-major, value-semantic matrix over a scalar type.
constexpr std::size_t pairwise_columns
The columns pairwise_sum_rows sums at once: its partial sums of eight rows, 32 KiB of doubles,...
Definition tensor.hpp:314
void pairwise_sum_rows(const T *first, std::size_t count, std::size_t stride, std::size_t width, T *out)
pairwise_sum of width neighbouring columns at once, into out: column q's sum that of pairwise_sum(fir...
Definition tensor.hpp:257
buffer< T > vector_times_transposed(const T *a, const T *bt, std::size_t ldbt, std::size_t inner, std::size_t cols)
canonical_product's vector a (inner) times a matrix given as its transpose bt (cols × inner,...
Definition tensor.hpp:439
T pairwise_sum(const T *first, std::size_t count, std::size_t stride)
Sum of count values from first at stride stride, by NumPy's pairwise summation: below 8 values in ord...
Definition tensor.hpp:216
bool is_label(char ch)
Is ch an ASCII letter (a valid index label)?
Definition tensor.hpp:341
void parallel_zero(T *out, std::size_t count)
Sets the count elements at out to zero, split across the threads the work pays for: a fresh result's ...
Definition parallel.hpp:682
tensor< T > einsum_impl(const std::string &spec_in, const std::vector< const tensor< T > * > &operands)
Definition tensor.hpp:986
T chunked_dot(const T *a, const T *b, std::size_t count)
Σ a[i] · b[i] for a plain dot product: chunks of dot_chunk elements in parallel, each by dot_product ...
Definition gemm.hpp:3142
constexpr std::size_t transpose_tile
The side of the tiles a transposing copy goes by (an einsum permutation of two axes,...
Definition simd.hpp:31
void add_one_product(T &y, const T &f, const T &x)
y += f · x of one element, as add_product computes each of its elements.
Definition simd.hpp:971
constexpr std::size_t gemm_kernel_speedup
The multiply-adds the register tile does in the time of one in work_per_thread's dependent chain (abo...
Definition gemm.hpp:497
std::vector< std::string > after_step(const std::vector< std::string > &labels, contraction_step step, const std::string &output)
The list after a contraction step: the two operands' labels removed, their result's appended.
Definition tensor.hpp:734
void add_product(double *y, const double &f, const double *x, std::size_t count)
y[i] += f · x[i] for double, as subtract_product.
Definition gemm.hpp:308
buffer< T > permuted(const buffer< T > &data, const std::string &from, const std::string &to, const std::map< char, std::size_t > &sizes)
The elements of data, whose axes carry the labels from (extents in sizes), laid out row-major along t...
Definition tensor.hpp:541
void cheapest_path(const std::vector< std::string > &labels, const std::string &output, const std::map< char, std::size_t > &sizes, std::vector< contraction_step > &path, double &cost, double &largest)
The cheapest order of contraction from labels, and its cost and largest intermediate: every order tri...
Definition tensor.hpp:750
tensor< T > tile_gemm(const T *a, const T *b, std::size_t rows, std::size_t inner, std::size_t cols, std::vector< std::size_t > out_shape)
Matrix product C(M×N) = A(M×K) · B(K×N), row-major.
Definition tensor.hpp:375
std::vector< std::string > split_inputs(const std::string &lhs)
Splits an einsum left-hand side into per-operand subscripts.
Definition tensor.hpp:347
std::string pair_keeps(const std::vector< std::string > &labels, std::size_t i, std::size_t j, const std::string &output)
The labels of labels[i] and labels[j] that the output or another operand carries, in their order of a...
Definition tensor.hpp:700
tensor< T > canonical_product(const T *a, const T *b, std::size_t rows, std::size_t inner, std::size_t cols, std::vector< std::size_t > out_shape)
The product of the row-major rows × inner a and inner × cols b, shaped out_shape: the canonical two-o...
Definition tensor.hpp:399
std::size_t ellipsis_in(const std::string &subscripts)
Evaluates a parsed einsum over its operands.
Definition tensor.hpp:851
void gemm(std::size_t m, std::size_t n, std::size_t k, const T *a, std::size_t lda, const T *b, std::size_t ldb, T *c, std::size_t ldc)
C += A·B, every element of C summed in ascending order of the depth.
Definition gemm.hpp:2740
void parallel_for(std::size_t count, std::size_t work_per_index, const Body &body)
Runs body(i) for every i in [0, count).
Definition parallel.hpp:580
gemm_path gemm_path_current() noexcept
The path the next double product takes: the most capable one under the cap that runs here (each path'...
Definition gemm.hpp:145
double dot_product(const double *x, const double *y, std::size_t count)
Σ x[i] · y[i] for double, as subtract_product chooses its units (in another order of summation there,...
Definition gemm.hpp:369
constexpr std::size_t transposed_columns
The columns of B one task of vector_times_transposed sums together: their sums' chains interleaved,...
Definition tensor.hpp:430
std::size_t checked_mul(std::size_t a, std::size_t b)
a * b, throwing numerical::dimension_error if the product overflows std::size_t. Guards dimension/sha...
Definition vector.hpp:50
std::vector< contraction_step > contraction_path(std::vector< std::string > labels, const std::string &output, const std::map< char, std::size_t > &sizes)
The order in which to contract operands labelled labels into output, as opt_einsum chooses it: the ch...
Definition tensor.hpp:794
tensor< T > einsum_ellipsis(const std::string &spec, const std::vector< const tensor< T > * > &operands)
einsum_impl of a spec with ellipses, as NumPy reads them: each ... stands for an operand's axes its l...
Definition tensor.hpp:874
void pairwise_sums(const T *data, std::size_t outer, std::size_t count, std::size_t inner, T *out)
The pairwise sums of outer blocks of count rows of inner elements (row-major), each column of a block...
Definition tensor.hpp:322
std::size_t element_count(It first, It last)
The elements a shape's extents [first, last) hold: 0 where one of them is 0, whatever the others,...
Definition vector.hpp:66
constexpr std::size_t vector_chunk
The columns a thread takes of a vector times a matrix: each of its rows added to them in order.
Definition tensor.hpp:391
std::optional< tensor< T > > sum_over_run(const std::string &in, const std::string &output, const std::vector< std::size_t > &shape, const T *data)
einsum of one row-major operand data (shape, its axes labelled in, each label once) into the labels o...
Definition tensor.hpp:500
std::optional< tensor< T > > contract_pair(const std::string &sa, const tensor< T > &a, const std::string &sb, const tensor< T > &b, const std::string &output, const std::string &summed, const std::map< char, std::size_t > &sizes)
A two-operand contraction as matrix products: the labels both operands and the output carry (batch),...
Definition tensor.hpp:610
constexpr std::size_t exhaustive_path_operands
The operands at most whose contraction order contraction_path searches exhaustively; more take the ch...
Definition tensor.hpp:688
void matrix_times_vector(const T *a, std::size_t rows, std::size_t cols, const T *x, T *y)
y = A x for the row-major rows × cols a: each element the dot product of its row with x,...
Definition gemm.hpp:3196
double extent_product(const std::string &labels, const std::map< char, std::size_t > &sizes)
The product of the extents of labels' distinct labels, as a double (a cost: it may pass size_t).
Definition tensor.hpp:719
The NumericAL public API.
Definition amx.hpp:44
std::vector< T, detail::default_init_allocator< T > > buffer
The storage of vector, matrix and tensor, namely a std::vector whose buffer<T>(n) and resize(n) leave...
Definition buffer.hpp:157
T sum(const tensor< T > &t)
The sum of every element, by pairwise halving (0 for an empty tensor).
gemm_path
The kernels a double matrix product can run on (a float one too on arm64), from the most portable....
Definition gemm.hpp:80
@ avx512
AVX-512 with fused multiply-adds.
@ avx2
AVX2 with fused multiply-adds.
@ amx
The Apple AMX coprocessor, fused multiply-adds; the products it does not suit on the NEON path's tile...
tensor< T > einsum(const std::string &spec, const tensor< T > &first, const Rest &... rest)
Einstein summation over one or more tensor operands.
Definition tensor.hpp:1297
bool approx_equal(const matrix< T > &a, const matrix< T > &b, double tolerance=1e-9)
Element-wise closeness within a tolerance (for inexact scalars).
Definition matrix.hpp:278
tensor< T > matmul(const tensor< T > &a, const tensor< T > &b)
The matrix product of two tensors, as NumPy's matmul: each operand a stack of matrices along its lead...
Definition tensor.hpp:1332
A minimal std::thread parallel-for for the hot kernels.
One step of a contraction path: the operands at first and second of the current list,...
Definition tensor.hpp:693
std::size_t second
The second's, past the first.
Definition tensor.hpp:695
std::size_t first
The first operand's position.
Definition tensor.hpp:694
A dense, value-semantic vector over a scalar type.