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
249
250
251
252
253
254
255
256
257
258
259
260
261
//! What a promoted parallel region carries from the thread that
//! submitted it, and which backends may be promoted at all.
//!
//! # The problem this exists for (GitHub issue #166)
//!
//! [`super::on_workers`] moves a whole forward pass onto a rayon worker
//! so its ~150 cold pool entries collapse into one. Moving work between
//! threads is free only for work that reads nothing off the thread it
//! runs on, and a Metal decode step is not that: `ferrox-cli` and
//! `ferrox-server` each decide per request whether the stack may fold
//! `final_norm + lm_head + argmax` into its own command buffer, and
//! they announce that decision into a THREAD-LOCAL.
//!
//! Measured on an M2 Pro, `Llama-3.2-3B-Instruct-Q4_K_M --ngl 99`,
//! `--temp 0 --no-cnv`: a step driven on a worker read the default
//! instead of the announcement and silently took the other `lm_head`
//! path, giving a different completion from the tenth token. The
//! workaround was to decline promotion under any GPU backend, which
//! cost Metal the whole scheduling win.
//!
//! # The shape of the fix
//!
//! [`Carry`] is the ONE list of thread-local *settings* a moved step
//! reads. It is captured by value on the submitting thread and adopted
//! on the worker for exactly the length of the job.
//!
//! [`Carry::adopt`] destructures `self` exhaustively, with no `..`, so a
//! field added to `Carry` and not adopted does not compile. That is the
//! enforcement, and it is the point: this repo's dominant defect is two
//! structures that must agree with nothing making them.
//!
//! # Settings, not caches
//!
//! Only state a CALLER configured belongs here. `ferrox-metal`'s other
//! thread-locals -- `TL_PIPELINE_CACHE`, `TL_WEIGHT_CACHE`,
//! `TL_F32_CACHE`, `TL_MOE_PACKED`, `TL_MOE_LAYER_RESIDENT` -- are
//! per-thread mirrors of process-wide `Mutex` caches holding `Arc`s of
//! the same GPU buffers, and `TL_MOE_PREFILL` / `MOE_SCRATCH` are
//! per-thread scratch. A fresh thread rebuilds a mirror or allocates
//! scratch; neither changes a value, and the mirrors clone `Arc`s
//! rather than GPU memory, so a worker does not duplicate weights. The
//! one hand-off that used to carry a value across a step, the resident
//! activation, no longer lives on a thread at all: it is a host address
//! recorded inside `DecodeScratch`, under the process mutex that owns
//! the buffer it describes (`ferrox_metal::resident_act`).
//!
//! So `Carry` has exactly one field, and the reason there is only one
//! is written down rather than assumed.
//!
//! # What losing the setting costs TODAY, which is not what it cost then
//!
//! Worth stating, because the before/after evidence for relaxing the
//! gate is "bit-identical" and issue #166's evidence was "different from
//! the tenth token", and those look contradictory.
//!
//! Issue #170 landed in between. It split the permission to fold into
//! two predicates and made the device argmax legal only where the host
//! sampler would have chosen the same id anyway, so the folded and
//! unfolded paths now agree by construction WHENEVER the fold is
//! permitted. A worker that loses the setting therefore falls back to a
//! correct answer computed the slow way -- it downloads a whole
//! vocabulary and argmaxes on the host instead of downloading four
//! bytes.
//!
//! Measured with the carry deleted from [`super::on_workers`] and
//! nothing else changed: on `Llama-3.2-3B-Instruct-Q4_K_M --ngl 99
//! --temp 0 --repeat-penalty 1.0` the step still ran on a worker and the
//! completion was byte-for-byte the same, but the fold went from `On` to
//! `Unset` and stopped firing. That is the whole of what this module
//! buys now, and it is why the enforcement above matters more than the
//! one field it currently guards: the NEXT setting to be added has no
//! guarantee of degrading so kindly.
use crateBackend;
/// The thread-local settings a moved forward pass reads.
///
/// Captured with [`Self::capture`] on the thread whose caller made the
/// decisions, adopted with [`Self::adopt`] on the thread that runs the
/// work.
pub
/// Restores every setting [`Carry::adopt`] installed, on drop.
pub
/// Whether [`super::on_workers`] may move a step running on `backend`
/// onto a rayon worker.
///
/// Exhaustive with no `_` arm, so a new backend cannot be added without
/// stating its verdict, and the only honest way to write `true` is to
/// have made [`Carry`] reproduce everything that backend reads off the
/// submitting thread AND to have checked on hardware that its output is
/// unchanged.
///
/// The verdicts, and what each rests on:
///
/// - **`Cpu`** -- nothing thread-affine. Token-identical under the move
/// at 135M, 3B and 8B (issue #167).
/// - **`Metal`** -- the one setting is carried by [`Carry`], and the
/// remaining `ferrox-metal` thread-locals are caches, mirrors and
/// scratch (this module's header lists them). Verified bit-identical
/// before and after with `ferrox verify` and `ferrox parity` on a
/// dense Llama, a sandwich-norm Gemma-2 and an OLMoE MoE. On a build
/// without the `metal` feature `active_backend` cannot return this,
/// so the arm is unreachable rather than wrong there.
/// - **`Cuda`** -- audited clean and NOT promoted. `ferrox-cuda` has no
/// thread-locals at all: the device handle, the loaded-module set and
/// the weight cache are process-wide `Mutex`es, and cudarc binds the
/// primary context to whichever thread calls it. The audit is not the
/// evidence this repo asks for, though; a GPU behaviour change merges
/// after it has been RUN on that GPU, and this development machine
/// has none. Flipping this row is a one-line change for whoever has
/// the hardware, plus the same before/after check Metal got.
/// - **`Vulkan`** -- one kernel, reached through MoltenVK, never
/// measured under promotion. Same rule as CUDA.
pub