Skip to main content

xlog_prob/compilation/
mod.rs

1//! GPU-native knowledge compilation.
2//!
3//! This module is the home of GPU-native compilation + verification utilities.
4//!
5//! Production correctness requires the GPU CDCL equivalence verifier (see `validation`).
6
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9use std::time::Instant;
10
11use xlog_core::{resolve_bool, Result, XlogError};
12use xlog_cuda::memory::TrackedCudaSlice;
13use xlog_cuda::CudaKernelProvider;
14use xlog_solve::{GpuCdclConfig, GpuCnf};
15
16use crate::compilation::gpu_cache::{GpuCircuitCache, GpuCircuitCacheHandle};
17use crate::gpu::{GpuCircuitBuilder, GpuCircuitLayout, GpuXgcf};
18
19pub mod disk_cache;
20pub mod gpu_cache;
21pub mod gpu_cnf;
22pub mod gpu_d4;
23pub mod gpu_pir;
24pub mod gpu_pir_intern;
25pub mod gpu_weights;
26pub mod validation;
27
28pub use gpu_cnf::{encode_cnf_gpu, GpuCnfEncoding, GpuCnfVarTables};
29pub use gpu_d4::GpuCompileConfig;
30pub use gpu_pir::{GpuPirGraph, GpuPirRoots, PIR_AND, PIR_LIT, PIR_NEG_LIT, PIR_OR};
31// PIR_CONST and PIR_DECISION are used within gpu_pir.rs and gpu_pir_intern.rs
32// via direct module paths; no crate-level re-export needed.
33pub use gpu_pir_intern::{GpuPirInterner, PirBatch};
34pub use gpu_weights::GpuWeights;
35pub use gpu_weights::{
36    apply_query_vars_device, build_evidence_by_var_gpu, build_weights_gpu, map_nodes_to_vars_gpu,
37    restore_query_vars_device,
38};
39pub use validation::{
40    build_equivalence_queries_gpu, validate_equivalence_gpu, validate_equivalence_gpu_gated,
41    GpuEquivalenceConfig, GpuEquivalenceQueries,
42};
43// check_equivalence_gpu and check_equivalence_gpu_gated are called only
44// within validation.rs itself; no crate-level re-export needed.
45
46/// Per-stage compilation timing (populated only when XLOG_WARMUP_PROFILE=1).
47#[derive(Debug, Clone, Default)]
48pub struct CircuitCompileProfile {
49    /// Seconds spent computing the canonical CNF hash.
50    pub cnf_hash_sec: f64,
51    /// Seconds spent compiling the circuit.
52    pub d4_compile_sec: f64,
53    /// Seconds spent verifying logical equivalence.
54    pub verify_sec: f64,
55    /// Seconds spent smoothing the final circuit.
56    pub smooth_sec: f64,
57    /// Seconds spent storing the verified circuit in cache.
58    pub cache_store_sec: f64,
59    /// Seconds spent constructing the free-variable mask.
60    pub free_var_mask_sec: f64,
61    /// Whether the circuit came from the resident GPU cache.
62    pub gpu_cache_hit: bool,
63    /// Whether the circuit came from the verified disk cache.
64    pub disk_cache_hit: bool,
65    /// BFS frontier item count after `frontier_depth` expansion steps
66    /// (0 on cache hits or when profiling is disabled).
67    pub frontier_items: u32,
68}
69
70/// Monotonic event ledger for one exact-circuit materialization path.
71///
72/// Mutation is private to this module so counts can only change at the GPU
73/// compiler, verified disk-restore, GPU-cache-hit, and successful
74/// materialization boundaries below. The exact state retains this ledger and
75/// snapshots it around every prepared evaluation.
76#[derive(Debug, Default)]
77pub(crate) struct CircuitCompilationLedger {
78    compiler_invocations: AtomicU64,
79    materializations: AtomicU64,
80    disk_cache_restores: AtomicU64,
81    gpu_cache_hits: AtomicU64,
82}
83
84#[cfg(feature = "host-io")]
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub(crate) struct CircuitCompilationSnapshot {
87    pub(crate) compiler_invocations: u64,
88    pub(crate) materializations: u64,
89    pub(crate) disk_cache_restores: u64,
90    pub(crate) gpu_cache_hits: u64,
91}
92
93impl CircuitCompilationLedger {
94    pub(crate) fn new() -> Self {
95        Self::default()
96    }
97
98    #[cfg(feature = "host-io")]
99    pub(crate) fn snapshot(&self) -> CircuitCompilationSnapshot {
100        CircuitCompilationSnapshot {
101            compiler_invocations: self.compiler_invocations.load(Ordering::Acquire),
102            materializations: self.materializations.load(Ordering::Acquire),
103            disk_cache_restores: self.disk_cache_restores.load(Ordering::Acquire),
104            gpu_cache_hits: self.gpu_cache_hits.load(Ordering::Acquire),
105        }
106    }
107
108    fn record_compiler_invocation(&self) -> Result<()> {
109        increment_circuit_event(
110            &self.compiler_invocations,
111            "GPU circuit compiler invocation",
112        )
113    }
114
115    fn record_materialization(&self) -> Result<()> {
116        increment_circuit_event(&self.materializations, "exact circuit materialization")
117    }
118
119    fn record_disk_cache_restore(&self) -> Result<()> {
120        increment_circuit_event(&self.disk_cache_restores, "verified disk-cache restoration")
121    }
122
123    fn record_gpu_cache_hit(&self) -> Result<()> {
124        increment_circuit_event(&self.gpu_cache_hits, "GPU circuit-cache hit")
125    }
126}
127
128/// Cache identity and authoritative event ledger for one compilation attempt.
129pub(crate) struct CircuitCompilationContext<'a> {
130    pub(crate) canonical_cnf_hash: Option<u64>,
131    pub(crate) ledger: &'a CircuitCompilationLedger,
132}
133
134fn increment_circuit_event(counter: &AtomicU64, event: &str) -> Result<()> {
135    counter
136        .try_update(Ordering::AcqRel, Ordering::Acquire, |value| {
137            value.checked_add(1)
138        })
139        .map(|_| ())
140        .map_err(|_| XlogError::Compilation(format!("{event} counter overflowed")))
141}
142
143pub(crate) fn warmup_profiling_enabled() -> Result<bool> {
144    resolve_bool(None, "XLOG_WARMUP_PROFILE", false)
145}
146
147/// Device-resident random-variable list for GPU smoothing.
148pub struct DeviceRandomVarList {
149    list: TrackedCudaSlice<u32>,
150    count: u32,
151}
152
153/// Circuit whose exact topology has passed equivalence verification.
154///
155/// The field is private so topology-changing code cannot construct this type
156/// without going through [`certify_final_circuit`].
157struct CertifiedGpuCircuit(GpuXgcf);
158
159impl CertifiedGpuCircuit {
160    fn as_xgcf(&self) -> &GpuXgcf {
161        &self.0
162    }
163}
164
165impl DeviceRandomVarList {
166    /// Constructs a validated list from an existing device allocation and logical count.
167    pub fn from_device(list: TrackedCudaSlice<u32>, count: u32) -> Result<Self> {
168        let len = u32::try_from(list.len()).map_err(|_| {
169            XlogError::Compilation("DeviceRandomVarList: list length exceeds u32".to_string())
170        })?;
171        if count > len {
172            return Err(XlogError::Compilation(format!(
173                "DeviceRandomVarList: count {} exceeds list len {}",
174                count, len
175            )));
176        }
177        Ok(Self { list, count })
178    }
179
180    /// Uploads a host list of random-variable identifiers.
181    pub fn from_host(provider: &CudaKernelProvider, host: &[u32]) -> Result<Self> {
182        let memory = provider.memory();
183        let mut list = memory.alloc::<u32>(host.len())?;
184        if !host.is_empty() {
185            provider
186                .htod_sync_copy_into_tracked(host, &mut list)
187                .map_err(|e| {
188                    XlogError::Kernel(format!("DeviceRandomVarList upload failed: {}", e))
189                })?;
190        }
191        let count = u32::try_from(host.len()).map_err(|_| {
192            XlogError::Compilation("DeviceRandomVarList: host len exceeds u32".to_string())
193        })?;
194        Ok(Self { list, count })
195    }
196
197    /// Returns whether the logical list is empty.
198    pub fn is_empty(&self) -> bool {
199        self.count == 0
200    }
201
202    /// Returns the logical number of random variables.
203    pub fn count(&self) -> u32 {
204        self.count
205    }
206
207    /// Returns the device-resident variable identifiers.
208    pub fn list(&self) -> &TrackedCudaSlice<u32> {
209        &self.list
210    }
211}
212
213fn certify_final_circuit(
214    cnf: &GpuCnf,
215    decision_var_limit: &TrackedCudaSlice<u32>,
216    circuit: GpuXgcf,
217    provider: &Arc<CudaKernelProvider>,
218    config: GpuEquivalenceConfig,
219    gate: &TrackedCudaSlice<u32>,
220) -> Result<CertifiedGpuCircuit> {
221    validate_equivalence_gpu_gated(cnf, decision_var_limit, &circuit, provider, config, gate)?;
222    Ok(CertifiedGpuCircuit(circuit))
223}
224
225fn upload_disk_artifact_for_verification(
226    artifact: &disk_cache::CircuitArtifact,
227    provider: &Arc<CudaKernelProvider>,
228) -> Result<GpuXgcf> {
229    let memory = provider.memory().clone();
230
231    macro_rules! upload {
232        ($field:expr, $ty:ty, $name:literal) => {{
233            let mut device = memory.alloc::<$ty>($field.len())?;
234            provider
235                .htod_sync_copy_into_tracked($field, &mut device)
236                .map_err(|e| {
237                    XlogError::Kernel(format!("disk cache verify upload {} failed: {}", $name, e))
238                })?;
239            device
240        }};
241    }
242
243    let builder = GpuCircuitBuilder {
244        node_type: upload!(&artifact.node_type, u8, "node_type"),
245        child_offsets: upload!(&artifact.child_offsets, u32, "child_offsets"),
246        child_indices: upload!(&artifact.child_indices, u32, "child_indices"),
247        lit: upload!(&artifact.lit, i32, "lit"),
248        decision_var: upload!(&artifact.decision_var, u32, "decision_var"),
249        decision_child_false: upload!(&artifact.decision_child_false, u32, "decision_child_false"),
250        decision_child_true: upload!(&artifact.decision_child_true, u32, "decision_child_true"),
251    };
252    let layout = GpuCircuitLayout {
253        num_nodes: artifact.num_nodes,
254        num_edges: artifact.num_edges,
255        num_levels: artifact.num_levels,
256        level_offsets: upload!(&artifact.level_offsets, u32, "level_offsets"),
257        level_nodes: upload!(&artifact.level_nodes, u32, "level_nodes"),
258        root: artifact.root,
259        max_var: artifact.max_var,
260        num_nodes_device: None,
261        num_edges_device: None,
262    };
263
264    GpuXgcf::from_device(builder, layout, provider)
265}
266
267/// Compile CNF on GPU, then verify equivalence with GPU CDCL.
268pub fn compile_gpu_d4_and_verify(
269    cnf: &GpuCnf,
270    decision_var_limit: &TrackedCudaSlice<u32>,
271    provider: &Arc<CudaKernelProvider>,
272    config: &GpuCompileConfig,
273) -> Result<GpuXgcf> {
274    if config.cdcl_conflict_budget.is_some() {
275        return Err(XlogError::Compilation(
276            "cdcl_conflict_budget is not supported by the GPU CDCL verifier".to_string(),
277        ));
278    }
279    // Size guard BEFORE compile — the D4 compile itself can crash with a
280    // context-poisoning launch failure on a large CNF, earlier than the verify.
281    validation::check_verify_size_bound(cnf, "compile_gpu_d4_and_verify")?;
282    let circuit = gpu_d4::compile_gpu_d4(cnf, provider, config)?;
283    let cdcl = cdcl_config_from_compile(config)?;
284    validate_equivalence_gpu(
285        cnf,
286        decision_var_limit,
287        &circuit,
288        provider,
289        GpuEquivalenceConfig {
290            cdcl,
291            reuse_workspace: config.incremental_verify,
292        },
293    )?;
294    Ok(circuit)
295}
296
297/// Compile CNF on GPU, cache the circuit, then verify equivalence with GPU CDCL.
298///
299/// `canonical_cnf_hash`: a process-independent hash of the PIR structure, used as
300/// the `cnf_hash` in the disk cache key. Computed via [`crate::cnf::canonical_pir_hash`].
301/// If `None`, disk caching is skipped.
302pub fn compile_gpu_d4_and_verify_cached(
303    cnf: &GpuCnf,
304    decision_var_limit: &TrackedCudaSlice<u32>,
305    provider: &Arc<CudaKernelProvider>,
306    config: &GpuCompileConfig,
307    cache: &mut GpuCircuitCache,
308    random_vars: &DeviceRandomVarList,
309    canonical_cnf_hash: Option<u64>,
310) -> Result<(GpuCircuitCacheHandle, Option<CircuitCompileProfile>)> {
311    let ledger = CircuitCompilationLedger::new();
312    compile_gpu_d4_and_verify_cached_with_ledger(
313        cnf,
314        decision_var_limit,
315        provider,
316        config,
317        cache,
318        random_vars,
319        CircuitCompilationContext {
320            canonical_cnf_hash,
321            ledger: &ledger,
322        },
323    )
324}
325
326pub(crate) fn compile_gpu_d4_and_verify_cached_with_ledger(
327    cnf: &GpuCnf,
328    decision_var_limit: &TrackedCudaSlice<u32>,
329    provider: &Arc<CudaKernelProvider>,
330    config: &GpuCompileConfig,
331    cache: &mut GpuCircuitCache,
332    random_vars: &DeviceRandomVarList,
333    context: CircuitCompilationContext<'_>,
334) -> Result<(GpuCircuitCacheHandle, Option<CircuitCompileProfile>)> {
335    let CircuitCompilationContext {
336        canonical_cnf_hash,
337        ledger,
338    } = context;
339    if config.cdcl_conflict_budget.is_some() {
340        return Err(XlogError::Compilation(
341            "cdcl_conflict_budget is not supported by the GPU CDCL verifier".to_string(),
342        ));
343    }
344    // Size guard BEFORE compile (the D4 compile can crash earlier than
345    // the verify on a large CNF).
346    validation::check_verify_size_bound(cnf, "compile_gpu_d4_and_verify_cached")?;
347
348    let profiling = warmup_profiling_enabled()?;
349    let mut profile = CircuitCompileProfile::default();
350
351    // --- CNF hash stage ---
352    #[cfg(debug_assertions)]
353    eprintln!("[xlog-prob] compile_gpu_d4_and_verify_cached: hash_cnf_gpu");
354    let t_hash = if profiling {
355        Some(Instant::now())
356    } else {
357        None
358    };
359    let key = gpu_cache::hash_cnf_gpu(cnf, provider)?;
360    if let Some(t0) = t_hash {
361        provider
362            .device()
363            .synchronize()
364            .map_err(|e| XlogError::Kernel(format!("sync after hash_cnf_gpu: {}", e)))?;
365        profile.cnf_hash_sec = t0.elapsed().as_secs_f64();
366    }
367    #[cfg(debug_assertions)]
368    {
369        if !profiling {
370            provider
371                .device()
372                .synchronize()
373                .map_err(|e| XlogError::Kernel(format!("sync after hash_cnf_gpu failed: {}", e)))?;
374        }
375    }
376    #[cfg(debug_assertions)]
377    eprintln!("[xlog-prob] compile_gpu_d4_and_verify_cached: lookup_or_insert_device");
378    let lookup = cache.lookup_or_insert_device(&key)?;
379    let mut handle = lookup.into_handle()?;
380
381    // --- Disk cache check (only on GPU cache miss) ---
382    //
383    // D→H copy compile_needed to decide whether we need to compile at all.
384    // If compile_needed == 0, the GPU cache already has the circuit (GPU cache hit).
385    // If compile_needed == 1, we check the disk cache before falling through to the
386    // GPU-native Decision-DNNF compiler.
387    let compile_needed_host: Vec<u32> = provider
388        .device()
389        .inner()
390        .dtoh_sync_copy(handle.compile_needed_device())
391        .map_err(|e| XlogError::Kernel(format!("dtoh compile_needed: {}", e)))?;
392    let compile_needed = compile_needed_host[0];
393
394    // GPU cache hit — short-circuit the entire compile pipeline.
395    if compile_needed == 0 {
396        profile.gpu_cache_hit = true;
397        ledger.record_gpu_cache_hit()?;
398        ledger.record_materialization()?;
399        let out_profile = if profiling { Some(profile) } else { None };
400        return Ok((handle, out_profile));
401    }
402
403    // Build the disk cache key (we know compile_needed == 1 at this point).
404    // The canonical PIR hash keeps semantically stable cache identity, while the encoded CNF
405    // hash guards symmetric conditioned programs whose PIR shape is identical but whose CNF is not.
406    let cache_key = if compile_needed == 1 {
407        if let Some(canonical_hash) = canonical_cnf_hash {
408            let config_hash = hash_compile_config(config);
409            let random_vars_hash = hash_random_vars(random_vars, provider)?;
410            let gpu_cnf_hash = read_gpu_cnf_hash(&key, provider)?;
411            let cnf_hash = combine_disk_cnf_hash(canonical_hash, gpu_cnf_hash);
412            let sm = detect_compute_capability(provider)?;
413            Some(disk_cache::CircuitCacheKey {
414                cnf_hash,
415                config_hash,
416                random_vars_hash,
417                sm,
418            })
419        } else {
420            None
421        }
422    } else {
423        None
424    };
425
426    // Check disk cache on GPU cache miss
427    if let Some(ref disk_key) = cache_key {
428        #[cfg(debug_assertions)]
429        eprintln!("[xlog-prob] compile_gpu_d4_and_verify_cached: checking disk cache");
430        if let Ok(Some(artifact)) = disk_cache::read_artifact(disk_key) {
431            #[cfg(debug_assertions)]
432            eprintln!("[xlog-prob] compile_gpu_d4_and_verify_cached: disk cache hit");
433            // Stale-artifact guard: the canonical PIR hash keeps semantically-stable
434            // cache identity, so an entry written by an earlier engine whose ENCODING
435            // differed (different var universe) can be served for the same key. A
436            // cached circuit referencing vars beyond the freshly-encoded CNF is
437            // staleness, not an engine bug — evict and fall through to a fresh
438            // compile (which overwrites the entry). Fail-closed equivalence checking
439            // stays authoritative for freshly-compiled circuits below.
440            if artifact.max_var > cnf.var_cap {
441                #[cfg(debug_assertions)]
442                eprintln!(
443                    "[xlog-prob] compile_gpu_d4_and_verify_cached: stale disk artifact \
444                     (max_var {} > cnf var_cap {}), evicting",
445                    artifact.max_var, cnf.var_cap
446                );
447                disk_cache::evict_artifact(disk_key);
448            } else {
449                let circuit = upload_disk_artifact_for_verification(&artifact, provider)?;
450                let verifier_decision_var_limit = if random_vars.is_empty() {
451                    &cnf.num_vars
452                } else {
453                    decision_var_limit
454                };
455                let cdcl = cdcl_config_from_compile(config)?;
456                let t_verify = if profiling {
457                    Some(Instant::now())
458                } else {
459                    None
460                };
461                match certify_final_circuit(
462                    cnf,
463                    verifier_decision_var_limit,
464                    circuit,
465                    provider,
466                    GpuEquivalenceConfig {
467                        cdcl,
468                        reuse_workspace: config.incremental_verify,
469                    },
470                    handle.compile_needed_device(),
471                ) {
472                    Ok(_certified_circuit) => {
473                        if let Some(t0) = t_verify {
474                            provider.device().synchronize().map_err(|e| {
475                                XlogError::Kernel(format!("sync after disk cache verify: {}", e))
476                            })?;
477                            profile.verify_sec = t0.elapsed().as_secs_f64();
478                        }
479                        cache.restore_from_host_arrays(&mut handle, &artifact)?;
480                        provider.device().synchronize().map_err(|e| {
481                            XlogError::Kernel(format!("sync after disk cache restore: {}", e))
482                        })?;
483                        profile.disk_cache_hit = true;
484                        ledger.record_disk_cache_restore()?;
485                        ledger.record_materialization()?;
486                        let out_profile = if profiling { Some(profile) } else { None };
487                        return Ok((handle, out_profile));
488                    }
489                    Err(_stale) => {
490                        // A cached artifact failing equivalence against the current
491                        // CNF is a stale entry (fresh-compile verification below
492                        // remains fail-closed). Evict and recompile.
493                        #[cfg(debug_assertions)]
494                        eprintln!(
495                            "[xlog-prob] compile_gpu_d4_and_verify_cached: cached circuit \
496                             failed equivalence against current CNF ({_stale}), evicting"
497                        );
498                        disk_cache::evict_artifact(disk_key);
499                    }
500                }
501            }
502        }
503        #[cfg(debug_assertions)]
504        eprintln!("[xlog-prob] compile_gpu_d4_and_verify_cached: disk cache miss");
505    }
506
507    let d4_config = d4_config_for_smoothing(config, random_vars.count())?;
508
509    // --- GPU-native Decision-DNNF compile stage ---
510    #[cfg(debug_assertions)]
511    eprintln!("[xlog-prob] compile_gpu_d4_and_verify_cached: compile_gpu_d4_gated");
512    let t_d4 = if profiling {
513        Some(Instant::now())
514    } else {
515        None
516    };
517    ledger.record_compiler_invocation()?;
518    let (circuit_base, frontier_items) = gpu_d4::compile_gpu_d4_gated_with_stats(
519        cnf,
520        provider,
521        &d4_config,
522        handle.compile_needed_device(),
523    )?;
524    profile.frontier_items = frontier_items;
525    if let Some(t0) = t_d4 {
526        provider
527            .device()
528            .synchronize()
529            .map_err(|e| XlogError::Kernel(format!("sync after d4 compile: {}", e)))?;
530        profile.d4_compile_sec = t0.elapsed().as_secs_f64();
531    }
532    #[cfg(debug_assertions)]
533    {
534        if !profiling {
535            provider.device().synchronize().map_err(|e| {
536                XlogError::Kernel(format!("sync after compile_gpu_d4_gated failed: {}", e))
537            })?;
538        }
539    }
540    if circuit_base.num_nodes() == 0 || circuit_base.num_levels() == 0 {
541        // Defensive: the GPU-native Decision-DNNF compiler returned an empty circuit
542        // (the primary GPU cache hit is handled by the compile_needed == 0 early return
543        // above; this catches degenerate CNFs).
544        ledger.record_materialization()?;
545        let out_profile = if profiling { Some(profile) } else { None };
546        return Ok((handle, out_profile));
547    }
548
549    // --- Smoothing stage ---
550    let t_smooth = if profiling {
551        Some(Instant::now())
552    } else {
553        None
554    };
555    let circuit_eval = if random_vars.is_empty() {
556        circuit_base
557    } else {
558        #[cfg(debug_assertions)]
559        eprintln!("[xlog-prob] compile_gpu_d4_and_verify_cached: smooth_random_vars_device");
560        let smoothed = circuit_base.smooth_random_vars_device(
561            provider,
562            random_vars.list(),
563            random_vars.count(),
564            config.smooth_node_cap,
565            config.smooth_edge_cap,
566        )?;
567        #[cfg(debug_assertions)]
568        {
569            if !profiling {
570                provider.device().synchronize().map_err(|e| {
571                    XlogError::Kernel(format!(
572                        "sync after smooth_random_vars_device failed: {}",
573                        e
574                    ))
575                })?;
576            }
577        }
578        smoothed
579    };
580    if let Some(t0) = t_smooth {
581        provider
582            .device()
583            .synchronize()
584            .map_err(|e| XlogError::Kernel(format!("sync after smooth: {}", e)))?;
585        profile.smooth_sec = t0.elapsed().as_secs_f64();
586    }
587
588    // --- Verify final-circuit equivalence stage ---
589    //
590    // Certification is deliberately after every topology-changing pass. The
591    // exact `circuit_eval` value verified here is the value cached below and
592    // consumed by weighted model counting and gradients.
593    //
594    // `encode_cnf_gpu` sets `decision_var_limit` to the end of the leaf+choice var range. For
595    // deterministic programs with no probabilistic vars, this range is empty (limit=0). In that
596    // case, the verifier must still be able to branch, so fall back to `cnf.num_vars` (all CNF
597    // vars are semantically meaningful when there is no probabilistic decision set).
598    let verifier_decision_var_limit = if random_vars.is_empty() {
599        &cnf.num_vars
600    } else {
601        decision_var_limit
602    };
603    let cdcl = cdcl_config_from_compile(config)?;
604    #[cfg(debug_assertions)]
605    eprintln!("[xlog-prob] compile_gpu_d4_and_verify_cached: validate_equivalence_gpu_gated");
606    let t_verify = if profiling {
607        Some(Instant::now())
608    } else {
609        None
610    };
611    let certified_circuit = certify_final_circuit(
612        cnf,
613        verifier_decision_var_limit,
614        circuit_eval,
615        provider,
616        GpuEquivalenceConfig {
617            cdcl,
618            reuse_workspace: config.incremental_verify,
619        },
620        handle.compile_needed_device(),
621    )?;
622    if let Some(t0) = t_verify {
623        provider
624            .device()
625            .synchronize()
626            .map_err(|e| XlogError::Kernel(format!("sync after verify: {}", e)))?;
627        profile.verify_sec = t0.elapsed().as_secs_f64();
628    }
629    #[cfg(debug_assertions)]
630    {
631        if !profiling {
632            provider.device().synchronize().map_err(|e| {
633                XlogError::Kernel(format!(
634                    "sync after validate_equivalence_gpu_gated failed: {}",
635                    e
636                ))
637            })?;
638        }
639    }
640
641    // --- Cache store stage ---
642    #[cfg(debug_assertions)]
643    eprintln!("[xlog-prob] compile_gpu_d4_and_verify_cached: store_from_xgcf");
644    let t_store = if profiling {
645        Some(Instant::now())
646    } else {
647        None
648    };
649    cache.store_from_xgcf(&mut handle, certified_circuit.as_xgcf())?;
650    if let Some(t0) = t_store {
651        provider
652            .device()
653            .synchronize()
654            .map_err(|e| XlogError::Kernel(format!("sync after cache store: {}", e)))?;
655        profile.cache_store_sec = t0.elapsed().as_secs_f64();
656    }
657    #[cfg(debug_assertions)]
658    {
659        if !profiling {
660            provider.device().synchronize().map_err(|e| {
661                XlogError::Kernel(format!("sync after store_from_xgcf failed: {}", e))
662            })?;
663        }
664    }
665
666    // --- Free-var mask stage ---
667    #[cfg(debug_assertions)]
668    eprintln!("[xlog-prob] compile_gpu_d4_and_verify_cached: compute_free_var_mask_gpu_gated");
669    let t_fvm = if profiling {
670        Some(Instant::now())
671    } else {
672        None
673    };
674    let free_var_mask = gpu_d4::compute_free_var_mask_gpu_gated(
675        cnf,
676        certified_circuit.as_xgcf(),
677        provider,
678        handle.compile_needed_device(),
679    )?;
680    #[cfg(debug_assertions)]
681    {
682        if !profiling {
683            provider.device().synchronize().map_err(|e| {
684                XlogError::Kernel(format!(
685                    "sync after compute_free_var_mask_gpu_gated failed: {}",
686                    e
687                ))
688            })?;
689        }
690    }
691    // Only enable free-var correction if there are actual free variables.
692    // When the mask is all-zero (common for smoothed d-DNNF circuits),
693    // skipping this keeps has_free_var_mask[slot]=false, which avoids unnecessary
694    // free-var correction kernel launches on every subsequent eval.
695    let mask_host: Vec<u8> = provider
696        .device()
697        .inner()
698        .dtoh_sync_copy(&free_var_mask)
699        .map_err(|e| XlogError::Kernel(format!("Failed to read free_var_mask: {}", e)))?;
700    let has_free_vars = mask_host.iter().any(|&b| b != 0);
701    #[cfg(debug_assertions)]
702    eprintln!(
703        "[xlog-prob] free_var_mask: {} free vars, batched eval {}",
704        if has_free_vars { "has" } else { "no" },
705        if has_free_vars { "DISABLED" } else { "ENABLED" },
706    );
707    if has_free_vars {
708        cache.store_free_var_mask(&handle, &free_var_mask)?;
709        #[cfg(debug_assertions)]
710        {
711            if !profiling {
712                provider.device().synchronize().map_err(|e| {
713                    XlogError::Kernel(format!("sync after store_free_var_mask failed: {}", e))
714                })?;
715            }
716        }
717    }
718    if let Some(t0) = t_fvm {
719        provider
720            .device()
721            .synchronize()
722            .map_err(|e| XlogError::Kernel(format!("sync after free_var_mask: {}", e)))?;
723        profile.free_var_mask_sec = t0.elapsed().as_secs_f64();
724    }
725
726    // --- Disk cache write (opportunistic) ---
727    //
728    // After a successful compilation, write the artifact to disk for next warm start.
729    // Errors are silently ignored — the disk cache is best-effort.
730    if let Some(ref disk_key) = cache_key {
731        if let Ok(artifact) = cache.build_artifact_from_device(&handle, provider) {
732            let _ = disk_cache::write_artifact(disk_key, &artifact);
733            #[cfg(debug_assertions)]
734            eprintln!("[xlog-prob] compile_gpu_d4_and_verify_cached: wrote disk cache artifact");
735        }
736    }
737
738    ledger.record_materialization()?;
739    let out_profile = if profiling { Some(profile) } else { None };
740    Ok((handle, out_profile))
741}
742
743fn d4_config_for_smoothing(
744    config: &GpuCompileConfig,
745    random_var_count: u32,
746) -> Result<GpuCompileConfig> {
747    if random_var_count == 0 {
748        return Ok(*config);
749    }
750    let headroom = 2u32
751        .checked_add(random_var_count)
752        .ok_or_else(|| XlogError::Compilation("smooth headroom overflow".to_string()))?;
753    if config.smooth_node_cap <= headroom {
754        return Err(XlogError::Compilation(format!(
755            "GpuCompileConfig smooth_node_cap {} too small for smoothing headroom {}",
756            config.smooth_node_cap, headroom
757        )));
758    }
759    let base_cap = config
760        .smooth_node_cap
761        .checked_sub(headroom)
762        .ok_or_else(|| XlogError::Compilation("smooth node cap underflow".to_string()))?;
763    if base_cap < 3 {
764        return Err(XlogError::Compilation(
765            "GpuCompileConfig smooth_node_cap leaves <3 base nodes".to_string(),
766        ));
767    }
768    let mut out = *config;
769    out.smooth_node_cap = base_cap;
770    Ok(out)
771}
772
773fn cdcl_config_from_compile(config: &GpuCompileConfig) -> Result<GpuCdclConfig> {
774    if config.cdcl_restart_interval == 0 {
775        return Err(XlogError::Compilation(
776            "cdcl_restart_interval must be > 0".to_string(),
777        ));
778    }
779    if config.cdcl_learned_bytes == 0 {
780        return Err(XlogError::Compilation(
781            "cdcl_learned_bytes must be > 0".to_string(),
782        ));
783    }
784
785    // Deterministic sizing: assume average learned clause length = 4.
786    const AVG_LEN: u64 = 4;
787    const META_BYTES_PER_CLAUSE: u64 = 24; // offsets + lbd + activity + flags + proof offsets (rounded up)
788    const PROOF_BYTES_PER_CLAUSE: u64 = 8 + (8 * AVG_LEN); // (conflict, steps) + 2*u32 per lit
789    const LIT_BYTES_PER_CLAUSE: u64 = 4 * AVG_LEN;
790
791    let bytes_per_clause = META_BYTES_PER_CLAUSE
792        .checked_add(PROOF_BYTES_PER_CLAUSE)
793        .and_then(|v| v.checked_add(LIT_BYTES_PER_CLAUSE))
794        .ok_or_else(|| XlogError::Compilation("cdcl bytes per clause overflow".to_string()))?;
795
796    let max_clauses = config
797        .cdcl_learned_bytes
798        .checked_div(bytes_per_clause)
799        .ok_or_else(|| XlogError::Compilation("cdcl_learned_bytes div overflow".to_string()))?;
800    if max_clauses == 0 {
801        return Err(XlogError::Compilation(
802            "cdcl_learned_bytes too small for learned clause arena".to_string(),
803        ));
804    }
805
806    let max_lits = max_clauses
807        .checked_mul(AVG_LEN)
808        .ok_or_else(|| XlogError::Compilation("max_learned_lits overflow".to_string()))?;
809    let max_proof_u32 = max_clauses
810        .checked_mul(2 + 2 * AVG_LEN)
811        .ok_or_else(|| XlogError::Compilation("max_proof_u32 overflow".to_string()))?;
812
813    let max_learned_clauses = u32::try_from(max_clauses)
814        .map_err(|_| XlogError::Compilation("max_learned_clauses exceeds u32::MAX".to_string()))?;
815    let max_learned_lits = u32::try_from(max_lits)
816        .map_err(|_| XlogError::Compilation("max_learned_lits exceeds u32::MAX".to_string()))?;
817    let max_proof_u32 = u32::try_from(max_proof_u32)
818        .map_err(|_| XlogError::Compilation("max_proof_u32 exceeds u32::MAX".to_string()))?;
819
820    let reduce_interval = config
821        .cdcl_restart_interval
822        .checked_mul(20)
823        .ok_or_else(|| XlogError::Compilation("cdcl reduce_interval overflow".to_string()))?;
824
825    let mut gpu_cdcl = GpuCdclConfig::default();
826    gpu_cdcl.max_learned_clauses = max_learned_clauses;
827    gpu_cdcl.max_learned_lits = max_learned_lits;
828    gpu_cdcl.max_proof_u32 = max_proof_u32;
829    gpu_cdcl.restart_base = config.cdcl_restart_interval;
830    gpu_cdcl.reduce_interval = reduce_interval;
831    // Fail-closed conflict budget. Treewidth-hard equivalence verifies can
832    // spin past the GPU watchdog and crash with a context-poisoning launch
833    // failure; a finite budget makes them decline (VerifyBudgetExceeded) before
834    // that. Default 0 = unlimited (no behavior change). A recommended value is a
835    // calibration follow-up (between a completing verify's conflict count and
836    // the watchdog boundary); operators opt in via XLOG_D4_VERIFY_MAX_CONFLICTS.
837    gpu_cdcl.max_conflicts = verify_max_conflicts();
838    Ok(gpu_cdcl)
839}
840
841/// Per-verify conflict budget, read once from
842/// `XLOG_D4_VERIFY_MAX_CONFLICTS`. Default 0 = unlimited.
843fn verify_max_conflicts() -> u32 {
844    static BUDGET: std::sync::OnceLock<u32> = std::sync::OnceLock::new();
845    *BUDGET.get_or_init(|| {
846        std::env::var("XLOG_D4_VERIFY_MAX_CONFLICTS")
847            .ok()
848            .and_then(|v| v.trim().parse::<u32>().ok())
849            .unwrap_or(0)
850    })
851}
852
853// ---------------------------------------------------------------------------
854// Disk cache helpers
855// ---------------------------------------------------------------------------
856
857/// FNV-1a 64-bit hash — deterministic across processes and Rust versions.
858/// Matches the FNV-1a algorithm used in the GPU hash kernel (kernels/cache.cu).
859fn fnv1a_u64(bytes: &[u8]) -> u64 {
860    const FNV_OFFSET: u64 = 0xcbf29ce484222325;
861    const FNV_PRIME: u64 = 0x100000001b3;
862    let mut h = FNV_OFFSET;
863    for &b in bytes {
864        h ^= b as u64;
865        h = h.wrapping_mul(FNV_PRIME);
866    }
867    h
868}
869
870fn combine_disk_cnf_hash(canonical_hash: u64, gpu_cnf_hash: u64) -> u64 {
871    let mut buf = Vec::with_capacity(16);
872    buf.extend_from_slice(&canonical_hash.to_le_bytes());
873    buf.extend_from_slice(&gpu_cnf_hash.to_le_bytes());
874    fnv1a_u64(&buf)
875}
876
877fn read_gpu_cnf_hash(
878    hash: &TrackedCudaSlice<u64>,
879    provider: &Arc<CudaKernelProvider>,
880) -> Result<u64> {
881    let host: Vec<u64> = provider
882        .device()
883        .inner()
884        .dtoh_sync_copy(hash)
885        .map_err(|e| XlogError::Kernel(format!("dtoh GPU CNF hash for disk cache key: {}", e)))?;
886    host.first()
887        .copied()
888        .ok_or_else(|| XlogError::Kernel("empty GPU CNF hash for disk cache key".to_string()))
889}
890
891/// Hash the compile config fields that affect circuit topology output.
892fn hash_compile_config(config: &GpuCompileConfig) -> u64 {
893    let mut buf = Vec::new();
894    buf.extend_from_slice(&config.frontier_depth.to_le_bytes());
895    buf.extend_from_slice(&config.max_frontier_items.to_le_bytes());
896    buf.extend_from_slice(&config.max_depth.to_le_bytes());
897    buf.extend_from_slice(&config.smooth_node_cap.to_le_bytes());
898    buf.extend_from_slice(&config.smooth_edge_cap.to_le_bytes());
899    // CDCL verifier params do not affect the compiled circuit topology,
900    // but we include them for safety so a verifier config change invalidates the cache.
901    buf.extend_from_slice(&config.cdcl_restart_interval.to_le_bytes());
902    buf.extend_from_slice(&config.cdcl_learned_bytes.to_le_bytes());
903    fnv1a_u64(&buf)
904}
905
906/// Hash the random variable list (D→H copy + hash).
907fn hash_random_vars(
908    random_vars: &DeviceRandomVarList,
909    provider: &Arc<CudaKernelProvider>,
910) -> Result<u64> {
911    let count = random_vars.count();
912    let mut buf = Vec::new();
913    buf.extend_from_slice(&count.to_le_bytes());
914    if count > 0 {
915        let host: Vec<u32> = provider
916            .device()
917            .inner()
918            .dtoh_sync_copy(random_vars.list())
919            .map_err(|e| {
920                XlogError::Kernel(format!("dtoh random_vars for disk cache hash: {}", e))
921            })?;
922        // Hash only the valid elements (count may be less than the allocation).
923        for &v in &host[..count as usize] {
924            buf.extend_from_slice(&v.to_le_bytes());
925        }
926    }
927    Ok(fnv1a_u64(&buf))
928}
929
930/// Query the device compute capability and encode as `major * 10 + minor` (e.g. 89 for sm_89).
931fn detect_compute_capability(provider: &Arc<CudaKernelProvider>) -> Result<u32> {
932    use cudarc::driver::sys::CUdevice_attribute;
933
934    let device = provider.device().inner();
935    let major = device
936        .attribute(CUdevice_attribute::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MAJOR)
937        .map_err(|e| {
938            XlogError::Kernel(format!("Failed to query compute capability major: {}", e))
939        })?;
940    let minor = device
941        .attribute(CUdevice_attribute::CU_DEVICE_ATTRIBUTE_COMPUTE_CAPABILITY_MINOR)
942        .map_err(|e| {
943            XlogError::Kernel(format!("Failed to query compute capability minor: {}", e))
944        })?;
945    let major_u32: u32 = major.try_into().map_err(|_| {
946        XlogError::Kernel(format!(
947            "compute capability major {} cannot be converted to u32",
948            major
949        ))
950    })?;
951    let minor_u32: u32 = minor.try_into().map_err(|_| {
952        XlogError::Kernel(format!(
953            "compute capability minor {} cannot be converted to u32",
954            minor
955        ))
956    })?;
957    Ok(major_u32 * 10 + minor_u32)
958}