Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
name: Benchmarks

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
baseline:
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/cuda-ci.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
name: cuda-ci

on:
push:
branches: ["**"]
pull_request:
workflow_dispatch:

jobs:
rust-tests:
Expand Down
3 changes: 0 additions & 3 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
name: Fuzz Testing

on:
schedule:
# Run daily at 3 AM UTC
- cron: '0 3 * * *'
workflow_dispatch:
inputs:
target:
Expand Down
24 changes: 21 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

All notable changes to this project are documented in this file.

## Unreleased — 2026-03-01
## Unreleased — 2026-03-04

### Added

Expand All @@ -11,8 +11,8 @@ All notable changes to this project are documented in this file.
the executor mask internally — no N3 tensor materialized, zero host→device transfer for the mask.
- **Trainer backend abstraction** (`MaskBackend` protocol): `SparseMaskBackend` (default) and `DenseMaskBackend`
(fallback via `debug_dense_mask=True`). Dense parity verified in tests.
- **`train_and_promote()`**: Wraps `train_only()` + trial compilation + 5 promotion gates (convergence, novel rate,
protected relations, holdout F1, ambiguity scan) → returns `PromotionResult` with transactional commit.
- **`train_and_promote()`**: Wraps `train_only()` + trial compilation + promotion gates (convergence, novel rate,
regression check, holdout F1, ambiguity scan, typed schema) → returns `PromotionResult` with transactional commit.
- **LOO holdout F1 scoring**: Leave-one-out cross-validation for ≤20 examples with per-fold precision/recall.
- **Ambiguity scan**: Top-M alternative rule detection with configurable `check_ambiguity` / `exhaustive_ambiguity`.
- **Hard-negative mining** (`sample_false_positives`): Rust-side false positive sampling, wired into trainer every
Expand All @@ -23,6 +23,11 @@ All notable changes to this project are documented in this file.
(behind config flag, default off).
- **Beta reliability gate**: 4 stages (reach, grandparent, colleague, plus2) x 5 seeds = 20/20 with sparse backend.
- **AtomicU32 row-count cache** on `CudaBuffer` for GPU-resident row counts without host reads.
- **Deterministic training path**: `TrainConfig(deterministic=True)` enables deterministic CUDA/Torch settings and
per-attempt seed derivation for reproducible runs.
- **`selected_hard` artifact field**: persisted selected candidate IDs with deterministic ordering for sparse/dense parity.
- **GA reliability gate test**: `test_ilp_ga_reliability.py` runs 50 seeds x 4 stages with Clopper-Pearson lower-bound check.
- **GA performance/transfer test**: `test_ilp_performance.py` validates `forward_p95_us` telemetry and host-transfer accounting.

