20#ifndef NUMERICAL_TENSOR_HPP
21#define NUMERICAL_TENSOR_HPP
30#include <initializer_list>
86 const std::vector<T>& elements)
88 buffer<
T>(elements.begin(), elements.end()))
93 std::initializer_list<T> elements)
103 for (std::size_t i {0}; i < v.
size(); ++i) {
104 data_.push_back(v[i]);
118 :
shape_ {m.rows(), m.cols()},
data_(m.begin(), m.end())
150 std::size_t
flat {0};
151 for (std::size_t
axis {0};
axis < index.size(); ++
axis) {
191 template <
typename T>
199 for (std::size_t i {0}; i < a.
size(); ++i) {
215 template <
typename T>
220 constexpr std::size_t
lanes {8};
221 constexpr std::size_t
block {128};
224 for (std::size_t i {0}; i < count; ++i) {
229 if (count <=
block) {
230 std::array<T, lanes>
partial {};
231 for (std::size_t j {0}; j <
lanes; ++j) {
234 std::size_t i {
lanes};
235 for (; i < count - (count %
lanes); i +=
lanes) {
236 for (std::size_t j {0}; j <
lanes; ++j) {
241 for (; i < count; ++i) {
246 std::size_t
half {count / 2};
256 template <
typename T>
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); };
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) {
275 if (count <=
block) {
277 for (std::size_t j {0}; j <
lanes; ++j) {
278 std::copy_n(
row(j), width,
partial.data() + (j * width));
280 std::size_t i {
lanes};
281 for (; i < count - (count %
lanes); i +=
lanes) {
282 for (std::size_t j {0}; j <
lanes; ++j) {
284 const T*
const next {
row(i + j)};
285 for (std::size_t q {0}; q < width; ++q) {
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]));
295 for (; i < count; ++i) {
296 for (std::size_t q {0}; q < width; ++q) {
302 std::size_t
half {count / 2};
304 std::vector<T> second(width);
307 for (std::size_t q {0}; q < width; ++q) {
308 out[q] =
out[q] + second[q];
321 template <
typename T>
328 for (std::size_t
o {0};
o < outer; ++
o) {
329 const T*
const base {data + (
o * count * inner)};
343 return std::isalpha(
static_cast<unsigned char>(
ch)) != 0;
349 std::vector<std::string>
groups;
351 for (
const char ch :
lhs) {
360 throw einsum_error(
"einsum: invalid character in input subscripts");
374 template <
typename T>
385 gemm(rows, cols, inner, a, inner, b, cols, c.data(), cols);
398 template <
typename T>
406 if (cols == 1 && rows > 1 && inner != 0) {
411 if (rows == 1 && cols > 1 && inner != 0) {
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);
422 if (rows * cols != 1) {
438 template <
typename T>
450 std::array<T, transposed_columns>
sums {};
451 for (std::size_t k {0}; k < inner; ++k) {
457 std::copy(
sums.begin(),
sums.end(),
y.data() + j);
459 for (; j <
last; ++j) {
461 for (std::size_t k {0}; k < inner; ++k) {
470#ifdef NUMERICAL_X86_COMPILED
499 template <
typename T>
501 const std::string&
output,
502 const std::vector<std::size_t>& shape,
505 const std::size_t first {
in.find_first_not_of(
output)};
506 if (first == std::string::npos) {
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};
515 for (std::size_t k {0}; k <
in.size(); ++k) {
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);
540 template <
typename T>
542 const std::string&
from,
543 const std::string&
to,
544 const std::map<char, std::size_t>&
sizes)
550 if (
to.size() == 2) {
551 const std::size_t rows {
sizes.at(
from[0])};
552 const std::size_t cols {
sizes.at(
from[1])};
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];
567 const std::size_t rank {
to.size()};
568 std::map<char, std::size_t>
source;
569 std::size_t
step {1};
574 std::vector<std::size_t>
stride(rank);
575 std::vector<std::size_t> extent(rank);
580 const std::size_t inner {extent[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)];
588 for (std::size_t
axis {rank - 1};
axis-- > 0;) {
609 template <
typename T>
612 const std::string&
sb,
614 const std::string&
output,
615 const std::string&
summed,
616 const std::map<char, std::size_t>&
sizes)
619 for (std::size_t i {0}; i <
labels.size(); ++i) {
620 if (
labels.find(
labels[i], i + 1) != std::string::npos) {
633 const bool in_a {
sa.find(
label) != std::string::npos};
634 const bool in_b {
sb.find(
label) != std::string::npos};
642 std::size_t inner {1};
644 if (
sa.find(
label) == std::string::npos ||
sb.find(
label) == std::string::npos) {
666 if (rows == 1 && cols == 1) {
675 c.data() + (p * rows * cols), cols);
678 std::vector<std::size_t> shape;
703 const std::string&
output)
706 for (std::size_t k {0}; k <
labels.size(); ++k) {
720 const std::map<char, std::size_t>&
sizes)
725 if (
seen.find(
label) == std::string::npos) {
736 const std::string&
output)
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]);
751 const std::string&
output,
752 const std::map<char, std::size_t>&
sizes,
753 std::vector<contraction_step>&
path,
764 for (std::size_t i {0}; i <
labels.size(); ++i) {
765 for (std::size_t j {i + 1}; j <
labels.size(); ++j) {
767 std::vector<contraction_step>
rest;
795 const std::string&
output,
796 const std::map<char, std::size_t>&
sizes)
798 std::vector<contraction_step>
path;
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;
808 while (
labels.size() > 1) {
813 for (std::size_t i {0}; i <
labels.size(); ++i) {
814 for (std::size_t j {i + 1}; j <
labels.size(); ++j) {
818 for (std::size_t i {0}; i <
labels.size(); ++i) {
819 for (std::size_t j {i + 1}; j <
labels.size(); ++j) {
826 best = {.first = i, .second = j};
853 const std::size_t at {
subscripts.find(
"...")};
855 if (
dots != (at == std::string::npos ? 0
U : 3U)) {
856 throw einsum_error(
"einsum: '.' stands only as one ellipsis, '...', per subscript");
861 template <
typename T>
873 template <
typename T>
877 const std::size_t
arrow {
spec.find(
"->")};
878 std::vector<std::string>
inputs {
""};
888 throw einsum_error(
"einsum: number of operands does not match the spec");
890 std::vector<std::size_t> at(
inputs.size());
893 for (std::size_t i {0}; i <
inputs.size(); ++i) {
895 if (at[i] != std::string::npos) {
898 throw einsum_error(
"einsum: operand rank does not match its subscripts");
904 static constexpr std::string_view
alphabet {
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"};
912 throw einsum_error(
"einsum: too many axes under the ellipsis for the letters left");
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]};
920 throw dimension_error(
"einsum: the axes under the ellipsis do not broadcast");
925 std::deque<tensor<T>>
owned;
926 std::vector<const tensor<T>*> next;
928 for (std::size_t i {0}; i <
inputs.size(); ++i) {
930 if (at[i] == std::string::npos) {
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) {
939 const std::size_t
dim {
operands[i]->shape()[at[i] + d]};
944 shape.push_back(
dim);
947 shape.insert(shape.end(),
operands[i]->shape().begin() +
static_cast<std::ptrdiff_t
>(at[i] +
under[i]),
operands[i]->shape().end());
949 if (shape.size() ==
operands[i]->rank()) {
954 next.push_back(&
owned.back());
958 if (
arrow != std::string::npos) {
962 throw einsum_error(
"einsum: the output keeps the axes under the ellipsis: '...' in it, as NumPy requires");
964 if (
where != std::string::npos) {
969 std::map<char, std::size_t>
seen;
976 for (
const auto& [
label, count] :
seen) {
985 template <
typename T>
992 [](
unsigned char ch) { return std::isspace(ch) != 0; }),
994 if (
spec.find(
'.') != std::string::npos) {
997 if (
spec.find(
"->") == std::string::npos) {
1000 std::map<char, std::size_t>
seen;
1007 for (
const auto& [
label, count] :
seen) {
1013 const std::size_t
arrow {
spec.find(
"->")};
1018 throw einsum_error(
"einsum: number of operands does not match the spec");
1022 std::map<char, std::size_t>
sizes;
1023 for (std::size_t i {0}; i <
operands.size(); ++i) {
1025 throw einsum_error(
"einsum: operand rank does not match its subscripts");
1035 throw dimension_error(
"einsum: inconsistent extent for a repeated label");
1042 for (
const char ch :
rhs) {
1044 throw einsum_error(
"einsum: invalid character in output subscripts");
1047 throw einsum_error(
"einsum: output label not present in any input");
1049 if (
output.find(
ch) != std::string::npos) {
1058 if (
output.find(
entry.first) == std::string::npos) {
1068 std::deque<tensor<T>>
owned;
1069 std::vector<const tensor<T>*>
current;
1070 std::vector<std::string> kept;
1071 for (std::size_t i {0}; i <
operands.size(); ++i) {
1073 for (std::size_t j {0}; j <
operands.size(); ++j) {
1090 kept.push_back(std::move(
keep));
1098 kept.erase(kept.begin() +
static_cast<std::ptrdiff_t
>(
step.second));
1099 kept.erase(kept.begin() +
static_cast<std::ptrdiff_t
>(
step.first));
1101 kept.push_back(
keep);
1103 return std::move(
owned.back());
1119 return *std::move(
sums);
1134 &&
output ==
s0.substr(0,
s0.size() - 1) +
s1.substr(1)) {
1137 for (std::size_t k {0}; k + 1 <
s0.size(); ++k) {
1140 for (std::size_t k {1}; k <
s1.size(); ++k) {
1148 const auto split {
out_shape.begin() +
static_cast<std::ptrdiff_t
>(
s0.size() - 1)};
1156 return *std::move(
paired);
1167 std::vector<std::size_t> extent(
n_labels);
1168 for (std::size_t k {0}; k <
n_labels; ++k) {
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()};
1201 for (std::size_t i {0}; i <
n_ops; ++i) {
1211 for (std::size_t k {0}; k <
n_free; ++k) {
1214 std::vector<std::size_t>
out_shape(extent.begin(),
1215 extent.begin() +
static_cast<std::ptrdiff_t
>(
n_free));
1228 std::vector<std::size_t> index(
n_labels, 0);
1229 std::vector<std::size_t> offset(
n_ops, 0);
1241 for (std::size_t i {1}; i <
n_ops; ++i) {
1248 if (++index[k] < extent[k]) {
1249 for (std::size_t i {0}; i <
n_ops; ++i) {
1250 offset[i] +=
coef[i][k];
1255 for (std::size_t i {0}; i <
n_ops; ++i) {
1256 offset[i] -=
coef[i][k] * (extent[k] - 1);
1264 if (++index[k] < extent[k]) {
1265 for (std::size_t i {0}; i <
n_ops; ++i) {
1266 offset[i] +=
coef[i][k];
1271 for (std::size_t i {0}; i <
n_ops; ++i) {
1272 offset[i] -=
coef[i][k] * (extent[k] - 1);
1296 template <
typename T,
typename ...
Rest>
1301 const std::vector<const tensor<T>*>
operands {&first, (&
rest)...};
1314 template <
typename T>
1331 template <
typename T>
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");
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()};
1346 throw dimension_error(
"matmul: the inner extents of the operands differ");
1348 const std::size_t
lead_a {
sa.size() - (
row ? 1 : 2)};
1351 std::vector<std::size_t>
batch(rank);
1352 std::vector<std::size_t>
stride_a(rank, 0);
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;) {
1359 if (
da !=
db &&
da != 1 &&
db != 1) {
1360 throw dimension_error(
"matmul: the leading axes of the operands do not broadcast");
1368 std::size_t count {1};
1369 for (
const std::size_t extent :
batch) {
1372 std::vector<std::size_t> shape {
batch};
1379 const std::array<std::size_t, 3> elements {count, m, n};
1384 return tensor<T> {std::move(shape), std::move(c)};
1386 const auto product {[&](std::size_t p) {
1387 std::size_t
rest {p};
1390 for (std::size_t d {
batch.size()}; d-- > 0;) {
1391 const std::size_t index {
rest %
batch[d]};
1399 return tensor<T> {std::move(shape), std::move(c)};
Thrown when an operation's operands have incompatible shapes.
Thrown when an einsum specification is malformed or inconsistent.
einsum_error(const std::string &message)
Builds the error.
A dense, row-major matrix of T with value semantics.
A dense, row-major, N-dimensional tensor of T with value semantics.
const std::vector< std::size_t > & shape() const noexcept
The size of each axis.
std::vector< std::size_t > strides_
Row-major strides.
tensor(std::vector< std::size_t > shape, std::initializer_list< T > elements)
From a shape and a braced list of the elements, row-major.
const T & operator()(const std::vector< std::size_t > &index) const
The element at a multi-index.
tensor(std::vector< std::size_t > shape, buffer< T > elements)
From a shape and a flat row-major buffer.
tensor(const matrix< T > &m)
A rank-2 tensor from a matrix.
void finalize()
Computes row-major strides and checks the buffer against the shape.
bool operator==(const tensor &other) const
Exact equality (same shape and equal elements).
buffer< T > data_
The elements, row-major.
std::size_t rank() const noexcept
The number of axes (0 for a scalar).
tensor(matrix< T > &&m)
A rank-2 tensor taking over an expiring matrix's elements, without a copy.
tensor(const vector< T > &v)
A rank-1 tensor from a vector.
std::size_t size() const noexcept
The total number of elements.
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).
const buffer< T > & data() const noexcept
The elements, row-major.
std::vector< std::size_t > shape_
The size of each axis.
A dense vector of T with value semantics.
std::size_t size() const noexcept
The number of elements.
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,...
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...
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,...
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...
bool is_label(char ch)
Is ch an ASCII letter (a valid index label)?
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 ...
tensor< T > einsum_impl(const std::string &spec_in, const std::vector< const tensor< T > * > &operands)
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 ...
constexpr std::size_t transpose_tile
The side of the tiles a transposing copy goes by (an einsum permutation of two axes,...
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.
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...
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.
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.
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...
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...
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.
std::vector< std::string > split_inputs(const std::string &lhs)
Splits an einsum left-hand side into per-operand subscripts.
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...
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...
std::size_t ellipsis_in(const std::string &subscripts)
Evaluates a parsed einsum over its operands.
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.
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).
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'...
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,...
constexpr std::size_t transposed_columns
The columns of B one task of vector_times_transposed sums together: their sums' chains interleaved,...
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...
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...
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...
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...
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,...
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.
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...
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),...
constexpr std::size_t exhaustive_path_operands
The operands at most whose contraction order contraction_path searches exhaustively; more take the ch...
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,...
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).
The NumericAL public API.
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...
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....
@ 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.
bool approx_equal(const matrix< T > &a, const matrix< T > &b, double tolerance=1e-9)
Element-wise closeness within a tolerance (for inexact scalars).
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...
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,...
std::size_t second
The second's, past the first.
std::size_t first
The first operand's position.
A dense, value-semantic vector over a scalar type.