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 xlog_core::Schema;
33use xlog_core::{MemoryBudget, Result, XlogError};
34use xlog_cuda::memory::TrackedCudaSlice;
35use xlog_cuda::CudaKernelProvider;
36#[cfg(feature = "host-io")]
37use xlog_logic::ast::{BodyLiteral, Rule};
38use xlog_logic::ast::{Directives, Evidence, ProbMethod, ProbQuery, Program};
39
40use crate::exact::GpuConfig;
41#[cfg(feature = "host-io")]
42use crate::provenance::presentation_atom_from_canonical;
43#[cfg(feature = "host-io")]
44use crate::provenance::Value;
45use crate::provenance::{
46 atom_key_from_ground_atom, canonicalize_probabilistic_program, GroundAtom,
47};
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum McSamplingMethod {
52 Rejection,
54 EvidenceClamping,
56}
57
58impl McSamplingMethod {
59 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,
106 pub sample_reset_us: u64,
108 pub sample_build_us: u64,
110 pub eval_us: u64,
112 pub count_us: u64,
114}
115
116impl McTimingBreakdown {
117 pub fn total_us(&self) -> u64 {
119 self.sampler_us
120 .saturating_add(self.sample_reset_us)
121 .saturating_add(self.sample_build_us)
122 .saturating_add(self.eval_us)
123 .saturating_add(self.count_us)
124 }
125}
126
127pub 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).";
129
130#[derive(Debug, Clone)]
131#[non_exhaustive]
132pub struct McEvalConfig {
137 pub samples: usize,
139 pub seed: u64,
141 pub confidence: f64,
143 pub max_nonmonotone_iterations: usize,
145 pub sampling_method: Option<McSamplingMethod>,
147 pub allow_cpu_oracle_fallback: bool,
153}
154
155#[derive(Debug, Clone, Default)]
159pub struct McEvalOverrides {
160 pub samples: Option<usize>,
162 pub seed: Option<u64>,
164 pub confidence: Option<f64>,
166 pub max_nonmonotone_iterations: Option<usize>,
168 pub sampling_method: Option<McSamplingMethod>,
170 pub allow_cpu_oracle_fallback: Option<bool>,
172}
173
174impl Default for McEvalConfig {
175 fn default() -> Self {
176 Self {
177 samples: 10000,
178 seed: 0,
179 confidence: 0.95,
180 max_nonmonotone_iterations: 1024,
181 sampling_method: None,
182 allow_cpu_oracle_fallback: false,
183 }
184 }
185}
186
187impl McEvalConfig {
188 pub fn from_directives(directives: &Directives) -> Result<Self> {
190 let mut cfg = Self::default();
191 if let Some(samples) = directives.prob_samples {
192 cfg.samples = samples;
193 }
194 if let Some(seed) = directives.prob_seed {
195 cfg.seed = seed;
196 }
197 if let Some(confidence) = directives.prob_confidence {
198 cfg.confidence = confidence;
199 }
200 if let Some(iterations) = directives.prob_max_nonmonotone_iterations {
201 cfg.max_nonmonotone_iterations = iterations;
202 }
203 cfg.sampling_method = directives.prob_method.map(McSamplingMethod::from);
204 cfg.validate()?;
205 Ok(cfg)
206 }
207
208 pub fn apply_overrides(&mut self, overrides: McEvalOverrides) -> Result<()> {
210 if let Some(samples) = overrides.samples {
211 self.samples = samples;
212 }
213 if let Some(seed) = overrides.seed {
214 self.seed = seed;
215 }
216 if let Some(confidence) = overrides.confidence {
217 self.confidence = confidence;
218 }
219 if let Some(iterations) = overrides.max_nonmonotone_iterations {
220 self.max_nonmonotone_iterations = iterations;
221 }
222 if let Some(method) = overrides.sampling_method {
223 self.sampling_method = Some(method);
224 }
225 if let Some(allow) = overrides.allow_cpu_oracle_fallback {
226 self.allow_cpu_oracle_fallback = allow;
227 }
228 self.validate()
229 }
230
231 pub fn validate(&self) -> Result<()> {
233 if self.samples == 0 {
234 return Err(XlogError::Compilation(
235 "MC inference requires samples > 0".to_string(),
236 ));
237 }
238 if !(0.0 < self.confidence && self.confidence < 1.0) || self.confidence.is_nan() {
239 return Err(XlogError::Compilation(format!(
240 "MC inference requires 0 < confidence < 1, got {}",
241 self.confidence
242 )));
243 }
244 if self.max_nonmonotone_iterations == 0 {
245 return Err(XlogError::Compilation(
246 "MC inference requires max_nonmonotone_iterations > 0".to_string(),
247 ));
248 }
249 Ok(())
250 }
251}
252
253#[derive(Debug, Clone)]
254pub struct McQueryEstimate {
256 pub atom: GroundAtom,
258 pub prob: f64,
260 pub log_prob: f64,
262 pub stderr: f64,
264 pub ci_low: f64,
266 pub ci_high: f64,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272pub enum McEngine {
273 GpuResident,
275 CpuOracle,
279}
280
281impl McEngine {
282 pub fn as_str(&self) -> &'static str {
284 match self {
285 McEngine::GpuResident => "gpu-resident",
286 McEngine::CpuOracle => "cpu-oracle",
287 }
288 }
289}
290
291#[derive(Debug, Clone)]
292pub struct McResult {
294 pub total_samples: usize,
296 pub evidence_samples: usize,
298 pub seed: u64,
300 pub confidence: f64,
302 pub query_estimates: Vec<McQueryEstimate>,
304 pub nonmonotone_sccs: usize,
306 pub nonmonotone_cycles: usize,
308 pub nonmonotone_iteration_limit_hits: usize,
310 pub sampling_method: McSamplingMethod,
312 pub engine: McEngine,
316}
317
318#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
330pub struct McHotLoopTransfers {
331 pub htod_calls: u64,
333 pub dtoh_calls: u64,
335 pub htod_bytes: u64,
337 pub dtoh_bytes: u64,
339}
340
341impl McHotLoopTransfers {
342 pub fn is_zero(&self) -> bool {
344 self.htod_calls == 0 && self.dtoh_calls == 0
345 }
346}
347
348pub struct McDeviceResult {
350 pub query_counts: TrackedCudaSlice<u32>,
352 pub evidence_count: TrackedCudaSlice<u32>,
354 pub total_samples: usize,
356 pub seed: u64,
358 pub confidence: f64,
360 pub nonmonotone_sccs: usize,
362 pub nonmonotone_cycles: usize,
364 pub nonmonotone_iteration_limit_hits: usize,
366 pub sampling_method: McSamplingMethod,
368 pub hot_loop_transfers: McHotLoopTransfers,
372 pub no_host: McNoHostStats,
374}
375
376#[derive(Debug, Clone)]
377pub(super) struct ProbFactSpec {
378 pub(super) var_idx: usize,
379 pub(super) atom: GroundAtom,
380}
381
382#[derive(Debug, Clone)]
383pub(super) struct AdSpec {
384 pub(super) decision_vars: Vec<usize>,
385 pub(super) choices: Vec<GroundAtom>,
386 #[cfg(feature = "host-io")]
387 pub(super) has_none: bool,
388}
389
390#[cfg(feature = "host-io")]
391#[derive(Debug, Clone, Default, PartialEq, Eq)]
392pub(super) struct Relation {
393 pub(super) tuples: HashSet<Vec<Value>>,
394}
395
396#[cfg(feature = "host-io")]
397impl Relation {
398 pub(super) fn insert_tuple(&mut self, tuple: Vec<Value>) {
399 self.tuples.insert(tuple);
400 }
401
402 pub(super) fn contains(&self, tuple: &[Value]) -> bool {
403 self.tuples.contains(tuple)
404 }
405
406 pub(super) fn is_empty(&self) -> bool {
407 self.tuples.is_empty()
408 }
409}
410
411#[cfg(feature = "host-io")]
412#[derive(Debug, Clone)]
413pub(super) enum SccKind {
414 MonotoneNonRecursive,
415 MonotoneRecursive,
416 NonMonotone,
417}
418
419#[cfg(feature = "host-io")]
420#[derive(Debug, Clone)]
421pub(super) struct SccPlan {
422 pub(super) predicates: Vec<String>,
423 pub(super) rules: Vec<Rule>,
424 pub(super) kind: SccKind,
425}
426
427#[cfg(feature = "host-io")]
428#[derive(Debug, Clone, Default)]
429pub(super) struct EvalStats {
430 pub(super) nonmonotone_sccs: usize,
431 pub(super) nonmonotone_cycles: usize,
432 pub(super) nonmonotone_iteration_limit_hits: usize,
433}
434
435#[derive(Clone)]
436pub struct McProgram {
438 pub(super) gpu_config: GpuConfig,
439 pub(super) program: Program,
440 #[cfg(feature = "host-io")]
441 pub(super) base_store: HashMap<String, Relation>,
442 #[cfg(feature = "host-io")]
443 pub(super) scc_plans: Vec<SccPlan>,
444 #[cfg(feature = "host-io")]
445 pub(super) arithmetic_schemas: HashMap<String, Schema>,
446 pub(super) queries: Vec<GroundAtom>,
447 #[cfg(feature = "host-io")]
448 query_presentations: Vec<GroundAtom>,
449 pub(super) evidence: Vec<(GroundAtom, bool)>,
450 pub(super) bernoulli_probs: Vec<f32>,
451 pub(super) prob_facts: Vec<ProbFactSpec>,
452 pub(super) annotated_disjunctions: Vec<AdSpec>,
453}
454
455impl McProgram {
456 pub fn compile_source(source: &str) -> Result<Self> {
458 let program = xlog_logic::parse_program(source)?;
459 Self::compile_from_program(&program, GpuConfig::default())
460 }
461
462 pub fn compile_source_with_gpu(source: &str, config: GpuConfig) -> Result<Self> {
464 let program = xlog_logic::parse_program(source)?;
465 Self::compile_from_program(&program, config)
466 }
467
468 pub fn compile_from_program(program: &Program, config: GpuConfig) -> Result<Self> {
473 let mut compiled = Self::compile_program(program)?;
474 compiled.gpu_config = config;
475 Ok(compiled)
476 }
477
478 pub fn num_vars(&self) -> usize {
480 self.bernoulli_probs.len()
481 }
482
483 #[cfg(feature = "host-io")]
498 pub fn evaluate(&self, cfg: McEvalConfig) -> Result<McResult> {
499 let provider = Arc::new(self.provider()?);
500 let cfg_clone = cfg.clone();
501 let device_result =
502 match self.evaluate_gpu_device_with_provider(cfg_clone, provider.clone()) {
503 Ok(result) => result,
504 Err(XlogError::Compilation(message))
505 if message.starts_with("resident MC engine rejected program") =>
506 {
507 if cfg.allow_cpu_oracle_fallback {
511 return self.evaluate_cpu(cfg);
512 }
513 return Err(XlogError::Compilation(format!(
514 "{message}; MC fail-closed: set \
515 McEvalConfig::allow_cpu_oracle_fallback to explicitly \
516 run the labeled CPU oracle instead"
517 )));
518 }
519 Err(err) => return Err(err),
520 };
521
522 let mut host_counts = vec![0u32; device_result.query_counts.len()];
523 if !host_counts.is_empty() {
524 provider
525 .device()
526 .inner()
527 .dtoh_sync_copy_into(&device_result.query_counts, &mut host_counts)
528 .map_err(|e| {
529 XlogError::Kernel(format!("Failed to download MC query counts: {}", e))
530 })?;
531 }
532
533 let mut host_evidence = [0u32];
534 provider
535 .device()
536 .inner()
537 .dtoh_sync_copy_into(&device_result.evidence_count, &mut host_evidence)
538 .map_err(|e| {
539 XlogError::Kernel(format!("Failed to download MC evidence count: {}", e))
540 })?;
541
542 let evidence_samples = if self.evidence.is_empty() {
543 cfg.samples
544 } else {
545 host_evidence[0] as usize
546 };
547
548 if device_result.sampling_method != McSamplingMethod::EvidenceClamping
549 && !self.evidence.is_empty()
550 && evidence_samples == 0
551 {
552 return Err(XlogError::Execution(format!(
553 "MC inference error: evidence was never satisfied across {} samples (seed={})",
554 cfg.samples, cfg.seed
555 )));
556 }
557
558 let z = results::normal_quantile(0.5 + cfg.confidence / 2.0);
559 let mut query_estimates: Vec<McQueryEstimate> = Vec::with_capacity(self.queries.len());
560 for (i, atom) in self.query_presentations.iter().enumerate() {
561 let k = host_counts.get(i).copied().unwrap_or(0) as usize;
562 let (p, stderr, ci_low, ci_high) = results::binomial_estimate(k, evidence_samples, z);
563 let log_prob = if p == 0.0 { f64::NEG_INFINITY } else { p.ln() };
564
565 query_estimates.push(McQueryEstimate {
566 atom: atom.clone(),
567 prob: p,
568 log_prob,
569 stderr,
570 ci_low,
571 ci_high,
572 });
573 }
574
575 Ok(McResult {
576 total_samples: cfg.samples,
577 evidence_samples,
578 seed: cfg.seed,
579 confidence: cfg.confidence,
580 query_estimates,
581 nonmonotone_sccs: device_result.nonmonotone_sccs,
582 nonmonotone_cycles: device_result.nonmonotone_cycles,
583 engine: McEngine::GpuResident,
584 nonmonotone_iteration_limit_hits: device_result.nonmonotone_iteration_limit_hits,
585 sampling_method: device_result.sampling_method,
586 })
587 }
588
589 #[cfg(feature = "host-io")]
600 pub fn evaluate_cpu(&self, cfg: McEvalConfig) -> Result<McResult> {
601 if cfg.samples == 0 {
602 return Err(XlogError::Execution(
603 "MC inference requires samples > 0".to_string(),
604 ));
605 }
606 if !(0.0 < cfg.confidence && cfg.confidence < 1.0) || cfg.confidence.is_nan() {
607 return Err(XlogError::Execution(format!(
608 "MC inference requires 0 < confidence < 1, got {}",
609 cfg.confidence
610 )));
611 }
612 if cfg.max_nonmonotone_iterations == 0 {
613 return Err(XlogError::Execution(
614 "MC inference requires max_nonmonotone_iterations > 0".to_string(),
615 ));
616 }
617
618 let (method, forcing) = self.resolve_sampling_method(cfg.sampling_method)?;
619 let is_clamped = method == McSamplingMethod::EvidenceClamping;
620
621 let mut n_evidence: usize = 0;
622 let mut n_query_true: Vec<usize> = vec![0; self.queries.len()];
623 let mut stats = EvalStats::default();
624
625 let num_vars = self.bernoulli_probs.len();
626 let samples_matrix: Vec<u8> = if num_vars == 0 {
627 Vec::new()
628 } else {
629 let total = num_vars
630 .checked_mul(cfg.samples)
631 .ok_or_else(|| XlogError::Execution("MC sample matrix overflow".to_string()))?;
632 let provider = Arc::new(self.provider()?);
633
634 let mut d_force_mask = provider.memory().alloc::<u8>(num_vars.max(1))?;
636 let mut d_forced_value = provider.memory().alloc::<u8>(num_vars.max(1))?;
637 if is_clamped {
638 provider
639 .htod_sync_copy_into_tracked(&forcing.force_mask, &mut d_force_mask)
640 .map_err(|e| {
641 XlogError::Kernel(format!("Failed to upload force_mask: {}", e))
642 })?;
643 provider
644 .htod_sync_copy_into_tracked(&forcing.forced_value, &mut d_forced_value)
645 .map_err(|e| {
646 XlogError::Kernel(format!("Failed to upload forced_value: {}", e))
647 })?;
648 } else {
649 provider
650 .device()
651 .inner()
652 .memset_zeros(&mut d_force_mask)
653 .map_err(|e| XlogError::Kernel(format!("Failed to zero force_mask: {}", e)))?;
654 provider
655 .device()
656 .inner()
657 .memset_zeros(&mut d_forced_value)
658 .map_err(|e| {
659 XlogError::Kernel(format!("Failed to zero forced_value: {}", e))
660 })?;
661 }
662
663 let samples_device = provider.sample_bernoulli_matrix_device(
664 &self.bernoulli_probs,
665 cfg.samples,
666 cfg.seed,
667 &d_force_mask.slice(..),
668 &d_forced_value.slice(..),
669 )?;
670 let mut host = vec![0u8; total];
671 if !host.is_empty() {
672 provider
673 .device()
674 .inner()
675 .dtoh_sync_copy_into(&samples_device, &mut host)
676 .map_err(|e| {
677 XlogError::Kernel(format!("Failed to download MC samples: {}", e))
678 })?;
679 }
680 host
681 };
682
683 for sample_idx in 0..cfg.samples {
684 let sample_bits = if num_vars == 0 {
685 &[][..]
686 } else {
687 let start = sample_idx * num_vars;
688 let end = start + num_vars;
689 &samples_matrix[start..end]
690 };
691
692 let mut store = self.base_store.clone();
693 self.apply_sample_facts(&mut store, sample_bits)?;
694
695 let sample_stats = results::evaluate_program_inplace(
696 &self.scc_plans,
697 &mut store,
698 cfg.max_nonmonotone_iterations,
699 &self.arithmetic_schemas,
700 )?;
701 stats.nonmonotone_sccs += sample_stats.nonmonotone_sccs;
702 stats.nonmonotone_cycles += sample_stats.nonmonotone_cycles;
703 stats.nonmonotone_iteration_limit_hits += sample_stats.nonmonotone_iteration_limit_hits;
704
705 if !is_clamped && !results::evidence_satisfied(&store, &self.evidence) {
707 continue;
708 }
709
710 n_evidence += 1;
711 for (i, q) in self.queries.iter().enumerate() {
712 if results::atom_holds(&store, q) {
713 n_query_true[i] += 1;
714 }
715 }
716 }
717
718 if !is_clamped && !self.evidence.is_empty() && n_evidence == 0 {
719 return Err(XlogError::Execution(format!(
720 "MC inference error: evidence was never satisfied across {} samples (seed={})",
721 cfg.samples, cfg.seed
722 )));
723 }
724
725 let denom = if self.evidence.is_empty() || is_clamped {
727 cfg.samples
728 } else {
729 n_evidence
730 };
731
732 let z = results::normal_quantile(0.5 + cfg.confidence / 2.0);
733
734 let mut query_estimates: Vec<McQueryEstimate> = Vec::with_capacity(self.queries.len());
735 for (i, atom) in self.query_presentations.iter().enumerate() {
736 let k = n_query_true[i];
737 let (p, stderr, ci_low, ci_high) = results::binomial_estimate(k, denom, z);
738 let log_prob = if p == 0.0 { f64::NEG_INFINITY } else { p.ln() };
739
740 query_estimates.push(McQueryEstimate {
741 atom: atom.clone(),
742 prob: p,
743 log_prob,
744 stderr,
745 ci_low,
746 ci_high,
747 });
748 }
749
750 Ok(McResult {
751 total_samples: cfg.samples,
752 evidence_samples: denom,
753 seed: cfg.seed,
754 confidence: cfg.confidence,
755 query_estimates,
756 nonmonotone_sccs: stats.nonmonotone_sccs,
757 nonmonotone_cycles: stats.nonmonotone_cycles,
758 nonmonotone_iteration_limit_hits: stats.nonmonotone_iteration_limit_hits,
759 sampling_method: method,
760 engine: McEngine::CpuOracle,
761 })
762 }
763
764 #[cfg(feature = "host-io")]
770 pub fn evaluate_gpu(&self, cfg: McEvalConfig) -> Result<McResult> {
771 self.evaluate(cfg)
772 }
773
774 pub fn evaluate_gpu_device(&self, cfg: McEvalConfig) -> Result<McDeviceResult> {
783 let provider = Arc::new(self.provider()?);
784 self.evaluate_gpu_device_with_provider(cfg, provider)
785 }
786
787 pub fn evaluate_gpu_device_with_provider(
798 &self,
799 cfg: McEvalConfig,
800 provider: Arc<CudaKernelProvider>,
801 ) -> Result<McDeviceResult> {
802 let r = self.evaluate_resident_with_provider(cfg, provider)?;
808 let no_host = r.no_host;
809 Ok(McDeviceResult {
810 query_counts: r.query_counts,
811 evidence_count: r.evidence_count,
812 total_samples: r.total_samples,
813 seed: r.seed,
814 confidence: r.confidence,
815 nonmonotone_sccs: 0,
819 nonmonotone_cycles: 0,
820 nonmonotone_iteration_limit_hits: 0,
821 sampling_method: r.sampling_method,
822 hot_loop_transfers: McHotLoopTransfers {
826 htod_calls: no_host.tracked_htod_calls,
827 dtoh_calls: no_host.tracked_dtoh_calls,
828 htod_bytes: 0,
829 dtoh_bytes: 0,
830 },
831 no_host,
832 })
833 }
834
835 fn compile_program(program: &Program) -> Result<Self> {
836 #[cfg(feature = "host-io")]
837 let source_program = program;
838 let arithmetic_schemas = crate::provenance::arithmetic_schemas(program)?;
839 let canonical_program = canonicalize_probabilistic_program(program, &arithmetic_schemas)?;
840 let program = &canonical_program;
841
842 let mut queries: Vec<GroundAtom> = Vec::new();
843 for ProbQuery { atom } in &program.prob_queries {
844 queries.push(atom_key_from_ground_atom(atom)?);
845 }
846 #[cfg(feature = "host-io")]
847 let mut query_presentations: Vec<GroundAtom> = Vec::new();
848 #[cfg(feature = "host-io")]
849 {
850 for (ProbQuery { atom }, ProbQuery { atom: source_atom }) in program
851 .prob_queries
852 .iter()
853 .zip(&source_program.prob_queries)
854 {
855 query_presentations.push(presentation_atom_from_canonical(
856 source_atom,
857 atom,
858 &arithmetic_schemas,
859 )?);
860 }
861 }
862
863 let mut evidence: Vec<(GroundAtom, bool)> = Vec::new();
864 let mut evidence_values: BTreeMap<GroundAtom, bool> = BTreeMap::new();
865 for Evidence { atom, value } in &program.evidence {
866 let atom = atom_key_from_ground_atom(atom)?;
867 match evidence_values.entry(atom.clone()) {
868 std::collections::btree_map::Entry::Occupied(previous)
869 if previous.get() != value =>
870 {
871 return Err(XlogError::Execution(format!(
872 "Monte Carlo inference error: conflicting evidence for {}",
873 atom.predicate
874 )));
875 }
876 std::collections::btree_map::Entry::Occupied(_) => continue,
877 std::collections::btree_map::Entry::Vacant(entry) => {
878 entry.insert(*value);
879 evidence.push((atom, *value));
880 }
881 }
882 }
883
884 let mut prob_facts = program.prob_facts.clone();
885 buffers::extend_prob_facts_with_coin(program, &mut prob_facts)?;
886 let (bernoulli_probs, prob_facts, annotated_disjunctions) =
887 buffers::compile_sampling_plan(&prob_facts, &program.annotated_disjunctions)?;
888
889 #[cfg(feature = "host-io")]
890 let mut base_store: HashMap<String, Relation> = HashMap::new();
891
892 #[cfg(feature = "host-io")]
894 {
895 for fact in program.facts() {
896 let atom = atom_key_from_ground_atom(&fact.head)?;
897 base_store
898 .entry(atom.predicate.clone())
899 .or_default()
900 .insert_tuple(atom.args);
901 }
902 }
903
904 #[cfg(feature = "host-io")]
907 {
908 let mut referenced: HashSet<String> = HashSet::new();
909 for rule in &program.rules {
910 referenced.insert(rule.head.predicate.clone());
911 for lit in &rule.body {
912 match lit {
913 BodyLiteral::Positive(a) | BodyLiteral::Negated(a) => {
914 referenced.insert(a.predicate.clone());
915 }
916 BodyLiteral::Epistemic(lit) => {
917 referenced.insert(lit.atom.predicate.clone());
918 }
919 BodyLiteral::Comparison(_)
920 | BodyLiteral::IsExpr(_)
921 | BodyLiteral::Univ(_) => {}
922 }
923 }
924 }
925 for pf in &program.prob_facts {
926 referenced.insert(pf.atom.predicate.clone());
927 }
928 for ad in &program.annotated_disjunctions {
929 for pf in &ad.choices {
930 referenced.insert(pf.atom.predicate.clone());
931 }
932 }
933 for q in &queries {
934 referenced.insert(q.predicate.clone());
935 }
936 for (e, _) in &evidence {
937 referenced.insert(e.predicate.clone());
938 }
939 for pred in referenced {
940 base_store.entry(pred).or_default();
941 }
942 }
943
944 #[cfg(feature = "host-io")]
945 let scc_plans = results::build_scc_plans(program)?;
946 Ok(Self {
947 gpu_config: GpuConfig::default(),
948 program: program.clone(),
949 #[cfg(feature = "host-io")]
950 base_store,
951 #[cfg(feature = "host-io")]
952 scc_plans,
953 #[cfg(feature = "host-io")]
954 arithmetic_schemas,
955 queries,
956 #[cfg(feature = "host-io")]
957 query_presentations,
958 evidence,
959 bernoulli_probs,
960 prob_facts,
961 annotated_disjunctions,
962 })
963 }
964
965 fn provider(&self) -> Result<CudaKernelProvider> {
966 if self.gpu_config.memory_bytes == 0 {
967 return Err(XlogError::Kernel(
968 "GPU memory budget must be non-zero".to_string(),
969 ));
970 }
971
972 xlog_cuda::CudaProviderBuilder::new(
973 self.gpu_config.device_ordinal,
974 MemoryBudget::with_limit(self.gpu_config.memory_bytes),
975 )
976 .build()
977 }
978}