blob: 3f6863dec642f4e30b93afe376ebf4e0d6f23116 [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 <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 {
const_cast<TestAllocator*>(this)->alloc_count_++;
return internal::aligned_malloc(num_bytes);
}
EIGEN_DEVICE_FUNC void deallocate(void* buffer) const EIGEN_OVERRIDE {
const_cast<TestAllocator*>(this)->dealloc_count_++;
internal::aligned_free(buffer);
}
int alloc_count() const { return alloc_count_; }
int dealloc_count() const { return dealloc_count_; }
private:
int alloc_count_ = 0;
int dealloc_count_ = 0;
};
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));
}
}
}
}
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));
}
}
}
}
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));
}
}
}
}
}
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));
}
}
}
}
}
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]);
}
}
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.
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]);
}
}
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]);
}
}
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]);
}
}
}
// We are triggering 'evalShardedByInnerDim' optimization.
template <int DataLayout>
static void test_sharded_by_inner_dim_contraction() {
typedef Tensor<float, 1>::DimensionPair DimPair;
const int num_threads = internal::random<int>(4, 16);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
Tensor<float, 2, DataLayout> t_left(2, 10000);
Tensor<float, 2, DataLayout> t_right(10000, 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);
typedef Map<Eigen::Matrix<float, Dynamic, Dynamic, DataLayout>> MapXf;
MapXf m_left(t_left.data(), 2, 10000);
MapXf m_right(t_right.data(), 10000, 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)}};
// compute results by separate methods
t_result.device(device) = t_left.contract(t_right, dims);
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() {
typedef Tensor<float, 1>::DimensionPair DimPair;
const int num_threads = internal::random<int>(4, 16);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
Tensor<float, 2, DataLayout> t_left(2, 10000);
Tensor<float, 2, DataLayout> t_right(10000, 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);
typedef Map<Eigen::Matrix<float, Dynamic, Dynamic, DataLayout>> MapXf;
MapXf m_left(t_left.data(), 2, 10000);
MapXf m_right(t_right.data(), 10000, 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)}};
// 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_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() {
typedef Tensor<float, 1>::DimensionPair DimPair;
const int num_threads = internal::random<int>(4, 16);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
Tensor<float, 2, DataLayout> t_left(2, 10000);
Tensor<float, 2, DataLayout> t_right(10000, 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);
typedef Map<Eigen::Matrix<float, Dynamic, Dynamic, DataLayout>> MapXf;
MapXf m_left(t_left.data(), 2, 10000);
MapXf m_right(t_right.data(), 10000, 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)}};
// compute results by separate methods
Eigen::Barrier barrier(1);
t_result.device(device, [&barrier]() { barrier.Notify(); }) = t_left.contract(t_right, dims);
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() {
typedef Tensor<float, 1>::DimensionPair DimPair;
const int num_threads = internal::random<int>(4, 16);
ThreadPool threads(num_threads);
Eigen::ThreadPoolDevice device(&threads, num_threads);
Tensor<float, 2, DataLayout> t_left(2, 10000);
Tensor<float, 2, DataLayout> t_right(10000, 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);
typedef Map<Eigen::Matrix<float, Dynamic, Dynamic, DataLayout>> MapXf;
MapXf m_left(t_left.data(), 2, 10000);
MapXf m_right(t_right.data(), 10000, 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)}};
// compute results by separate methods
Eigen::Barrier barrier(1);
t_result.device(device, [&barrier]() { barrier.Notify(); }) = t_left.contract(t_right, dims, SqrtOutputKernel());
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>
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());
}
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)));
}
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);
}
}
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]);
}
}
}
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));
}
}
}
}
}
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>());
// 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_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_complex_partial_reductions());
CALL_SUBTEST_12(test_memcpy());
CALL_SUBTEST_12(test_multithread_random());
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
}