blob: c6fc9e52ba5d1a4877bc1b6f2124049b1a9725c9 [file] [edit]
// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// Copyright (C) 2026 The Eigen Authors.
//
// 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
#include "main.h"
#include <Eigen/Tensor>
using Eigen::DefaultDevice;
using Eigen::Tensor;
using Eigen::TensorEvaluator;
// Regression: TensorCwiseNullaryOp must report bytes_loaded == 0.
// NullaryOps (constants, Zero, Identity, Random, sequence generators)
// produce values from registers or minimal state without loading from
// memory. If they reported nonzero bytes_loaded, expressions dominated
// by constants (e.g. Horner-form polynomials) would be misclassified as
// memory-bound and the threadpool cost model would over-restrict
// parallelism. See TensorEvaluator.h, the CwiseNullaryOp specialization
// of costPerCoeff().
template <typename Scalar>
static void test_nullary_zero_bytes_loaded() {
Tensor<Scalar, 1> shape(/*size=*/16);
auto zeros = shape.constant(Scalar(0));
auto sevens = shape.constant(Scalar(7));
using ZeroEval = TensorEvaluator<const decltype(zeros), DefaultDevice>;
using ConstEval = TensorEvaluator<const decltype(sevens), DefaultDevice>;
DefaultDevice device;
ZeroEval zero_eval(zeros, device);
ConstEval const_eval(sevens, device);
for (bool vectorized : {false, true}) {
const auto zero_cost = zero_eval.costPerCoeff(vectorized);
const auto const_cost = const_eval.costPerCoeff(vectorized);
VERIFY_IS_EQUAL(zero_cost.bytes_loaded(), 0.0);
VERIFY_IS_EQUAL(zero_cost.bytes_stored(), 0.0);
VERIFY_IS_EQUAL(const_cost.bytes_loaded(), 0.0);
VERIFY_IS_EQUAL(const_cost.bytes_stored(), 0.0);
}
}
// A volatile pointer forces odr-use even when optimization could fold the value.
template <typename T>
T read_through_address(const T& value) {
const T* volatile address = &value;
return *address;
}
static void test_cost_model_constants_odr_use() {
using Cost = Eigen::TensorCostModel<DefaultDevice>;
VERIFY_IS_EQUAL(read_through_address(Cost::kDeviceCyclesPerComputeCycle), 1);
VERIFY_IS_EQUAL(read_through_address(Cost::kStartupCycles), 100000);
VERIFY_IS_EQUAL(read_through_address(Cost::kPerThreadCycles), 50000);
VERIFY_IS_EQUAL(read_through_address(Cost::kTaskSize), 40000);
VERIFY_IS_EQUAL(read_through_address(Cost::kMemBandwidthSaturationThreads), 4);
VERIFY_IS_EQUAL(read_through_address(Cost::kMemBoundThreshold), 2.0);
VERIFY_IS_EQUAL(read_through_address(Cost::kDramThresholdBytes), 1024.0 * 1024.0);
}
template <typename Scalar>
struct scalar_only_unary_op {
EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Scalar operator()(const Scalar& x) const { return -x; }
};
template <typename Scalar>
struct scalar_only_binary_op {
EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Scalar operator()(const Scalar& x, const Scalar& y) const { return x + y; }
};
template <typename Scalar>
struct scalar_only_ternary_op {
EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Scalar operator()(const Scalar& x, const Scalar& y, const Scalar& z) const {
return x + y + z;
}
};
namespace Eigen {
namespace internal {
template <typename Scalar>
struct functor_traits<scalar_only_unary_op<Scalar>> {
enum { Cost = 16, PacketAccess = false };
};
template <typename Scalar>
struct functor_traits<scalar_only_binary_op<Scalar>> {
enum { Cost = 16, PacketAccess = false };
};
template <typename Scalar>
struct functor_traits<scalar_only_ternary_op<Scalar>> {
enum { Cost = 16, PacketAccess = false };
};
} // namespace internal
} // namespace Eigen
template <typename Scalar>
static void test_unary_binary_ternary_block_resource_cost() {
Tensor<Scalar, 2> a(64, 64);
Tensor<Scalar, 2> b(64, 64);
Tensor<Scalar, 2> c(64, 64);
a.setRandom();
b.setRandom();
c.setRandom();
DefaultDevice device;
auto unary_expr = a.unaryExpr(scalar_only_unary_op<Scalar>());
using UnaryEval = TensorEvaluator<const decltype(unary_expr), DefaultDevice>;
STATIC_CHECK(!UnaryEval::PacketAccess);
UnaryEval unary_eval(unary_expr, device);
VERIFY_IS_EQUAL(unary_eval.getResourceRequirements().cost_per_coeff.compute_cycles(), 16.0);
auto binary_expr = a.binaryExpr(b, scalar_only_binary_op<Scalar>());
using BinaryEval = TensorEvaluator<const decltype(binary_expr), DefaultDevice>;
STATIC_CHECK(!BinaryEval::PacketAccess);
BinaryEval binary_eval(binary_expr, device);
VERIFY_IS_EQUAL(binary_eval.getResourceRequirements().cost_per_coeff.compute_cycles(), 16.0);
Eigen::TensorCwiseTernaryOp<scalar_only_ternary_op<Scalar>, const decltype(a), const decltype(b), const decltype(c)>
ternary_expr(a, b, c, scalar_only_ternary_op<Scalar>());
using TernaryEval = TensorEvaluator<const decltype(ternary_expr), DefaultDevice>;
STATIC_CHECK(!TernaryEval::PacketAccess);
TernaryEval ternary_eval(ternary_expr, device);
VERIFY_IS_EQUAL(ternary_eval.getResourceRequirements().cost_per_coeff.compute_cycles(), 16.0);
}
template <typename Scalar>
static void test_shuffling_cost_and_block_requirements() {
Tensor<Scalar, 2> a(256, 256);
a.setRandom();
Eigen::array<Eigen::Index, 2> perm = {1, 0};
auto shuffle_expr = a.shuffle(perm);
using ShuffleEval = TensorEvaluator<const decltype(shuffle_expr), DefaultDevice>;
DefaultDevice device;
ShuffleEval eval(shuffle_expr, device);
const auto scalar_cost = eval.costPerCoeff(/*vectorized=*/false);
const auto vec_cost = eval.costPerCoeff(/*vectorized=*/true);
if (ShuffleEval::PacketAccess && ShuffleEval::PacketSize > 1) {
VERIFY(vec_cost.compute_cycles() < scalar_cost.compute_cycles());
}
const auto req = eval.getResourceRequirements();
VERIFY(req.shape_type == Eigen::internal::TensorBlockShapeType::kUniformAllDims);
VERIFY_IS_EQUAL(req.cost_per_coeff.bytes_loaded(), static_cast<double>(sizeof(Scalar)));
VERIFY_IS_EQUAL(req.cost_per_coeff.bytes_stored(), static_cast<double>(sizeof(Scalar)));
VERIFY_IS_EQUAL(req.cost_per_coeff.compute_cycles(), 1.0);
// Small 256x256 2D transpose should stay single-threaded rather than paying
// thread-pool dispatch and barrier overhead.
const int threads_256 =
Eigen::TensorCostModel<DefaultDevice>::numThreads(256.0 * 256.0, req.cost_per_coeff, /*max_threads=*/8);
VERIFY_IS_EQUAL(threads_256, 1);
// Larger 1024x1024 2D transpose should parallelize across threads.
const int threads_1024 =
Eigen::TensorCostModel<DefaultDevice>::numThreads(1024.0 * 1024.0, req.cost_per_coeff, /*max_threads=*/8);
VERIFY(threads_1024 > 1);
}
EIGEN_DECLARE_TEST(tensor_cost_model) {
CALL_SUBTEST(test_cost_model_constants_odr_use());
CALL_SUBTEST(test_nullary_zero_bytes_loaded<float>());
CALL_SUBTEST(test_nullary_zero_bytes_loaded<double>());
CALL_SUBTEST(test_nullary_zero_bytes_loaded<int>());
CALL_SUBTEST(test_unary_binary_ternary_block_resource_cost<float>());
CALL_SUBTEST(test_unary_binary_ternary_block_resource_cost<double>());
CALL_SUBTEST(test_shuffling_cost_and_block_requirements<float>());
CALL_SUBTEST(test_shuffling_cost_and_block_requirements<double>());
}