1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//! Shared parallel/serial gate thresholds for the elementwise kernel classes
//!
//! Every gated pass in the crate belongs to one of a few **cost classes**. The calibration
//! bench measures the serial/parallel crossover per class, not per call site. Declaring one
//! constant per class here keeps each calibration result in one place; the call sites import the
//! constant matching their kernel's class instead of restating the value
//!
//! The classes come in two element widths: the `f32` constants serve the neural-network layers,
//! the `f64` constants serve the classical-ML and utils modules. The crossovers differ (an f64
//! stream moves twice the bytes per element; an f64 `exp` costs more than the f32 one), so the
//! widths are calibrated separately
//!
//! The engine-specific gates stay with their engines, because their work metrics are
//! engine-specific rather than class-shared: `MatmulElem::{PAR_GEMM_MIN_FLOPS,
//! PAR_GEMV_MIN_FLOPS}` and the block/tiling constants (`crate::math::matmul`),
//! `CONV_PARALLEL_MIN_FLOPS`/`CONV_MIN_CHUNK_COLS` (im2col+GEMM engine),
//! `POOL_PARALLEL_MIN_OPS`/`POOL_MIN_CHUNK_OUT` (pooling engine), and
//! `BATCH_NORM_PARALLEL_THRESHOLD` (a per-layer analogy mapping). `metrics` keeps its
//! silhouette gate module-local on purpose - a lightweight leaf module that does not import
//! crate internals - but documents its value against the same calibration tables
// f32 classes (neural-network layers)
/// Cheap memory-bound `f32` maps: ReLU's `max(0, x)`, the dropout layers' compare-into-mask
/// thresholding, and similar one-stream copy-speed loops
///
/// In calibration the parallel path never beat serial up to 1M elements: these ops run at
/// memory bandwidth on a single core, so rayon only adds fork/join overhead. The gate sits far
/// out; at every practical tensor size this class runs serial
pub const CHEAP_MAP_PARALLEL_THRESHOLD: usize = 4_000_000;
/// Exp-dominated `f32` maps: sigmoid, tanh, and softmax (whose per-element cost is dominated by
/// the shifted `exp`)
///
/// Measured crossover bracket: 64K-128K elements
pub const EXP_MAP_PARALLEL_THRESHOLD: usize = 131_072;
/// The spatial-dropout per-channel scale: a copy-with-scale that multiplies each
/// `(batch, channel)` segment of a `[batch, channels, *spatial]` tensor by its channel's
/// inverted-dropout factor. Each element is independent (no reduction), so the gate is a pure
/// performance knob that never changes the result bits
///
/// This is the cheap-map class: a single multiply per element makes it nearly a pure memory
/// copy, which one core almost saturates up to ~1M elements, so the parallel path's fork/join
/// and allocation only pay off well past 1M - the same crossover as
/// [`CHEAP_MAP_PARALLEL_THRESHOLD`]. Measured crossover bracket 1M-4M elements (0.60x at 1M,
/// 1.13x at 4M, 2.05x at 8.4M)
pub const SPATIAL_DROPOUT_SCALE_PARALLEL_MIN_ELEMS: usize = 4_194_304;
/// Fused multi-slice `f32` updates: the optimizer kernels' parameter/gradient/moment loops,
/// which stream several arrays at once
///
/// Measured crossover bracket: 256K-1M elements
pub const FUSED_SLICE_PARALLEL_THRESHOLD: usize = 1_000_000;
/// `f32`-elements, `f64`-accumulator square-sum reductions: the clip-by-global-norm gradient
/// scan, gated per parameter tensor
///
/// Above the gate, callers must use [`crate::math::reduction::det_reduce`] (or its
/// index-range twin) rather than a bare rayon `sum`/`reduce`, whose scheduling-dependent
/// grouping makes the float result non-reproducible
///
/// Measured crossover bracket: 32K-64K elements (0.88x at 32K, 1.13x at 64K, 12.7x at 1M)
pub const SQ_SUM_F32_PARALLEL_MIN_ELEMS: usize = 65_536;
/// Naive (non-im2col) convolution loop nests: the DepthwiseConv2D forward/backward and the
/// SeparableConv2D depthwise stage, gated on estimated FLOPs
/// (`2 * batch * channels [* depth_multiplier] * out_h * out_w * kh * kw`)
///
/// Estimated by analogy, not directly calibrated: these loops cost more per FLOP than the
/// im2col+GEMM engine (whose measured crossover is ~4M FLOPs), so the crossover sits
/// proportionally lower
pub const NAIVE_CONV_PARALLEL_MIN_FLOPS: usize = 1_000_000;
// f64 classes (classical ML / utils)
/// Cheap memory-bound `f64` maps: centering, scaling, normalization, kernel-matrix centering,
/// and similar one-or-two-stream copy-speed loops, gated on the total element count
///
/// Measured crossover bracket: 1M-4.2M elements (1.95x at 4.2M) - the same far-out gate as the
/// f32 class; at typical preprocessing sizes this class runs serial
pub const CHEAP_MAP_F64_PARALLEL_THRESHOLD: usize = 4_000_000;
/// Exp-dominated `f64` maps: the logistic sigmoid and the RBF/Sigmoid kernel transforms, gated
/// on the total element count
///
/// Measured crossover bracket: 16K-32K elements, but the win at 32K is a thin 1.09x; the gate
/// sits at 65K where the win is a 1.82x. Lower than the f32 class (131K), as expected from the
/// costlier f64 `exp`
pub const EXP_MAP_F64_PARALLEL_THRESHOLD: usize = 65_536;
/// Short `f64` row scans: KMeans' per-sample arg-min over centroid projections, LDA's per-row
/// best-class pick, per-sample distance scans (DBSCAN region queries, MeanShift label
/// assignment), and similar `O(row)` per-task loops, gated on the **total elements scanned**
/// (tasks x per-task row length, including any per-element dimension multiplier)
///
/// Measured crossover bracket: 65K-262K scanned elements (1.61x at 262K)
pub const SCAN_F64_PARALLEL_MIN_ELEMS: usize = 262_144;
/// Tree-traversal tasks: per-sample root-to-leaf walks (DecisionTree and IsolationForest
/// prediction), gated on the **total node visits** (samples x walk length; for a forest,
/// samples x trees x average path length)
///
/// Measured on a synthetic depth-16 heap-layout tree (the same compare-and-jump shape):
/// crossover bracket 65K-262K node visits (6.3x at 262K)
pub const TREE_TRAVERSAL_MIN_VISITS: usize = 262_144;
/// Sort-dominated split-search tasks: DecisionTree's per-feature copy + sort + scan in
/// `find_best_split`, gated on the **total sorted elements** (node samples x features)
///
/// Measured on the same copy/sort/prefix-scan shape (8 feature tasks): crossover bracket
/// 2K-8K sorted elements (1.8x at 8K)
pub const SORT_SCAN_MIN_ELEMS: usize = 8_192;
/// `f64` sum-style reductions (sum of squares, Welford moments), gated on the element count
/// (or an equivalent work metric, e.g. samples x features for k-means' per-sample
/// centroid accumulation)
///
/// Below the gate a parallel reduction cannot win; above it, callers must use
/// [`crate::math::reduction::det_reduce`] (or its index-range twin) rather than a bare rayon
/// `sum`/`reduce`, whose scheduling-dependent grouping makes the float result non-reproducible
///
/// Measured crossover bracket: 131K-262K elements (1.24x at 262K, 3.5x at 1M)
pub const SUM_F64_PARALLEL_MIN_ELEMS: usize = 262_144;