Eigen-Contrib  5.0.1
 
Loading...
Searching...
No Matches
TensorCostModel.h
1// This file is part of Eigen, a lightweight C++ template library
2// for linear algebra.
3//
4// Copyright (C) 2016 Rasmus Munk Larsen <rmlarsen@gmail.com>
5//
6// This Source Code Form is subject to the terms of the Mozilla
7// Public License v. 2.0. If a copy of the MPL was not distributed
8// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
9// SPDX-License-Identifier: MPL-2.0
10
11#ifndef EIGEN_TENSOR_TENSOR_COST_MODEL_H
12#define EIGEN_TENSOR_TENSOR_COST_MODEL_H
13
14// IWYU pragma: private
15#include "./InternalHeaderCheck.h"
16
17namespace Eigen {
18
19// Class storing the cost of evaluating a tensor expression in terms of the
20// estimated number of operand bytes loaded, bytes stored, and compute cycles.
21class TensorOpCost {
22 public:
23 // TODO(rmlarsen): Fix the scalar op costs in Eigen proper. Even a simple
24 // model based on minimal reciprocal throughput numbers from Intel or
25 // Agner Fog's tables would be better than what is there now.
26 template <typename ArgType>
27 static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE int MulCost() {
28 return internal::functor_traits<internal::scalar_product_op<ArgType, ArgType> >::Cost;
29 }
30 template <typename ArgType>
31 static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE int AddCost() {
32 return internal::functor_traits<internal::scalar_sum_op<ArgType> >::Cost;
33 }
34 template <typename ArgType>
35 static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE int DivCost() {
36 return internal::functor_traits<internal::scalar_quotient_op<ArgType, ArgType> >::Cost;
37 }
38 template <typename ArgType>
39 static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE int ModCost() {
40 return internal::functor_traits<internal::scalar_mod_op<ArgType> >::Cost;
41 }
42 template <typename SrcType, typename TargetType>
43 static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE int CastCost() {
44 return internal::functor_traits<internal::scalar_cast_op<SrcType, TargetType> >::Cost;
45 }
46
47 constexpr EIGEN_DEVICE_FUNC TensorOpCost() = default;
48 constexpr EIGEN_DEVICE_FUNC TensorOpCost(double bytes_loaded, double bytes_stored, double compute_cycles)
49 : bytes_loaded_(bytes_loaded), bytes_stored_(bytes_stored), compute_cycles_(compute_cycles) {}
50
51 EIGEN_DEVICE_FUNC TensorOpCost(double bytes_loaded, double bytes_stored, double compute_cycles, bool vectorized,
52 double packet_size)
53 : bytes_loaded_(bytes_loaded),
54 bytes_stored_(bytes_stored),
55 compute_cycles_(vectorized ? compute_cycles / packet_size : compute_cycles) {
56 eigen_assert(bytes_loaded >= 0 && (numext::isfinite)(bytes_loaded));
57 eigen_assert(bytes_stored >= 0 && (numext::isfinite)(bytes_stored));
58 eigen_assert(compute_cycles >= 0 && (numext::isfinite)(compute_cycles));
59 }
60
61 constexpr EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE double bytes_loaded() const { return bytes_loaded_; }
62 constexpr EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE double bytes_stored() const { return bytes_stored_; }
63 constexpr EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE double compute_cycles() const { return compute_cycles_; }
64 constexpr EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE double total_bytes() const { return bytes_loaded_ + bytes_stored_; }
65 constexpr EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE double total_cost(double load_cost, double store_cost,
66 double compute_cost) const {
67 return load_cost * bytes_loaded_ + store_cost * bytes_stored_ + compute_cost * compute_cycles_;
68 }
69
70 // Drop memory access component. Intended for cases when memory accesses are
71 // sequential or are completely masked by computations.
72 EIGEN_DEVICE_FUNC void dropMemoryCost() {
73 bytes_loaded_ = 0;
74 bytes_stored_ = 0;
75 }
76
77 // TODO(rmlarsen): Define min in terms of total cost, not elementwise.
78 EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE TensorOpCost cwiseMin(const TensorOpCost& rhs) const {
79 double bytes_loaded = numext::mini(bytes_loaded_, rhs.bytes_loaded());
80 double bytes_stored = numext::mini(bytes_stored_, rhs.bytes_stored());
81 double compute_cycles = numext::mini(compute_cycles_, rhs.compute_cycles());
82 return TensorOpCost(bytes_loaded, bytes_stored, compute_cycles);
83 }
84
85 // TODO(rmlarsen): Define max in terms of total cost, not elementwise.
86 EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE TensorOpCost cwiseMax(const TensorOpCost& rhs) const {
87 double bytes_loaded = numext::maxi(bytes_loaded_, rhs.bytes_loaded());
88 double bytes_stored = numext::maxi(bytes_stored_, rhs.bytes_stored());
89 double compute_cycles = numext::maxi(compute_cycles_, rhs.compute_cycles());
90 return TensorOpCost(bytes_loaded, bytes_stored, compute_cycles);
91 }
92
93 EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE TensorOpCost& operator+=(const TensorOpCost& rhs) {
94 bytes_loaded_ += rhs.bytes_loaded();
95 bytes_stored_ += rhs.bytes_stored();
96 compute_cycles_ += rhs.compute_cycles();
97 return *this;
98 }
99
100 EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE TensorOpCost& operator*=(double rhs) {
101 bytes_loaded_ *= rhs;
102 bytes_stored_ *= rhs;
103 compute_cycles_ *= rhs;
104 return *this;
105 }
106
107 EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE friend TensorOpCost operator+(TensorOpCost lhs, const TensorOpCost& rhs) {
108 lhs += rhs;
109 return lhs;
110 }
111 EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE friend TensorOpCost operator*(TensorOpCost lhs, double rhs) {
112 lhs *= rhs;
113 return lhs;
114 }
115 EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE friend TensorOpCost operator*(double lhs, TensorOpCost rhs) {
116 rhs *= lhs;
117 return rhs;
118 }
119
120 friend std::ostream& operator<<(std::ostream& os, const TensorOpCost& tc) {
121 return os << "[bytes_loaded = " << tc.bytes_loaded() << ", bytes_stored = " << tc.bytes_stored()
122 << ", compute_cycles = " << tc.compute_cycles() << "]";
123 }
124
125 private:
126 double bytes_loaded_ = 0;
127 double bytes_stored_ = 0;
128 double compute_cycles_ = 0;
129};
130
142template <typename Device>
144 public:
145 // Scaling from Eigen compute cost to device cycles.
146 static constexpr int kDeviceCyclesPerComputeCycle = 1;
147
148 // Thread overhead in device cycles. ~8us at 3GHz.
149 // Minimum total work to justify thread pool dispatch.
150 static constexpr int kStartupCycles = 25000;
151 // Minimum work per thread to amortize dispatch and synchronization overhead.
152 static constexpr int kPerThreadCycles = 25000;
153 static constexpr int kTaskSize = 40000;
154
155 // Memory bandwidth saturation: on typical multi-socket servers, 2-6 cores
156 // saturate DRAM bandwidth. 4 is a conservative default.
157 static constexpr int kMemBandwidthSaturationThreads = 4;
158
159 // If memory_time / compute_time exceeds this ratio, the op is memory-bound.
160 // With vectorized costs (AVX2, PacketSize=8), typical ratios are:
161 // Add/Mul (2 loads + 1 store): mem/comp = 6.0
162 // FMA (3 loads + 1 store): mem/comp = 4.0
163 // ReLU max(x,0) (1 load + 1 store): mem/comp = 4.0
164 // Polynomial 3rd-order (3 loads + 1 store, 6 ops): mem/comp = 1.3
165 // Exp (1 load + 1 store, ~8 ops): mem/comp = 0.5
166 // Threshold of 2.0 cleanly separates memory-bound (>=4) from compute-bound.
167 static constexpr double kMemBoundThreshold = 2.0;
168
169 // Data sets larger than this are assumed to be DRAM-resident.
170 // Below this threshold, data is likely L2-cache-resident and benefits from
171 // high per-core L2 bandwidth, so no bandwidth saturation cap is applied.
172 static constexpr double kDramThresholdBytes = 1024.0 * 1024.0;
173
174 // Returns the number of threads in [1:max_threads] to use for
175 // evaluating an expression with the given output size and cost per
176 // coefficient.
177 static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE int numThreads(double output_size, const TensorOpCost& cost_per_coeff,
178 int max_threads) {
179 if (max_threads <= 1) return 1;
180
181 double mem = memoryTime(cost_per_coeff);
182 double comp = computeTime(cost_per_coeff);
183 double per_coeff = numext::maxi(mem, comp);
184 double total = output_size * per_coeff;
185
186 // Not enough total work to justify thread pool dispatch.
187 if (total < kStartupCycles) return 1;
188
189 // Each thread needs at least kPerThreadCycles of work to
190 // amortize dispatch and synchronization overhead.
191 double threads = total / kPerThreadCycles;
192 // Guard against integer overflow.
193 threads = numext::mini<double>(threads, GenericNumTraits<int>::highest());
194 int candidate = numext::mini(max_threads, numext::maxi<int>(1, static_cast<int>(threads)));
195
196 // Memory-bound ops on DRAM-resident data: cap at bandwidth saturation.
197 // Cache-resident data has high per-core L2 bandwidth, so no cap needed.
198 //
199 // The total-traffic proxy below overestimates the working set whenever an
200 // operand is reused (e.g. broadcasted): every output coefficient charges a
201 // fresh load, so a 1xN broadcast can show up as MxN bytes even though the
202 // live data is N. A working-set-aware estimate would need each evaluator to
203 // surface its unique-operand footprint; until that exists we accept a small
204 // bias toward over-capping for reuse-heavy expressions.
205 const int mem_bandwidth_saturation_threads = kMemBandwidthSaturationThreads;
206 if (candidate > mem_bandwidth_saturation_threads) {
207 bool is_memory_bound = (comp > 0) ? (mem / comp > kMemBoundThreshold) : (mem > 0);
208 if (is_memory_bound) {
209 double total_bytes = output_size * cost_per_coeff.total_bytes();
210 if (total_bytes > kDramThresholdBytes) {
211 candidate = numext::mini(candidate, mem_bandwidth_saturation_threads);
212 }
213 }
214 }
215 return candidate;
216 }
217
218 // taskSize assesses parallel task size.
219 // Value of 1.0 means ideal parallel task size. Values < 1.0 mean that task
220 // granularity needs to be increased to mitigate parallelization overheads.
221 static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE double taskSize(double output_size, const TensorOpCost& cost_per_coeff) {
222 return totalCost(output_size, cost_per_coeff) / kTaskSize;
223 }
224
225 // Roofline model: cost is the max of memory time and compute time.
226 static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE double totalCost(double output_size,
227 const TensorOpCost& cost_per_coeff) {
228 double mem_cost = memoryTime(cost_per_coeff);
229 double comp_cost = computeTime(cost_per_coeff);
230 return output_size * numext::maxi(mem_cost, comp_cost);
231 }
232
233 private:
234 // Effective sustained bandwidth cost in cycles/byte.
235 // ~1/16 = 0.0625 cycles/byte represents L3/DRAM streaming bandwidth
236 // on modern CPUs (~16 bytes/cycle single-core sustained throughput).
237 // For L1/L2-resident data, compute typically dominates anyway.
238 static constexpr double kByteCost = 1.0 / 16.0;
239
240 static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE double memoryTime(const TensorOpCost& cost) {
241 return cost.total_bytes() * kByteCost;
242 }
243
244 static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE double computeTime(const TensorOpCost& cost) {
245 return cost.compute_cycles() * kDeviceCyclesPerComputeCycle;
246 }
247};
248
249} // namespace Eigen
250
251#endif // EIGEN_TENSOR_TENSOR_COST_MODEL_H
A cost model used to limit the number of threads used for evaluating tensor expressions.
Definition TensorCostModel.h:143
Namespace containing all symbols from the Eigen library.