1use std::collections::BTreeMap;
30use std::sync::Arc;
31
32use cudarc::driver::LaunchConfig;
33use xlog_core::{Result, XlogError};
34use xlog_cuda::memory::TrackedCudaSlice;
35use xlog_cuda::provider::{mc_resident_kernels, MC_RESIDENT_MODULE};
36use xlog_cuda::{CudaKernelProvider, LaunchAsync};
37use xlog_logic::ast::{Atom, BodyLiteral, Term};
38
39use super::{McEvalConfig, McProgram, McSamplingMethod};
40use crate::provenance::{GroundAtom, Value};
41
42pub(super) const MAX_ARITY: usize = 3;
44pub(super) const MAX_BODY: usize = 3;
46pub(super) const MAX_VARS: usize = 8;
48pub(super) const MAX_UNIVERSE: usize = 1 << 16;
51pub(super) const MAX_DOMAIN: usize = 256;
53const ATOM_REC: usize = 6;
55const RULE_REC: usize = 3 + 4 * ATOM_REC;
57const CONST_FLAG: u32 = 0x8000_0000;
59const RESIDENT_BUDGET_ENV: &str = "XLOG_MC_RESIDENT_MEMORY_BUDGET_BYTES";
60const RESIDENT_BLOCKS_PER_WORLD_ENV: &str = "XLOG_MC_RESIDENT_BLOCKS_PER_WORLD";
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum ResidentRejectKind {
65 Negation,
67 EpistemicLiteral,
69 NonRelationalLiteral,
71 ArityTooHigh,
73 BodyTooLong,
75 TooManyVars,
77 UnboundedTerm,
79 DomainTooLarge,
81 UniverseTooLarge,
83 InconsistentArity,
85 AnnotatedDisjunctionUnsupported,
87}
88
89impl ResidentRejectKind {
90 pub fn as_str(self) -> &'static str {
92 match self {
93 ResidentRejectKind::Negation => "negation",
94 ResidentRejectKind::EpistemicLiteral => "epistemic_literal",
95 ResidentRejectKind::NonRelationalLiteral => "non_relational_literal",
96 ResidentRejectKind::ArityTooHigh => "arity_too_high",
97 ResidentRejectKind::BodyTooLong => "body_too_long",
98 ResidentRejectKind::TooManyVars => "too_many_vars",
99 ResidentRejectKind::UnboundedTerm => "unbounded_term",
100 ResidentRejectKind::DomainTooLarge => "domain_too_large",
101 ResidentRejectKind::UniverseTooLarge => "universe_too_large",
102 ResidentRejectKind::InconsistentArity => "inconsistent_arity",
103 ResidentRejectKind::AnnotatedDisjunctionUnsupported => {
104 "annotated_disjunction_unsupported"
105 }
106 }
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct ResidentRejection {
114 pub kind: ResidentRejectKind,
116 pub construct: String,
118 pub context: String,
120}
121
122impl ResidentRejection {
123 fn err(
124 kind: ResidentRejectKind,
125 construct: impl Into<String>,
126 context: impl Into<String>,
127 ) -> Self {
128 ResidentRejection {
129 kind,
130 construct: construct.into(),
131 context: context.into(),
132 }
133 }
134
135 pub fn into_error(self) -> XlogError {
138 XlogError::Compilation(format!(
139 "resident MC engine rejected program [kind={}] construct=`{}` context=`{}`",
140 self.kind.as_str(),
141 self.construct,
142 self.context
143 ))
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
150enum ConstKey {
151 Int(i64),
152 Sym(u32),
153 Str(String),
154 FloatBits(u64),
155}
156
157impl ConstKey {
158 fn from_value(v: &Value) -> ConstKey {
159 match v {
160 Value::I64(i) => ConstKey::Int(*i),
161 Value::Symbol(s) => ConstKey::Sym(*s),
162 Value::String(s) => ConstKey::Str(s.clone()),
163 Value::F64(bits) => ConstKey::FloatBits(*bits),
164 }
165 }
166
167 fn from_term(t: &Term) -> std::result::Result<TermClass, ResidentRejection> {
169 match t {
170 Term::Variable(name) => Ok(TermClass::Var(name.clone())),
171 Term::Integer(i) => Ok(TermClass::Const(ConstKey::Int(*i))),
172 Term::Symbol(s) => Ok(TermClass::Const(ConstKey::Sym(*s))),
173 Term::String(s) => Ok(TermClass::Const(ConstKey::Str(s.clone()))),
174 Term::Float(f) => Ok(TermClass::Const(ConstKey::FloatBits(f.to_bits()))),
175 other => Err(ResidentRejection::err(
176 ResidentRejectKind::UnboundedTerm,
177 format!("{:?}", other),
178 "rule term must be a variable or ground constant",
179 )),
180 }
181 }
182}
183
184enum TermClass {
185 Var(String),
186 Const(ConstKey),
187}
188
189#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
191pub struct McNoHostStats {
192 pub tracked_htod_calls: u64,
194 pub tracked_dtoh_calls: u64,
196 pub untracked_metadata_reads: u64,
198 pub engine_launches: u64,
200 pub host_loop_iterations: u64,
203 pub per_sample_host_launches: u64,
206 pub host_fixpoint_iterations: u64,
210 pub per_operator_host_allocations: u64,
214}
215
216impl McNoHostStats {
217 pub fn is_no_host(&self) -> bool {
223 self.tracked_htod_calls == 0
224 && self.tracked_dtoh_calls == 0
225 && self.untracked_metadata_reads == 0
226 && self.host_loop_iterations == 0
227 && self.per_sample_host_launches == 0
228 && self.host_fixpoint_iterations == 0
229 && self.per_operator_host_allocations == 0
230 }
231}
232
233pub struct McResidentResult {
236 pub query_counts: TrackedCudaSlice<u32>,
238 pub evidence_count: TrackedCudaSlice<u32>,
240 pub iter_trace: TrackedCudaSlice<u32>,
242 pub sparse_final_row_counts: TrackedCudaSlice<u32>,
244 pub sparse_offsets: TrackedCudaSlice<u32>,
247 pub resident_status_flags: TrackedCudaSlice<u32>,
251 pub total_samples: usize,
253 pub seed: u64,
255 pub confidence: f64,
257 pub sampling_method: McSamplingMethod,
259 pub num_queries: usize,
261 pub no_host: McNoHostStats,
263}
264
265#[derive(Debug, Clone)]
267pub struct ResidentPlan {
268 pub universe_size: u32,
270 pub domain_size: u32,
272 pub max_iters: u32,
274 edb_slots: Vec<u32>,
275 pf_slot: Vec<u32>,
276 pf_var: Vec<u32>,
277 rule_data: Vec<u32>,
278 num_rules: u32,
279 q_slot: Vec<u32>,
280 ev_slot: Vec<u32>,
281 ev_expected: Vec<u8>,
282 ad_data: Vec<u32>,
283 num_ads: u32,
284 pub num_vars: usize,
286 bernoulli_probs: Vec<f32>,
287}
288
289const RESIDENT_POSTFLIGHT_WORDS: usize = 3;
290const RESIDENT_WORLD_STATUS_SECTIONS: usize = 4;
291const RESIDENT_CONTROL_WORDS: usize = 1;
292
293fn resident_status_words(num_worlds: u32) -> usize {
294 (num_worlds as usize)
295 .saturating_mul(RESIDENT_WORLD_STATUS_SECTIONS)
296 .saturating_add(RESIDENT_POSTFLIGHT_WORDS)
297 .saturating_add(RESIDENT_CONTROL_WORDS)
298 .max(1)
299}
300
301fn validate_resident_postflight(
302 summary: &[u32],
303 total_worlds: u32,
304 iteration_limit: u32,
305 sparse_cap: usize,
306) -> Result<()> {
307 let [unconverged_worlds, overflowed_worlds, minimum_required_sparse_rows] = summary else {
308 return Err(XlogError::Kernel(format!(
309 "resident postflight summary length {}, expected {}",
310 summary.len(),
311 RESIDENT_POSTFLIGHT_WORDS
312 )));
313 };
314
315 if *overflowed_worlds > 0 {
316 let minimum_required = sparse_cap.saturating_add(1) as u64;
317 return Err(XlogError::CapacityExceeded {
318 context: format!(
319 "resident Monte Carlo sparse relation arena ({} of {} worlds overflowed)",
320 overflowed_worlds, total_worlds
321 ),
322 required: u64::from(*minimum_required_sparse_rows).max(minimum_required),
323 limit: sparse_cap as u64,
324 unit: "rows per world".to_string(),
325 });
326 }
327
328 if *unconverged_worlds > 0 {
329 return Err(XlogError::ConvergenceFailure {
330 context: "resident Monte Carlo fixpoint".to_string(),
331 unconverged_worlds: u64::from(*unconverged_worlds),
332 total_worlds: u64::from(total_worlds),
333 iteration_limit: u64::from(iteration_limit),
334 });
335 }
336
337 Ok(())
338}
339
340fn resident_memory_budget_bytes() -> Result<Option<u64>> {
341 match std::env::var(RESIDENT_BUDGET_ENV) {
342 Ok(raw) => raw.parse::<u64>().map(Some).map_err(|e| {
343 XlogError::Execution(format!("invalid {RESIDENT_BUDGET_ENV} value `{raw}`: {e}"))
344 }),
345 Err(std::env::VarError::NotPresent) => Ok(None),
346 Err(e) => Err(XlogError::Execution(format!(
347 "invalid {RESIDENT_BUDGET_ENV}: {e}"
348 ))),
349 }
350}
351
352fn resident_blocks_per_world() -> Result<u32> {
353 match std::env::var(RESIDENT_BLOCKS_PER_WORLD_ENV) {
354 Ok(raw) => {
355 let blocks = raw.parse::<u32>().map_err(|e| {
356 XlogError::Execution(format!(
357 "invalid {RESIDENT_BLOCKS_PER_WORLD_ENV} value `{raw}`: {e}"
358 ))
359 })?;
360 if blocks == 0 {
361 return Err(XlogError::Execution(format!(
362 "invalid {RESIDENT_BLOCKS_PER_WORLD_ENV} value `{raw}`: must be >= 1"
363 )));
364 }
365 Ok(blocks)
366 }
367 Err(std::env::VarError::NotPresent) => Ok(1),
368 Err(e) => Err(XlogError::Execution(format!(
369 "invalid {RESIDENT_BLOCKS_PER_WORLD_ENV}: {e}"
370 ))),
371 }
372}
373
374fn sat_mul(a: u64, b: u64) -> u64 {
375 a.saturating_mul(b)
376}
377
378fn sat_pow(mut base: u64, mut exp: u32) -> u64 {
379 let mut acc = 1u64;
380 while exp > 0 {
381 if exp & 1 == 1 {
382 acc = sat_mul(acc, base);
383 }
384 exp >>= 1;
385 if exp > 0 {
386 base = sat_mul(base, base);
387 }
388 }
389 acc
390}
391
392fn estimate_resident_bound_bytes(plan: &ResidentPlan, num_worlds: u32) -> u64 {
393 let worlds = num_worlds.max(1) as u64;
394 let vars = plan.num_vars.max(1) as u64;
395 let universe = plan.universe_size.max(1) as u64;
396 let meta_words = plan
397 .edb_slots
398 .len()
399 .saturating_add(plan.pf_slot.len())
400 .saturating_add(plan.pf_var.len())
401 .saturating_add(plan.rule_data.len())
402 .saturating_add(plan.q_slot.len())
403 .saturating_add(plan.ev_slot.len())
404 .saturating_add(plan.ev_expected.len())
405 .saturating_add(plan.ad_data.len())
406 .saturating_add(18);
407
408 let sparse_cap = universe;
409 let setup_bytes = sat_mul(2, vars)
410 .saturating_add(sat_mul(worlds, vars))
411 .saturating_add(sat_mul(sat_mul(sat_mul(worlds, 2), universe), 4))
412 .saturating_add(sat_mul(sat_mul(sat_mul(worlds, 2), sparse_cap), 16))
413 .saturating_add(sat_mul(sat_mul(worlds, 2), 4))
414 .saturating_add(sat_mul(worlds, 4))
415 .saturating_add(sat_mul(worlds.saturating_add(1), 4))
416 .saturating_add(sat_mul(worlds.saturating_mul(4).saturating_add(4), 4))
417 .saturating_add(sat_mul(meta_words as u64, 4))
418 .saturating_add(sat_mul(plan.q_slot.len().max(1) as u64, 4))
419 .saturating_add(4)
420 .saturating_add(sat_mul(worlds, 4));
421
422 let mut sparse_join_bytes = 0u64;
423 for rule in plan.rule_data.as_chunks::<RULE_REC>().0 {
424 let n_body = rule[0];
425 if n_body < 2 {
426 continue;
427 }
428 let n_vars = rule[1];
429 let assignments = sat_pow(plan.domain_size.max(1) as u64, n_vars);
430 let row_words = (n_body as u64).saturating_add(1);
431 sparse_join_bytes =
432 sparse_join_bytes.max(sat_mul(sat_mul(worlds, assignments), row_words * 4));
433 }
434
435 setup_bytes.saturating_add(sparse_join_bytes)
436}
437
438struct PredInfo {
439 arity: usize,
440 base: u32,
441}
442
443struct Universe {
444 domain: BTreeMap<ConstKey, u32>,
445 preds: BTreeMap<String, PredInfo>,
446 domain_size: u32,
447}
448
449impl Universe {
450 fn stride0(&self, arity: usize) -> u32 {
451 if arity >= 2 {
452 self.domain_size.pow((arity - 1) as u32)
453 } else {
454 1
455 }
456 }
457
458 fn arg_stride(&self, arity: usize, arg_idx: usize) -> u32 {
459 if arity <= arg_idx + 1 {
460 1
461 } else {
462 self.domain_size.pow((arity - arg_idx - 1) as u32)
463 }
464 }
465
466 fn ground_slot(&self, atom: &GroundAtom) -> std::result::Result<u32, ResidentRejection> {
468 let info = self.preds.get(&atom.predicate).ok_or_else(|| {
469 ResidentRejection::err(
470 ResidentRejectKind::InconsistentArity,
471 atom.predicate.clone(),
472 "ground atom references unknown predicate",
473 )
474 })?;
475 if atom.args.len() != info.arity {
476 return Err(ResidentRejection::err(
477 ResidentRejectKind::InconsistentArity,
478 atom.predicate.clone(),
479 format!("expected arity {} got {}", info.arity, atom.args.len()),
480 ));
481 }
482 let mut slot = info.base;
483 for (i, v) in atom.args.iter().enumerate() {
484 let key = ConstKey::from_value(v);
485 let idx = *self.domain.get(&key).ok_or_else(|| {
486 ResidentRejection::err(
487 ResidentRejectKind::UnboundedTerm,
488 format!("{:?}", v),
489 "ground constant absent from bounded domain",
490 )
491 })?;
492 slot += idx * self.arg_stride(info.arity, i);
493 }
494 Ok(slot)
495 }
496}
497
498pub fn compile_resident_plan(
501 mc: &McProgram,
502) -> std::result::Result<ResidentPlan, ResidentRejection> {
503 let program = &mc.program;
504
505 let mut arities: BTreeMap<String, usize> = BTreeMap::new();
507 let mut note_pred = |pred: &str, arity: usize| -> std::result::Result<(), ResidentRejection> {
508 if arity > MAX_ARITY {
509 return Err(ResidentRejection::err(
510 ResidentRejectKind::ArityTooHigh,
511 pred.to_string(),
512 format!("arity {} exceeds max {}", arity, MAX_ARITY),
513 ));
514 }
515 match arities.get(pred) {
516 Some(&existing) if existing != arity => Err(ResidentRejection::err(
517 ResidentRejectKind::InconsistentArity,
518 pred.to_string(),
519 format!("arity {} vs {}", existing, arity),
520 )),
521 _ => {
522 arities.insert(pred.to_string(), arity);
523 Ok(())
524 }
525 }
526 };
527
528 let mut domain: BTreeMap<ConstKey, u32> = BTreeMap::new();
530 let mut note_const = |key: ConstKey, domain: &mut BTreeMap<ConstKey, u32>| {
531 let next = domain.len() as u32;
532 domain.entry(key).or_insert(next);
533 };
534
535 for fact in program.facts() {
537 note_pred(&fact.head.predicate, fact.head.terms.len())?;
538 for t in &fact.head.terms {
539 match ConstKey::from_term(t)? {
540 TermClass::Const(k) => note_const(k, &mut domain),
541 TermClass::Var(_) => {
542 return Err(ResidentRejection::err(
543 ResidentRejectKind::UnboundedTerm,
544 fact.head.predicate.clone(),
545 "fact head contains a variable",
546 ))
547 }
548 }
549 }
550 }
551 for pf in &mc.prob_facts {
553 note_pred(&pf.atom.predicate, pf.atom.args.len())?;
554 for v in &pf.atom.args {
555 note_const(ConstKey::from_value(v), &mut domain);
556 }
557 }
558 for q in &mc.queries {
560 note_pred(&q.predicate, q.args.len())?;
561 for v in &q.args {
562 note_const(ConstKey::from_value(v), &mut domain);
563 }
564 }
565 for (e, _) in &mc.evidence {
566 note_pred(&e.predicate, e.args.len())?;
567 for v in &e.args {
568 note_const(ConstKey::from_value(v), &mut domain);
569 }
570 }
571 for ad in &mc.annotated_disjunctions {
573 for atom in &ad.choices {
574 note_pred(&atom.predicate, atom.args.len())?;
575 for v in &atom.args {
576 note_const(ConstKey::from_value(v), &mut domain);
577 }
578 }
579 }
580 for rule in &program.rules {
582 if rule.is_fact() {
583 continue;
584 }
585 note_pred(&rule.head.predicate, rule.head.terms.len())?;
586 collect_atom_consts(&rule.head, &mut domain, &mut note_const)?;
587 if rule.body.len() > MAX_BODY {
588 return Err(ResidentRejection::err(
589 ResidentRejectKind::BodyTooLong,
590 rule.head.predicate.clone(),
591 format!("body length {} exceeds max {}", rule.body.len(), MAX_BODY),
592 ));
593 }
594 for lit in &rule.body {
595 let atom = classify_body_literal(lit, &rule.head.predicate)?;
596 note_pred(&atom.predicate, atom.terms.len())?;
597 collect_atom_consts(atom, &mut domain, &mut note_const)?;
598 }
599 }
600
601 if domain.len() > MAX_DOMAIN {
602 return Err(ResidentRejection::err(
603 ResidentRejectKind::DomainTooLarge,
604 format!("{} constants", domain.len()),
605 format!("domain exceeds max {}", MAX_DOMAIN),
606 ));
607 }
608 let domain_size = domain.len() as u32;
609
610 let mut preds: BTreeMap<String, PredInfo> = BTreeMap::new();
612 let mut base: u64 = 0;
613 for (pred, &arity) in &arities {
614 let slot_count: u64 = if arity == 0 {
615 1
616 } else {
617 (domain_size as u64).pow(arity as u32)
618 };
619 preds.insert(
620 pred.clone(),
621 PredInfo {
622 arity,
623 base: base as u32,
624 },
625 );
626 base += slot_count;
627 if base > MAX_UNIVERSE as u64 {
628 return Err(ResidentRejection::err(
629 ResidentRejectKind::UniverseTooLarge,
630 format!("{} slots", base),
631 format!("universe exceeds max {}", MAX_UNIVERSE),
632 ));
633 }
634 }
635 let universe_size = base as u32;
636
637 let universe = Universe {
638 domain,
639 preds,
640 domain_size,
641 };
642
643 let mut edb_slots = Vec::new();
645 for fact in program.facts() {
646 let ga = ground_atom_from_atom(&fact.head)?;
647 edb_slots.push(universe.ground_slot(&ga)?);
648 }
649 let mut pf_slot = Vec::new();
650 let mut pf_var = Vec::new();
651 for pf in &mc.prob_facts {
652 pf_slot.push(universe.ground_slot(&pf.atom)?);
653 pf_var.push(pf.var_idx as u32);
654 }
655 let mut q_slot = Vec::new();
656 for q in &mc.queries {
657 q_slot.push(universe.ground_slot(q)?);
658 }
659 let mut ev_slot = Vec::new();
660 let mut ev_expected = Vec::new();
661 for (e, v) in &mc.evidence {
662 ev_slot.push(universe.ground_slot(e)?);
663 ev_expected.push(if *v { 1u8 } else { 0u8 });
664 }
665
666 let mut rule_data = Vec::new();
668 let mut num_rules = 0u32;
669 for rule in &program.rules {
670 if rule.is_fact() {
671 continue;
672 }
673 let rec = lower_rule(rule, &universe)?;
674 rule_data.extend_from_slice(&rec);
675 num_rules += 1;
676 }
677
678 let mut ad_data: Vec<u32> = Vec::new();
683 let mut num_ads = 0u32;
684 for ad in &mc.annotated_disjunctions {
685 let n_choices = ad.choices.len() as u32;
686 let n_dvars = ad.decision_vars.len() as u32;
687 ad_data.push(n_choices);
688 ad_data.push(n_dvars);
689 for &dv in &ad.decision_vars {
690 ad_data.push(u32::try_from(dv).map_err(|_| {
691 ResidentRejection::err(
692 ResidentRejectKind::UnboundedTerm,
693 "decision_var",
694 "AD decision var index exceeds u32",
695 )
696 })?);
697 }
698 for atom in &ad.choices {
699 ad_data.push(universe.ground_slot(atom)?);
700 }
701 num_ads += 1;
702 }
703
704 let max_iters = universe_size.saturating_add(1).max(1);
708
709 Ok(ResidentPlan {
710 universe_size,
711 domain_size,
712 max_iters,
713 edb_slots,
714 pf_slot,
715 pf_var,
716 rule_data,
717 num_rules,
718 q_slot,
719 ev_slot,
720 ev_expected,
721 ad_data,
722 num_ads,
723 num_vars: mc.bernoulli_probs.len(),
724 bernoulli_probs: mc.bernoulli_probs.clone(),
725 })
726}
727
728fn collect_atom_consts<F: FnMut(ConstKey, &mut BTreeMap<ConstKey, u32>)>(
729 atom: &Atom,
730 domain: &mut BTreeMap<ConstKey, u32>,
731 note_const: &mut F,
732) -> std::result::Result<(), ResidentRejection> {
733 if atom.terms.len() > MAX_ARITY {
734 return Err(ResidentRejection::err(
735 ResidentRejectKind::ArityTooHigh,
736 atom.predicate.clone(),
737 format!("arity {} exceeds max {}", atom.terms.len(), MAX_ARITY),
738 ));
739 }
740 for t in &atom.terms {
741 if let TermClass::Const(k) = ConstKey::from_term(t)? {
742 note_const(k, domain);
743 }
744 }
745 Ok(())
746}
747
748fn classify_body_literal<'a>(
750 lit: &'a BodyLiteral,
751 rule_ctx: &str,
752) -> std::result::Result<&'a Atom, ResidentRejection> {
753 match lit {
754 BodyLiteral::Positive(a) => Ok(a),
755 BodyLiteral::Negated(a) => Err(ResidentRejection::err(
756 ResidentRejectKind::Negation,
757 a.predicate.clone(),
758 format!("negated literal in rule for `{}`", rule_ctx),
759 )),
760 BodyLiteral::Epistemic(l) => Err(ResidentRejection::err(
761 ResidentRejectKind::EpistemicLiteral,
762 l.atom.predicate.clone(),
763 format!("epistemic literal in rule for `{}`", rule_ctx),
764 )),
765 BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {
766 Err(ResidentRejection::err(
767 ResidentRejectKind::NonRelationalLiteral,
768 "comparison/is/univ",
769 format!("non-relational literal in rule for `{}`", rule_ctx),
770 ))
771 }
772 }
773}
774
775fn lower_rule(
777 rule: &xlog_logic::ast::Rule,
778 universe: &Universe,
779) -> std::result::Result<Vec<u32>, ResidentRejection> {
780 let mut var_ids: BTreeMap<String, u32> = BTreeMap::new();
782 let assign_var = |name: &str,
783 var_ids: &mut BTreeMap<String, u32>|
784 -> std::result::Result<u32, ResidentRejection> {
785 if let Some(&id) = var_ids.get(name) {
786 return Ok(id);
787 }
788 let id = var_ids.len() as u32;
789 if id as usize >= MAX_VARS {
790 return Err(ResidentRejection::err(
791 ResidentRejectKind::TooManyVars,
792 rule.head.predicate.clone(),
793 format!("more than {} distinct variables", MAX_VARS),
794 ));
795 }
796 var_ids.insert(name.to_string(), id);
797 Ok(id)
798 };
799
800 let body_atoms: Vec<&Atom> = {
802 let mut v = Vec::new();
803 for lit in &rule.body {
804 v.push(classify_body_literal(lit, &rule.head.predicate)?);
805 }
806 v
807 };
808 for t in &rule.head.terms {
809 if let TermClass::Var(name) = ConstKey::from_term(t)? {
810 assign_var(&name, &mut var_ids)?;
811 }
812 }
813 for atom in &body_atoms {
814 for t in &atom.terms {
815 if let TermClass::Var(name) = ConstKey::from_term(t)? {
816 assign_var(&name, &mut var_ids)?;
817 }
818 }
819 }
820 let n_vars = var_ids.len() as u32;
821
822 let encode_atom = |atom: &Atom| -> std::result::Result<[u32; ATOM_REC], ResidentRejection> {
823 let info = universe.preds.get(&atom.predicate).ok_or_else(|| {
824 ResidentRejection::err(
825 ResidentRejectKind::InconsistentArity,
826 atom.predicate.clone(),
827 "rule atom references unknown predicate",
828 )
829 })?;
830 let arity = info.arity as u32;
831 let mut rec = [0u32; ATOM_REC];
832 rec[0] = info.base;
833 rec[1] = arity;
834 rec[5] = universe.stride0(info.arity);
835 for (i, t) in atom.terms.iter().enumerate() {
836 let spec = match ConstKey::from_term(t)? {
837 TermClass::Var(name) => *var_ids.get(&name).expect("var assigned above"),
838 TermClass::Const(k) => {
839 let idx = *universe.domain.get(&k).ok_or_else(|| {
840 ResidentRejection::err(
841 ResidentRejectKind::UnboundedTerm,
842 format!("{:?}", k),
843 "rule constant absent from bounded domain",
844 )
845 })?;
846 CONST_FLAG | idx
847 }
848 };
849 rec[2 + i] = spec;
850 }
851 Ok(rec)
852 };
853
854 let mut rec = vec![0u32; RULE_REC];
855 rec[0] = body_atoms.len() as u32;
856 rec[1] = n_vars;
857 rec[2] = universe.domain_size;
858 let head_rec = encode_atom(&rule.head)?;
859 rec[3..3 + ATOM_REC].copy_from_slice(&head_rec);
860 for (bi, atom) in body_atoms.iter().enumerate() {
861 let a = encode_atom(atom)?;
862 let off = 3 + ATOM_REC + bi * ATOM_REC;
863 rec[off..off + ATOM_REC].copy_from_slice(&a);
864 }
865 Ok(rec)
866}
867
868fn ground_atom_from_atom(atom: &Atom) -> std::result::Result<GroundAtom, ResidentRejection> {
869 let mut args = Vec::with_capacity(atom.terms.len());
870 for t in &atom.terms {
871 let v = match t {
872 Term::Integer(i) => Value::I64(*i),
873 Term::Symbol(s) => Value::Symbol(*s),
874 Term::String(s) => Value::String(s.clone()),
875 Term::Float(f) => Value::F64(f.to_bits()),
876 other => {
877 return Err(ResidentRejection::err(
878 ResidentRejectKind::UnboundedTerm,
879 format!("{:?}", other),
880 "fact term must be a ground constant",
881 ))
882 }
883 };
884 args.push(v);
885 }
886 Ok(GroundAtom {
887 predicate: atom.predicate.clone(),
888 args,
889 })
890}
891
892impl McProgram {
893 pub fn evaluate_resident_with_provider(
898 &self,
899 cfg: McEvalConfig,
900 provider: Arc<CudaKernelProvider>,
901 ) -> Result<McResidentResult> {
902 cfg.validate()?;
903 let plan = compile_resident_plan(self).map_err(ResidentRejection::into_error)?;
904 let sparse_cap = plan.universe_size.max(1) as usize;
905 run_resident(&plan, &cfg, self, provider, sparse_cap)
906 }
907
908 pub fn evaluate_resident(&self, cfg: McEvalConfig) -> Result<McResidentResult> {
910 let provider = Arc::new(self.provider()?);
911 self.evaluate_resident_with_provider(cfg, provider)
912 }
913}
914
915fn run_resident(
916 plan: &ResidentPlan,
917 cfg: &McEvalConfig,
918 mc: &McProgram,
919 provider: Arc<CudaKernelProvider>,
920 sparse_cap: usize,
921) -> Result<McResidentResult> {
922 let (method, forcing) = mc.resolve_sampling_method(cfg.sampling_method)?;
923 let num_worlds = u32::try_from(cfg.samples)
924 .map_err(|_| XlogError::Execution("MC samples exceed u32::MAX".to_string()))?;
925 let blocks_per_world = resident_blocks_per_world()?;
926 let num_vars = plan.num_vars;
927
928 if let Some(budget_bytes) = resident_memory_budget_bytes()? {
929 let bound_bytes = estimate_resident_bound_bytes(plan, num_worlds);
930 if bound_bytes > budget_bytes {
931 return Err(XlogError::ResourceExhausted {
932 context: format!(
933 "resident_resource_budget operator=sparse_wcoj bound_bytes={bound_bytes} budget_bytes={budget_bytes}"
934 ),
935 estimated_bytes: bound_bytes,
936 budget_bytes,
937 });
938 }
939 }
940
941 let dev = provider.device();
946
947 let mut d_force_mask = provider.memory().alloc::<u8>(num_vars.max(1))?;
949 let mut d_forced_value = provider.memory().alloc::<u8>(num_vars.max(1))?;
950 if method == McSamplingMethod::EvidenceClamping && num_vars > 0 {
951 provider.htod_sync_copy_into_tracked(&forcing.force_mask, &mut d_force_mask)?;
952 provider.htod_sync_copy_into_tracked(&forcing.forced_value, &mut d_forced_value)?;
953 } else {
954 dev.inner()
955 .memset_zeros(&mut d_force_mask)
956 .map_err(|e| XlogError::Kernel(format!("zero force_mask: {e}")))?;
957 dev.inner()
958 .memset_zeros(&mut d_forced_value)
959 .map_err(|e| XlogError::Kernel(format!("zero forced_value: {e}")))?;
960 }
961 let samples_device = if num_vars == 0 || cfg.samples == 0 {
962 provider.memory().alloc::<u8>(1)?
963 } else {
964 provider.sample_bernoulli_matrix_device(
965 &plan.bernoulli_probs,
966 cfg.samples,
967 cfg.seed,
968 &d_force_mask.slice(..),
969 &d_forced_value.slice(..),
970 )?
971 };
972
973 let u = plan.universe_size.max(1) as usize;
976 let rel_len = (num_worlds as usize)
977 .saturating_mul(u)
978 .saturating_mul(2)
979 .max(1);
980 let mut d_rel = provider.memory().alloc::<u32>(rel_len)?;
981 dev.inner()
982 .memset_zeros(&mut d_rel)
983 .map_err(|e| XlogError::Kernel(format!("zero rel: {e}")))?;
984
985 if sparse_cap == 0 {
989 return Err(XlogError::CapacityExceeded {
990 context: "resident Monte Carlo sparse relation arena".to_string(),
991 required: 1,
992 limit: 0,
993 unit: "rows per world".to_string(),
994 });
995 }
996 let sparse_len = (num_worlds as usize)
997 .saturating_mul(2)
998 .saturating_mul(sparse_cap)
999 .max(1);
1000 let mut d_sparse_columns = provider
1002 .memory()
1003 .alloc::<u32>(sparse_len.saturating_mul(4).max(1))?;
1004 let mut d_sparse_counts = provider
1005 .memory()
1006 .alloc::<u32>((num_worlds as usize).saturating_mul(2).max(1))?;
1007 let mut d_sparse_final_counts = provider
1008 .memory()
1009 .alloc::<u32>((num_worlds as usize).max(1))?;
1010 let mut d_sparse_offsets = provider
1011 .memory()
1012 .alloc::<u32>((num_worlds as usize).saturating_add(1).max(1))?;
1013 let mut d_resident_status_flags = provider
1014 .memory()
1015 .alloc::<u32>(resident_status_words(num_worlds))?;
1016 dev.inner()
1017 .memset_zeros(&mut d_sparse_columns)
1018 .map_err(|e| XlogError::Kernel(format!("zero sparse_columns: {e}")))?;
1019 dev.inner()
1020 .memset_zeros(&mut d_sparse_counts)
1021 .map_err(|e| XlogError::Kernel(format!("zero sparse_counts: {e}")))?;
1022 dev.inner()
1023 .memset_zeros(&mut d_sparse_final_counts)
1024 .map_err(|e| XlogError::Kernel(format!("zero sparse_final_counts: {e}")))?;
1025 dev.inner()
1026 .memset_zeros(&mut d_sparse_offsets)
1027 .map_err(|e| XlogError::Kernel(format!("zero sparse_offsets: {e}")))?;
1028 dev.inner()
1029 .memset_zeros(&mut d_resident_status_flags)
1030 .map_err(|e| XlogError::Kernel(format!("zero resident_status_flags: {e}")))?;
1031
1032 let q_count = plan.q_slot.len();
1036 let ev_expected_u32: Vec<u32> = plan.ev_expected.iter().map(|&b| b as u32).collect();
1037
1038 let mut meta: Vec<u32> = Vec::new();
1039 let push_meta = |data: &[u32], meta: &mut Vec<u32>| -> u32 {
1040 let off = meta.len() as u32;
1041 meta.extend_from_slice(data);
1042 off
1043 };
1044 let edb_off = push_meta(&plan.edb_slots, &mut meta);
1045 let pf_slot_off = push_meta(&plan.pf_slot, &mut meta);
1046 let pf_var_off = push_meta(&plan.pf_var, &mut meta);
1047 let rules_off = push_meta(&plan.rule_data, &mut meta);
1048 let q_off = push_meta(&plan.q_slot, &mut meta);
1049 let ev_slot_off = push_meta(&plan.ev_slot, &mut meta);
1050 let ev_exp_off = push_meta(&ev_expected_u32, &mut meta);
1051 let ad_off = push_meta(&plan.ad_data, &mut meta);
1052
1053 let cfg_host: [u32; 19] = [
1055 num_worlds,
1056 plan.universe_size,
1057 num_vars as u32,
1058 plan.max_iters,
1059 edb_off,
1060 plan.edb_slots.len() as u32,
1061 pf_slot_off,
1062 pf_var_off,
1063 plan.pf_slot.len() as u32,
1064 rules_off,
1065 plan.num_rules,
1066 q_off,
1067 q_count as u32,
1068 ev_slot_off,
1069 ev_exp_off,
1070 plan.ev_slot.len() as u32,
1071 ad_off,
1072 plan.num_ads,
1073 blocks_per_world,
1074 ];
1075
1076 let mut d_cfg = provider.memory().alloc::<u32>(cfg_host.len())?;
1077 provider.htod_sync_copy_into_tracked(&cfg_host, &mut d_cfg)?;
1078 let mut d_meta = provider.memory().alloc::<u32>(meta.len().max(1))?;
1079 if !meta.is_empty() {
1080 provider.htod_sync_copy_into_tracked(&meta, &mut d_meta)?;
1081 }
1082
1083 let mut d_query_counts = provider.memory().alloc::<u32>(q_count.max(1))?;
1084 dev.inner()
1085 .memset_zeros(&mut d_query_counts)
1086 .map_err(|e| XlogError::Kernel(format!("zero query_counts: {e}")))?;
1087 let mut d_evidence_count = provider.memory().alloc::<u32>(1)?;
1088 dev.inner()
1089 .memset_zeros(&mut d_evidence_count)
1090 .map_err(|e| XlogError::Kernel(format!("zero evidence_count: {e}")))?;
1091 let mut d_iter_trace = provider.memory().alloc::<u32>(num_worlds.max(1) as usize)?;
1092 dev.inner()
1093 .memset_zeros(&mut d_iter_trace)
1094 .map_err(|e| XlogError::Kernel(format!("zero iter_trace: {e}")))?;
1095
1096 let engine_fn = dev
1097 .inner()
1098 .get_func(MC_RESIDENT_MODULE, mc_resident_kernels::MC_RESIDENT_ENGINE)
1099 .ok_or_else(|| XlogError::Kernel("mc_resident_engine kernel not found".to_string()))?;
1100
1101 dev.synchronize()?;
1103
1104 let pre = provider.host_transfer_stats();
1106 let pre_untracked = provider.untracked_metadata_dtoh_count();
1107 let pre_allocs = provider.memory().alloc_count();
1108 let mut engine_launches = 0u64;
1109
1110 let block_dim = 128u32;
1111 let grid_dim = num_worlds
1112 .max(1)
1113 .checked_mul(blocks_per_world)
1114 .ok_or_else(|| {
1115 XlogError::Execution(format!(
1116 "resident grid overflow: worlds={num_worlds} blocks_per_world={blocks_per_world}"
1117 ))
1118 })?;
1119 let launch_cfg = LaunchConfig {
1120 grid_dim: (grid_dim, 1, 1),
1121 block_dim: (block_dim, 1, 1),
1122 shared_mem_bytes: 0,
1123 };
1124 unsafe {
1127 let args = (
1128 &d_cfg,
1129 &d_meta,
1130 &mut d_rel,
1131 &samples_device,
1132 &mut d_query_counts,
1133 &mut d_evidence_count,
1134 &mut d_iter_trace,
1135 &mut d_sparse_columns,
1136 &mut d_sparse_counts,
1137 &mut d_sparse_final_counts,
1138 &mut d_sparse_offsets,
1139 &mut d_resident_status_flags,
1140 sparse_cap as u32,
1141 );
1142 if blocks_per_world == 1 {
1143 engine_fn
1144 .launch(launch_cfg, args)
1145 .map_err(|e| XlogError::Kernel(format!("mc_resident_engine launch failed: {e}")))?;
1146 } else {
1147 engine_fn
1148 .launch_cooperative(launch_cfg, args)
1149 .map_err(|e| {
1150 XlogError::Kernel(format!("mc_resident_engine cooperative launch failed: {e}"))
1151 })?;
1152 }
1153 }
1154 engine_launches += 1;
1155 dev.synchronize()?;
1156
1157 let post = provider.host_transfer_stats();
1158 let post_untracked = provider.untracked_metadata_dtoh_count();
1159 let post_allocs = provider.memory().alloc_count();
1160 let no_host = McNoHostStats {
1163 tracked_htod_calls: post.htod_calls.saturating_sub(pre.htod_calls),
1164 tracked_dtoh_calls: post.dtoh_calls.saturating_sub(pre.dtoh_calls),
1165 untracked_metadata_reads: post_untracked.saturating_sub(pre_untracked),
1166 engine_launches,
1167 host_loop_iterations: 0,
1168 host_fixpoint_iterations: 0,
1171 per_operator_host_allocations: post_allocs.saturating_sub(pre_allocs),
1172 per_sample_host_launches: 0,
1173 };
1174
1175 let postflight = provider
1179 .dtoh_small_metadata_untracked(&d_resident_status_flags, RESIDENT_POSTFLIGHT_WORDS)?;
1180 validate_resident_postflight(&postflight, num_worlds, plan.max_iters, sparse_cap)?;
1181
1182 Ok(McResidentResult {
1183 query_counts: d_query_counts,
1184 evidence_count: d_evidence_count,
1185 iter_trace: d_iter_trace,
1186 sparse_final_row_counts: d_sparse_final_counts,
1187 sparse_offsets: d_sparse_offsets,
1188 resident_status_flags: d_resident_status_flags,
1189 total_samples: cfg.samples,
1190 seed: cfg.seed,
1191 confidence: cfg.confidence,
1192 sampling_method: method,
1193 num_queries: q_count,
1194 no_host,
1195 })
1196}
1197
1198#[cfg(all(test, feature = "host-io"))]
1199mod tests {
1200 use super::*;
1201
1202 #[test]
1203 fn resident_plan_deduplicates_schema_equivalent_evidence() {
1204 let program = McProgram::compile_source(
1205 "0.5::gate(\"alpha\").\n\
1206 evidence(gate(\"alpha\"), true).\n\
1207 evidence(gate(alpha), true).\n\
1208 query(gate(\"alpha\")).\n",
1209 )
1210 .expect("compile equivalent symbol evidence");
1211 let plan = compile_resident_plan(&program).expect("compile resident plan");
1212
1213 assert_eq!(plan.ev_slot.len(), 1);
1214 assert_eq!(plan.ev_expected, [1]);
1215 }
1216
1217 #[test]
1218 fn resident_postflight_rejects_unconverged_worlds() {
1219 let err = validate_resident_postflight(&[3, 0, 0], 128, 7, 64)
1220 .expect_err("unconverged worlds must reject the result");
1221 assert!(matches!(
1222 err,
1223 XlogError::ConvergenceFailure {
1224 unconverged_worlds: 3,
1225 total_worlds: 128,
1226 iteration_limit: 7,
1227 ..
1228 }
1229 ));
1230 }
1231
1232 #[test]
1233 fn resident_postflight_rejects_sparse_capacity_overflow() {
1234 let err = validate_resident_postflight(&[0, 2, 65], 128, 7, 64)
1235 .expect_err("overflowed worlds must reject the result");
1236 assert!(matches!(
1237 err,
1238 XlogError::CapacityExceeded {
1239 required: 65,
1240 limit: 64,
1241 ..
1242 }
1243 ));
1244 }
1245
1246 #[test]
1247 #[ignore = "requires a CUDA device"]
1248 fn resident_kernel_nonconvergence_is_authoritative() {
1249 let program = McProgram::compile_source(
1250 "start().\n\
1251 first() :- start().\n\
1252 second() :- first().\n\
1253 query(second()).\n",
1254 )
1255 .expect("compile recursive chain");
1256 let mut plan = compile_resident_plan(&program).expect("compile resident plan");
1257 plan.max_iters = 1;
1258 let cfg = McEvalConfig {
1259 samples: 8,
1260 ..McEvalConfig::default()
1261 };
1262 let provider = Arc::new(program.provider().expect("CUDA provider"));
1263 let sparse_cap = plan.universe_size.max(1) as usize;
1264
1265 let err = match run_resident(&plan, &cfg, &program, provider, sparse_cap) {
1266 Ok(_) => panic!("unconverged resident run returned counts"),
1267 Err(err) => err,
1268 };
1269 assert!(matches!(
1270 err,
1271 XlogError::ConvergenceFailure {
1272 unconverged_worlds: 8,
1273 total_worlds: 8,
1274 iteration_limit: 1,
1275 ..
1276 }
1277 ));
1278 }
1279
1280 #[test]
1281 #[ignore = "requires a CUDA device"]
1282 fn resident_kernel_sparse_overflow_is_authoritative() {
1283 let program = McProgram::compile_source(
1284 "first().\n\
1285 second().\n\
1286 query(first()).\n",
1287 )
1288 .expect("compile two-fact program");
1289 let plan = compile_resident_plan(&program).expect("compile resident plan");
1290 let cfg = McEvalConfig {
1291 samples: 8,
1292 ..McEvalConfig::default()
1293 };
1294 let provider = Arc::new(program.provider().expect("CUDA provider"));
1295
1296 let err = match run_resident(&plan, &cfg, &program, provider, 1) {
1297 Ok(_) => panic!("overflowed resident run returned counts"),
1298 Err(err) => err,
1299 };
1300 assert!(matches!(
1301 err,
1302 XlogError::CapacityExceeded {
1303 required: 2,
1304 limit: 1,
1305 ..
1306 }
1307 ));
1308 }
1309}