Skip to main content

xlog_prob/compilation/
gpu_cache.rs

1//! GPU-resident circuit cache helpers.
2
3use std::sync::Arc;
4
5use cudarc::driver::LaunchConfig;
6use xlog_core::{Result, XlogError};
7use xlog_cuda::memory::TrackedCudaSlice;
8use xlog_cuda::provider::{cache_kernels, CACHE_MODULE};
9use xlog_cuda::{AsKernelParam, CudaKernelProvider, LaunchAsync};
10use xlog_solve::GpuCnf;
11
12use super::disk_cache;
13use crate::gpu::GpuXgcf;
14
15/// Configuration for the GPU-resident circuit cache.
16///
17/// Controls the number of cached XGCF circuit slots and the per-slot capacity
18/// limits for nodes, edges, levels, and variables. Production callers should
19/// use [`crate::exact::default_cache_config`] which sizes caps from the CNF
20/// and compile config.
21#[derive(Debug, Clone, Copy)]
22#[non_exhaustive]
23pub struct GpuCircuitCacheConfig {
24    /// Number of circuit slots kept resident on the GPU.
25    pub num_slots: u32,
26    /// Hash table size for the circuit lookup (should be >= 2 * num_slots).
27    pub table_size: u32,
28    /// Maximum nodes per cached circuit.
29    pub node_cap: u32,
30    /// Maximum edges per cached circuit.
31    pub edge_cap: u32,
32    /// Maximum levels (BFS depth) per cached circuit.
33    pub level_cap: u32,
34    /// Maximum CNF variable id (1-based, DIMACS) across all cached circuits.
35    pub var_cap: u32,
36}
37
38impl Default for GpuCircuitCacheConfig {
39    /// Conservative defaults for small CNFs (< 64 variables).
40    ///
41    /// Production callers should derive caps from the actual CNF dimensions.
42    fn default() -> Self {
43        Self {
44            num_slots: 4,
45            table_size: 8,
46            node_cap: 65_536,
47            edge_cap: 131_072,
48            level_cap: 65_536,
49            var_cap: 128,
50        }
51    }
52}
53
54fn cache_grid_dim_for_u32_count(context: &str, count: u32, block_dim: u32) -> Result<u32> {
55    if count == 0 {
56        return Ok(0);
57    }
58    if block_dim == 0 {
59        return Err(XlogError::Compilation(format!(
60            "{context}: GPU cache block size must be nonzero"
61        )));
62    }
63    let padded = count
64        .checked_add(block_dim - 1)
65        .ok_or_else(|| XlogError::Compilation(format!("{context}: GPU cache grid overflow")))?;
66    Ok(padded / block_dim)
67}
68
69fn cache_grid_dim_for_u64_count(context: &str, count: u64, block_dim: u32) -> Result<u32> {
70    if count == 0 {
71        return Ok(0);
72    }
73    if block_dim == 0 {
74        return Err(XlogError::Compilation(format!(
75            "{context}: GPU cache block size must be nonzero"
76        )));
77    }
78    let block = block_dim as u64;
79    let grid = count
80        .checked_add(block - 1)
81        .map(|padded| padded / block)
82        .ok_or_else(|| XlogError::Compilation(format!("{context}: GPU cache grid overflow")))?;
83    u32::try_from(grid)
84        .map_err(|_| XlogError::Compilation(format!("{context}: GPU cache grid exceeds u32")))
85}
86
87/// Fixed-capacity device-resident cache of compiled circuits and evaluation state.
88pub struct GpuCircuitCache {
89    provider: Arc<CudaKernelProvider>,
90    table_size: u32,
91    num_slots: u32,
92    node_cap: u32,
93    edge_cap: u32,
94    level_cap: u32,
95    var_cap: u32,
96    keys: TrackedCudaSlice<u64>,
97    slots: TrackedCudaSlice<u32>,
98    state: TrackedCudaSlice<u32>,
99    last_used: TrackedCudaSlice<u64>,
100    slot_states: TrackedCudaSlice<u32>,
101    clock: TrackedCudaSlice<u64>,
102    node_type: TrackedCudaSlice<u8>,
103    child_offsets: TrackedCudaSlice<u32>,
104    child_indices: TrackedCudaSlice<u32>,
105    lit: TrackedCudaSlice<i32>,
106    decision_var: TrackedCudaSlice<u32>,
107    decision_child_false: TrackedCudaSlice<u32>,
108    decision_child_true: TrackedCudaSlice<u32>,
109    level_nodes: TrackedCudaSlice<u32>,
110    level_offsets: TrackedCudaSlice<u32>,
111    var_log_true: TrackedCudaSlice<f64>,
112    var_log_false: TrackedCudaSlice<f64>,
113    values: TrackedCudaSlice<f64>,
114    adj: TrackedCudaSlice<f64>,
115    grad_true: TrackedCudaSlice<f64>,
116    grad_false: TrackedCudaSlice<f64>,
117    meta_num_nodes: TrackedCudaSlice<u32>,
118    meta_num_levels: TrackedCudaSlice<u32>,
119    meta_root: TrackedCudaSlice<u32>,
120    meta_max_var: TrackedCudaSlice<u32>,
121    always_on: TrackedCudaSlice<u32>,
122    zero_f64: TrackedCudaSlice<f64>,
123    one_f64: TrackedCudaSlice<f64>,
124    free_var_mask: TrackedCudaSlice<u8>,
125    has_free_var_mask: Vec<bool>,
126}
127
128/// Device-resident result of a cache lookup or slot claim.
129pub struct GpuCacheLookup {
130    provider: Arc<CudaKernelProvider>,
131    slot: TrackedCudaSlice<u32>,
132    compile_needed: TrackedCudaSlice<u32>,
133}
134
135impl GpuCacheLookup {
136    /// Returns the selected cache slot as a device scalar.
137    pub fn slot_device(&self) -> &TrackedCudaSlice<u32> {
138        &self.slot
139    }
140
141    /// Returns the device flag indicating whether the slot requires compilation.
142    pub fn compile_needed_device(&self) -> &TrackedCudaSlice<u32> {
143        &self.compile_needed
144    }
145
146    /// Returns the CUDA provider that owns the lookup buffers.
147    pub fn provider(&self) -> &Arc<CudaKernelProvider> {
148        &self.provider
149    }
150
151    /// Resolves device metadata and converts this lookup into a cache handle.
152    pub fn into_handle(self) -> Result<GpuCircuitCacheHandle> {
153        let slot_host_vec: Vec<u32> = self
154            .provider
155            .device()
156            .inner()
157            .dtoh_sync_copy(&self.slot)
158            .map_err(|e| XlogError::Kernel(format!("dtoh slot index: {}", e)))?;
159        Ok(GpuCircuitCacheHandle {
160            provider: self.provider,
161            slot: self.slot,
162            compile_needed: self.compile_needed,
163            slot_host: slot_host_vec[0],
164            num_nodes: 0,
165            num_levels: 0,
166            root: 0,
167            max_var: 0,
168        })
169    }
170}
171
172/// Host-resolved handle to one claimed GPU circuit-cache slot.
173pub struct GpuCircuitCacheHandle {
174    provider: Arc<CudaKernelProvider>,
175    slot: TrackedCudaSlice<u32>,
176    compile_needed: TrackedCudaSlice<u32>,
177    slot_host: u32,
178    num_nodes: u32,
179    num_levels: u32,
180    root: u32,
181    max_var: u32,
182}
183
184impl GpuCircuitCacheHandle {
185    /// Returns the selected cache slot as a device scalar.
186    pub fn slot_device(&self) -> &TrackedCudaSlice<u32> {
187        &self.slot
188    }
189
190    /// Returns the device flag indicating whether the slot requires compilation.
191    pub fn compile_needed_device(&self) -> &TrackedCudaSlice<u32> {
192        &self.compile_needed
193    }
194
195    /// Returns the CUDA provider that owns the handle buffers.
196    pub fn provider(&self) -> &Arc<CudaKernelProvider> {
197        &self.provider
198    }
199
200    /// Returns the materialized circuit node count.
201    pub fn num_nodes(&self) -> u32 {
202        self.num_nodes
203    }
204
205    /// Returns the materialized circuit level count.
206    pub fn num_levels(&self) -> u32 {
207        self.num_levels
208    }
209
210    /// Returns the materialized circuit root node.
211    pub fn root(&self) -> u32 {
212        self.root
213    }
214
215    /// Returns the largest DIMACS variable in the materialized circuit.
216    pub fn max_var(&self) -> u32 {
217        self.max_var
218    }
219
220    pub(crate) fn slot_index(&self) -> u32 {
221        self.slot_host
222    }
223}
224
225/// Compute a deterministic CNF hash on the GPU.
226///
227/// Hash input order matches the cache kernel: num_vars, num_clauses, num_lits,
228/// clause_offsets[0..num_clauses], literals[0..num_lits-1].
229pub fn hash_cnf_gpu(
230    cnf: &GpuCnf,
231    provider: &Arc<CudaKernelProvider>,
232) -> Result<TrackedCudaSlice<u64>> {
233    let memory = provider.memory();
234    let mut out_hash = memory.alloc::<u64>(1)?;
235
236    let func = provider
237        .device()
238        .inner()
239        .get_func(CACHE_MODULE, cache_kernels::CACHE_CNF_HASH)
240        .ok_or_else(|| XlogError::Kernel("cache_cnf_hash kernel not found".to_string()))?;
241
242    // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
243    unsafe {
244        func.clone().launch(
245            LaunchConfig {
246                grid_dim: (1, 1, 1),
247                block_dim: (1, 1, 1),
248                shared_mem_bytes: 0,
249            },
250            (
251                &cnf.num_vars,
252                &cnf.num_clauses,
253                &cnf.num_lits,
254                &cnf.clause_offsets,
255                &cnf.literals,
256                &mut out_hash,
257            ),
258        )
259    }
260    .map_err(|e| XlogError::Kernel(format!("cache_cnf_hash launch failed: {}", e)))?;
261    // No device synchronize: hash stays device-resident for lookup kernel; same-stream ordering suffices.
262    Ok(out_hash)
263}
264
265impl GpuCircuitCache {
266    /// Returns the CUDA provider that owns all cache allocations.
267    pub fn provider(&self) -> &Arc<CudaKernelProvider> {
268        &self.provider
269    }
270
271    /// Returns mutable device-resident log-weight tables.
272    ///
273    /// Device-only callers are responsible for validating numeric inputs and outputs. Positive
274    /// infinity is valid when it does not enter an undefined normalization; NaN or undefined
275    /// arithmetic that reaches evaluation is represented by a non-finite device result.
276    pub fn var_log_weights_mut(
277        &mut self,
278    ) -> (&mut TrackedCudaSlice<f64>, &mut TrackedCudaSlice<f64>) {
279        (&mut self.var_log_true, &mut self.var_log_false)
280    }
281
282    /// Returns the device gradients for true variable log weights.
283    pub fn grad_true(&self) -> &TrackedCudaSlice<f64> {
284        &self.grad_true
285    }
286
287    /// Returns the device gradients for false variable log weights.
288    pub fn grad_false(&self) -> &TrackedCudaSlice<f64> {
289        &self.grad_false
290    }
291
292    /// Returns the device circuit-value workspace.
293    pub fn values(&self) -> &TrackedCudaSlice<f64> {
294        &self.values
295    }
296
297    /// Returns the device scalar containing the active node count.
298    pub fn meta_num_nodes_device(&self) -> &TrackedCudaSlice<u32> {
299        &self.meta_num_nodes
300    }
301
302    /// Returns the device scalar containing the active level count.
303    pub fn meta_num_levels_device(&self) -> &TrackedCudaSlice<u32> {
304        &self.meta_num_levels
305    }
306
307    /// Returns the device scalar containing the active root node.
308    pub fn meta_root_device(&self) -> &TrackedCudaSlice<u32> {
309        &self.meta_root
310    }
311
312    /// Returns the device scalar containing the active maximum variable.
313    pub fn meta_max_var_device(&self) -> &TrackedCudaSlice<u32> {
314        &self.meta_max_var
315    }
316
317    /// Returns the number of circuit slots allocated by the cache.
318    pub fn num_slots(&self) -> u32 {
319        self.num_slots
320    }
321
322    pub(crate) fn has_any_free_var_mask(&self) -> bool {
323        self.has_free_var_mask.iter().any(|&v| v)
324    }
325
326    pub(crate) fn has_free_var_mask_for_slot(&self, slot: u32) -> bool {
327        self.has_free_var_mask
328            .get(slot as usize)
329            .copied()
330            .unwrap_or(false)
331    }
332
333    pub(crate) fn var_stride(&self) -> Result<u32> {
334        self.var_cap
335            .checked_add(1)
336            .ok_or_else(|| XlogError::Compilation("GpuCircuitCache var_cap overflow".to_string()))
337    }
338
339    pub(crate) fn node_stride(&self) -> u32 {
340        self.node_cap
341    }
342
343    pub(crate) fn copy_slot_weights_to_batch(
344        &mut self,
345        handle: &GpuCircuitCacheHandle,
346        out_true_batch: &mut TrackedCudaSlice<f64>,
347        out_false_batch: &mut TrackedCudaSlice<f64>,
348        batch_size: u32,
349    ) -> Result<()> {
350        if batch_size == 0 {
351            return Ok(());
352        }
353        let var_stride = self.var_stride()?;
354        let expected = (batch_size as usize)
355            .checked_mul(var_stride as usize)
356            .ok_or_else(|| {
357                XlogError::Compilation("GpuCircuitCache batch weight size overflow".to_string())
358            })?;
359        if out_true_batch.len() != expected || out_false_batch.len() != expected {
360            return Err(XlogError::Compilation(format!(
361                "GpuCircuitCache batched weight buffers must both have len {}, got {} and {}",
362                expected,
363                out_true_batch.len(),
364                out_false_batch.len()
365            )));
366        }
367
368        let device = self.provider.device().inner();
369        let func = device
370            .get_func(
371                xlog_cuda::provider::WEIGHTS_MODULE,
372                xlog_cuda::provider::weights_kernels::WEIGHTS_COPY_SLOT_TO_BATCH,
373            )
374            .ok_or_else(|| {
375                XlogError::Kernel("weights_copy_slot_to_batch kernel not found".to_string())
376            })?;
377
378        let block_dim = 256u32;
379        let total = (batch_size as u64)
380            .checked_mul(var_stride as u64)
381            .ok_or_else(|| {
382                XlogError::Compilation("GpuCircuitCache batch copy overflow".to_string())
383            })?;
384        let grid_dim =
385            cache_grid_dim_for_u64_count("GpuCircuitCache batch weight copy", total, block_dim)?;
386        if grid_dim == 0 {
387            return Ok(());
388        }
389
390        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
391        unsafe {
392            func.clone().launch(
393                LaunchConfig {
394                    grid_dim: (grid_dim, 1, 1),
395                    block_dim: (block_dim, 1, 1),
396                    shared_mem_bytes: 0,
397                },
398                (
399                    handle.slot_device(),
400                    self.var_cap,
401                    &self.var_log_true,
402                    &self.var_log_false,
403                    out_true_batch,
404                    out_false_batch,
405                    var_stride,
406                    batch_size,
407                ),
408            )
409        }
410        .map_err(|e| XlogError::Kernel(format!("weights_copy_slot_to_batch failed: {}", e)))?;
411
412        Ok(())
413    }
414
415    #[expect(
416        clippy::too_many_arguments,
417        reason = "fused batched evaluation keeps the circuit handle, paired weight tables, four output workspaces, and batch size explicit"
418    )]
419    pub(crate) fn eval_grads_inplace_fused_batched(
420        &mut self,
421        handle: &GpuCircuitCacheHandle,
422        var_log_true_batch: &TrackedCudaSlice<f64>,
423        var_log_false_batch: &TrackedCudaSlice<f64>,
424        values_batch: &mut TrackedCudaSlice<f64>,
425        adj_batch: &mut TrackedCudaSlice<f64>,
426        grad_true_batch: &mut TrackedCudaSlice<f64>,
427        grad_false_batch: &mut TrackedCudaSlice<f64>,
428        batch_size: u32,
429    ) -> Result<()> {
430        if batch_size == 0 {
431            return Ok(());
432        }
433        if self.has_free_var_mask_for_slot(handle.slot_index()) {
434            return Err(XlogError::Execution(
435                "Batched fused eval currently does not support free-var correction".to_string(),
436            ));
437        }
438
439        let var_stride = self.var_stride()?;
440        let node_stride = self.node_stride();
441        let expected_var = (batch_size as usize)
442            .checked_mul(var_stride as usize)
443            .ok_or_else(|| {
444                XlogError::Compilation("GpuCircuitCache batched var buffer overflow".to_string())
445            })?;
446        let expected_node = (batch_size as usize)
447            .checked_mul(node_stride as usize)
448            .ok_or_else(|| {
449                XlogError::Compilation("GpuCircuitCache batched node buffer overflow".to_string())
450            })?;
451
452        if var_log_true_batch.len() != expected_var
453            || var_log_false_batch.len() != expected_var
454            || grad_true_batch.len() != expected_var
455            || grad_false_batch.len() != expected_var
456        {
457            return Err(XlogError::Compilation(format!(
458                "GpuCircuitCache batched var buffers must have len {}",
459                expected_var
460            )));
461        }
462        if values_batch.len() != expected_node || adj_batch.len() != expected_node {
463            return Err(XlogError::Compilation(format!(
464                "GpuCircuitCache batched node buffers must have len {}",
465                expected_node
466            )));
467        }
468
469        let device = self.provider.device().inner();
470        device
471            .memset_zeros(adj_batch)
472            .map_err(|e| XlogError::Kernel(format!("Failed to zero batched adj: {}", e)))?;
473        device
474            .memset_zeros(grad_true_batch)
475            .map_err(|e| XlogError::Kernel(format!("Failed to zero batched grad_true: {}", e)))?;
476        device
477            .memset_zeros(grad_false_batch)
478            .map_err(|e| XlogError::Kernel(format!("Failed to zero batched grad_false: {}", e)))?;
479
480        let eval_all = device
481            .get_func(
482                xlog_cuda::CIRCUIT_MODULE,
483                xlog_cuda::circuit_kernels::XGCF_EVAL_ALL_LEVELS_CACHED_BATCHED,
484            )
485            .ok_or_else(|| {
486                XlogError::Kernel("xgcf_eval_all_levels_cached_batched not found".to_string())
487            })?;
488        let set_root_adj = device
489            .get_func(
490                xlog_cuda::CIRCUIT_MODULE,
491                xlog_cuda::circuit_kernels::XGCF_SET_ROOT_ADJ_CACHED_BATCHED,
492            )
493            .ok_or_else(|| {
494                XlogError::Kernel("xgcf_set_root_adj_cached_batched not found".to_string())
495            })?;
496        let backward_all = device
497            .get_func(
498                xlog_cuda::CIRCUIT_MODULE,
499                xlog_cuda::circuit_kernels::XGCF_BACKWARD_ALL_LEVELS_CACHED_BATCHED,
500            )
501            .ok_or_else(|| {
502                XlogError::Kernel("xgcf_backward_all_levels_cached_batched not found".to_string())
503            })?;
504
505        let block_size = 256u32;
506        let mut eval_params: Vec<*mut std::ffi::c_void> = vec![
507            handle.slot_device().as_kernel_param(),
508            self.node_cap.as_kernel_param(),
509            self.edge_cap.as_kernel_param(),
510            self.level_cap.as_kernel_param(),
511            self.var_cap.as_kernel_param(),
512            (&self.node_type).as_kernel_param(),
513            (&self.child_offsets).as_kernel_param(),
514            (&self.child_indices).as_kernel_param(),
515            (&self.lit).as_kernel_param(),
516            (&self.decision_var).as_kernel_param(),
517            (&self.decision_child_false).as_kernel_param(),
518            (&self.decision_child_true).as_kernel_param(),
519            (&self.level_nodes).as_kernel_param(),
520            (&self.level_offsets).as_kernel_param(),
521            (&self.meta_num_levels).as_kernel_param(),
522            var_log_true_batch.as_kernel_param(),
523            var_log_false_batch.as_kernel_param(),
524            var_stride.as_kernel_param(),
525            values_batch.as_kernel_param(),
526            node_stride.as_kernel_param(),
527            batch_size.as_kernel_param(),
528        ];
529        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
530        unsafe {
531            eval_all.clone().launch(
532                LaunchConfig {
533                    grid_dim: (batch_size, 1, 1),
534                    block_dim: (block_size, 1, 1),
535                    shared_mem_bytes: 0,
536                },
537                &mut eval_params,
538            )
539        }
540        .map_err(|e| {
541            XlogError::Kernel(format!("xgcf_eval_all_levels_cached_batched failed: {}", e))
542        })?;
543
544        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
545        unsafe {
546            set_root_adj.clone().launch(
547                LaunchConfig {
548                    grid_dim: (batch_size, 1, 1),
549                    block_dim: (1, 1, 1),
550                    shared_mem_bytes: 0,
551                },
552                (
553                    handle.slot_device(),
554                    self.node_cap,
555                    &self.meta_root,
556                    &mut *adj_batch,
557                    node_stride,
558                    batch_size,
559                ),
560            )
561        }
562        .map_err(|e| {
563            XlogError::Kernel(format!("xgcf_set_root_adj_cached_batched failed: {}", e))
564        })?;
565
566        let mut backward_params: Vec<*mut std::ffi::c_void> = vec![
567            handle.slot_device().as_kernel_param(),
568            self.node_cap.as_kernel_param(),
569            self.edge_cap.as_kernel_param(),
570            self.level_cap.as_kernel_param(),
571            self.var_cap.as_kernel_param(),
572            (&self.node_type).as_kernel_param(),
573            (&self.child_offsets).as_kernel_param(),
574            (&self.child_indices).as_kernel_param(),
575            (&self.decision_var).as_kernel_param(),
576            (&self.decision_child_false).as_kernel_param(),
577            (&self.decision_child_true).as_kernel_param(),
578            (&self.lit).as_kernel_param(),
579            (&self.level_nodes).as_kernel_param(),
580            (&self.level_offsets).as_kernel_param(),
581            (&self.meta_num_levels).as_kernel_param(),
582            var_log_true_batch.as_kernel_param(),
583            var_log_false_batch.as_kernel_param(),
584            var_stride.as_kernel_param(),
585            values_batch.as_kernel_param(),
586            node_stride.as_kernel_param(),
587            adj_batch.as_kernel_param(),
588            node_stride.as_kernel_param(),
589            grad_true_batch.as_kernel_param(),
590            grad_false_batch.as_kernel_param(),
591            var_stride.as_kernel_param(),
592            batch_size.as_kernel_param(),
593        ];
594        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
595        unsafe {
596            backward_all.clone().launch(
597                LaunchConfig {
598                    grid_dim: (batch_size, 1, 1),
599                    block_dim: (block_size, 1, 1),
600                    shared_mem_bytes: 0,
601                },
602                &mut backward_params,
603            )
604        }
605        .map_err(|e| {
606            XlogError::Kernel(format!(
607                "xgcf_backward_all_levels_cached_batched failed: {}",
608                e
609            ))
610        })?;
611
612        Ok(())
613    }
614
615    pub(crate) fn copy_root_batched_from_values(
616        &self,
617        handle: &GpuCircuitCacheHandle,
618        values_batch: &TrackedCudaSlice<f64>,
619        out_roots: &mut TrackedCudaSlice<f64>,
620        batch_size: u32,
621    ) -> Result<()> {
622        if batch_size == 0 {
623            return Ok(());
624        }
625        let node_stride = self.node_stride();
626        let expected_values = (batch_size as usize)
627            .checked_mul(node_stride as usize)
628            .ok_or_else(|| {
629                XlogError::Compilation("GpuCircuitCache batched values overflow".to_string())
630            })?;
631        if values_batch.len() != expected_values || out_roots.len() != batch_size as usize {
632            return Err(XlogError::Compilation(format!(
633                "GpuCircuitCache root copy expects values len {} and roots len {}, got {} and {}",
634                expected_values,
635                batch_size,
636                values_batch.len(),
637                out_roots.len()
638            )));
639        }
640
641        let device = self.provider.device().inner();
642        let copy_root = device
643            .get_func(
644                xlog_cuda::CIRCUIT_MODULE,
645                xlog_cuda::circuit_kernels::XGCF_COPY_ROOT_CACHED_META_BATCHED,
646            )
647            .ok_or_else(|| {
648                XlogError::Kernel("xgcf_copy_root_cached_meta_batched not found".to_string())
649            })?;
650        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
651        unsafe {
652            copy_root.clone().launch(
653                LaunchConfig {
654                    grid_dim: (batch_size, 1, 1),
655                    block_dim: (1, 1, 1),
656                    shared_mem_bytes: 0,
657                },
658                (
659                    handle.slot_device(),
660                    self.node_cap,
661                    &self.meta_root,
662                    values_batch,
663                    node_stride,
664                    out_roots,
665                    batch_size,
666                ),
667            )
668        }
669        .map_err(|e| {
670            XlogError::Kernel(format!("xgcf_copy_root_cached_meta_batched failed: {}", e))
671        })?;
672        Ok(())
673    }
674
675    /// Allocates an empty circuit cache with the requested fixed capacities.
676    pub fn new(provider: &Arc<CudaKernelProvider>, config: GpuCircuitCacheConfig) -> Result<Self> {
677        if config.num_slots == 0 {
678            return Err(XlogError::Compilation(
679                "GpuCircuitCache requires num_slots > 0".to_string(),
680            ));
681        }
682        if config.table_size == 0 {
683            return Err(XlogError::Compilation(
684                "GpuCircuitCache requires table_size > 0".to_string(),
685            ));
686        }
687        if config.table_size < config.num_slots {
688            return Err(XlogError::Compilation(format!(
689                "GpuCircuitCache table_size {} < num_slots {}",
690                config.table_size, config.num_slots
691            )));
692        }
693        if config.node_cap == 0
694            || config.edge_cap == 0
695            || config.level_cap == 0
696            || config.var_cap == 0
697        {
698            return Err(XlogError::Compilation(
699                "GpuCircuitCache requires non-zero caps".to_string(),
700            ));
701        }
702
703        let memory = provider.memory();
704        let device = provider.device().inner();
705
706        let table_len = usize::try_from(config.table_size).map_err(|_| {
707            XlogError::Compilation("GpuCircuitCache table_size overflow".to_string())
708        })?;
709        let slot_len = usize::try_from(config.num_slots).map_err(|_| {
710            XlogError::Compilation("GpuCircuitCache num_slots overflow".to_string())
711        })?;
712
713        let node_cap = usize::try_from(config.node_cap)
714            .map_err(|_| XlogError::Compilation("GpuCircuitCache node_cap overflow".to_string()))?;
715        let edge_cap = usize::try_from(config.edge_cap)
716            .map_err(|_| XlogError::Compilation("GpuCircuitCache edge_cap overflow".to_string()))?;
717        let level_cap = usize::try_from(config.level_cap).map_err(|_| {
718            XlogError::Compilation("GpuCircuitCache level_cap overflow".to_string())
719        })?;
720        let var_cap = usize::try_from(config.var_cap)
721            .map_err(|_| XlogError::Compilation("GpuCircuitCache var_cap overflow".to_string()))?;
722
723        let node_slots = slot_len.checked_mul(node_cap).ok_or_else(|| {
724            XlogError::Compilation("GpuCircuitCache node slots overflow".to_string())
725        })?;
726        let edge_slots = slot_len.checked_mul(edge_cap).ok_or_else(|| {
727            XlogError::Compilation("GpuCircuitCache edge slots overflow".to_string())
728        })?;
729        let var_slots = slot_len.checked_mul(var_cap + 1).ok_or_else(|| {
730            XlogError::Compilation("GpuCircuitCache var slots overflow".to_string())
731        })?;
732        let node_offsets = slot_len.checked_mul(node_cap + 1).ok_or_else(|| {
733            XlogError::Compilation("GpuCircuitCache offset slots overflow".to_string())
734        })?;
735        let level_offsets = slot_len.checked_mul(level_cap + 1).ok_or_else(|| {
736            XlogError::Compilation("GpuCircuitCache level offsets overflow".to_string())
737        })?;
738
739        let mut keys = memory.alloc::<u64>(table_len)?;
740        let mut slots = memory.alloc::<u32>(table_len)?;
741        let mut state = memory.alloc::<u32>(table_len)?;
742        let mut last_used = memory.alloc::<u64>(table_len)?;
743        let mut slot_states = memory.alloc::<u32>(slot_len)?;
744        let mut clock = memory.alloc::<u64>(1)?;
745
746        let mut node_type = memory.alloc::<u8>(node_slots)?;
747        let mut child_offsets = memory.alloc::<u32>(node_offsets)?;
748        let mut child_indices = memory.alloc::<u32>(edge_slots)?;
749        let mut lit = memory.alloc::<i32>(node_slots)?;
750        let mut decision_var = memory.alloc::<u32>(node_slots)?;
751        let mut decision_child_false = memory.alloc::<u32>(node_slots)?;
752        let mut decision_child_true = memory.alloc::<u32>(node_slots)?;
753        let mut level_nodes = memory.alloc::<u32>(node_slots)?;
754        let mut level_offsets = memory.alloc::<u32>(level_offsets)?;
755
756        let mut var_log_true = memory.alloc::<f64>(var_slots)?;
757        let mut var_log_false = memory.alloc::<f64>(var_slots)?;
758        let mut values = memory.alloc::<f64>(node_slots)?;
759        let mut adj = memory.alloc::<f64>(node_slots)?;
760        let mut grad_true = memory.alloc::<f64>(var_slots)?;
761        let mut grad_false = memory.alloc::<f64>(var_slots)?;
762        let mut free_var_mask = memory.alloc::<u8>(var_slots)?;
763        let mut meta_num_nodes = memory.alloc::<u32>(slot_len)?;
764        let mut meta_num_levels = memory.alloc::<u32>(slot_len)?;
765        let mut meta_root = memory.alloc::<u32>(slot_len)?;
766        let mut meta_max_var = memory.alloc::<u32>(slot_len)?;
767        let mut always_on = memory.alloc::<u32>(1)?;
768        let zero_len = node_cap.max(var_cap + 1);
769        let mut zero_f64 = memory.alloc::<f64>(zero_len)?;
770        let mut one_f64 = memory.alloc::<f64>(1)?;
771
772        device
773            .memset_zeros(&mut keys)
774            .map_err(|e| XlogError::Kernel(format!("GpuCircuitCache zero keys failed: {}", e)))?;
775        device
776            .memset_zeros(&mut slots)
777            .map_err(|e| XlogError::Kernel(format!("GpuCircuitCache zero slots failed: {}", e)))?;
778        device
779            .memset_zeros(&mut state)
780            .map_err(|e| XlogError::Kernel(format!("GpuCircuitCache zero state failed: {}", e)))?;
781        device.memset_zeros(&mut last_used).map_err(|e| {
782            XlogError::Kernel(format!("GpuCircuitCache zero last_used failed: {}", e))
783        })?;
784        device.memset_zeros(&mut slot_states).map_err(|e| {
785            XlogError::Kernel(format!("GpuCircuitCache zero slot_states failed: {}", e))
786        })?;
787        device
788            .memset_zeros(&mut clock)
789            .map_err(|e| XlogError::Kernel(format!("GpuCircuitCache zero clock failed: {}", e)))?;
790
791        device.memset_zeros(&mut node_type).map_err(|e| {
792            XlogError::Kernel(format!("GpuCircuitCache zero node_type failed: {}", e))
793        })?;
794        device.memset_zeros(&mut child_offsets).map_err(|e| {
795            XlogError::Kernel(format!("GpuCircuitCache zero child_offsets failed: {}", e))
796        })?;
797        device.memset_zeros(&mut child_indices).map_err(|e| {
798            XlogError::Kernel(format!("GpuCircuitCache zero child_indices failed: {}", e))
799        })?;
800        device
801            .memset_zeros(&mut lit)
802            .map_err(|e| XlogError::Kernel(format!("GpuCircuitCache zero lit failed: {}", e)))?;
803        device.memset_zeros(&mut decision_var).map_err(|e| {
804            XlogError::Kernel(format!("GpuCircuitCache zero decision_var failed: {}", e))
805        })?;
806        device
807            .memset_zeros(&mut decision_child_false)
808            .map_err(|e| {
809                XlogError::Kernel(format!(
810                    "GpuCircuitCache zero decision_child_false failed: {}",
811                    e
812                ))
813            })?;
814        device.memset_zeros(&mut decision_child_true).map_err(|e| {
815            XlogError::Kernel(format!(
816                "GpuCircuitCache zero decision_child_true failed: {}",
817                e
818            ))
819        })?;
820        device.memset_zeros(&mut level_nodes).map_err(|e| {
821            XlogError::Kernel(format!("GpuCircuitCache zero level_nodes failed: {}", e))
822        })?;
823        device.memset_zeros(&mut level_offsets).map_err(|e| {
824            XlogError::Kernel(format!("GpuCircuitCache zero level_offsets failed: {}", e))
825        })?;
826        device.memset_zeros(&mut var_log_true).map_err(|e| {
827            XlogError::Kernel(format!("GpuCircuitCache zero var_log_true failed: {}", e))
828        })?;
829        device.memset_zeros(&mut var_log_false).map_err(|e| {
830            XlogError::Kernel(format!("GpuCircuitCache zero var_log_false failed: {}", e))
831        })?;
832        device
833            .memset_zeros(&mut values)
834            .map_err(|e| XlogError::Kernel(format!("GpuCircuitCache zero values failed: {}", e)))?;
835        device
836            .memset_zeros(&mut adj)
837            .map_err(|e| XlogError::Kernel(format!("GpuCircuitCache zero adj failed: {}", e)))?;
838        device.memset_zeros(&mut grad_true).map_err(|e| {
839            XlogError::Kernel(format!("GpuCircuitCache zero grad_true failed: {}", e))
840        })?;
841        device.memset_zeros(&mut grad_false).map_err(|e| {
842            XlogError::Kernel(format!("GpuCircuitCache zero grad_false failed: {}", e))
843        })?;
844        device.memset_zeros(&mut free_var_mask).map_err(|e| {
845            XlogError::Kernel(format!("GpuCircuitCache zero free_var_mask failed: {}", e))
846        })?;
847        device.memset_zeros(&mut meta_num_nodes).map_err(|e| {
848            XlogError::Kernel(format!("GpuCircuitCache zero meta_num_nodes failed: {}", e))
849        })?;
850        device.memset_zeros(&mut meta_num_levels).map_err(|e| {
851            XlogError::Kernel(format!(
852                "GpuCircuitCache zero meta_num_levels failed: {}",
853                e
854            ))
855        })?;
856        device.memset_zeros(&mut meta_root).map_err(|e| {
857            XlogError::Kernel(format!("GpuCircuitCache zero meta_root failed: {}", e))
858        })?;
859        device.memset_zeros(&mut meta_max_var).map_err(|e| {
860            XlogError::Kernel(format!("GpuCircuitCache zero meta_max_var failed: {}", e))
861        })?;
862        device.memset_zeros(&mut zero_f64).map_err(|e| {
863            XlogError::Kernel(format!("GpuCircuitCache zero zero_f64 failed: {}", e))
864        })?;
865        provider
866            .htod_launch_metadata_sync_copy_into(&[1u32], &mut always_on)
867            .map_err(|e| {
868                XlogError::Kernel(format!("GpuCircuitCache init always_on failed: {}", e))
869            })?;
870        provider
871            .htod_launch_metadata_sync_copy_into(&[1.0f64], &mut one_f64)
872            .map_err(|e| {
873                XlogError::Kernel(format!("GpuCircuitCache init one_f64 failed: {}", e))
874            })?;
875
876        Ok(Self {
877            provider: provider.clone(),
878            table_size: config.table_size,
879            num_slots: config.num_slots,
880            node_cap: config.node_cap,
881            edge_cap: config.edge_cap,
882            level_cap: config.level_cap,
883            var_cap: config.var_cap,
884            keys,
885            slots,
886            state,
887            last_used,
888            slot_states,
889            clock,
890            node_type,
891            child_offsets,
892            child_indices,
893            lit,
894            decision_var,
895            decision_child_false,
896            decision_child_true,
897            level_nodes,
898            level_offsets,
899            var_log_true,
900            var_log_false,
901            values,
902            adj,
903            grad_true,
904            grad_false,
905            meta_num_nodes,
906            meta_num_levels,
907            meta_root,
908            meta_max_var,
909            always_on,
910            zero_f64,
911            one_f64,
912            free_var_mask,
913            has_free_var_mask: vec![false; config.num_slots as usize],
914        })
915    }
916
917    /// Finds or claims a slot for a host-resident cache key.
918    pub fn lookup_or_insert(&mut self, key: u64) -> Result<GpuCacheLookup> {
919        let memory = self.provider.memory();
920        let mut key_device = memory.alloc::<u64>(1)?;
921        self.provider
922            .htod_launch_metadata_sync_copy_into(&[key], &mut key_device)
923            .map_err(|e| XlogError::Kernel(format!("cache upload key failed: {}", e)))?;
924        self.lookup_or_insert_device(&key_device)
925    }
926
927    pub(crate) fn lookup_or_insert_device(
928        &mut self,
929        key_device: &TrackedCudaSlice<u64>,
930    ) -> Result<GpuCacheLookup> {
931        let memory = self.provider.memory();
932        let mut out_slot = memory.alloc::<u32>(1)?;
933        let mut out_compile_needed = memory.alloc::<u32>(1)?;
934
935        let func = self
936            .provider
937            .device()
938            .inner()
939            .get_func(CACHE_MODULE, cache_kernels::CACHE_LOOKUP_OR_INSERT)
940            .ok_or_else(|| {
941                XlogError::Kernel("cache_lookup_or_insert kernel not found".to_string())
942            })?;
943
944        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
945        unsafe {
946            func.clone().launch(
947                LaunchConfig {
948                    grid_dim: (1, 1, 1),
949                    block_dim: (1, 1, 1),
950                    shared_mem_bytes: 0,
951                },
952                (
953                    key_device,
954                    self.table_size,
955                    self.num_slots,
956                    &mut self.keys,
957                    &mut self.slots,
958                    &mut self.state,
959                    &mut self.last_used,
960                    &mut self.slot_states,
961                    &mut self.clock,
962                    &mut out_slot,
963                    &mut out_compile_needed,
964                ),
965            )
966        }
967        .map_err(|e| XlogError::Kernel(format!("cache_lookup_or_insert failed: {}", e)))?;
968        // No device synchronize: slot and compile_needed stay device-resident; same-stream ordering suffices.
969        Ok(GpuCacheLookup {
970            provider: self.provider.clone(),
971            slot: out_slot,
972            compile_needed: out_compile_needed,
973        })
974    }
975
976    /// Finds or claims a slot and resolves it into a host-visible handle.
977    pub fn claim_slot(&mut self, key: u64) -> Result<GpuCircuitCacheHandle> {
978        let lookup = self.lookup_or_insert(key)?;
979        lookup.into_handle()
980    }
981
982    /// Copies a compiled device circuit and its authoritative metadata into a claimed slot.
983    pub fn store_from_xgcf(
984        &mut self,
985        handle: &mut GpuCircuitCacheHandle,
986        xgcf: &GpuXgcf,
987    ) -> Result<()> {
988        // Download the actual node/edge counts from device-resident metadata.
989        // xgcf.num_nodes() / num_edges() return the CAPACITY (node_cap / edge_cap),
990        // not the actual count produced by d4. Using the capacity would store garbage
991        // data beyond the actual circuit, corrupting disk cache artifacts.
992        let device = self.provider.device().inner();
993        let num_nodes_host: Vec<u32> = device
994            .dtoh_sync_copy(xgcf.num_nodes_device())
995            .map_err(|e| XlogError::Kernel(format!("dtoh meta_num_nodes: {}", e)))?;
996        let num_nodes = num_nodes_host[0];
997        if num_nodes == 0 {
998            return Err(XlogError::Compilation(
999                "GpuCircuitCache store: num_nodes must be > 0".to_string(),
1000            ));
1001        }
1002        if num_nodes > self.node_cap {
1003            return Err(XlogError::Compilation(format!(
1004                "GpuCircuitCache store: num_nodes {} exceeds node_cap {}",
1005                num_nodes, self.node_cap
1006            )));
1007        }
1008
1009        let num_edges_host: Vec<u32> = device
1010            .dtoh_sync_copy(xgcf.num_edges_device())
1011            .map_err(|e| XlogError::Kernel(format!("dtoh meta_num_edges: {}", e)))?;
1012        let num_edges = num_edges_host[0];
1013        if num_edges > self.edge_cap {
1014            return Err(XlogError::Compilation(format!(
1015                "GpuCircuitCache store: num_edges {} exceeds edge_cap {}",
1016                num_edges, self.edge_cap
1017            )));
1018        }
1019
1020        let num_levels = xgcf.num_levels();
1021        if num_levels == 0 {
1022            return Err(XlogError::Compilation(
1023                "GpuCircuitCache store: num_levels must be > 0".to_string(),
1024            ));
1025        }
1026        if num_levels > self.level_cap {
1027            return Err(XlogError::Compilation(format!(
1028                "GpuCircuitCache store: num_levels {} exceeds level_cap {}",
1029                num_levels, self.level_cap
1030            )));
1031        }
1032
1033        let root = xgcf.root();
1034        if root >= num_nodes {
1035            return Err(XlogError::Compilation(format!(
1036                "GpuCircuitCache store: root {} out of bounds (num_nodes={})",
1037                root, num_nodes
1038            )));
1039        }
1040
1041        let max_var = xgcf.max_var();
1042        if max_var > self.var_cap {
1043            return Err(XlogError::Compilation(format!(
1044                "GpuCircuitCache store: max_var {} exceeds var_cap {}",
1045                max_var, self.var_cap
1046            )));
1047        }
1048
1049        let expected_child_offsets = (num_nodes as usize) + 1;
1050        if xgcf.child_offsets().len() < expected_child_offsets {
1051            return Err(XlogError::Compilation(format!(
1052                "GpuCircuitCache store: child_offsets len {} < num_nodes+1 {}",
1053                xgcf.child_offsets().len(),
1054                expected_child_offsets
1055            )));
1056        }
1057        if xgcf.level_nodes().len() < num_nodes as usize {
1058            return Err(XlogError::Compilation(format!(
1059                "GpuCircuitCache store: level_nodes len {} < num_nodes {}",
1060                xgcf.level_nodes().len(),
1061                num_nodes
1062            )));
1063        }
1064        let expected_level_offsets = (num_levels as usize) + 1;
1065        if xgcf.level_offsets().len() != expected_level_offsets {
1066            return Err(XlogError::Compilation(format!(
1067                "GpuCircuitCache store: level_offsets len {} != num_levels+1 {}",
1068                xgcf.level_offsets().len(),
1069                expected_level_offsets
1070            )));
1071        }
1072
1073        handle.num_nodes = num_nodes;
1074        handle.num_levels = num_levels;
1075        handle.root = root;
1076        handle.max_var = max_var;
1077
1078        let store_u8 = device
1079            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_U8)
1080            .ok_or_else(|| XlogError::Kernel("cache_store_u8 kernel not found".to_string()))?;
1081        let store_u32 = device
1082            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_U32)
1083            .ok_or_else(|| XlogError::Kernel("cache_store_u32 kernel not found".to_string()))?;
1084        let store_i32 = device
1085            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_I32)
1086            .ok_or_else(|| XlogError::Kernel("cache_store_i32 kernel not found".to_string()))?;
1087        let store_f64 = device
1088            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_F64)
1089            .ok_or_else(|| XlogError::Kernel("cache_store_f64 kernel not found".to_string()))?;
1090        let store_meta = device
1091            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_META)
1092            .ok_or_else(|| XlogError::Kernel("cache_store_meta kernel not found".to_string()))?;
1093
1094        let block_dim = 256u32;
1095
1096        let node_stride = self.node_cap;
1097        let offset_stride = self.node_cap.checked_add(1).ok_or_else(|| {
1098            XlogError::Compilation("GpuCircuitCache store: node_cap overflow".to_string())
1099        })?;
1100        let level_offset_stride = self.level_cap.checked_add(1).ok_or_else(|| {
1101            XlogError::Compilation("GpuCircuitCache store: level_cap overflow".to_string())
1102        })?;
1103        let var_stride = self.var_cap.checked_add(1).ok_or_else(|| {
1104            XlogError::Compilation("GpuCircuitCache store: var_cap overflow".to_string())
1105        })?;
1106
1107        let num_nodes_plus1 = num_nodes.checked_add(1).ok_or_else(|| {
1108            XlogError::Compilation("GpuCircuitCache store: num_nodes overflow".to_string())
1109        })?;
1110        let num_levels_plus1 = num_levels.checked_add(1).ok_or_else(|| {
1111            XlogError::Compilation("GpuCircuitCache store: num_levels overflow".to_string())
1112        })?;
1113        let weights_len = max_var.checked_add(1).ok_or_else(|| {
1114            XlogError::Compilation("GpuCircuitCache store: max_var overflow".to_string())
1115        })?;
1116
1117        let grid_nodes =
1118            cache_grid_dim_for_u32_count("GpuCircuitCache store node_type", num_nodes, block_dim)?;
1119        if grid_nodes != 0 {
1120            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1121            unsafe {
1122                store_u8.clone().launch(
1123                    LaunchConfig {
1124                        grid_dim: (grid_nodes, 1, 1),
1125                        block_dim: (block_dim, 1, 1),
1126                        shared_mem_bytes: 0,
1127                    },
1128                    (
1129                        handle.slot_device(),
1130                        handle.compile_needed_device(),
1131                        node_stride,
1132                        xgcf.node_type(),
1133                        &mut self.node_type,
1134                        num_nodes,
1135                    ),
1136                )
1137            }
1138            .map_err(|e| XlogError::Kernel(format!("cache_store_u8 failed: {}", e)))?;
1139        }
1140
1141        let grid_offsets = cache_grid_dim_for_u32_count(
1142            "GpuCircuitCache store child_offsets",
1143            num_nodes_plus1,
1144            block_dim,
1145        )?;
1146        if grid_offsets != 0 {
1147            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1148            unsafe {
1149                store_u32.clone().launch(
1150                    LaunchConfig {
1151                        grid_dim: (grid_offsets, 1, 1),
1152                        block_dim: (block_dim, 1, 1),
1153                        shared_mem_bytes: 0,
1154                    },
1155                    (
1156                        handle.slot_device(),
1157                        handle.compile_needed_device(),
1158                        offset_stride,
1159                        xgcf.child_offsets(),
1160                        &mut self.child_offsets,
1161                        num_nodes_plus1,
1162                    ),
1163                )
1164            }
1165            .map_err(|e| XlogError::Kernel(format!("cache_store_child_offsets failed: {}", e)))?;
1166        }
1167
1168        let grid_edges = cache_grid_dim_for_u32_count(
1169            "GpuCircuitCache store child_indices",
1170            num_edges,
1171            block_dim,
1172        )?;
1173        if grid_edges != 0 {
1174            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1175            unsafe {
1176                store_u32.clone().launch(
1177                    LaunchConfig {
1178                        grid_dim: (grid_edges, 1, 1),
1179                        block_dim: (block_dim, 1, 1),
1180                        shared_mem_bytes: 0,
1181                    },
1182                    (
1183                        handle.slot_device(),
1184                        handle.compile_needed_device(),
1185                        self.edge_cap,
1186                        xgcf.child_indices(),
1187                        &mut self.child_indices,
1188                        num_edges,
1189                    ),
1190                )
1191            }
1192            .map_err(|e| XlogError::Kernel(format!("cache_store_child_indices failed: {}", e)))?;
1193        }
1194
1195        if grid_nodes != 0 {
1196            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1197            unsafe {
1198                store_i32.clone().launch(
1199                    LaunchConfig {
1200                        grid_dim: (grid_nodes, 1, 1),
1201                        block_dim: (block_dim, 1, 1),
1202                        shared_mem_bytes: 0,
1203                    },
1204                    (
1205                        handle.slot_device(),
1206                        handle.compile_needed_device(),
1207                        node_stride,
1208                        xgcf.lit(),
1209                        &mut self.lit,
1210                        num_nodes,
1211                    ),
1212                )
1213            }
1214            .map_err(|e| XlogError::Kernel(format!("cache_store_lit failed: {}", e)))?;
1215
1216            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1217            unsafe {
1218                store_u32.clone().launch(
1219                    LaunchConfig {
1220                        grid_dim: (grid_nodes, 1, 1),
1221                        block_dim: (block_dim, 1, 1),
1222                        shared_mem_bytes: 0,
1223                    },
1224                    (
1225                        handle.slot_device(),
1226                        handle.compile_needed_device(),
1227                        node_stride,
1228                        xgcf.decision_var(),
1229                        &mut self.decision_var,
1230                        num_nodes,
1231                    ),
1232                )
1233            }
1234            .map_err(|e| XlogError::Kernel(format!("cache_store_decision_var failed: {}", e)))?;
1235
1236            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1237            unsafe {
1238                store_u32.clone().launch(
1239                    LaunchConfig {
1240                        grid_dim: (grid_nodes, 1, 1),
1241                        block_dim: (block_dim, 1, 1),
1242                        shared_mem_bytes: 0,
1243                    },
1244                    (
1245                        handle.slot_device(),
1246                        handle.compile_needed_device(),
1247                        node_stride,
1248                        xgcf.decision_child_false(),
1249                        &mut self.decision_child_false,
1250                        num_nodes,
1251                    ),
1252                )
1253            }
1254            .map_err(|e| {
1255                XlogError::Kernel(format!("cache_store_decision_child_false failed: {}", e))
1256            })?;
1257
1258            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1259            unsafe {
1260                store_u32.clone().launch(
1261                    LaunchConfig {
1262                        grid_dim: (grid_nodes, 1, 1),
1263                        block_dim: (block_dim, 1, 1),
1264                        shared_mem_bytes: 0,
1265                    },
1266                    (
1267                        handle.slot_device(),
1268                        handle.compile_needed_device(),
1269                        node_stride,
1270                        xgcf.decision_child_true(),
1271                        &mut self.decision_child_true,
1272                        num_nodes,
1273                    ),
1274                )
1275            }
1276            .map_err(|e| {
1277                XlogError::Kernel(format!("cache_store_decision_child_true failed: {}", e))
1278            })?;
1279
1280            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1281            unsafe {
1282                store_u32.clone().launch(
1283                    LaunchConfig {
1284                        grid_dim: (grid_nodes, 1, 1),
1285                        block_dim: (block_dim, 1, 1),
1286                        shared_mem_bytes: 0,
1287                    },
1288                    (
1289                        handle.slot_device(),
1290                        handle.compile_needed_device(),
1291                        node_stride,
1292                        xgcf.level_nodes(),
1293                        &mut self.level_nodes,
1294                        num_nodes,
1295                    ),
1296                )
1297            }
1298            .map_err(|e| XlogError::Kernel(format!("cache_store_level_nodes failed: {}", e)))?;
1299        }
1300
1301        let grid_levels = cache_grid_dim_for_u32_count(
1302            "GpuCircuitCache store level_offsets",
1303            num_levels_plus1,
1304            block_dim,
1305        )?;
1306        if grid_levels != 0 {
1307            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1308            unsafe {
1309                store_u32.clone().launch(
1310                    LaunchConfig {
1311                        grid_dim: (grid_levels, 1, 1),
1312                        block_dim: (block_dim, 1, 1),
1313                        shared_mem_bytes: 0,
1314                    },
1315                    (
1316                        handle.slot_device(),
1317                        handle.compile_needed_device(),
1318                        level_offset_stride,
1319                        xgcf.level_offsets(),
1320                        &mut self.level_offsets,
1321                        num_levels_plus1,
1322                    ),
1323                )
1324            }
1325            .map_err(|e| XlogError::Kernel(format!("cache_store_level_offsets failed: {}", e)))?;
1326        }
1327
1328        let grid_weights = cache_grid_dim_for_u32_count(
1329            "GpuCircuitCache store free_var_mask",
1330            weights_len,
1331            block_dim,
1332        )?;
1333        if grid_weights != 0 {
1334            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1335            unsafe {
1336                store_f64.clone().launch(
1337                    LaunchConfig {
1338                        grid_dim: (grid_weights, 1, 1),
1339                        block_dim: (block_dim, 1, 1),
1340                        shared_mem_bytes: 0,
1341                    },
1342                    (
1343                        handle.slot_device(),
1344                        handle.compile_needed_device(),
1345                        var_stride,
1346                        xgcf.var_log_true(),
1347                        &mut self.var_log_true,
1348                        weights_len,
1349                    ),
1350                )
1351            }
1352            .map_err(|e| XlogError::Kernel(format!("cache_store_var_log_true failed: {}", e)))?;
1353
1354            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1355            unsafe {
1356                store_f64.clone().launch(
1357                    LaunchConfig {
1358                        grid_dim: (grid_weights, 1, 1),
1359                        block_dim: (block_dim, 1, 1),
1360                        shared_mem_bytes: 0,
1361                    },
1362                    (
1363                        handle.slot_device(),
1364                        handle.compile_needed_device(),
1365                        var_stride,
1366                        xgcf.var_log_false(),
1367                        &mut self.var_log_false,
1368                        weights_len,
1369                    ),
1370                )
1371            }
1372            .map_err(|e| XlogError::Kernel(format!("cache_store_var_log_false failed: {}", e)))?;
1373        }
1374
1375        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1376        unsafe {
1377            store_meta.clone().launch(
1378                LaunchConfig {
1379                    grid_dim: (1, 1, 1),
1380                    block_dim: (1, 1, 1),
1381                    shared_mem_bytes: 0,
1382                },
1383                (
1384                    handle.slot_device(),
1385                    handle.compile_needed_device(),
1386                    self.num_slots,
1387                    num_nodes,
1388                    num_levels,
1389                    root,
1390                    max_var,
1391                    &mut self.meta_num_nodes,
1392                    &mut self.meta_num_levels,
1393                    &mut self.meta_root,
1394                    &mut self.meta_max_var,
1395                ),
1396            )
1397        }
1398        .map_err(|e| XlogError::Kernel(format!("cache_store_meta failed: {}", e)))?;
1399
1400        // No device synchronize needed: all stores are GPU-to-GPU on the same stream.
1401        // Same-stream ordering guarantees subsequent kernels see the stored data.
1402        Ok(())
1403    }
1404
1405    /// Initializes the log-weight tables for a claimed slot.
1406    pub fn store_weights(
1407        &mut self,
1408        handle: &GpuCircuitCacheHandle,
1409        weights_true: &TrackedCudaSlice<f64>,
1410        weights_false: &TrackedCudaSlice<f64>,
1411    ) -> Result<()> {
1412        let weights_len = handle.max_var.checked_add(1).ok_or_else(|| {
1413            XlogError::Compilation("GpuCircuitCache store_weights max_var overflow".to_string())
1414        })?;
1415        let weights_len_usize = usize::try_from(weights_len).map_err(|_| {
1416            XlogError::Compilation("GpuCircuitCache store_weights len overflow".to_string())
1417        })?;
1418        if weights_true.len() < weights_len_usize || weights_false.len() < weights_len_usize {
1419            return Err(XlogError::Compilation(format!(
1420                "GpuCircuitCache store_weights requires weights len >= {}, got true={} false={}",
1421                weights_len,
1422                weights_true.len(),
1423                weights_false.len()
1424            )));
1425        }
1426
1427        let device = self.provider.device().inner();
1428        let store_f64 = device
1429            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_F64)
1430            .ok_or_else(|| XlogError::Kernel("cache_store_f64 kernel not found".to_string()))?;
1431
1432        let block_dim = 256u32;
1433        let grid_dim = if weights_len == 0 {
1434            0
1435        } else {
1436            weights_len.div_ceil(block_dim)
1437        };
1438        if grid_dim != 0 {
1439            let var_stride = self.var_cap.checked_add(1).ok_or_else(|| {
1440                XlogError::Compilation("GpuCircuitCache store_weights var_cap overflow".to_string())
1441            })?;
1442            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1443            unsafe {
1444                store_f64.clone().launch(
1445                    LaunchConfig {
1446                        grid_dim: (grid_dim, 1, 1),
1447                        block_dim: (block_dim, 1, 1),
1448                        shared_mem_bytes: 0,
1449                    },
1450                    (
1451                        handle.slot_device(),
1452                        handle.compile_needed_device(),
1453                        var_stride,
1454                        weights_true,
1455                        &mut self.var_log_true,
1456                        weights_len,
1457                    ),
1458                )
1459            }
1460            .map_err(|e| XlogError::Kernel(format!("cache_store_weights_true failed: {}", e)))?;
1461
1462            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1463            unsafe {
1464                store_f64.clone().launch(
1465                    LaunchConfig {
1466                        grid_dim: (grid_dim, 1, 1),
1467                        block_dim: (block_dim, 1, 1),
1468                        shared_mem_bytes: 0,
1469                    },
1470                    (
1471                        handle.slot_device(),
1472                        handle.compile_needed_device(),
1473                        var_stride,
1474                        weights_false,
1475                        &mut self.var_log_false,
1476                        weights_len,
1477                    ),
1478                )
1479            }
1480            .map_err(|e| XlogError::Kernel(format!("cache_store_weights_false failed: {}", e)))?;
1481        }
1482
1483        // No device synchronize: same-stream ordering guarantees visibility.
1484        Ok(())
1485    }
1486
1487    /// Replaces log-weight tables for an already materialized slot.
1488    pub fn overwrite_weights(
1489        &mut self,
1490        handle: &GpuCircuitCacheHandle,
1491        weights_true: &TrackedCudaSlice<f64>,
1492        weights_false: &TrackedCudaSlice<f64>,
1493    ) -> Result<()> {
1494        let weights_len = handle.max_var.checked_add(1).ok_or_else(|| {
1495            XlogError::Compilation("GpuCircuitCache overwrite_weights max_var overflow".to_string())
1496        })?;
1497        let weights_len_usize = usize::try_from(weights_len).map_err(|_| {
1498            XlogError::Compilation("GpuCircuitCache overwrite_weights len overflow".to_string())
1499        })?;
1500        if weights_true.len() < weights_len_usize || weights_false.len() < weights_len_usize {
1501            return Err(XlogError::Compilation(format!(
1502                "GpuCircuitCache overwrite_weights requires weights len >= {}, got true={} false={}",
1503                weights_len,
1504                weights_true.len(),
1505                weights_false.len()
1506            )));
1507        }
1508
1509        let device = self.provider.device().inner();
1510        let store_f64 = device
1511            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_F64)
1512            .ok_or_else(|| XlogError::Kernel("cache_store_f64 kernel not found".to_string()))?;
1513
1514        let block_dim = 256u32;
1515        let grid_dim = if weights_len == 0 {
1516            0
1517        } else {
1518            weights_len.div_ceil(block_dim)
1519        };
1520        if grid_dim != 0 {
1521            let var_stride = self.var_cap.checked_add(1).ok_or_else(|| {
1522                XlogError::Compilation(
1523                    "GpuCircuitCache overwrite_weights var_cap overflow".to_string(),
1524                )
1525            })?;
1526            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1527            unsafe {
1528                store_f64.clone().launch(
1529                    LaunchConfig {
1530                        grid_dim: (grid_dim, 1, 1),
1531                        block_dim: (block_dim, 1, 1),
1532                        shared_mem_bytes: 0,
1533                    },
1534                    (
1535                        handle.slot_device(),
1536                        &self.always_on,
1537                        var_stride,
1538                        weights_true,
1539                        &mut self.var_log_true,
1540                        weights_len,
1541                    ),
1542                )
1543            }
1544            .map_err(|e| {
1545                XlogError::Kernel(format!("cache_overwrite_weights_true failed: {}", e))
1546            })?;
1547
1548            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1549            unsafe {
1550                store_f64.clone().launch(
1551                    LaunchConfig {
1552                        grid_dim: (grid_dim, 1, 1),
1553                        block_dim: (block_dim, 1, 1),
1554                        shared_mem_bytes: 0,
1555                    },
1556                    (
1557                        handle.slot_device(),
1558                        &self.always_on,
1559                        var_stride,
1560                        weights_false,
1561                        &mut self.var_log_false,
1562                        weights_len,
1563                    ),
1564                )
1565            }
1566            .map_err(|e| {
1567                XlogError::Kernel(format!("cache_overwrite_weights_false failed: {}", e))
1568            })?;
1569        }
1570
1571        // No device synchronize: same-stream ordering guarantees visibility.
1572        Ok(())
1573    }
1574
1575    /// Stores the free-variable mask associated with a compiled slot.
1576    pub fn store_free_var_mask(
1577        &mut self,
1578        handle: &GpuCircuitCacheHandle,
1579        mask: &TrackedCudaSlice<u8>,
1580    ) -> Result<()> {
1581        let mask_len = u32::try_from(mask.len()).map_err(|_| {
1582            XlogError::Compilation("GpuCircuitCache free_var_mask len overflow".to_string())
1583        })?;
1584        let expected_len = handle.max_var.checked_add(1).ok_or_else(|| {
1585            XlogError::Compilation("GpuCircuitCache free_var_mask max_var overflow".to_string())
1586        })?;
1587        if mask_len != expected_len {
1588            return Err(XlogError::Compilation(format!(
1589                "GpuCircuitCache free_var_mask len {} != expected {}",
1590                mask_len, expected_len
1591            )));
1592        }
1593        let var_stride = self.var_cap.checked_add(1).ok_or_else(|| {
1594            XlogError::Compilation("GpuCircuitCache free_var_mask var_cap overflow".to_string())
1595        })?;
1596        if expected_len > var_stride {
1597            return Err(XlogError::Compilation(format!(
1598                "GpuCircuitCache free_var_mask len {} exceeds var_cap+1 {}",
1599                expected_len, var_stride
1600            )));
1601        }
1602
1603        let device = self.provider.device().inner();
1604        let store_u8 = device
1605            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_U8)
1606            .ok_or_else(|| XlogError::Kernel("cache_store_u8 kernel not found".to_string()))?;
1607
1608        let block_dim = 256u32;
1609        let grid_dim = mask_len.div_ceil(block_dim);
1610        if grid_dim == 0 {
1611            return Ok(());
1612        }
1613
1614        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1615        unsafe {
1616            store_u8.clone().launch(
1617                LaunchConfig {
1618                    grid_dim: (grid_dim, 1, 1),
1619                    block_dim: (block_dim, 1, 1),
1620                    shared_mem_bytes: 0,
1621                },
1622                (
1623                    handle.slot_device(),
1624                    handle.compile_needed_device(),
1625                    var_stride,
1626                    mask,
1627                    &mut self.free_var_mask,
1628                    mask_len,
1629                ),
1630            )
1631        }
1632        .map_err(|e| XlogError::Kernel(format!("cache_store_free_var_mask failed: {}", e)))?;
1633
1634        // No device synchronize: same-stream ordering guarantees visibility.
1635        let slot_idx = handle.slot_index() as usize;
1636        debug_assert!(
1637            slot_idx < self.has_free_var_mask.len(),
1638            "slot_index {} exceeds num_slots {}",
1639            slot_idx,
1640            self.has_free_var_mask.len()
1641        );
1642        if slot_idx < self.has_free_var_mask.len() {
1643            self.has_free_var_mask[slot_idx] = true;
1644        }
1645        Ok(())
1646    }
1647
1648    /// Populate a cache slot from host-resident arrays loaded from the disk cache.
1649    ///
1650    /// This mirrors [`store_from_xgcf`] but takes a [`disk_cache::CircuitArtifact`]
1651    /// (host `Vec`s) instead of a device-resident `GpuXgcf`. Each host array is
1652    /// uploaded to a temporary device buffer and then stored into the slot via the
1653    /// same `cache_store_*` kernels.
1654    pub(crate) fn restore_from_host_arrays(
1655        &mut self,
1656        handle: &mut GpuCircuitCacheHandle,
1657        artifact: &disk_cache::CircuitArtifact,
1658    ) -> Result<()> {
1659        // -- Validate sizes against cache caps --
1660        let num_nodes = artifact.num_nodes;
1661        if num_nodes == 0 {
1662            return Err(XlogError::Compilation(
1663                "GpuCircuitCache restore: num_nodes must be > 0".to_string(),
1664            ));
1665        }
1666        if num_nodes > self.node_cap {
1667            return Err(XlogError::Compilation(format!(
1668                "GpuCircuitCache restore: num_nodes {} exceeds node_cap {}",
1669                num_nodes, self.node_cap
1670            )));
1671        }
1672
1673        let num_edges = artifact.num_edges;
1674        if num_edges > self.edge_cap {
1675            return Err(XlogError::Compilation(format!(
1676                "GpuCircuitCache restore: num_edges {} exceeds edge_cap {}",
1677                num_edges, self.edge_cap
1678            )));
1679        }
1680
1681        let num_levels = artifact.num_levels;
1682        if num_levels == 0 {
1683            return Err(XlogError::Compilation(
1684                "GpuCircuitCache restore: num_levels must be > 0".to_string(),
1685            ));
1686        }
1687        if num_levels > self.level_cap {
1688            return Err(XlogError::Compilation(format!(
1689                "GpuCircuitCache restore: num_levels {} exceeds level_cap {}",
1690                num_levels, self.level_cap
1691            )));
1692        }
1693
1694        let root = artifact.root;
1695        if root >= num_nodes {
1696            return Err(XlogError::Compilation(format!(
1697                "GpuCircuitCache restore: root {} out of bounds (num_nodes={})",
1698                root, num_nodes
1699            )));
1700        }
1701
1702        let max_var = artifact.max_var;
1703        if max_var > self.var_cap {
1704            return Err(XlogError::Compilation(format!(
1705                "GpuCircuitCache restore: max_var {} exceeds var_cap {}",
1706                max_var, self.var_cap
1707            )));
1708        }
1709
1710        let expected_child_offsets = (num_nodes as usize) + 1;
1711        if artifact.child_offsets.len() < expected_child_offsets {
1712            return Err(XlogError::Compilation(format!(
1713                "GpuCircuitCache restore: child_offsets len {} < num_nodes+1 {}",
1714                artifact.child_offsets.len(),
1715                expected_child_offsets
1716            )));
1717        }
1718        if artifact.level_nodes.len() < num_nodes as usize {
1719            return Err(XlogError::Compilation(format!(
1720                "GpuCircuitCache restore: level_nodes len {} < num_nodes {}",
1721                artifact.level_nodes.len(),
1722                num_nodes
1723            )));
1724        }
1725        let expected_level_offsets = (num_levels as usize) + 1;
1726        if artifact.level_offsets.len() != expected_level_offsets {
1727            return Err(XlogError::Compilation(format!(
1728                "GpuCircuitCache restore: level_offsets len {} != num_levels+1 {}",
1729                artifact.level_offsets.len(),
1730                expected_level_offsets
1731            )));
1732        }
1733
1734        // -- Set handle metadata --
1735        handle.num_nodes = num_nodes;
1736        handle.num_levels = num_levels;
1737        handle.root = root;
1738        handle.max_var = max_var;
1739
1740        // -- Load kernels --
1741        let device = self.provider.device().inner();
1742        let memory = self.provider.memory();
1743
1744        let store_u8 = device
1745            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_U8)
1746            .ok_or_else(|| XlogError::Kernel("cache_store_u8 kernel not found".to_string()))?;
1747        let store_u32 = device
1748            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_U32)
1749            .ok_or_else(|| XlogError::Kernel("cache_store_u32 kernel not found".to_string()))?;
1750        let store_i32 = device
1751            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_I32)
1752            .ok_or_else(|| XlogError::Kernel("cache_store_i32 kernel not found".to_string()))?;
1753        let store_meta = device
1754            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_META)
1755            .ok_or_else(|| XlogError::Kernel("cache_store_meta kernel not found".to_string()))?;
1756
1757        let block_dim = 256u32;
1758
1759        let node_stride = self.node_cap;
1760        let offset_stride = self.node_cap.checked_add(1).ok_or_else(|| {
1761            XlogError::Compilation("GpuCircuitCache restore: node_cap overflow".to_string())
1762        })?;
1763        let level_offset_stride = self.level_cap.checked_add(1).ok_or_else(|| {
1764            XlogError::Compilation("GpuCircuitCache restore: level_cap overflow".to_string())
1765        })?;
1766        let var_stride = self.var_cap.checked_add(1).ok_or_else(|| {
1767            XlogError::Compilation("GpuCircuitCache restore: var_cap overflow".to_string())
1768        })?;
1769
1770        let num_nodes_plus1 = num_nodes.checked_add(1).ok_or_else(|| {
1771            XlogError::Compilation("GpuCircuitCache restore: num_nodes overflow".to_string())
1772        })?;
1773        let num_levels_plus1 = num_levels.checked_add(1).ok_or_else(|| {
1774            XlogError::Compilation("GpuCircuitCache restore: num_levels overflow".to_string())
1775        })?;
1776
1777        // -- Upload node_type (u8, num_nodes elements) --
1778        let grid_nodes = cache_grid_dim_for_u32_count(
1779            "GpuCircuitCache restore node_type",
1780            num_nodes,
1781            block_dim,
1782        )?;
1783        if grid_nodes != 0 {
1784            let mut d_node_type = memory.alloc::<u8>(num_nodes as usize)?;
1785            self.provider
1786                .htod_sync_copy_into_tracked(
1787                    &artifact.node_type[..num_nodes as usize],
1788                    &mut d_node_type,
1789                )
1790                .map_err(|e| XlogError::Kernel(format!("restore htod node_type failed: {}", e)))?;
1791            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1792            unsafe {
1793                store_u8.clone().launch(
1794                    LaunchConfig {
1795                        grid_dim: (grid_nodes, 1, 1),
1796                        block_dim: (block_dim, 1, 1),
1797                        shared_mem_bytes: 0,
1798                    },
1799                    (
1800                        handle.slot_device(),
1801                        handle.compile_needed_device(),
1802                        node_stride,
1803                        &d_node_type,
1804                        &mut self.node_type,
1805                        num_nodes,
1806                    ),
1807                )
1808            }
1809            .map_err(|e| {
1810                XlogError::Kernel(format!("restore cache_store node_type failed: {}", e))
1811            })?;
1812        }
1813
1814        // -- Upload child_offsets (u32, num_nodes+1 elements) --
1815        let grid_offsets = cache_grid_dim_for_u32_count(
1816            "GpuCircuitCache restore child_offsets",
1817            num_nodes_plus1,
1818            block_dim,
1819        )?;
1820        if grid_offsets != 0 {
1821            let mut d_child_offsets = memory.alloc::<u32>(num_nodes_plus1 as usize)?;
1822            self.provider
1823                .htod_sync_copy_into_tracked(
1824                    &artifact.child_offsets[..num_nodes_plus1 as usize],
1825                    &mut d_child_offsets,
1826                )
1827                .map_err(|e| {
1828                    XlogError::Kernel(format!("restore htod child_offsets failed: {}", e))
1829                })?;
1830            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1831            unsafe {
1832                store_u32.clone().launch(
1833                    LaunchConfig {
1834                        grid_dim: (grid_offsets, 1, 1),
1835                        block_dim: (block_dim, 1, 1),
1836                        shared_mem_bytes: 0,
1837                    },
1838                    (
1839                        handle.slot_device(),
1840                        handle.compile_needed_device(),
1841                        offset_stride,
1842                        &d_child_offsets,
1843                        &mut self.child_offsets,
1844                        num_nodes_plus1,
1845                    ),
1846                )
1847            }
1848            .map_err(|e| {
1849                XlogError::Kernel(format!("restore cache_store child_offsets failed: {}", e))
1850            })?;
1851        }
1852
1853        // -- Upload child_indices (u32, num_edges elements) --
1854        let grid_edges = cache_grid_dim_for_u32_count(
1855            "GpuCircuitCache restore child_indices",
1856            num_edges,
1857            block_dim,
1858        )?;
1859        if grid_edges != 0 {
1860            let mut d_child_indices = memory.alloc::<u32>(num_edges as usize)?;
1861            self.provider
1862                .htod_sync_copy_into_tracked(
1863                    &artifact.child_indices[..num_edges as usize],
1864                    &mut d_child_indices,
1865                )
1866                .map_err(|e| {
1867                    XlogError::Kernel(format!("restore htod child_indices failed: {}", e))
1868                })?;
1869            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1870            unsafe {
1871                store_u32.clone().launch(
1872                    LaunchConfig {
1873                        grid_dim: (grid_edges, 1, 1),
1874                        block_dim: (block_dim, 1, 1),
1875                        shared_mem_bytes: 0,
1876                    },
1877                    (
1878                        handle.slot_device(),
1879                        handle.compile_needed_device(),
1880                        self.edge_cap,
1881                        &d_child_indices,
1882                        &mut self.child_indices,
1883                        num_edges,
1884                    ),
1885                )
1886            }
1887            .map_err(|e| {
1888                XlogError::Kernel(format!("restore cache_store child_indices failed: {}", e))
1889            })?;
1890        }
1891
1892        // -- Upload lit (i32, num_nodes elements) --
1893        if grid_nodes != 0 {
1894            let mut d_lit = memory.alloc::<i32>(num_nodes as usize)?;
1895            self.provider
1896                .htod_sync_copy_into_tracked(&artifact.lit[..num_nodes as usize], &mut d_lit)
1897                .map_err(|e| XlogError::Kernel(format!("restore htod lit failed: {}", e)))?;
1898            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1899            unsafe {
1900                store_i32.clone().launch(
1901                    LaunchConfig {
1902                        grid_dim: (grid_nodes, 1, 1),
1903                        block_dim: (block_dim, 1, 1),
1904                        shared_mem_bytes: 0,
1905                    },
1906                    (
1907                        handle.slot_device(),
1908                        handle.compile_needed_device(),
1909                        node_stride,
1910                        &d_lit,
1911                        &mut self.lit,
1912                        num_nodes,
1913                    ),
1914                )
1915            }
1916            .map_err(|e| XlogError::Kernel(format!("restore cache_store lit failed: {}", e)))?;
1917
1918            // -- Upload decision_var (u32, num_nodes elements) --
1919            let mut d_decision_var = memory.alloc::<u32>(num_nodes as usize)?;
1920            self.provider
1921                .htod_sync_copy_into_tracked(
1922                    &artifact.decision_var[..num_nodes as usize],
1923                    &mut d_decision_var,
1924                )
1925                .map_err(|e| {
1926                    XlogError::Kernel(format!("restore htod decision_var failed: {}", e))
1927                })?;
1928            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1929            unsafe {
1930                store_u32.clone().launch(
1931                    LaunchConfig {
1932                        grid_dim: (grid_nodes, 1, 1),
1933                        block_dim: (block_dim, 1, 1),
1934                        shared_mem_bytes: 0,
1935                    },
1936                    (
1937                        handle.slot_device(),
1938                        handle.compile_needed_device(),
1939                        node_stride,
1940                        &d_decision_var,
1941                        &mut self.decision_var,
1942                        num_nodes,
1943                    ),
1944                )
1945            }
1946            .map_err(|e| {
1947                XlogError::Kernel(format!("restore cache_store decision_var failed: {}", e))
1948            })?;
1949
1950            // -- Upload decision_child_false (u32, num_nodes elements) --
1951            let mut d_decision_child_false = memory.alloc::<u32>(num_nodes as usize)?;
1952            self.provider
1953                .htod_sync_copy_into_tracked(
1954                    &artifact.decision_child_false[..num_nodes as usize],
1955                    &mut d_decision_child_false,
1956                )
1957                .map_err(|e| {
1958                    XlogError::Kernel(format!("restore htod decision_child_false failed: {}", e))
1959                })?;
1960            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1961            unsafe {
1962                store_u32.clone().launch(
1963                    LaunchConfig {
1964                        grid_dim: (grid_nodes, 1, 1),
1965                        block_dim: (block_dim, 1, 1),
1966                        shared_mem_bytes: 0,
1967                    },
1968                    (
1969                        handle.slot_device(),
1970                        handle.compile_needed_device(),
1971                        node_stride,
1972                        &d_decision_child_false,
1973                        &mut self.decision_child_false,
1974                        num_nodes,
1975                    ),
1976                )
1977            }
1978            .map_err(|e| {
1979                XlogError::Kernel(format!(
1980                    "restore cache_store decision_child_false failed: {}",
1981                    e
1982                ))
1983            })?;
1984
1985            // -- Upload decision_child_true (u32, num_nodes elements) --
1986            let mut d_decision_child_true = memory.alloc::<u32>(num_nodes as usize)?;
1987            self.provider
1988                .htod_sync_copy_into_tracked(
1989                    &artifact.decision_child_true[..num_nodes as usize],
1990                    &mut d_decision_child_true,
1991                )
1992                .map_err(|e| {
1993                    XlogError::Kernel(format!("restore htod decision_child_true failed: {}", e))
1994                })?;
1995            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1996            unsafe {
1997                store_u32.clone().launch(
1998                    LaunchConfig {
1999                        grid_dim: (grid_nodes, 1, 1),
2000                        block_dim: (block_dim, 1, 1),
2001                        shared_mem_bytes: 0,
2002                    },
2003                    (
2004                        handle.slot_device(),
2005                        handle.compile_needed_device(),
2006                        node_stride,
2007                        &d_decision_child_true,
2008                        &mut self.decision_child_true,
2009                        num_nodes,
2010                    ),
2011                )
2012            }
2013            .map_err(|e| {
2014                XlogError::Kernel(format!(
2015                    "restore cache_store decision_child_true failed: {}",
2016                    e
2017                ))
2018            })?;
2019
2020            // -- Upload level_nodes (u32, num_nodes elements) --
2021            let mut d_level_nodes = memory.alloc::<u32>(num_nodes as usize)?;
2022            self.provider
2023                .htod_sync_copy_into_tracked(
2024                    &artifact.level_nodes[..num_nodes as usize],
2025                    &mut d_level_nodes,
2026                )
2027                .map_err(|e| {
2028                    XlogError::Kernel(format!("restore htod level_nodes failed: {}", e))
2029                })?;
2030            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2031            unsafe {
2032                store_u32.clone().launch(
2033                    LaunchConfig {
2034                        grid_dim: (grid_nodes, 1, 1),
2035                        block_dim: (block_dim, 1, 1),
2036                        shared_mem_bytes: 0,
2037                    },
2038                    (
2039                        handle.slot_device(),
2040                        handle.compile_needed_device(),
2041                        node_stride,
2042                        &d_level_nodes,
2043                        &mut self.level_nodes,
2044                        num_nodes,
2045                    ),
2046                )
2047            }
2048            .map_err(|e| {
2049                XlogError::Kernel(format!("restore cache_store level_nodes failed: {}", e))
2050            })?;
2051        }
2052
2053        // -- Upload level_offsets (u32, num_levels+1 elements) --
2054        let grid_levels = cache_grid_dim_for_u32_count(
2055            "GpuCircuitCache restore level_offsets",
2056            num_levels_plus1,
2057            block_dim,
2058        )?;
2059        if grid_levels != 0 {
2060            let mut d_level_offsets = memory.alloc::<u32>(num_levels_plus1 as usize)?;
2061            self.provider
2062                .htod_sync_copy_into_tracked(
2063                    &artifact.level_offsets[..num_levels_plus1 as usize],
2064                    &mut d_level_offsets,
2065                )
2066                .map_err(|e| {
2067                    XlogError::Kernel(format!("restore htod level_offsets failed: {}", e))
2068                })?;
2069            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2070            unsafe {
2071                store_u32.clone().launch(
2072                    LaunchConfig {
2073                        grid_dim: (grid_levels, 1, 1),
2074                        block_dim: (block_dim, 1, 1),
2075                        shared_mem_bytes: 0,
2076                    },
2077                    (
2078                        handle.slot_device(),
2079                        handle.compile_needed_device(),
2080                        level_offset_stride,
2081                        &d_level_offsets,
2082                        &mut self.level_offsets,
2083                        num_levels_plus1,
2084                    ),
2085                )
2086            }
2087            .map_err(|e| {
2088                XlogError::Kernel(format!("restore cache_store level_offsets failed: {}", e))
2089            })?;
2090        }
2091
2092        // -- Store metadata via cache_store_meta kernel --
2093        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2094        unsafe {
2095            store_meta.clone().launch(
2096                LaunchConfig {
2097                    grid_dim: (1, 1, 1),
2098                    block_dim: (1, 1, 1),
2099                    shared_mem_bytes: 0,
2100                },
2101                (
2102                    handle.slot_device(),
2103                    handle.compile_needed_device(),
2104                    self.num_slots,
2105                    num_nodes,
2106                    num_levels,
2107                    root,
2108                    max_var,
2109                    &mut self.meta_num_nodes,
2110                    &mut self.meta_num_levels,
2111                    &mut self.meta_root,
2112                    &mut self.meta_max_var,
2113                ),
2114            )
2115        }
2116        .map_err(|e| XlogError::Kernel(format!("restore cache_store_meta failed: {}", e)))?;
2117
2118        // -- Zero the free_var_mask region for this slot, then conditionally write --
2119        let slot_idx = handle.slot_index() as usize;
2120
2121        // Zero the slot's mask region by uploading a zero buffer and storing it.
2122        // We always zero to ensure stale mask data from a previous occupant is cleared.
2123        let mask_cap = var_stride; // max_var+1 capacity per slot
2124        let grid_mask_zero = cache_grid_dim_for_u32_count(
2125            "GpuCircuitCache restore zero free_var_mask",
2126            mask_cap,
2127            block_dim,
2128        )?;
2129        if grid_mask_zero != 0 {
2130            let mut d_zeros = memory.alloc::<u8>(mask_cap as usize)?;
2131            device.memset_zeros(&mut d_zeros).map_err(|e| {
2132                XlogError::Kernel(format!("restore memset_zeros free_var_mask failed: {}", e))
2133            })?;
2134            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2135            unsafe {
2136                store_u8.clone().launch(
2137                    LaunchConfig {
2138                        grid_dim: (grid_mask_zero, 1, 1),
2139                        block_dim: (block_dim, 1, 1),
2140                        shared_mem_bytes: 0,
2141                    },
2142                    (
2143                        handle.slot_device(),
2144                        handle.compile_needed_device(),
2145                        var_stride,
2146                        &d_zeros,
2147                        &mut self.free_var_mask,
2148                        mask_cap,
2149                    ),
2150                )
2151            }
2152            .map_err(|e| {
2153                XlogError::Kernel(format!(
2154                    "restore cache_store zero free_var_mask failed: {}",
2155                    e
2156                ))
2157            })?;
2158        }
2159
2160        // Write the actual free_var_mask if the artifact has one.
2161        let has_mask = artifact.has_free_var_mask && !artifact.free_var_mask.is_empty();
2162        if has_mask {
2163            let mask_len = max_var.checked_add(1).ok_or_else(|| {
2164                XlogError::Compilation(
2165                    "GpuCircuitCache restore: free_var_mask max_var overflow".to_string(),
2166                )
2167            })?;
2168            let actual_len = std::cmp::min(mask_len as usize, artifact.free_var_mask.len());
2169            if actual_len > 0 {
2170                let actual_len_u32 = u32::try_from(actual_len).map_err(|_| {
2171                    XlogError::Compilation(
2172                        "GpuCircuitCache restore free_var_mask len exceeds u32".to_string(),
2173                    )
2174                })?;
2175                let grid_mask = cache_grid_dim_for_u32_count(
2176                    "GpuCircuitCache restore free_var_mask",
2177                    actual_len_u32,
2178                    block_dim,
2179                )?;
2180                if grid_mask != 0 {
2181                    let mut d_mask = memory.alloc::<u8>(actual_len)?;
2182                    self.provider
2183                        .htod_sync_copy_into_tracked(
2184                            &artifact.free_var_mask[..actual_len],
2185                            &mut d_mask,
2186                        )
2187                        .map_err(|e| {
2188                            XlogError::Kernel(format!("restore htod free_var_mask failed: {}", e))
2189                        })?;
2190                    // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2191                    unsafe {
2192                        store_u8.clone().launch(
2193                            LaunchConfig {
2194                                grid_dim: (grid_mask, 1, 1),
2195                                block_dim: (block_dim, 1, 1),
2196                                shared_mem_bytes: 0,
2197                            },
2198                            (
2199                                handle.slot_device(),
2200                                handle.compile_needed_device(),
2201                                var_stride,
2202                                &d_mask,
2203                                &mut self.free_var_mask,
2204                                actual_len_u32,
2205                            ),
2206                        )
2207                    }
2208                    .map_err(|e| {
2209                        XlogError::Kernel(format!(
2210                            "restore cache_store free_var_mask failed: {}",
2211                            e
2212                        ))
2213                    })?;
2214                }
2215            }
2216        }
2217
2218        // Set per-slot has_free_var_mask flag.
2219        debug_assert!(
2220            slot_idx < self.has_free_var_mask.len(),
2221            "slot_index {} exceeds num_slots {}",
2222            slot_idx,
2223            self.has_free_var_mask.len()
2224        );
2225        if slot_idx < self.has_free_var_mask.len() {
2226            self.has_free_var_mask[slot_idx] = has_mask;
2227        }
2228
2229        // No device synchronize needed: all stores are H→D copies followed by
2230        // same-stream kernel launches, so ordering is guaranteed.
2231        Ok(())
2232    }
2233
2234    /// Extract a [`disk_cache::CircuitArtifact`] from a populated GPU cache slot.
2235    ///
2236    /// This is the inverse of [`restore_from_host_arrays`]: it reads device-resident
2237    /// topology arrays from the cache slot and builds host vectors suitable for disk
2238    /// serialization. The caller must ensure the slot has been populated (i.e. after
2239    /// `store_from_xgcf` + `store_free_var_mask`).
2240    pub(crate) fn build_artifact_from_device(
2241        &self,
2242        handle: &GpuCircuitCacheHandle,
2243        provider: &Arc<CudaKernelProvider>,
2244    ) -> Result<disk_cache::CircuitArtifact> {
2245        let device = provider.device().inner();
2246        let slot = handle.slot_index() as usize;
2247        let num_nodes = handle.num_nodes();
2248        let num_levels = handle.num_levels();
2249        let root = handle.root();
2250        let max_var = handle.max_var();
2251
2252        if num_nodes == 0 {
2253            return Err(XlogError::Compilation(
2254                "build_artifact_from_device: num_nodes is 0".to_string(),
2255            ));
2256        }
2257
2258        let node_stride = self.node_cap as usize;
2259        let offset_stride = (self.node_cap as usize) + 1;
2260        let edge_stride = self.edge_cap as usize;
2261        let level_offset_stride = (self.level_cap as usize) + 1;
2262        let var_stride = (self.var_cap as usize) + 1;
2263
2264        let slot_node_start = slot * node_stride;
2265        let slot_offset_start = slot * offset_stride;
2266        let slot_level_offset_start = slot * level_offset_stride;
2267        let slot_var_start = slot * var_stride;
2268
2269        let nn = num_nodes as usize;
2270        let nn1 = nn + 1;
2271        let nl1 = (num_levels as usize) + 1;
2272
2273        // Determine num_edges from child_offsets[num_nodes] - child_offsets[0].
2274        // We read child_offsets first, then derive num_edges from it.
2275        let child_offsets_view = self
2276            .child_offsets
2277            .slice(slot_offset_start..(slot_offset_start + nn1));
2278        let child_offsets: Vec<u32> = device
2279            .dtoh_sync_copy(&child_offsets_view)
2280            .map_err(|e| XlogError::Kernel(format!("build_artifact dtoh child_offsets: {}", e)))?;
2281        let num_edges = if nn1 > 0 {
2282            child_offsets[nn]
2283                .checked_sub(child_offsets[0])
2284                .ok_or_else(|| {
2285                    XlogError::Compilation(
2286                        "build_artifact_from_device: child_offsets[num_nodes] < child_offsets[0]"
2287                            .to_string(),
2288                    )
2289                })?
2290        } else {
2291            0
2292        };
2293
2294        // Read child_indices from the edge region.
2295        let slot_edge_start = slot * edge_stride;
2296        let ne = num_edges as usize;
2297        let child_indices: Vec<u32> = if ne > 0 {
2298            let view = self
2299                .child_indices
2300                .slice(slot_edge_start..(slot_edge_start + ne));
2301            device.dtoh_sync_copy(&view).map_err(|e| {
2302                XlogError::Kernel(format!("build_artifact dtoh child_indices: {}", e))
2303            })?
2304        } else {
2305            Vec::new()
2306        };
2307
2308        // node_type (u8)
2309        let node_type_view = self
2310            .node_type
2311            .slice(slot_node_start..(slot_node_start + nn));
2312        let node_type: Vec<u8> = device
2313            .dtoh_sync_copy(&node_type_view)
2314            .map_err(|e| XlogError::Kernel(format!("build_artifact dtoh node_type: {}", e)))?;
2315
2316        // lit (i32)
2317        let lit_view = self.lit.slice(slot_node_start..(slot_node_start + nn));
2318        let lit: Vec<i32> = device
2319            .dtoh_sync_copy(&lit_view)
2320            .map_err(|e| XlogError::Kernel(format!("build_artifact dtoh lit: {}", e)))?;
2321
2322        // decision_var (u32)
2323        let dv_view = self
2324            .decision_var
2325            .slice(slot_node_start..(slot_node_start + nn));
2326        let decision_var: Vec<u32> = device
2327            .dtoh_sync_copy(&dv_view)
2328            .map_err(|e| XlogError::Kernel(format!("build_artifact dtoh decision_var: {}", e)))?;
2329
2330        // decision_child_false (u32)
2331        let dcf_view = self
2332            .decision_child_false
2333            .slice(slot_node_start..(slot_node_start + nn));
2334        let decision_child_false: Vec<u32> = device.dtoh_sync_copy(&dcf_view).map_err(|e| {
2335            XlogError::Kernel(format!("build_artifact dtoh decision_child_false: {}", e))
2336        })?;
2337
2338        // decision_child_true (u32)
2339        let dct_view = self
2340            .decision_child_true
2341            .slice(slot_node_start..(slot_node_start + nn));
2342        let decision_child_true: Vec<u32> = device.dtoh_sync_copy(&dct_view).map_err(|e| {
2343            XlogError::Kernel(format!("build_artifact dtoh decision_child_true: {}", e))
2344        })?;
2345
2346        // level_nodes (u32)
2347        let ln_view = self
2348            .level_nodes
2349            .slice(slot_node_start..(slot_node_start + nn));
2350        let level_nodes: Vec<u32> = device
2351            .dtoh_sync_copy(&ln_view)
2352            .map_err(|e| XlogError::Kernel(format!("build_artifact dtoh level_nodes: {}", e)))?;
2353
2354        // level_offsets (u32)
2355        let lo_view = self
2356            .level_offsets
2357            .slice(slot_level_offset_start..(slot_level_offset_start + nl1));
2358        let level_offsets: Vec<u32> = device
2359            .dtoh_sync_copy(&lo_view)
2360            .map_err(|e| XlogError::Kernel(format!("build_artifact dtoh level_offsets: {}", e)))?;
2361
2362        // free_var_mask (u8)
2363        let has_free_var_mask = self.has_free_var_mask_for_slot(slot as u32);
2364        let mask_len = (max_var as usize) + 1;
2365        let free_var_mask: Vec<u8> = if mask_len > 0 {
2366            let fvm_view = self
2367                .free_var_mask
2368                .slice(slot_var_start..(slot_var_start + mask_len));
2369            device.dtoh_sync_copy(&fvm_view).map_err(|e| {
2370                XlogError::Kernel(format!("build_artifact dtoh free_var_mask: {}", e))
2371            })?
2372        } else {
2373            Vec::new()
2374        };
2375
2376        Ok(disk_cache::CircuitArtifact {
2377            num_nodes,
2378            num_edges,
2379            num_levels,
2380            root,
2381            max_var,
2382            has_free_var_mask,
2383            node_type,
2384            child_offsets,
2385            child_indices,
2386            lit,
2387            decision_var,
2388            decision_child_false,
2389            decision_child_true,
2390            level_nodes,
2391            level_offsets,
2392            free_var_mask,
2393        })
2394    }
2395
2396    /// Evaluates cached logZ without a host transfer.
2397    ///
2398    /// Callers must ensure evaluated resident weights are NaN-free. NaN or undefined arithmetic
2399    /// that reaches evaluation is represented by a NaN sentinel in `out_log_z`; callers must reject
2400    /// that sentinel at an existing readback. Positive infinity remains valid for value-only
2401    /// evaluation.
2402    pub fn eval_log_wmc_device_inplace(
2403        &mut self,
2404        handle: &GpuCircuitCacheHandle,
2405        out_log_z: &mut TrackedCudaSlice<f64>,
2406    ) -> Result<()> {
2407        self.eval_log_wmc_device_only(handle, out_log_z)
2408    }
2409
2410    /// Device-only implementation of cached value evaluation; see
2411    /// [`Self::eval_log_wmc_device_inplace`] for its numeric preconditions and sentinel contract.
2412    pub fn eval_log_wmc_device_only(
2413        &mut self,
2414        handle: &GpuCircuitCacheHandle,
2415        out_log_z: &mut TrackedCudaSlice<f64>,
2416    ) -> Result<()> {
2417        if out_log_z.len() != 1 {
2418            return Err(XlogError::Compilation(format!(
2419                "GPU cache logZ output len {} != 1",
2420                out_log_z.len()
2421            )));
2422        }
2423
2424        {
2425            let device = self.provider.device().inner();
2426            let eval_all = device
2427                .get_func(
2428                    xlog_cuda::CIRCUIT_MODULE,
2429                    xlog_cuda::circuit_kernels::XGCF_EVAL_ALL_LEVELS_CACHED,
2430                )
2431                .ok_or_else(|| {
2432                    XlogError::Kernel("xgcf_eval_all_levels_cached kernel not found".to_string())
2433                })?;
2434
2435            let block_size: u32 = 256;
2436            let mut params: Vec<*mut std::ffi::c_void> = vec![
2437                handle.slot_device().as_kernel_param(),
2438                self.node_cap.as_kernel_param(),
2439                self.edge_cap.as_kernel_param(),
2440                self.level_cap.as_kernel_param(),
2441                self.var_cap.as_kernel_param(),
2442                (&self.node_type).as_kernel_param(),
2443                (&self.child_offsets).as_kernel_param(),
2444                (&self.child_indices).as_kernel_param(),
2445                (&self.lit).as_kernel_param(),
2446                (&self.decision_var).as_kernel_param(),
2447                (&self.decision_child_false).as_kernel_param(),
2448                (&self.decision_child_true).as_kernel_param(),
2449                (&self.level_nodes).as_kernel_param(),
2450                (&self.level_offsets).as_kernel_param(),
2451                (&self.var_log_true).as_kernel_param(),
2452                (&self.var_log_false).as_kernel_param(),
2453                (&self.values).as_kernel_param(),
2454                (&self.meta_num_levels).as_kernel_param(),
2455            ];
2456            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2457            unsafe {
2458                eval_all.clone().launch(
2459                    LaunchConfig {
2460                        grid_dim: (1, 1, 1),
2461                        block_dim: (block_size, 1, 1),
2462                        shared_mem_bytes: 0,
2463                    },
2464                    &mut params,
2465                )
2466            }
2467            .map_err(|e| XlogError::Kernel(format!("xgcf_eval_all_levels_cached failed: {}", e)))?;
2468        }
2469
2470        self.apply_free_var_correction_cached(handle, true, false)?;
2471
2472        let device = self.provider.device().inner();
2473        let copy_root = device
2474            .get_func(
2475                xlog_cuda::CIRCUIT_MODULE,
2476                xlog_cuda::circuit_kernels::XGCF_COPY_ROOT_CACHED_META,
2477            )
2478            .ok_or_else(|| {
2479                XlogError::Kernel("xgcf_copy_root_cached_meta kernel not found".to_string())
2480            })?;
2481        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2482        unsafe {
2483            copy_root.clone().launch(
2484                LaunchConfig {
2485                    grid_dim: (1, 1, 1),
2486                    block_dim: (1, 1, 1),
2487                    shared_mem_bytes: 0,
2488                },
2489                (
2490                    handle.slot_device(),
2491                    self.node_cap,
2492                    &self.values,
2493                    &self.meta_root,
2494                    out_log_z,
2495                ),
2496            )
2497        }
2498        .map_err(|e| XlogError::Kernel(format!("xgcf_copy_root_cached_meta failed: {}", e)))?;
2499
2500        // No device synchronize: callers read back with a synchronous host copy
2501        // or pass the result to subsequent GPU operations (same-stream ordering).
2502        Ok(())
2503    }
2504
2505    /// Evaluates cached gradients without a host transfer.
2506    ///
2507    /// Callers must validate numeric inputs and eventual outputs. Positive infinity is valid when
2508    /// the actual backward path remains finite. This asynchronous API cannot return a host-side
2509    /// numeric error; NaN or undefined normalization that reaches evaluation is observable as a
2510    /// non-finite device result.
2511    pub fn eval_grads_inplace(&mut self, handle: &GpuCircuitCacheHandle) -> Result<()> {
2512        let device = self.provider.device().inner();
2513        let eval_all = device
2514            .get_func(
2515                xlog_cuda::CIRCUIT_MODULE,
2516                xlog_cuda::circuit_kernels::XGCF_EVAL_ALL_LEVELS_CACHED,
2517            )
2518            .ok_or_else(|| {
2519                XlogError::Kernel("xgcf_eval_all_levels_cached kernel not found".to_string())
2520            })?;
2521        let block_size: u32 = 256;
2522        let mut params: Vec<*mut std::ffi::c_void> = vec![
2523            handle.slot_device().as_kernel_param(),
2524            self.node_cap.as_kernel_param(),
2525            self.edge_cap.as_kernel_param(),
2526            self.level_cap.as_kernel_param(),
2527            self.var_cap.as_kernel_param(),
2528            (&self.node_type).as_kernel_param(),
2529            (&self.child_offsets).as_kernel_param(),
2530            (&self.child_indices).as_kernel_param(),
2531            (&self.lit).as_kernel_param(),
2532            (&self.decision_var).as_kernel_param(),
2533            (&self.decision_child_false).as_kernel_param(),
2534            (&self.decision_child_true).as_kernel_param(),
2535            (&self.level_nodes).as_kernel_param(),
2536            (&self.level_offsets).as_kernel_param(),
2537            (&self.var_log_true).as_kernel_param(),
2538            (&self.var_log_false).as_kernel_param(),
2539            (&self.values).as_kernel_param(),
2540            (&self.meta_num_levels).as_kernel_param(),
2541        ];
2542        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2543        unsafe {
2544            eval_all.clone().launch(
2545                LaunchConfig {
2546                    grid_dim: (1, 1, 1),
2547                    block_dim: (block_size, 1, 1),
2548                    shared_mem_bytes: 0,
2549                },
2550                &mut params,
2551            )
2552        }
2553        .map_err(|e| XlogError::Kernel(format!("xgcf_eval_all_levels_cached failed: {}", e)))?;
2554
2555        let device = self.provider.device().inner();
2556        let store_f64 = device
2557            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_F64)
2558            .ok_or_else(|| XlogError::Kernel("cache_store_f64 kernel not found".to_string()))?;
2559
2560        let node_stride = self.node_cap;
2561        let var_stride = self.var_cap.checked_add(1).ok_or_else(|| {
2562            XlogError::Compilation("GPU cache eval_grads var_cap overflow".to_string())
2563        })?;
2564        let weights_len = self.var_cap.checked_add(1).ok_or_else(|| {
2565            XlogError::Compilation("GPU cache eval_grads var_cap overflow".to_string())
2566        })?;
2567
2568        let grid_nodes = cache_grid_dim_for_u32_count(
2569            "GpuCircuitCache eval_grads zero adj",
2570            self.node_cap,
2571            block_size,
2572        )?;
2573        if grid_nodes != 0 {
2574            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2575            unsafe {
2576                store_f64.clone().launch(
2577                    LaunchConfig {
2578                        grid_dim: (grid_nodes, 1, 1),
2579                        block_dim: (block_size, 1, 1),
2580                        shared_mem_bytes: 0,
2581                    },
2582                    (
2583                        handle.slot_device(),
2584                        &self.always_on,
2585                        node_stride,
2586                        &self.zero_f64,
2587                        &mut self.adj,
2588                        self.node_cap,
2589                    ),
2590                )
2591            }
2592            .map_err(|e| XlogError::Kernel(format!("cache zero adj failed: {}", e)))?;
2593        }
2594
2595        let grid_weights = cache_grid_dim_for_u32_count(
2596            "GpuCircuitCache eval_grads zero weights",
2597            weights_len,
2598            block_size,
2599        )?;
2600        if grid_weights != 0 {
2601            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2602            unsafe {
2603                store_f64.clone().launch(
2604                    LaunchConfig {
2605                        grid_dim: (grid_weights, 1, 1),
2606                        block_dim: (block_size, 1, 1),
2607                        shared_mem_bytes: 0,
2608                    },
2609                    (
2610                        handle.slot_device(),
2611                        &self.always_on,
2612                        var_stride,
2613                        &self.zero_f64,
2614                        &mut self.grad_true,
2615                        weights_len,
2616                    ),
2617                )
2618            }
2619            .map_err(|e| XlogError::Kernel(format!("cache zero grad_true failed: {}", e)))?;
2620
2621            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2622            unsafe {
2623                store_f64.clone().launch(
2624                    LaunchConfig {
2625                        grid_dim: (grid_weights, 1, 1),
2626                        block_dim: (block_size, 1, 1),
2627                        shared_mem_bytes: 0,
2628                    },
2629                    (
2630                        handle.slot_device(),
2631                        &self.always_on,
2632                        var_stride,
2633                        &self.zero_f64,
2634                        &mut self.grad_false,
2635                        weights_len,
2636                    ),
2637                )
2638            }
2639            .map_err(|e| XlogError::Kernel(format!("cache zero grad_false failed: {}", e)))?;
2640        }
2641
2642        let add_scalar = device
2643            .get_func(
2644                xlog_cuda::CIRCUIT_MODULE,
2645                xlog_cuda::circuit_kernels::XGCF_ADD_SCALAR_CACHED,
2646            )
2647            .ok_or_else(|| {
2648                XlogError::Kernel("xgcf_add_scalar_cached kernel not found".to_string())
2649            })?;
2650        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2651        unsafe {
2652            add_scalar.clone().launch(
2653                LaunchConfig {
2654                    grid_dim: (1, 1, 1),
2655                    block_dim: (1, 1, 1),
2656                    shared_mem_bytes: 0,
2657                },
2658                (
2659                    handle.slot_device(),
2660                    self.node_cap,
2661                    &mut self.adj,
2662                    &self.meta_root,
2663                    &self.one_f64,
2664                ),
2665            )
2666        }
2667        .map_err(|e| XlogError::Kernel(format!("xgcf_add_scalar_cached (adj) failed: {}", e)))?;
2668
2669        let propagate = device
2670            .get_func(
2671                xlog_cuda::CIRCUIT_MODULE,
2672                xlog_cuda::circuit_kernels::XGCF_BACKWARD_LEVEL_PROPAGATE_CACHED,
2673            )
2674            .ok_or_else(|| {
2675                XlogError::Kernel(
2676                    "xgcf_backward_level_propagate_cached kernel not found".to_string(),
2677                )
2678            })?;
2679        let decision_grad = device
2680            .get_func(
2681                xlog_cuda::CIRCUIT_MODULE,
2682                xlog_cuda::circuit_kernels::XGCF_BACKWARD_LEVEL_DECISION_GRAD_CACHED,
2683            )
2684            .ok_or_else(|| {
2685                XlogError::Kernel(
2686                    "xgcf_backward_level_decision_grad_cached kernel not found".to_string(),
2687                )
2688            })?;
2689        let lit_grad = device
2690            .get_func(
2691                xlog_cuda::CIRCUIT_MODULE,
2692                xlog_cuda::circuit_kernels::XGCF_BACKWARD_LEVEL_LIT_GRAD_CACHED,
2693            )
2694            .ok_or_else(|| {
2695                XlogError::Kernel(
2696                    "xgcf_backward_level_lit_grad_cached kernel not found".to_string(),
2697                )
2698            })?;
2699
2700        let num_blocks = self.node_cap.div_ceil(block_size);
2701        let num_levels = self.level_cap;
2702        for level in (0..num_levels).rev() {
2703            if num_blocks == 0 {
2704                continue;
2705            }
2706            let level_u32: u32 = level;
2707            let mut params: Vec<*mut std::ffi::c_void> = vec![
2708                handle.slot_device().as_kernel_param(),
2709                self.node_cap.as_kernel_param(),
2710                self.edge_cap.as_kernel_param(),
2711                self.level_cap.as_kernel_param(),
2712                self.var_cap.as_kernel_param(),
2713                (&self.node_type).as_kernel_param(),
2714                (&self.child_offsets).as_kernel_param(),
2715                (&self.child_indices).as_kernel_param(),
2716                (&self.decision_var).as_kernel_param(),
2717                (&self.decision_child_false).as_kernel_param(),
2718                (&self.decision_child_true).as_kernel_param(),
2719                (&self.level_nodes).as_kernel_param(),
2720                (&self.level_offsets).as_kernel_param(),
2721                level_u32.as_kernel_param(),
2722                (&self.var_log_true).as_kernel_param(),
2723                (&self.var_log_false).as_kernel_param(),
2724                (&self.values).as_kernel_param(),
2725                (&self.adj).as_kernel_param(),
2726                (&self.meta_num_levels).as_kernel_param(),
2727            ];
2728
2729            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2730            unsafe {
2731                propagate.clone().launch(
2732                    LaunchConfig {
2733                        grid_dim: (num_blocks, 1, 1),
2734                        block_dim: (block_size, 1, 1),
2735                        shared_mem_bytes: 0,
2736                    },
2737                    &mut params,
2738                )
2739            }
2740            .map_err(|e| {
2741                XlogError::Kernel(format!(
2742                    "xgcf_backward_level_propagate_cached failed: {}",
2743                    e
2744                ))
2745            })?;
2746
2747            let mut params: Vec<*mut std::ffi::c_void> = vec![
2748                handle.slot_device().as_kernel_param(),
2749                self.node_cap.as_kernel_param(),
2750                self.edge_cap.as_kernel_param(),
2751                self.level_cap.as_kernel_param(),
2752                self.var_cap.as_kernel_param(),
2753                (&self.node_type).as_kernel_param(),
2754                (&self.decision_var).as_kernel_param(),
2755                (&self.decision_child_false).as_kernel_param(),
2756                (&self.decision_child_true).as_kernel_param(),
2757                (&self.level_nodes).as_kernel_param(),
2758                (&self.level_offsets).as_kernel_param(),
2759                level_u32.as_kernel_param(),
2760                (&self.var_log_true).as_kernel_param(),
2761                (&self.var_log_false).as_kernel_param(),
2762                (&self.values).as_kernel_param(),
2763                (&self.adj).as_kernel_param(),
2764                (&self.grad_true).as_kernel_param(),
2765                (&self.grad_false).as_kernel_param(),
2766                (&self.meta_num_levels).as_kernel_param(),
2767            ];
2768
2769            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2770            unsafe {
2771                decision_grad.clone().launch(
2772                    LaunchConfig {
2773                        grid_dim: (num_blocks, 1, 1),
2774                        block_dim: (block_size, 1, 1),
2775                        shared_mem_bytes: 0,
2776                    },
2777                    &mut params,
2778                )
2779            }
2780            .map_err(|e| {
2781                XlogError::Kernel(format!(
2782                    "xgcf_backward_level_decision_grad_cached failed: {}",
2783                    e
2784                ))
2785            })?;
2786
2787            let mut params: Vec<*mut std::ffi::c_void> = vec![
2788                handle.slot_device().as_kernel_param(),
2789                self.node_cap.as_kernel_param(),
2790                self.edge_cap.as_kernel_param(),
2791                self.level_cap.as_kernel_param(),
2792                self.var_cap.as_kernel_param(),
2793                (&self.node_type).as_kernel_param(),
2794                (&self.lit).as_kernel_param(),
2795                (&self.level_nodes).as_kernel_param(),
2796                (&self.level_offsets).as_kernel_param(),
2797                level_u32.as_kernel_param(),
2798                (&self.adj).as_kernel_param(),
2799                (&self.grad_true).as_kernel_param(),
2800                (&self.grad_false).as_kernel_param(),
2801                (&self.meta_num_levels).as_kernel_param(),
2802            ];
2803
2804            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2805            unsafe {
2806                lit_grad.clone().launch(
2807                    LaunchConfig {
2808                        grid_dim: (num_blocks, 1, 1),
2809                        block_dim: (block_size, 1, 1),
2810                        shared_mem_bytes: 0,
2811                    },
2812                    &mut params,
2813                )
2814            }
2815            .map_err(|e| {
2816                XlogError::Kernel(format!("xgcf_backward_level_lit_grad_cached failed: {}", e))
2817            })?;
2818        }
2819
2820        self.apply_free_var_correction_cached(handle, true, true)?;
2821        // No device synchronize: callers batch multiple eval/backward calls
2822        // before syncing at the query boundary.
2823        Ok(())
2824    }
2825
2826    /// Like [`eval_grads_inplace`] but replaces the per-level backward loop
2827    /// with a single launch of `xgcf_backward_all_levels_cached`, and omits the
2828    /// trailing `device().synchronize()` so that the caller can batch multiple
2829    /// queries before syncing.
2830    /// The same caller-side numeric validation contract applies.
2831    pub fn eval_grads_inplace_fused(&mut self, handle: &GpuCircuitCacheHandle) -> Result<()> {
2832        let device = self.provider.device().inner();
2833        let eval_all = device
2834            .get_func(
2835                xlog_cuda::CIRCUIT_MODULE,
2836                xlog_cuda::circuit_kernels::XGCF_EVAL_ALL_LEVELS_CACHED,
2837            )
2838            .ok_or_else(|| {
2839                XlogError::Kernel("xgcf_eval_all_levels_cached kernel not found".to_string())
2840            })?;
2841        let block_size: u32 = 256;
2842        let mut params: Vec<*mut std::ffi::c_void> = vec![
2843            handle.slot_device().as_kernel_param(),
2844            self.node_cap.as_kernel_param(),
2845            self.edge_cap.as_kernel_param(),
2846            self.level_cap.as_kernel_param(),
2847            self.var_cap.as_kernel_param(),
2848            (&self.node_type).as_kernel_param(),
2849            (&self.child_offsets).as_kernel_param(),
2850            (&self.child_indices).as_kernel_param(),
2851            (&self.lit).as_kernel_param(),
2852            (&self.decision_var).as_kernel_param(),
2853            (&self.decision_child_false).as_kernel_param(),
2854            (&self.decision_child_true).as_kernel_param(),
2855            (&self.level_nodes).as_kernel_param(),
2856            (&self.level_offsets).as_kernel_param(),
2857            (&self.var_log_true).as_kernel_param(),
2858            (&self.var_log_false).as_kernel_param(),
2859            (&self.values).as_kernel_param(),
2860            (&self.meta_num_levels).as_kernel_param(),
2861        ];
2862        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2863        unsafe {
2864            eval_all.clone().launch(
2865                LaunchConfig {
2866                    grid_dim: (1, 1, 1),
2867                    block_dim: (block_size, 1, 1),
2868                    shared_mem_bytes: 0,
2869                },
2870                &mut params,
2871            )
2872        }
2873        .map_err(|e| XlogError::Kernel(format!("xgcf_eval_all_levels_cached failed: {}", e)))?;
2874
2875        let device = self.provider.device().inner();
2876        let store_f64 = device
2877            .get_func(CACHE_MODULE, cache_kernels::CACHE_STORE_F64)
2878            .ok_or_else(|| XlogError::Kernel("cache_store_f64 kernel not found".to_string()))?;
2879
2880        let node_stride = self.node_cap;
2881        let var_stride = self.var_cap.checked_add(1).ok_or_else(|| {
2882            XlogError::Compilation("GPU cache eval_grads var_cap overflow".to_string())
2883        })?;
2884        let weights_len = self.var_cap.checked_add(1).ok_or_else(|| {
2885            XlogError::Compilation("GPU cache eval_grads var_cap overflow".to_string())
2886        })?;
2887
2888        let grid_nodes = cache_grid_dim_for_u32_count(
2889            "GpuCircuitCache batched eval_grads zero adj",
2890            self.node_cap,
2891            block_size,
2892        )?;
2893        if grid_nodes != 0 {
2894            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2895            unsafe {
2896                store_f64.clone().launch(
2897                    LaunchConfig {
2898                        grid_dim: (grid_nodes, 1, 1),
2899                        block_dim: (block_size, 1, 1),
2900                        shared_mem_bytes: 0,
2901                    },
2902                    (
2903                        handle.slot_device(),
2904                        &self.always_on,
2905                        node_stride,
2906                        &self.zero_f64,
2907                        &mut self.adj,
2908                        self.node_cap,
2909                    ),
2910                )
2911            }
2912            .map_err(|e| XlogError::Kernel(format!("cache zero adj failed: {}", e)))?;
2913        }
2914
2915        let grid_weights = cache_grid_dim_for_u32_count(
2916            "GpuCircuitCache batched eval_grads zero weights",
2917            weights_len,
2918            block_size,
2919        )?;
2920        if grid_weights != 0 {
2921            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2922            unsafe {
2923                store_f64.clone().launch(
2924                    LaunchConfig {
2925                        grid_dim: (grid_weights, 1, 1),
2926                        block_dim: (block_size, 1, 1),
2927                        shared_mem_bytes: 0,
2928                    },
2929                    (
2930                        handle.slot_device(),
2931                        &self.always_on,
2932                        var_stride,
2933                        &self.zero_f64,
2934                        &mut self.grad_true,
2935                        weights_len,
2936                    ),
2937                )
2938            }
2939            .map_err(|e| XlogError::Kernel(format!("cache zero grad_true failed: {}", e)))?;
2940
2941            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2942            unsafe {
2943                store_f64.clone().launch(
2944                    LaunchConfig {
2945                        grid_dim: (grid_weights, 1, 1),
2946                        block_dim: (block_size, 1, 1),
2947                        shared_mem_bytes: 0,
2948                    },
2949                    (
2950                        handle.slot_device(),
2951                        &self.always_on,
2952                        var_stride,
2953                        &self.zero_f64,
2954                        &mut self.grad_false,
2955                        weights_len,
2956                    ),
2957                )
2958            }
2959            .map_err(|e| XlogError::Kernel(format!("cache zero grad_false failed: {}", e)))?;
2960        }
2961
2962        let add_scalar = device
2963            .get_func(
2964                xlog_cuda::CIRCUIT_MODULE,
2965                xlog_cuda::circuit_kernels::XGCF_ADD_SCALAR_CACHED,
2966            )
2967            .ok_or_else(|| {
2968                XlogError::Kernel("xgcf_add_scalar_cached kernel not found".to_string())
2969            })?;
2970        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
2971        unsafe {
2972            add_scalar.clone().launch(
2973                LaunchConfig {
2974                    grid_dim: (1, 1, 1),
2975                    block_dim: (1, 1, 1),
2976                    shared_mem_bytes: 0,
2977                },
2978                (
2979                    handle.slot_device(),
2980                    self.node_cap,
2981                    &mut self.adj,
2982                    &self.meta_root,
2983                    &self.one_f64,
2984                ),
2985            )
2986        }
2987        .map_err(|e| XlogError::Kernel(format!("xgcf_add_scalar_cached (adj) failed: {}", e)))?;
2988
2989        // Fused backward: single kernel replaces the per-level loop.
2990        let backward_all = device
2991            .get_func(
2992                xlog_cuda::CIRCUIT_MODULE,
2993                xlog_cuda::circuit_kernels::XGCF_BACKWARD_ALL_LEVELS_CACHED,
2994            )
2995            .ok_or_else(|| XlogError::Kernel("xgcf_backward_all_levels_cached not found".into()))?;
2996
2997        let mut params: Vec<*mut std::ffi::c_void> = vec![
2998            handle.slot_device().as_kernel_param(),
2999            self.node_cap.as_kernel_param(),
3000            self.edge_cap.as_kernel_param(),
3001            self.level_cap.as_kernel_param(),
3002            self.var_cap.as_kernel_param(),
3003            (&self.node_type).as_kernel_param(),
3004            (&self.child_offsets).as_kernel_param(),
3005            (&self.child_indices).as_kernel_param(),
3006            (&self.decision_var).as_kernel_param(),
3007            (&self.decision_child_false).as_kernel_param(),
3008            (&self.decision_child_true).as_kernel_param(),
3009            (&self.lit).as_kernel_param(),
3010            (&self.level_nodes).as_kernel_param(),
3011            (&self.level_offsets).as_kernel_param(),
3012            (&self.var_log_true).as_kernel_param(),
3013            (&self.var_log_false).as_kernel_param(),
3014            (&self.values).as_kernel_param(),
3015            (&self.adj).as_kernel_param(),
3016            (&self.grad_true).as_kernel_param(),
3017            (&self.grad_false).as_kernel_param(),
3018            (&self.meta_num_levels).as_kernel_param(),
3019        ];
3020
3021        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
3022        unsafe {
3023            backward_all.clone().launch(
3024                LaunchConfig {
3025                    grid_dim: (1, 1, 1),
3026                    block_dim: (block_size, 1, 1),
3027                    shared_mem_bytes: 0,
3028                },
3029                &mut params,
3030            )
3031        }
3032        .map_err(|e| XlogError::Kernel(format!("xgcf_backward_all_levels_cached failed: {}", e)))?;
3033
3034        self.apply_free_var_correction_cached(handle, true, true)?;
3035        Ok(())
3036    }
3037
3038    fn apply_free_var_correction_cached(
3039        &mut self,
3040        handle: &GpuCircuitCacheHandle,
3041        apply_log_z: bool,
3042        apply_grads: bool,
3043    ) -> Result<()> {
3044        if !self.has_free_var_mask_for_slot(handle.slot_index()) {
3045            return Ok(());
3046        }
3047        let n = self
3048            .var_cap
3049            .checked_add(1)
3050            .ok_or_else(|| XlogError::Compilation("GPU cache free-var overflow".to_string()))?;
3051        if n == 0 {
3052            return Ok(());
3053        }
3054
3055        let device = self.provider.device().inner();
3056        let block_dim = 256u32;
3057        let grid_dim = n.div_ceil(block_dim);
3058
3059        if apply_grads {
3060            let apply_grad = device
3061                .get_func(
3062                    xlog_cuda::CIRCUIT_MODULE,
3063                    xlog_cuda::circuit_kernels::XGCF_FREE_VAR_APPLY_GRAD_CACHED,
3064                )
3065                .ok_or_else(|| {
3066                    XlogError::Kernel(
3067                        "xgcf_free_var_apply_grad_cached kernel not found".to_string(),
3068                    )
3069                })?;
3070            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
3071            unsafe {
3072                apply_grad.clone().launch(
3073                    LaunchConfig {
3074                        grid_dim: (grid_dim, 1, 1),
3075                        block_dim: (block_dim, 1, 1),
3076                        shared_mem_bytes: 0,
3077                    },
3078                    (
3079                        handle.slot_device(),
3080                        self.var_cap,
3081                        &self.free_var_mask,
3082                        &self.var_log_true,
3083                        &self.var_log_false,
3084                        n,
3085                        &mut self.grad_true,
3086                        &mut self.grad_false,
3087                    ),
3088                )
3089            }
3090            .map_err(|e| {
3091                XlogError::Kernel(format!("xgcf_free_var_apply_grad_cached failed: {}", e))
3092            })?;
3093        }
3094
3095        if apply_log_z {
3096            let reduce_stage = device
3097                .get_func(
3098                    xlog_cuda::CIRCUIT_MODULE,
3099                    xlog_cuda::circuit_kernels::XGCF_FREE_VAR_REDUCE_STAGE_CACHED,
3100                )
3101                .ok_or_else(|| {
3102                    XlogError::Kernel(
3103                        "xgcf_free_var_reduce_stage_cached kernel not found".to_string(),
3104                    )
3105                })?;
3106            let add_scalar = device
3107                .get_func(
3108                    xlog_cuda::CIRCUIT_MODULE,
3109                    xlog_cuda::circuit_kernels::XGCF_ADD_SCALAR_CACHED,
3110                )
3111                .ok_or_else(|| {
3112                    XlogError::Kernel("xgcf_add_scalar_cached kernel not found".to_string())
3113                })?;
3114
3115            let memory = self.provider.memory();
3116            let mut buf_a = memory.alloc::<f64>(n as usize)?;
3117            let mut buf_b = memory.alloc::<f64>(n as usize)?;
3118
3119            let mut stage_n = n;
3120            let mut stage0 = true;
3121            let mut output_is_a = true;
3122            loop {
3123                let out_len = stage_n.div_ceil(2);
3124                let stage_grid = out_len.div_ceil(block_dim);
3125
3126                let (in_buf, out_buf): (&TrackedCudaSlice<f64>, &mut TrackedCudaSlice<f64>) =
3127                    if output_is_a {
3128                        (&buf_b, &mut buf_a)
3129                    } else {
3130                        (&buf_a, &mut buf_b)
3131                    };
3132                let mode = if stage0 { 0u32 } else { 1u32 };
3133
3134                // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
3135                unsafe {
3136                    reduce_stage.clone().launch(
3137                        LaunchConfig {
3138                            grid_dim: (stage_grid, 1, 1),
3139                            block_dim: (block_dim, 1, 1),
3140                            shared_mem_bytes: 0,
3141                        },
3142                        (
3143                            handle.slot_device(),
3144                            self.var_cap,
3145                            &self.free_var_mask,
3146                            &self.var_log_true,
3147                            &self.var_log_false,
3148                            in_buf,
3149                            stage_n,
3150                            mode,
3151                            out_buf,
3152                        ),
3153                    )
3154                }
3155                .map_err(|e| {
3156                    XlogError::Kernel(format!("xgcf_free_var_reduce_stage_cached failed: {}", e))
3157                })?;
3158
3159                if out_len == 1 {
3160                    let result_buf = if output_is_a { &buf_a } else { &buf_b };
3161                    // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
3162                    unsafe {
3163                        add_scalar.clone().launch(
3164                            LaunchConfig {
3165                                grid_dim: (1, 1, 1),
3166                                block_dim: (1, 1, 1),
3167                                shared_mem_bytes: 0,
3168                            },
3169                            (
3170                                handle.slot_device(),
3171                                self.node_cap,
3172                                &mut self.values,
3173                                &self.meta_root,
3174                                result_buf,
3175                            ),
3176                        )
3177                    }
3178                    .map_err(|e| {
3179                        XlogError::Kernel(format!("xgcf_add_scalar_cached failed: {}", e))
3180                    })?;
3181                    break;
3182                }
3183
3184                stage_n = out_len;
3185                stage0 = false;
3186                output_is_a = !output_is_a;
3187            }
3188        }
3189
3190        Ok(())
3191    }
3192}