Skip to main content

xlog_prob/
gpu.rs

1//! GPU evaluator for XGCF circuits (CUDA).
2
3use std::ffi::c_void;
4
5use cudarc::driver::LaunchConfig;
6use xlog_core::{Result, XlogError};
7use xlog_cuda::memory::TrackedCudaSlice;
8use xlog_cuda::provider::{
9    arith_kernels, d4_kernels, filter_kernels, ARITH_MODULE, D4_MODULE, FILTER_MODULE,
10};
11use xlog_cuda::{circuit_kernels, AsKernelParam, CudaKernelProvider, LaunchAsync, CIRCUIT_MODULE};
12
13use crate::compilation::gpu_d4::exclusive_scan_u32_inplace;
14use crate::logsumexp::validate_circuit_log_weight_pair;
15#[cfg(feature = "host-io")]
16use crate::logsumexp::{validate_circuit_gradient_values, validate_circuit_value};
17use crate::xgcf::{Xgcf, XgcfNodeType};
18
19/// Device-resident circuit buffers produced by the GPU compiler.
20///
21/// This matches the XGCF node layout used by `kernels/circuit.cu` and the SAT verifier CNF encoder
22/// in `kernels/sat.cu`.
23pub struct GpuCircuitBuilder {
24    /// Device opcode for every circuit node.
25    pub node_type: TrackedCudaSlice<u8>,
26    /// Device CSR offsets into [`Self::child_indices`].
27    pub child_offsets: TrackedCudaSlice<u32>,
28    /// Device-resident flattened child indices.
29    pub child_indices: TrackedCudaSlice<u32>,
30    /// Signed DIMACS literal for every literal node.
31    pub lit: TrackedCudaSlice<i32>,
32    /// Choice-variable identifier for every decision node.
33    pub decision_var: TrackedCudaSlice<u32>,
34    /// False-branch child for every decision node.
35    pub decision_child_false: TrackedCudaSlice<u32>,
36    /// True-branch child for every decision node.
37    pub decision_child_true: TrackedCudaSlice<u32>,
38}
39
40/// Device layout metadata for XGCF construction.
41pub struct GpuCircuitLayout {
42    /// Node-buffer capacity.
43    pub num_nodes: u32,
44    /// Edge-buffer capacity.
45    pub num_edges: u32,
46    /// Number of topological evaluation levels.
47    pub num_levels: u32,
48    /// Device CSR offsets into [`Self::level_nodes`].
49    pub level_offsets: TrackedCudaSlice<u32>,
50    /// Device node indices ordered by evaluation level.
51    pub level_nodes: TrackedCudaSlice<u32>,
52    /// Circuit root node identifier.
53    pub root: u32,
54    /// Largest DIMACS variable represented by the circuit.
55    pub max_var: u32,
56    /// Optional device scalar containing the authoritative node count.
57    pub num_nodes_device: Option<TrackedCudaSlice<u32>>,
58    /// Optional device scalar containing the authoritative edge count.
59    pub num_edges_device: Option<TrackedCudaSlice<u32>>,
60}
61
62/// Device-resident XGCF circuit and reusable evaluation workspaces.
63pub struct GpuXgcf {
64    node_type: TrackedCudaSlice<u8>,
65    child_offsets: TrackedCudaSlice<u32>,
66    child_indices: TrackedCudaSlice<u32>,
67    lit: TrackedCudaSlice<i32>,
68    decision_var: TrackedCudaSlice<u32>,
69    decision_child_false: TrackedCudaSlice<u32>,
70    decision_child_true: TrackedCudaSlice<u32>,
71    level_nodes: TrackedCudaSlice<u32>,
72    level_offsets: TrackedCudaSlice<u32>,
73    /// Optional host mirror for efficient per-level launch sizing when the circuit was uploaded
74    /// from host (`GpuXgcf::upload`). GPU-native compilation paths do not populate this.
75    level_offsets_host: Option<Vec<u32>>,
76    node_cap: u32,
77    edge_cap: u32,
78    num_levels: u32,
79    root: u32,
80    max_var: u32,
81    meta_num_nodes: TrackedCudaSlice<u32>,
82    meta_num_edges: TrackedCudaSlice<u32>,
83    var_log_true: TrackedCudaSlice<f64>,
84    var_log_false: TrackedCudaSlice<f64>,
85    values: TrackedCudaSlice<f64>,
86    adj: TrackedCudaSlice<f64>,
87    grad_true: TrackedCudaSlice<f64>,
88    grad_false: TrackedCudaSlice<f64>,
89    free_var_mask: Option<TrackedCudaSlice<u8>>,
90}
91
92fn checked_gpu_u32_len(context: &str, len: usize) -> Result<u32> {
93    u32::try_from(len)
94        .map_err(|_| XlogError::Compilation(format!("{context} exceeds u32::MAX: {len}")))
95}
96
97fn checked_gpu_len_add_one(context: &str, len: usize) -> Result<usize> {
98    len.checked_add(1)
99        .ok_or_else(|| XlogError::Compilation(format!("{context} length overflow")))
100}
101
102fn checked_gpu_launch_blocks(context: &str, item_count: usize, block_size: u32) -> Result<u32> {
103    let item_count = u32::try_from(item_count).map_err(|_| {
104        XlogError::Kernel(format!(
105            "{context} launch item count exceeds u32::MAX: {item_count}"
106        ))
107    })?;
108    item_count
109        .checked_add(block_size - 1)
110        .map(|rounded| rounded / block_size)
111        .ok_or_else(|| XlogError::Kernel(format!("{context} launch grid overflow")))
112}
113
114fn checked_host_level_width(level_offsets: &[u32], level: usize) -> Result<usize> {
115    let start = level_offsets[level];
116    let end = level_offsets[level + 1];
117    if end < start {
118        return Err(XlogError::Compilation(format!(
119            "XGCF invariant violation: level_offsets decrease at level {} ({} > {})",
120            level, start, end
121        )));
122    }
123    Ok((end - start) as usize)
124}
125
126fn validate_xgcf_for_gpu_upload(circuit: &Xgcf) -> Result<(u32, u32, u32)> {
127    let n = circuit.node_type.len();
128    if n == 0 {
129        return Err(XlogError::Compilation(
130            "GPU XGCF upload requires at least one node".to_string(),
131        ));
132    }
133    let node_count = checked_gpu_u32_len("GPU XGCF node count", n)?;
134    let child_offsets_len = checked_gpu_len_add_one("GPU XGCF child_offsets", n)?;
135    if circuit.child_offsets.len() != child_offsets_len {
136        return Err(XlogError::Compilation(format!(
137            "XGCF invariant violation: child_offsets len {} != num_nodes+1 ({})",
138            circuit.child_offsets.len(),
139            child_offsets_len
140        )));
141    }
142    if circuit.lit.len() != n
143        || circuit.decision_var.len() != n
144        || circuit.decision_child_false.len() != n
145        || circuit.decision_child_true.len() != n
146    {
147        return Err(XlogError::Compilation(
148            "XGCF invariant violation: per-node arrays length mismatch".to_string(),
149        ));
150    }
151
152    let edge_count = checked_gpu_u32_len("GPU XGCF edge count", circuit.child_indices.len())?;
153    let mut previous_offset = 0u32;
154    for (idx, &offset) in circuit.child_offsets.iter().enumerate() {
155        if offset < previous_offset {
156            return Err(XlogError::Compilation(format!(
157                "XGCF invariant violation: child_offsets decrease at index {} ({} > {})",
158                idx, previous_offset, offset
159            )));
160        }
161        if offset > edge_count {
162            return Err(XlogError::Compilation(format!(
163                "XGCF invariant violation: child_offsets[{}] {} exceeds child_indices len {}",
164                idx, offset, edge_count
165            )));
166        }
167        previous_offset = offset;
168    }
169    if previous_offset != edge_count {
170        return Err(XlogError::Compilation(format!(
171            "XGCF invariant violation: final child offset {} != child_indices len {}",
172            previous_offset, edge_count
173        )));
174    }
175    for (edge, &child) in circuit.child_indices.iter().enumerate() {
176        if child >= node_count {
177            return Err(XlogError::Compilation(format!(
178                "XGCF invariant violation: child_indices[{}] {} out of bounds (num_nodes={})",
179                edge, child, node_count
180            )));
181        }
182    }
183
184    for (idx, &ty) in circuit.node_type.iter().enumerate() {
185        match ty {
186            XgcfNodeType::Const0 | XgcfNodeType::Const1 => {}
187            XgcfNodeType::Lit => {
188                if circuit.lit[idx] == 0 {
189                    return Err(XlogError::Compilation(format!(
190                        "XGCF invariant violation: LIT node {} has lit=0",
191                        idx
192                    )));
193                }
194            }
195            XgcfNodeType::And | XgcfNodeType::Or => {
196                if circuit.child_offsets[idx] == circuit.child_offsets[idx + 1] {
197                    return Err(XlogError::Compilation(format!(
198                        "XGCF invariant violation: {:?} node {} has no children",
199                        ty, idx
200                    )));
201                }
202            }
203            XgcfNodeType::Decision => {
204                if circuit.decision_var[idx] == 0 {
205                    return Err(XlogError::Compilation(format!(
206                        "XGCF invariant violation: DECISION node {} has var=0",
207                        idx
208                    )));
209                }
210                if circuit.decision_child_false[idx] >= node_count {
211                    return Err(XlogError::Compilation(format!(
212                        "XGCF invariant violation: DECISION node {} false child {} out of bounds",
213                        idx, circuit.decision_child_false[idx]
214                    )));
215                }
216                if circuit.decision_child_true[idx] >= node_count {
217                    return Err(XlogError::Compilation(format!(
218                        "XGCF invariant violation: DECISION node {} true child {} out of bounds",
219                        idx, circuit.decision_child_true[idx]
220                    )));
221                }
222            }
223        }
224    }
225
226    if circuit.level_offsets.is_empty() || circuit.level_offsets[0] != 0 {
227        return Err(XlogError::Compilation(
228            "XGCF invariant violation: level_offsets must start at 0".to_string(),
229        ));
230    }
231    let level_nodes_len =
232        checked_gpu_u32_len("GPU XGCF level_nodes len", circuit.level_nodes.len())?;
233    let mut previous_level_offset = 0u32;
234    for (idx, &offset) in circuit.level_offsets.iter().enumerate() {
235        if offset < previous_level_offset {
236            return Err(XlogError::Compilation(format!(
237                "XGCF invariant violation: level_offsets decrease at index {} ({} > {})",
238                idx, previous_level_offset, offset
239            )));
240        }
241        if offset > level_nodes_len {
242            return Err(XlogError::Compilation(format!(
243                "XGCF invariant violation: level_offsets[{}] {} exceeds level_nodes len {}",
244                idx, offset, level_nodes_len
245            )));
246        }
247        previous_level_offset = offset;
248    }
249    if previous_level_offset != level_nodes_len {
250        return Err(XlogError::Compilation(format!(
251            "XGCF invariant violation: level_offsets last {} != level_nodes.len {}",
252            previous_level_offset, level_nodes_len
253        )));
254    }
255    for (idx, &node) in circuit.level_nodes.iter().enumerate() {
256        if node >= node_count {
257            return Err(XlogError::Compilation(format!(
258                "XGCF invariant violation: level_nodes[{}] {} out of bounds (num_nodes={})",
259                idx, node, node_count
260            )));
261        }
262    }
263    let num_levels_usize = circuit.level_offsets.len() - 1;
264    let num_levels = checked_gpu_u32_len("GPU XGCF level count", num_levels_usize)?;
265    if num_levels == 0 {
266        return Err(XlogError::Compilation(
267            "GPU XGCF upload requires at least one level".to_string(),
268        ));
269    }
270
271    if circuit.roots.len() != 1 {
272        return Err(XlogError::Compilation(format!(
273            "GPU XGCF eval expects exactly 1 root, got {}",
274            circuit.roots.len()
275        )));
276    }
277    if circuit.roots[0] >= node_count {
278        return Err(XlogError::Compilation(format!(
279            "XGCF invariant violation: root {} out of bounds (num_nodes={})",
280            circuit.roots[0], node_count
281        )));
282    }
283
284    Ok((node_count, edge_count, num_levels))
285}
286
287impl GpuXgcf {
288    /// Constructs a validated GPU circuit from existing device buffers and layout metadata.
289    pub fn from_device(
290        builder: GpuCircuitBuilder,
291        layout: GpuCircuitLayout,
292        provider: &CudaKernelProvider,
293    ) -> Result<GpuXgcf> {
294        if layout.num_nodes == 0 {
295            return Err(XlogError::Compilation(
296                "GpuXgcf::from_device requires num_nodes > 0".to_string(),
297            ));
298        }
299        if layout.root >= layout.num_nodes {
300            return Err(XlogError::Compilation(format!(
301                "GpuXgcf::from_device: root {} out of bounds (num_nodes={})",
302                layout.root, layout.num_nodes
303            )));
304        }
305        if layout.num_levels == 0 {
306            return Err(XlogError::Compilation(
307                "GpuXgcf::from_device requires num_levels > 0".to_string(),
308            ));
309        }
310
311        let num_nodes = layout.num_nodes as usize;
312        let num_edges = layout.num_edges as usize;
313        let node_cap = builder.node_type.len();
314        if num_nodes == 0 || num_nodes > node_cap {
315            return Err(XlogError::Compilation(
316                "GpuXgcf::from_device: num_nodes out of bounds".to_string(),
317            ));
318        }
319        let child_offsets_len =
320            checked_gpu_len_add_one("GpuXgcf::from_device child_offsets", node_cap)?;
321        if builder.child_offsets.len() != child_offsets_len
322            || builder.lit.len() != node_cap
323            || builder.decision_var.len() != node_cap
324            || builder.decision_child_false.len() != node_cap
325            || builder.decision_child_true.len() != node_cap
326        {
327            return Err(XlogError::Compilation(
328                "GpuXgcf::from_device: circuit buffer length mismatch".to_string(),
329            ));
330        }
331        if num_edges > builder.child_indices.len() {
332            return Err(XlogError::Compilation(
333                "GpuXgcf::from_device: num_edges out of bounds".to_string(),
334            ));
335        }
336
337        let num_levels = layout.num_levels as usize;
338        let level_offsets_len =
339            checked_gpu_len_add_one("GpuXgcf::from_device level_offsets", num_levels)?;
340        if layout.level_offsets.len() != level_offsets_len {
341            return Err(XlogError::Compilation(format!(
342                "GpuXgcf::from_device: level_offsets len {} != num_levels+1 ({})",
343                layout.level_offsets.len(),
344                level_offsets_len
345            )));
346        }
347        if layout.level_nodes.len() < num_nodes {
348            return Err(XlogError::Compilation(format!(
349                "GpuXgcf::from_device: level_nodes len {} < num_nodes ({})",
350                layout.level_nodes.len(),
351                num_nodes
352            )));
353        }
354
355        let memory = provider.memory();
356
357        let weights_len = (layout.max_var as usize) + 1;
358        let var_log_true = memory.alloc::<f64>(weights_len)?;
359        let var_log_false = memory.alloc::<f64>(weights_len)?;
360        let values = memory.alloc::<f64>(num_nodes)?;
361        let adj = memory.alloc::<f64>(num_nodes)?;
362        let grad_true = memory.alloc::<f64>(weights_len)?;
363        let grad_false = memory.alloc::<f64>(weights_len)?;
364
365        let meta_num_nodes = match layout.num_nodes_device {
366            Some(meta) => meta,
367            None => {
368                let mut meta = memory.alloc::<u32>(1)?;
369                provider
370                    .htod_launch_metadata_sync_copy_into(&[layout.num_nodes], &mut meta)
371                    .map_err(|e| {
372                        XlogError::Kernel(format!("Failed to upload num_nodes meta: {}", e))
373                    })?;
374                meta
375            }
376        };
377        let meta_num_edges = match layout.num_edges_device {
378            Some(meta) => meta,
379            None => {
380                let mut meta = memory.alloc::<u32>(1)?;
381                provider
382                    .htod_launch_metadata_sync_copy_into(&[layout.num_edges], &mut meta)
383                    .map_err(|e| {
384                        XlogError::Kernel(format!("Failed to upload num_edges meta: {}", e))
385                    })?;
386                meta
387            }
388        };
389
390        Ok(Self {
391            node_type: builder.node_type,
392            child_offsets: builder.child_offsets,
393            child_indices: builder.child_indices,
394            lit: builder.lit,
395            decision_var: builder.decision_var,
396            decision_child_false: builder.decision_child_false,
397            decision_child_true: builder.decision_child_true,
398            level_nodes: layout.level_nodes,
399            level_offsets: layout.level_offsets,
400            level_offsets_host: None,
401            node_cap: layout.num_nodes,
402            edge_cap: layout.num_edges,
403            num_levels: layout.num_levels,
404            root: layout.root,
405            max_var: layout.max_var,
406            meta_num_nodes,
407            meta_num_edges,
408            var_log_true,
409            var_log_false,
410            values,
411            adj,
412            grad_true,
413            grad_false,
414            free_var_mask: None,
415        })
416    }
417
418    /// GPU-native smoothing pass for random variables.
419    ///
420    /// Returns a new device-resident circuit that is smooth w.r.t. `random_var_list`.
421    /// This method performs no device->host data-plane transfers and traps on capacity overflow.
422    pub fn smooth_random_vars_device(
423        &self,
424        provider: &CudaKernelProvider,
425        random_var_list: &TrackedCudaSlice<u32>,
426        random_var_count: u32,
427        smooth_node_cap: u32,
428        smooth_edge_cap: u32,
429    ) -> Result<GpuXgcf> {
430        if smooth_node_cap == 0 || smooth_edge_cap == 0 {
431            return Err(XlogError::Compilation(
432                "GPU smoothing requires non-zero node/edge caps".to_string(),
433            ));
434        }
435
436        let num_nodes = self.node_cap;
437        if num_nodes == 0 {
438            return Err(XlogError::Compilation(
439                "GPU smoothing: num_nodes must be > 0".to_string(),
440            ));
441        }
442        if self.child_offsets.len() < (num_nodes as usize + 1) {
443            return Err(XlogError::Compilation(
444                "GPU smoothing: child_offsets len mismatch".to_string(),
445            ));
446        }
447        let num_edges = self.edge_cap;
448        if num_edges == 0 {
449            return Err(XlogError::Compilation(
450                "GPU smoothing: num_edges must be > 0".to_string(),
451            ));
452        }
453
454        let list_len = u32::try_from(random_var_list.len()).map_err(|_| {
455            XlogError::Compilation("GPU smoothing: random var list len exceeds u32".to_string())
456        })?;
457        let num_random_vars = random_var_count;
458        if num_random_vars > list_len {
459            return Err(XlogError::Compilation(format!(
460                "GPU smoothing: random var count {} exceeds list len {}",
461                num_random_vars, list_len
462            )));
463        }
464
465        let base_node = 2u32.checked_add(num_random_vars).ok_or_else(|| {
466            XlogError::Compilation("GPU smoothing: base node overflow".to_string())
467        })?;
468        let base_nodes = (base_node as u64)
469            .checked_add(num_nodes as u64)
470            .ok_or_else(|| {
471                XlogError::Compilation("GPU smoothing: base node overflow".to_string())
472            })?;
473        if base_nodes > smooth_node_cap as u64 {
474            return Err(XlogError::Compilation(format!(
475                "GPU smoothing: base nodes {} exceed smooth_node_cap {}",
476                base_nodes, smooth_node_cap
477            )));
478        }
479
480        let words_per_support = num_random_vars.div_ceil(32).max(1);
481
482        let support_len = (num_nodes as u64)
483            .checked_mul(words_per_support as u64)
484            .and_then(|v| usize::try_from(v).ok())
485            .ok_or_else(|| {
486                XlogError::Compilation("GPU smoothing: support size overflow".to_string())
487            })?;
488
489        let dec_entries = (num_nodes as u64)
490            .checked_mul(2)
491            .and_then(|v| usize::try_from(v).ok())
492            .ok_or_else(|| {
493                XlogError::Compilation("GPU smoothing: decision array overflow".to_string())
494            })?;
495        let dec_entries_u32 = u32::try_from(dec_entries).map_err(|_| {
496            XlogError::Compilation("GPU smoothing: decision entries exceed u32".to_string())
497        })?;
498
499        let device = provider.device().inner();
500        let memory = provider.memory();
501        let block_size: u32 = 256;
502
503        let map_len = (self.max_var as usize)
504            .checked_add(1)
505            .ok_or_else(|| XlogError::Compilation("GPU smoothing: max_var overflow".to_string()))?;
506        let map_len_u32 = u32::try_from(map_len).map_err(|_| {
507            XlogError::Compilation("GPU smoothing: random map len exceeds u32".to_string())
508        })?;
509        let mut d_random_map = memory.alloc::<u32>(map_len)?;
510        if map_len > 0 {
511            let fill_const = device
512                .get_func(FILTER_MODULE, filter_kernels::FILL_U32_CONST)
513                .ok_or_else(|| XlogError::Kernel("fill_u32_const kernel not found".to_string()))?;
514            let grid = map_len_u32.div_ceil(block_size);
515            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
516            unsafe {
517                fill_const.clone().launch(
518                    LaunchConfig {
519                        grid_dim: (grid, 1, 1),
520                        block_dim: (block_size, 1, 1),
521                        shared_mem_bytes: 0,
522                    },
523                    (&mut d_random_map, map_len_u32, u32::MAX),
524                )
525            }
526            .map_err(|e| XlogError::Kernel(format!("fill_u32_const failed: {}", e)))?;
527        }
528        if num_random_vars > 0 {
529            let map_kernel = device
530                .get_func(FILTER_MODULE, filter_kernels::RANDOM_VAR_TO_BIT_FROM_LIST)
531                .ok_or_else(|| {
532                    XlogError::Kernel("random_var_to_bit_from_list kernel not found".to_string())
533                })?;
534            let grid = num_random_vars.div_ceil(block_size);
535            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
536            unsafe {
537                map_kernel.clone().launch(
538                    LaunchConfig {
539                        grid_dim: (grid, 1, 1),
540                        block_dim: (block_size, 1, 1),
541                        shared_mem_bytes: 0,
542                    },
543                    (
544                        random_var_list,
545                        num_random_vars,
546                        map_len_u32,
547                        &mut d_random_map,
548                    ),
549                )
550            }
551            .map_err(|e| XlogError::Kernel(format!("random_var_to_bit_from_list failed: {}", e)))?;
552        }
553
554        let mut support = memory.alloc::<u32>(support_len)?;
555        device
556            .memset_zeros(&mut support)
557            .map_err(|e| XlogError::Kernel(format!("Failed to zero support: {}", e)))?;
558
559        let support_kernel = device
560            .get_func(D4_MODULE, d4_kernels::D4_SUPPORT_LEVEL)
561            .ok_or_else(|| XlogError::Kernel("d4_support_level kernel not found".to_string()))?;
562
563        let num_levels = self.num_levels as usize;
564        let random_map_len = map_len_u32;
565        for level in 0..num_levels {
566            let num_level_nodes: usize = match self.level_offsets_host.as_ref() {
567                Some(off) => checked_host_level_width(off, level)?,
568                None => self.level_nodes.len(),
569            };
570            if num_level_nodes == 0 {
571                continue;
572            }
573            let num_blocks =
574                checked_gpu_launch_blocks("d4_support_level", num_level_nodes, block_size)?;
575            let config = LaunchConfig {
576                grid_dim: (num_blocks, 1, 1),
577                block_dim: (block_size, 1, 1),
578                shared_mem_bytes: 0,
579            };
580            let level_u32 = level as u32;
581            let mut params: Vec<*mut c_void> = vec![
582                (&self.node_type).as_kernel_param(),
583                (&self.child_offsets).as_kernel_param(),
584                (&self.child_indices).as_kernel_param(),
585                (&self.lit).as_kernel_param(),
586                (&self.decision_var).as_kernel_param(),
587                (&self.decision_child_false).as_kernel_param(),
588                (&self.decision_child_true).as_kernel_param(),
589                (&self.level_nodes).as_kernel_param(),
590                (&self.level_offsets).as_kernel_param(),
591                level_u32.as_kernel_param(),
592                (&d_random_map).as_kernel_param(),
593                random_map_len.as_kernel_param(),
594                words_per_support.as_kernel_param(),
595                (&support).as_kernel_param(),
596            ];
597            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
598            unsafe { support_kernel.clone().launch(config, &mut params) }
599                .map_err(|e| XlogError::Kernel(format!("d4_support_level failed: {}", e)))?;
600        }
601
602        if num_random_vars > 0 {
603            let root_kernel = device
604                .get_func(D4_MODULE, d4_kernels::D4_SUPPORT_SET_ROOT_BITS)
605                .ok_or_else(|| {
606                    XlogError::Kernel("d4_support_set_root_bits kernel not found".to_string())
607                })?;
608            let num_words = num_random_vars.div_ceil(32);
609            let grid = num_words.div_ceil(block_size);
610            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
611            unsafe {
612                root_kernel.clone().launch(
613                    LaunchConfig {
614                        grid_dim: (grid, 1, 1),
615                        block_dim: (block_size, 1, 1),
616                        shared_mem_bytes: 0,
617                    },
618                    (self.root, num_random_vars, words_per_support, &mut support),
619                )
620            }
621            .map_err(|e| XlogError::Kernel(format!("d4_support_set_root_bits failed: {}", e)))?;
622        }
623
624        let mut wrap_prefix_or = memory.alloc::<u32>(num_edges as usize)?;
625        let mut wrap_missing_or = memory.alloc::<u32>(num_edges as usize)?;
626        let mut wrap_prefix_dec = memory.alloc::<u32>(dec_entries)?;
627        let mut wrap_missing_dec = memory.alloc::<u32>(dec_entries)?;
628
629        device
630            .memset_zeros(&mut wrap_prefix_or)
631            .map_err(|e| XlogError::Kernel(format!("Failed to zero wrap_prefix_or: {}", e)))?;
632        device
633            .memset_zeros(&mut wrap_missing_or)
634            .map_err(|e| XlogError::Kernel(format!("Failed to zero wrap_missing_or: {}", e)))?;
635        device
636            .memset_zeros(&mut wrap_prefix_dec)
637            .map_err(|e| XlogError::Kernel(format!("Failed to zero wrap_prefix_dec: {}", e)))?;
638        device
639            .memset_zeros(&mut wrap_missing_dec)
640            .map_err(|e| XlogError::Kernel(format!("Failed to zero wrap_missing_dec: {}", e)))?;
641
642        let mut out_edge_counts = memory.alloc::<u32>(smooth_node_cap as usize)?;
643        device
644            .memset_zeros(&mut out_edge_counts)
645            .map_err(|e| XlogError::Kernel(format!("Failed to zero edge_counts: {}", e)))?;
646
647        let count_kernel = device
648            .get_func(D4_MODULE, d4_kernels::D4_SMOOTH_COUNT)
649            .ok_or_else(|| XlogError::Kernel("d4_smooth_count kernel not found".to_string()))?;
650        let num_blocks = num_nodes.div_ceil(block_size);
651        let mut params: Vec<*mut c_void> = vec![
652            (&self.node_type).as_kernel_param(),
653            (&self.child_offsets).as_kernel_param(),
654            (&self.child_indices).as_kernel_param(),
655            (&self.decision_var).as_kernel_param(),
656            (&self.decision_child_false).as_kernel_param(),
657            (&self.decision_child_true).as_kernel_param(),
658            (&self.meta_num_nodes).as_kernel_param(),
659            (&support).as_kernel_param(),
660            words_per_support.as_kernel_param(),
661            (&d_random_map).as_kernel_param(),
662            random_map_len.as_kernel_param(),
663            (&wrap_prefix_or).as_kernel_param(),
664            (&wrap_missing_or).as_kernel_param(),
665            (&wrap_prefix_dec).as_kernel_param(),
666            (&wrap_missing_dec).as_kernel_param(),
667            (&out_edge_counts).as_kernel_param(),
668            base_node.as_kernel_param(),
669            smooth_node_cap.as_kernel_param(),
670        ];
671        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
672        unsafe {
673            count_kernel.clone().launch(
674                LaunchConfig {
675                    grid_dim: (num_blocks, 1, 1),
676                    block_dim: (block_size, 1, 1),
677                    shared_mem_bytes: 0,
678                },
679                &mut params,
680            )
681        }
682        .map_err(|e| XlogError::Kernel(format!("d4_smooth_count failed: {}", e)))?;
683
684        exclusive_scan_u32_inplace(provider, &mut wrap_prefix_or, num_edges)?;
685        exclusive_scan_u32_inplace(provider, &mut wrap_prefix_dec, dec_entries_u32)?;
686
687        let mut wrap_counts = memory.alloc::<u32>(3)?;
688        device
689            .memset_zeros(&mut wrap_counts)
690            .map_err(|e| XlogError::Kernel(format!("Failed to zero wrap_counts: {}", e)))?;
691
692        let counts_kernel = device
693            .get_func(D4_MODULE, d4_kernels::D4_SMOOTH_WRAPPER_COUNTS)
694            .ok_or_else(|| {
695                XlogError::Kernel("d4_smooth_wrapper_counts kernel not found".to_string())
696            })?;
697        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
698        unsafe {
699            counts_kernel.clone().launch(
700                LaunchConfig {
701                    grid_dim: (1, 1, 1),
702                    block_dim: (1, 1, 1),
703                    shared_mem_bytes: 0,
704                },
705                (
706                    &wrap_prefix_or,
707                    &wrap_missing_or,
708                    num_edges,
709                    &wrap_prefix_dec,
710                    &wrap_missing_dec,
711                    dec_entries_u32,
712                    base_node,
713                    &self.meta_num_nodes,
714                    u32::MAX,
715                    &mut wrap_counts,
716                ),
717            )
718        }
719        .map_err(|e| XlogError::Kernel(format!("d4_smooth_wrapper_counts failed: {}", e)))?;
720
721        let wrap_or_kernel = device
722            .get_func(D4_MODULE, d4_kernels::D4_SMOOTH_WRAPPER_EDGE_COUNTS_OR)
723            .ok_or_else(|| {
724                XlogError::Kernel("d4_smooth_wrapper_edge_counts_or kernel not found".to_string())
725            })?;
726        if num_edges > 0 {
727            let num_blocks = num_edges.div_ceil(block_size);
728            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
729            unsafe {
730                wrap_or_kernel.clone().launch(
731                    LaunchConfig {
732                        grid_dim: (num_blocks, 1, 1),
733                        block_dim: (block_size, 1, 1),
734                        shared_mem_bytes: 0,
735                    },
736                    (
737                        &wrap_prefix_or,
738                        &wrap_missing_or,
739                        num_edges,
740                        base_node,
741                        &self.meta_num_nodes,
742                        smooth_node_cap,
743                        &mut out_edge_counts,
744                    ),
745                )
746            }
747            .map_err(|e| {
748                XlogError::Kernel(format!("d4_smooth_wrapper_edge_counts_or failed: {}", e))
749            })?;
750        }
751
752        let wrap_dec_kernel = device
753            .get_func(D4_MODULE, d4_kernels::D4_SMOOTH_WRAPPER_EDGE_COUNTS_DEC)
754            .ok_or_else(|| {
755                XlogError::Kernel("d4_smooth_wrapper_edge_counts_dec kernel not found".to_string())
756            })?;
757        if dec_entries > 0 {
758            let num_blocks = dec_entries_u32.div_ceil(block_size);
759            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
760            unsafe {
761                wrap_dec_kernel.clone().launch(
762                    LaunchConfig {
763                        grid_dim: (num_blocks, 1, 1),
764                        block_dim: (block_size, 1, 1),
765                        shared_mem_bytes: 0,
766                    },
767                    (
768                        &wrap_prefix_dec,
769                        &wrap_missing_dec,
770                        dec_entries_u32,
771                        base_node,
772                        &self.meta_num_nodes,
773                        &wrap_counts,
774                        smooth_node_cap,
775                        &mut out_edge_counts,
776                    ),
777                )
778            }
779            .map_err(|e| {
780                XlogError::Kernel(format!("d4_smooth_wrapper_edge_counts_dec failed: {}", e))
781            })?;
782        }
783
784        let mut out_child_offsets = memory.alloc::<u32>((smooth_node_cap as usize) + 1)?;
785        device
786            .memset_zeros(&mut out_child_offsets)
787            .map_err(|e| XlogError::Kernel(format!("Failed to zero child_offsets: {}", e)))?;
788        if smooth_node_cap > 0 {
789            device
790                .dtod_copy(
791                    &out_edge_counts,
792                    &mut out_child_offsets.slice_mut(0..smooth_node_cap as usize),
793                )
794                .map_err(|e| XlogError::Kernel(format!("Failed to copy edge_counts: {}", e)))?;
795        }
796        let child_scan_len = smooth_node_cap.checked_add(1).ok_or_else(|| {
797            XlogError::Compilation("GPU smoothing: child offset scan overflow".to_string())
798        })?;
799        exclusive_scan_u32_inplace(provider, &mut out_child_offsets, child_scan_len)?;
800
801        let edge_cap_check = device
802            .get_func(D4_MODULE, d4_kernels::D4_SMOOTH_CHECK_EDGE_CAP)
803            .ok_or_else(|| {
804                XlogError::Kernel("d4_smooth_check_edge_cap kernel not found".to_string())
805            })?;
806        let mut meta_num_nodes = memory.alloc::<u32>(1)?;
807        let mut meta_num_edges = memory.alloc::<u32>(1)?;
808        device
809            .memset_zeros(&mut meta_num_nodes)
810            .map_err(|e| XlogError::Kernel(format!("Failed to zero smooth num_nodes: {}", e)))?;
811        device
812            .memset_zeros(&mut meta_num_edges)
813            .map_err(|e| XlogError::Kernel(format!("Failed to zero smooth num_edges: {}", e)))?;
814        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
815        unsafe {
816            edge_cap_check.clone().launch(
817                LaunchConfig {
818                    grid_dim: (1, 1, 1),
819                    block_dim: (1, 1, 1),
820                    shared_mem_bytes: 0,
821                },
822                (
823                    &out_child_offsets,
824                    smooth_node_cap,
825                    smooth_edge_cap,
826                    &wrap_counts,
827                    &mut meta_num_nodes,
828                    &mut meta_num_edges,
829                ),
830            )
831        }
832        .map_err(|e| XlogError::Kernel(format!("d4_smooth_check_edge_cap failed: {}", e)))?;
833
834        let mut out_node_type = memory.alloc::<u8>(smooth_node_cap as usize)?;
835        let mut out_child_indices = memory.alloc::<u32>(smooth_edge_cap as usize)?;
836        let mut out_lit = memory.alloc::<i32>(smooth_node_cap as usize)?;
837        let mut out_decision_var = memory.alloc::<u32>(smooth_node_cap as usize)?;
838        let mut out_decision_child_false = memory.alloc::<u32>(smooth_node_cap as usize)?;
839        let mut out_decision_child_true = memory.alloc::<u32>(smooth_node_cap as usize)?;
840        let mut out_node_level = memory.alloc::<u32>(smooth_node_cap as usize)?;
841
842        device
843            .memset_zeros(&mut out_node_type)
844            .map_err(|e| XlogError::Kernel(format!("Failed to zero node_type: {}", e)))?;
845        device
846            .memset_zeros(&mut out_child_indices)
847            .map_err(|e| XlogError::Kernel(format!("Failed to zero child_indices: {}", e)))?;
848        device
849            .memset_zeros(&mut out_lit)
850            .map_err(|e| XlogError::Kernel(format!("Failed to zero lit: {}", e)))?;
851        device
852            .memset_zeros(&mut out_decision_var)
853            .map_err(|e| XlogError::Kernel(format!("Failed to zero decision_var: {}", e)))?;
854        device
855            .memset_zeros(&mut out_decision_child_false)
856            .map_err(|e| {
857                XlogError::Kernel(format!("Failed to zero decision_child_false: {}", e))
858            })?;
859        device
860            .memset_zeros(&mut out_decision_child_true)
861            .map_err(|e| XlogError::Kernel(format!("Failed to zero decision_child_true: {}", e)))?;
862        device
863            .memset_zeros(&mut out_node_level)
864            .map_err(|e| XlogError::Kernel(format!("Failed to zero node_level: {}", e)))?;
865
866        let init_kernel = device
867            .get_func(D4_MODULE, d4_kernels::D4_SMOOTH_INIT_NODES)
868            .ok_or_else(|| {
869                XlogError::Kernel("d4_smooth_init_nodes kernel not found".to_string())
870            })?;
871        let init_blocks = checked_gpu_launch_blocks(
872            "d4_smooth_init_nodes",
873            num_random_vars.max(1) as usize,
874            block_size,
875        )?;
876        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
877        unsafe {
878            init_kernel.clone().launch(
879                LaunchConfig {
880                    grid_dim: (init_blocks, 1, 1),
881                    block_dim: (block_size, 1, 1),
882                    shared_mem_bytes: 0,
883                },
884                (
885                    random_var_list,
886                    num_random_vars,
887                    smooth_node_cap,
888                    &mut out_node_type,
889                    &mut out_lit,
890                    &mut out_decision_var,
891                    &mut out_decision_child_false,
892                    &mut out_decision_child_true,
893                    &mut out_node_level,
894                ),
895            )
896        }
897        .map_err(|e| XlogError::Kernel(format!("d4_smooth_init_nodes failed: {}", e)))?;
898
899        let num_levels_out = self
900            .num_levels
901            .checked_mul(2)
902            .and_then(|levels| levels.checked_add(4))
903            .ok_or_else(|| {
904                XlogError::Compilation("GPU smoothing output level count overflow".to_string())
905            })?;
906        let num_levels_out_usize = num_levels_out as usize;
907        let level_offsets_len =
908            checked_gpu_len_add_one("GPU smoothing level offsets", num_levels_out_usize)?;
909
910        let emit_kernel = device
911            .get_func(D4_MODULE, d4_kernels::D4_SMOOTH_EMIT_LEVEL)
912            .ok_or_else(|| {
913                XlogError::Kernel("d4_smooth_emit_level kernel not found".to_string())
914            })?;
915        for level in 0..num_levels {
916            let num_level_nodes: usize = match self.level_offsets_host.as_ref() {
917                Some(off) => checked_host_level_width(off, level)?,
918                None => self.level_nodes.len(),
919            };
920            if num_level_nodes == 0 {
921                continue;
922            }
923            let num_blocks =
924                checked_gpu_launch_blocks("xgcf_smooth_forward", num_level_nodes, block_size)?;
925            let level_u32 = level as u32;
926            let mut params: Vec<*mut c_void> = vec![
927                (&self.node_type).as_kernel_param(),
928                (&self.child_offsets).as_kernel_param(),
929                (&self.child_indices).as_kernel_param(),
930                (&self.lit).as_kernel_param(),
931                (&self.decision_var).as_kernel_param(),
932                (&self.decision_child_false).as_kernel_param(),
933                (&self.decision_child_true).as_kernel_param(),
934                (&self.level_nodes).as_kernel_param(),
935                (&self.level_offsets).as_kernel_param(),
936                level_u32.as_kernel_param(),
937                (&support).as_kernel_param(),
938                words_per_support.as_kernel_param(),
939                (&wrap_prefix_or).as_kernel_param(),
940                (&wrap_missing_or).as_kernel_param(),
941                (&wrap_prefix_dec).as_kernel_param(),
942                (&wrap_missing_dec).as_kernel_param(),
943                base_node.as_kernel_param(),
944                (&self.meta_num_nodes).as_kernel_param(),
945                (&wrap_counts).as_kernel_param(),
946                num_random_vars.as_kernel_param(),
947                num_levels_out.as_kernel_param(),
948                (&out_node_type).as_kernel_param(),
949                (&out_child_offsets).as_kernel_param(),
950                (&out_child_indices).as_kernel_param(),
951                (&out_lit).as_kernel_param(),
952                (&out_decision_var).as_kernel_param(),
953                (&out_decision_child_false).as_kernel_param(),
954                (&out_decision_child_true).as_kernel_param(),
955                (&out_node_level).as_kernel_param(),
956            ];
957            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
958            unsafe {
959                emit_kernel.clone().launch(
960                    LaunchConfig {
961                        grid_dim: (num_blocks, 1, 1),
962                        block_dim: (block_size, 1, 1),
963                        shared_mem_bytes: 0,
964                    },
965                    &mut params,
966                )
967            }
968            .map_err(|e| XlogError::Kernel(format!("d4_smooth_emit_level failed: {}", e)))?;
969        }
970
971        let mut level_counts = memory.alloc::<u32>(num_levels_out_usize)?;
972        let mut level_offsets = memory.alloc::<u32>(level_offsets_len)?;
973        let mut level_cursors = memory.alloc::<u32>(num_levels_out_usize)?;
974        let mut level_nodes = memory.alloc::<u32>(smooth_node_cap as usize)?;
975
976        device
977            .memset_zeros(&mut level_counts)
978            .map_err(|e| XlogError::Kernel(format!("Failed to zero level_counts: {}", e)))?;
979        device
980            .memset_zeros(&mut level_offsets)
981            .map_err(|e| XlogError::Kernel(format!("Failed to zero level_offsets: {}", e)))?;
982        device
983            .memset_zeros(&mut level_cursors)
984            .map_err(|e| XlogError::Kernel(format!("Failed to zero level_cursors: {}", e)))?;
985        device
986            .memset_zeros(&mut level_nodes)
987            .map_err(|e| XlogError::Kernel(format!("Failed to zero level_nodes: {}", e)))?;
988
989        let mut compile_needed = memory.alloc::<u32>(1)?;
990        provider
991            .htod_launch_metadata_sync_copy_into(&[1u32], &mut compile_needed)
992            .map_err(|e| XlogError::Kernel(format!("Failed to upload compile_needed: {}", e)))?;
993
994        let levelize_counts = device
995            .get_func(D4_MODULE, d4_kernels::D4_LEVELIZE_COUNTS)
996            .ok_or_else(|| XlogError::Kernel("d4_levelize_counts kernel not found".to_string()))?;
997        let num_blocks =
998            checked_gpu_launch_blocks("d4_smooth_levelize", smooth_node_cap as usize, block_size)?;
999        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1000        unsafe {
1001            levelize_counts.clone().launch(
1002                LaunchConfig {
1003                    grid_dim: (num_blocks, 1, 1),
1004                    block_dim: (block_size, 1, 1),
1005                    shared_mem_bytes: 0,
1006                },
1007                (
1008                    &compile_needed,
1009                    &out_node_level,
1010                    &meta_num_nodes,
1011                    num_levels_out,
1012                    &mut level_counts,
1013                ),
1014            )
1015        }
1016        .map_err(|e| XlogError::Kernel(format!("d4_levelize_counts failed: {}", e)))?;
1017
1018        device
1019            .dtod_copy(
1020                &level_counts,
1021                &mut level_offsets.slice_mut(0..num_levels_out_usize),
1022            )
1023            .map_err(|e| XlogError::Kernel(format!("Failed to copy level_counts: {}", e)))?;
1024        let level_scan_len = num_levels_out.checked_add(1).ok_or_else(|| {
1025            XlogError::Compilation("GPU smoothing: level offset scan overflow".to_string())
1026        })?;
1027        exclusive_scan_u32_inplace(provider, &mut level_offsets, level_scan_len)?;
1028
1029        let levelize_emit = device
1030            .get_func(D4_MODULE, d4_kernels::D4_LEVELIZE_EMIT)
1031            .ok_or_else(|| XlogError::Kernel("d4_levelize_emit kernel not found".to_string()))?;
1032        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1033        unsafe {
1034            levelize_emit.clone().launch(
1035                LaunchConfig {
1036                    grid_dim: (num_blocks, 1, 1),
1037                    block_dim: (block_size, 1, 1),
1038                    shared_mem_bytes: 0,
1039                },
1040                (
1041                    &compile_needed,
1042                    &out_node_level,
1043                    &meta_num_nodes,
1044                    num_levels_out,
1045                    &level_offsets,
1046                    &mut level_cursors,
1047                    &mut level_nodes,
1048                ),
1049            )
1050        }
1051        .map_err(|e| XlogError::Kernel(format!("d4_levelize_emit failed: {}", e)))?;
1052
1053        // No device synchronize: result is device buffers used by subsequent GPU ops.
1054        let builder = GpuCircuitBuilder {
1055            node_type: out_node_type,
1056            child_offsets: out_child_offsets,
1057            child_indices: out_child_indices,
1058            lit: out_lit,
1059            decision_var: out_decision_var,
1060            decision_child_false: out_decision_child_false,
1061            decision_child_true: out_decision_child_true,
1062        };
1063        let layout = GpuCircuitLayout {
1064            num_nodes: smooth_node_cap,
1065            num_edges: smooth_edge_cap,
1066            num_levels: num_levels_out,
1067            level_offsets,
1068            level_nodes,
1069            root: base_node + self.root,
1070            max_var: self.max_var,
1071            num_nodes_device: Some(meta_num_nodes),
1072            num_edges_device: Some(meta_num_edges),
1073        };
1074
1075        let circuit = GpuXgcf::from_device(builder, layout, provider)?;
1076        // The smoothing kernels use scratch allocations that are intentionally
1077        // absent from the returned circuit. Release builds must complete the
1078        // final levelization launch before those owners enter physical
1079        // retirement. This is a cold topology pass and performs no D2H copy.
1080        provider.device().synchronize().map_err(|error| {
1081            XlogError::Kernel(format!("GPU circuit smoothing completion failed: {error}"))
1082        })?;
1083        Ok(circuit)
1084    }
1085
1086    /// Validates and uploads a host XGCF circuit.
1087    pub fn upload(provider: &CudaKernelProvider, circuit: &Xgcf) -> Result<Self> {
1088        let (node_cap, edge_cap, num_levels) = validate_xgcf_for_gpu_upload(circuit)?;
1089
1090        let memory = provider.memory().clone();
1091
1092        let n = circuit.node_type.len();
1093        let mut host_node_type: Vec<u8> = Vec::with_capacity(n);
1094        for &ty in &circuit.node_type {
1095            host_node_type.push(ty as u8);
1096        }
1097
1098        let mut max_var: u32 = 0;
1099        for (&ty, &lit) in circuit.node_type.iter().zip(circuit.lit.iter()) {
1100            if ty == XgcfNodeType::Lit && lit != 0 {
1101                max_var = max_var.max(lit.unsigned_abs());
1102            }
1103        }
1104        for &var in &circuit.decision_var {
1105            max_var = max_var.max(var);
1106        }
1107
1108        let mut d_node_type = memory.alloc::<u8>(n)?;
1109        provider
1110            .htod_sync_copy_into_tracked(&host_node_type, &mut d_node_type)
1111            .map_err(|e| XlogError::Kernel(format!("Failed to upload circuit node_type: {}", e)))?;
1112
1113        let mut d_child_offsets = memory.alloc::<u32>(circuit.child_offsets.len())?;
1114        provider
1115            .htod_sync_copy_into_tracked(&circuit.child_offsets, &mut d_child_offsets)
1116            .map_err(|e| {
1117                XlogError::Kernel(format!("Failed to upload circuit child_offsets: {}", e))
1118            })?;
1119
1120        let mut d_child_indices = memory.alloc::<u32>(circuit.child_indices.len())?;
1121        provider
1122            .htod_sync_copy_into_tracked(&circuit.child_indices, &mut d_child_indices)
1123            .map_err(|e| {
1124                XlogError::Kernel(format!("Failed to upload circuit child_indices: {}", e))
1125            })?;
1126
1127        let mut d_lit = memory.alloc::<i32>(circuit.lit.len())?;
1128        provider
1129            .htod_sync_copy_into_tracked(&circuit.lit, &mut d_lit)
1130            .map_err(|e| XlogError::Kernel(format!("Failed to upload circuit lit: {}", e)))?;
1131
1132        let mut d_decision_var = memory.alloc::<u32>(circuit.decision_var.len())?;
1133        provider
1134            .htod_sync_copy_into_tracked(&circuit.decision_var, &mut d_decision_var)
1135            .map_err(|e| {
1136                XlogError::Kernel(format!("Failed to upload circuit decision_var: {}", e))
1137            })?;
1138
1139        let mut d_decision_child_false = memory.alloc::<u32>(circuit.decision_child_false.len())?;
1140        provider
1141            .htod_sync_copy_into_tracked(&circuit.decision_child_false, &mut d_decision_child_false)
1142            .map_err(|e| {
1143                XlogError::Kernel(format!(
1144                    "Failed to upload circuit decision_child_false: {}",
1145                    e
1146                ))
1147            })?;
1148
1149        let mut d_decision_child_true = memory.alloc::<u32>(circuit.decision_child_true.len())?;
1150        provider
1151            .htod_sync_copy_into_tracked(&circuit.decision_child_true, &mut d_decision_child_true)
1152            .map_err(|e| {
1153                XlogError::Kernel(format!(
1154                    "Failed to upload circuit decision_child_true: {}",
1155                    e
1156                ))
1157            })?;
1158
1159        let mut d_level_nodes = memory.alloc::<u32>(circuit.level_nodes.len())?;
1160        provider
1161            .htod_sync_copy_into_tracked(&circuit.level_nodes, &mut d_level_nodes)
1162            .map_err(|e| {
1163                XlogError::Kernel(format!("Failed to upload circuit level_nodes: {}", e))
1164            })?;
1165
1166        let mut d_level_offsets = memory.alloc::<u32>(circuit.level_offsets.len())?;
1167        provider
1168            .htod_sync_copy_into_tracked(&circuit.level_offsets, &mut d_level_offsets)
1169            .map_err(|e| {
1170                XlogError::Kernel(format!("Failed to upload circuit level_offsets: {}", e))
1171            })?;
1172
1173        let weights_len = (max_var as usize) + 1;
1174        let var_log_true = memory.alloc::<f64>(weights_len)?;
1175        let var_log_false = memory.alloc::<f64>(weights_len)?;
1176        let values = memory.alloc::<f64>(n)?;
1177        let adj = memory.alloc::<f64>(n)?;
1178        let grad_true = memory.alloc::<f64>(weights_len)?;
1179        let grad_false = memory.alloc::<f64>(weights_len)?;
1180        let mut meta_num_nodes = memory.alloc::<u32>(1)?;
1181        provider
1182            .htod_launch_metadata_sync_copy_into(&[node_cap], &mut meta_num_nodes)
1183            .map_err(|e| XlogError::Kernel(format!("Failed to upload num_nodes meta: {}", e)))?;
1184        let mut meta_num_edges = memory.alloc::<u32>(1)?;
1185        provider
1186            .htod_launch_metadata_sync_copy_into(&[edge_cap], &mut meta_num_edges)
1187            .map_err(|e| XlogError::Kernel(format!("Failed to upload num_edges meta: {}", e)))?;
1188
1189        Ok(Self {
1190            node_type: d_node_type,
1191            child_offsets: d_child_offsets,
1192            child_indices: d_child_indices,
1193            lit: d_lit,
1194            decision_var: d_decision_var,
1195            decision_child_false: d_decision_child_false,
1196            decision_child_true: d_decision_child_true,
1197            level_nodes: d_level_nodes,
1198            level_offsets: d_level_offsets,
1199            level_offsets_host: Some(circuit.level_offsets.clone()),
1200            node_cap,
1201            edge_cap,
1202            num_levels,
1203            root: circuit.roots[0],
1204            max_var,
1205            meta_num_nodes,
1206            meta_num_edges,
1207            var_log_true,
1208            var_log_false,
1209            values,
1210            adj,
1211            grad_true,
1212            grad_false,
1213            free_var_mask: None,
1214        })
1215    }
1216
1217    /// Returns the largest DIMACS variable represented by the circuit.
1218    pub fn max_var(&self) -> u32 {
1219        self.max_var
1220    }
1221
1222    /// Root node id of the circuit (XGCF requires exactly one root for evaluation/verification).
1223    pub fn root(&self) -> u32 {
1224        self.root
1225    }
1226
1227    /// Capacity (upper bound) for XGCF nodes in the circuit buffers.
1228    pub fn num_nodes(&self) -> usize {
1229        self.node_cap as usize
1230    }
1231
1232    /// Capacity (upper bound) for XGCF edges in the circuit buffers.
1233    pub fn num_edges(&self) -> usize {
1234        self.edge_cap as usize
1235    }
1236
1237    /// Number of topological levels in the circuit.
1238    pub fn num_levels(&self) -> u32 {
1239        self.num_levels
1240    }
1241
1242    /// Device-resident actual node count (len = 1).
1243    pub fn num_nodes_device(&self) -> &TrackedCudaSlice<u32> {
1244        &self.meta_num_nodes
1245    }
1246
1247    /// Device-resident actual edge count (len = 1).
1248    pub fn num_edges_device(&self) -> &TrackedCudaSlice<u32> {
1249        &self.meta_num_edges
1250    }
1251
1252    /// Device-resident level -> node index mapping (len = num_nodes).
1253    pub fn level_nodes(&self) -> &TrackedCudaSlice<u32> {
1254        &self.level_nodes
1255    }
1256
1257    /// Device-resident offsets for each level (len = num_levels + 1).
1258    pub fn level_offsets(&self) -> &TrackedCudaSlice<u32> {
1259        &self.level_offsets
1260    }
1261
1262    /// Device-resident node type tags (see `XgcfNodeType`).
1263    pub fn node_type(&self) -> &TrackedCudaSlice<u8> {
1264        &self.node_type
1265    }
1266
1267    /// Device-resident CSR child offsets for AND/OR nodes (len = num_nodes + 1).
1268    pub fn child_offsets(&self) -> &TrackedCudaSlice<u32> {
1269        &self.child_offsets
1270    }
1271
1272    /// Device-resident CSR child indices for AND/OR nodes.
1273    pub fn child_indices(&self) -> &TrackedCudaSlice<u32> {
1274        &self.child_indices
1275    }
1276
1277    /// Device-resident literals for LIT nodes (signed DIMACS, 1-based var ids).
1278    pub fn lit(&self) -> &TrackedCudaSlice<i32> {
1279        &self.lit
1280    }
1281
1282    /// Device-resident decision var ids for DECISION nodes (0 for non-decision).
1283    pub fn decision_var(&self) -> &TrackedCudaSlice<u32> {
1284        &self.decision_var
1285    }
1286
1287    /// Returns the device false-branch child for every decision node.
1288    pub fn decision_child_false(&self) -> &TrackedCudaSlice<u32> {
1289        &self.decision_child_false
1290    }
1291
1292    /// Returns the device true-branch child for every decision node.
1293    pub fn decision_child_true(&self) -> &TrackedCudaSlice<u32> {
1294        &self.decision_child_true
1295    }
1296
1297    /// Device-resident per-node values buffer (log-space). Written by forward pass.
1298    pub fn values(&self) -> &TrackedCudaSlice<f64> {
1299        &self.values
1300    }
1301
1302    /// Device-resident gradient buffer for ln(true-weight) per CNF variable.
1303    pub fn grad_true(&self) -> &TrackedCudaSlice<f64> {
1304        &self.grad_true
1305    }
1306
1307    /// Device-resident gradient buffer for ln(false-weight) per CNF variable.
1308    pub fn grad_false(&self) -> &TrackedCudaSlice<f64> {
1309        &self.grad_false
1310    }
1311
1312    /// Device-resident log(true-weight) table.
1313    pub fn var_log_true(&self) -> &TrackedCudaSlice<f64> {
1314        &self.var_log_true
1315    }
1316
1317    /// Device-resident log(false-weight) table.
1318    pub fn var_log_false(&self) -> &TrackedCudaSlice<f64> {
1319        &self.var_log_false
1320    }
1321
1322    /// Mutable access to both log-weight tables (true/false) at once.
1323    ///
1324    /// This is useful when passing both slices to a single CUDA kernel launch, avoiding
1325    /// overlapping mutable borrows of `self`.
1326    pub fn var_log_weights_mut(
1327        &mut self,
1328    ) -> (&mut TrackedCudaSlice<f64>, &mut TrackedCudaSlice<f64>) {
1329        (&mut self.var_log_true, &mut self.var_log_false)
1330    }
1331
1332    /// Attach a device-resident free-variable mask (length = max_var + 1).
1333    pub fn set_free_var_mask_device(&mut self, mask: TrackedCudaSlice<u8>) -> Result<()> {
1334        if mask.len() != self.var_log_true.len() {
1335            return Err(XlogError::Compilation(format!(
1336                "GPU free-var mask len {} != weights len {}",
1337                mask.len(),
1338                self.var_log_true.len()
1339            )));
1340        }
1341        self.free_var_mask = Some(mask);
1342        Ok(())
1343    }
1344
1345    /// Upload a host weight table into the device-resident `var_log_true/var_log_false` buffers.
1346    ///
1347    /// This is intended for one-time initialization of static weights (evidence + non-neural facts).
1348    /// Neural fast-path updates should overwrite only the relevant subset on GPU.
1349    /// The 1-based table prefix (1 through `max_var`) is validated before either upload: NaN is
1350    /// rejected with a typed compilation error, while positive and negative infinity remain valid
1351    /// for value evaluation. Reserved slot 0 is uploaded unchanged but is never consumed.
1352    pub fn set_base_weights(
1353        &mut self,
1354        provider: &CudaKernelProvider,
1355        var_log_weights: &[(f64, f64)],
1356    ) -> Result<()> {
1357        let weights_len = (self.max_var as usize) + 1;
1358        if var_log_weights.len() < weights_len {
1359            return Err(XlogError::Compilation(format!(
1360                "GPU XGCF weights init expects weight table len >= {}, got {}",
1361                weights_len,
1362                var_log_weights.len()
1363            )));
1364        }
1365        for &weights in &var_log_weights[1..weights_len] {
1366            validate_circuit_log_weight_pair(weights)?;
1367        }
1368
1369        let mut host_true: Vec<f64> = Vec::with_capacity(weights_len);
1370        let mut host_false: Vec<f64> = Vec::with_capacity(weights_len);
1371        for &(t, f) in &var_log_weights[..weights_len] {
1372            host_true.push(t);
1373            host_false.push(f);
1374        }
1375
1376        provider
1377            .htod_sync_copy_into_tracked(&host_true, &mut self.var_log_true)
1378            .map_err(|e| XlogError::Kernel(format!("Failed to upload log_true weights: {}", e)))?;
1379        provider
1380            .htod_sync_copy_into_tracked(&host_false, &mut self.var_log_false)
1381            .map_err(|e| XlogError::Kernel(format!("Failed to upload log_false weights: {}", e)))?;
1382
1383        Ok(())
1384    }
1385
1386    /// Evaluate logZ on the device using the currently loaded weights and write it into `out_log_z`.
1387    ///
1388    /// This method performs no device->host transfers. Callers that mutate the device weight
1389    /// buffers directly are responsible for numeric validity. NaN or undefined arithmetic that
1390    /// reaches circuit evaluation emits a NaN sentinel in `out_log_z`; a host boundary must
1391    /// validate that scalar after readback. Positive infinity is supported for value-only
1392    /// log-sum-exp evaluation.
1393    pub fn eval_log_wmc_device_inplace(
1394        &mut self,
1395        provider: &CudaKernelProvider,
1396        out_log_z: &mut TrackedCudaSlice<f64>,
1397    ) -> Result<()> {
1398        if out_log_z.len() != 1 {
1399            return Err(XlogError::Compilation(format!(
1400                "GPU device logZ output len {} != 1",
1401                out_log_z.len()
1402            )));
1403        }
1404
1405        let device = provider.device().inner();
1406        let func = device
1407            .get_func(CIRCUIT_MODULE, circuit_kernels::XGCF_FORWARD_LEVEL)
1408            .ok_or_else(|| XlogError::Kernel("xgcf_forward_level kernel not found".to_string()))?;
1409
1410        let block_size: u32 = 256;
1411        let num_levels: usize = self.num_levels as usize;
1412        for level in 0..num_levels {
1413            let num_level_nodes: usize = match self.level_offsets_host.as_ref() {
1414                Some(off) => checked_host_level_width(off, level)?,
1415                None => self.level_nodes.len(),
1416            };
1417            if num_level_nodes == 0 {
1418                continue;
1419            }
1420
1421            let num_blocks =
1422                checked_gpu_launch_blocks("xgcf_forward_level", num_level_nodes, block_size)?;
1423            let config = LaunchConfig {
1424                grid_dim: (num_blocks, 1, 1),
1425                block_dim: (block_size, 1, 1),
1426                shared_mem_bytes: 0,
1427            };
1428            let level_u32: u32 = level as u32;
1429
1430            let mut params: Vec<*mut c_void> = vec![
1431                (&self.node_type).as_kernel_param(),
1432                (&self.child_offsets).as_kernel_param(),
1433                (&self.child_indices).as_kernel_param(),
1434                (&self.lit).as_kernel_param(),
1435                (&self.decision_var).as_kernel_param(),
1436                (&self.decision_child_false).as_kernel_param(),
1437                (&self.decision_child_true).as_kernel_param(),
1438                (&self.level_nodes).as_kernel_param(),
1439                (&self.level_offsets).as_kernel_param(),
1440                level_u32.as_kernel_param(),
1441                (&self.var_log_true).as_kernel_param(),
1442                (&self.var_log_false).as_kernel_param(),
1443                (&self.values).as_kernel_param(),
1444            ];
1445
1446            // SAFETY: xgcf_forward_level(...) writes values for the provided level nodes.
1447            unsafe { func.clone().launch(config, &mut params) }
1448                .map_err(|e| XlogError::Kernel(format!("xgcf_forward_level failed: {}", e)))?;
1449        }
1450
1451        self.apply_free_var_correction(provider, true, false)?;
1452
1453        let root_idx = self.root as usize;
1454        let root_view = self.values.slice(root_idx..(root_idx + 1));
1455        device
1456            .dtod_copy(&root_view, out_log_z)
1457            .map_err(|e| XlogError::Kernel(format!("Failed to copy device logZ: {}", e)))?;
1458
1459        // No device synchronize: callers read back with a synchronous host copy
1460        // or pass the result to subsequent GPU operations (same-stream ordering).
1461        Ok(())
1462    }
1463
1464    /// Evaluate logZ on the device and write it into `out_log_z` (uploads weights from host).
1465    ///
1466    /// Host weights are validated before launch. Undefined arithmetic can still produce a NaN
1467    /// sentinel in `out_log_z`, which the eventual host readback boundary must reject.
1468    pub fn eval_log_wmc_device_into(
1469        &mut self,
1470        provider: &CudaKernelProvider,
1471        var_log_weights: &[(f64, f64)],
1472        out_log_z: &mut TrackedCudaSlice<f64>,
1473    ) -> Result<()> {
1474        self.set_base_weights(provider, var_log_weights)?;
1475        self.eval_log_wmc_device_inplace(provider, out_log_z)
1476    }
1477
1478    /// Evaluate logZ on the device and return a device-resident scalar (uploads weights from host).
1479    ///
1480    /// The returned scalar uses NaN as the device-only sentinel for undefined circuit arithmetic;
1481    /// callers that read it back must convert that sentinel to a typed error.
1482    pub fn eval_log_wmc_device(
1483        &mut self,
1484        provider: &CudaKernelProvider,
1485        var_log_weights: &[(f64, f64)],
1486    ) -> Result<TrackedCudaSlice<f64>> {
1487        let memory = provider.memory();
1488        let mut out_log_z = memory.alloc::<f64>(1)?;
1489        self.eval_log_wmc_device_into(provider, var_log_weights, &mut out_log_z)?;
1490        Ok(out_log_z)
1491    }
1492
1493    fn apply_free_var_correction(
1494        &mut self,
1495        provider: &CudaKernelProvider,
1496        apply_log_z: bool,
1497        apply_grads: bool,
1498    ) -> Result<()> {
1499        let Some(mask) = self.free_var_mask.as_ref() else {
1500            return Ok(());
1501        };
1502
1503        if mask.len() != self.var_log_true.len() {
1504            return Err(XlogError::Compilation(format!(
1505                "GPU free-var mask len {} != weights len {}",
1506                mask.len(),
1507                self.var_log_true.len()
1508            )));
1509        }
1510
1511        let n = u32::try_from(mask.len())
1512            .map_err(|_| XlogError::Compilation("GPU free-var mask length overflow".to_string()))?;
1513        if n == 0 {
1514            return Ok(());
1515        }
1516
1517        let device = provider.device().inner();
1518        let block_dim = 256u32;
1519        let grid_dim = n.div_ceil(block_dim);
1520
1521        if apply_grads {
1522            let apply_grad = device
1523                .get_func(CIRCUIT_MODULE, circuit_kernels::XGCF_FREE_VAR_APPLY_GRAD)
1524                .ok_or_else(|| {
1525                    XlogError::Kernel("xgcf_free_var_apply_grad kernel not found".to_string())
1526                })?;
1527            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1528            unsafe {
1529                apply_grad.clone().launch(
1530                    LaunchConfig {
1531                        grid_dim: (grid_dim, 1, 1),
1532                        block_dim: (block_dim, 1, 1),
1533                        shared_mem_bytes: 0,
1534                    },
1535                    (
1536                        mask,
1537                        &self.var_log_true,
1538                        &self.var_log_false,
1539                        n,
1540                        &mut self.grad_true,
1541                        &mut self.grad_false,
1542                    ),
1543                )
1544            }
1545            .map_err(|e| XlogError::Kernel(format!("xgcf_free_var_apply_grad failed: {}", e)))?;
1546        }
1547
1548        if apply_log_z {
1549            let reduce_stage = device
1550                .get_func(CIRCUIT_MODULE, circuit_kernels::XGCF_FREE_VAR_REDUCE_STAGE)
1551                .ok_or_else(|| {
1552                    XlogError::Kernel("xgcf_free_var_reduce_stage kernel not found".to_string())
1553                })?;
1554            let add_scalar = device
1555                .get_func(CIRCUIT_MODULE, circuit_kernels::XGCF_ADD_SCALAR)
1556                .ok_or_else(|| XlogError::Kernel("xgcf_add_scalar kernel not found".to_string()))?;
1557
1558            let memory = provider.memory();
1559            let mut buf_a = memory.alloc::<f64>(mask.len())?;
1560            let mut buf_b = memory.alloc::<f64>(mask.len())?;
1561
1562            let mut stage_n = n;
1563            let mut stage0 = true;
1564            let mut output_is_a = true;
1565            loop {
1566                let out_len = stage_n.div_ceil(2);
1567                let stage_grid = out_len.div_ceil(block_dim);
1568
1569                let (in_buf, out_buf): (&TrackedCudaSlice<f64>, &mut TrackedCudaSlice<f64>) =
1570                    if output_is_a {
1571                        (&buf_b, &mut buf_a)
1572                    } else {
1573                        (&buf_a, &mut buf_b)
1574                    };
1575                let mode = if stage0 { 0u32 } else { 1u32 };
1576
1577                // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1578                unsafe {
1579                    reduce_stage.clone().launch(
1580                        LaunchConfig {
1581                            grid_dim: (stage_grid, 1, 1),
1582                            block_dim: (block_dim, 1, 1),
1583                            shared_mem_bytes: 0,
1584                        },
1585                        (
1586                            mask,
1587                            &self.var_log_true,
1588                            &self.var_log_false,
1589                            in_buf,
1590                            stage_n,
1591                            mode,
1592                            out_buf,
1593                        ),
1594                    )
1595                }
1596                .map_err(|e| {
1597                    XlogError::Kernel(format!("xgcf_free_var_reduce_stage failed: {}", e))
1598                })?;
1599
1600                if out_len == 1 {
1601                    let result_buf = if output_is_a { &buf_a } else { &buf_b };
1602                    // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1603                    unsafe {
1604                        add_scalar.clone().launch(
1605                            LaunchConfig {
1606                                grid_dim: (1, 1, 1),
1607                                block_dim: (1, 1, 1),
1608                                shared_mem_bytes: 0,
1609                            },
1610                            (&mut self.values, self.root, result_buf),
1611                        )
1612                    }
1613                    .map_err(|e| XlogError::Kernel(format!("xgcf_add_scalar failed: {}", e)))?;
1614                    break;
1615                }
1616
1617                stage_n = out_len;
1618                stage0 = false;
1619                output_is_a = !output_is_a;
1620            }
1621        }
1622
1623        Ok(())
1624    }
1625
1626    /// Evaluate the circuit and populate `grad_true/grad_false` on the device (no host reads).
1627    ///
1628    /// Preconditions:
1629    /// - `var_log_true/var_log_false` contain the current weights on device. Callers must validate
1630    ///   numeric outputs: NaN or undefined normalization that reaches evaluation is represented by
1631    ///   a non-finite device result because this API cannot return a host-side numeric error.
1632    /// - Caller may read back results for testing/debugging, but this API performs no dtoh transfers.
1633    pub fn eval_grads_inplace(&mut self, provider: &CudaKernelProvider) -> Result<()> {
1634        let device = provider.device().inner();
1635
1636        // Forward pass (identical to eval_log_wmc, minus weight upload and root readback).
1637        let func = device
1638            .get_func(CIRCUIT_MODULE, circuit_kernels::XGCF_FORWARD_LEVEL)
1639            .ok_or_else(|| XlogError::Kernel("xgcf_forward_level kernel not found".to_string()))?;
1640
1641        let block_size: u32 = 256;
1642        let num_levels: usize = self.num_levels as usize;
1643        for level in 0..num_levels {
1644            let num_level_nodes: usize = match self.level_offsets_host.as_ref() {
1645                Some(off) => checked_host_level_width(off, level)?,
1646                None => self.level_nodes.len(),
1647            };
1648            if num_level_nodes == 0 {
1649                continue;
1650            }
1651
1652            let num_blocks =
1653                checked_gpu_launch_blocks("xgcf_forward_level", num_level_nodes, block_size)?;
1654            let config = LaunchConfig {
1655                grid_dim: (num_blocks, 1, 1),
1656                block_dim: (block_size, 1, 1),
1657                shared_mem_bytes: 0,
1658            };
1659            let level_u32: u32 = level as u32;
1660
1661            let mut params: Vec<*mut c_void> = vec![
1662                (&self.node_type).as_kernel_param(),
1663                (&self.child_offsets).as_kernel_param(),
1664                (&self.child_indices).as_kernel_param(),
1665                (&self.lit).as_kernel_param(),
1666                (&self.decision_var).as_kernel_param(),
1667                (&self.decision_child_false).as_kernel_param(),
1668                (&self.decision_child_true).as_kernel_param(),
1669                (&self.level_nodes).as_kernel_param(),
1670                (&self.level_offsets).as_kernel_param(),
1671                level_u32.as_kernel_param(),
1672                (&self.var_log_true).as_kernel_param(),
1673                (&self.var_log_false).as_kernel_param(),
1674                (&self.values).as_kernel_param(),
1675            ];
1676
1677            // SAFETY: xgcf_forward_level(...) writes values for the provided level nodes.
1678            unsafe { func.clone().launch(config, &mut params) }
1679                .map_err(|e| XlogError::Kernel(format!("xgcf_forward_level failed: {}", e)))?;
1680        }
1681
1682        // Backward pass buffers.
1683        device
1684            .memset_zeros(&mut self.adj)
1685            .map_err(|e| XlogError::Kernel(format!("Failed to zero adj buffer: {}", e)))?;
1686        device
1687            .memset_zeros(&mut self.grad_true)
1688            .map_err(|e| XlogError::Kernel(format!("Failed to zero grad_true buffer: {}", e)))?;
1689        device
1690            .memset_zeros(&mut self.grad_false)
1691            .map_err(|e| XlogError::Kernel(format!("Failed to zero grad_false buffer: {}", e)))?;
1692
1693        // Set root adjoint to 1.0 via GPU kernel (avoid host copy).
1694        let root_idx = self.root as usize;
1695        let mut root_adj_view = self.adj.slice_mut(root_idx..(root_idx + 1));
1696        let fill_const = device
1697            .get_func(ARITH_MODULE, arith_kernels::ARITH_FILL_CONST_F64)
1698            .ok_or_else(|| {
1699                XlogError::Kernel("arith_fill_const_f64 kernel not found".to_string())
1700            })?;
1701        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1702        unsafe {
1703            fill_const.clone().launch(
1704                LaunchConfig {
1705                    grid_dim: (1, 1, 1),
1706                    block_dim: (1, 1, 1),
1707                    shared_mem_bytes: 0,
1708                },
1709                (1.0_f64, 1u32, &mut root_adj_view),
1710            )
1711        }
1712        .map_err(|e| XlogError::Kernel(format!("arith_fill_const_f64 failed: {}", e)))?;
1713
1714        let propagate = device
1715            .get_func(
1716                CIRCUIT_MODULE,
1717                circuit_kernels::XGCF_BACKWARD_LEVEL_PROPAGATE,
1718            )
1719            .ok_or_else(|| {
1720                XlogError::Kernel("xgcf_backward_level_propagate kernel not found".to_string())
1721            })?;
1722        let decision_grad = device
1723            .get_func(
1724                CIRCUIT_MODULE,
1725                circuit_kernels::XGCF_BACKWARD_LEVEL_DECISION_GRAD,
1726            )
1727            .ok_or_else(|| {
1728                XlogError::Kernel("xgcf_backward_level_decision_grad kernel not found".to_string())
1729            })?;
1730        let lit_grad = device
1731            .get_func(
1732                CIRCUIT_MODULE,
1733                circuit_kernels::XGCF_BACKWARD_LEVEL_LIT_GRAD,
1734            )
1735            .ok_or_else(|| {
1736                XlogError::Kernel("xgcf_backward_level_lit_grad kernel not found".to_string())
1737            })?;
1738
1739        let num_levels: usize = self.num_levels as usize;
1740        for level in (0..num_levels).rev() {
1741            let num_level_nodes: usize = match self.level_offsets_host.as_ref() {
1742                Some(off) => checked_host_level_width(off, level)?,
1743                None => self.level_nodes.len(),
1744            };
1745            if num_level_nodes == 0 {
1746                continue;
1747            }
1748
1749            let num_blocks =
1750                checked_gpu_launch_blocks("xgcf_backward_level", num_level_nodes, block_size)?;
1751            let config = LaunchConfig {
1752                grid_dim: (num_blocks, 1, 1),
1753                block_dim: (block_size, 1, 1),
1754                shared_mem_bytes: 0,
1755            };
1756            let level_u32: u32 = level as u32;
1757
1758            let mut params: Vec<*mut c_void> = vec![
1759                (&self.node_type).as_kernel_param(),
1760                (&self.child_offsets).as_kernel_param(),
1761                (&self.child_indices).as_kernel_param(),
1762                (&self.decision_var).as_kernel_param(),
1763                (&self.decision_child_false).as_kernel_param(),
1764                (&self.decision_child_true).as_kernel_param(),
1765                (&self.level_nodes).as_kernel_param(),
1766                (&self.level_offsets).as_kernel_param(),
1767                level_u32.as_kernel_param(),
1768                (&self.var_log_true).as_kernel_param(),
1769                (&self.var_log_false).as_kernel_param(),
1770                (&self.values).as_kernel_param(),
1771                (&self.adj).as_kernel_param(),
1772            ];
1773
1774            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1775            unsafe { propagate.clone().launch(config, &mut params) }.map_err(|e| {
1776                XlogError::Kernel(format!("xgcf_backward_level_propagate failed: {}", e))
1777            })?;
1778
1779            let mut params: Vec<*mut c_void> = vec![
1780                (&self.node_type).as_kernel_param(),
1781                (&self.decision_var).as_kernel_param(),
1782                (&self.decision_child_false).as_kernel_param(),
1783                (&self.decision_child_true).as_kernel_param(),
1784                (&self.level_nodes).as_kernel_param(),
1785                (&self.level_offsets).as_kernel_param(),
1786                level_u32.as_kernel_param(),
1787                (&self.var_log_true).as_kernel_param(),
1788                (&self.var_log_false).as_kernel_param(),
1789                (&self.values).as_kernel_param(),
1790                (&self.adj).as_kernel_param(),
1791                (&self.grad_true).as_kernel_param(),
1792                (&self.grad_false).as_kernel_param(),
1793            ];
1794
1795            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1796            unsafe { decision_grad.clone().launch(config, &mut params) }.map_err(|e| {
1797                XlogError::Kernel(format!("xgcf_backward_level_decision_grad failed: {}", e))
1798            })?;
1799
1800            // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1801            unsafe {
1802                lit_grad.clone().launch(
1803                    config,
1804                    (
1805                        &self.node_type,
1806                        &self.lit,
1807                        &self.level_nodes,
1808                        &self.level_offsets,
1809                        level_u32,
1810                        &self.adj,
1811                        &self.grad_true,
1812                        &self.grad_false,
1813                    ),
1814                )
1815            }
1816            .map_err(|e| {
1817                XlogError::Kernel(format!("xgcf_backward_level_lit_grad failed: {}", e))
1818            })?;
1819        }
1820
1821        self.apply_free_var_correction(provider, true, true)?;
1822        // No device synchronize: callers batch multiple eval/backward calls
1823        // before syncing at the query boundary.
1824        Ok(())
1825    }
1826
1827    #[cfg(feature = "host-io")]
1828    /// Evaluates log weighted model count and returns the host scalar.
1829    pub fn eval_log_wmc(
1830        &mut self,
1831        provider: &CudaKernelProvider,
1832        var_log_weights: &[(f64, f64)],
1833    ) -> Result<f64> {
1834        let device = provider.device().inner();
1835        let mut out_log_z = provider.memory().alloc::<f64>(1)?;
1836        self.eval_log_wmc_device_into(provider, var_log_weights, &mut out_log_z)?;
1837
1838        let mut host = [0.0_f64];
1839        device
1840            .dtoh_sync_copy_into(&out_log_z, &mut host)
1841            .map_err(|e| XlogError::Kernel(format!("Failed to read circuit root value: {}", e)))?;
1842        validate_circuit_value(host[0])
1843    }
1844
1845    #[cfg(feature = "host-io")]
1846    /// Evaluates log weighted model count and returns host gradients.
1847    pub fn eval_log_wmc_and_grads(
1848        &mut self,
1849        provider: &CudaKernelProvider,
1850        var_log_weights: &[(f64, f64)],
1851    ) -> Result<(f64, Vec<f64>, Vec<f64>)> {
1852        let weights_len = (self.max_var as usize) + 1;
1853        if var_log_weights.len() < weights_len {
1854            return Err(XlogError::Compilation(format!(
1855                "GPU XGCF weights init expects weight table len >= {}, got {}",
1856                weights_len,
1857                var_log_weights.len()
1858            )));
1859        }
1860        self.set_base_weights(provider, var_log_weights)?;
1861        self.eval_grads_inplace(provider)?;
1862
1863        let device = provider.device().inner();
1864
1865        let mut host_grad_true: Vec<f64> = vec![0.0; weights_len];
1866        let mut host_grad_false: Vec<f64> = vec![0.0; weights_len];
1867
1868        let root_idx = self.root as usize;
1869        let root_view = self.values.slice(root_idx..(root_idx + 1));
1870        let mut log_z = [0.0_f64];
1871        device
1872            .dtoh_sync_copy_into(&root_view, &mut log_z)
1873            .map_err(|e| XlogError::Kernel(format!("Failed to read circuit root value: {}", e)))?;
1874        let log_z = validate_circuit_value(log_z[0])?;
1875
1876        device
1877            .dtoh_sync_copy_into(&self.grad_true, &mut host_grad_true)
1878            .map_err(|e| XlogError::Kernel(format!("Failed to download grad_true: {}", e)))?;
1879        device
1880            .dtoh_sync_copy_into(&self.grad_false, &mut host_grad_false)
1881            .map_err(|e| XlogError::Kernel(format!("Failed to download grad_false: {}", e)))?;
1882        validate_circuit_gradient_values(&host_grad_true, &host_grad_false)?;
1883
1884        Ok((log_z, host_grad_true, host_grad_false))
1885    }
1886}