Skip to main content

xlog_solve/
gpu_cdcl.rs

1use std::ffi::c_void;
2use std::sync::Arc;
3
4use cudarc::driver::LaunchConfig;
5#[cfg(debug_assertions)]
6use xlog_core::resolve_bool;
7use xlog_core::{Result, XlogError};
8use xlog_cuda::memory::TrackedCudaSlice;
9use xlog_cuda::provider::{sat_kernels, SAT_MODULE};
10use xlog_cuda::{AsKernelParam, CudaKernelProvider, LaunchAsync};
11
12use crate::gpu_cnf::GpuCnf;
13
14// Must match kernels/sat.cu.
15const SAT_STATUS_UNSAT: i32 = 0;
16const SAT_STATUS_SAT: i32 = 1;
17/// Conflict budget reached before the search terminated (mirrors the kernel's
18/// `SAT_STATUS_BUDGET_EXHAUSTED`). INDETERMINATE — the verifier must treat this
19/// as a fail-closed decline, never as a proof.
20const SAT_STATUS_BUDGET_EXHAUSTED: i32 = 2;
21
22struct GpuCdclRun {
23    assignment: TrackedCudaSlice<i8>,
24    // Every buffer passed to the asynchronous sat_cdcl_solve launch must remain
25    // owned until the first status read or explicit synchronization completes.
26    _level: TrackedCudaSlice<u32>,
27    _reason: TrackedCudaSlice<i32>,
28    _var_activity: TrackedCudaSlice<u32>,
29    _var_phase: TrackedCudaSlice<i8>,
30    _decision_heap: TrackedCudaSlice<u32>,
31    _decision_heap_pos: TrackedCudaSlice<u32>,
32    _trail: TrackedCudaSlice<i32>,
33    _trail_lim: TrackedCudaSlice<u32>,
34    _seen: TrackedCudaSlice<u8>,
35    _learnt_tmp: TrackedCudaSlice<i32>,
36    _proof_vars_tmp: TrackedCudaSlice<u32>,
37    _proof_reason_tmp: TrackedCudaSlice<u32>,
38    _watch0_pos: TrackedCudaSlice<u32>,
39    _watch1_pos: TrackedCudaSlice<u32>,
40    _watch_head: TrackedCudaSlice<i32>,
41    _watch_next: TrackedCudaSlice<i32>,
42    _watch_prev: TrackedCudaSlice<i32>,
43
44    learned_offsets: TrackedCudaSlice<u32>,
45    learned_lits: TrackedCudaSlice<i32>,
46    _learned_deleted: TrackedCudaSlice<u8>,
47    _learned_lbd: TrackedCudaSlice<u32>,
48    _learned_activity: TrackedCudaSlice<u32>,
49    _learned_locked: TrackedCudaSlice<u8>,
50    proof_offsets: TrackedCudaSlice<u32>,
51    proof_data: TrackedCudaSlice<u32>,
52
53    out_status: TrackedCudaSlice<i32>,
54    out_error: TrackedCudaSlice<i32>,
55    out_learned_count: TrackedCudaSlice<u32>,
56}
57
58/// Configuration for the GPU CDCL (Conflict-Driven Clause Learning) solver.
59///
60/// Controls arena sizes for learned clauses, proof traces, and the
61/// deterministic restart/reduce schedule. The defaults are tuned for
62/// equivalence verification of circuits with up to ~10 000 variables;
63/// larger problems may need proportionally larger arenas.
64#[derive(Debug, Clone, Copy)]
65#[non_exhaustive]
66pub struct GpuCdclConfig {
67    /// Maximum number of learned clauses retained in the arena.
68    pub max_learned_clauses: u32,
69    /// Maximum total literals across all learned clauses.
70    pub max_learned_lits: u32,
71    /// Maximum size (in u32 words) of the resolution proof trace.
72    pub max_proof_u32: u32,
73    /// Base conflict count between deterministic restarts.
74    pub restart_base: u32,
75    /// Conflict count between clause-database reductions.
76    pub reduce_interval: u32,
77    /// Conflict budget: `0` = unlimited (default; no behavior change). When
78    /// nonzero, the solver bails with status `BUDGET_EXHAUSTED` after this many
79    /// conflicts without terminating, bounding wall-clock so a treewidth-hard
80    /// instance returns gracefully instead of running to a CUDA launch timeout
81    /// that poisons the primary context.
82    pub max_conflicts: u32,
83}
84
85impl Default for GpuCdclConfig {
86    fn default() -> Self {
87        Self {
88            max_learned_clauses: 32_768,
89            max_learned_lits: 262_144,
90            max_proof_u32: 1_048_576,
91            restart_base: 100,
92            reduce_interval: 2000,
93            max_conflicts: 0,
94        }
95    }
96}
97
98/// GPU-native CDCL SAT solver backed by CUDA kernels.
99pub struct GpuCdclSolver {
100    provider: Arc<CudaKernelProvider>,
101    config: GpuCdclConfig,
102}
103
104/// Pre-allocated solver arena for reuse across multiple CDCL solves.
105///
106/// Owns the 30 device buffers that `launch_cdcl_with_decision_ranges_gated` normally
107/// allocates per call. Does NOT own CNF storage (clause_offsets/literals stay on GpuCnf).
108///
109/// Created via [`GpuCdclSolver::new_workspace`]. Passed as `&mut` to `_ws` solver methods.
110///
111/// `reset_for_solve()` is intentionally a no-op: the `sat_cdcl_solve` kernel initializes
112/// mutable state at launch. The learned-import path preserves the learned/proof arenas
113/// below `out_learned_count[0]` and initializes the remaining workspace buffers.
114pub struct GpuCdclWorkspace {
115    // Capacity limits (used for overflow checks)
116    pub(crate) var_cap: usize,
117    pub(crate) clause_total_cap: usize,
118
119    // Variable state (var_cap + 1 each)
120    pub(crate) assign: TrackedCudaSlice<i8>,
121    pub(crate) level: TrackedCudaSlice<u32>,
122    pub(crate) reason: TrackedCudaSlice<i32>,
123    pub(crate) var_activity: TrackedCudaSlice<u32>,
124    pub(crate) var_phase: TrackedCudaSlice<i8>,
125    pub(crate) decision_heap: TrackedCudaSlice<u32>,
126    pub(crate) decision_heap_pos: TrackedCudaSlice<u32>,
127
128    // Trail (var_cap + 1 each)
129    pub(crate) trail: TrackedCudaSlice<i32>,
130    pub(crate) trail_lim: TrackedCudaSlice<u32>,
131
132    // Analysis scratch (var_cap + 1 each)
133    pub(crate) seen: TrackedCudaSlice<u8>,
134    pub(crate) learnt_tmp: TrackedCudaSlice<i32>,
135    pub(crate) proof_vars_tmp: TrackedCudaSlice<u32>,
136    pub(crate) proof_reason_tmp: TrackedCudaSlice<u32>,
137
138    // Watch lists
139    pub(crate) watch0_pos: TrackedCudaSlice<u32>, // clause_total_cap
140    pub(crate) watch1_pos: TrackedCudaSlice<u32>, // clause_total_cap
141    pub(crate) watch_head: TrackedCudaSlice<i32>, // 2 * var_cap
142    pub(crate) watch_next: TrackedCudaSlice<i32>, // 2 * clause_total_cap
143    pub(crate) watch_prev: TrackedCudaSlice<i32>, // 2 * clause_total_cap
144
145    // Learned clause arena
146    pub(crate) learned_offsets: TrackedCudaSlice<u32>, // max_learned_clauses + 1
147    pub(crate) learned_lits: TrackedCudaSlice<i32>,    // max_learned_lits
148    pub(crate) learned_deleted: TrackedCudaSlice<u8>,  // max_learned_clauses
149    pub(crate) learned_lbd: TrackedCudaSlice<u32>,     // max_learned_clauses
150    pub(crate) learned_activity: TrackedCudaSlice<u32>, // max_learned_clauses
151    pub(crate) learned_locked: TrackedCudaSlice<u8>,   // max_learned_clauses
152
153    // Proof trace
154    pub(crate) proof_offsets: TrackedCudaSlice<u32>, // max_learned_clauses + 1
155    pub(crate) proof_data: TrackedCudaSlice<u32>,    // max_proof_u32
156
157    // Scalar outputs
158    pub(crate) out_status: TrackedCudaSlice<i32>, // 1
159    pub(crate) out_error: TrackedCudaSlice<i32>,  // 1
160    pub(crate) out_learned_count: TrackedCudaSlice<u32>, // 1
161}
162
163impl GpuCdclWorkspace {
164    /// No-op: the sat_cdcl_solve kernel initializes mutable state at launch.
165    #[inline]
166    pub(crate) fn reset_for_solve(&mut self) {
167        // Intentionally empty; launch flags decide whether learned/proof arenas are imported.
168    }
169
170    /// Device pointer of the assignment buffer (for diagnostics / reuse verification).
171    #[inline]
172    pub fn assign_device_ptr(&self) -> cudarc::driver::sys::CUdeviceptr {
173        self.assign.device_ptr_value()
174    }
175}
176
177/// Raw CDCL outputs (device-resident) for debugging and research.
178///
179/// Production verifier paths should prefer `solve_expect_sat*` / `solve_expect_unsat*`,
180/// which validate results on GPU and return typed errors for status mismatches.
181pub struct GpuCdclRawOutput {
182    /// Device-resident variable assignment indexed by one-based SAT variable identifier.
183    pub assignment: TrackedCudaSlice<i8>,
184    /// Device-resident terminal solver status.
185    pub out_status: TrackedCudaSlice<i32>,
186    /// Device-resident kernel error code; zero indicates no kernel error.
187    pub out_error: TrackedCudaSlice<i32>,
188    /// Device-resident count of learned clauses retained by the solve.
189    pub out_learned_count: TrackedCudaSlice<u32>,
190}
191
192fn checked_solver_len_add_one(context: &str, value: usize) -> Result<usize> {
193    value
194        .checked_add(1)
195        .ok_or_else(|| XlogError::Kernel(format!("{context} length overflow")))
196}
197
198fn checked_solver_len_double(context: &str, value: usize) -> Result<usize> {
199    value
200        .checked_mul(2)
201        .ok_or_else(|| XlogError::Kernel(format!("{context} length overflow")))
202}
203
204impl GpuCdclSolver {
205    fn require_expected_status(
206        &self,
207        out_status: &TrackedCudaSlice<i32>,
208        out_error: &TrackedCudaSlice<i32>,
209        expected_status: i32,
210        context: &'static str,
211    ) -> Result<()> {
212        let actual_status = self
213            .provider
214            .dtoh_scalar_untracked(out_status, 0)
215            .map_err(|e| XlogError::Kernel(format!("Failed to read {context} status: {e}")))?;
216        let actual_error = self
217            .provider
218            .dtoh_scalar_untracked(out_error, 0)
219            .map_err(|e| XlogError::Kernel(format!("Failed to read {context} error: {e}")))?;
220        // A conflict-budget bail is not a solver error and not a proof: it is a
221        // fail-closed decline (the search was cut short on purpose to avoid a
222        // context-poisoning launch timeout). Surface it as the typed,
223        // catchable VerifyBudgetExceeded rather than a generic kernel error.
224        if actual_error == 0 && actual_status == SAT_STATUS_BUDGET_EXHAUSTED {
225            return Err(XlogError::VerifyBudgetExceeded {
226                context: context.to_string(),
227                detail: "CDCL conflict budget exhausted before the search \
228                         terminated (indeterminate; verify declined fail-closed)"
229                    .to_string(),
230            });
231        }
232        if actual_error != 0 || actual_status != expected_status {
233            return Err(XlogError::Kernel(format!(
234                "{context} expected status {expected_status}, got status {actual_status} error {actual_error}"
235            )));
236        }
237        Ok(())
238    }
239
240    fn provider_memory_ptr(&self) -> usize {
241        Arc::as_ptr(self.provider.memory()) as usize
242    }
243
244    fn require_slice_on_provider<T: cudarc::driver::DeviceRepr>(
245        &self,
246        name: &'static str,
247        slice: &TrackedCudaSlice<T>,
248    ) -> Result<()> {
249        let expected_memory = self.provider_memory_ptr();
250        let actual_memory = slice.memory_manager_ptr_value();
251        if actual_memory != expected_memory {
252            return Err(XlogError::UnsupportedEpistemicConstruct {
253                construct: "GPU CDCL solver provider boundary".to_string(),
254                context: format!(
255                    "{name} belongs to memory manager {actual_memory}, expected {expected_memory}"
256                ),
257            });
258        }
259        Ok(())
260    }
261
262    pub(crate) fn require_workspace_on_provider(&self, ws: &GpuCdclWorkspace) -> Result<()> {
263        macro_rules! require_workspace_slice {
264            ($field:ident) => {
265                self.require_slice_on_provider(
266                    concat!("workspace.", stringify!($field)),
267                    &ws.$field,
268                )?
269            };
270        }
271
272        require_workspace_slice!(assign);
273        require_workspace_slice!(level);
274        require_workspace_slice!(reason);
275        require_workspace_slice!(var_activity);
276        require_workspace_slice!(var_phase);
277        require_workspace_slice!(decision_heap);
278        require_workspace_slice!(decision_heap_pos);
279        require_workspace_slice!(trail);
280        require_workspace_slice!(trail_lim);
281        require_workspace_slice!(seen);
282        require_workspace_slice!(learnt_tmp);
283        require_workspace_slice!(proof_vars_tmp);
284        require_workspace_slice!(proof_reason_tmp);
285        require_workspace_slice!(watch0_pos);
286        require_workspace_slice!(watch1_pos);
287        require_workspace_slice!(watch_head);
288        require_workspace_slice!(watch_next);
289        require_workspace_slice!(watch_prev);
290        require_workspace_slice!(learned_offsets);
291        require_workspace_slice!(learned_lits);
292        require_workspace_slice!(learned_deleted);
293        require_workspace_slice!(learned_lbd);
294        require_workspace_slice!(learned_activity);
295        require_workspace_slice!(learned_locked);
296        require_workspace_slice!(proof_offsets);
297        require_workspace_slice!(proof_data);
298        require_workspace_slice!(out_status);
299        require_workspace_slice!(out_error);
300        require_workspace_slice!(out_learned_count);
301        Ok(())
302    }
303
304    pub(crate) fn require_workspace_capacity_for_cnf(
305        &self,
306        ws: &GpuCdclWorkspace,
307        var_cap: u32,
308        clause_cap: u32,
309    ) -> Result<()> {
310        let num_vars_cap = var_cap as usize;
311        if num_vars_cap > ws.var_cap {
312            return Err(XlogError::Kernel(format!(
313                "CNF var_cap {} exceeds workspace var_cap {}",
314                num_vars_cap, ws.var_cap
315            )));
316        }
317
318        let max_learned_clauses = self.config.max_learned_clauses as usize;
319        let max_total_clauses = (clause_cap as usize)
320            .checked_add(max_learned_clauses)
321            .ok_or_else(|| XlogError::Kernel("SAT clause capacity overflow".to_string()))?;
322        if max_total_clauses > ws.clause_total_cap {
323            return Err(XlogError::Kernel(format!(
324                "CNF clause_total {} exceeds workspace clause_total_cap {}",
325                max_total_clauses, ws.clause_total_cap
326            )));
327        }
328
329        Ok(())
330    }
331
332    /// Create a solver that allocates and launches through `provider` using `config`.
333    pub fn new(provider: Arc<CudaKernelProvider>, config: GpuCdclConfig) -> Self {
334        Self { provider, config }
335    }
336
337    /// Pre-allocate a reusable solver arena.
338    ///
339    /// `max_var_cap` and `max_clause_cap` must be >= the `var_cap` / `clause_cap` of any
340    /// `GpuCnf` that will be solved with this workspace. If a solve call exceeds these
341    /// capacities, it returns `XlogError::Kernel`.
342    pub fn new_workspace(&self, max_var_cap: u32, max_clause_cap: u32) -> Result<GpuCdclWorkspace> {
343        let num_vars_cap = max_var_cap as usize;
344        let num_clauses_cap = max_clause_cap as usize;
345        let max_learned_clauses = self.config.max_learned_clauses as usize;
346        let max_learned_lits = self.config.max_learned_lits as usize;
347        let max_proof_u32 = self.config.max_proof_u32 as usize;
348
349        if max_var_cap == 0 {
350            return Err(XlogError::Compilation(
351                "GpuCdclSolver workspace requires max_var_cap > 0".to_string(),
352            ));
353        }
354        if self.config.max_learned_clauses == 0 {
355            return Err(XlogError::Compilation(
356                "GpuCdclSolver requires max_learned_clauses > 0".to_string(),
357            ));
358        }
359        if self.config.max_learned_lits == 0 {
360            return Err(XlogError::Compilation(
361                "GpuCdclSolver requires max_learned_lits > 0".to_string(),
362            ));
363        }
364        if self.config.max_proof_u32 < 2 {
365            return Err(XlogError::Compilation(
366                "GpuCdclSolver requires max_proof_u32 >= 2".to_string(),
367            ));
368        }
369
370        let max_total_clauses = num_clauses_cap
371            .checked_add(max_learned_clauses)
372            .ok_or_else(|| XlogError::Kernel("SAT clause capacity overflow".to_string()))?;
373        let vars_plus_one =
374            checked_solver_len_add_one("SAT workspace variable arena", num_vars_cap)?;
375        let learned_offsets_len =
376            checked_solver_len_add_one("SAT workspace learned offsets", max_learned_clauses)?;
377        let watch_head_len = checked_solver_len_double("SAT workspace watch head", num_vars_cap)?;
378        let watch_clause_len =
379            checked_solver_len_double("SAT workspace watch clauses", max_total_clauses)?;
380
381        let memory = self.provider.memory();
382
383        Ok(GpuCdclWorkspace {
384            var_cap: num_vars_cap,
385            clause_total_cap: max_total_clauses,
386
387            // Variable state
388            assign: memory.alloc::<i8>(vars_plus_one)?,
389            level: memory.alloc::<u32>(vars_plus_one)?,
390            reason: memory.alloc::<i32>(vars_plus_one)?,
391            var_activity: memory.alloc::<u32>(vars_plus_one)?,
392            var_phase: memory.alloc::<i8>(vars_plus_one)?,
393            decision_heap: memory.alloc::<u32>(vars_plus_one)?,
394            decision_heap_pos: memory.alloc::<u32>(vars_plus_one)?,
395
396            // Trail
397            trail: memory.alloc::<i32>(vars_plus_one)?,
398            trail_lim: memory.alloc::<u32>(vars_plus_one)?,
399
400            // Analysis scratch
401            seen: memory.alloc::<u8>(vars_plus_one)?,
402            learnt_tmp: memory.alloc::<i32>(vars_plus_one)?,
403            proof_vars_tmp: memory.alloc::<u32>(vars_plus_one)?,
404            proof_reason_tmp: memory.alloc::<u32>(vars_plus_one)?,
405
406            // Watch lists
407            watch0_pos: memory.alloc::<u32>(max_total_clauses)?,
408            watch1_pos: memory.alloc::<u32>(max_total_clauses)?,
409            watch_head: memory.alloc::<i32>(watch_head_len)?,
410            watch_next: memory.alloc::<i32>(watch_clause_len)?,
411            watch_prev: memory.alloc::<i32>(watch_clause_len)?,
412
413            // Learned
414            learned_offsets: memory.alloc::<u32>(learned_offsets_len)?,
415            learned_lits: memory.alloc::<i32>(max_learned_lits)?,
416            learned_deleted: memory.alloc::<u8>(max_learned_clauses)?,
417            learned_lbd: memory.alloc::<u32>(max_learned_clauses)?,
418            learned_activity: memory.alloc::<u32>(max_learned_clauses)?,
419            learned_locked: memory.alloc::<u8>(max_learned_clauses)?,
420
421            // Proof
422            proof_offsets: memory.alloc::<u32>(learned_offsets_len)?,
423            proof_data: memory.alloc::<u32>(max_proof_u32)?,
424
425            // Outputs
426            out_status: memory.alloc::<i32>(1)?,
427            out_error: memory.alloc::<i32>(1)?,
428            out_learned_count: memory.alloc::<u32>(1)?,
429        })
430    }
431
432    fn alloc_u32_scalar(&self, value: u32) -> Result<TrackedCudaSlice<u32>> {
433        let memory = self.provider.memory();
434        let mut gate = memory.alloc::<u32>(1)?;
435        self.provider
436            .htod_launch_metadata_sync_copy_into(&[value], &mut gate)
437            .map_err(|e| XlogError::Kernel(format!("GpuCdclSolver gate upload failed: {}", e)))?;
438        Ok(gate)
439    }
440
441    fn launch_cdcl_with_decision_ranges_gated(
442        &self,
443        cnf: &GpuCnf,
444        compile_needed: &TrackedCudaSlice<u32>,
445        decision_base_limit: &TrackedCudaSlice<u32>,
446        decision_extra_base: &TrackedCudaSlice<u32>,
447        decision_extra_count: &TrackedCudaSlice<u32>,
448    ) -> Result<GpuCdclRun> {
449        let num_vars_cap = cnf.var_cap as usize;
450        let num_clauses_cap = cnf.clause_cap as usize;
451
452        cnf.require_provider_memory(&self.provider, "GPU CDCL solver provider boundary")?;
453        self.require_slice_on_provider("compile_needed", compile_needed)?;
454        self.require_slice_on_provider("decision_base_limit", decision_base_limit)?;
455        self.require_slice_on_provider("decision_extra_base", decision_extra_base)?;
456        self.require_slice_on_provider("decision_extra_count", decision_extra_count)?;
457        if compile_needed.len() != 1 {
458            return Err(XlogError::Compilation(format!(
459                "GpuCdclSolver requires compile_needed len=1, got {}",
460                compile_needed.len()
461            )));
462        }
463
464        if cnf.var_cap == 0 {
465            return Err(XlogError::Compilation(
466                "GpuCdclSolver requires num_vars > 0".to_string(),
467            ));
468        }
469        if decision_base_limit.len() != 1 {
470            return Err(XlogError::Compilation(format!(
471                "GpuCdclSolver requires decision_base_limit len=1, got {}",
472                decision_base_limit.len()
473            )));
474        }
475        if decision_extra_base.len() != 1 {
476            return Err(XlogError::Compilation(format!(
477                "GpuCdclSolver requires decision_extra_base len=1, got {}",
478                decision_extra_base.len()
479            )));
480        }
481        if decision_extra_count.len() != 1 {
482            return Err(XlogError::Compilation(format!(
483                "GpuCdclSolver requires decision_extra_count len=1, got {}",
484                decision_extra_count.len()
485            )));
486        }
487        if self.config.max_learned_clauses == 0 {
488            return Err(XlogError::Compilation(
489                "GpuCdclSolver requires max_learned_clauses > 0".to_string(),
490            ));
491        }
492        if self.config.max_learned_lits == 0 {
493            return Err(XlogError::Compilation(
494                "GpuCdclSolver requires max_learned_lits > 0".to_string(),
495            ));
496        }
497        if self.config.max_proof_u32 < 2 {
498            return Err(XlogError::Compilation(
499                "GpuCdclSolver requires max_proof_u32 >= 2".to_string(),
500            ));
501        }
502
503        let max_learned_clauses = self.config.max_learned_clauses as usize;
504        let max_learned_lits = self.config.max_learned_lits as usize;
505        let max_proof_u32 = self.config.max_proof_u32 as usize;
506
507        let max_total_clauses = num_clauses_cap
508            .checked_add(max_learned_clauses)
509            .ok_or_else(|| XlogError::Kernel("SAT clause capacity overflow".to_string()))?;
510        let vars_plus_one = checked_solver_len_add_one("SAT variable arena", num_vars_cap)?;
511        let learned_offsets_len =
512            checked_solver_len_add_one("SAT learned offsets", max_learned_clauses)?;
513        let watch_head_len = checked_solver_len_double("SAT watch head", num_vars_cap)?;
514        let watch_clause_len = checked_solver_len_double("SAT watch clauses", max_total_clauses)?;
515
516        let memory = self.provider.memory();
517
518        // Variable state
519        let assign = memory.alloc::<i8>(vars_plus_one)?;
520        let level = memory.alloc::<u32>(vars_plus_one)?;
521        let reason = memory.alloc::<i32>(vars_plus_one)?;
522        let var_activity = memory.alloc::<u32>(vars_plus_one)?;
523        let var_phase = memory.alloc::<i8>(vars_plus_one)?;
524        let decision_heap = memory.alloc::<u32>(vars_plus_one)?;
525        let decision_heap_pos = memory.alloc::<u32>(vars_plus_one)?;
526
527        // Trail / levels
528        let trail = memory.alloc::<i32>(vars_plus_one)?;
529        let trail_lim = memory.alloc::<u32>(vars_plus_one)?;
530
531        // Analysis scratch
532        let seen = memory.alloc::<u8>(vars_plus_one)?;
533        let learnt_tmp = memory.alloc::<i32>(vars_plus_one)?;
534        let proof_vars_tmp = memory.alloc::<u32>(vars_plus_one)?;
535        let proof_reason_tmp = memory.alloc::<u32>(vars_plus_one)?;
536
537        // Watched literals
538        let watch0_pos = memory.alloc::<u32>(max_total_clauses)?;
539        let watch1_pos = memory.alloc::<u32>(max_total_clauses)?;
540        let watch_head = memory.alloc::<i32>(watch_head_len)?;
541        let watch_next = memory.alloc::<i32>(watch_clause_len)?;
542        let watch_prev = memory.alloc::<i32>(watch_clause_len)?;
543
544        // Learned clause arena
545        let learned_offsets = memory.alloc::<u32>(learned_offsets_len)?;
546        let learned_lits = memory.alloc::<i32>(max_learned_lits)?;
547        let learned_deleted = memory.alloc::<u8>(max_learned_clauses)?;
548        let learned_lbd = memory.alloc::<u32>(max_learned_clauses)?;
549        let learned_activity = memory.alloc::<u32>(max_learned_clauses)?;
550        let learned_locked = memory.alloc::<u8>(max_learned_clauses)?;
551
552        // Proof trace arena
553        let proof_offsets = memory.alloc::<u32>(learned_offsets_len)?;
554        let proof_data = memory.alloc::<u32>(max_proof_u32)?;
555
556        // Device-resident outputs
557        let out_status = memory.alloc::<i32>(1)?;
558        let out_error = memory.alloc::<i32>(1)?;
559        let mut out_learned_count = memory.alloc::<u32>(1)?;
560        self.provider
561            .htod_launch_metadata_sync_copy_into(&[0u32], &mut out_learned_count)
562            .map_err(|e| {
563                XlogError::Kernel(format!("Failed to init learned import count: {}", e))
564            })?;
565
566        let sat_fn = self
567            .provider
568            .device()
569            .inner()
570            .get_func(SAT_MODULE, sat_kernels::SAT_CDCL_SOLVE)
571            .ok_or_else(|| XlogError::Kernel("sat_cdcl_solve kernel not found".to_string()))?;
572
573        // IMPORTANT: When launching with an explicit `Vec<*mut c_void>` parameter list, scalar
574        // kernel arguments MUST be backed by stable host storage until `cuLaunchKernel` copies
575        // them. Do not pass temporaries like `self.config.restart_base.as_kernel_param()`.
576        let cnf_var_cap = cnf.var_cap;
577        let cnf_clause_cap = cnf.clause_cap;
578        let cfg_max_learned_clauses = self.config.max_learned_clauses;
579        let cfg_max_learned_lits = self.config.max_learned_lits;
580        let cfg_max_proof_u32 = self.config.max_proof_u32;
581        let cfg_restart_base = self.config.restart_base;
582        let cfg_reduce_interval = self.config.reduce_interval;
583        let cfg_max_conflicts = self.config.max_conflicts;
584        let learned_import_count_param = (&out_learned_count).as_kernel_param();
585
586        let mut params: Vec<*mut c_void> = vec![
587            compile_needed.as_kernel_param(),
588            (&cnf.clause_offsets).as_kernel_param(),
589            (&cnf.literals).as_kernel_param(),
590            (&cnf.num_vars).as_kernel_param(),
591            (&cnf.num_clauses).as_kernel_param(),
592            decision_base_limit.as_kernel_param(),
593            decision_extra_base.as_kernel_param(),
594            decision_extra_count.as_kernel_param(),
595            cnf_var_cap.as_kernel_param(),
596            cnf_clause_cap.as_kernel_param(),
597            cfg_max_learned_clauses.as_kernel_param(),
598            cfg_max_learned_lits.as_kernel_param(),
599            cfg_max_proof_u32.as_kernel_param(),
600            cfg_restart_base.as_kernel_param(),
601            cfg_reduce_interval.as_kernel_param(),
602            cfg_max_conflicts.as_kernel_param(),
603            learned_import_count_param,
604            (&assign).as_kernel_param(),
605            (&level).as_kernel_param(),
606            (&reason).as_kernel_param(),
607            (&var_activity).as_kernel_param(),
608            (&var_phase).as_kernel_param(),
609            (&decision_heap).as_kernel_param(),
610            (&decision_heap_pos).as_kernel_param(),
611            (&trail).as_kernel_param(),
612            (&trail_lim).as_kernel_param(),
613            (&seen).as_kernel_param(),
614            (&learnt_tmp).as_kernel_param(),
615            (&proof_vars_tmp).as_kernel_param(),
616            (&proof_reason_tmp).as_kernel_param(),
617            (&watch0_pos).as_kernel_param(),
618            (&watch1_pos).as_kernel_param(),
619            (&watch_head).as_kernel_param(),
620            (&watch_next).as_kernel_param(),
621            (&watch_prev).as_kernel_param(),
622            (&learned_offsets).as_kernel_param(),
623            (&learned_lits).as_kernel_param(),
624            (&learned_deleted).as_kernel_param(),
625            (&learned_lbd).as_kernel_param(),
626            (&learned_activity).as_kernel_param(),
627            (&learned_locked).as_kernel_param(),
628            (&proof_offsets).as_kernel_param(),
629            (&proof_data).as_kernel_param(),
630            (&out_status).as_kernel_param(),
631            (&out_error).as_kernel_param(),
632            (&out_learned_count).as_kernel_param(),
633        ];
634
635        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
636        unsafe {
637            sat_fn.clone().launch(
638                LaunchConfig {
639                    grid_dim: (1, 1, 1),
640                    // One block per SAT instance; use a full block so sat_cdcl_solve can do
641                    // block-parallel propagation and initialization.
642                    block_dim: (256, 1, 1),
643                    shared_mem_bytes: 0,
644                },
645                &mut params,
646            )
647        }
648        .map_err(|e| XlogError::Kernel(format!("Failed to launch SAT solver kernel: {}", e)))?;
649
650        Ok(GpuCdclRun {
651            assignment: assign,
652            _level: level,
653            _reason: reason,
654            _var_activity: var_activity,
655            _var_phase: var_phase,
656            _decision_heap: decision_heap,
657            _decision_heap_pos: decision_heap_pos,
658            _trail: trail,
659            _trail_lim: trail_lim,
660            _seen: seen,
661            _learnt_tmp: learnt_tmp,
662            _proof_vars_tmp: proof_vars_tmp,
663            _proof_reason_tmp: proof_reason_tmp,
664            _watch0_pos: watch0_pos,
665            _watch1_pos: watch1_pos,
666            _watch_head: watch_head,
667            _watch_next: watch_next,
668            _watch_prev: watch_prev,
669            learned_offsets,
670            learned_lits,
671            _learned_deleted: learned_deleted,
672            _learned_lbd: learned_lbd,
673            _learned_activity: learned_activity,
674            _learned_locked: learned_locked,
675            proof_offsets,
676            proof_data,
677            out_status,
678            out_error,
679            out_learned_count,
680        })
681    }
682
683    /// Launch CDCL using pre-allocated workspace buffers.
684    ///
685    /// Like `launch_cdcl_with_decision_ranges_gated` but uses `ws` buffers instead of
686    /// allocating per call. Returns `Result<()>` — the caller reads `ws.out_*` directly.
687    #[expect(
688        clippy::too_many_arguments,
689        reason = "the kernel launch contract keeps its workspace, CNF, four device-side decision bounds, and learned-arena mode explicit"
690    )]
691    fn launch_cdcl_with_workspace_gated(
692        &self,
693        ws: &mut GpuCdclWorkspace,
694        cnf: &GpuCnf,
695        compile_needed: &TrackedCudaSlice<u32>,
696        decision_base_limit: &TrackedCudaSlice<u32>,
697        decision_extra_base: &TrackedCudaSlice<u32>,
698        decision_extra_count: &TrackedCudaSlice<u32>,
699        import_existing_learned: bool,
700    ) -> Result<()> {
701        cnf.require_provider_memory(&self.provider, "GPU CDCL solver provider boundary")?;
702        self.require_workspace_on_provider(ws)?;
703        self.require_slice_on_provider("compile_needed", compile_needed)?;
704        self.require_slice_on_provider("decision_base_limit", decision_base_limit)?;
705        self.require_slice_on_provider("decision_extra_base", decision_extra_base)?;
706        self.require_slice_on_provider("decision_extra_count", decision_extra_count)?;
707        if compile_needed.len() != 1 {
708            return Err(XlogError::Compilation(format!(
709                "GpuCdclSolver requires compile_needed len=1, got {}",
710                compile_needed.len()
711            )));
712        }
713
714        self.require_workspace_capacity_for_cnf(ws, cnf.var_cap, cnf.clause_cap)?;
715
716        // Replicate all validation checks from the existing launch method.
717        if cnf.var_cap == 0 {
718            return Err(XlogError::Compilation(
719                "GpuCdclSolver requires num_vars > 0".to_string(),
720            ));
721        }
722        if decision_base_limit.len() != 1 {
723            return Err(XlogError::Compilation(format!(
724                "GpuCdclSolver requires decision_base_limit len=1, got {}",
725                decision_base_limit.len()
726            )));
727        }
728        if decision_extra_base.len() != 1 {
729            return Err(XlogError::Compilation(format!(
730                "GpuCdclSolver requires decision_extra_base len=1, got {}",
731                decision_extra_base.len()
732            )));
733        }
734        if decision_extra_count.len() != 1 {
735            return Err(XlogError::Compilation(format!(
736                "GpuCdclSolver requires decision_extra_count len=1, got {}",
737                decision_extra_count.len()
738            )));
739        }
740        if self.config.max_learned_clauses == 0 {
741            return Err(XlogError::Compilation(
742                "GpuCdclSolver requires max_learned_clauses > 0".to_string(),
743            ));
744        }
745        if self.config.max_learned_lits == 0 {
746            return Err(XlogError::Compilation(
747                "GpuCdclSolver requires max_learned_lits > 0".to_string(),
748            ));
749        }
750        if self.config.max_proof_u32 < 2 {
751            return Err(XlogError::Compilation(
752                "GpuCdclSolver requires max_proof_u32 >= 2".to_string(),
753            ));
754        }
755
756        // No-op: the sat_cdcl_solve kernel initializes all mutable state at launch.
757        ws.reset_for_solve();
758        if !import_existing_learned {
759            self.provider
760                .htod_launch_metadata_sync_copy_into(&[0u32], &mut ws.out_learned_count)
761                .map_err(|e| {
762                    XlogError::Kernel(format!("Failed to init learned import count: {}", e))
763                })?;
764        }
765
766        let sat_fn = self
767            .provider
768            .device()
769            .inner()
770            .get_func(SAT_MODULE, sat_kernels::SAT_CDCL_SOLVE)
771            .ok_or_else(|| XlogError::Kernel("sat_cdcl_solve kernel not found".to_string()))?;
772
773        // Scalar kernel arguments must be backed by stable host storage until cuLaunchKernel
774        // copies them.
775        let cnf_var_cap = cnf.var_cap;
776        let cnf_clause_cap = cnf.clause_cap;
777        let cfg_max_learned_clauses = self.config.max_learned_clauses;
778        let cfg_max_learned_lits = self.config.max_learned_lits;
779        let cfg_max_proof_u32 = self.config.max_proof_u32;
780        let cfg_restart_base = self.config.restart_base;
781        let cfg_reduce_interval = self.config.reduce_interval;
782        let cfg_max_conflicts = self.config.max_conflicts;
783        let learned_import_count_param = (&ws.out_learned_count).as_kernel_param();
784
785        // Parameter order MUST match launch_cdcl_with_decision_ranges_gated exactly.
786        let mut params: Vec<*mut c_void> = vec![
787            compile_needed.as_kernel_param(),
788            (&cnf.clause_offsets).as_kernel_param(),
789            (&cnf.literals).as_kernel_param(),
790            (&cnf.num_vars).as_kernel_param(),
791            (&cnf.num_clauses).as_kernel_param(),
792            decision_base_limit.as_kernel_param(),
793            decision_extra_base.as_kernel_param(),
794            decision_extra_count.as_kernel_param(),
795            cnf_var_cap.as_kernel_param(),
796            cnf_clause_cap.as_kernel_param(),
797            cfg_max_learned_clauses.as_kernel_param(),
798            cfg_max_learned_lits.as_kernel_param(),
799            cfg_max_proof_u32.as_kernel_param(),
800            cfg_restart_base.as_kernel_param(),
801            cfg_reduce_interval.as_kernel_param(),
802            cfg_max_conflicts.as_kernel_param(),
803            learned_import_count_param,
804            (&ws.assign).as_kernel_param(),
805            (&ws.level).as_kernel_param(),
806            (&ws.reason).as_kernel_param(),
807            (&ws.var_activity).as_kernel_param(),
808            (&ws.var_phase).as_kernel_param(),
809            (&ws.decision_heap).as_kernel_param(),
810            (&ws.decision_heap_pos).as_kernel_param(),
811            (&ws.trail).as_kernel_param(),
812            (&ws.trail_lim).as_kernel_param(),
813            (&ws.seen).as_kernel_param(),
814            (&ws.learnt_tmp).as_kernel_param(),
815            (&ws.proof_vars_tmp).as_kernel_param(),
816            (&ws.proof_reason_tmp).as_kernel_param(),
817            (&ws.watch0_pos).as_kernel_param(),
818            (&ws.watch1_pos).as_kernel_param(),
819            (&ws.watch_head).as_kernel_param(),
820            (&ws.watch_next).as_kernel_param(),
821            (&ws.watch_prev).as_kernel_param(),
822            (&ws.learned_offsets).as_kernel_param(),
823            (&ws.learned_lits).as_kernel_param(),
824            (&ws.learned_deleted).as_kernel_param(),
825            (&ws.learned_lbd).as_kernel_param(),
826            (&ws.learned_activity).as_kernel_param(),
827            (&ws.learned_locked).as_kernel_param(),
828            (&ws.proof_offsets).as_kernel_param(),
829            (&ws.proof_data).as_kernel_param(),
830            (&ws.out_status).as_kernel_param(),
831            (&ws.out_error).as_kernel_param(),
832            (&ws.out_learned_count).as_kernel_param(),
833        ];
834
835        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
836        unsafe {
837            sat_fn.clone().launch(
838                LaunchConfig {
839                    grid_dim: (1, 1, 1),
840                    block_dim: (256, 1, 1),
841                    shared_mem_bytes: 0,
842                },
843                &mut params,
844            )
845        }
846        .map_err(|e| XlogError::Kernel(format!("Failed to launch SAT solver kernel: {}", e)))?;
847
848        Ok(())
849    }
850
851    /// Launch CDCL and return raw device outputs without enforcing SAT/UNSAT on device.
852    ///
853    /// This is intentionally **not** used in production verifier paths. It exists so tests and
854    /// debugging tools can inspect `out_status/out_error` without modifying kernel behavior.
855    pub fn solve_raw_with_branch_limit(
856        &self,
857        cnf: &GpuCnf,
858        branch_var_limit: &TrackedCudaSlice<u32>,
859    ) -> Result<GpuCdclRawOutput> {
860        let compile_needed = self.alloc_u32_scalar(1)?;
861        self.solve_raw_with_branch_limit_gated(cnf, &compile_needed, branch_var_limit)
862    }
863
864    /// Gated variant of `solve_raw_with_branch_limit`.
865    pub fn solve_raw_with_branch_limit_gated(
866        &self,
867        cnf: &GpuCnf,
868        compile_needed: &TrackedCudaSlice<u32>,
869        branch_var_limit: &TrackedCudaSlice<u32>,
870    ) -> Result<GpuCdclRawOutput> {
871        let zero = self.alloc_u32_scalar(0)?;
872        let run = self.launch_cdcl_with_decision_ranges_gated(
873            cnf,
874            compile_needed,
875            branch_var_limit,
876            &zero,
877            &zero,
878        )?;
879        // Ensure kernel completion so `out_*` are valid for inspection.
880        self.provider.device().synchronize()?;
881
882        let GpuCdclRun {
883            assignment,
884            out_status,
885            out_error,
886            out_learned_count,
887            ..
888        } = run;
889
890        Ok(GpuCdclRawOutput {
891            assignment,
892            out_status,
893            out_error,
894            out_learned_count,
895        })
896    }
897
898    /// Launch CDCL and return raw device outputs without enforcing SAT/UNSAT on device, using an
899    /// explicit decision variable set:
900    /// - decision vars include all `v` in `1..=decision_base_limit[0]` and
901    ///   `decision_extra_base[0]..(decision_extra_base[0] + decision_extra_count[0] - 1)`.
902    ///
903    /// Production verifier paths should prefer `solve_expect_*` methods which enforce results on
904    /// GPU without host reads.
905    pub fn solve_raw_with_decision_ranges(
906        &self,
907        cnf: &GpuCnf,
908        decision_base_limit: &TrackedCudaSlice<u32>,
909        decision_extra_base: &TrackedCudaSlice<u32>,
910        decision_extra_count: &TrackedCudaSlice<u32>,
911    ) -> Result<GpuCdclRawOutput> {
912        let compile_needed = self.alloc_u32_scalar(1)?;
913        self.solve_raw_with_decision_ranges_gated(
914            cnf,
915            &compile_needed,
916            decision_base_limit,
917            decision_extra_base,
918            decision_extra_count,
919        )
920    }
921
922    /// Gated variant of `solve_raw_with_decision_ranges`.
923    pub fn solve_raw_with_decision_ranges_gated(
924        &self,
925        cnf: &GpuCnf,
926        compile_needed: &TrackedCudaSlice<u32>,
927        decision_base_limit: &TrackedCudaSlice<u32>,
928        decision_extra_base: &TrackedCudaSlice<u32>,
929        decision_extra_count: &TrackedCudaSlice<u32>,
930    ) -> Result<GpuCdclRawOutput> {
931        let run = self.launch_cdcl_with_decision_ranges_gated(
932            cnf,
933            compile_needed,
934            decision_base_limit,
935            decision_extra_base,
936            decision_extra_count,
937        )?;
938        // Ensure kernel completion so `out_*` are valid for inspection.
939        self.provider.device().synchronize()?;
940
941        let GpuCdclRun {
942            assignment,
943            out_status,
944            out_error,
945            out_learned_count,
946            ..
947        } = run;
948
949        Ok(GpuCdclRawOutput {
950            assignment,
951            out_status,
952            out_error,
953            out_learned_count,
954        })
955    }
956
957    /// Solve and enforce SAT entirely on GPU (no device->host reads).
958    pub fn solve_expect_sat(&self, cnf: &GpuCnf) -> Result<TrackedCudaSlice<i8>> {
959        let compile_needed = self.alloc_u32_scalar(1)?;
960        self.solve_expect_sat_gated(cnf, &compile_needed)
961    }
962
963    /// Solve and enforce SAT entirely on GPU (no device->host reads),
964    /// skipping all GPU work if `compile_needed` is 0.
965    pub fn solve_expect_sat_gated(
966        &self,
967        cnf: &GpuCnf,
968        compile_needed: &TrackedCudaSlice<u32>,
969    ) -> Result<TrackedCudaSlice<i8>> {
970        self.solve_expect_sat_with_branch_limit_gated(cnf, compile_needed, &cnf.num_vars)
971    }
972
973    /// Solve and enforce SAT entirely on GPU (no device->host reads), using an explicit decision
974    /// variable set:
975    /// - decision vars include all `v` in `1..=decision_base_limit[0]` and
976    ///   `decision_extra_base[0]..(decision_extra_base[0] + decision_extra_count[0] - 1)`.
977    pub fn solve_expect_sat_with_decision_ranges(
978        &self,
979        cnf: &GpuCnf,
980        decision_base_limit: &TrackedCudaSlice<u32>,
981        decision_extra_base: &TrackedCudaSlice<u32>,
982        decision_extra_count: &TrackedCudaSlice<u32>,
983    ) -> Result<TrackedCudaSlice<i8>> {
984        let compile_needed = self.alloc_u32_scalar(1)?;
985        self.solve_expect_sat_with_decision_ranges_gated(
986            cnf,
987            &compile_needed,
988            decision_base_limit,
989            decision_extra_base,
990            decision_extra_count,
991        )
992    }
993
994    /// Gated variant of `solve_expect_sat_with_decision_ranges`.
995    pub fn solve_expect_sat_with_decision_ranges_gated(
996        &self,
997        cnf: &GpuCnf,
998        compile_needed: &TrackedCudaSlice<u32>,
999        decision_base_limit: &TrackedCudaSlice<u32>,
1000        decision_extra_base: &TrackedCudaSlice<u32>,
1001        decision_extra_count: &TrackedCudaSlice<u32>,
1002    ) -> Result<TrackedCudaSlice<i8>> {
1003        #[cfg(debug_assertions)]
1004        let trace = resolve_bool(None, "XLOG_CDCL_TRACE", false)?;
1005        #[cfg(debug_assertions)]
1006        let t0 = std::time::Instant::now();
1007
1008        let run = self.launch_cdcl_with_decision_ranges_gated(
1009            cnf,
1010            compile_needed,
1011            decision_base_limit,
1012            decision_extra_base,
1013            decision_extra_count,
1014        )?;
1015        self.require_expected_status(
1016            &run.out_status,
1017            &run.out_error,
1018            SAT_STATUS_SAT,
1019            "GPU CDCL SAT expectation",
1020        )?;
1021
1022        let device = self.provider.device().inner();
1023        let memory = self.provider.memory();
1024
1025        let assert_status_fn = device
1026            .get_func(SAT_MODULE, sat_kernels::SAT_ASSERT_STATUS)
1027            .ok_or_else(|| XlogError::Kernel("sat_assert_status kernel not found".to_string()))?;
1028        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1029        unsafe {
1030            assert_status_fn
1031                .clone()
1032                .launch(
1033                    LaunchConfig {
1034                        grid_dim: (1, 1, 1),
1035                        block_dim: (1, 1, 1),
1036                        shared_mem_bytes: 0,
1037                    },
1038                    (
1039                        compile_needed,
1040                        &run.out_status,
1041                        &run.out_error,
1042                        SAT_STATUS_SAT,
1043                    ),
1044                )
1045                .map_err(|e| {
1046                    XlogError::Kernel(format!("Failed to launch sat_assert_status: {}", e))
1047                })?;
1048        }
1049        // Fail-fast if the solver did not produce SAT.
1050        self.provider.device().synchronize()?;
1051        #[cfg(debug_assertions)]
1052        if trace {
1053            eprintln!("[xlog-solve] cdcl(sat) time: {:?}", t0.elapsed());
1054        }
1055
1056        let mut out_ok = memory.alloc::<i32>(1)?;
1057        let check_fn = device
1058            .get_func(SAT_MODULE, sat_kernels::SAT_CHECK_MODEL)
1059            .ok_or_else(|| XlogError::Kernel("sat_check_model kernel not found".to_string()))?;
1060        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1061        unsafe {
1062            check_fn
1063                .clone()
1064                .launch(
1065                    LaunchConfig {
1066                        grid_dim: (1, 1, 1),
1067                        block_dim: (256, 1, 1),
1068                        shared_mem_bytes: 0,
1069                    },
1070                    (
1071                        compile_needed,
1072                        &cnf.clause_offsets,
1073                        &cnf.literals,
1074                        &cnf.num_clauses,
1075                        &run.assignment,
1076                        &mut out_ok,
1077                    ),
1078                )
1079                .map_err(|e| {
1080                    XlogError::Kernel(format!("Failed to launch SAT model check: {}", e))
1081                })?;
1082        }
1083
1084        let assert_ok_fn = device
1085            .get_func(SAT_MODULE, sat_kernels::SAT_ASSERT_OK)
1086            .ok_or_else(|| XlogError::Kernel("sat_assert_ok kernel not found".to_string()))?;
1087        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1088        unsafe {
1089            assert_ok_fn
1090                .clone()
1091                .launch(
1092                    LaunchConfig {
1093                        grid_dim: (1, 1, 1),
1094                        block_dim: (1, 1, 1),
1095                        shared_mem_bytes: 0,
1096                    },
1097                    (compile_needed, &out_ok),
1098                )
1099                .map_err(|e| XlogError::Kernel(format!("Failed to launch sat_assert_ok: {}", e)))?;
1100        }
1101        self.provider.device().synchronize()?;
1102        #[cfg(debug_assertions)]
1103        if trace {
1104            eprintln!(
1105                "[xlog-solve] cdcl(sat)+model_check time: {:?}",
1106                t0.elapsed()
1107            );
1108        }
1109
1110        Ok(run.assignment)
1111    }
1112
1113    /// Solve and enforce SAT entirely on GPU (no device->host reads),
1114    /// restricting branching to variables in `1..=branch_var_limit[0]`.
1115    pub fn solve_expect_sat_with_branch_limit(
1116        &self,
1117        cnf: &GpuCnf,
1118        branch_var_limit: &TrackedCudaSlice<u32>,
1119    ) -> Result<TrackedCudaSlice<i8>> {
1120        let compile_needed = self.alloc_u32_scalar(1)?;
1121        self.solve_expect_sat_with_branch_limit_gated(cnf, &compile_needed, branch_var_limit)
1122    }
1123
1124    /// Solve and enforce SAT entirely on GPU (no device->host reads),
1125    /// skipping all GPU work if `compile_needed` is 0, and restricting branching to
1126    /// variables in `1..=branch_var_limit[0]`.
1127    pub fn solve_expect_sat_with_branch_limit_gated(
1128        &self,
1129        cnf: &GpuCnf,
1130        compile_needed: &TrackedCudaSlice<u32>,
1131        branch_var_limit: &TrackedCudaSlice<u32>,
1132    ) -> Result<TrackedCudaSlice<i8>> {
1133        let zero = self.alloc_u32_scalar(0)?;
1134        self.solve_expect_sat_with_decision_ranges_gated(
1135            cnf,
1136            compile_needed,
1137            branch_var_limit,
1138            &zero,
1139            &zero,
1140        )
1141    }
1142
1143    /// Solve and enforce UNSAT entirely on GPU (no device->host reads).
1144    pub fn solve_expect_unsat(&self, cnf: &GpuCnf) -> Result<()> {
1145        let compile_needed = self.alloc_u32_scalar(1)?;
1146        self.solve_expect_unsat_gated(cnf, &compile_needed)
1147    }
1148
1149    /// Solve and enforce UNSAT entirely on GPU (no device->host reads),
1150    /// skipping all GPU work if `compile_needed` is 0.
1151    pub fn solve_expect_unsat_gated(
1152        &self,
1153        cnf: &GpuCnf,
1154        compile_needed: &TrackedCudaSlice<u32>,
1155    ) -> Result<()> {
1156        self.solve_expect_unsat_with_branch_limit_gated(cnf, compile_needed, &cnf.num_vars)
1157    }
1158
1159    /// Solve and enforce UNSAT entirely on GPU (no device->host reads),
1160    /// restricting branching to variables in `1..=branch_var_limit[0]`.
1161    pub fn solve_expect_unsat_with_branch_limit(
1162        &self,
1163        cnf: &GpuCnf,
1164        branch_var_limit: &TrackedCudaSlice<u32>,
1165    ) -> Result<()> {
1166        let compile_needed = self.alloc_u32_scalar(1)?;
1167        self.solve_expect_unsat_with_branch_limit_gated(cnf, &compile_needed, branch_var_limit)
1168    }
1169
1170    /// Solve and enforce UNSAT with GPU proof verification, using an explicit decision variable set:
1171    /// - decision vars include all `v` in `1..=decision_base_limit[0]` and
1172    ///   `decision_extra_base[0]..(decision_extra_base[0] + decision_extra_count[0] - 1)`.
1173    pub fn solve_expect_unsat_with_decision_ranges(
1174        &self,
1175        cnf: &GpuCnf,
1176        decision_base_limit: &TrackedCudaSlice<u32>,
1177        decision_extra_base: &TrackedCudaSlice<u32>,
1178        decision_extra_count: &TrackedCudaSlice<u32>,
1179    ) -> Result<()> {
1180        let compile_needed = self.alloc_u32_scalar(1)?;
1181        self.solve_expect_unsat_with_decision_ranges_gated(
1182            cnf,
1183            &compile_needed,
1184            decision_base_limit,
1185            decision_extra_base,
1186            decision_extra_count,
1187        )
1188    }
1189
1190    /// Solve and enforce UNSAT entirely on GPU (no device->host reads),
1191    /// skipping all GPU work if `compile_needed` is 0, and restricting branching to
1192    /// variables in `1..=branch_var_limit[0]`.
1193    pub fn solve_expect_unsat_with_branch_limit_gated(
1194        &self,
1195        cnf: &GpuCnf,
1196        compile_needed: &TrackedCudaSlice<u32>,
1197        branch_var_limit: &TrackedCudaSlice<u32>,
1198    ) -> Result<()> {
1199        let zero = self.alloc_u32_scalar(0)?;
1200        self.solve_expect_unsat_with_decision_ranges_gated(
1201            cnf,
1202            compile_needed,
1203            branch_var_limit,
1204            &zero,
1205            &zero,
1206        )
1207    }
1208
1209    /// Solve and enforce UNSAT entirely on GPU (no device->host reads), using an explicit decision
1210    /// variable set:
1211    /// - decision vars include all `v` in `1..=decision_base_limit[0]` and
1212    ///   `decision_extra_base[0]..(decision_extra_base[0] + decision_extra_count[0] - 1)`.
1213    pub fn solve_expect_unsat_with_decision_ranges_gated(
1214        &self,
1215        cnf: &GpuCnf,
1216        compile_needed: &TrackedCudaSlice<u32>,
1217        decision_base_limit: &TrackedCudaSlice<u32>,
1218        decision_extra_base: &TrackedCudaSlice<u32>,
1219        decision_extra_count: &TrackedCudaSlice<u32>,
1220    ) -> Result<()> {
1221        #[cfg(debug_assertions)]
1222        let trace = resolve_bool(None, "XLOG_CDCL_TRACE", false)?;
1223        #[cfg(debug_assertions)]
1224        let t0 = std::time::Instant::now();
1225
1226        let run = self.launch_cdcl_with_decision_ranges_gated(
1227            cnf,
1228            compile_needed,
1229            decision_base_limit,
1230            decision_extra_base,
1231            decision_extra_count,
1232        )?;
1233        self.require_expected_status(
1234            &run.out_status,
1235            &run.out_error,
1236            SAT_STATUS_UNSAT,
1237            "GPU CDCL UNSAT expectation",
1238        )?;
1239
1240        let device = self.provider.device().inner();
1241        let memory = self.provider.memory();
1242
1243        let assert_status_fn = device
1244            .get_func(SAT_MODULE, sat_kernels::SAT_ASSERT_STATUS)
1245            .ok_or_else(|| XlogError::Kernel("sat_assert_status kernel not found".to_string()))?;
1246        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1247        unsafe {
1248            assert_status_fn
1249                .clone()
1250                .launch(
1251                    LaunchConfig {
1252                        grid_dim: (1, 1, 1),
1253                        block_dim: (1, 1, 1),
1254                        shared_mem_bytes: 0,
1255                    },
1256                    (
1257                        compile_needed,
1258                        &run.out_status,
1259                        &run.out_error,
1260                        SAT_STATUS_UNSAT,
1261                    ),
1262                )
1263                .map_err(|e| {
1264                    XlogError::Kernel(format!("Failed to launch sat_assert_status: {}", e))
1265                })?;
1266        }
1267        // Fail-fast if the solver did not produce UNSAT.
1268        self.provider.device().synchronize()?;
1269        #[cfg(debug_assertions)]
1270        if trace {
1271            eprintln!("[xlog-solve] cdcl(unsat) time: {:?}", t0.elapsed());
1272        }
1273
1274        let mut out_ok = memory.alloc::<i32>(1)?;
1275        self.provider
1276            .htod_launch_metadata_sync_copy_into(&[1i32], &mut out_ok)
1277            .map_err(|e| XlogError::Kernel(format!("Failed to init proof out_ok: {}", e)))?;
1278
1279        // sat_proof_check uses scratch buffers sized to `scratch_cap` per verifier block. To keep
1280        // proof checking fast on large instances, allocate multiple scratch regions and verify
1281        // learned clauses in parallel across blocks.
1282        let scratch_cap_u32 = cnf
1283            .var_cap
1284            .checked_add(1)
1285            .ok_or_else(|| XlogError::Kernel("Proof scratch capacity overflow".to_string()))?;
1286        let scratch_cap = scratch_cap_u32 as usize;
1287
1288        let mut proof_blocks: usize = 1;
1289        let mut scratch_a = None;
1290        let mut scratch_b = None;
1291        let mut scratch_map = None;
1292        let mut last_alloc_err: Option<XlogError> = None;
1293        for blocks in [512usize, 256, 128, 64, 32, 16, 8, 4, 2, 1] {
1294            let len = match scratch_cap.checked_mul(blocks) {
1295                Some(v) => v,
1296                None => {
1297                    last_alloc_err = Some(XlogError::Kernel(
1298                        "Proof scratch allocation length overflow".to_string(),
1299                    ));
1300                    continue;
1301                }
1302            };
1303
1304            let a = match memory.alloc::<i32>(len) {
1305                Ok(buf) => buf,
1306                Err(e) => {
1307                    last_alloc_err = Some(e);
1308                    continue;
1309                }
1310            };
1311            let b = match memory.alloc::<i32>(len) {
1312                Ok(buf) => buf,
1313                Err(e) => {
1314                    last_alloc_err = Some(e);
1315                    // Drop `a` before retrying with a smaller configuration.
1316                    drop(a);
1317                    continue;
1318                }
1319            };
1320            let m = match memory.alloc::<u32>(len) {
1321                Ok(buf) => buf,
1322                Err(e) => {
1323                    last_alloc_err = Some(e);
1324                    drop(a);
1325                    drop(b);
1326                    continue;
1327                }
1328            };
1329
1330            proof_blocks = blocks;
1331            scratch_a = Some(a);
1332            scratch_b = Some(b);
1333            scratch_map = Some(m);
1334            break;
1335        }
1336        let scratch_a = scratch_a.ok_or_else(|| {
1337            last_alloc_err.unwrap_or_else(|| {
1338                XlogError::Kernel("Failed to allocate proof scratch buffers".to_string())
1339            })
1340        })?;
1341        let scratch_b = scratch_b
1342            .ok_or_else(|| XlogError::Kernel("Missing proof scratch buffer".to_string()))?;
1343        let mut scratch_map = scratch_map
1344            .ok_or_else(|| XlogError::Kernel("Missing proof scratch map buffer".to_string()))?;
1345        device
1346            .memset_zeros(&mut scratch_map)
1347            .map_err(|e| XlogError::Kernel(format!("Failed to zero proof scratch map: {}", e)))?;
1348        #[cfg(debug_assertions)]
1349        if trace {
1350            eprintln!("[xlog-solve] proof_check blocks: {}", proof_blocks);
1351        }
1352        #[cfg(debug_assertions)]
1353        let t_mark = std::time::Instant::now();
1354
1355        let needed_cap_u32 = self.config.max_learned_clauses;
1356        let needed_cap = needed_cap_u32 as usize;
1357        let mut needed = memory.alloc::<u8>(needed_cap)?;
1358        device
1359            .memset_zeros(&mut needed)
1360            .map_err(|e| XlogError::Kernel(format!("Failed to zero proof needed mask: {}", e)))?;
1361
1362        let mark_needed_fn = device
1363            .get_func(SAT_MODULE, sat_kernels::SAT_PROOF_MARK_NEEDED)
1364            .ok_or_else(|| {
1365                XlogError::Kernel("sat_proof_mark_needed kernel not found".to_string())
1366            })?;
1367        let mut mark_params: Vec<*mut c_void> = vec![
1368            compile_needed.as_kernel_param(),
1369            (&cnf.num_clauses).as_kernel_param(),
1370            (&run.out_learned_count).as_kernel_param(),
1371            (&run.proof_offsets).as_kernel_param(),
1372            (&run.proof_data).as_kernel_param(),
1373            needed_cap_u32.as_kernel_param(),
1374            (&needed).as_kernel_param(),
1375        ];
1376        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1377        unsafe {
1378            mark_needed_fn
1379                .clone()
1380                .launch(
1381                    LaunchConfig {
1382                        grid_dim: (1, 1, 1),
1383                        block_dim: (1, 1, 1),
1384                        shared_mem_bytes: 0,
1385                    },
1386                    &mut mark_params,
1387                )
1388                .map_err(|e| {
1389                    XlogError::Kernel(format!("Failed to launch sat_proof_mark_needed: {}", e))
1390                })?;
1391        }
1392        self.provider.device().synchronize()?;
1393        #[cfg(debug_assertions)]
1394        if trace {
1395            eprintln!(
1396                "[xlog-solve] proof_mark_needed time: {:?}",
1397                t_mark.elapsed()
1398            );
1399        }
1400
1401        let proof_fn = device
1402            .get_func(SAT_MODULE, sat_kernels::SAT_PROOF_CHECK)
1403            .ok_or_else(|| XlogError::Kernel("sat_proof_check kernel not found".to_string()))?;
1404        #[cfg(debug_assertions)]
1405        let t_proof = std::time::Instant::now();
1406        let proof_blocks_u32 = u32::try_from(proof_blocks)
1407            .map_err(|_| XlogError::Kernel("Proof check grid dim exceeds u32::MAX".to_string()))?;
1408        let mut proof_params: Vec<*mut c_void> = vec![
1409            compile_needed.as_kernel_param(),
1410            (&cnf.clause_offsets).as_kernel_param(),
1411            (&cnf.literals).as_kernel_param(),
1412            (&cnf.num_clauses).as_kernel_param(),
1413            (&run.learned_offsets).as_kernel_param(),
1414            (&run.learned_lits).as_kernel_param(),
1415            (&run.out_learned_count).as_kernel_param(),
1416            (&run.proof_offsets).as_kernel_param(),
1417            (&run.proof_data).as_kernel_param(),
1418            (&needed).as_kernel_param(),
1419            needed_cap_u32.as_kernel_param(),
1420            (&scratch_a).as_kernel_param(),
1421            (&scratch_b).as_kernel_param(),
1422            (&scratch_map).as_kernel_param(),
1423            scratch_cap_u32.as_kernel_param(),
1424            (&out_ok).as_kernel_param(),
1425        ];
1426        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1427        unsafe {
1428            proof_fn
1429                .clone()
1430                .launch(
1431                    LaunchConfig {
1432                        grid_dim: (proof_blocks_u32, 1, 1),
1433                        block_dim: (128, 1, 1),
1434                        shared_mem_bytes: 0,
1435                    },
1436                    &mut proof_params,
1437                )
1438                .map_err(|e| {
1439                    XlogError::Kernel(format!("Failed to launch SAT proof check: {}", e))
1440                })?;
1441        }
1442
1443        let assert_ok_fn = device
1444            .get_func(SAT_MODULE, sat_kernels::SAT_ASSERT_OK)
1445            .ok_or_else(|| XlogError::Kernel("sat_assert_ok kernel not found".to_string()))?;
1446        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1447        unsafe {
1448            assert_ok_fn
1449                .clone()
1450                .launch(
1451                    LaunchConfig {
1452                        grid_dim: (1, 1, 1),
1453                        block_dim: (1, 1, 1),
1454                        shared_mem_bytes: 0,
1455                    },
1456                    (compile_needed, &out_ok),
1457                )
1458                .map_err(|e| XlogError::Kernel(format!("Failed to launch sat_assert_ok: {}", e)))?;
1459        }
1460        self.provider.device().synchronize()?;
1461        #[cfg(debug_assertions)]
1462        if trace {
1463            eprintln!("[xlog-solve] proof_check time: {:?}", t_proof.elapsed());
1464            eprintln!(
1465                "[xlog-solve] cdcl(unsat)+proof_check time: {:?}",
1466                t0.elapsed()
1467            );
1468        }
1469
1470        Ok(())
1471    }
1472
1473    // ── Workspace-reuse variants ──────────────────────────────────────────
1474
1475    /// Solve and enforce UNSAT entirely on GPU using a pre-allocated workspace,
1476    /// restricting branching to variables in `1..=branch_var_limit[0]`.
1477    pub fn solve_expect_unsat_with_branch_limit_ws(
1478        &self,
1479        ws: &mut GpuCdclWorkspace,
1480        cnf: &GpuCnf,
1481        branch_var_limit: &TrackedCudaSlice<u32>,
1482    ) -> Result<()> {
1483        let compile_needed = self.alloc_u32_scalar(1)?;
1484        self.solve_expect_unsat_with_branch_limit_gated_ws(
1485            ws,
1486            cnf,
1487            &compile_needed,
1488            branch_var_limit,
1489        )
1490    }
1491
1492    /// Gated workspace variant: solve and enforce UNSAT entirely on GPU,
1493    /// restricting branching to variables in `1..=branch_var_limit[0]`.
1494    /// Skips all GPU work if `compile_needed` is 0.
1495    pub fn solve_expect_unsat_with_branch_limit_gated_ws(
1496        &self,
1497        ws: &mut GpuCdclWorkspace,
1498        cnf: &GpuCnf,
1499        compile_needed: &TrackedCudaSlice<u32>,
1500        branch_var_limit: &TrackedCudaSlice<u32>,
1501    ) -> Result<()> {
1502        let zero = self.alloc_u32_scalar(0)?;
1503        self.solve_expect_unsat_with_decision_ranges_gated_ws(
1504            ws,
1505            cnf,
1506            compile_needed,
1507            branch_var_limit,
1508            &zero,
1509            &zero,
1510        )
1511    }
1512
1513    /// Solve and enforce UNSAT entirely on GPU using a pre-allocated workspace,
1514    /// with explicit decision variable ranges.
1515    pub fn solve_expect_unsat_with_decision_ranges_ws(
1516        &self,
1517        ws: &mut GpuCdclWorkspace,
1518        cnf: &GpuCnf,
1519        decision_base_limit: &TrackedCudaSlice<u32>,
1520        decision_extra_base: &TrackedCudaSlice<u32>,
1521        decision_extra_count: &TrackedCudaSlice<u32>,
1522    ) -> Result<()> {
1523        let compile_needed = self.alloc_u32_scalar(1)?;
1524        self.solve_expect_unsat_with_decision_ranges_gated_ws(
1525            ws,
1526            cnf,
1527            &compile_needed,
1528            decision_base_limit,
1529            decision_extra_base,
1530            decision_extra_count,
1531        )
1532    }
1533
1534    /// Gated workspace variant (LEAF): solve and enforce UNSAT entirely on GPU
1535    /// using a pre-allocated workspace, with explicit decision variable ranges.
1536    /// Skips all GPU work if `compile_needed` is 0.
1537    ///
1538    /// This is the leaf implementation for all `_ws` UNSAT methods. It:
1539    /// 1. Launches CDCL via `launch_cdcl_with_workspace_gated`
1540    /// 2. Asserts UNSAT status on GPU
1541    /// 3. Verifies the UNSAT proof on GPU (sat_proof_mark_needed + sat_proof_check + sat_assert_ok)
1542    pub fn solve_expect_unsat_with_decision_ranges_gated_ws(
1543        &self,
1544        ws: &mut GpuCdclWorkspace,
1545        cnf: &GpuCnf,
1546        compile_needed: &TrackedCudaSlice<u32>,
1547        decision_base_limit: &TrackedCudaSlice<u32>,
1548        decision_extra_base: &TrackedCudaSlice<u32>,
1549        decision_extra_count: &TrackedCudaSlice<u32>,
1550    ) -> Result<()> {
1551        self.solve_expect_unsat_with_decision_ranges_gated_ws_inner(
1552            ws,
1553            cnf,
1554            compile_needed,
1555            decision_base_limit,
1556            decision_extra_base,
1557            decision_extra_count,
1558            false,
1559        )
1560    }
1561
1562    /// Solve and enforce UNSAT on GPU while importing the current workspace learned arena.
1563    ///
1564    /// The imported learned count is read from `ws.out_learned_count` on device. Callers must only
1565    /// use this when the imported learned clauses are valid for `cnf`.
1566    pub fn solve_expect_unsat_with_branch_limit_ws_importing_learned(
1567        &self,
1568        ws: &mut GpuCdclWorkspace,
1569        cnf: &GpuCnf,
1570        branch_var_limit: &TrackedCudaSlice<u32>,
1571    ) -> Result<()> {
1572        let compile_needed = self.alloc_u32_scalar(1)?;
1573        let zero = self.alloc_u32_scalar(0)?;
1574        self.solve_expect_unsat_with_decision_ranges_gated_ws_inner(
1575            ws,
1576            cnf,
1577            &compile_needed,
1578            branch_var_limit,
1579            &zero,
1580            &zero,
1581            true,
1582        )
1583    }
1584
1585    #[expect(
1586        clippy::too_many_arguments,
1587        reason = "the workspace UNSAT operation preserves the exact kernel decision-range contract and learned-arena mode"
1588    )]
1589    fn solve_expect_unsat_with_decision_ranges_gated_ws_inner(
1590        &self,
1591        ws: &mut GpuCdclWorkspace,
1592        cnf: &GpuCnf,
1593        compile_needed: &TrackedCudaSlice<u32>,
1594        decision_base_limit: &TrackedCudaSlice<u32>,
1595        decision_extra_base: &TrackedCudaSlice<u32>,
1596        decision_extra_count: &TrackedCudaSlice<u32>,
1597        import_existing_learned: bool,
1598    ) -> Result<()> {
1599        #[cfg(debug_assertions)]
1600        let trace = resolve_bool(None, "XLOG_CDCL_TRACE", false)?;
1601        #[cfg(debug_assertions)]
1602        let t0 = std::time::Instant::now();
1603
1604        self.launch_cdcl_with_workspace_gated(
1605            ws,
1606            cnf,
1607            compile_needed,
1608            decision_base_limit,
1609            decision_extra_base,
1610            decision_extra_count,
1611            import_existing_learned,
1612        )?;
1613        self.require_expected_status(
1614            &ws.out_status,
1615            &ws.out_error,
1616            SAT_STATUS_UNSAT,
1617            "GPU CDCL workspace UNSAT expectation",
1618        )?;
1619
1620        let device = self.provider.device().inner();
1621        let memory = self.provider.memory();
1622
1623        let assert_status_fn = device
1624            .get_func(SAT_MODULE, sat_kernels::SAT_ASSERT_STATUS)
1625            .ok_or_else(|| XlogError::Kernel("sat_assert_status kernel not found".to_string()))?;
1626        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1627        unsafe {
1628            assert_status_fn
1629                .clone()
1630                .launch(
1631                    LaunchConfig {
1632                        grid_dim: (1, 1, 1),
1633                        block_dim: (1, 1, 1),
1634                        shared_mem_bytes: 0,
1635                    },
1636                    (
1637                        compile_needed,
1638                        &ws.out_status,
1639                        &ws.out_error,
1640                        SAT_STATUS_UNSAT,
1641                    ),
1642                )
1643                .map_err(|e| {
1644                    XlogError::Kernel(format!("Failed to launch sat_assert_status: {}", e))
1645                })?;
1646        }
1647        // Fail-fast if the solver did not produce UNSAT.
1648        self.provider.device().synchronize()?;
1649        #[cfg(debug_assertions)]
1650        if trace {
1651            eprintln!("[xlog-solve] cdcl_ws(unsat) time: {:?}", t0.elapsed());
1652        }
1653
1654        let mut out_ok = memory.alloc::<i32>(1)?;
1655        self.provider
1656            .htod_launch_metadata_sync_copy_into(&[1i32], &mut out_ok)
1657            .map_err(|e| XlogError::Kernel(format!("Failed to init proof out_ok: {}", e)))?;
1658
1659        // sat_proof_check uses scratch buffers sized to `scratch_cap` per verifier block. To keep
1660        // proof checking fast on large instances, allocate multiple scratch regions and verify
1661        // learned clauses in parallel across blocks.
1662        let scratch_cap_u32 = cnf
1663            .var_cap
1664            .checked_add(1)
1665            .ok_or_else(|| XlogError::Kernel("Proof scratch capacity overflow".to_string()))?;
1666        let scratch_cap = scratch_cap_u32 as usize;
1667
1668        let mut proof_blocks: usize = 1;
1669        let mut scratch_a = None;
1670        let mut scratch_b = None;
1671        let mut scratch_map = None;
1672        let mut last_alloc_err: Option<XlogError> = None;
1673        for blocks in [512usize, 256, 128, 64, 32, 16, 8, 4, 2, 1] {
1674            let len = match scratch_cap.checked_mul(blocks) {
1675                Some(v) => v,
1676                None => {
1677                    last_alloc_err = Some(XlogError::Kernel(
1678                        "Proof scratch allocation length overflow".to_string(),
1679                    ));
1680                    continue;
1681                }
1682            };
1683
1684            let a = match memory.alloc::<i32>(len) {
1685                Ok(buf) => buf,
1686                Err(e) => {
1687                    last_alloc_err = Some(e);
1688                    continue;
1689                }
1690            };
1691            let b = match memory.alloc::<i32>(len) {
1692                Ok(buf) => buf,
1693                Err(e) => {
1694                    last_alloc_err = Some(e);
1695                    // Drop `a` before retrying with a smaller configuration.
1696                    drop(a);
1697                    continue;
1698                }
1699            };
1700            let m = match memory.alloc::<u32>(len) {
1701                Ok(buf) => buf,
1702                Err(e) => {
1703                    last_alloc_err = Some(e);
1704                    drop(a);
1705                    drop(b);
1706                    continue;
1707                }
1708            };
1709
1710            proof_blocks = blocks;
1711            scratch_a = Some(a);
1712            scratch_b = Some(b);
1713            scratch_map = Some(m);
1714            break;
1715        }
1716        let scratch_a = scratch_a.ok_or_else(|| {
1717            last_alloc_err.unwrap_or_else(|| {
1718                XlogError::Kernel("Failed to allocate proof scratch buffers".to_string())
1719            })
1720        })?;
1721        let scratch_b = scratch_b
1722            .ok_or_else(|| XlogError::Kernel("Missing proof scratch buffer".to_string()))?;
1723        let mut scratch_map = scratch_map
1724            .ok_or_else(|| XlogError::Kernel("Missing proof scratch map buffer".to_string()))?;
1725        device
1726            .memset_zeros(&mut scratch_map)
1727            .map_err(|e| XlogError::Kernel(format!("Failed to zero proof scratch map: {}", e)))?;
1728        #[cfg(debug_assertions)]
1729        if trace {
1730            eprintln!("[xlog-solve] proof_check_ws blocks: {}", proof_blocks);
1731        }
1732        #[cfg(debug_assertions)]
1733        let t_mark = std::time::Instant::now();
1734
1735        let needed_cap_u32 = self.config.max_learned_clauses;
1736        let needed_cap = needed_cap_u32 as usize;
1737        let mut needed = memory.alloc::<u8>(needed_cap)?;
1738        device
1739            .memset_zeros(&mut needed)
1740            .map_err(|e| XlogError::Kernel(format!("Failed to zero proof needed mask: {}", e)))?;
1741
1742        let mark_needed_fn = device
1743            .get_func(SAT_MODULE, sat_kernels::SAT_PROOF_MARK_NEEDED)
1744            .ok_or_else(|| {
1745                XlogError::Kernel("sat_proof_mark_needed kernel not found".to_string())
1746            })?;
1747        let mut mark_params: Vec<*mut c_void> = vec![
1748            compile_needed.as_kernel_param(),
1749            (&cnf.num_clauses).as_kernel_param(),
1750            (&ws.out_learned_count).as_kernel_param(),
1751            (&ws.proof_offsets).as_kernel_param(),
1752            (&ws.proof_data).as_kernel_param(),
1753            needed_cap_u32.as_kernel_param(),
1754            (&needed).as_kernel_param(),
1755        ];
1756        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1757        unsafe {
1758            mark_needed_fn
1759                .clone()
1760                .launch(
1761                    LaunchConfig {
1762                        grid_dim: (1, 1, 1),
1763                        block_dim: (1, 1, 1),
1764                        shared_mem_bytes: 0,
1765                    },
1766                    &mut mark_params,
1767                )
1768                .map_err(|e| {
1769                    XlogError::Kernel(format!("Failed to launch sat_proof_mark_needed: {}", e))
1770                })?;
1771        }
1772        self.provider.device().synchronize()?;
1773        #[cfg(debug_assertions)]
1774        if trace {
1775            eprintln!(
1776                "[xlog-solve] proof_mark_needed_ws time: {:?}",
1777                t_mark.elapsed()
1778            );
1779        }
1780
1781        let proof_fn = device
1782            .get_func(SAT_MODULE, sat_kernels::SAT_PROOF_CHECK)
1783            .ok_or_else(|| XlogError::Kernel("sat_proof_check kernel not found".to_string()))?;
1784        #[cfg(debug_assertions)]
1785        let t_proof = std::time::Instant::now();
1786        let proof_blocks_u32 = u32::try_from(proof_blocks)
1787            .map_err(|_| XlogError::Kernel("Proof check grid dim exceeds u32::MAX".to_string()))?;
1788        let mut proof_params: Vec<*mut c_void> = vec![
1789            compile_needed.as_kernel_param(),
1790            (&cnf.clause_offsets).as_kernel_param(),
1791            (&cnf.literals).as_kernel_param(),
1792            (&cnf.num_clauses).as_kernel_param(),
1793            (&ws.learned_offsets).as_kernel_param(),
1794            (&ws.learned_lits).as_kernel_param(),
1795            (&ws.out_learned_count).as_kernel_param(),
1796            (&ws.proof_offsets).as_kernel_param(),
1797            (&ws.proof_data).as_kernel_param(),
1798            (&needed).as_kernel_param(),
1799            needed_cap_u32.as_kernel_param(),
1800            (&scratch_a).as_kernel_param(),
1801            (&scratch_b).as_kernel_param(),
1802            (&scratch_map).as_kernel_param(),
1803            scratch_cap_u32.as_kernel_param(),
1804            (&out_ok).as_kernel_param(),
1805        ];
1806        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1807        unsafe {
1808            proof_fn
1809                .clone()
1810                .launch(
1811                    LaunchConfig {
1812                        grid_dim: (proof_blocks_u32, 1, 1),
1813                        block_dim: (128, 1, 1),
1814                        shared_mem_bytes: 0,
1815                    },
1816                    &mut proof_params,
1817                )
1818                .map_err(|e| {
1819                    XlogError::Kernel(format!("Failed to launch SAT proof check: {}", e))
1820                })?;
1821        }
1822
1823        let assert_ok_fn = device
1824            .get_func(SAT_MODULE, sat_kernels::SAT_ASSERT_OK)
1825            .ok_or_else(|| XlogError::Kernel("sat_assert_ok kernel not found".to_string()))?;
1826        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
1827        unsafe {
1828            assert_ok_fn
1829                .clone()
1830                .launch(
1831                    LaunchConfig {
1832                        grid_dim: (1, 1, 1),
1833                        block_dim: (1, 1, 1),
1834                        shared_mem_bytes: 0,
1835                    },
1836                    (compile_needed, &out_ok),
1837                )
1838                .map_err(|e| XlogError::Kernel(format!("Failed to launch sat_assert_ok: {}", e)))?;
1839        }
1840        self.provider.device().synchronize()?;
1841        #[cfg(debug_assertions)]
1842        if trace {
1843            eprintln!("[xlog-solve] proof_check_ws time: {:?}", t_proof.elapsed());
1844            eprintln!(
1845                "[xlog-solve] cdcl_ws(unsat)+proof_check time: {:?}",
1846                t0.elapsed()
1847            );
1848        }
1849
1850        Ok(())
1851    }
1852}