blob: 86201fef19b020feabdaea1fb5dda9ec77d0cdc2 [file]
// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// Copyright (C) 2014 Benoit Steiner <benoit.steiner.goog@gmail.com>
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
#define EIGEN_USE_THREADS
#include "main.h"
#include <atomic>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <thread>
#include <Eigen/Tensor>
using Eigen::Tensor;
class TestAllocator : public Allocator {
public:
~TestAllocator() EIGEN_OVERRIDE {}
EIGEN_DEVICE_FUNC void* allocate(size_t num_bytes) const EIGEN_OVERRIDE {
alloc_count_++;
return internal::aligned_malloc(num_bytes);
}
EIGEN_DEVICE_FUNC void deallocate(void* buffer) const EIGEN_OVERRIDE {
dealloc_count_++;
internal::aligned_free(buffer);
}
int alloc_count() const { return alloc_count_.load(); }
int dealloc_count() const { return dealloc_count_.load(); }
private:
// Pool workers allocate through the device concurrently.
mutable std::atomic<int> alloc_count_{0};
mutable std::atomic<int> dealloc_count_{0};
};
template <typename = void>
void test_multithread_elementwise() {
Tensor<float, 3> in1(200, 30, 70);
Tensor<float, 3> in2(200, 30, 70);
Tensor<double, 3> out(200, 30, 70);
in1.setRandom();
in2.setRandom();
Eigen::ThreadPool tp(internal::random<int>(3, 11));
Eigen::ThreadPoolDevice thread_pool_device(&tp, internal::random<int>(3, 11));
out.device(thread_pool_device) = (in1 + in2 * 3.14f).cast<double>();
for (int i = 0; i < 200; ++i) {
for (int j = 0; j < 30; ++j) {
for (int k = 0; k < 70; ++k) {
VERIFY_IS_APPROX(out(i, j, k), static_cast<double>(in1(i, j, k) + in2(i, j, k) * 3.14f));
}
}
}
}
template <typename = void>
void test_async_multithread_elementwise() {
Tensor<float, 3> in1(200, 30, 70);
Tensor<float, 3> in2(200, 30, 70);
Tensor<double, 3> out(200, 30, 70);
in1.setRandom();
in2.setRandom();
Eigen::ThreadPool tp(internal::random<int>(3, 11));
Eigen::ThreadPoolDevice thread_pool_device(&tp, internal::random<int>(3, 11));
Eigen::Barrier b(1);
out.device(thread_pool_device, [&b]() { b.Notify(); }) = (in1 + in2 * 3.14f).cast<double>();
b.Wait();
for (int i = 0; i < 200; ++i) {
for (int j = 0; j < 30; ++j) {
for (int k = 0; k < 70; ++k) {
VERIFY_IS_APPROX(out(i, j, k), static_cast<double>(in1(i, j, k) + in2(i, j, k) * 3.14f));
}
}
}
}
template <typename = void>
void test_multithread_chip() {
Tensor<float, 5> in(2, 3, 5, 7, 11);
Tensor<float, 4> out(3, 5, 7, 11);
in.setRandom();
Eigen::ThreadPool tp(internal::random<int>(3, 11));
Eigen::ThreadPoolDevice thread_pool_device(&tp, internal::random<int>(3, 11));
out.device(thread_pool_device) = in.chip(1, 0);
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 5; ++j) {
for (int k = 0; k < 7; ++k) {
for (int l = 0; l < 11; ++l) {
VERIFY_IS_EQUAL(out(i, j, k, l), in(1, i, j, k, l));
}
}
}
}
}
template <typename = void>
void test_async_multithread_chip() {
Tensor<float, 5> in(2, 3, 5, 7, 11);
Tensor<float, 4> out(3, 5, 7, 11);
in.setRandom();
Eigen::ThreadPool tp(internal::random<int>(3, 11));
Eigen::ThreadPoolDevice thread_pool_device(&tp, internal::random<int>(3, 11));
Eigen::Barrier b(1);
out.device(thread_pool_device, [&b]() { b.Notify(); }) = in.chip(1, 0);
b.Wait();
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 5; ++j) {
for (int k = 0; k < 7; ++k) {
for (int l = 0; l < 11; ++l) {
VERIFY_IS_EQUAL(out(i, j, k, l), in(1, i, j, k, l));
}
}
}
}
}
template <typename = void>
void test_multithread_volume_patch() {
Tensor<float, 5> in(4, 2, 3, 5, 7);
Tensor<float, 6> out(4, 1, 1, 1, 2 * 3 * 5, 7);
in.setRandom();
Eigen::ThreadPool tp(internal::random<int>(3, 11));
Eigen::ThreadPoolDevice thread_pool_device(&tp, internal::random<int>(3, 11));
out.device(thread_pool_device) = in.extract_volume_patches(1, 1, 1);
for (int i = 0; i < in.size(); ++i) {
VERIFY_IS_EQUAL(in.data()[i], out.data()[i]);
}
}
template <typename = void>
void test_async_multithread_volume_patch() {
Tensor<float, 5> in(4, 2, 3, 5, 7);
Tensor<float, 6> out(4, 1, 1, 1, 2 * 3 * 5, 7);
in.setRandom();
Eigen::ThreadPool tp(internal::random<int>(3, 11));
Eigen::ThreadPoolDevice thread_pool_device(&tp, internal::random<int>(3, 11));
Eigen::Barrier b(1);
out.device(thread_pool_device, [&b]() { b.Notify(); }) = in.extract_volume_patches(1, 1, 1);
b.Wait();
for (int i = 0; i < in.size(); ++i) {
VERIFY_IS_EQUAL(in.data()[i], out.data()[i]);
}
}
// Stateful-but-copyable unary functor: flags an overlap when two threads call
// the same instance concurrently. The executors must copy the evaluator (and
// with it the functor) into each task, so no instance is ever called
// concurrently even though the expression is evaluated by multiple threads.
struct OverlapDetectingOp {
explicit OverlapDetectingOp(std::atomic<int>* overlaps) : overlaps_(overlaps) {}
OverlapDetectingOp(const OverlapDetectingOp& other) : overlaps_(other.overlaps_) {}
float operator()(const float& x) const {
if (busy_.exchange(true)) overlaps_->fetch_add(1);
const float result = x + 1.0f;
busy_.store(false);
return result;
}
std::atomic<int>* overlaps_;
mutable std::atomic<bool> busy_{false};
};
// The driver expression uses reverse() because its block() has always read
// the argument through coeff() with BlockAccess independent of the argument,
// so these tests exercise the tiled executor's per-task evaluator copy
// regardless of how any one evaluator's traits evolve.
template <typename = void>
void test_multithread_tiled_stateful_functor() {
Tensor<float, 4> in(64, 64, 64, 8);
in.setRandom();
const array<bool, 4> rev = {{false, true, true, false}};
Tensor<float, 4> expected(64, 64, 64, 8);
expected = (in + 1.0f).reverse(rev);
std::atomic<int> overlaps(0);
Eigen::ThreadPool tp(4);
Eigen::ThreadPoolDevice thread_pool_device(&tp, 4);
Tensor<float, 4> out(64, 64, 64, 8);
out.device(thread_pool_device) = in.unaryExpr(OverlapDetectingOp(&overlaps)).reverse(rev);
VERIFY_IS_EQUAL(overlaps.load(), 0);
for (int i = 0; i < out.size(); ++i) {
VERIFY_IS_EQUAL(out.data()[i], expected.data()[i]);
}
}
template <typename = void>
void test_async_multithread_tiled_stateful_functor() {
Tensor<float, 4> in(64, 64, 64, 8);
in.setRandom();
const array<bool, 4> rev = {{false, true, true, false}};
Tensor<float, 4> expected(64, 64, 64, 8);
expected = (in + 1.0f).reverse(rev);
std::atomic<int> overlaps(0);
Eigen::ThreadPool tp(4);
Eigen::ThreadPoolDevice thread_pool_device(&tp, 4);
Tensor<float, 4> out(64, 64, 64, 8);
Eigen::Barrier b(1);
out.device(thread_pool_device, [&b]() { b.Notify(); }) = in.unaryExpr(OverlapDetectingOp(&overlaps)).reverse(rev);
b.Wait();
VERIFY_IS_EQUAL(overlaps.load(), 0);
for (int i = 0; i < out.size(); ++i) {
VERIFY_IS_EQUAL(out.data()[i], expected.data()[i]);
}
}
template <typename = void>
void test_multithread_compound_assignment() {
Tensor<float, 3> in1(2, 3, 7);
Tensor<float, 3> in2(2, 3, 7);
Tensor<float, 3> out(2, 3, 7);
in1.setRandom();
in2.setRandom();
Eigen::ThreadPool tp(internal::random<int>(3, 11));
Eigen::ThreadPoolDevice thread_pool_device(&tp, internal::random<int>(3, 11));
out.device(thread_pool_device) = in1;
out.device(thread_pool_device) += in2 * 3.14f;
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
for (int k = 0; k < 7; ++k) {
VERIFY_IS_APPROX(out(i, j, k), in1(i, j, k) + in2(i, j, k) * 3.14f);
}
}
}
}
template <int DataLayout>
void test_multithread_contraction() {
Tensor<float, 4, DataLayout> t_left(30, 50, 37, 31);
Tensor<float, 5, DataLayout> t_right(37, 31, 70, 2, 10);
Tensor<float, 5, DataLayout> t_result(30, 50, 70, 2, 10);
t_left.setRandom();
t_right.setRandom();
// this contraction should be equivalent to a single matrix multiplication
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 2> dims{{DimPair(2, 0), DimPair(3, 1)}};
typedef Map<Matrix<float, Dynamic, Dynamic, DataLayout>> MapXf;
MapXf m_left(t_left.data(), 1500, 1147);
MapXf m_right(t_right.data(), 1147, 1400);
Matrix<float, Dynamic, Dynamic, DataLayout> m_result(1500, 1400);
Eigen::ThreadPool tp(4);
Eigen::ThreadPoolDevice thread_pool_device(&tp, 4);
// compute results by separate methods
t_result.device(thread_pool_device) = t_left.contract(t_right, dims);
m_result = m_left * m_right;
// The tensor contraction and the matrix product sum the k=1147 terms in different orders; their results differ
// by a small multiple of eps, covered here as absolute slack before the relative check.
for (ptrdiff_t i = 0; i < t_result.size(); i++) {
VERIFY(&t_result.data()[i] != &m_result.data()[i]);
if (fabsf(t_result(i) - m_result(i)) < 128 * NumTraits<float>::epsilon()) {
continue;
}
if (Eigen::internal::isApprox(t_result(i), m_result(i), 128 * NumTraits<float>::epsilon())) {
continue;
}
std::cout << "mismatch detected at index " << i << ": " << t_result(i) << " vs " << m_result(i) << std::endl;
assert(false);
}
}
// Issue #1648: lock the threaded dispatch of gemv-shape contractions
// (TensorContractionThreadPool forces num_threads = 1 for n == 1 and routes
// through evalProductSequential, which now selects one of four direct GEMV
// fast paths). Covers y = A^T * x where the LHS contracted dim is the
// contiguous one — the path that motivated the fix.
template <int DataLayout>
void test_multithread_gemv_transpose() {
for (int M : {17, 64, 129}) {
for (int N : {16, 33, 100}) {
Tensor<float, 2, DataLayout> t_left(M, N);
Tensor<float, 1, DataLayout> t_right(M);
t_left.setRandom();
t_right.setRandom();
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 1> dims{{DimPair(0, 0)}};
Eigen::ThreadPool tp(4);
Eigen::ThreadPoolDevice thread_pool_device(&tp, 4);
Tensor<float, 1, DataLayout> t_result(N);
t_result.device(thread_pool_device) = t_left.contract(t_right, dims);
typedef Map<const Matrix<float, Dynamic, Dynamic, DataLayout>> MapMat;
typedef Map<const Matrix<float, Dynamic, 1>> MapVec;
MapMat m_left(t_left.data(), M, N);
MapVec m_right(t_right.data(), M);
Matrix<float, Dynamic, 1> m_result = m_left.transpose() * m_right;
for (int j = 0; j < N; ++j) {
VERIFY(internal::isApprox(t_result(j), m_result(j), 16 * NumTraits<float>::epsilon()));
}
}
}
}
template <int DataLayout>
void test_contraction_corner_cases() {
Tensor<float, 2, DataLayout> t_left(32, 500);
Tensor<float, 2, DataLayout> t_right(32, 28 * 28);
Tensor<float, 2, DataLayout> t_result(500, 28 * 28);
t_left = (t_left.constant(-0.5f) + t_left.random()) * 2.0f;
t_right = (t_right.constant(-0.6f) + t_right.random()) * 2.0f;
t_result = t_result.constant(NAN);
// this contraction should be equivalent to a single matrix multiplication
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 1> dims{{DimPair(0, 0)}};
typedef Map<Matrix<float, Dynamic, Dynamic, DataLayout>> MapXf;
MapXf m_left(t_left.data(), 32, 500);
MapXf m_right(t_right.data(), 32, 28 * 28);
Matrix<float, Dynamic, Dynamic, DataLayout> m_result(500, 28 * 28);
Eigen::ThreadPool tp(12);
Eigen::ThreadPoolDevice thread_pool_device(&tp, 12);
// compute results by separate methods
t_result.device(thread_pool_device) = t_left.contract(t_right, dims);
m_result = m_left.transpose() * m_right;
for (ptrdiff_t i = 0; i < t_result.size(); i++) {
assert(!(numext::isnan)(t_result.data()[i]));
if (fabsf(t_result.data()[i] - m_result.data()[i]) >= 256 * NumTraits<float>::epsilon()) {
std::cout << "mismatch detected at index " << i << " : " << t_result.data()[i] << " vs " << m_result.data()[i]
<< std::endl;
assert(false);
}
}
t_left.resize(32, 1);
t_left = (t_left.constant(-0.5f) + t_left.random()) * 2.0f;
t_result.resize(1, 28 * 28);
t_result = t_result.constant(NAN);
t_result.device(thread_pool_device) = t_left.contract(t_right, dims);
new (&m_left) MapXf(t_left.data(), 32, 1);
m_result = m_left.transpose() * m_right;
for (ptrdiff_t i = 0; i < t_result.size(); i++) {
assert(!(numext::isnan)(t_result.data()[i]));
if (fabsf(t_result.data()[i] - m_result.data()[i]) >= 256 * NumTraits<float>::epsilon()) {
std::cout << "mismatch detected: " << t_result.data()[i] << " vs " << m_result.data()[i] << std::endl;
assert(false);
}
}
t_left.resize(32, 500);
t_right.resize(32, 4);
t_left = (t_left.constant(-0.5f) + t_left.random()) * 2.0f;
t_right = (t_right.constant(-0.6f) + t_right.random()) * 2.0f;
t_result.resize(500, 4);
t_result = t_result.constant(NAN);
t_result.device(thread_pool_device) = t_left.contract(t_right, dims);
new (&m_left) MapXf(t_left.data(), 32, 500);
new (&m_right) MapXf(t_right.data(), 32, 4);
m_result = m_left.transpose() * m_right;
for (ptrdiff_t i = 0; i < t_result.size(); i++) {
assert(!(numext::isnan)(t_result.data()[i]));
if (fabsf(t_result.data()[i] - m_result.data()[i]) >= 256 * NumTraits<float>::epsilon()) {
std::cout << "mismatch detected: " << t_result.data()[i] << " vs " << m_result.data()[i] << std::endl;
assert(false);
}
}
t_left.resize(32, 1);
t_right.resize(32, 4);
t_left = (t_left.constant(-0.5f) + t_left.random()) * 2.0f;
t_right = (t_right.constant(-0.6f) + t_right.random()) * 2.0f;
t_result.resize(1, 4);
t_result = t_result.constant(NAN);
t_result.device(thread_pool_device) = t_left.contract(t_right, dims);
new (&m_left) MapXf(t_left.data(), 32, 1);
new (&m_right) MapXf(t_right.data(), 32, 4);
m_result = m_left.transpose() * m_right;
for (ptrdiff_t i = 0; i < t_result.size(); i++) {
assert(!(numext::isnan)(t_result.data()[i]));
if (fabsf(t_result.data()[i] - m_result.data()[i]) >= 256 * NumTraits<float>::epsilon()) {
std::cout << "mismatch detected: " << t_result.data()[i] << " vs " << m_result.data()[i] << std::endl;
assert(false);
}
}
}
template <int DataLayout>
void test_multithread_contraction_agrees_with_singlethread() {
int contract_size = internal::random<int>(1, 5000);
Tensor<float, 3, DataLayout> left(internal::random<int>(1, 80), contract_size, internal::random<int>(1, 100));
Tensor<float, 4, DataLayout> right(internal::random<int>(1, 25), internal::random<int>(1, 37), contract_size,
internal::random<int>(1, 51));
left.setRandom();
right.setRandom();
// add constants to shift values away from 0 for more precision
left += left.constant(1.5f);
right += right.constant(1.5f);
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 1> dims{{DimPair(1, 2)}};
Eigen::ThreadPool tp(internal::random<int>(2, 11));
Eigen::ThreadPoolDevice thread_pool_device(&tp, internal::random<int>(2, 11));
Tensor<float, 5, DataLayout> st_result;
st_result = left.contract(right, dims);
Tensor<float, 5, DataLayout> tp_result(st_result.dimensions());
tp_result.device(thread_pool_device) = left.contract(right, dims);
VERIFY(dimensions_match(st_result.dimensions(), tp_result.dimensions()));
for (ptrdiff_t i = 0; i < st_result.size(); i++) {
// if both of the values are very small, then do nothing (because the test will fail
// due to numerical precision issues when values are small)
if (numext::abs(st_result.data()[i] - tp_result.data()[i]) >= 1024 * NumTraits<float>::epsilon()) {
VERIFY_IS_APPROX(st_result.data()[i], tp_result.data()[i]);
}
}
}
// Apply Sqrt to all output elements.
struct SqrtOutputKernel {
template <typename Index, typename Scalar>
EIGEN_ALWAYS_INLINE void operator()(const internal::blas_data_mapper<Scalar, Index, ColMajor>& output_mapper,
const TensorContractionParams&, Index, Index, Index num_rows,
Index num_cols) const {
for (int i = 0; i < num_rows; ++i) {
for (int j = 0; j < num_cols; ++j) {
output_mapper(i, j) = std::sqrt(output_mapper(i, j));
}
}
}
};
template <int DataLayout>
static void test_multithread_contraction_with_output_kernel() {
typedef Tensor<float, 1>::DimensionPair DimPair;
const int num_threads = internal::random<int>(2, 11);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
Tensor<float, 4, DataLayout> t_left(30, 50, 8, 31);
Tensor<float, 5, DataLayout> t_right(8, 31, 7, 20, 10);
Tensor<float, 5, DataLayout> t_result(30, 50, 7, 20, 10);
t_left.setRandom();
t_right.setRandom();
// Put trash in mat4 to verify contraction clears output memory.
t_result.setRandom();
// Add a little offset so that the results won't be close to zero.
t_left += t_left.constant(1.0f);
t_right += t_right.constant(1.0f);
typedef Map<Eigen::Matrix<float, Dynamic, Dynamic, DataLayout>> MapXf;
MapXf m_left(t_left.data(), 1500, 248);
MapXf m_right(t_right.data(), 248, 1400);
Eigen::Matrix<float, Dynamic, Dynamic, DataLayout> m_result(1500, 1400);
// this contraction should be equivalent to a single matrix multiplication
Eigen::array<DimPair, 2> dims{{DimPair(2, 0), DimPair(3, 1)}};
// compute results by separate methods
t_result.device(device) = t_left.contract(t_right, dims, SqrtOutputKernel());
m_result = m_left * m_right;
for (Index i = 0; i < t_result.dimensions().TotalSize(); i++) {
VERIFY(&t_result.data()[i] != &m_result.data()[i]);
VERIFY_IS_APPROX(t_result.data()[i], std::sqrt(m_result.data()[i]));
}
}
template <int DataLayout>
void test_async_multithread_contraction_agrees_with_singlethread() {
int contract_size = internal::random<int>(100, 500);
Tensor<float, 3, DataLayout> left(internal::random<int>(10, 40), contract_size, internal::random<int>(10, 40));
Tensor<float, 4, DataLayout> right(internal::random<int>(1, 20), internal::random<int>(1, 20), contract_size,
internal::random<int>(1, 20));
left.setRandom();
right.setRandom();
// add constants to shift values away from 0 for more precision
left += left.constant(1.5f);
right += right.constant(1.5f);
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 1> dims{{DimPair(1, 2)}};
Eigen::ThreadPool tp(internal::random<int>(2, 11));
Eigen::ThreadPoolDevice thread_pool_device(&tp, internal::random<int>(8, 32));
Tensor<float, 5, DataLayout> st_result;
st_result = left.contract(right, dims);
Tensor<float, 5, DataLayout> tp_result(st_result.dimensions());
Eigen::Barrier barrier(1);
tp_result.device(thread_pool_device, [&barrier]() { barrier.Notify(); }) = left.contract(right, dims);
barrier.Wait();
VERIFY(dimensions_match(st_result.dimensions(), tp_result.dimensions()));
for (ptrdiff_t i = 0; i < st_result.size(); i++) {
// if both of the values are very small, then do nothing (because the test
// will fail due to numerical precision issues when values are small)
if (numext::abs(st_result.data()[i] - tp_result.data()[i]) >= 1024 * NumTraits<float>::epsilon()) {
VERIFY_IS_APPROX(st_result.data()[i], tp_result.data()[i]);
}
}
}
template <typename Contraction>
struct AsyncContractionEvalToProbe;
namespace Eigen {
namespace internal {
template <typename Contraction>
struct traits<AsyncContractionEvalToProbe<Contraction>> : traits<TensorEvaluator<const Contraction, ThreadPoolDevice>> {
};
} // namespace internal
} // namespace Eigen
template <typename Contraction>
struct AsyncContractionEvalToProbe : Eigen::TensorContractionEvaluatorBase<AsyncContractionEvalToProbe<Contraction>> {
using Base = Eigen::TensorContractionEvaluatorBase<AsyncContractionEvalToProbe<Contraction>>;
using Scalar = typename Base::Scalar;
AsyncContractionEvalToProbe(const Contraction& op, const Eigen::ThreadPoolDevice& device) : Base(op, device) {}
template <int Alignment>
void evalProduct(Scalar*) const {}
template <typename EvalToCallback, int Alignment>
void evalProductAsync(Scalar* buffer, EvalToCallback done) const {
static int shared_instantiation_calls = 0;
last_instantiation_calls = ++shared_instantiation_calls;
last_buffer = buffer;
done();
}
mutable Scalar* last_buffer = nullptr;
mutable int last_instantiation_calls = 0;
};
template <int DataLayout>
void test_async_contraction_eval_subexprs_dedup() {
using DimPair = typename Tensor<float, 1>::DimensionPair;
Eigen::ThreadPool tp(4);
Eigen::ThreadPoolDevice device(&tp, 4);
Tensor<float, 2, DataLayout> left(4, 4);
Tensor<float, 2, DataLayout> right(4, 4);
Tensor<float, 2, DataLayout> dest(4, 4);
left.setRandom();
right.setRandom();
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
auto expr = left.contract(right, dims);
AsyncContractionEvalToProbe<decltype(expr)> probe(expr, device);
bool need_assign = true;
auto on_done = [&need_assign](bool na) { need_assign = na; };
probe.evalSubExprsIfNeededAsync(dest.data(), on_done);
VERIFY_IS_EQUAL(need_assign, false);
VERIFY_IS_EQUAL(probe.last_buffer, dest.data());
VERIFY_IS_EQUAL(probe.data(), static_cast<float*>(nullptr));
VERIFY_IS_EQUAL(probe.last_instantiation_calls, 1);
probe.evalSubExprsIfNeededAsync(nullptr, on_done);
VERIFY_IS_EQUAL(need_assign, true);
VERIFY(probe.data() != nullptr);
VERIFY_IS_EQUAL(probe.last_buffer, probe.data());
VERIFY_IS_EQUAL(probe.last_instantiation_calls, 2);
probe.cleanup();
VERIFY_IS_EQUAL(probe.data(), static_cast<float*>(nullptr));
// Verify real ThreadPoolDevice evaluation along the dest == nullptr branch
// (nested contraction subexpression) across parallel GEMM, inner-dim sharded,
// and k = 0 shapes.
const int shapes[][3] = {{64, 64, 64}, {2, 2048, 10}, {10, 0, 20}};
for (const auto& shape : shapes) {
const int m = shape[0];
const int k = shape[1];
const int n = shape[2];
Tensor<float, 2, DataLayout> t_left(m, k);
Tensor<float, 2, DataLayout> t_right(k, n);
Tensor<float, 2, DataLayout> t_bias(m, n);
t_left.setRandom();
t_right.setRandom();
t_bias.setRandom();
t_left += t_left.constant(1.5f);
t_right += t_right.constant(1.5f);
t_bias += t_bias.constant(1.5f);
Tensor<float, 2, DataLayout> st_result(m, n);
Tensor<float, 2, DataLayout> tp_result(m, n);
tp_result.setZero();
st_result = t_left.contract(t_right, dims) + t_bias;
Eigen::Barrier barrier(1);
tp_result.device(device, [&barrier]() { barrier.Notify(); }) = t_left.contract(t_right, dims) + t_bias;
barrier.Wait();
for (Index i = 0; i < st_result.size(); ++i) {
VERIFY_IS_APPROX(tp_result.data()[i], st_result.data()[i]);
}
}
}
// We are triggering 'evalShardedByInnerDim' optimization.
template <int DataLayout>
static void test_sharded_by_inner_dim_contraction() {
using DimPair = typename Tensor<float, 1>::DimensionPair;
const int num_threads = internal::random<int>(4, 16);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
const Index k = 30000 * PacketType<float, Eigen::ThreadPoolDevice>::size;
Tensor<float, 2, DataLayout> t_left(2, k);
Tensor<float, 2, DataLayout> t_right(k, 10);
Tensor<float, 2, DataLayout> t_result(2, 10);
t_left.setRandom();
t_right.setRandom();
// Put trash in t_result to verify contraction clears output memory.
t_result.setRandom();
// Add a little offset so that the results won't be close to zero.
t_left += t_left.constant(1.0f);
t_right += t_right.constant(1.0f);
using MapXf = Map<Eigen::Matrix<float, Dynamic, Dynamic, DataLayout>>;
MapXf m_left(t_left.data(), 2, k);
MapXf m_right(t_right.data(), k, 10);
Eigen::Matrix<float, Dynamic, Dynamic, DataLayout> m_result(2, 10);
// this contraction should be equivalent to a single matrix multiplication
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
auto contraction = t_left.contract(t_right, dims);
using ContractionEval = TensorEvaluator<const decltype(contraction), Eigen::ThreadPoolDevice>;
ContractionEval eval(contraction, device);
const int nt_k = eval.numThreadsInnerDim(2, 10, k);
VERIFY(nt_k > 1);
VERIFY(ContractionEval::shardByInnerDim(2, 10, k, /*num_threads=*/1, nt_k));
// compute results by separate methods
t_result.device(device) = contraction;
m_result = m_left * m_right;
for (Index i = 0; i < t_result.dimensions().TotalSize(); i++) {
VERIFY_IS_APPROX(t_result.data()[i], m_result.data()[i]);
}
}
// We are triggering 'evalShardedByInnerDim' optimization with output kernel.
template <int DataLayout>
static void test_sharded_by_inner_dim_contraction_with_output_kernel() {
using DimPair = typename Tensor<float, 1>::DimensionPair;
const int num_threads = internal::random<int>(4, 16);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
const Index k = 30000 * PacketType<float, Eigen::ThreadPoolDevice>::size;
Tensor<float, 2, DataLayout> t_left(2, k);
Tensor<float, 2, DataLayout> t_right(k, 10);
Tensor<float, 2, DataLayout> t_result(2, 10);
t_left.setRandom();
t_right.setRandom();
// Put trash in t_result to verify contraction clears output memory.
t_result.setRandom();
// Add a little offset so that the results won't be close to zero.
t_left += t_left.constant(1.0f);
t_right += t_right.constant(1.0f);
using MapXf = Map<Eigen::Matrix<float, Dynamic, Dynamic, DataLayout>>;
MapXf m_left(t_left.data(), 2, k);
MapXf m_right(t_right.data(), k, 10);
Eigen::Matrix<float, Dynamic, Dynamic, DataLayout> m_result(2, 10);
// this contraction should be equivalent to a single matrix multiplication
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
auto contraction = t_left.contract(t_right, dims, SqrtOutputKernel());
using ContractionEval = TensorEvaluator<const decltype(contraction), Eigen::ThreadPoolDevice>;
ContractionEval eval(contraction, device);
const int nt_k = eval.numThreadsInnerDim(2, 10, k);
VERIFY(nt_k > 1);
VERIFY(ContractionEval::shardByInnerDim(2, 10, k, /*num_threads=*/1, nt_k));
// compute results by separate methods
t_result.device(device) = contraction;
m_result = m_left * m_right;
for (Index i = 0; i < t_result.dimensions().TotalSize(); i++) {
VERIFY_IS_APPROX(t_result.data()[i], std::sqrt(m_result.data()[i]));
}
}
// We are triggering 'evalShardedByInnerDim' optimization.
template <int DataLayout>
static void test_async_sharded_by_inner_dim_contraction() {
using DimPair = typename Tensor<float, 1>::DimensionPair;
const int num_threads = internal::random<int>(4, 16);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
const Index k = 30000 * PacketType<float, Eigen::ThreadPoolDevice>::size;
Tensor<float, 2, DataLayout> t_left(2, k);
Tensor<float, 2, DataLayout> t_right(k, 10);
Tensor<float, 2, DataLayout> t_result(2, 10);
t_left.setRandom();
t_right.setRandom();
// Put trash in t_result to verify contraction clears output memory.
t_result.setRandom();
// Add a little offset so that the results won't be close to zero.
t_left += t_left.constant(1.0f);
t_right += t_right.constant(1.0f);
using MapXf = Map<Eigen::Matrix<float, Dynamic, Dynamic, DataLayout>>;
MapXf m_left(t_left.data(), 2, k);
MapXf m_right(t_right.data(), k, 10);
Eigen::Matrix<float, Dynamic, Dynamic, DataLayout> m_result(2, 10);
// this contraction should be equivalent to a single matrix multiplication
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
auto contraction = t_left.contract(t_right, dims);
using ContractionEval = TensorEvaluator<const decltype(contraction), Eigen::ThreadPoolDevice>;
ContractionEval eval(contraction, device);
const int nt_k = eval.numThreadsInnerDim(2, 10, k);
VERIFY(nt_k > 1);
VERIFY(ContractionEval::shardByInnerDim(2, 10, k, /*num_threads=*/1, nt_k));
// compute results by separate methods
Eigen::Barrier barrier(1);
t_result.device(device, [&barrier]() { barrier.Notify(); }) = contraction;
barrier.Wait();
m_result = m_left * m_right;
for (Index i = 0; i < t_result.dimensions().TotalSize(); i++) {
VERIFY_IS_APPROX(t_result.data()[i], m_result.data()[i]);
}
}
// We are triggering 'evalShardedByInnerDim' optimization with output kernel.
template <int DataLayout>
static void test_async_sharded_by_inner_dim_contraction_with_output_kernel() {
using DimPair = typename Tensor<float, 1>::DimensionPair;
const int num_threads = internal::random<int>(4, 16);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
const Index k = 30000 * PacketType<float, Eigen::ThreadPoolDevice>::size;
Tensor<float, 2, DataLayout> t_left(2, k);
Tensor<float, 2, DataLayout> t_right(k, 10);
Tensor<float, 2, DataLayout> t_result(2, 10);
t_left.setRandom();
t_right.setRandom();
// Put trash in t_result to verify contraction clears output memory.
t_result.setRandom();
// Add a little offset so that the results won't be close to zero.
t_left += t_left.constant(1.0f);
t_right += t_right.constant(1.0f);
using MapXf = Map<Eigen::Matrix<float, Dynamic, Dynamic, DataLayout>>;
MapXf m_left(t_left.data(), 2, k);
MapXf m_right(t_right.data(), k, 10);
Eigen::Matrix<float, Dynamic, Dynamic, DataLayout> m_result(2, 10);
// this contraction should be equivalent to a single matrix multiplication
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
auto contraction = t_left.contract(t_right, dims, SqrtOutputKernel());
using ContractionEval = TensorEvaluator<const decltype(contraction), Eigen::ThreadPoolDevice>;
ContractionEval eval(contraction, device);
const int nt_k = eval.numThreadsInnerDim(2, 10, k);
VERIFY(nt_k > 1);
VERIFY(ContractionEval::shardByInnerDim(2, 10, k, /*num_threads=*/1, nt_k));
// compute results by separate methods
Eigen::Barrier barrier(1);
t_result.device(device, [&barrier]() { barrier.Notify(); }) = contraction;
barrier.Wait();
m_result = m_left * m_right;
for (Index i = 0; i < t_result.dimensions().TotalSize(); i++) {
VERIFY_IS_APPROX(t_result.data()[i], std::sqrt(m_result.data()[i]));
}
}
template <int DataLayout>
static void test_sharded_by_inner_dim_cost_model() {
using DimPair = typename Tensor<float, 1>::DimensionPair;
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
ThreadPool threads(8);
Eigen::ThreadPoolDevice device(&threads, 8);
Tensor<float, 2, DataLayout> t_left(2, 2);
Tensor<float, 2, DataLayout> t_right(2, 2);
auto expr = t_left.contract(t_right, dims);
using ContractionEval = TensorEvaluator<const decltype(expr), Eigen::ThreadPoolDevice>;
ContractionEval eval(expr, device);
// Scale k by the packet width so the parallel cost (6.6e4..2.6e5 cycles across FMA and panel-width regimes) sits
// between the old hardcoded threshold (~5.7e4 cycles) and the TensorCostModel threshold (~4.0e5 cycles) under every
// SIMD width.
const Index f32_packet_size = PacketType<float, Eigen::ThreadPoolDevice>::size;
const Index moderate_f32_k = 64 * f32_packet_size;
VERIFY_IS_EQUAL(eval.numThreadsInnerDim(32, 64, moderate_f32_k), 1);
VERIFY(!ContractionEval::shardByInnerDim(32, 64, moderate_f32_k, /*num_threads=*/1,
eval.numThreadsInnerDim(32, 64, moderate_f32_k)));
// Cover the bfloat16 contraction shape (16 x 128 x k) that motivated the fix.
Tensor<Eigen::bfloat16, 2, DataLayout> bf16_left(2, 2);
Tensor<Eigen::bfloat16, 2, DataLayout> bf16_right(2, 2);
auto bf16_expr = bf16_left.contract(bf16_right, dims);
using Bf16ContractionEval = TensorEvaluator<const decltype(bf16_expr), Eigen::ThreadPoolDevice>;
Bf16ContractionEval bf16_eval(bf16_expr, device);
const Index bf16_packet_size = PacketType<Eigen::bfloat16, Eigen::ThreadPoolDevice>::size;
const Index moderate_bf16_k = 64 * bf16_packet_size;
VERIFY_IS_EQUAL(bf16_eval.numThreadsInnerDim(16, 128, moderate_bf16_k), 1);
VERIFY(!Bf16ContractionEval::shardByInnerDim(16, 128, moderate_bf16_k, /*num_threads=*/1,
bf16_eval.numThreadsInnerDim(16, 128, moderate_bf16_k)));
// Deep inner dimension with tiny outer dimensions should still shard by k.
const Index deep_f32_k = 30000 * f32_packet_size;
const int nt_k = eval.numThreadsInnerDim(2, 10, deep_f32_k);
VERIFY(nt_k > 1);
VERIFY(ContractionEval::shardByInnerDim(2, 10, deep_f32_k, /*num_threads=*/1, nt_k));
}
template <int DataLayout>
void test_full_contraction() {
int contract_size1 = internal::random<int>(1, 500);
int contract_size2 = internal::random<int>(1, 500);
Tensor<float, 2, DataLayout> left(contract_size1, contract_size2);
Tensor<float, 2, DataLayout> right(contract_size1, contract_size2);
left.setRandom();
right.setRandom();
// add constants to shift values away from 0 for more precision
left += left.constant(1.5f);
right += right.constant(1.5f);
typedef Tensor<float, 2>::DimensionPair DimPair;
Eigen::array<DimPair, 2> dims{{DimPair(0, 0), DimPair(1, 1)}};
Eigen::ThreadPool tp(internal::random<int>(2, 11));
Eigen::ThreadPoolDevice thread_pool_device(&tp, internal::random<int>(2, 11));
Tensor<float, 0, DataLayout> st_result;
st_result = left.contract(right, dims);
Tensor<float, 0, DataLayout> tp_result;
tp_result.device(thread_pool_device) = left.contract(right, dims);
VERIFY(dimensions_match(st_result.dimensions(), tp_result.dimensions()));
// if both of the values are very small, then do nothing (because the test will fail
// due to numerical precision issues when values are small)
if (numext::abs(st_result() - tp_result()) >= 1024 * NumTraits<float>::epsilon()) {
VERIFY_IS_APPROX(st_result(), tp_result());
}
}
template <int DataLayout>
void test_multithreaded_reductions() {
const int num_threads = internal::random<int>(3, 11);
ThreadPool thread_pool(num_threads);
Eigen::ThreadPoolDevice thread_pool_device(&thread_pool, num_threads);
const int num_rows = internal::random<int>(13, 732);
const int num_cols = internal::random<int>(13, 732);
Tensor<float, 2, DataLayout> t1(num_rows, num_cols);
t1.setRandom();
Tensor<float, 0, DataLayout> full_redux;
full_redux = t1.sum();
Tensor<float, 0, DataLayout> full_redux_tp;
full_redux_tp.device(thread_pool_device) = t1.sum();
// Check that the single threaded and the multi threaded reductions return
// the same result.
VERIFY_IS_APPROX(full_redux(), full_redux_tp());
}
// reduce() squares each value, so partial results cannot be merged through it.
struct SquaredSumReducer {
void reduce(const double t, double* accum) const { *accum += t * t; }
double initialize() const { return 0; }
double finalize(const double accum) const { return accum; }
};
template <typename = void>
void test_multithreaded_value_transforming_full_reduction() {
ThreadPool thread_pool(4);
Eigen::ThreadPoolDevice thread_pool_device(&thread_pool, 4);
Tensor<double, 1> t(1 << 20);
t.setConstant(1.0);
Tensor<double, 0> result;
result.device(thread_pool_device) = t.reduce(array<Index, 1>{{0}}, SquaredSumReducer());
VERIFY_IS_EQUAL(result(), static_cast<double>(t.size()));
}
// argmax()/argmin() take the sharded full reduction; each extremum recurs in every shard, so the
// merge must keep the first occurrence.
template <typename = void>
void test_multithreaded_argmax() {
STATIC_CHECK((internal::reducer_can_reorder_accumulators<internal::ArgMaxPairReducer<Pair<Index, float>>>::value));
STATIC_CHECK((internal::reducer_can_reorder_accumulators<internal::ArgMinPairReducer<Pair<Index, float>>>::value));
ThreadPool thread_pool(4);
Eigen::ThreadPoolDevice thread_pool_device(&thread_pool, 4);
Tensor<float, 1> t(1 << 20);
for (Index i = 0; i < t.size(); ++i) t(i) = static_cast<float>((i + 500) % 1000);
Tensor<Index, 0> result;
result.device(thread_pool_device) = t.argmax();
VERIFY_IS_EQUAL(result(), Index(499));
result.device(thread_pool_device) = t.argmin();
VERIFY_IS_EQUAL(result(), Index(500));
}
template <typename = void>
void test_multithreaded_complex_reduction() {
using Scalar = std::complex<float>;
constexpr Index size = 4096;
// Preserve the mapped source and fixed-size mapped destination from the issue #1647 backtrace: this exact
// instantiation triggers the old GCC compiler bug.
Tensor<Scalar, 1, RowMajor> storage(size);
storage.setConstant(Scalar(1.0f, 2.0f));
TensorMap<Tensor<Scalar, 1, RowMajor>> input(storage.data(), size);
EIGEN_ALIGN_MAX Scalar output_storage;
TensorMap<TensorFixedSize<Scalar, Sizes<>, RowMajor>, Aligned> output(&output_storage);
ThreadPool thread_pool(2);
ThreadPoolDevice thread_pool_device(&thread_pool, 2);
output.device(thread_pool_device) = input.sum();
VERIFY_IS_EQUAL(output(), Scalar(static_cast<float>(size), static_cast<float>(2 * size)));
}
template <typename = void>
void test_multithreaded_complex_partial_reductions() {
using Scalar = std::complex<float>;
constexpr Index outer_size = 32;
constexpr Index reduced_size = 257;
Tensor<Scalar, 2, RowMajor> input(outer_size, reduced_size);
for (Index i = 0; i < outer_size; ++i) {
for (Index j = 0; j < reduced_size; ++j) {
input(i, j) = Scalar(static_cast<float>(i + 1), static_cast<float>(2 * i + 1));
}
}
ThreadPool thread_pool(2);
ThreadPoolDevice thread_pool_device(&thread_pool, 2);
IndexList<type2index<1>> static_reduction_dim;
Tensor<Scalar, 1, RowMajor> static_result(outer_size);
static_result.device(thread_pool_device) = input.sum(static_reduction_dim);
array<Index, 1> runtime_reduction_dim{1};
Tensor<Scalar, 1, RowMajor> runtime_result(outer_size);
runtime_result.device(thread_pool_device) = input.sum(runtime_reduction_dim);
for (Index i = 0; i < outer_size; ++i) {
const Scalar expected = static_cast<float>(reduced_size) * input(i, 0);
VERIFY_IS_EQUAL(static_result(i), expected);
VERIFY_IS_EQUAL(runtime_result(i), expected);
}
}
template <typename = void>
void test_memcpy() {
for (int i = 0; i < 5; ++i) {
const int num_threads = internal::random<int>(3, 11);
Eigen::ThreadPool tp(num_threads);
Eigen::ThreadPoolDevice thread_pool_device(&tp, num_threads);
const int size = internal::random<int>(13, 7632);
Tensor<float, 1> t1(size);
t1.setRandom();
std::vector<float> result(size);
thread_pool_device.memcpy(&result[0], t1.data(), size * sizeof(float));
for (int j = 0; j < size; j++) {
VERIFY_IS_EQUAL(t1(j), result[j]);
}
}
}
// The executors copy the evaluator per task; a copy must not destroy the forced-eval buffer's elements.
template <typename = void>
void test_multithread_forced_eval_non_pod() {
Eigen::ThreadPool tp(4);
Eigen::ThreadPoolDevice thread_pool_device(&tp, 4);
Tensor<std::string, 1> in(64), out(64);
for (int i = 0; i < 64; ++i) in(i) = std::string(40, static_cast<char>('a' + i % 26));
out.device(thread_pool_device) = in.eval();
for (int i = 0; i < 64; ++i) VERIFY_IS_EQUAL(out(i), in(i));
}
// A scalar that requires initialization: its constructors set a magic value that operator= checks, and it counts
// live instances so that missing destructor calls are detected.
struct TrackedScalar {
static constexpr unsigned kMagic = 0x5EEDu;
static std::atomic<int> instances;
static std::atomic<int> unconstructed_assignments;
// Assignments on threads other than test_thread, to check that the work ran on the pool.
static std::atomic<int> pool_assignments;
// When positive, the throw_countdown-th default construction on test_thread throws.
static std::atomic<int> throw_countdown;
// When positive, the multiply_countdown-th multiplication on test_thread throws.
static std::atomic<int> multiply_countdown;
static std::thread::id test_thread;
float v;
unsigned magic;
TrackedScalar() : v(0.0f), magic(kMagic) {
if (throw_countdown.load() > 0 && std::this_thread::get_id() == test_thread) {
const int remaining = --throw_countdown;
if (remaining == 0) EIGEN_THROW_X(std::runtime_error("TrackedScalar"));
}
++instances;
}
TrackedScalar(float f) : v(f), magic(kMagic) { ++instances; }
TrackedScalar(const TrackedScalar& o) : TrackedScalar(o.v) {}
~TrackedScalar() {
magic = 0;
--instances;
}
TrackedScalar& operator=(const TrackedScalar& o) {
if (magic != kMagic) ++unconstructed_assignments;
if (std::this_thread::get_id() != test_thread) ++pool_assignments;
v = o.v;
return *this;
}
TrackedScalar operator+(const TrackedScalar& o) const { return TrackedScalar(v + o.v); }
TrackedScalar operator-(const TrackedScalar& o) const { return TrackedScalar(v - o.v); }
TrackedScalar operator*(const TrackedScalar& o) const {
if (multiply_countdown.load() > 0 && std::this_thread::get_id() == test_thread) {
const int remaining = --multiply_countdown;
if (remaining == 0) EIGEN_THROW_X(std::runtime_error("TrackedScalar"));
}
return TrackedScalar(v * o.v);
}
TrackedScalar operator-() const { return TrackedScalar(-v); }
TrackedScalar& operator+=(const TrackedScalar& o) { return *this = *this + o; }
TrackedScalar& operator*=(const TrackedScalar& o) { return *this = *this * o; }
bool operator==(const TrackedScalar& o) const { return v == o.v; }
bool operator!=(const TrackedScalar& o) const { return v != o.v; }
};
std::atomic<int> TrackedScalar::instances(0);
std::atomic<int> TrackedScalar::unconstructed_assignments(0);
std::atomic<int> TrackedScalar::pool_assignments(0);
std::atomic<int> TrackedScalar::throw_countdown(0);
std::atomic<int> TrackedScalar::multiply_countdown(0);
std::thread::id TrackedScalar::test_thread;
namespace Eigen {
template <>
struct NumTraits<TrackedScalar> : GenericNumTraits<TrackedScalar> {
using Real = TrackedScalar;
using NonInteger = TrackedScalar;
using Literal = TrackedScalar;
using Nested = TrackedScalar;
static constexpr int IsComplex = 0;
static constexpr int IsInteger = 0;
static constexpr int IsSigned = 1;
static constexpr int RequireInitialization = 1;
static constexpr int ReadCost = 1;
static constexpr int AddCost = 1;
static constexpr int MulCost = 1;
};
} // namespace Eigen
// The thread pool contraction paths, identified by the allocations of one direct contraction: parallel packing
// allocates its packed blocks once, thread-local packing (parallelize_by_sharding_dim_only) also allocates the
// thread-local blocks, and sharding by the inner dimension allocates a buffer and packed blocks per block.
enum class ContractionPath { ParallelPack, ThreadLocalPack, ShardByInnerDim };
// The path depends on the cache sizes, so the tests that check it pin them to typical x86 values.
struct ScopedCacheSizes {
ScopedCacheSizes() : l1(l1CacheSize()), l2(l2CacheSize()), l3(l3CacheSize()) {
setCpuCacheSizes(32 * 1024, 1024 * 1024, 32 * 1024 * 1024);
}
~ScopedCacheSizes() { setCpuCacheSizes(l1, l2, l3); }
std::ptrdiff_t l1, l2, l3;
};
// Issue #2714: the threaded contraction paths, async evaluation and temporary results construct and destruct scalars
// that require initialization.
template <int DataLayout>
static void test_multithread_contraction_require_initialization(Index m, Index k, Index n, ContractionPath path) {
using DimPair = typename Tensor<float, 1>::DimensionPair;
const ScopedCacheSizes cache_sizes;
TestAllocator test_allocator;
TrackedScalar::test_thread = std::this_thread::get_id();
Tensor<float, 2, DataLayout> left(m, k);
Tensor<float, 2, DataLayout> right(k, n);
left.setRandom();
right.setRandom();
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
Tensor<float, 2, DataLayout> expected = left.contract(right, dims);
TrackedScalar::instances = 0;
TrackedScalar::unconstructed_assignments = 0;
{
// Destroying the pool at the end of this block joins the workers before the checks below: the kernel task that
// sends the final notification still holds alpha and beta until its frame unwinds.
Eigen::ThreadPool tp(4);
Eigen::ThreadPoolDevice device(&tp, 4, &test_allocator);
Tensor<TrackedScalar, 2, DataLayout> t_left = left.template cast<TrackedScalar>();
Tensor<TrackedScalar, 2, DataLayout> t_right = right.template cast<TrackedScalar>();
Tensor<TrackedScalar, 2, DataLayout> result(m, n);
const auto check = [&](float scale) {
for (Index i = 0; i < result.size(); ++i) {
VERIFY_IS_APPROX(result.data()[i].v, scale * expected.data()[i]);
}
};
TrackedScalar::pool_assignments = 0;
const int allocations_before = test_allocator.alloc_count();
result.device(device) = t_left.contract(t_right, dims);
check(1);
VERIFY(TrackedScalar::pool_assignments.load() > 0);
const int allocations = test_allocator.alloc_count() - allocations_before;
switch (path) {
case ContractionPath::ParallelPack:
VERIFY_IS_EQUAL(allocations, 1);
break;
case ContractionPath::ThreadLocalPack:
VERIFY_IS_EQUAL(allocations, 2);
break;
case ContractionPath::ShardByInnerDim:
VERIFY(allocations >= 3);
break;
}
// A contraction nested in another expression evaluates into a temporary result buffer. The buffer does not
// depend on the path, so the slowest shape skips it.
const bool nested = path != ContractionPath::ThreadLocalPack;
if (nested) {
result.device(device) = t_left.contract(t_right, dims) + t_left.contract(t_right, dims);
check(2);
}
Eigen::Barrier done(1);
result.device(device, [&done]() { done.Notify(); }) = t_left.contract(t_right, dims);
done.Wait();
check(1);
if (nested) {
Eigen::Barrier done_nested(1);
result.device(device, [&done_nested]() { done_nested.Notify(); }) =
t_left.contract(t_right, dims) + t_left.contract(t_right, dims);
done_nested.Wait();
check(2);
}
}
VERIFY_IS_EQUAL(TrackedScalar::unconstructed_assignments.load(), 0);
VERIFY_IS_EQUAL(TrackedScalar::instances.load(), 0);
}
// A TestAllocator whose throw_countdown-th allocation on TrackedScalar::test_thread throws.
class ThrowingAllocator : public TestAllocator {
public:
EIGEN_DEVICE_FUNC void* allocate(size_t num_bytes) const EIGEN_OVERRIDE {
if (throw_countdown.load() > 0 && std::this_thread::get_id() == TrackedScalar::test_thread) {
const int remaining = --throw_countdown;
if (remaining == 0) EIGEN_THROW_X(std::runtime_error("ThrowingAllocator"));
}
return TestAllocator::allocate(num_bytes);
}
mutable std::atomic<int> throw_countdown{0};
};
// Issue #2714: if a constructor throws partway through constructing the contraction buffers, the elements constructed
// so far are destructed and the buffers are freed.
template <int DataLayout>
static void test_contraction_require_initialization_throws() {
#ifdef EIGEN_EXCEPTIONS
using DimPair = typename Tensor<float, 1>::DimensionPair;
const ScopedCacheSizes cache_sizes;
Eigen::ThreadPool tp(4);
ThrowingAllocator test_allocator;
Eigen::ThreadPoolDevice device(&tp, 4, &test_allocator);
TrackedScalar::test_thread = std::this_thread::get_id();
// Runs f with the count-th default construction on this thread throwing; returns whether it threw.
const auto throws_at = [](int count, const auto& f) {
TrackedScalar::throw_countdown = count;
bool threw = false;
try {
f();
} catch (const std::runtime_error&) {
threw = true;
}
TrackedScalar::throw_countdown = 0;
return threw;
};
const auto verify_released = [&](int instances) {
VERIFY_IS_EQUAL(TrackedScalar::instances.load(), instances);
VERIFY_IS_EQUAL(test_allocator.alloc_count(), test_allocator.dealloc_count());
};
// Throws at every element the allocator constructs. With float lhs blocks, only the rhs blocks are constructed.
const auto sweep_allocator = [&](auto* lhs_tag, int lhs_constructed) {
using LhsScalar = std::remove_pointer_t<decltype(lhs_tag)>;
using BlockAllocator = internal::TensorContractionBlockMemAllocator<LhsScalar, TrackedScalar>;
const Index bm = 3, bk = 4, bn = 5;
const int block_elements = lhs_constructed * bm * bk + bn * bk;
for (int count = 1; count <= block_elements; ++count) {
LhsScalar* lhs = nullptr;
TrackedScalar* rhs = nullptr;
VERIFY(throws_at(count, [&]() { BlockAllocator::allocate(device, bm, bk, bn, &lhs, &rhs); }));
verify_released(0);
}
const Index num_lhs = 2, num_rhs = 3, num_slices = 2;
const int slice_elements = (lhs_constructed * num_lhs * bm * bk + num_rhs * bn * bk) * num_slices;
for (int count = 1; count <= slice_elements; ++count) {
std::vector<LhsScalar*> lhs_blocks[num_slices];
std::vector<TrackedScalar*> rhs_blocks[num_slices];
VERIFY(throws_at(count, [&]() {
BlockAllocator::allocateSlices(device, bm, bk, bn, num_lhs, num_rhs, num_slices, lhs_blocks, rhs_blocks);
}));
verify_released(0);
}
};
TrackedScalar::instances = 0;
sweep_allocator(static_cast<TrackedScalar*>(nullptr), 1);
sweep_allocator(static_cast<float*>(nullptr), 0);
// Parallel evaluation: the EvalParallelContext constructor allocates the packed blocks and, with thread-local
// packing, the thread-local blocks, all on this thread and before any task starts. A throw while constructing the
// first block, or from either allocation, releases what the constructor allocated.
{
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
const auto parallel_throws = [&](Index pm, Index pk, Index pn, int allocations) {
Tensor<TrackedScalar, 2, DataLayout> t_left(pm, pk);
Tensor<TrackedScalar, 2, DataLayout> t_right(pk, pn);
Tensor<TrackedScalar, 2, DataLayout> result(pm, pn);
t_left.setConstant(TrackedScalar(1.0f));
t_right.setConstant(TrackedScalar(1.0f));
const auto contract = [&]() { result.device(device) = t_left.contract(t_right, dims); };
const int live = TrackedScalar::instances.load();
VERIFY(throws_at(1, contract));
verify_released(live);
for (int allocation = 1; allocation <= allocations; ++allocation) {
test_allocator.throw_countdown = allocation;
VERIFY(throws_at(0, contract));
test_allocator.throw_countdown = 0;
verify_released(live);
}
};
// The paths of these shapes are checked in test_multithread_contraction_require_initialization.
parallel_throws(131, 67, 509, /*allocations=*/1);
parallel_throws(3, 127, 20001, /*allocations=*/2);
}
// Sharded by the inner dimension: the result of a nested contraction, then the per-block buffers.
const Index m = 3, k = 100003, n = 11;
Tensor<float, 2, DataLayout> left(m, k);
Tensor<float, 2, DataLayout> right(k, n);
left.setRandom();
right.setRandom();
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
Tensor<float, 2, DataLayout> expected = left.contract(right, dims);
{
Tensor<TrackedScalar, 2, DataLayout> t_left = left.template cast<TrackedScalar>();
Tensor<TrackedScalar, 2, DataLayout> t_right = right.template cast<TrackedScalar>();
Tensor<TrackedScalar, 2, DataLayout> result(m, n);
const int live = TrackedScalar::instances.load();
const int allocations_before = test_allocator.alloc_count();
result.device(device) = t_left.contract(t_right, dims);
// Sharding by the inner dimension allocates a buffer and packed blocks per block (see ContractionPath).
VERIFY(test_allocator.alloc_count() - allocations_before >= 3);
const int result_elements = static_cast<int>(m * n);
for (int count : {1, result_elements, result_elements + 1}) {
VERIFY(throws_at(count, [&]() { result.device(device) = t_left.contract(t_right, dims); }));
verify_released(live);
VERIFY(throws_at(
count, [&]() { result.device(device) = t_left.contract(t_right, dims) + t_left.contract(t_right, dims); }));
verify_released(live);
}
result.device(device) = t_left.contract(t_right, dims);
for (Index i = 0; i < result.size(); ++i) VERIFY_IS_APPROX(result.data()[i].v, expected.data()[i]);
verify_released(live);
}
VERIFY_IS_EQUAL(TrackedScalar::instances.load(), 0);
// A throw in the multiplication of a sequential contraction releases the packed blocks, the temporary result and
// the operands, here a nested contraction with its own temporary result. A throw in a later operand of an
// expression releases the operands evaluated before it.
{
Tensor<TrackedScalar, 2, DataLayout> t_left(9, 10);
Tensor<TrackedScalar, 2, DataLayout> t_right(10, 11);
Tensor<TrackedScalar, 2, DataLayout> t_square(11, 11);
Tensor<TrackedScalar, 2, DataLayout> result(9, 11);
Tensor<TrackedScalar, 2, DataLayout> concatenated(18, 11);
t_left.setConstant(TrackedScalar(1.0f));
t_right.setConstant(TrackedScalar(2.0f));
t_square.setConstant(TrackedScalar(1.0f));
// Runs f with the count-th multiplication on this thread throwing; returns whether it threw.
const auto throws_in_multiply = [](int count, const auto& f) {
TrackedScalar::multiply_countdown = count;
bool threw = false;
try {
f();
} catch (const std::runtime_error&) {
threw = true;
}
TrackedScalar::multiply_countdown = 0;
return threw;
};
const int live = TrackedScalar::instances.load();
TrackedScalar::pool_assignments = 0;
const int countdown = std::numeric_limits<int>::max();
TrackedScalar::multiply_countdown = countdown;
result.device(device) = t_left.contract(t_right, dims);
const int multiplications = countdown - TrackedScalar::multiply_countdown.load();
TrackedScalar::multiply_countdown = 0;
VERIFY_IS_EQUAL(TrackedScalar::pool_assignments.load(), 0);
VERIFY_IS_EQUAL(result(0, 0).v, 20.0f);
VERIFY(throws_in_multiply(1, [&]() { result.device(device) = t_left.contract(t_right, dims); }));
verify_released(live);
VERIFY(throws_in_multiply(1, [&]() { result.device(device) = t_left.contract(t_right, dims) + result; }));
verify_released(live);
// The nested contraction is the operand: it completes, then the outer contraction throws.
VERIFY(throws_in_multiply(multiplications + 1, [&]() {
result.device(device) = t_left.contract(t_right, dims).contract(t_square, dims);
}));
verify_released(live);
// A later operand throws after an earlier one completed: the earlier operand's temporary result is released.
const auto contraction = [&]() { return t_left.contract(t_right, dims); };
VERIFY(throws_in_multiply(multiplications + 1, [&]() { result.device(device) = contraction() + contraction(); }));
verify_released(live);
VERIFY(throws_in_multiply(multiplications + 1, [&]() {
result.device(device) = contraction().contract(t_square.contract(t_square, dims), dims);
}));
verify_released(live);
Tensor<bool, 2, DataLayout> t_cond(9, 11);
t_cond.setConstant(true);
VERIFY(throws_in_multiply(multiplications + 1,
[&]() { result.device(device) = t_cond.select(contraction(), contraction()); }));
verify_released(live);
VERIFY(throws_in_multiply(multiplications + 1,
[&]() { concatenated.device(device) = contraction().concatenate(contraction(), 0); }));
verify_released(live);
// A throw in the DefaultDevice executor's own loop, after the operands were evaluated, releases them. The
// ThreadPoolDevice executor is not covered: its loop can still be running on other threads when this one throws.
Eigen::DefaultDevice default_device;
TrackedScalar::multiply_countdown = countdown;
result.device(default_device) = t_left.contract(t_right, dims);
const int default_multiplications = countdown - TrackedScalar::multiply_countdown.load();
TrackedScalar::multiply_countdown = 0;
VERIFY(throws_in_multiply(default_multiplications + 1,
[&]() { result.device(default_device) = contraction() * result; }));
verify_released(live);
}
VERIFY_IS_EQUAL(TrackedScalar::instances.load(), 0);
#endif
}
template <typename = void>
void test_enqueue_with_args() {
Eigen::ThreadPool tp(2);
Eigen::ThreadPoolDevice thread_pool_device(&tp, 2);
Eigen::Barrier barrier(2);
std::atomic<int> sum(0);
thread_pool_device.enqueue([&sum](Eigen::Barrier* b, int x) { sum += x, b->Notify(); }, &barrier, 3);
thread_pool_device.enqueueNoNotification([&sum](Eigen::Barrier* b) { sum += 4, b->Notify(); }, &barrier);
barrier.Wait();
VERIFY_IS_EQUAL(sum.load(), 7);
}
// The async path constructs the forced-eval buffer's non-POD elements before evaluating into them.
template <typename = void>
void test_async_forced_eval_non_pod() {
Eigen::ThreadPool tp(4);
Eigen::ThreadPoolDevice thread_pool_device(&tp, 4);
Tensor<std::string, 1> in(64), out(64);
for (int i = 0; i < 64; ++i) in(i) = std::string(40, static_cast<char>('a' + i % 26));
Eigen::Barrier done(1);
out.device(thread_pool_device, [&done]() { done.Notify(); }) = in.eval();
done.Wait();
for (int i = 0; i < 64; ++i) VERIFY_IS_EQUAL(out(i), in(i));
}
// The single-block async path frees its scratch before the done callback, after which the device may be destroyed.
template <typename = void>
void test_async_single_block_scratch_freed_before_done() {
TestAllocator allocator;
Eigen::ThreadPool tp(4);
Eigen::ThreadPoolDevice thread_pool_device(&tp, 4, &allocator);
Tensor<float, 3> src(4, 5, 6), other(6, 5, 4), dst(6, 5, 4);
src.setRandom();
other.setRandom();
Eigen::array<Index, 3> perm{{2, 1, 0}};
// The binary op does not hand the destination buffer to the shuffle, so its block is materialized in scratch.
const auto expr = src.shuffle(perm) + other;
int deallocs_at_done = -1;
Eigen::Barrier done(1);
auto on_done = [&]() {
deallocs_at_done = allocator.dealloc_count();
done.Notify();
};
using Assign = TensorAssignOp<decltype(dst), const decltype(expr)>;
using Executor = internal::TensorAsyncExecutor<const Assign, ThreadPoolDevice, decltype(on_done),
/*Vectorizable=*/true, internal::TiledEvaluation::On>;
// With no async subexpressions and a single block, runAsync evaluates and signals on this thread.
Executor::runAsync(Assign(dst, expr), thread_pool_device, on_done);
done.Wait();
VERIFY(allocator.alloc_count() > 0);
VERIFY_IS_EQUAL(allocator.dealloc_count(), deallocs_at_done);
}
template <typename = void>
void test_multithread_random() {
Eigen::ThreadPool tp(2);
Eigen::ThreadPoolDevice device(&tp, 2);
Tensor<float, 1> t(1 << 20);
t.device(device) = t.random<Eigen::internal::NormalRandomGenerator<float>>();
}
template <int DataLayout>
void test_multithread_shuffle(Allocator* allocator) {
Tensor<float, 4, DataLayout> tensor(17, 5, 7, 11);
tensor.setRandom();
const int num_threads = internal::random<int>(2, 11);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads, allocator);
Tensor<float, 4, DataLayout> shuffle(7, 5, 11, 17);
array<ptrdiff_t, 4> shuffles = {{2, 1, 3, 0}};
shuffle.device(device) = tensor.shuffle(shuffles);
for (int i = 0; i < 17; ++i) {
for (int j = 0; j < 5; ++j) {
for (int k = 0; k < 7; ++k) {
for (int l = 0; l < 11; ++l) {
VERIFY_IS_EQUAL(tensor(i, j, k, l), shuffle(k, j, l, i));
}
}
}
}
}
template <typename = void>
void test_threadpool_allocate(TestAllocator* allocator) {
const int num_threads = internal::random<int>(2, 11);
const int num_allocs = internal::random<int>(2, 11);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads, allocator);
for (int a = 0; a < num_allocs; ++a) {
void* ptr = device.allocate(512);
device.deallocate(ptr);
}
VERIFY(allocator != nullptr);
VERIFY_IS_EQUAL(allocator->alloc_count(), num_allocs);
VERIFY_IS_EQUAL(allocator->dealloc_count(), num_allocs);
}
template <int DataLayout>
void test_multithread_zero_dim_contraction() {
const int num_threads = internal::random<int>(2, 11);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
// 2D contraction: {10, 0} * {0, 20} -> {10, 20}
{
Tensor<float, 2, DataLayout> t_left(10, 0);
Tensor<float, 2, DataLayout> t_right(0, 20);
Tensor<float, 2, DataLayout> t_result(10, 20);
t_result.setConstant(123.0f);
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
t_result.device(device) = t_left.contract(t_right, dims);
VERIFY_IS_EQUAL(t_result.dimension(0), 10);
VERIFY_IS_EQUAL(t_result.dimension(1), 20);
for (Index i = 0; i < t_result.size(); ++i) {
VERIFY_IS_EQUAL(t_result.data()[i], 0.0f);
}
}
// 2D contraction with non-empty dim >= 48: {64, 0} * {0, 64} -> {64, 64}
{
Tensor<float, 2, DataLayout> t_left(64, 0);
Tensor<float, 2, DataLayout> t_right(0, 64);
Tensor<float, 2, DataLayout> t_result(64, 64);
t_result.setConstant(123.0f);
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
t_result.device(device) = t_left.contract(t_right, dims);
VERIFY_IS_EQUAL(t_result.dimension(0), 64);
VERIFY_IS_EQUAL(t_result.dimension(1), 64);
for (Index i = 0; i < t_result.size(); ++i) {
VERIFY_IS_EQUAL(t_result.data()[i], 0.0f);
}
}
// Multidim contraction: {5, 0, 4} * {0, 3, 4} with dims {1, 0} and {2, 2} -> {5, 3}
{
Tensor<float, 3, DataLayout> t_left(5, 0, 4);
Tensor<float, 3, DataLayout> t_right(0, 3, 4);
Tensor<float, 2, DataLayout> t_result(5, 3);
t_result.setConstant(123.0f);
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 2> dims{{DimPair(1, 0), DimPair(2, 2)}};
t_result.device(device) = t_left.contract(t_right, dims);
VERIFY_IS_EQUAL(t_result.dimension(0), 5);
VERIFY_IS_EQUAL(t_result.dimension(1), 3);
for (Index i = 0; i < t_result.size(); ++i) {
VERIFY_IS_EQUAL(t_result.data()[i], 0.0f);
}
}
}
template <int DataLayout>
void test_async_multithread_zero_dim_contraction() {
const int num_threads = internal::random<int>(2, 11);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
Tensor<float, 2, DataLayout> t_left(10, 0);
Tensor<float, 2, DataLayout> t_right(0, 20);
Tensor<float, 2, DataLayout> t_result(10, 20);
t_result.setConstant(123.0f);
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
Eigen::Barrier barrier(1);
t_result.device(device, [&barrier]() { barrier.Notify(); }) = t_left.contract(t_right, dims);
barrier.Wait();
VERIFY_IS_EQUAL(t_result.dimension(0), 10);
VERIFY_IS_EQUAL(t_result.dimension(1), 20);
for (Index i = 0; i < t_result.size(); ++i) {
VERIFY_IS_EQUAL(t_result.data()[i], 0.0f);
}
}
template <typename Scalar>
struct AddBiasOutputKernel {
Scalar bias;
explicit AddBiasOutputKernel(Scalar b) : bias(b) {}
template <typename Index, typename ResScalar>
EIGEN_ALWAYS_INLINE void operator()(const internal::blas_data_mapper<ResScalar, Index, ColMajor>& output_mapper,
const TensorContractionParams&, Index, Index, Index num_rows,
Index num_cols) const {
for (Index i = 0; i < num_rows; ++i) {
for (Index j = 0; j < num_cols; ++j) {
output_mapper(i, j) += bias;
}
}
}
};
template <int DataLayout>
void test_multithread_zero_dim_contraction_with_output_kernel() {
const int num_threads = internal::random<int>(2, 11);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
const float bias = 42.0f;
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
// {10, 0} * {0, 20} -> {10, 20}
{
Tensor<float, 2, DataLayout> t_left(10, 0);
Tensor<float, 2, DataLayout> t_right(0, 20);
Tensor<float, 2, DataLayout> t_result(10, 20);
t_result.setConstant(123.0f);
t_result.device(device) = t_left.contract(t_right, dims, AddBiasOutputKernel<float>(bias));
VERIFY_IS_EQUAL(t_result.dimension(0), 10);
VERIFY_IS_EQUAL(t_result.dimension(1), 20);
for (Index i = 0; i < t_result.size(); ++i) {
VERIFY_IS_EQUAL(t_result.data()[i], bias);
}
}
// {64, 0} * {0, 64} -> {64, 64}
{
Tensor<float, 2, DataLayout> t_left(64, 0);
Tensor<float, 2, DataLayout> t_right(0, 64);
Tensor<float, 2, DataLayout> t_result(64, 64);
t_result.setConstant(123.0f);
t_result.device(device) = t_left.contract(t_right, dims, AddBiasOutputKernel<float>(bias));
VERIFY_IS_EQUAL(t_result.dimension(0), 64);
VERIFY_IS_EQUAL(t_result.dimension(1), 64);
for (Index i = 0; i < t_result.size(); ++i) {
VERIFY_IS_EQUAL(t_result.data()[i], bias);
}
}
}
template <int DataLayout>
void test_async_multithread_zero_dim_contraction_with_output_kernel() {
const int num_threads = internal::random<int>(2, 11);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
const float bias = 42.0f;
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
// {10, 0} * {0, 20} -> {10, 20}
{
Tensor<float, 2, DataLayout> t_left(10, 0);
Tensor<float, 2, DataLayout> t_right(0, 20);
Tensor<float, 2, DataLayout> t_result(10, 20);
t_result.setConstant(123.0f);
Eigen::Barrier barrier(1);
t_result.device(device, [&barrier]() { barrier.Notify(); }) =
t_left.contract(t_right, dims, AddBiasOutputKernel<float>(bias));
barrier.Wait();
VERIFY_IS_EQUAL(t_result.dimension(0), 10);
VERIFY_IS_EQUAL(t_result.dimension(1), 20);
for (Index i = 0; i < t_result.size(); ++i) {
VERIFY_IS_EQUAL(t_result.data()[i], bias);
}
}
// {64, 0} * {0, 64} -> {64, 64}
{
Tensor<float, 2, DataLayout> t_left(64, 0);
Tensor<float, 2, DataLayout> t_right(0, 64);
Tensor<float, 2, DataLayout> t_result(64, 64);
t_result.setConstant(123.0f);
Eigen::Barrier barrier(1);
t_result.device(device, [&barrier]() { barrier.Notify(); }) =
t_left.contract(t_right, dims, AddBiasOutputKernel<float>(bias));
barrier.Wait();
VERIFY_IS_EQUAL(t_result.dimension(0), 64);
VERIFY_IS_EQUAL(t_result.dimension(1), 64);
for (Index i = 0; i < t_result.size(); ++i) {
VERIFY_IS_EQUAL(t_result.data()[i], bias);
}
}
}
template <int DataLayout>
void test_multithread_zero_dim_outer_zeros() {
const int num_threads = internal::random<int>(2, 11);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
typedef Tensor<float, 1>::DimensionPair DimPair;
Eigen::array<DimPair, 1> dims{{DimPair(1, 0)}};
// Outer zero on LHS: {0, 5} * {5, 10} -> {0, 10}
{
Tensor<float, 2, DataLayout> t_left(0, 5);
Tensor<float, 2, DataLayout> t_right(5, 10);
t_right.setRandom();
Tensor<float, 2, DataLayout> t_result(0, 10);
t_result.device(device) = t_left.contract(t_right, dims);
VERIFY_IS_EQUAL(t_result.dimension(0), 0);
VERIFY_IS_EQUAL(t_result.dimension(1), 10);
VERIFY_IS_EQUAL(t_result.size(), 0);
}
// Outer zero on RHS: {10, 5} * {5, 0} -> {10, 0}
{
Tensor<float, 2, DataLayout> t_left(10, 5);
Tensor<float, 2, DataLayout> t_right(5, 0);
t_left.setRandom();
Tensor<float, 2, DataLayout> t_result(10, 0);
t_result.device(device) = t_left.contract(t_right, dims);
VERIFY_IS_EQUAL(t_result.dimension(0), 10);
VERIFY_IS_EQUAL(t_result.dimension(1), 0);
VERIFY_IS_EQUAL(t_result.size(), 0);
}
// Outer zero on both: {0, 5} * {5, 0} -> {0, 0}
{
Tensor<float, 2, DataLayout> t_left(0, 5);
Tensor<float, 2, DataLayout> t_right(5, 0);
Tensor<float, 2, DataLayout> t_result(0, 0);
t_result.device(device) = t_left.contract(t_right, dims);
VERIFY_IS_EQUAL(t_result.dimension(0), 0);
VERIFY_IS_EQUAL(t_result.dimension(1), 0);
VERIFY_IS_EQUAL(t_result.size(), 0);
}
// Async zero outer dim
{
Tensor<float, 2, DataLayout> t_left(0, 5);
Tensor<float, 2, DataLayout> t_right(5, 10);
t_right.setRandom();
Tensor<float, 2, DataLayout> t_result(0, 10);
Eigen::Barrier barrier(1);
t_result.device(device, [&barrier]() { barrier.Notify(); }) = t_left.contract(t_right, dims);
barrier.Wait();
VERIFY_IS_EQUAL(t_result.dimension(0), 0);
VERIFY_IS_EQUAL(t_result.dimension(1), 10);
VERIFY_IS_EQUAL(t_result.size(), 0);
}
}
EIGEN_DECLARE_TEST(tensor_thread_pool) {
CALL_SUBTEST_1(test_multithread_elementwise<>());
CALL_SUBTEST_1(test_async_multithread_elementwise<>());
CALL_SUBTEST_1(test_multithread_compound_assignment<>());
CALL_SUBTEST_2(test_multithread_contraction<ColMajor>());
CALL_SUBTEST_2(test_multithread_contraction<RowMajor>());
CALL_SUBTEST_2(test_multithread_gemv_transpose<ColMajor>());
CALL_SUBTEST_2(test_multithread_gemv_transpose<RowMajor>());
CALL_SUBTEST_3(test_multithread_chip<>());
CALL_SUBTEST_3(test_async_multithread_chip<>());
CALL_SUBTEST_4(test_multithread_volume_patch<>());
CALL_SUBTEST_4(test_async_multithread_volume_patch<>());
CALL_SUBTEST_4(test_multithread_tiled_stateful_functor<>());
CALL_SUBTEST_4(test_async_multithread_tiled_stateful_functor<>());
CALL_SUBTEST_5(test_multithread_contraction_agrees_with_singlethread<ColMajor>());
CALL_SUBTEST_5(test_multithread_contraction_agrees_with_singlethread<RowMajor>());
CALL_SUBTEST_5(test_multithread_contraction_with_output_kernel<ColMajor>());
CALL_SUBTEST_5(test_multithread_contraction_with_output_kernel<RowMajor>());
CALL_SUBTEST_6(test_async_multithread_contraction_agrees_with_singlethread<ColMajor>());
CALL_SUBTEST_6(test_async_multithread_contraction_agrees_with_singlethread<RowMajor>());
CALL_SUBTEST_6(test_async_contraction_eval_subexprs_dedup<ColMajor>());
CALL_SUBTEST_6(test_async_contraction_eval_subexprs_dedup<RowMajor>());
// Test EvalShardedByInnerDimContext parallelization strategy.
CALL_SUBTEST_7(test_sharded_by_inner_dim_contraction<ColMajor>());
CALL_SUBTEST_7(test_sharded_by_inner_dim_contraction<RowMajor>());
CALL_SUBTEST_7(test_sharded_by_inner_dim_contraction_with_output_kernel<ColMajor>());
CALL_SUBTEST_7(test_sharded_by_inner_dim_contraction_with_output_kernel<RowMajor>());
CALL_SUBTEST_7(test_sharded_by_inner_dim_cost_model<ColMajor>());
CALL_SUBTEST_7(test_sharded_by_inner_dim_cost_model<RowMajor>());
CALL_SUBTEST_8(test_async_sharded_by_inner_dim_contraction<ColMajor>());
CALL_SUBTEST_8(test_async_sharded_by_inner_dim_contraction<RowMajor>());
CALL_SUBTEST_8(test_async_sharded_by_inner_dim_contraction_with_output_kernel<ColMajor>());
CALL_SUBTEST_8(test_async_sharded_by_inner_dim_contraction_with_output_kernel<RowMajor>());
// Exercise various cases that have been problematic in the past.
CALL_SUBTEST_9(test_contraction_corner_cases<ColMajor>());
CALL_SUBTEST_9(test_contraction_corner_cases<RowMajor>());
CALL_SUBTEST_10(test_full_contraction<ColMajor>());
CALL_SUBTEST_10(test_full_contraction<RowMajor>());
CALL_SUBTEST_11(test_multithreaded_reductions<ColMajor>());
CALL_SUBTEST_11(test_multithreaded_reductions<RowMajor>());
CALL_SUBTEST_11(test_multithreaded_complex_reduction<>());
CALL_SUBTEST_11(test_multithreaded_value_transforming_full_reduction<>());
CALL_SUBTEST_11(test_multithreaded_argmax<>());
CALL_SUBTEST_11(test_multithreaded_complex_partial_reductions<>());
CALL_SUBTEST_12(test_memcpy<>());
CALL_SUBTEST_12(test_multithread_random<>());
CALL_SUBTEST_12(test_multithread_forced_eval_non_pod<>());
CALL_SUBTEST_12(test_enqueue_with_args<>());
CALL_SUBTEST_12(test_async_forced_eval_non_pod<>());
CALL_SUBTEST_12(test_async_single_block_scratch_freed_before_done<>());
CALL_SUBTEST_12(
test_multithread_contraction_require_initialization<ColMajor>(131, 67, 509, ContractionPath::ParallelPack));
CALL_SUBTEST_12(
test_multithread_contraction_require_initialization<RowMajor>(131, 67, 509, ContractionPath::ParallelPack));
CALL_SUBTEST_12(
test_multithread_contraction_require_initialization<ColMajor>(3, 127, 20001, ContractionPath::ThreadLocalPack));
CALL_SUBTEST_12(
test_multithread_contraction_require_initialization<RowMajor>(3, 127, 20001, ContractionPath::ThreadLocalPack));
CALL_SUBTEST_12(
test_multithread_contraction_require_initialization<ColMajor>(3, 100003, 11, ContractionPath::ShardByInnerDim));
CALL_SUBTEST_12(
test_multithread_contraction_require_initialization<RowMajor>(3, 100003, 11, ContractionPath::ShardByInnerDim));
CALL_SUBTEST_12(test_contraction_require_initialization_throws<ColMajor>());
CALL_SUBTEST_12(test_contraction_require_initialization_throws<RowMajor>());
TestAllocator test_allocator;
CALL_SUBTEST_13(test_multithread_shuffle<ColMajor>(nullptr));
CALL_SUBTEST_13(test_multithread_shuffle<RowMajor>(&test_allocator));
CALL_SUBTEST_13(test_threadpool_allocate<>(&test_allocator));
CALL_SUBTEST_14(test_multithread_zero_dim_contraction<ColMajor>());
CALL_SUBTEST_14(test_multithread_zero_dim_contraction<RowMajor>());
CALL_SUBTEST_14(test_async_multithread_zero_dim_contraction<ColMajor>());
CALL_SUBTEST_14(test_async_multithread_zero_dim_contraction<RowMajor>());
CALL_SUBTEST_14(test_multithread_zero_dim_contraction_with_output_kernel<ColMajor>());
CALL_SUBTEST_14(test_multithread_zero_dim_contraction_with_output_kernel<RowMajor>());
CALL_SUBTEST_14(test_async_multithread_zero_dim_contraction_with_output_kernel<ColMajor>());
CALL_SUBTEST_14(test_async_multithread_zero_dim_contraction_with_output_kernel<RowMajor>());
CALL_SUBTEST_14(test_multithread_zero_dim_outer_zeros<ColMajor>());
CALL_SUBTEST_14(test_multithread_zero_dim_outer_zeros<RowMajor>());
// Force CMake to split this test.
// EIGEN_SUFFIXES;1;2;3;4;5;6;7;8;9;10;11;12;13;14
}