pyxlog lets you compile and run XLOG programs from Python and exchange CUDA
tensors with PyTorch, CuPy, JAX, and TensorFlow through DLPack. Transient inputs
and query-result handoffs share device allocations; persistent relation imports
and stored-relation exports make device-to-device ownership copies so session
state cannot be mutated through an external tensor.
Always
import pyxlog, never pyxlog._native. The package has two layers: a native
PyO3 extension (pyxlog._native) and a pure-Python wrapper that re-exports it and adds
convenience methods. Some documented methods — including evaluate_async,
evaluate_stream, the relation callbacks, temporal provenance, and the nn/4 lineage
helpers — live only on the wrapped classes. Importing from pyxlog._native directly
will make those methods appear missing.Overview
Thepyxlog Python module provides:
- Deterministic (ordinary, non-probabilistic) Datalog execution via
LogicProgram - Probabilistic inference (facts and rules carry probabilities) via
Program - Term embedding registration and lookup via
register_embedding/forward_embedding - Differentiable inductive logic programming (ILP) — learning Datalog rules from
labeled examples — via
pyxlog.ilp - Reusable diagnostics for downstream applications (“external consumers”): learned-rule inventories, audits of the tight GPU inner loop (“hot loop”), and grouped transfer metrics
- DLPack GPU tensor exchange, with zero-copy transient inputs and query-result handoffs plus owned persistent relation storage
- Optional, experimental Apache Arrow C Device interop (enabled by a build feature)
- Runtime introspection surfaces (“living-world diagnostics”): rule provenance, proof traces, incremental relation-change (“delta”) debugging, temporal relation metadata, native whole-fact relation evidence, and neural hot-loop audits
host-io Cargo build feature. Keeping them
optional lets GPU-native code enforce a rule that results never transfer from the
device (GPU) back to host (CPU) memory. For the full map of runtime introspection
surfaces, see Living-World Diagnostics.
Installation
To install a published wheel:pyxlog checks for bundled CUDA kernel artifacts under
pyxlog/kernels/ and, when present, exports that directory to
XLOG_CUBIN_DIR automatically. Any pilot script, probe harness, or artifact
replay that runs outside the packaged wheel layout should set
XLOG_CUBIN_DIR explicitly before importing pyxlog, for example:
XLOG_CUBIN_DIR, startup can fail if the active install does not contain
pyxlog/kernels/.
For local development or an API present in this checkout but absent from the
selected wheel:
maturin build -i, installs the wheel
with the same interpreter’s pip, and verifies that the installed package has
pyxlog/kernels/. Generated .ptx and .cubin files remain build artifacts
and are not tracked in git.
Build Features
Build features are compile-time flags that turn optional API surfaces on:host-io: enable host-read convenience APIs (e.g.CompiledProgram.evaluate(...))arrow-device-import: enable experimental Arrow C Device export/import helpers
Package Details
API Reference
LogicProgram (Deterministic)
Compiles and runs an ordinary (non-probabilistic) Datalog program on the GPU and returns each query’s answer as DLPack columns (one tensor per column).Supplying Input Relations (DLPack)
CompiledLogicProgram.evaluate(dlpack_inputs=...) accepts a dict mapping relation name to a
sequence of DLPack columns.
Persistent Named Relations (DLPack)
For repeated evaluation with long-lived GPU relations, create a persistent session instead of re-supplyingdlpack_inputs on every call.
evaluate(dlpack_inputs=...)remains the stateless one-shot APIsession()exposes a mutable named relation store with schema-checked DLPack import/export
Persistent Relation Deltas
A delta is an incremental change to a relation — rows to add or remove — applied without recomputing the whole program. Persistent sessions accept DLPack-backed deltas so a caller can update its data in a loop.insert_relation(...), delete_relation(...), and apply_relation_delta(...)
update the session’s stored relations through the runtime’s incremental-recompute
path (RelationDelta / apply_deltas_and_recompute). Updates that only insert
rows into a mutually-recursive relation group (a strongly connected component, or
SCC) reuse previously computed output where the plan allows. Updates that delete
rows clear and recompute the affected groups so results stay correct.
changed_relations, insert_rows,
delete_rows, affected_sccs, recomputed_sccs, incremental_sccs,
input_delta_count, coalesced_insert_rows, coalesced_delete_rows, and
canceled_rows. Delta debug output also includes
changed_relation_names, equivalent_to_full_recompute, debug_trace, and
nested planner_telemetry. Planner telemetry reports cache_reused,
fallback_decision, affected/recomputed/incremental SCC counts,
estimated_delta_speedup, measured_delta_speedup, and planner_advice.
equivalent_to_full_recompute is None unless the caller opts into
check_equivalence=True.
Batch updates merge repeated changes to the same relation before the runtime
recomputes, using device-resident set operations. Update dictionaries reject
unknown keys before any relation is changed. A batch whose inserts and deletes
cancel completely is a semantic no-op: changed_relations is 0, no runtime
version or callback generation advances, and no callback fires for the canceled
relation. Callback and diagnostic code must not copy relation rows down to the
host.
If a delta operation fails before commit but after preparation takes ownership
of cached derived state, the authoritative relation rows and evidence remain
unchanged, but XLOG discards the derived cache and retained runtime. The next
evaluate() rebuilds them. This makes failure recovery safe at the cost of
losing cache-hit and incremental-planner continuity for that attempt.
Calling put_relation, remove_relation, or clear_relations directly
invalidates the cached runtime store. The next evaluate() then does a full plan
run before later deltas can reuse it.
Persistent sessions keep their runtime executor across evaluate() and
delta recompute calls, so persistent hash indexes can be reused through public
pyxlog mutation loops. session.join_index_cache_stats() returns the retained
executor’s lookups, hits, misses, builds, invalidation counters,
background-build counters, entries, and total_bytes.
Relation Change Callbacks
Persistent sessions expose opt-in metadata callbacks for relation delta commits:relation, generation, input_delta_count,
insert_rows, delete_rows, has_deletes, coalesced_insert_rows,
coalesced_delete_rows, canceled_rows, affected_sccs,
recomputed_sccs, incremental_sccs, and nested telemetry.
Callbacks are invoked synchronously while the pyxlog method holds the Python
GIL. Registration order is callback order, and relation events are emitted in
the caller’s update order after duplicate relation names are coalesced. This
ordering is deterministic: a regression fixture confirms that 100 repeated runs
produce identical callback sequences. Relations whose net batch update cancels
completely are omitted from mixed-batch callback sequences and do not consume a
generation number. Building a callback payload does not export DLPack tensors or
download relation data rows; use explicit evaluate() or export_relation()
when you actually need the rows materialized.
Rule, proof, temporal, and relation provenance
Compiled logic/probabilistic programs and sessions can report where their results came from (provenance):rule_provenance() returns stable rule_id, source_kind,
generation_trace_hash, support_relation_ids, and
counterexample_relation_ids fields. proof_traces() returns each query’s
answer relation, deriving rule ids, source facts, and rejected alternatives.
Temporal stream loads can keep provenance metadata next to the relation:
timestamp_column, dataset_id,
row_hashes, field_hashes, uncertainty, stream_id, source,
order_column, process_boundary, and temporal_order. Temporal metadata is a
Python helper facility; it is separate from the native whole-fact evidence API
below.
Native relation evidence binds ordered semantic roles and provenance records to
complete facts of any positive arity. The role order must match the compiled
predicate argument order, and each fact is identified by all of its cells:
name; optional sort and type fields, when present,
must match the compiled schema. Returned snapshots resolve all three fields.
Source-named predicate arguments require their compiled names. Positional
arguments accept application-defined role names on the first metadata-bearing
load, then enforce that role contract on later metadata-bearing replacements and
manifest imports. A plain put_relation, a manifest import with
metadata_present=False, remove_relation, or clear_relations removes that
positional role contract. A later metadata-bearing load may then register new
positional names. Source-named arguments always remain bound to their compiled
names.
A fact supplies exactly one of tuple or cells. tuple is the convenient
Python representation. cells is a sequence of exact
{"type": ..., "hex": ...} values for bit-preserving values such as NaNs and
signed zero. Provenance records may contain source, document, span,
content_hash, kind, and polarity; at least one field must be non-null. Two
different records for the same complete tuple remain distinct. Replacement rows
retain their stored multiplicity, so row_count includes duplicate rows. Evidence
is keyed by the distinct complete typed tuple rather than by row offset: duplicate
stored rows share one fact entry, repeated fact entries merge their records, exact
duplicate records collapse, and facts and records are returned in deterministic
canonical order.
Evidence follows the native relation lifecycle atomically:
put_relation_with_provenancereplaces both rows and evidence;- plain
put_relationreplaces the rows and clears old evidence and any positional role contract; insert_relation(..., facts=...)andapply_relation_delta(..., insert_facts=...)add evidence for inserted facts;- batch and debug updates accept
insert_factsin each update dictionary; - deleting a complete fact deletes its evidence, and coalesced or canceled batch updates cannot leave stale evidence; and
- metadata-free manifest replacement,
remove_relation, andclear_relationsremove the matching evidence and positional role contract.
facts
or insert_facts requires insert columns for the same update, a positive-arity
relation, and a role contract previously registered by a metadata-bearing
replacement or manifest import. Every annotated fact must occur in that specific
insert buffer; being present only in the session’s existing rows is not enough.
Passing an empty facts=[] or insert_facts=[] still opts into these contract
checks, although it performs no membership-mask transfer. Duplicate or already
stored inserted rows may add distinct provenance records when the annotated fact
is present in the insert buffer.
All role, type, arity, evidence-tuple membership, and batch validation completes
before mutation. A validation or pre-commit preparation failure leaves relation
rows, evidence, delta statistics, callback generations, and callbacks unchanged.
Nullary relations support plain put_relation, metadata-free inserts, deletes,
deltas, evaluation, relation, and evidence. They reject
put_relation_with_provenance, every put_relation_from_manifest and
export_relation_with_provenance call, and any insert, combined delta, batch, or
debug update that supplies facts or insert_facts (including an empty list).
Persistent replacement methods take a device-to-device snapshot of imported
DLPack columns before committing them. Mutating a retained producer tensor after
put_relation, put_relation_with_provenance, or
put_relation_from_manifest therefore cannot change stored rows behind the
session’s versions, callbacks, or evidence. This is an owned GPU snapshot, not a
zero-copy persistent import. Transient evaluate(dlpack_inputs=...) inputs and
the handoff of query-result buffers to a DLPack consumer remain zero-copy.
For each tensor-like input, XLOG calls __dlpack_device__() exactly once. Only
CUDA device memory (kDLCUDA) is accepted; another device raises BufferError
before XLOG requests or consumes a capsule. XLOG then calls
__dlpack__(stream=1) exactly once so the CUDA producer orders pending work
before consumption on the legacy default stream. If the producer rejects that
stream argument, the exception propagates; XLOG does not retry without a stream.
Raw capsules bypass both protocol calls. The caller must create each capsule for
stream 1 or synchronize its producer first, and must pass it to only one
consumer. The native importer still validates the capsule’s device header.
Breaking in 0.12.0. This device gate is new, and it applies to every pyxlog
entry point that accepts a __dlpack__ object. A CPU tensor that previously
travelled some distance into XLOG before failing now raises BufferError
immediately, at import.
Membership is checked as complete tuples on the GPU. For a non-empty evidence
set, the runtime downloads one boolean membership mask in a single transfer—one
byte per distinct fact—not the relation rows. Role-only metadata and
metadata-free manifests need no membership transfer. Use
set_strict_deterministic_d2h(True) to reject even this deterministic mask
transfer; deterministic_d2h_violation_count() reports rejected attempts, and a
rejection is atomic.
relation(name) returns a frozen native RelationEvidence captured at that
call. Later session changes do not alter it, and each provenance() call returns
fresh Python dictionaries and lists whose mutation cannot change the captured
snapshot. evidence(name=None) returns a deterministic program_hash and a
relations mapping of packed snapshot dictionaries. With evidence(name), XLOG
still computes program_hash over every stored relation in the session and only
then filters the returned relations mapping to name; named and unfiltered
reads therefore share the same hash at the same session state. These are native
session snapshots, not Python package sidecar records.
Invalid role, whole-fact provenance, insert-evidence, or manifest input raises
pyxlog.RelationMetadataError, a ValueError subclass. Looking up an unstored
relation with relation(name) or evidence(name) raises KeyError. Without
pyxlog._native, the package-level RelationMetadataError and
RelationEvidence names remain importable. The fallback metadata error still
subclasses ValueError; constructing the fallback RelationEvidence raises
RuntimeError because it has no native snapshot. Native evidence instances and
all session operations require the extension. Source builds that compile the
extension expose this API; for a packaged build, determine availability from that
release’s notes.
Provenance manifest round trips
Use the paired DLPack-and-manifest API to reconstruct rows and native evidence in another compatible session:xlog.relation-provenance version 1 shape. Its
required top-level fields are format, version, predicate, row_count,
metadata_present, roles, and facts. predicate contains name, arity,
and the compiled schema_sha256. Manifest facts contain identity, exact
cells, and fixed-shape provenance records; they intentionally omit the
friendly tuple. Every dictionary level rejects missing or unknown fields.
version, arity, and row_count must be non-negative Python integers, not
booleans, and metadata_present must be an actual Python bool. If it is false,
both roles and facts must be empty and importing the manifest resets any
registered positional role contract.
Each exact cell contains only type and hex. Its type must match the compiled
column, and its lowercase hexadecimal value must encode exactly that scalar
type’s little-endian byte width; a boolean cell is exactly 00 or 01. Manifest
provenance records contain all six fields (source, document, span,
content_hash, kind, and polarity), using None for absent values. A
non-null span contains exactly start and end; both are non-negative Python
integers rather than booleans, both must be representable as u64, and
start <= end.
The schema fingerprint includes the predicate name and arity plus every compiled
column’s name, scalar type, and optional source-domain sort. A fact identity
includes the predicate name and arity plus each cell’s type code, byte length,
and exact bytes; it does not include provenance records. Both hashes use
domain-separated SHA-256 inputs. Fact identity is independent of row position
and role labels, and import recomputes both hashes instead of trusting the
supplied strings.
Import validates static manifest structure, compiled schema identity, and column
count before consuming a DLPack capsule. Once column import starts, all supplied
capsules are consumed before dtype, equal-column-length, manifest row-count, and
whole-fact membership validation completes. A failure at any of those stages
still leaves the target relation and evidence unchanged, but the spent source
capsules cannot be reused. Successful import consumes every column once.
Fact and record order and exact duplicates are normalized deterministically.
The DLPack columns are process-local, single-consumer ownership objects; the
manifest may be serialized as data, but it does not contain the relation columns
and is not a cross-process persistence format. Keep the exported columns alive
until import consumes them. For portable storage, use a host-serialized relation
format and treat native manifest reconstruction as a separate in-process
operation.
Migrating from Python-side relation evidence
The native API replaces the earlier Python helper contract. The oldrelation_schema, source_path, source_hash, row_hashes, field_hashes,
accepted_count, rejected_count, output_path, output_hash, and
decision_counts keyword arguments are not accepted by
put_relation_with_provenance. Pass ordered roles and complete facts
instead. A per-fact source location can map to a record’s source or document,
and a per-fact source digest can map to content_hash; XLOG derives exact fact
identities from the typed cells. Row/field hash collections, output locations and
hashes, and aggregate acceptance, rejection, and decision counters remain
application-level data rather than native whole-fact evidence. The returned
snapshot and evidence() payload contain native roles and facts, and unknown
named reads raise KeyError instead of returning an empty sidecar record.
Runtime controls and diagnostics
Long-running callers can submit logic or probabilistic evaluations to a background Python worker withevaluate_async(...). The returned
AsyncEvaluation is awaitable and also exposes done(), cancel(),
exception(), and result(timeout=None) for synchronous orchestration.
memory_mb is accepted by CompiledLogicProgram.evaluate,
LogicRelationSession.evaluate, CompiledProgram.evaluate, and
CompiledProgram.evaluate_device. A zero limit raises ValueError; a limit
below the provider’s current tracked allocation raises MemoryError before the
evaluation starts. The provider-level compile-time budget remains the hard GPU
allocator budget.
Runtime progress and diagnostics are exposed as stable dictionaries:
memory_stats() reports allocated_bytes, memory_limit_bytes,
peak_memory_bytes, and status. peak_memory_bytes is the high-water mark of
successful reservations recorded by the shared provider’s memory manager. It
spans the provider lifetime (or the window after an explicit quiescent reset),
so it is not reset between evaluations or executors that reuse that provider.
It is not physical/NVML usage, and direct CUDA allocations that bypass the
manager are not included. CUDA Graph stats report
csm_cuda_graph_captures, csm_cuda_graph_launches,
csm_cuda_graph_fallbacks, and csm_cuda_graph_cache_hits. If an environment
cannot supply a given diagnostic, it reports an explicit unavailable status or
error rather than a fabricated zero.
neural_hot_loop_diagnostics() is the single audit surface for the neural inner
loop. (nn/4 is a neural predicate declared with four arguments — the
classification form, which carries a list of output labels.) It reports
post_load_dtoh_bytes, post_load_htod_bytes,
control_plane_bytes_per_iteration, scalar_sync_checks, nested
cuda_graph, and nested circuit_cache diagnostics from the same runtime API.
When this runtime cannot yet provide a separate control-plane or scalar-sync
counter, the corresponding value is None and a *_status field explains why.
The top-level pyxlog wrapper also carries nn/4 training lineage:
checkpoint_hash, split_hashes,
calibration_metrics, cuda_device, influence_audit, and
changed_acceptance evidence recorded through record_nn4_influence(...).
register_network also accepts three keyword-only arguments that record and
check the network’s typed signature. All three are available since 0.11.0:
arity=is the declared argument count. XLOG validates it against everynn/4declaration bound to that network name in the program rather than trusting it; a disagreement raisesValueError.arg_sorts=is a sequence of integer sort ids, one per argument. It requiresarityand must have exactly that length. Aboolelement is rejected outright — Python’sboolis anintsubclass, soTruewould otherwise be read as sort id1.artifact_hash=records the checkpoint identity.
program.network_metadata("mnist_net") reads that back, together with what the
program itself declares:
Epistemic evidence -> exact probability
CompiledLogicProgram.evaluate_conditioned(prob_source) runs a compiled epistemic
program (know / possible) on the GPU and conditions an exact probabilistic query
on its accepted world view. Only facts declared in the epistemic program’s own
source feed that world view.
Limitation: unlike evaluate, this method does not accept dlpack_inputs.
Caller-supplied input relations are NOT consulted. If the epistemic program depends
on a relation that would normally be supplied at call time via
evaluate(dlpack_inputs=...), that relation is empty here, no world view is
accepted, and evaluate_conditioned raises RuntimeError:
epistemic_evidence() first — it reports
accepted_world_views == 0 and does not raise.
gpu_conditioned_circuit_preparation_compiles counts actual GPU circuit
compiler invocations owned by that prepared handle: it is 1 after a fresh
compile and 0 after a verified disk-cache restoration or GPU-cache hit. The
three origin counters above are mutually exclusive for the handle’s single
materialization. Every later evaluation reports zero source/program compile
deltas.
For an independent lifetime-reuse check, compare the
gpu_conditioned_circuit_generation and gpu_conditioned_circuit_cache_slot
trace pair across evaluations. It is derived from the retained exact state and
cache handle and must remain unchanged as priors are updated. The generation is
opaque and process-local; do not persist it or compare it between processes.
These reuse keys are present on every EpistemicEvalResult. Results returned
directly by evaluate_conditioned() carry zeroes for the reuse fields;
gpu_conditioned_circuit_generation == 0 is the sentinel that no prepared
circuit identity is attached. A prepared evaluation always reports a positive
generation.
set_fact_probabilities() validates the complete mapping before one serialized
state change. Each variable id is its prob_var_map() list index. Index 0 is
unused padding with kind == "other", not a mutable variable id, and only
entries with kind == "fact" are mutable. Annotated-disjunction choices,
compiler-introduced entries, Monte Carlo and count-lift programs, non-finite
probabilities, and values outside [0, 1] are rejected without a partial update.
When a mutable fact is itself fixed by accepted evidence, changing its prior keeps
the evidence assignment fixed and changes the evidence likelihood (log_z_e),
matching a fresh conditioned compilation at the new prior.
Evaluations and setters on the same prepared handle serialize. Coordinate access
at a higher level when an application needs an update and its following evaluation
to behave as one larger transaction.
Waiting for the prepared handle’s shared native state, including in
prob_var_map(), releases the Python GIL. The Python dictionaries are constructed
only after the native metadata snapshot completes. A failed device write is rolled
back before set_fact_probabilities() returns. If that rollback also fails, the
prepared circuit is permanently invalidated: every later evaluation, gradient,
metadata read, or setter through that handle or any clone fails closed rather than
using potentially partial device weights.
On a CUDA device, conditioning 0.6::fact(). query(fact()). on know fact() raises
P(fact()) from the unconditioned 0.6 to the exact 1.0: the epistemic layer
already accepted fact() into its world view before the probabilistic query ran, so
result.log_z_e is ln(0.6), not 0.0.
result.log_z_e is log P(evidence): the exact log-probability of the conditioned
evidence under the probabilistic program’s distribution, computed by weighted model
counting over the compiled circuit, not the log-evidence of the whole query program.
Query probabilities are exp(log_z_eq - log_z_e). When the conditioned atoms are
independent root facts it coincides with the log of the product of their priors —
measured on GPU, one known atom at prior 0.6 gives log_z_e == ln(0.6) and two known
atoms each at prior 0.5 give log_z_e == ln(0.25) — but that is the independent-root
special case, not the definition. Evidence on a derived atom (0.6::a(). b() :- a().
with know b() gives ln(0.6) though b has no prior), on atoms sharing an ancestor,
or negated evidence all depart from the product form.
Trace invariant. Conditioning reached the GPU exact path when
gpu_conditioned_evidence_facts — the total the engine itself validates — is
non-zero. A direct result also requires the GPU exact, PIR/CNF, and
knowledge-compilation event counters to be positive; a prepared result instead requires
the prepared-circuit reuse counter to be positive, reports one materialization, and
identifies exactly one compile/cache origin. The four evidence classes
(gpu_conditioned_know_evidence_facts, gpu_conditioned_possible_evidence_facts,
gpu_conditioned_not_known_evidence_facts,
gpu_conditioned_not_possible_evidence_facts) decompose that total; a possible-only
or negated-evidence program conditions correctly with the know class at 0, so
check the total rather than the know class alone.
EpistemicEvalResult carries atoms, prob and log_prob (DLPack capsules, like
EvalResult), log_z_e, and trace. CompiledLogicProgram.epistemic_evidence()
runs the same epistemic program and returns an EpistemicEvidence with the
accepted-world-view counters alone (epistemic_mode, know_operator_count,
possible_operator_count, accepted_candidates, rejected_candidates,
accepted_world_views, final_output_rows), without touching the probabilistic
tier. Like evaluate_conditioned, it only ever sees facts declared in the
program’s own source — but a program that depends on a caller-supplied relation
reports accepted_world_views == 0 (with accepted_candidates and
final_output_rows at 0) here rather than raising. know_operator_count and
possible_operator_count are plan-level censuses and stay non-zero even then, so the
state to check is the accepted/consumed family, not “every counter”.
Which plans are accepted. Only single-component epistemic plans are supported;
split, stratified and WFS plans raise instead of being silently reduced. Both
epistemic modes reach this surface: FAEEL programs and non-recursive
#pragma epistemic_mode = g91 programs both lower to a single-component epistemic
plan and condition normally. epistemic_evidence().epistemic_mode names the mode, and
the trace’s accepted_faeel_world_view_evidence_consumed /
accepted_g91_world_view_evidence_consumed pair says which one supplied the evidence.
Only the recursive G91 shapes — positive possible cycles that need tuple-level
compatibility — compile to a dedicated G91-compatibility plan and are rejected at
planning.
Rejection also covers one case that reads like a false negative: an admissible
recursive modal program such as reach(X, Z) :- reach(X, Y), know link(Y, Z). is
reduced to ordinary recursion at compile time (the ordinary_recursive_modal_reduction
provenance class). The reduction erases the world-view machinery, so there is no
accepted world view left to condition on and the program is rejected as “ordinary”
despite being full of know. That is deliberate, not a compiler bug. A real CUDA
device is required.
Program (Probabilistic)
Compiles and runs a probabilistic Datalog program — facts and rules annotated with probabilities, such as0.3::rain — and computes query probabilities. The
prob_engine argument selects the inference method: "exact_ddnnf" compiles the
program to a Boolean circuit (a deterministic, decomposable negation normal form,
or d-DNNF) that makes exact probability and gradient computation tractable, while
"mc" estimates probabilities by Monte Carlo sampling.
Host Outputs (Requires host-io)
When built with --features host-io, you can call CompiledProgram.evaluate(...) to get host-derived
probability outputs as device tensors (DLPack):
Monte Carlo Inference (Device-Only)
For GPU-native workflows, preferCompiledProgram.evaluate_device(...) (no host reads for results).
Monte Carlo Inference (Host Outputs, Requires host-io)
When built with --features host-io, CompiledProgram.evaluate(...) computes probabilities and
confidence intervals and uploads them as device tensors (DLPack):
Experimental Arrow C Device Interop (Feature arrow-device-import)
These helpers bridge XLOG’s DLPack columns to Apache Arrow’s C Device interface —
a standard for sharing columnar data that already lives on a device (GPU) —
without host copies. When built with --features arrow-device-import, pyxlog
exposes:
pyxlog.export_arrow_device(...) -> PyCapsule(namearrow_device_array)pyxlog.import_arrow_device(...) -> (dlpack_tensors, names, num_rows)
Bool.
JointConstraintCarrier (joint constraint solving)
pyxlog.JointConstraintCarrier picks one label per entity subject to pairwise
constraints, entirely on the GPU. Its result buffers are handed back as DLPack
capsules, so a caller never copies them to the host. Available since 0.11.0.
register_schema(catalog_sha, solver_identity)must be givenpyxlog.SOLVER_ABI_IDENTITY— a module-level string naming the solver ABI this build speaks — so a carrier cannot be driven by a mismatched client.bind_signatures(head_masks, tail_masks)must run before any solve.note_producer_stream(...)andnote_consumer_stream(...)perform the CUDA stream handoff when the buffers cross into or out of another library’s stream.fuel_spentis a read-only property reporting how much offuel_limitthe solves consumed.
pyxlog.CarrierRefused, and running out of fuel_limit raises
pyxlog.SolverResourceExhausted. See Error Handling.
Term Embeddings
Theregister_embedding / forward_embedding API enables explicit PyTorch-side embedding training
through the logic program. Embedding predicates use the label-free nn/3
declaration form (a neural predicate declared with three arguments and no output
label list).
Embedding Registration
Forward Lookup
Cross-Registration Validation
- Embedding declarations (
nn/3, no labels) rejectregister_network()— error directs toregister_embedding() - Classification declarations (
nn/4, with labels) rejectregister_embedding()— error directs toregister_network() - Same network name as both embedding and classification → compile-time error
Constraints
trainable=Truerequiresnn.Embedding; rawtorch.Tensorwithtrainable=TrueraisesValueError- Raw tensors with
requires_grad=Trueare detached at registration (frozen contract enforced) - Integer IDs only (symbol/string lookup keys deferred)
- Optimizer ownership is user-managed; classification-network optimizer helpers do not cover embeddings
- Inference through rules (dot/cosine evaluation, grounded query API) is deferred to future embedding-rule integration
Training Loop API (Neural-Symbolic)
For neural-symbolic training with neural predicates (nn/k — a predicate backed
by a neural network), Program exposes loss computation, optimizer stepping,
gradient clipping, learning-rate control, and batched training loops, in addition
to the single-query forward_backward* helpers.
Loss computation
Thenll_loss* helpers compute a negative log-likelihood (NLL) loss — the
standard training objective that penalizes low predicted probability for the
target answer.
External Consumer Bridge Helpers
These helpers keep a four-valued evidence model (Belnap logic) in the Python/ML layer: each candidate carries separatepro (evidence for), contra (evidence
against), and quarantine (held-out) scores. The GPU structural kernels never see
those channels. The helper surfaces operate on PyTorch tensors and preserve
autograd unless the caller explicitly detaches inputs.
deterministic_topk(...) resolves ties by lower input index. neural_cache_stats()
reports circuit-cache size, hit/miss counters, template compile count,
query-signature cache size, and registered-network cache/top-k/deterministic
configuration. belnap_loss(...) returns a dictionary containing loss,
pro_reward, contra_penalty, quarantine_penalty, cfr_regret_proxy, and
the formula string.
Registered-network output modes reuse the existing register_network(..., k=N, det=True) configuration. forward_backward_tensor(...),
forward_backward(...), and batched neural-query training apply the configured
stable top-k or deterministic top-1 mode before NLL loss and cached circuit
probability import. Deterministic mode picks a single hard top-1 value on the
forward pass, then routes gradients straight through the selected probability on
the backward pass (a straight-through estimator, so the discrete choice still
trains).
Optimizer and scheduler control
Batched training epoch
Profiling
ILP Training (dILP Beta)
Thepyxlog.ilp subpackage provides differentiable ILP (Inductive Logic
Programming): it learns Datalog rules from labeled positive and negative examples
by gradient descent, treating rule choice as something that can be optimized like
neural weights. This surface is beta.
Training API
Artifact Persistence
TrainConfig Fields
The temperature fields (
tau_start, tau_floor) control a Gumbel-softmax
relaxation — a technique that makes discrete rule choices differentiable so they
can be trained by gradient descent. Training starts hot (tau_start, soft and
exploratory) and anneals toward tau_floor (sharp, near-discrete).
Result Types
External Consumer Diagnostics
These helpers package the ILP audit surface for downstream applications (“external consumers”) as reusable pyxlog utilities:NeuralBodySpec carries an opt-in train_phi_gradient flag, default False.
Left at the default, the entity features φ(x) are detached on the training
upload, and the uploaded values are byte-identical to earlier releases. Set it to
True to leave φ(x) attached, so gradients flow back into whatever produced the
features (backbone coupling). Only the autograd linkage changes — never the
values that are uploaded. Available since 0.12.0.
train_and_promote(...) accepts transfer-audit metadata through
training_fold, held_out_domains, base_kernel_checksum_before, and
base_kernel_checksum_after. The returned PromotionResult.rule_inventory
records those values with selected and rejected clauses, scores, and gate
outcomes.
Device Query APIs
For GPU-native ILP workflows,CompiledIlpProgram exposes device-resident query
helpers (results stay on the GPU) alongside the existing host-returning helpers:
batch_fact_membership()andbatch_tagged_credit()remain available for host-materialized Python outputsbatch_fact_membership_device()returns a DLPack bool tensor on CUDAbatch_tagged_credit_device()returns CSR-style device outputs:fact_row_offsets,entry_indices,entry_i,entry_j,entry_k- The device query path avoids semantic-loop device-to-host transfers; inspect
host_transfer_stats()/reset_host_transfer_stats()when enforcing that contract in tests - Unsigned metadata/count tensors are exported as DLPack
int32for broad framework compatibility
Bounded Exact Induction API
Bounded exact induction searches a bounded space of candidate rules and scores each one exactly on the GPU, rather than optimizing rule choice by gradient descent.pyxlog.ilp.induce_exact(..., backend="native") is the GPU-native
scorer. The public entry point returns an ExactInductionResult containing
ScoredCandidate rows grouped by join shape (topology): chain, star,
fanout, then fanin.
backend="python", strict_per_topology=True; leaving strict_per_topology
at its default preserves legacy prototype behavior and is not semantically
equivalent to native scoring.
Exact induction accepts pair relations whose two columns share one of these
scalar types: u64, u32, or symbol. Generated ilp_exact.portable.ptx and
.cubin files are packaged build artifacts, not checked-in source files.
Sparse Mask APIs
A rule mask selects which candidate rules are active during training. Sparse mask setters let you pass only the selected candidates instead of a full dense vector.CompiledIlpProgram exposes two:
set_rule_mask_sparse(name, candidate_ids, soft_probs, budget, allow_recursive=False)is the legacy compatibility path. Rust receives the full candidate soft-probability vector and ranks it internally.set_rule_mask_sparse_selected(name, selected_candidate_ids, selected_soft_probs, allow_recursive=False)is the preferred inner-loop path. Python/Torch performs ranking on CUDA, then Rust consumes only the selected subset and preserves that order as the sparse active-rule list.
GPU-Native Contract
For Python consumers that need an auditable GPU-native ILP inner loop, the intended contract is:- Zero provider-tracked device-to-host transfer inside the semantic loop:
set_rule_mask_sparse_selected(...),batch_fact_membership_device(...),batch_tagged_credit_device(...), andcompute_ilp_loss_grad_gpu(...) - Metadata/control-plane reads may still occur behind public runtime/provider helpers such as cached row-count access; these are not relation-column materializations
- Compatibility paths that are not suitable for a strict GPU-native inner loop:
set_rule_mask_sparse(...),batch_fact_membership(...),batch_tagged_credit(...), and any host-output API gated behindhost-io - Use
host_transfer_stats()/reset_host_transfer_stats()to audit the provider-tracked transfer behavior of the chosen path
DLPack integration
XLOG accepts CUDA-backed DLPack producer objects and returns single-consumer capsules. Transient input imports and framework wrapping of query-result capsules are zero-copy. Persistent relation replacement takes an owned device-to-device snapshot, and stored-relation export leaves a device-to-device clone in the session before transferring the exported allocation. Compatible CUDA-backed producers include:- PyTorch
- CuPy
- JAX
- TensorFlow
- Any CUDA-backed DLPack-compatible library
Input via DLPack
Output via DLPack
dlpack_roundtrip helper
dlpack_roundtrip sends a CUDA tensor into XLOG and returns it as a fresh DLPack
capsule — a quick way to verify zero-copy interop end to end. All three arguments are
required:
Compile Options
LogicProgram.compile()
Program.compile() (Probabilistic)
Result Objects
Deterministic Results
Probabilistic Results
Device-Only MC Results
Error Handling
Python exceptions are raised for errors:relation(name) and evidence(name) use KeyError for an unstored relation.
export_relation_with_provenance(name) uses ValueError for the same condition.
The joint-constraint carrier raises two exceptions of its own, both
RuntimeError subclasses, available since 0.11.0:
pyxlog.CarrierRefused— every typed refusal: a schema or ABI mismatch, a zero-capacity dimension, use beforeregister_schema, rebinding already-bound signature masks, a signature-mask shape mismatch, solving beforebind_signatures, running the top-two stage before feasibility has solved, a malformed component plan, an out-of-range abstain label index, or an unavailable joint-solve kernel.pyxlog.SolverResourceExhausted— a solve used up itsfuel_limit, with the messagesolver fuel exhausted: spent {fuel_spent} of {fuel_limit} node expansions. The fuel meter lives in the carrier session, so a retry reproduces the identical refusal instead of making partial progress; build a new carrier with a largerfuel_limit.
Memory Management
- An unconsumed output capsule owns its exported GPU buffer and releases it when garbage collected.
- A framework conversion such as
torch.from_dlpack(capsule)consumes that capsule exactly once and takes over its managed-tensor ownership. - Passing a CUDA tensor object lets XLOG request a fresh capsule through the producer protocol. Do not manufacture or reuse raw input capsules unless you also satisfy the stream-ordering and single-consumer contract.
- Persistent relation replacement and stored-relation export use owned device-to-device copies rather than mutable views of session storage.
Thread Safety
compile()is thread-safeevaluate()is NOT thread-safe on the same program instance- Use separate program instances for concurrent execution
Examples
Integration with PyTorch
Batch Processing
Limitations
- Linux x86_64 + CUDA only
- Source builds that compile the PyO3 extension expose the documented native API. For prebuilt packages, use the release notes for that package version to determine which surfaces it contains.
- Pure-Python helper modules can import without
pyxlog._native, but native-backed compile, evaluate, and session APIs still require the extension.RelationEvidenceandRelationMetadataErrorremain importable for annotations and exception handling in source-only mode. The fallbackRelationMetadataErroris aValueErrorsubclass, while constructing fallbackRelationEvidenceraisesRuntimeErrorbecause its state is native-owned.
See Also
- Living-World Diagnostics — Rule provenance, proof traces, delta debug, temporal metadata, and nn/4 hot-loop audit surface
- dILP Training Architecture — System design, mask backends, promotion pipeline
- Data Interoperability — DLPack and Arrow details
- Probabilistic Tier — Inference engine details
- CLI Reference — Command-line alternative