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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//! Which CPU scheduler one operation's parallel regions run on, and the
//! work quantity that decides it.
//!
//! `FERROX_CPU_POOL=spin` is **+123% at 3B and +87% at 8B** on aarch64
//! and takes decode past llama.cpp; it is **-37% at 135M** on the same
//! host (#27, and the aarch64 table in
//! `docs/plans/cpu-cuda-parity.md`). A default that is right at 3B is
//! wrong at 135M and vice versa, so the switch cannot flip and cannot
//! stay: it needs a rule, and the rule has to be about the operation
//! rather than the process. That is step 3 of
//! `docs/plans/cpu-cuda-parity.md`.
//!
//! [`backend`] is that rule, and it is the ONLY place either scheduler
//! is chosen. Every helper in [`crate::par`] calls it, so no call site
//! can grow its own opinion: `weight_matrix` had four spellings of one
//! GPU-router eligibility test and they drifted four ways, which is the
//! failure this module exists to make impossible.
//!
//! # What deciding per operation costs, and has not been measured
//!
//! Deciding per operation means a single token can use both schedulers:
//! the FFN projections over the crossover go to the pool while
//! attention, the norms and the narrow projections fork. Both sets of
//! workers are then alive at once, and the pool workers spin for
//! `FERROX_CPU_POOL_SPIN_US` before parking. Whether that costs
//! anything is exactly the question a sweep on a quiet host answers and
//! reading this file does not. `FERROX_CPU_POOL=rayon` is the revert,
//! and it restores the previous behaviour exactly rather than
//! approximately.
use Cell;
use Backend;
/// Multiply-accumulates below which one operation's parallel regions are
/// worth less than the persistent pool's wake-up, so they fork with
/// rayon instead.
///
/// **This value is BRACKETED by measurement, not measured.** What is
/// measured (#27, `docs/CONFIG.md`, quiet rented hosts, 2026-09-04) is
/// per MODEL, not per operation:
///
/// | host | 135M | 3B | 8B |
/// |---|---|---|---|
/// | 20-core Cortex-A725 (aarch64) | **-37%** | +123% | +87% |
/// | 10-core Xeon E5-2630 v4 (x86) | +49% | +23% | +15% |
///
/// Turning that into a per-operation number is arithmetic over the
/// shapes, not a sweep, and nobody has run the sweep:
///
/// | model | widest decode matvec | narrowest |
/// |---|---|---|
/// | SmolLM2-135M (576 / 1536) | 576 x 1536 = 0.88M | 576 x 576 = 0.33M |
/// | Llama-3.2-3B (3072 / 8192) | 3072 x 8192 = 25.2M | 3072 x 1024 = 3.1M |
/// | Llama-3.1-8B (4096 / 14336) | 4096 x 14336 = 58.7M | 4096 x 1024 = 4.2M |
///
/// So every operation in the model where the pool LOST is under 0.9M and
/// every operation in the models where it WON is over 3.1M. `1 << 21`
/// (2.1M) is the middle of that bracket in log space. The true crossover
/// is somewhere in `0.9M ..= 3.1M` and this constant is a guess inside
/// it; the exit criterion in `docs/plans/cpu-cuda-parity.md` asks for a
/// sweep on aarch64 AND x86, and that sweep is still owed.
///
/// One constant, both architectures. Above it the pool won on both;
/// below it the pool lost on aarch64 and WON on x86 (+49% at 135M), so
/// forking below the crossover leaves a measured x86 win unclaimed.
/// That is deliberate: an architecture-conditional default is what
/// `FERROX_CPU_INT_DOT` was, and it cost x86 between 4x and 8.8x of
/// decode before anyone noticed
/// (`weight_matrix::int_dot_is_a_win_here`). Claiming the x86 small-
/// model win needs the sweep, not a second `cfg!`.
///
/// `FERROX_CPU_POOL` stays as the A/B override precisely because of
/// all that: `rayon` restores the pre-rule behaviour exactly and `spin`
/// forces the pool at every size, so bracketing the constant is one
/// environment variable rather than two builds.
pub const SPIN_MIN_OP_MACS: usize = 1 << 21;
/// What `FERROX_CPU_POOL` pins, if anything. Read once, cached.
///
/// - `spin` / `persistent` / `1` / `on` / `true` — the pool, at every size
/// - `rayon` / `0` / `off` / `false` — fork-join, at every size
/// - unset / anything else — no pin: [`backend`] decides per operation
pub
thread_local!
/// Publish the shape of the operation `f` performs: `rows` output rows,
/// each dotting `macs_per_row` elements.
///
/// Restores the previous value, so nesting is safe.
/// Total multiply-accumulates of the published operation; `0` when
/// nothing published one.
/// Multiply-accumulates per output row of the published operation; `0`
/// when nothing published one. Read by
/// [`crate::weight_matrix::WeightMatrix::min_rows_per_task`].
/// **The one predicate.** Which scheduler the parallel regions of the
/// operation now being set up run on.
///
/// An unpublished operation (`op_macs() == 0`) reads as small and forks
/// with rayon. That is deliberate: attention, sampling and the norm
/// kernels do not publish a matrix shape, and their regions are the ones
/// the -37% at 135M is made of.