- **Arrow C Data Interface device export** for `CudaBuffer` record batches (`to_arrow_device_record_batch`) returning
`ArrowDeviceArrayOwned` handles with CUDA device descriptors and zero host transfers (import exists but is
Expand Down Expand Up @@ -61,6 +66,12 @@ All notable changes to this project are documented in this file.
### Changed

- dILP trainer defaults to sparse mask backend (`SparseMaskBackend`); dense fallback via `TrainConfig(debug_dense_mask=True)`.
- dILP holdout strategy now defaults to:
- LOO for `<=20` positives
- k-fold for `>20` positives (`holdout_strategy`, `holdout_folds` configurable)
- dILP promotion now enforces configurable holdout threshold (`holdout_threshold`, default `0.95`) and supports
typed-schema gate controls (`typed_schema_required`, `waiver_untyped`).
- PyO3 exposes host transfer counters via `host_transfer_stats()` / `reset_host_transfer_stats()`.
- `GpuCnf` literal storage field renamed to `literals` (DIMACS `i32`) to match the solver/kernel interface.
- CUDA-dependent tests now skip cleanly when the CUDA runtime is unavailable (developer ergonomics).
- Workspace testing avoids building the PyO3 `extension-module` target when running `cargo test --workspace`.
Expand Down Expand Up @@ -95,6 +106,13 @@ All notable changes to this project are documented in this file.
- Workspace tests pass (CUDA-dependent tests skip cleanly when CUDA is unavailable).
- CUDA certification suite passes (C01-C25 + G01-G08): 206/206.
- dILP beta reliability gate: 20/20 (4 stages x 5 seeds, sparse backend).
- Consolidated CUDA Python ILP gate batch (fresh):
- `44 passed` (`test_ilp_types`, `test_ilp_trainer`, `test_ilp_promoter`, `test_ilp_holdout`, `test_ilp_robustness`)
- `19 passed` (`test_ilp_d2h_gate`, `test_ilp_sparse`)
- `20 passed` (`test_ilp_beta_gate`)
- `20 passed` (`test_ilp_reliability`)
- `3 passed` (`test_ilp_performance`)
- `1 passed` (`test_ilp_ga_reliability`, default `GA_RELIABILITY_SEEDS=50`)

## Neural-Symbolic Integration Milestone (v0.4.0-alpha) — 2026-02-23

Expand Down
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

> **Release status:** Latest tagged release is `v0.3.2`. The `main` branch is ahead of `v0.3.2` and contains
> unreleased work: GPU-native knowledge compilation, GPU CDCL verifier + cache, neural-symbolic training APIs,
> and a **dILP beta trainer** (sparse mask API, promotion pipeline, artifact persistence, 20/20 reliability gate).
> The `v0.4.0-alpha` milestone is **achieved**; dILP beta is **achieved**.
> and a **dILP beta trainer** (sparse mask API, promotion pipeline, artifact persistence, reliability gates).
> The `v0.4.0-alpha` milestone is **achieved**; dILP beta is **achieved**; GA hardening is in progress.
> See `docs/ROADMAP.md` and `docs/VALIDATION_REPORT.md`.

**XLOG** is a GPU-accelerated Datalog query engine with neural-symbolic integration. It compiles declarative logic programs into optimized relational plans and executes them on NVIDIA GPUs, achieving high throughput for recursive queries, graph analytics, probabilistic inference, and neural-symbolic training.
Expand All @@ -27,7 +27,7 @@
| **Float Predicates** | IEEE 754 total ordering for `f32`/`f64` (`NaN > Inf > nums > +0 > -0 > -Inf`) |
| **Probabilistic** | Exact inference (knowledge compilation), Monte Carlo sampling, negation (stratified + WFS) |
| **Neural-Symbolic** | Neural predicates (`nn/4`), PyTorch integration, differentiable training, circuit caching |
| **dILP Training** | Differentiable ILP: sparse GPU mask, multi-start optimizer, promotion gates, artifact save/load |
| **dILP Training** | Differentiable ILP: sparse GPU mask, deterministic mode, promotion gates, holdout validation, artifact save/load |
| **Interop** | Arrow IPC, DLPack (zero-copy), Python bindings, PyTorch autograd |
| **Profiling** | `--stats` flag for per-stratum/per-operation timing, memory tracking |

Expand Down Expand Up @@ -375,7 +375,11 @@ if result.converged:
```python
from pyxlog.ilp import train_and_promote, TrainConfig

config = TrainConfig(check_ambiguity=True, max_novel_rate=0.05)
config = TrainConfig(
check_ambiguity=True,
holdout_threshold=0.95,
typed_schema_required=True,
)
promotion = train_and_promote(source, "W", pos, neg, config)

print(f"Status: {promotion.status}")
Expand All @@ -385,9 +389,12 @@ for gate in promotion.gates:

### Key Features

- **Sparse GPU mask**: Candidate soft-probs sent to Rust; executor mask built on device with zero host transfers
- **Sparse GPU mask**: Candidate soft-probs sent via `set_rule_mask_sparse`; no Python-side N³ mask materialization
- **Deterministic mode**: Seeded training path with reproducible candidate ranking and persisted `selected_hard`
- **Multi-start optimizer**: Adaptive temperature, entropy regularization, plateau detection
- **Promotion gates**: Convergence, novel rate, protected relations, holdout F1, ambiguity scan
- **Promotion gates**: Convergence, novel rate audit, regression check, holdout F1 threshold, ambiguity scan, typed schema gate
- **Holdout strategies**: LOO for small sets (`<=20` positives), k-fold for larger sets
- **Telemetry**: `forward_p95_us`, allocation summaries, and host-transfer accounting (`host_transfer_stats`)
- **Artifact persistence**: JSON save/load with SHA-256 hash verification
- **Recursive candidates**: Optional body-references-head rules via `allow_recursive_candidates=True`

Expand Down Expand Up @@ -595,6 +602,9 @@ The legacy CPU D4 vendor pipeline is removed.
| [Neural Examples](examples/neural/) | Neural-symbolic training examples |
| [dILP Beta Design](docs/plans/2026-02-26-dilp-hardening-design.md) | dILP trainer hardening design |
| [dILP Beta Plan](docs/plans/2026-02-26-dilp-beta-impl.md) | dILP beta implementation plan (9 tasks) |
| [dILP Architecture](docs/architecture/dilp-training.md) | Runtime/trainer architecture and GPU hot-loop contract |
| [GPU Hot-loop Transfer Elimination](docs/plans/2026-03-01-gpu-hotloop-transfer-elimination.md) | Transfer-reduction design |
| [Sparse Executor Transfer Fix](docs/plans/2026-03-01-sparse-executor-transfer-fix.md) | Sparse-mask executor alignment and implementation |
| [v0.3.2 Showcase](examples/xlog/80-v032-showcase/) | Production-grade multi-module examples |
| [CUDA Certification](docs/architecture/cuda-certification.md) | Certification suite coverage (current HEAD) |

Expand Down
1 change: 1 addition & 0 deletions crates/pyxlog/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ name = "pyxlog"
version = "0.4.0"
description = "GPU-accelerated XLOG probabilistic logic programming (PyO3 bindings)"
requires-python = ">=3.8"
optional-dependencies = { test = ["scipy"] }
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: 3",
Expand Down
144 changes: 92 additions & 52 deletions crates/pyxlog/python/pyxlog/ilp/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ def apply_mask(
budget: int,
candidates: list[dict],
n: int,
allow_recursive: bool = False, # TODO(task-8): forward to set_rule_mask_sparse
) -> torch.Tensor:
"""Apply mask to program. Returns candidate_soft_probs shape (C,)."""
allow_recursive: bool = False,
) -> tuple[torch.Tensor, list[int]]:
"""Apply mask to program.

Returns:
candidate_soft_probs: Tensor of shape (C,) and selected hard candidate IDs.
"""
...

def decode_argmax(self, W: torch.Tensor, candidates: list[dict], n: int) -> int:
Expand All @@ -55,10 +59,33 @@ def apply_mask(
):
M_soft = F.gumbel_softmax(W, tau=tau, hard=False, dim=-1)
flat = M_soft.view(-1)
k = min(budget, flat.numel())
_, topk_idx = flat.topk(k)
# Restrict hard-mask activation to valid candidate cells only.
# This keeps dense fallback semantically aligned with valid_candidates().
candidate_flat_indices = [
int(c["i"]) * n * n + int(c["j"]) * n + int(c["k"])
for c in candidates
]

if candidate_flat_indices:
candidate_flat_indices_tensor = torch.tensor(
candidate_flat_indices, device=flat.device, dtype=torch.long,
)
candidate_scores = flat[candidate_flat_indices_tensor]
k = min(budget, candidate_scores.numel())
if k > 0:
_, topk_rel_idx = candidate_scores.topk(k)
topk_idx = candidate_flat_indices_tensor[topk_rel_idx]
selected_hard = topk_rel_idx.detach().cpu().tolist()
else:
topk_idx = torch.empty(0, dtype=torch.long, device=flat.device)
selected_hard = []
else:
topk_idx = torch.empty(0, dtype=torch.long, device=flat.device)
selected_hard = []

M_hard_flat = torch.zeros_like(flat)
M_hard_flat[topk_idx] = 1.0
if topk_idx.numel() > 0:
M_hard_flat[topk_idx] = 1.0
M_hard = M_hard_flat.view_as(M_soft)

prog.set_rule_mask(
Expand All @@ -72,19 +99,17 @@ def apply_mask(
cand_probs = torch.stack(
[M_soft[c["i"], c["j"], c["k"]] for c in candidates]
)
return cand_probs
selected_hard.sort()
return cand_probs, selected_hard

def decode_argmax(self, W, candidates, n):
if not candidates:
return 0
with torch.no_grad():
flat = W.view(-1)
idx = flat.argmax().item()
i = idx // (n * n)
j = (idx % (n * n)) // n
k = idx % n
for ci, c in enumerate(candidates):
if c["i"] == i and c["j"] == j and c["k"] == k:
return ci
return 0
candidate_vals = torch.stack(
[W[c["i"], c["j"], c["k"]] for c in candidates]
)
return int(candidate_vals.argmax().item())


class SparseMaskBackend:
Expand All @@ -94,8 +119,9 @@ class SparseMaskBackend:
This gives a smaller, more focused parameter space that converges
faster on the correct candidate.

Internally materializes the candidate probs into the dense N^3 mask
format via set_rule_mask for store-state-independent evaluation.
Passes DLPack-wrapped soft-probs directly to Rust via
set_rule_mask_sparse — no Python-side N^3 materialization.
Rust owns top-k ranking and sparse mask construction.
"""

def init_weights(self, C: int, n: int, device: str) -> torch.Tensor:
Expand All @@ -105,44 +131,58 @@ def apply_mask(
self, prog, mask_name, W, tau, budget, candidates, n,
allow_recursive=False,
):
C = len(candidates)

# Gumbel-softmax over C-dimensional logits
cand_probs = F.gumbel_softmax(W, tau=tau, hard=False, dim=0)

# Materialize into dense N^3 format for set_rule_mask.
# TODO(RC): switch to prog.set_rule_mask_sparse() once store-state
# validation issue is resolved — eliminates Python-side N^3 tensor.
M_soft = torch.zeros((n, n, n), device=W.device)
for ci, c in enumerate(candidates):
M_soft[c["i"], c["j"], c["k"]] = cand_probs[ci]

# Top-k hard mask over C candidates (not N^3 cells).
#
# Key insight: in the dense backend, topk(budget) selects budget cells
# out of N^3 total, so only a fraction of the C valid candidate positions
# end up active (typically 2-5 for budget=32, N=6). In the sparse backend,
# ALL C cells are non-zero after materialization, so topk(budget) with
# budget >= C would activate ALL candidates -- leaking distractor rules
# into the hard mask and causing Gate 2 to fail.
#
# Fix: cap the hard mask at half of C (matching the ~15% activation
# ratio of the dense backend) while ensuring at least 1 candidate.
C = len(candidates)
k = max(1, min(budget, C) // 2)
_, topk_idx = cand_probs.topk(k)
# Build hard mask: only activate top-k candidates in the N^3 tensor
M_hard = torch.zeros((n, n, n), device=W.device)
for idx in topk_idx:
c = candidates[idx]
M_hard[c["i"], c["j"], c["k"]] = 1.0

prog.set_rule_mask(
mask_name,
M_hard.detach().contiguous().view(-1),
M_soft.detach().contiguous().view(-1),
n,
# Rust validates candidate count against current store state.
# After evaluate(), derived relations may gain tuples, changing
# the expected count. Re-query the live set and pad with zeros
# for any new candidates that appeared since compile time.
live_cands = prog.valid_candidates(mask_name, allow_recursive)
C_live = len(live_cands)

# Build (i,j,k) -> original index lookup
orig_ijk = {
(c["i"], c["j"], c["k"]): idx for idx, c in enumerate(candidates)
}
live_to_orig: list[int | None] = []
for lc in live_cands:
key = (lc["i"], lc["j"], lc["k"])
live_to_orig.append(orig_ijk.get(key))

if C_live == C:
soft = cand_probs.detach().contiguous().double()
else:
soft = torch.zeros(C_live, device=W.device, dtype=torch.float64)
for li, lc in enumerate(live_cands):
key = (lc["i"], lc["j"], lc["k"])
if key in orig_ijk:
soft[li] = cand_probs[orig_ijk[key]].detach().double()

effective_budget = min(budget, soft.numel())

candidate_ids = list(range(C_live))
prog.set_rule_mask_sparse(
mask_name, candidate_ids, soft.contiguous(), effective_budget,
allow_recursive,
)

return cand_probs
selected = torch.topk(
soft,
k=min(effective_budget, soft.numel()),
largest=True,
sorted=True,
).indices
selected_hard = []
for i in selected.detach().cpu().tolist():
idx = int(i)
orig_idx = live_to_orig[idx] if 0 <= idx < len(live_to_orig) else None
if orig_idx is not None:
selected_hard.append(orig_idx)

selected_hard.sort()
return cand_probs, selected_hard

def decode_argmax(self, W, candidates, n):
with torch.no_grad():
Expand Down
Loading