1use cudarc::driver::DeviceSlice;
11use pyo3::exceptions::{PyRuntimeError, PyValueError};
12use pyo3::prelude::*;
13use pyo3::types::PyDict;
14
15#[cfg(feature = "host-io")]
16use xlog_core::symbol;
17use xlog_core::{ScalarType, Schema};
18use xlog_logic::ast::Term;
19use xlog_prob::exact::ExactDdnnfProgram;
20#[cfg(feature = "host-io")]
21use xlog_prob::exact::{ExactResultWithGrads, QueryProbability};
22use xlog_prob::mc::{McEvalConfig, McProgram, McSamplingMethod};
23use xlog_prob::neural_fast_path::GpuWeightSlots;
24
25use super::neural_registry::NeuralPredicateInfo;
26use super::{
27 dlpack_capsule_from_tensor, enforce_call_memory_limit, pack_query_proof_traces,
28 pack_rule_provenance, provider_memory_stats, types, CompiledProgram, EpochStats, EvalResult,
29 McDeviceEvalResult, TrainingHistory,
30};
31
32pub(crate) struct CachedCircuit {
41 pub(crate) program: ExactDdnnfProgram,
43
44 pub(crate) slots: GpuWeightSlots,
46
47 pub(crate) target_domain: Vec<String>,
49}
50
51#[derive(Debug, Clone)]
52pub(crate) enum InputSource {
53 QueryArg(usize),
54 ImplicitSlot(usize),
55 DomainRow(usize),
59 ConstDummy,
63}
64
65#[derive(Debug, Clone)]
66pub(crate) struct NeuralGroup {
67 pub(crate) info: NeuralPredicateInfo,
68 pub(crate) input_source: InputSource,
69 pub(crate) ground_const: Option<Term>,
74 #[cfg(feature = "host-io")]
75 pub(crate) output_var: Option<String>,
76}
77
78#[derive(Debug, Clone)]
84pub(crate) struct HardFilter {
85 pub(crate) relation: String,
87 pub(crate) arg_head_positions: Vec<usize>,
92}
93
94#[derive(Debug, Clone)]
101pub(crate) struct JoinPlan {
102 pub(crate) relations: Vec<String>,
105 pub(crate) head_domain: Vec<String>,
109}
110
111#[derive(Debug, Clone)]
112pub(crate) enum QuerySignature {
113 Boolean {
114 groups: Vec<NeuralGroup>,
115 hard_filters: Vec<HardFilter>,
116 },
117 Targeted {
118 target_position: usize,
119 groups: Vec<NeuralGroup>,
120 hard_filters: Vec<HardFilter>,
121 join: Option<JoinPlan>,
125 },
126}
127
128impl QuerySignature {
129 pub(crate) fn groups(&self) -> &[NeuralGroup] {
130 match self {
131 QuerySignature::Boolean { groups, .. } | QuerySignature::Targeted { groups, .. } => {
132 groups
133 }
134 }
135 }
136
137 pub(crate) fn hard_filters(&self) -> &[HardFilter] {
138 match self {
139 QuerySignature::Boolean { hard_filters, .. }
140 | QuerySignature::Targeted { hard_filters, .. } => hard_filters,
141 }
142 }
143
144 pub(crate) fn join(&self) -> Option<&JoinPlan> {
146 match self {
147 QuerySignature::Targeted { join, .. } => join.as_ref(),
148 QuerySignature::Boolean { .. } => None,
149 }
150 }
151}
152
153pub(crate) enum CompiledProbProgram {
154 Exact(ExactDdnnfProgram),
155 Mc(McProgram),
156}
157
158impl CompiledProbProgram {
159 #[cfg(feature = "host-io")]
160 pub(crate) fn num_vars(&self) -> usize {
161 match self {
162 Self::Exact(p) => p.num_vars(),
163 Self::Mc(p) => p.num_vars(),
164 }
165 }
166}
167
168#[cfg(feature = "host-io")]
173pub(crate) fn atom_to_string(atom: &xlog_prob::provenance::GroundAtom) -> String {
174 use xlog_prob::provenance::Value;
175
176 if atom.args.is_empty() {
177 return format!("{}()", atom.predicate);
178 }
179
180 let mut s = String::new();
181 s.push_str(&atom.predicate);
182 s.push('(');
183 for (i, arg) in atom.args.iter().enumerate() {
184 if i != 0 {
185 s.push_str(", ");
186 }
187 match arg {
188 Value::I64(v) => s.push_str(&v.to_string()),
189 Value::F64(bits) => s.push_str(&f64::from_bits(*bits).to_string()),
190 Value::Symbol(sym) => {
191 s.push_str(&symbol::resolve_checked(*sym).unwrap_or_else(|| format!("sym#{}", sym)))
192 }
193 Value::String(v) => s.push_str(v),
194 }
195 }
196 s.push(')');
197 s
198}
199
200impl CompiledProgram {
205 pub(crate) fn parse_sampling_method(s: Option<String>) -> PyResult<Option<McSamplingMethod>> {
206 match s.as_deref() {
207 None => Ok(None),
208 Some("rejection") => Ok(Some(McSamplingMethod::Rejection)),
209 Some("evidence_clamping") => Ok(Some(McSamplingMethod::EvidenceClamping)),
210 Some(other) => Err(PyValueError::new_err(format!(
211 "Unknown sampling_method '{}'. Use 'rejection' or 'evidence_clamping'.",
212 other
213 ))),
214 }
215 }
216
217 pub(crate) fn evaluate_query_probability(&self, query: &str) -> PyResult<f64> {
219 let probs = self.evaluate_query_probabilities(&[query.to_string()])?;
220 probs
221 .into_iter()
222 .next()
223 .ok_or_else(|| PyRuntimeError::new_err("Query evaluation returned no results"))
224 }
225
226 pub(crate) fn evaluate_query_probabilities(&self, queries: &[String]) -> PyResult<Vec<f64>> {
228 #[cfg(not(feature = "host-io"))]
229 {
230 let _ = queries;
231 return Err(types::host_io_disabled_pyerr());
232 }
233
234 #[cfg(feature = "host-io")]
235 {
236 let mut source_with_queries = self._source.clone();
238 for query in queries {
239 source_with_queries.push_str(&format!("\nquery({}).", query));
240 }
241
242 let result: Vec<QueryProbability> = match self._prob_engine {
244 xlog_logic::ast::ProbEngine::ExactDdnnf => {
245 let program = ExactDdnnfProgram::compile_source_with_gpu(
246 &source_with_queries,
247 self._gpu_config,
248 )
249 .map_err(|e| types::gpu_err("Query compilation error", e))?;
250
251 program
252 .evaluate()
253 .map_err(|e| types::gpu_err("Query evaluation error", e))?
254 .query_probs
255 }
256 xlog_logic::ast::ProbEngine::Mc => {
257 let program =
258 McProgram::compile_source_with_gpu(&source_with_queries, self._gpu_config)
259 .map_err(|e| types::gpu_err("Query compilation error", e))?;
260
261 let cfg = McEvalConfig::default();
262 program
263 .evaluate(cfg)
264 .map_err(|e| types::gpu_err("Query evaluation error", e))?
265 .query_estimates
266 .into_iter()
267 .map(|e| QueryProbability {
268 atom: e.atom,
269 prob: e.prob,
270 log_prob: e.log_prob,
271 })
272 .collect()
273 }
274 };
275
276 let probs: Vec<f64> = result.iter().map(|qp| qp.prob).collect();
279
280 if probs.len() != queries.len() {
281 return Err(PyRuntimeError::new_err(format!(
282 "Expected {} query results, got {}",
283 queries.len(),
284 probs.len()
285 )));
286 }
287
288 Ok(probs)
289 }
290 }
291
292 #[cfg(feature = "host-io")]
293 fn pack_result_probs(
294 &self,
295 py: Python<'_>,
296 query_probs: Vec<QueryProbability>,
297 log_z_e: f64,
298 ) -> PyResult<EvalResult> {
299 let mut atoms: Vec<String> = Vec::with_capacity(query_probs.len());
300 let mut probs: Vec<f64> = Vec::with_capacity(query_probs.len());
301 let mut log_probs: Vec<f64> = Vec::with_capacity(query_probs.len());
302
303 for q in query_probs {
304 atoms.push(atom_to_string(&q.atom));
305 probs.push(q.prob);
306 log_probs.push(q.log_prob);
307 }
308
309 let schema = Schema::new(vec![("col0".to_string(), ScalarType::F64)]);
310 let prob_buf = self
311 .output_provider
312 .create_buffer_from_slice::<f64>(&probs, schema.clone())
313 .map_err(types::xlog_err)?;
314 let log_prob_buf = self
315 .output_provider
316 .create_buffer_from_slice::<f64>(&log_probs, schema)
317 .map_err(types::xlog_err)?;
318
319 let prob_tensor = self
320 .output_provider
321 .to_dlpack_table(prob_buf)
322 .column(0)
323 .map_err(types::xlog_err)?;
324 let log_prob_tensor = self
325 .output_provider
326 .to_dlpack_table(log_prob_buf)
327 .column(0)
328 .map_err(types::xlog_err)?;
329
330 Ok(EvalResult {
331 atoms,
332 prob: dlpack_capsule_from_tensor(py, prob_tensor)?,
333 log_prob: dlpack_capsule_from_tensor(py, log_prob_tensor)?,
334 num_vars: self.program.num_vars(),
335 log_z_e: Some(log_z_e),
336 grad_true: None,
337 grad_false: None,
338 approx: false,
339 stderr: None,
340 ci_low: None,
341 ci_high: None,
342 samples: None,
343 evidence_samples: None,
344 seed: None,
345 confidence: None,
346 nonmonotone_semantics: None,
347 nonmonotone_sccs: None,
348 nonmonotone_cycles: None,
349 nonmonotone_iteration_limit_hits: None,
350 sampling_method: None,
351 mc_engine: None,
352 })
353 }
354
355 #[cfg(feature = "host-io")]
356 fn pack_result_with_grads(
357 &self,
358 py: Python<'_>,
359 result: ExactResultWithGrads,
360 ) -> PyResult<EvalResult> {
361 let mut atoms: Vec<String> = Vec::with_capacity(result.query_grads.len());
362 let mut probs: Vec<f64> = Vec::with_capacity(result.query_grads.len());
363 let mut log_probs: Vec<f64> = Vec::with_capacity(result.query_grads.len());
364
365 let mut grad_true_caps: Vec<Py<PyAny>> = Vec::with_capacity(result.query_grads.len());
366 let mut grad_false_caps: Vec<Py<PyAny>> = Vec::with_capacity(result.query_grads.len());
367
368 let schema = Schema::new(vec![("col0".to_string(), ScalarType::F64)]);
369
370 let num_vars = self.program.num_vars();
371 let log_z_e = result.log_z_e;
372 for q in result.query_grads {
373 atoms.push(atom_to_string(&q.atom));
374 probs.push(q.prob);
375 log_probs.push(q.log_prob);
376
377 let grad_true_buf = self
378 .output_provider
379 .create_buffer_from_slice::<f64>(&q.grad_true, schema.clone())
380 .map_err(types::xlog_err)?;
381 let grad_false_buf = self
382 .output_provider
383 .create_buffer_from_slice::<f64>(&q.grad_false, schema.clone())
384 .map_err(types::xlog_err)?;
385
386 let grad_true_tensor = self
387 .output_provider
388 .to_dlpack_table(grad_true_buf)
389 .column(0)
390 .map_err(types::xlog_err)?;
391 let grad_false_tensor = self
392 .output_provider
393 .to_dlpack_table(grad_false_buf)
394 .column(0)
395 .map_err(types::xlog_err)?;
396
397 grad_true_caps.push(dlpack_capsule_from_tensor(py, grad_true_tensor)?);
398 grad_false_caps.push(dlpack_capsule_from_tensor(py, grad_false_tensor)?);
399 }
400
401 let prob_buf = self
402 .output_provider
403 .create_buffer_from_slice::<f64>(&probs, schema.clone())
404 .map_err(types::xlog_err)?;
405 let log_prob_buf = self
406 .output_provider
407 .create_buffer_from_slice::<f64>(&log_probs, schema)
408 .map_err(types::xlog_err)?;
409
410 let prob_tensor = self
411 .output_provider
412 .to_dlpack_table(prob_buf)
413 .column(0)
414 .map_err(types::xlog_err)?;
415 let log_prob_tensor = self
416 .output_provider
417 .to_dlpack_table(log_prob_buf)
418 .column(0)
419 .map_err(types::xlog_err)?;
420
421 Ok(EvalResult {
422 atoms,
423 prob: dlpack_capsule_from_tensor(py, prob_tensor)?,
424 log_prob: dlpack_capsule_from_tensor(py, log_prob_tensor)?,
425 num_vars,
426 log_z_e: Some(log_z_e),
427 grad_true: Some(grad_true_caps),
428 grad_false: Some(grad_false_caps),
429 approx: false,
430 stderr: None,
431 ci_low: None,
432 ci_high: None,
433 samples: None,
434 evidence_samples: None,
435 seed: None,
436 confidence: None,
437 nonmonotone_semantics: None,
438 nonmonotone_sccs: None,
439 nonmonotone_cycles: None,
440 nonmonotone_iteration_limit_hits: None,
441 sampling_method: None,
442 mc_engine: None,
443 })
444 }
445
446 #[cfg(feature = "host-io")]
447 fn pack_result_mc(
448 &self,
449 py: Python<'_>,
450 result: xlog_prob::mc::McResult,
451 ) -> PyResult<EvalResult> {
452 let mut atoms: Vec<String> = Vec::with_capacity(result.query_estimates.len());
453 let mut probs: Vec<f64> = Vec::with_capacity(result.query_estimates.len());
454 let mut log_probs: Vec<f64> = Vec::with_capacity(result.query_estimates.len());
455 let mut stderrs: Vec<f64> = Vec::with_capacity(result.query_estimates.len());
456 let mut ci_lows: Vec<f64> = Vec::with_capacity(result.query_estimates.len());
457 let mut ci_highs: Vec<f64> = Vec::with_capacity(result.query_estimates.len());
458
459 for q in &result.query_estimates {
460 atoms.push(atom_to_string(&q.atom));
461 probs.push(q.prob);
462 log_probs.push(q.log_prob);
463 stderrs.push(q.stderr);
464 ci_lows.push(q.ci_low);
465 ci_highs.push(q.ci_high);
466 }
467
468 let schema = Schema::new(vec![("col0".to_string(), ScalarType::F64)]);
469 let prob_buf = self
470 .output_provider
471 .create_buffer_from_slice::<f64>(&probs, schema.clone())
472 .map_err(types::xlog_err)?;
473 let log_prob_buf = self
474 .output_provider
475 .create_buffer_from_slice::<f64>(&log_probs, schema.clone())
476 .map_err(types::xlog_err)?;
477 let stderr_buf = self
478 .output_provider
479 .create_buffer_from_slice::<f64>(&stderrs, schema.clone())
480 .map_err(types::xlog_err)?;
481 let ci_low_buf = self
482 .output_provider
483 .create_buffer_from_slice::<f64>(&ci_lows, schema.clone())
484 .map_err(types::xlog_err)?;
485 let ci_high_buf = self
486 .output_provider
487 .create_buffer_from_slice::<f64>(&ci_highs, schema)
488 .map_err(types::xlog_err)?;
489
490 let prob_tensor = self
491 .output_provider
492 .to_dlpack_table(prob_buf)
493 .column(0)
494 .map_err(types::xlog_err)?;
495 let log_prob_tensor = self
496 .output_provider
497 .to_dlpack_table(log_prob_buf)
498 .column(0)
499 .map_err(types::xlog_err)?;
500 let stderr_tensor = self
501 .output_provider
502 .to_dlpack_table(stderr_buf)
503 .column(0)
504 .map_err(types::xlog_err)?;
505 let ci_low_tensor = self
506 .output_provider
507 .to_dlpack_table(ci_low_buf)
508 .column(0)
509 .map_err(types::xlog_err)?;
510 let ci_high_tensor = self
511 .output_provider
512 .to_dlpack_table(ci_high_buf)
513 .column(0)
514 .map_err(types::xlog_err)?;
515
516 Ok(EvalResult {
517 atoms,
518 prob: dlpack_capsule_from_tensor(py, prob_tensor)?,
519 log_prob: dlpack_capsule_from_tensor(py, log_prob_tensor)?,
520 num_vars: self.program.num_vars(),
521 log_z_e: None,
522 grad_true: None,
523 grad_false: None,
524 approx: true,
525 stderr: Some(dlpack_capsule_from_tensor(py, stderr_tensor)?),
526 ci_low: Some(dlpack_capsule_from_tensor(py, ci_low_tensor)?),
527 ci_high: Some(dlpack_capsule_from_tensor(py, ci_high_tensor)?),
528 samples: Some(result.total_samples),
529 evidence_samples: Some(result.evidence_samples),
530 seed: Some(result.seed),
531 confidence: Some(result.confidence),
532 nonmonotone_semantics: Some(xlog_prob::mc::NONMONOTONE_SEMANTICS.to_string()),
533 nonmonotone_sccs: Some(result.nonmonotone_sccs),
534 nonmonotone_cycles: Some(result.nonmonotone_cycles),
535 nonmonotone_iteration_limit_hits: Some(result.nonmonotone_iteration_limit_hits),
536 sampling_method: Some(match result.sampling_method {
537 McSamplingMethod::Rejection => "rejection".to_string(),
538 McSamplingMethod::EvidenceClamping => "evidence_clamping".to_string(),
539 }),
540 mc_engine: Some(result.engine.as_str().to_string()),
541 })
542 }
543}
544
545#[pymethods]
550impl CompiledProgram {
551 #[pyo3(signature = (return_grads=false, samples=None, seed=None, confidence=0.95, max_nonmonotone_iterations=1024, sampling_method=None, memory_mb=None, allow_cpu_oracle=false))]
552 pub fn evaluate(
553 &self,
554 _py: Python<'_>,
555 return_grads: bool,
556 samples: Option<usize>,
557 seed: Option<u64>,
558 confidence: f64,
559 max_nonmonotone_iterations: usize,
560 sampling_method: Option<String>,
561 memory_mb: Option<u64>,
562 allow_cpu_oracle: bool,
563 ) -> PyResult<EvalResult> {
564 enforce_call_memory_limit(&self.output_provider, memory_mb)?;
565 match &self.program {
566 CompiledProbProgram::Exact(_program) => {
567 if samples.is_some() || seed.is_some() {
568 return Err(PyValueError::new_err(
569 "samples/seed are only supported for prob_engine='mc'",
570 ));
571 }
572 #[cfg(feature = "host-io")]
573 {
574 if return_grads {
575 let result = _program
576 .evaluate_gpu_with_grads()
577 .map_err(types::xlog_err)?;
578 self.pack_result_with_grads(_py, result)
579 } else {
580 let result = _program.evaluate().map_err(types::xlog_err)?;
581 self.pack_result_probs(_py, result.query_probs, result.log_z_e)
582 }
583 }
584 #[cfg(not(feature = "host-io"))]
585 {
586 let _ = return_grads;
587 Err(types::host_io_disabled_pyerr())
588 }
589 }
590 CompiledProbProgram::Mc(_program) => {
591 if return_grads {
592 return Err(PyValueError::new_err(
593 "MC inference does not support gradients (return_grads must be false)",
594 ));
595 }
596
597 let mut cfg = McEvalConfig::default();
598 cfg.samples = samples.unwrap_or(10000);
599 cfg.seed = seed.unwrap_or(0);
600 cfg.confidence = confidence;
601 cfg.max_nonmonotone_iterations = max_nonmonotone_iterations;
602 cfg.sampling_method = Self::parse_sampling_method(sampling_method)?;
603 cfg.allow_cpu_oracle_fallback = allow_cpu_oracle;
607 #[cfg(feature = "host-io")]
608 {
609 let result = _program.evaluate(cfg).map_err(types::xlog_err)?;
610 self.pack_result_mc(_py, result)
611 }
612 #[cfg(not(feature = "host-io"))]
613 {
614 let _ = cfg;
615 Err(types::host_io_disabled_pyerr())
616 }
617 }
618 }
619 }
620
621 pub fn prob_var_map(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
646 #[cfg(feature = "host-io")]
647 {
648 use xlog_prob::exact::ProbVarInfo;
649
650 let entries = match &self.program {
651 CompiledProbProgram::Exact(p) => {
652 if p.uses_gpu_native_count_lift() {
653 return Err(PyValueError::new_err(
654 "prob_var_map is unavailable: this program was compiled through \
655 the GPU count-lift fast path (a count aggregate without evidence \
656 or annotated disjunctions), which evaluates the aggregate with a \
657 dedicated kernel and never builds a CNF encoding. There is no \
658 CNF-variable-to-fact map to report. This does not mean the \
659 program has no probabilistic facts.",
660 ));
661 }
662 p.prob_var_map()
663 }
664 CompiledProbProgram::Mc(_) => {
665 return Err(PyValueError::new_err(
666 "prob_var_map is only available for the exact engine",
667 ))
668 }
669 };
670
671 let mut out: Vec<Py<PyAny>> = Vec::with_capacity(entries.len());
672 for entry in entries {
673 let d = PyDict::new(py);
674 match entry {
675 ProbVarInfo::Fact { atom, prob } => {
676 d.set_item("kind", "fact")?;
677 d.set_item("atom", atom_to_string(&atom))?;
678 d.set_item("prob", prob)?;
679 }
680 ProbVarInfo::Choice {
681 choices,
682 choice_index,
683 prob,
684 } => {
685 d.set_item("kind", "choice")?;
686 d.set_item(
687 "atoms",
688 choices
689 .iter()
690 .map(|(a, _)| atom_to_string(a))
691 .collect::<Vec<_>>(),
692 )?;
693 d.set_item(
697 "probs",
698 choices.iter().map(|(_, p)| *p).collect::<Vec<f64>>(),
699 )?;
700 d.set_item("choice_index", choice_index)?;
701 d.set_item("prob", prob)?;
707 }
708 ProbVarInfo::Other => {
709 d.set_item("kind", "other")?;
710 }
711 }
712 out.push(d.into());
713 }
714 Ok(out)
715 }
716 #[cfg(not(feature = "host-io"))]
717 {
718 let _ = py;
719 Err(types::host_io_disabled_pyerr())
720 }
721 }
722
723 #[pyo3(signature = (samples=None, seed=None, confidence=0.95, max_nonmonotone_iterations=1024, sampling_method=None, memory_mb=None))]
728 pub fn evaluate_device(
729 &self,
730 py: Python<'_>,
731 samples: Option<usize>,
732 seed: Option<u64>,
733 confidence: f64,
734 max_nonmonotone_iterations: usize,
735 sampling_method: Option<String>,
736 memory_mb: Option<u64>,
737 ) -> PyResult<McDeviceEvalResult> {
738 enforce_call_memory_limit(&self.output_provider, memory_mb)?;
739 let (
740 query_counts,
741 evidence_count,
742 total_samples,
743 seed,
744 confidence,
745 nonmonotone_sccs,
746 nonmonotone_cycles,
747 nonmonotone_iteration_limit_hits,
748 sampling_method_val,
749 no_host,
750 ) = match &self.program {
751 CompiledProbProgram::Mc(program) => {
752 let mut cfg = McEvalConfig::default();
753 cfg.samples = samples.unwrap_or(10000);
754 cfg.seed = seed.unwrap_or(0);
755 cfg.confidence = confidence;
756 cfg.max_nonmonotone_iterations = max_nonmonotone_iterations;
757 cfg.sampling_method = Self::parse_sampling_method(sampling_method)?;
758
759 let result = program
760 .evaluate_gpu_device_with_provider(cfg, self.output_provider.clone())
761 .map_err(types::xlog_err)?;
762
763 (
764 result.query_counts,
765 result.evidence_count,
766 result.total_samples,
767 result.seed,
768 result.confidence,
769 result.nonmonotone_sccs,
770 result.nonmonotone_cycles,
771 result.nonmonotone_iteration_limit_hits,
772 result.sampling_method,
773 result.no_host,
774 )
775 }
776 _ => {
777 return Err(PyValueError::new_err(
778 "evaluate_device is only supported for prob_engine='mc'",
779 ))
780 }
781 };
782
783 let schema_i32 = Schema::new(vec![("col0".to_string(), ScalarType::I32)]);
786
787 let make_count_tensor =
788 |counts: xlog_cuda::memory::TrackedCudaSlice<u32>, rows: u64| -> PyResult<Py<PyAny>> {
789 let rows_u32 = u32::try_from(rows).map_err(|_| {
790 PyValueError::new_err(format!("Row count {} exceeds u32::MAX", rows))
791 })?;
792
793 let mut d_num_rows = self
794 .output_provider
795 .memory()
796 .alloc::<u32>(1)
797 .map_err(types::xlog_err)?;
798 self.output_provider
799 .device()
800 .inner()
801 .htod_sync_copy_into(&[rows_u32], &mut d_num_rows)
802 .map_err(types::xlog_err)?;
803
804 let buffer = xlog_cuda::CudaBuffer::from_columns(
805 vec![counts.into_bytes().into()],
806 rows,
807 d_num_rows,
808 schema_i32.clone(),
809 );
810 let tensor = self
811 .output_provider
812 .to_dlpack_table(buffer)
813 .column(0)
814 .map_err(types::xlog_err)?;
815 dlpack_capsule_from_tensor(py, tensor)
816 };
817
818 let query_rows = u64::try_from(query_counts.len())
819 .map_err(|_| PyValueError::new_err("query_counts length overflow"))?;
820 let query_counts_capsule = make_count_tensor(query_counts, query_rows)?;
821 let evidence_count_capsule = make_count_tensor(evidence_count, 1)?;
822 let resident_no_host_certified = no_host.is_no_host();
823
824 Ok(McDeviceEvalResult {
825 query_counts: query_counts_capsule,
826 evidence_count: evidence_count_capsule,
827 total_samples,
828 seed,
829 confidence,
830 nonmonotone_semantics: xlog_prob::mc::NONMONOTONE_SEMANTICS.to_string(),
831 nonmonotone_sccs,
832 nonmonotone_cycles,
833 nonmonotone_iteration_limit_hits,
834 sampling_method: match sampling_method_val {
835 McSamplingMethod::Rejection => "rejection".to_string(),
836 McSamplingMethod::EvidenceClamping => "evidence_clamping".to_string(),
837 },
838 resident_no_host_certified,
839 resident_no_host_policy_result: if resident_no_host_certified {
840 "certified".to_string()
841 } else {
842 "failed".to_string()
843 },
844 resident_no_host_tracked_dtoh_calls: no_host.tracked_dtoh_calls,
845 resident_no_host_tracked_htod_calls: no_host.tracked_htod_calls,
846 resident_no_host_host_loop_iterations: no_host.host_loop_iterations,
847 resident_no_host_per_sample_host_launches: no_host.per_sample_host_launches,
848 resident_no_host_untracked_metadata_reads: no_host.untracked_metadata_reads,
849 resident_no_host_engine_launches: no_host.engine_launches,
850 resident_no_host_host_fixpoint_iterations: no_host.host_fixpoint_iterations,
851 resident_no_host_per_operator_host_allocations: no_host.per_operator_host_allocations,
852 })
853 }
854
855 fn nll_loss(&self, query: &str) -> PyResult<f64> {
872 let prob = self.evaluate_query_probability(query)?;
873 Ok(types::nll_loss_value(prob))
874 }
875
876 fn nll_loss_batch(&self, queries: Vec<String>) -> PyResult<f64> {
889 if queries.is_empty() {
890 return Ok(0.0);
891 }
892
893 let probs = self.evaluate_query_probabilities(&queries)?;
894 Ok(probs.iter().map(|&p| types::nll_loss_value(p)).sum())
895 }
896
897 fn nll_loss_mean(&self, queries: Vec<String>) -> PyResult<f64> {
912 if queries.is_empty() {
913 return Err(PyValueError::new_err(
914 "Cannot compute mean NLL loss for empty query batch",
915 ));
916 }
917
918 let probs = self.evaluate_query_probabilities(&queries)?;
919 let sum: f64 = probs.iter().map(|&p| types::nll_loss_value(p)).sum();
920 Ok(sum / probs.len() as f64)
921 }
922
923 fn nll_loss_tensor(&self, py: Python<'_>, query: &str) -> PyResult<Py<PyAny>> {
934 let loss = self.nll_loss(query)?;
935 types::create_torch_tensor(py, loss)
936 }
937
938 fn nll_loss_batch_tensor(&self, py: Python<'_>, queries: Vec<String>) -> PyResult<Py<PyAny>> {
946 let loss = self.nll_loss_batch(queries)?;
947 types::create_torch_tensor(py, loss)
948 }
949
950 pub fn zero_grad(&self, py: Python<'_>) -> PyResult<()> {
959 for name in self.network_registry.names() {
960 if let Some(handle) = self.network_registry.get(name) {
961 if let Some(optimizer) = handle.optimizer() {
962 optimizer.call_method0(py, "zero_grad")?;
963 }
964 }
965 }
966 Ok(())
967 }
968
969 pub fn optimizer_step(&self, py: Python<'_>) -> PyResult<()> {
974 for name in self.network_registry.names() {
975 if let Some(handle) = self.network_registry.get(name) {
976 if let Some(optimizer) = handle.optimizer() {
977 optimizer.call_method0(py, "step")?;
978 }
979 }
980 }
981 Ok(())
982 }
983
984 pub fn clip_grad_norms(&self, py: Python<'_>, max_norm: f64) -> PyResult<()> {
988 let clip_fn = py.import("torch.nn.utils")?.getattr("clip_grad_norm_")?;
989 for name in self.network_registry.names() {
990 if let Some(handle) = self.network_registry.get(name) {
991 if let Some(module) = handle.module() {
992 let params = module.call_method0(py, "parameters")?;
993 clip_fn.call1((params, max_norm))?;
994 }
995 }
996 }
997 Ok(())
998 }
999
1000 #[pyo3(signature = (network_name=None))]
1009 fn scheduler_step(&self, py: Python<'_>, network_name: Option<&str>) -> PyResult<()> {
1010 match network_name {
1011 Some(name) => {
1012 let handle = self.network_registry.get(name).ok_or_else(|| {
1013 pyo3::exceptions::PyValueError::new_err(format!(
1014 "No network registered with name '{name}'"
1015 ))
1016 })?;
1017 if let Some(scheduler) = handle.scheduler() {
1018 scheduler.call_method0(py, "step")?;
1019 }
1020 }
1021 None => {
1022 for name in self.network_registry.names() {
1023 if let Some(handle) = self.network_registry.get(name) {
1024 if let Some(scheduler) = handle.scheduler() {
1025 scheduler.call_method0(py, "step")?;
1026 }
1027 }
1028 }
1029 }
1030 }
1031 Ok(())
1032 }
1033
1034 fn get_lr(&self, py: Python<'_>, network_name: &str) -> PyResult<f64> {
1041 let handle = self.network_registry.get(network_name).ok_or_else(|| {
1042 pyo3::exceptions::PyValueError::new_err(format!(
1043 "No network registered with name '{network_name}'"
1044 ))
1045 })?;
1046 let optimizer = handle.optimizer().ok_or_else(|| {
1047 pyo3::exceptions::PyValueError::new_err(format!(
1048 "Network '{network_name}' has no optimizer"
1049 ))
1050 })?;
1051 let param_groups = optimizer.getattr(py, "param_groups")?;
1052 let group0 = param_groups.call_method1(py, "__getitem__", (0i32,))?;
1053 let lr = group0.call_method1(py, "__getitem__", ("lr",))?;
1054 lr.extract(py)
1055 }
1056
1057 fn set_lr(&self, py: Python<'_>, network_name: &str, lr: f64) -> PyResult<()> {
1065 let handle = self.network_registry.get(network_name).ok_or_else(|| {
1066 pyo3::exceptions::PyValueError::new_err(format!(
1067 "No network registered with name '{network_name}'"
1068 ))
1069 })?;
1070 let optimizer = handle.optimizer().ok_or_else(|| {
1071 pyo3::exceptions::PyValueError::new_err(format!(
1072 "Network '{network_name}' has no optimizer"
1073 ))
1074 })?;
1075 let param_groups = optimizer.getattr(py, "param_groups")?;
1076 let num_groups: usize = param_groups.call_method0(py, "__len__")?.extract(py)?;
1077 for i in 0..num_groups {
1078 let group = param_groups.call_method1(py, "__getitem__", (i as i32,))?;
1079 group.call_method(py, "__setitem__", ("lr", lr), None)?;
1080 }
1081 Ok(())
1082 }
1083
1084 #[pyo3(signature = (queries, batch_size=32, max_grad_norm=None))]
1102 fn train_epoch(
1103 &mut self,
1104 py: Python<'_>,
1105 queries: Vec<String>,
1106 batch_size: usize,
1107 max_grad_norm: Option<f64>,
1108 ) -> PyResult<EpochStats> {
1109 let mut history = TrainingHistory::new();
1110 self.train_epoch_internal(
1111 py,
1112 &queries,
1113 batch_size,
1114 usize::MAX,
1115 max_grad_norm,
1116 &mut history,
1117 )
1118 }
1119
1120 pub fn evaluate_loss(&self, queries: Vec<String>) -> PyResult<f64> {
1130 if queries.is_empty() {
1131 return Ok(0.0);
1132 }
1133
1134 let probs = self.evaluate_query_probabilities(&queries)?;
1135 let total_loss: f64 = probs.iter().map(|&p| types::nll_loss_value(p)).sum();
1136 Ok(total_loss / queries.len() as f64)
1137 }
1138
1139 #[pyo3(signature = (queries, batch_size=32, max_grad_norm=None))]
1141 fn train_epoch_tensor(
1142 &mut self,
1143 py: Python<'_>,
1144 queries: Vec<String>,
1145 batch_size: usize,
1146 max_grad_norm: Option<f64>,
1147 ) -> PyResult<EpochStats> {
1148 let mut history = TrainingHistory::new();
1149 self.train_epoch_tensor_internal(
1150 py,
1151 &queries,
1152 batch_size,
1153 usize::MAX,
1154 max_grad_norm,
1155 &mut history,
1156 )
1157 }
1158
1159 fn warmup_breakdown(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
1166 let ptx_profile = self.output_provider.ptx_load_profile();
1167 let circuit_profile = self
1174 .last_compile_profile
1175 .as_ref()
1176 .or_else(|| match &self.program {
1177 CompiledProbProgram::Exact(p) => p.last_compile_profile(),
1178 CompiledProbProgram::Mc(_) => None,
1179 });
1180
1181 if ptx_profile.is_none() && circuit_profile.is_none() {
1183 return Ok(None);
1184 }
1185
1186 let result = PyDict::new(py);
1187
1188 if let Some(ptx) = ptx_profile {
1189 let ptx_dict = PyDict::new(py);
1190 ptx_dict.set_item("total_sec", ptx.total_sec)?;
1191 ptx_dict.set_item("cubin_loaded", ptx.cubin_loaded)?;
1192 ptx_dict.set_item("ptx_fallback", ptx.ptx_fallback)?;
1193 let per_module = PyDict::new(py);
1194 for (name, sec) in &ptx.per_module_sec {
1195 per_module.set_item(name, *sec)?;
1196 }
1197 ptx_dict.set_item("per_module_sec", per_module)?;
1198 result.set_item("ptx", ptx_dict)?;
1199 }
1200
1201 if let Some(circuit) = circuit_profile {
1202 let circuit_dict = PyDict::new(py);
1203 circuit_dict.set_item("gpu_cache_hit", circuit.gpu_cache_hit)?;
1204 circuit_dict.set_item("disk_cache_hit", circuit.disk_cache_hit)?;
1205 circuit_dict.set_item("d4_compile_sec", circuit.d4_compile_sec)?;
1206 circuit_dict.set_item("verify_sec", circuit.verify_sec)?;
1207 circuit_dict.set_item("smooth_sec", circuit.smooth_sec)?;
1208 circuit_dict.set_item("cache_store_sec", circuit.cache_store_sec)?;
1209 circuit_dict.set_item("free_var_mask_sec", circuit.free_var_mask_sec)?;
1210 circuit_dict.set_item("cnf_hash_sec", circuit.cnf_hash_sec)?;
1211 result.set_item("circuit", circuit_dict)?;
1212 }
1213
1214 Ok(Some(result.into()))
1215 }
1216
1217 fn clear_circuit_cache(&mut self) {
1220 self.circuit_cache.clear();
1221 }
1222
1223 pub fn memory_stats(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1225 provider_memory_stats(py, &self.output_provider)
1226 }
1227
1228 pub fn rule_provenance(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1229 let provenance = xlog_logic::rule_provenance(&self.ast, None);
1230 pack_rule_provenance(py, &provenance)
1231 }
1232
1233 pub fn proof_traces(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1234 let provenance = xlog_logic::rule_provenance(&self.ast, None);
1235 let traces = xlog_logic::query_proof_traces(&self.ast, &provenance);
1236 pack_query_proof_traces(py, &traces)
1237 }
1238
1239 pub fn host_transfer_stats(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1240 let stats = self.output_provider.host_transfer_stats();
1241 let dict = PyDict::new(py);
1242 dict.set_item("dtoh_bytes", stats.dtoh_bytes)?;
1243 dict.set_item("htod_bytes", stats.htod_bytes)?;
1244 dict.set_item("dtoh_calls", stats.dtoh_calls)?;
1245 dict.set_item("htod_calls", stats.htod_calls)?;
1246 Ok(dict.into())
1247 }
1248
1249 pub fn reset_host_transfer_stats(&self) {
1250 self.output_provider.reset_host_transfer_stats()
1251 }
1252
1253 pub fn neural_hot_loop_diagnostics(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1254 let transfers = self.output_provider.host_transfer_stats();
1255 let dict = PyDict::new(py);
1256 dict.set_item("post_load_dtoh_bytes", transfers.dtoh_bytes)?;
1257 dict.set_item("post_load_htod_bytes", transfers.htod_bytes)?;
1258 dict.set_item("post_load_dtoh_calls", transfers.dtoh_calls)?;
1259 dict.set_item("post_load_htod_calls", transfers.htod_calls)?;
1260 dict.set_item("control_plane_bytes_per_iteration", py.None())?;
1261 dict.set_item(
1262 "control_plane_status",
1263 "unavailable: per-iteration control-plane byte counter is not registered",
1264 )?;
1265 dict.set_item("scalar_sync_checks", py.None())?;
1266 dict.set_item(
1267 "scalar_sync_status",
1268 "unavailable: scalar synchronization counter is not registered",
1269 )?;
1270
1271 let cuda_graph = PyDict::new(py);
1272 cuda_graph.set_item(
1273 "csm_cuda_graph_captures",
1274 self.output_provider.csm_cuda_graph_captures(),
1275 )?;
1276 cuda_graph.set_item(
1277 "csm_cuda_graph_launches",
1278 self.output_provider.csm_cuda_graph_launches(),
1279 )?;
1280 cuda_graph.set_item(
1281 "csm_cuda_graph_fallbacks",
1282 self.output_provider.csm_cuda_graph_fallbacks(),
1283 )?;
1284 cuda_graph.set_item(
1285 "csm_cuda_graph_cache_hits",
1286 self.output_provider.csm_cuda_graph_cache_hits(),
1287 )?;
1288 dict.set_item("cuda_graph", cuda_graph)?;
1289
1290 let circuit_cache = PyDict::new(py);
1291 circuit_cache.set_item("circuit_cache_size", self.circuit_cache.len())?;
1292 circuit_cache.set_item("circuit_cache_hits", self.circuit_cache_hits)?;
1293 circuit_cache.set_item("circuit_cache_misses", self.circuit_cache_misses)?;
1294 circuit_cache.set_item("template_compile_count", self.template_compile_count)?;
1295 circuit_cache.set_item(
1296 "query_signature_cache_size",
1297 self.query_signature_cache.len(),
1298 )?;
1299 dict.set_item("circuit_cache", circuit_cache)?;
1300
1301 Ok(dict.into())
1302 }
1303
1304 pub fn cuda_graph_stats(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1305 let dict = PyDict::new(py);
1306 dict.set_item(
1307 "csm_cuda_graph_captures",
1308 self.output_provider.csm_cuda_graph_captures(),
1309 )?;
1310 dict.set_item(
1311 "csm_cuda_graph_launches",
1312 self.output_provider.csm_cuda_graph_launches(),
1313 )?;
1314 dict.set_item(
1315 "csm_cuda_graph_fallbacks",
1316 self.output_provider.csm_cuda_graph_fallbacks(),
1317 )?;
1318 dict.set_item(
1319 "csm_cuda_graph_cache_hits",
1320 self.output_provider.csm_cuda_graph_cache_hits(),
1321 )?;
1322 Ok(dict.into())
1323 }
1324}