Skip to content
Open
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
13 changes: 13 additions & 0 deletions crates/pyxlog/python/pyxlog/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ importable from Python.

from __future__ import annotations

from os import PathLike
from typing import Any, Literal, Optional, Sequence, TypedDict, Union

# ---------------------------------------------------------------------------
Expand All @@ -17,6 +18,8 @@ from typing import Any, Literal, Optional, Sequence, TypedDict, Union

__version__: str

_Path = Union[str, PathLike[str]]

# ---------------------------------------------------------------------------
# Native relation provenance
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -173,6 +176,16 @@ class LogicProgram:
memory_mb: int = 32768,
) -> CompiledLogicProgram: ...

@staticmethod
def compile_file(
entrypoint: _Path,
module_paths: Sequence[_Path] = (),
device: int = 0,
memory_mb: int = 32768,
) -> CompiledLogicProgram:
"""Compile an entry file and its complete transitive module closure."""
...

class CompiledLogicProgram:
"""A compiled GPU-resident Datalog program ready to evaluate."""

Expand Down
53 changes: 40 additions & 13 deletions crates/pyxlog/src/logic.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;

Expand Down Expand Up @@ -142,22 +143,48 @@ impl LogicProgram {
#[staticmethod]
#[pyo3(signature = (source, device=0, memory_mb=32768))]
pub fn compile(source: &str, device: usize, memory_mb: u64) -> PyResult<CompiledLogicProgram> {
if memory_mb == 0 {
return Err(PyValueError::new_err("memory_mb must be > 0"));
}

let mut config = GpuConfig::default();
config.device_ordinal = device;
config.memory_bytes = memory_mb * 1024 * 1024;

require_logic_memory_budget(memory_mb)?;
let program = gpu_logic::LogicProgram::compile(source).map_err(types::xlog_err)?;
let provider = provider_from_config(config).map_err(types::xlog_err)?;
compiled_logic_program(program, device, memory_mb)
}

Ok(CompiledLogicProgram {
program: Arc::new(program),
provider: Arc::new(provider),
})
#[staticmethod]
#[pyo3(signature = (entrypoint, module_paths=Vec::new(), device=0, memory_mb=32768))]
pub fn compile_file(
entrypoint: PathBuf,
module_paths: Vec<PathBuf>,
device: usize,
memory_mb: u64,
) -> PyResult<CompiledLogicProgram> {
require_logic_memory_budget(memory_mb)?;
let program = gpu_logic::LogicProgram::compile_file(&entrypoint, module_paths)
.map_err(types::xlog_err)?;
compiled_logic_program(program, device, memory_mb)
}
}

fn require_logic_memory_budget(memory_mb: u64) -> PyResult<()> {
if memory_mb == 0 {
return Err(PyValueError::new_err("memory_mb must be > 0"));
}
Ok(())
}

fn compiled_logic_program(
program: gpu_logic::LogicProgram,
device: usize,
memory_mb: u64,
) -> PyResult<CompiledLogicProgram> {
let mut config = GpuConfig::default();
config.device_ordinal = device;
config.memory_bytes = memory_mb * 1024 * 1024;

let provider = provider_from_config(config).map_err(types::xlog_err)?;

Ok(CompiledLogicProgram {
program: Arc::new(program),
provider: Arc::new(provider),
})
}

#[pymethods]
Expand Down
16 changes: 16 additions & 0 deletions crates/xlog-gpu/src/logic.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! GPU-accelerated evaluation of compiled Datalog programs.

use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};

Expand Down Expand Up @@ -932,6 +933,21 @@ impl LogicProgram {
Self::compile_program(program)
}

/// Compile an entry file and its complete transitive module closure.
pub fn compile_file(entry_file: &Path, search_paths: Vec<PathBuf>) -> Result<Self> {
let resolver = xlog_logic::compile::load_modules(entry_file, search_paths)
.map_err(|error| XlogError::Compilation(error.to_string()))?;
let entry_program = resolver
.entry()
.ok_or_else(|| XlogError::Compilation("module resolver has no entry program".into()))?
.program
.clone();
let merged = resolver
.merge_imports(entry_program)
.map_err(|error| XlogError::Compilation(error.to_string()))?;
Self::compile_program(merged)
}

/// Compile an already parsed program into a GPU-executable program.
///
/// This method does not resolve imports; import-aware callers merge them
Expand Down
43 changes: 43 additions & 0 deletions python/tests/test_pyxlog_compile_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

import os

import pytest


torch = pytest.importorskip("torch")
pyxlog = pytest.importorskip("pyxlog")


def _require_cuda() -> None:
if torch.cuda.is_available():
return
if os.environ.get("XLOG_REQUIRE_CUDA") == "1":
raise RuntimeError("XLOG_REQUIRE_CUDA=1 but PyTorch cannot access CUDA")
pytest.skip("CUDA is unavailable")


def test_compile_file_resolves_transitive_modules_with_native_resolver(tmp_path) -> None:
_require_cuda()

(tmp_path / "facts.xlog").write_text("source(7).\n", encoding="utf-8")
(tmp_path / "rules.xlog").write_text(
"use facts.\nresult(X) :- source(X).\n",
encoding="utf-8",
)
entrypoint = tmp_path / "main.xlog"
entrypoint.write_text("use rules.\n?- result(X).\n", encoding="utf-8")

program = pyxlog.LogicProgram.compile_file(
entrypoint,
module_paths=[tmp_path],
device=0,
memory_mb=512,
)
evaluated = program.evaluate()

assert len(evaluated.queries) == 1
query = evaluated.queries[0]
assert query.relation_name == "__xlog_query_0"
values = torch.utils.dlpack.from_dlpack(query.tensors[0]).cpu().tolist()
assert values == [7]
Loading