1mod buffers;
16mod evidence;
17mod resident;
18mod results;
19
20pub use evidence::{EvidenceForcing, ForceabilityReason};
21pub use resident::{
22 compile_resident_plan, McNoHostStats, McResidentResult, ResidentPlan, ResidentRejectKind,
23 ResidentRejection,
24};
25
26use std::collections::BTreeMap;
27#[cfg(feature = "host-io")]
28use std::collections::{HashMap, HashSet};
29use std::sync::Arc;
30
31#[cfg(feature = "host-io")]
32use cudarc::driver::DeviceSlice;
33#[cfg(feature = "host-io")]
34use xlog_core::Schema;
35use xlog_core::{MemoryBudget, Result, XlogError};
36use xlog_cuda::memory::TrackedCudaSlice;
37use xlog_cuda::{CudaDevice, CudaKernelProvider, GpuMemoryManager};
38#[cfg(feature = "host-io")]
39use xlog_logic::ast::{BodyLiteral, Rule};
40use xlog_logic::ast::{Directives, Evidence, ProbMethod, ProbQuery, Program};
41
42use crate::exact::GpuConfig;
43#[cfg(feature = "host-io")]
44use crate::provenance::Value;
45use crate::provenance::{
46 atom_key_from_ground_atom, canonicalize_probabilistic_program,
47 presentation_atom_from_canonical, GroundAtom,
48};
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum McSamplingMethod {
53 Rejection,
55 EvidenceClamping,
57}
58
59impl McSamplingMethod {
60 pub fn as_str(self) -> &'static str {
61 match self {
62 McSamplingMethod::Rejection => "rejection",
63 McSamplingMethod::EvidenceClamping => "evidence_clamping",
64 }
65 }
66}
67
68impl From<ProbMethod> for McSamplingMethod {
69 fn from(value: ProbMethod) -> Self {
70 match value {
71 ProbMethod::Rejection => Self::Rejection,
72 ProbMethod::EvidenceClamping => Self::EvidenceClamping,
73 }
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum McCountStrategy {
84 QueriesAndEvidence,
86 QueriesOnly,
88}
89
90impl McCountStrategy {
91 pub fn from_method(method: McSamplingMethod) -> Self {
93 match method {
94 McSamplingMethod::Rejection => Self::QueriesAndEvidence,
95 McSamplingMethod::EvidenceClamping => Self::QueriesOnly,
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
103pub struct McTimingBreakdown {
104 pub sampler_us: u64,
105 pub sample_reset_us: u64,
106 pub sample_build_us: u64,
107 pub eval_us: u64,
108 pub count_us: u64,
109}
110
111impl McTimingBreakdown {
112 pub fn total_us(&self) -> u64 {
113 self.sampler_us
114 .saturating_add(self.sample_reset_us)
115 .saturating_add(self.sample_build_us)
116 .saturating_add(self.eval_us)
117 .saturating_add(self.count_us)
118 }
119}
120
121pub const NONMONOTONE_SEMANTICS: &str = "Synchronous iteration per SCC; if a fixpoint is reached, use it; if a cycle is detected, use the intersection of all states in the cycle (skeptical tuples only); if the iteration budget is exceeded, use the intersection across all visited states (conservative).";
123
124#[derive(Debug, Clone)]
125#[non_exhaustive]
126pub struct McEvalConfig {
131 pub samples: usize,
133 pub seed: u64,
135 pub confidence: f64,
137 pub max_nonmonotone_iterations: usize,
139 pub sampling_method: Option<McSamplingMethod>,
141 pub allow_cpu_oracle_fallback: bool,
147}
148
149impl Default for McEvalConfig {
150 fn default() -> Self {
151 Self {
152 samples: 10000,
153 seed: 0,
154 confidence: 0.95,
155 max_nonmonotone_iterations: 1024,
156 sampling_method: None,
157 allow_cpu_oracle_fallback: false,
158 }
159 }
160}
161
162impl McEvalConfig {
163 pub fn from_directives(directives: &Directives) -> Result<Self> {
164 let mut cfg = Self::default();
165 if let Some(samples) = directives.prob_samples {
166 cfg.samples = samples;
167 }
168 if let Some(seed) = directives.prob_seed {
169 cfg.seed = seed;
170 }
171 if let Some(confidence) = directives.prob_confidence {
172 cfg.confidence = confidence;
173 }
174 if let Some(iterations) = directives.prob_max_nonmonotone_iterations {
175 cfg.max_nonmonotone_iterations = iterations;
176 }
177 cfg.sampling_method = directives.prob_method.map(McSamplingMethod::from);
178 cfg.validate()?;
179 Ok(cfg)
180 }
181
182 pub fn validate(&self) -> Result<()> {
183 if self.samples == 0 {
184 return Err(XlogError::Compilation(
185 "MC inference requires samples > 0".to_string(),
186 ));
187 }
188 if !(0.0 < self.confidence && self.confidence < 1.0) || self.confidence.is_nan() {
189 return Err(XlogError::Compilation(format!(
190 "MC inference requires 0 < confidence < 1, got {}",
191 self.confidence
192 )));
193 }
194 if self.max_nonmonotone_iterations == 0 {
195 return Err(XlogError::Compilation(
196 "MC inference requires max_nonmonotone_iterations > 0".to_string(),
197 ));
198 }
199 Ok(())
200 }
201}
202
203#[derive(Debug, Clone)]
204pub struct McQueryEstimate {
205 pub atom: GroundAtom,
206 pub prob: f64,
207 pub log_prob: f64,
208 pub stderr: f64,
209 pub ci_low: f64,
210 pub ci_high: f64,
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub enum McEngine {
216 GpuResident,
218 CpuOracle,
222}
223
224impl McEngine {
225 pub fn as_str(&self) -> &'static str {
227 match self {
228 McEngine::GpuResident => "gpu-resident",
229 McEngine::CpuOracle => "cpu-oracle",
230 }
231 }
232}
233
234#[derive(Debug, Clone)]
235pub struct McResult {
236 pub total_samples: usize,
237 pub evidence_samples: usize,
238 pub seed: u64,
239 pub confidence: f64,
240 pub query_estimates: Vec<McQueryEstimate>,
241 pub nonmonotone_sccs: usize,
242 pub nonmonotone_cycles: usize,
243 pub nonmonotone_iteration_limit_hits: usize,
244 pub sampling_method: McSamplingMethod,
245 pub engine: McEngine,
249}
250
251#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
263pub struct McHotLoopTransfers {
264 pub htod_calls: u64,
266 pub dtoh_calls: u64,
268 pub htod_bytes: u64,
270 pub dtoh_bytes: u64,
272}
273
274impl McHotLoopTransfers {
275 pub fn is_zero(&self) -> bool {
277 self.htod_calls == 0 && self.dtoh_calls == 0
278 }
279}
280
281pub struct McDeviceResult {
283 pub query_counts: TrackedCudaSlice<u32>,
284 pub evidence_count: TrackedCudaSlice<u32>,
285 pub total_samples: usize,
286 pub seed: u64,
287 pub confidence: f64,
288 pub nonmonotone_sccs: usize,
289 pub nonmonotone_cycles: usize,
290 pub nonmonotone_iteration_limit_hits: usize,
291 pub sampling_method: McSamplingMethod,
292 pub hot_loop_transfers: McHotLoopTransfers,
296 pub no_host: McNoHostStats,
298}
299
300#[derive(Debug, Clone)]
301pub(super) struct ProbFactSpec {
302 pub(super) var_idx: usize,
303 pub(super) atom: GroundAtom,
304}
305
306#[derive(Debug, Clone)]
307pub(super) struct AdSpec {
308 pub(super) decision_vars: Vec<usize>,
309 pub(super) choices: Vec<GroundAtom>,
310 #[cfg_attr(not(feature = "host-io"), allow(dead_code))]
311 pub(super) has_none: bool,
312}
313
314#[cfg(feature = "host-io")]
315#[derive(Debug, Clone, Default, PartialEq, Eq)]
316pub(super) struct Relation {
317 pub(super) tuples: HashSet<Vec<Value>>,
318}
319
320#[cfg(feature = "host-io")]
321impl Relation {
322 pub(super) fn insert_tuple(&mut self, tuple: Vec<Value>) {
323 self.tuples.insert(tuple);
324 }
325
326 pub(super) fn contains(&self, tuple: &[Value]) -> bool {
327 self.tuples.contains(tuple)
328 }
329
330 pub(super) fn is_empty(&self) -> bool {
331 self.tuples.is_empty()
332 }
333}
334
335#[cfg(feature = "host-io")]
336#[derive(Debug, Clone)]
337pub(super) enum SccKind {
338 MonotoneNonRecursive,
339 MonotoneRecursive,
340 NonMonotone,
341}
342
343#[cfg(feature = "host-io")]
344#[derive(Debug, Clone)]
345pub(super) struct SccPlan {
346 pub(super) predicates: Vec<String>,
347 pub(super) rules: Vec<Rule>,
348 pub(super) kind: SccKind,
349}
350
351#[cfg_attr(not(feature = "host-io"), allow(dead_code))]
352#[derive(Debug, Clone, Default)]
353pub(super) struct EvalStats {
354 pub(super) nonmonotone_sccs: usize,
355 pub(super) nonmonotone_cycles: usize,
356 pub(super) nonmonotone_iteration_limit_hits: usize,
357}
358
359#[derive(Clone)]
360pub struct McProgram {
361 pub(super) gpu_config: GpuConfig,
362 pub(super) program: Program,
363 #[cfg(feature = "host-io")]
364 pub(super) base_store: HashMap<String, Relation>,
365 #[cfg(feature = "host-io")]
366 pub(super) scc_plans: Vec<SccPlan>,
367 #[cfg(feature = "host-io")]
368 pub(super) arithmetic_schemas: HashMap<String, Schema>,
369 pub(super) queries: Vec<GroundAtom>,
370 #[cfg_attr(not(feature = "host-io"), allow(dead_code))]
371 query_presentations: Vec<GroundAtom>,
372 pub(super) evidence: Vec<(GroundAtom, bool)>,
373 pub(super) bernoulli_probs: Vec<f32>,
374 pub(super) prob_facts: Vec<ProbFactSpec>,
375 pub(super) annotated_disjunctions: Vec<AdSpec>,
376}
377
378impl McProgram {
379 pub fn compile_source(source: &str) -> Result<Self> {
380 let program = xlog_logic::parse_program(source)?;
381 Self::compile_from_program(&program, GpuConfig::default())
382 }
383
384 pub fn compile_source_with_gpu(source: &str, config: GpuConfig) -> Result<Self> {
385 let program = xlog_logic::parse_program(source)?;
386 Self::compile_from_program(&program, config)
387 }
388
389 pub fn compile_from_program(program: &Program, config: GpuConfig) -> Result<Self> {
394 let mut compiled = Self::compile_program(program)?;
395 compiled.gpu_config = config;
396 Ok(compiled)
397 }
398
399 pub fn num_vars(&self) -> usize {
400 self.bernoulli_probs.len()
401 }
402
403 #[cfg(feature = "host-io")]
418 pub fn evaluate(&self, cfg: McEvalConfig) -> Result<McResult> {
419 let provider = Arc::new(self.provider()?);
420 let cfg_clone = cfg.clone();
421 let device_result =
422 match self.evaluate_gpu_device_with_provider(cfg_clone, provider.clone()) {
423 Ok(result) => result,
424 Err(XlogError::Compilation(message))
425 if message.starts_with("resident MC engine rejected program") =>
426 {
427 if cfg.allow_cpu_oracle_fallback {
431 return self.evaluate_cpu(cfg);
432 }
433 return Err(XlogError::Compilation(format!(
434 "{message}; MC fail-closed: set \
435 McEvalConfig::allow_cpu_oracle_fallback to explicitly \
436 run the labeled CPU oracle instead"
437 )));
438 }
439 Err(err) => return Err(err),
440 };
441
442 let mut host_counts = vec![0u32; device_result.query_counts.len()];
443 if !host_counts.is_empty() {
444 provider
445 .device()
446 .inner()
447 .dtoh_sync_copy_into(&device_result.query_counts, &mut host_counts)
448 .map_err(|e| {
449 XlogError::Kernel(format!("Failed to download MC query counts: {}", e))
450 })?;
451 }
452
453 let mut host_evidence = [0u32];
454 provider
455 .device()
456 .inner()
457 .dtoh_sync_copy_into(&device_result.evidence_count, &mut host_evidence)
458 .map_err(|e| {
459 XlogError::Kernel(format!("Failed to download MC evidence count: {}", e))
460 })?;
461
462 let evidence_samples = if self.evidence.is_empty() {
463 cfg.samples
464 } else {
465 host_evidence[0] as usize
466 };
467
468 if device_result.sampling_method != McSamplingMethod::EvidenceClamping
469 && !self.evidence.is_empty()
470 && evidence_samples == 0
471 {
472 return Err(XlogError::Execution(format!(
473 "MC inference error: evidence was never satisfied across {} samples (seed={})",
474 cfg.samples, cfg.seed
475 )));
476 }
477
478 let z = results::normal_quantile(0.5 + cfg.confidence / 2.0);
479 let mut query_estimates: Vec<McQueryEstimate> = Vec::with_capacity(self.queries.len());
480 for (i, atom) in self.query_presentations.iter().enumerate() {
481 let k = host_counts.get(i).copied().unwrap_or(0) as usize;
482 let (p, stderr, ci_low, ci_high) = results::binomial_estimate(k, evidence_samples, z);
483 let log_prob = if p == 0.0 { f64::NEG_INFINITY } else { p.ln() };
484
485 query_estimates.push(McQueryEstimate {
486 atom: atom.clone(),
487 prob: p,
488 log_prob,
489 stderr,
490 ci_low,
491 ci_high,
492 });
493 }
494
495 Ok(McResult {
496 total_samples: cfg.samples,
497 evidence_samples,
498 seed: cfg.seed,
499 confidence: cfg.confidence,
500 query_estimates,
501 nonmonotone_sccs: device_result.nonmonotone_sccs,
502 nonmonotone_cycles: device_result.nonmonotone_cycles,
503 engine: McEngine::GpuResident,
504 nonmonotone_iteration_limit_hits: device_result.nonmonotone_iteration_limit_hits,
505 sampling_method: device_result.sampling_method,
506 })
507 }
508
509 #[cfg(feature = "host-io")]
520 pub fn evaluate_cpu(&self, cfg: McEvalConfig) -> Result<McResult> {
521 if cfg.samples == 0 {
522 return Err(XlogError::Execution(
523 "MC inference requires samples > 0".to_string(),
524 ));
525 }
526 if !(0.0 < cfg.confidence && cfg.confidence < 1.0) || cfg.confidence.is_nan() {
527 return Err(XlogError::Execution(format!(
528 "MC inference requires 0 < confidence < 1, got {}",
529 cfg.confidence
530 )));
531 }
532 if cfg.max_nonmonotone_iterations == 0 {
533 return Err(XlogError::Execution(
534 "MC inference requires max_nonmonotone_iterations > 0".to_string(),
535 ));
536 }
537
538 let (method, forcing) = self.resolve_sampling_method(cfg.sampling_method)?;
539 let is_clamped = method == McSamplingMethod::EvidenceClamping;
540
541 let mut n_evidence: usize = 0;
542 let mut n_query_true: Vec<usize> = vec![0; self.queries.len()];
543 let mut stats = EvalStats::default();
544
545 let num_vars = self.bernoulli_probs.len();
546 let samples_matrix: Vec<u8> = if num_vars == 0 {
547 Vec::new()
548 } else {
549 let total = num_vars
550 .checked_mul(cfg.samples)
551 .ok_or_else(|| XlogError::Execution("MC sample matrix overflow".to_string()))?;
552 let provider = Arc::new(self.provider()?);
553
554 let mut d_force_mask = provider.memory().alloc::<u8>(num_vars.max(1))?;
556 let mut d_forced_value = provider.memory().alloc::<u8>(num_vars.max(1))?;
557 if is_clamped {
558 provider
559 .htod_sync_copy_into_tracked(&forcing.force_mask, &mut d_force_mask)
560 .map_err(|e| {
561 XlogError::Kernel(format!("Failed to upload force_mask: {}", e))
562 })?;
563 provider
564 .htod_sync_copy_into_tracked(&forcing.forced_value, &mut d_forced_value)
565 .map_err(|e| {
566 XlogError::Kernel(format!("Failed to upload forced_value: {}", e))
567 })?;
568 } else {
569 provider
570 .device()
571 .inner()
572 .memset_zeros(&mut d_force_mask)
573 .map_err(|e| XlogError::Kernel(format!("Failed to zero force_mask: {}", e)))?;
574 provider
575 .device()
576 .inner()
577 .memset_zeros(&mut d_forced_value)
578 .map_err(|e| {
579 XlogError::Kernel(format!("Failed to zero forced_value: {}", e))
580 })?;
581 }
582
583 let samples_device = provider.sample_bernoulli_matrix_device(
584 &self.bernoulli_probs,
585 cfg.samples,
586 cfg.seed,
587 &d_force_mask.slice(..),
588 &d_forced_value.slice(..),
589 )?;
590 let mut host = vec![0u8; total];
591 if !host.is_empty() {
592 provider
593 .device()
594 .inner()
595 .dtoh_sync_copy_into(&samples_device, &mut host)
596 .map_err(|e| {
597 XlogError::Kernel(format!("Failed to download MC samples: {}", e))
598 })?;
599 }
600 host
601 };
602
603 for sample_idx in 0..cfg.samples {
604 let sample_bits = if num_vars == 0 {
605 &[][..]
606 } else {
607 let start = sample_idx * num_vars;
608 let end = start + num_vars;
609 &samples_matrix[start..end]
610 };
611
612 let mut store = self.base_store.clone();
613 self.apply_sample_facts(&mut store, sample_bits)?;
614
615 let sample_stats = results::evaluate_program_inplace(
616 &self.scc_plans,
617 &mut store,
618 cfg.max_nonmonotone_iterations,
619 &self.arithmetic_schemas,
620 )?;
621 stats.nonmonotone_sccs += sample_stats.nonmonotone_sccs;
622 stats.nonmonotone_cycles += sample_stats.nonmonotone_cycles;
623 stats.nonmonotone_iteration_limit_hits += sample_stats.nonmonotone_iteration_limit_hits;
624
625 if !is_clamped && !results::evidence_satisfied(&store, &self.evidence) {
627 continue;
628 }
629
630 n_evidence += 1;
631 for (i, q) in self.queries.iter().enumerate() {
632 if results::atom_holds(&store, q) {
633 n_query_true[i] += 1;
634 }
635 }
636 }
637
638 if !is_clamped && !self.evidence.is_empty() && n_evidence == 0 {
639 return Err(XlogError::Execution(format!(
640 "MC inference error: evidence was never satisfied across {} samples (seed={})",
641 cfg.samples, cfg.seed
642 )));
643 }
644
645 let denom = if self.evidence.is_empty() || is_clamped {
647 cfg.samples
648 } else {
649 n_evidence
650 };
651
652 let z = results::normal_quantile(0.5 + cfg.confidence / 2.0);
653
654 let mut query_estimates: Vec<McQueryEstimate> = Vec::with_capacity(self.queries.len());
655 for (i, atom) in self.query_presentations.iter().enumerate() {
656 let k = n_query_true[i];
657 let (p, stderr, ci_low, ci_high) = results::binomial_estimate(k, denom, z);
658 let log_prob = if p == 0.0 { f64::NEG_INFINITY } else { p.ln() };
659
660 query_estimates.push(McQueryEstimate {
661 atom: atom.clone(),
662 prob: p,
663 log_prob,
664 stderr,
665 ci_low,
666 ci_high,
667 });
668 }
669
670 Ok(McResult {
671 total_samples: cfg.samples,
672 evidence_samples: denom,
673 seed: cfg.seed,
674 confidence: cfg.confidence,
675 query_estimates,
676 nonmonotone_sccs: stats.nonmonotone_sccs,
677 nonmonotone_cycles: stats.nonmonotone_cycles,
678 nonmonotone_iteration_limit_hits: stats.nonmonotone_iteration_limit_hits,
679 sampling_method: method,
680 engine: McEngine::CpuOracle,
681 })
682 }
683
684 #[cfg(feature = "host-io")]
690 pub fn evaluate_gpu(&self, cfg: McEvalConfig) -> Result<McResult> {
691 self.evaluate(cfg)
692 }
693
694 pub fn evaluate_gpu_device(&self, cfg: McEvalConfig) -> Result<McDeviceResult> {
703 let provider = Arc::new(self.provider()?);
704 self.evaluate_gpu_device_with_provider(cfg, provider)
705 }
706
707 pub fn evaluate_gpu_device_with_provider(
718 &self,
719 cfg: McEvalConfig,
720 provider: Arc<CudaKernelProvider>,
721 ) -> Result<McDeviceResult> {
722 let r = self.evaluate_resident_with_provider(cfg, provider)?;
728 let no_host = r.no_host;
729 Ok(McDeviceResult {
730 query_counts: r.query_counts,
731 evidence_count: r.evidence_count,
732 total_samples: r.total_samples,
733 seed: r.seed,
734 confidence: r.confidence,
735 nonmonotone_sccs: 0,
739 nonmonotone_cycles: 0,
740 nonmonotone_iteration_limit_hits: 0,
741 sampling_method: r.sampling_method,
742 hot_loop_transfers: McHotLoopTransfers {
746 htod_calls: no_host.tracked_htod_calls,
747 dtoh_calls: no_host.tracked_dtoh_calls,
748 htod_bytes: 0,
749 dtoh_bytes: 0,
750 },
751 no_host,
752 })
753 }
754
755 fn compile_program(program: &Program) -> Result<Self> {
756 let source_program = program;
757 let arithmetic_schemas = crate::provenance::arithmetic_schemas(program)?;
758 let canonical_program = canonicalize_probabilistic_program(program, &arithmetic_schemas)?;
759 let program = &canonical_program;
760
761 let mut queries: Vec<GroundAtom> = Vec::new();
762 let mut query_presentations: Vec<GroundAtom> = Vec::new();
763 for (ProbQuery { atom }, ProbQuery { atom: source_atom }) in program
764 .prob_queries
765 .iter()
766 .zip(&source_program.prob_queries)
767 {
768 queries.push(atom_key_from_ground_atom(atom)?);
769 query_presentations.push(presentation_atom_from_canonical(
770 source_atom,
771 atom,
772 &arithmetic_schemas,
773 )?);
774 }
775
776 let mut evidence: Vec<(GroundAtom, bool)> = Vec::new();
777 let mut evidence_values: BTreeMap<GroundAtom, bool> = BTreeMap::new();
778 for Evidence { atom, value } in &program.evidence {
779 let atom = atom_key_from_ground_atom(atom)?;
780 match evidence_values.entry(atom.clone()) {
781 std::collections::btree_map::Entry::Occupied(previous)
782 if previous.get() != value =>
783 {
784 return Err(XlogError::Execution(format!(
785 "Monte Carlo inference error: conflicting evidence for {}",
786 atom.predicate
787 )));
788 }
789 std::collections::btree_map::Entry::Occupied(_) => continue,
790 std::collections::btree_map::Entry::Vacant(entry) => {
791 entry.insert(*value);
792 evidence.push((atom, *value));
793 }
794 }
795 }
796
797 let mut prob_facts = program.prob_facts.clone();
798 buffers::extend_prob_facts_with_coin(program, &mut prob_facts)?;
799 let (bernoulli_probs, prob_facts, annotated_disjunctions) =
800 buffers::compile_sampling_plan(&prob_facts, &program.annotated_disjunctions)?;
801
802 #[cfg(feature = "host-io")]
803 let mut base_store: HashMap<String, Relation> = HashMap::new();
804
805 #[cfg(feature = "host-io")]
807 {
808 for fact in program.facts() {
809 let atom = atom_key_from_ground_atom(&fact.head)?;
810 base_store
811 .entry(atom.predicate.clone())
812 .or_default()
813 .insert_tuple(atom.args);
814 }
815 }
816
817 #[cfg(feature = "host-io")]
820 {
821 let mut referenced: HashSet<String> = HashSet::new();
822 for rule in &program.rules {
823 referenced.insert(rule.head.predicate.clone());
824 for lit in &rule.body {
825 match lit {
826 BodyLiteral::Positive(a) | BodyLiteral::Negated(a) => {
827 referenced.insert(a.predicate.clone());
828 }
829 BodyLiteral::Epistemic(lit) => {
830 referenced.insert(lit.atom.predicate.clone());
831 }
832 BodyLiteral::Comparison(_)
833 | BodyLiteral::IsExpr(_)
834 | BodyLiteral::Univ(_) => {}
835 }
836 }
837 }
838 for pf in &program.prob_facts {
839 referenced.insert(pf.atom.predicate.clone());
840 }
841 for ad in &program.annotated_disjunctions {
842 for pf in &ad.choices {
843 referenced.insert(pf.atom.predicate.clone());
844 }
845 }
846 for q in &queries {
847 referenced.insert(q.predicate.clone());
848 }
849 for (e, _) in &evidence {
850 referenced.insert(e.predicate.clone());
851 }
852 for pred in referenced {
853 base_store.entry(pred).or_default();
854 }
855 }
856
857 #[cfg(feature = "host-io")]
858 let scc_plans = results::build_scc_plans(program)?;
859 Ok(Self {
860 gpu_config: GpuConfig::default(),
861 program: program.clone(),
862 #[cfg(feature = "host-io")]
863 base_store,
864 #[cfg(feature = "host-io")]
865 scc_plans,
866 #[cfg(feature = "host-io")]
867 arithmetic_schemas,
868 queries,
869 query_presentations,
870 evidence,
871 bernoulli_probs,
872 prob_facts,
873 annotated_disjunctions,
874 })
875 }
876
877 fn provider(&self) -> Result<CudaKernelProvider> {
878 if self.gpu_config.memory_bytes == 0 {
879 return Err(XlogError::Kernel(
880 "GPU memory budget must be non-zero".to_string(),
881 ));
882 }
883
884 let device = Arc::new(CudaDevice::new(self.gpu_config.device_ordinal)?);
885 let memory = Arc::new(GpuMemoryManager::new(
886 device.clone(),
887 MemoryBudget::with_limit(self.gpu_config.memory_bytes),
888 ));
889 CudaKernelProvider::new(device, memory)
890 }
891}