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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Live CUDA `load_kernel` for a JIT-synthesized kernel (kernel-seam-interop
//! §5.2's device-specific step): given a [`SynthArtifact`] (PTX bytes + a
//! symbol name), load it as a CUDA module, resolve the function, and wrap it
//! as a Fuel [`KernelRef`] — the closure
//! [`crate::jit_adopt::adopt_from_response`] takes as `load_kernel`.
//!
//! ## Why this lives in fuel-dispatch, not fuel-cuda-backend
//!
//! The task shape suggests a `fuel-cuda-backend` function, but `KernelRef`
//! is defined in [`crate::kernel`] over `fuel_memory::Storage` — and
//! `fuel-memory` already depends on `fuel-cuda-backend` (its `cuda` feature
//! re-exports `CudaStorageBytes`), so the reverse dependency
//! (`fuel-cuda-backend` → `fuel-memory`/`fuel-kernel-seam`) would be a cycle.
//! `fuel-dispatch` already depends on both (optionally, behind `cuda` and
//! `jit`), so the loader lives here, gated behind both features — the actual
//! module-load + symbol-resolve step reuses
//! [`fuel_cuda_backend::CudaDevice::get_or_load_custom_func`] verbatim
//! (already exactly "load PTX as a module, resolve a symbol, cache by name,
//! return a launchable `CudaFunc`" — no new fuel-cuda-backend code needed).
//!
//! ## The closure problem: why a slot table
//!
//! [`KernelRef`] is a bare `fn` pointer with no environment — a *capturing*
//! closure over the just-loaded `CudaFunc` cannot coerce to it. So a
//! dynamically loaded PTX kernel's identity (which `CudaFunc` to launch) has
//! to live in a process-global slot, and the `KernelRef` this module hands
//! back is one of a small, fixed bank of monomorphized `dispatch_slot::<N>`
//! functions — each a genuinely distinct compiled function (its own address),
//! so `N` is baked into the *code* at compile time, not captured at runtime.
//! [`load_synth_kernel`] claims the next free slot and returns its dispatcher.
//! Slots are never freed (a `FusedOpId`'s kernel lives for the process, per
//! [`crate::runtime_fused_kernels::adopt_runtime_fused`]'s docs), so the bank
//! is sized generously; bump [`MAX_JIT_SLOTS`] + the `dispatch_table!` call if
//! a workload ever adopts more distinct JIT kernels than that in one process.
//!
//! ## Launch-marshaling scope (read before trusting this on a new region shape)
//!
//! `baracuda-cuda-emit`'s emitter chooses one of several kernel ABIs per
//! `Schedule` (`Scalar` / `Vectorized{width}` / `Strided`) — see its
//! `cuda.rs::Cuda::lower`. Only the **scalar** ABI is implemented here:
//! `(const T* in0, .., const T* inK, T* out, long long n)`, one thread per
//! output element, `n` = the output's element count. This is what
//! `emit_scalar` builds for a small, fully-contiguous, uniform-dtype,
//! parameter-free elementwise region — the common case a Fuel-chosen fusion
//! region is expected to hit. It is **not** what `Vectorized` (pointee
//! reinterpreted as `float4`/`float2`/a packed f16-pair struct, `n` counted in
//! *vector* units per `effective_count_width`) or `Strided` (extra by-value
//! shape/stride args, needed for broadcast/gather/scatter/non-contiguous
//! regions) build. Rather than launch the wrong ABI silently — the exact
//! failure mode this loader must never produce — [`load_synth_kernel`]
//! declines (a typed `Err`) any artifact whose `link.symbol` doesn't end in
//! `_scalar`. A robust version would read the FKC contract's schedule/
//! `count_unit:` metadata instead of sniffing the symbol suffix; sniffing is
//! today's honest shortcut (documented, not hidden).
//!
//! Runtime scalar `Param`s (from `AddScalar`/`MulScalar` regions) ARE
//! supported: `compile_one` maps the fused node's `Runtime { scalars }` to
//! [`OpParams::JitScalars`], and [`launch_scalar`] appends them as the
//! kernel's trailing `, float p0, float p1, …` args (the emitter's
//! `param_args` suffix — always `float`, after `long long n`, ascending),
//! narrowing each `f64` slot value to `f32` per that ABI. A param-COUNT
//! mismatch between the node and the kernel signature remains undetectable
//! from `SynthArtifact` alone (the contract's `op_params` metadata is the
//! eventual cross-check); the slot machinery keeps them aligned by
//! construction (one canonical pattern pre-order on both sides).
//!
//! ## GAP-214 increment 3 — JIT-slot reclamation, and what must stay true
//!
//! A `CudaFunc` is valid only inside the CUDA context that loaded it, so the slot
//! bank ([`slots`]) is a process-global set of context-bound kernels. Increment 3
//! reclaims a slot ([`reclaim_dead_slots`], run by [`claim_slot`] lazily when the
//! bank is full) once its device is torn down, restoring capacity. That is safe
//! ONLY while ALL of these hold — if any one changes, reclamation becomes unsafe
//! SILENTLY, so they are named as the invariant, not just as the rationale:
//!
//! 1. [`dispatch_slot`] REFUSES a launch on a `DeviceId` mismatch (increment 2).
//! 2. Same-ordinal context reuse mints a FRESH `DeviceId` (fuel-cuda-backend's
//! monotonic counter). So an old `KernelRef` dispatched through a
//! reclaimed-then-reused slot index carries the OLD id while the new occupant
//! carries the NEW id, and (1) refuses it — the ordering constraint, enforced
//! by inc 2 rather than by reclamation timing.
//! 3. The liveness signal (`fuel_cuda_backend::is_device_live`) is
//! `DeviceId`-keyed, NOT ordinal-keyed — an ordinal-keyed signal would report a
//! live successor's slot reclaimable.
//! 4. baracuda's `StreamInner` holds its `Context` ("outlives the stream"), so
//! dropping a reclaimed `CudaFunc` drops stream-then-context — no
//! `cuStreamDestroy` in a dead context.
//! 5. Lock order is bank → `LIVE_DEVICES`: `claim_slot` holds the bank write lock
//! THEN calls `is_device_live`. Nothing holding `LIVE_DEVICES` may take the
//! bank lock (`DeviceLiveness::drop` honours this — it touches only the set).
//! 6. Reclamation of a slot is IMPOSSIBLE while any storage from its device is
//! live: storage holds a device clone, and liveness is clone-keyed, so the
//! device reads live until its last clone (storage included) drops. This is why
//! the "operands on the dead device" hazard is unreachable rather than untested,
//! and why the born-red's cross-context arm uses a foreign LIVE device. It is
//! also what makes clone-keying stronger than merely not-the-`Arc<CudaDevice>`
//! leak the ruling rejected: it makes the dangerous case structurally
//! impossible, not just bounded.
use ;
use ;
use ;
use ;
use ;
use crate;
/// Size of the process-global JIT-kernel slot bank. See the module docs'
/// "why a slot table" section for why this exists at all.
const MAX_JIT_SLOTS: usize = 64;
/// One claimed slot: the loaded+resolved `CudaFunc`, its entry point (carried
/// for diagnostics in launch-time error messages), and the identity of the CUDA
/// device (context) that loaded it. **GAP-214**: a `CudaFunc` is meaningful only
/// inside the context it was loaded in, so the slot records which device that
/// was; [`dispatch_slot`] refuses to launch it against operands on any other.
/// Load `art` as a CUDA module on `device`, resolve `art.link.symbol`, and
/// wrap it as a [`KernelRef`] — the `load_kernel` closure
/// [`crate::jit_adopt::adopt_from_response`] needs. See the module docs for
/// the scalar-ABI-only scope this implements.
///
/// # Errors
/// - `art.kind` isn't [`ArtifactKind::Ptx`] (Cubin loading would need
/// `Module::load_raw`, not wired here).
/// - `art.artifact` isn't valid UTF-8 PTX text.
/// - `art.link.symbol` doesn't look like a scalar-schedule kernel (see the
/// module docs' launch-marshaling scope) — an honest decline rather than a
/// silent wrong-ABI launch.
/// - the driver fails to load the module / resolve the symbol
/// ([`CudaDevice::get_or_load_custom_func`]'s errors).
/// - the slot bank ([`MAX_JIT_SLOTS`]) is full.
/// Claim the next free slot in the bank and return its dispatcher.
/// GAP-214 increment 3: free every slot whose recorded device is no longer live,
/// returning the freed indices. See the module docs' invariants — reclamation is
/// safe only because inc 2 refuses on `DeviceId` mismatch, same-ordinal reuse
/// mints a fresh `DeviceId`, the liveness signal is `DeviceId`-keyed (not
/// ordinal), and dropping a reclaimed `CudaFunc` drops its stream then its
/// context in that order (baracuda's `StreamInner` holds its `Context`), so no
/// `cuStreamDestroy` in a dead context. And it can only ever fire on a dead
/// device: reclamation is impossible while any storage from that device is live,
/// because storage holds a device clone and liveness is clone-keyed.
///
/// The caller holds the slot-bank write lock; this then takes `LIVE_DEVICES` via
/// `is_device_live` (invariant 5: bank → LIVE_DEVICES, never the reverse — so
/// nothing holding `LIVE_DEVICES` may take the bank lock).
/// Claim the next free slot in the bank and return its dispatcher.
/// Test hook (GAP-214 inc 3): run the same reclamation `claim_slot` runs when the
/// bank is full, returning the freed slot indices — so a born-red can assert not
/// just HOW MANY slots were reclaimed but WHICH (a correct count could hide
/// reclaiming the wrong slot).
/// Test hook: the recorded `DeviceId` of each slot (`None` = free), so a born-red
/// can verify a specific slot's occupant before/after reclamation and reuse.
/// Test hook: drop every slot, resetting the bank to empty. Live-GPU born-reds
/// run serially (gpu-run) and are invoked `--exact`, so no other bank user races
/// this; it exists so a test can start from a known-empty bank.
/// The output element count for the scalar-ABI launch: prefer the output
/// layout Fuel passed (the [`KernelRef`] contract's `layouts[inputs.len()]`);
/// fall back to the output storage's raw byte length / dtype size when no
/// layout was supplied (mirrors `fuel_cuda_backend::baracuda::elementwise`'s
/// same fallback for binding-table callers that haven't migrated to
/// layout-passing).
/// GAP-214 increment 2: a JIT slot's `CudaFunc` is launchable only inside the
/// CUDA context that loaded it. Refuse — a typed `Err`, never a panic, never a
/// launch — if an operand is on a different device than the slot recorded. A
/// torn-down context reuses an ordinal with a FRESH [`DeviceId`] (ids are minted
/// from a monotonic counter), so this catches same-ordinal-different-context,
/// not only cross-ordinal. Called on EVERY operand (each input and the output):
/// a mixed-device operand set is the exact multi-session hazard this guards, and
/// checking one operand would make the guard's claim false for the rest.
/// The scalar-ABI launch shared by every slot dispatcher: a pointer arg for
/// each input (in order), the output pointer, then the output element count
/// as `long long` — exactly the parameter list `baracuda-cuda-emit`'s
/// `emit_scalar` builds. Modeled on `CudaUgIOp1::fwd`
/// (`fuel-cuda-backend/src/ug.rs`) — the crate's other "launch a `CudaFunc`
/// against Fuel storage" site — adapted from one typed `CudaSlice<T>` arg to N
/// byte-erased `CudaStorageBytes` buffers: a synth kernel is dtype-generic at
/// this Rust call boundary (the PTX signature carries the concrete C type;
/// CUDA kernel params are addresses regardless), so the launch only ever
/// pushes the raw device pointer via `&DeviceBuffer<u8>`'s `KernelArg` impl.
/// One dispatcher per slot — a genuinely distinct `fn` item per `N` (via
/// monomorphization), so a bare-`fn`-pointer `KernelRef` can carry a
/// runtime-loaded kernel's identity without a captured environment. See the
/// module docs' "why a slot table" section.
static DISPATCH_TABLE: = dispatch_table!;
// No unit tests in this module: every code path that runs before a live
// device call (the `ArtifactKind`/`_scalar`-symbol declines) still requires a
// `&CudaDevice` argument, and this crate's live-CUDA convention (see
// `tests/baracuda_*_live.rs`) is that anything constructing a `CudaDevice`
// lives in an `#[ignore]`'d integration test, never a always-run unit test —
// see `tests/jit_synth_kernel_live.rs` for the on-device coverage.