perf(pyxlog): release GIL during epistemic evaluation - #253
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the pyxlog Python bindings to release the CPython GIL while running long-running Rust/CUDA epistemic evaluation work, improving Python-side concurrency without changing public Python signatures or return schemas.
Changes:
- Run
evaluate_conditioned’s full native + GPU pipeline insidePython::detach, reacquiring the GIL only to build Python objects/DLPack capsules. - Update
epistemic_evidenceto also execute the native work underPython::detach. - Add GPU concurrency regression tests to ensure observer threads can progress during both public epistemic calls.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
crates/pyxlog/src/epistemic.rs |
Clones inputs and detaches from the GIL while running epistemic + conditioned GPU pipelines; repacks results after reacquiring Python. |
python/tests/test_pyxlog_epistemic_concurrency.py |
Adds deterministic concurrency tests validating the GIL is released during long GPU-backed epistemic calls. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Deep review — PR #253 @ 9995a8c16d9ed1b9aba31da7f1db664a0e92d883 (fixes #212)
Verdict: approve. 0 HIGH / 0 MEDIUM / 2 LOW / 3 NOTEs. Issue #212's specific asks — including the one the previous reverted attempt failed on — are all satisfied, and the live legs reproduce on a pinned head wheel (12/12, 0 skipped).
The Sync question from #212 — answered the right way
The issue recorded why the previous fix was reverted: the Sync argument was hand-derived against pyo3 0.24, and it needed a compiler check against 0.29. This PR delivers exactly that:
- Both closures capture only owned, pre-cloned values — no
&self, noPy<…>types, noPythontoken (verified line-by-line).Python::detachbounds closure and return byUngil(=Send), soCudaKernelProvider: Send + Sync— the fact #212 said was only hand-derived — is now compiler-checked;cargo check -p pyxlog --all-features(exit 0, reproduced) is that proof. - Honest caveat (NOTE): the proof composes over pre-existing
unsafe impl Send/Syncleaves in xlog-cuda (dlpack.rs:161, device.rs:25, cuda_graph.rs, arrow_device.rs). Those leaves were already load-bearing at the merge-base for the 5 existingdetachsites in neural.rs/ilp.rs — this PR doesn't widen the trusted base, it adds traffic. Your "principal risk" sentence names this honestly. - The scope split is exactly right: only
enforce_call_memory_limitruns before detach; onlyPyDict/DLPack-capsule construction runs after, on the GIL. Return schemas byte-identical; the argument-freeepistemic_evidencesignature guard passes live.
Live verification (head debug wheel, probe venv, pyxlog.__file__ pinned, native sha 4c062920…9408, GPU-serialized, XLOG_REQUIRE_CUDA=1)
| Claim | Observed |
|---|---|
| fixed wheel: 2 concurrency tests pass | 2/2 passed |
| combined selection: 12 passed, 0 skipped | exactly 12 passed, 0 skipped, 4.05s |
| checks/clippy-strict/fmt/diff-check | all reproduced |
make check declined (fs exhaustion + pre-existing xlog-cuda clippy) |
both reasons reproduced first-hand (ENOSPC hit this review too; the 4 warnings are at ilp_exact_nary.rs:102/263/266/310, pre-existing) |
The red leg ("pre-fix wheel: 2 failed") was verified structurally rather than live (a merge-base wheel needs a second full CUDA tree; disk was at 99%): the merge-base has zero GIL-release sites, GIL-drop requests are serviced only at bytecode boundaries, and sys.setswitchinterval(60.0) closes the pre-call window — so the observer necessarily sees the return first and the base assertion necessarily fails. The tests themselves are deterministic by construction (Barrier/Event with timeouts, error channel, no sleeps) — this is the strongest concurrency-test design in the python suite.
Findings
- LOW (inline) —
self.program.clone()at the two detach sites is a full deep clone ofLogicProgram(two complete host ASTs — your own test program carries 4096 fact clauses — plus the boxed executable plan and schema maps), paid per call to give the closure an ownedSendvalue. Dominated by the GPU pipeline, so not a perf regression worth blocking on — butArc<LogicProgram>would make the capture free, and #257 adds a third and fourth clone site of the same pattern, so it's worth doing when that lands. - LOW (process) — the verification bullet
cargo test -p pyxlog --all-featuresexecutes zero tests (theextension-modulefeature cfg-empties the only integration target;[lib] test = false). True-but-vacuous evidence; the non-vacuous form is default-features (1 passed, reproduced). The meaningful evidence for this PR is the wheel pytest legs, which are real — just use the default-features form in future verification lists. - NOTE — closed predecessor #246 (same branch, closed-not-merged) leaves no misleading residue; #253 carries the
Fixes #212linkage. Stacking: #257 modifies this file additively only (new methods + trace keys), so the 253→257 review/merge order is clean.
Process
Single conventional perf(pyxlog) commit — patch bump, honest for a throughput-only change; no AI trailers; no version/CHANGELOG edits.
Methodology: pinned worktree; line-by-line closure-capture audit; compiler-check reproduction on 0.29.2; head wheel built in an isolated probe venv with import provenance pinned (stale user-site pyxlog exists on this host and was guarded against); GPU runs serialized; red leg verified structurally with the reasoning stated.
9995a8c to
f902297
Compare
Summary
Release Python's global interpreter lock while the two epistemic APIs execute their native logic and chained GPU pipeline.
Root cause
Both
epistemic_evidenceandevaluate_conditionedentered long-running Rust/CUDA work while holding the Python GIL. That serialized unrelated Python threads for the full evaluation, including conditioned compilation, device allocation/copies, and DLPack preparation.Result
Python::detachExceptionfailures instead of swallowing process-controlBaseExceptionvaluesThe Python-visible signatures and return schemas are unchanged.
Verification
Base:
33dfea9d197035f6c0ac01c7ab4ff15a71665adfHead:
f9022971515958abd7cec2c5d06032fdb2a87295python3.8 -m py_compile python/tests/test_pyxlog_epistemic_concurrency.pyruff check python/tests/test_pyxlog_epistemic_concurrency.pycargo check -p pyxlog --all-features(compile-only proof that detached closures satisfy PyO3'sUngilbounds)cargo test -p pyxlogwith default features: 1 passed, 0 ignoredcargo check -p pyxlog --no-default-featurescargo clippy -p pyxlog --all-features --no-deps -- -D warningscargo fmt --all -- --checkgit diff --checkThe broad
make checkreplay was not accepted as evidence: one attempt exhausted the filesystem while compiling CUDA, and strict workspace Clippy is independently blocked on pre-existing warnings inxlog-cuda/src/provider/ilp_exact_nary.rs.Risk
Low API risk: signatures and result construction are unchanged. The principal risk is exposing latent synchronization assumptions now that unrelated Python threads can run concurrently; the tests exercise both real CUDA entry points with barriers/events and validate returned results.
The per-call owned
LogicProgramclone is removed by the stacked conditioned-circuit follow-up, which stores the compiled program in anArcand makes detached captures constant-time.Fixes #212