Skip to content

Add lightweight subprocess interpreter - #5

Merged
isaacbmiller merged 1 commit into
mainfrom
subprocess-interpreter-main
Aug 18, 2026
Merged

Add lightweight subprocess interpreter#5
isaacbmiller merged 1 commit into
mainfrom
subprocess-interpreter-main

Conversation

@isaacbmiller

@isaacbmiller isaacbmiller commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • rename the trusted local implementation to InProcessInterpreter
  • add a separate lightweight SubprocessInterpreter backed by one persistent CPython worker
  • support persistent state, JSON variable transport, host-tool RPC, typed SUBMIT, recoverable errors, execution timeout termination, and child-local stdout capture
  • run both explicit interpreter types through conformance, deterministic RLM/Flex consumers, and the standard benchmark matrix
  • document the measured overhead and security boundary

This intentionally excludes the much larger IsolationSpec framework from #1. A subprocess provides memory, stdout, and lifecycle separation, but it is not an OS security sandbox: generated code retains the host user's filesystem, environment, credentials, process, and network authority.

Builds on merged PR #3, which removed the experimental bind() API and updated the execution-instructions work.

Size

The execution implementation is 350 lines:

  • SubprocessInterpreter: 206 lines
  • persistent worker: 144 lines

The in-process implementation remains approximately 100 lines. They are separate public classes because their value boundaries, failure behavior, and operational contracts differ materially.

Measured overhead

Focused Linux/Python 3.11 run with 5 cold sessions and 100 warm executions; values are median / p95:

Backend Time to interactive Warm scalar Host-tool RPC 1 MiB variable Shutdown Startup RSS
In-process 0.021 / 0.077 ms 0.015 / 0.029 ms 0.024 / 0.039 ms 0.024 / 0.063 ms 0.001 / 0.001 ms 0.0 MiB
Subprocess 25.264 / 29.012 ms 0.127 / 0.177 ms 0.315 / 0.346 ms 7.502 / 9.218 ms 7.456 / 7.500 ms 10.5 MiB

The persistent worker therefore adds about 25 ms once, while warm scalar execution remains below 0.2 ms p95 and host callbacks below 0.4 ms p95. Large JSON transfer is the material boundary cost.

Real DSPy validation

A real-LM Flex flow passed with the subprocess interpreter:

import dspy
from dspy_interpreters import SubprocessInterpreter

lm = dspy.LM("openai/gpt-5-mini", temperature=1.0, max_tokens=16000)
dspy.configure(lm=lm)

solve = dspy.Flex(
    "question: str -> answer: int",
    interpreter_factory=SubprocessInterpreter,
)
result = solve(question="What is 19 + 23? Return only the integer answer.")

print(result.answer)  # 42

The exact run returned integer 42 with one real LM call.

A small real-LM GEPA optimization also passed with SubprocessInterpreter: it produced LOCAL INTERPRETER, successfully evaluated through Flex, and the compiled Flex retained SubprocessInterpreter as its interpreter factory.

Verification

  • uv run pytest -q44 passed, 1 expected xfail
  • uv run ruff check src tests scripts — passed
  • focused Ruff format check for all changed Python files — passed
  • uv build — wheel and sdist built; wheel contains both interpreter modules and the worker
  • git diff --check — clean
  • real-LM Flex flow — integer 42, one LM call
  • real-LM GEPA flow — LOCAL INTERPRETER, subprocess factory preserved

@github-actions

Copy link
Copy Markdown

Interpreter benchmark

Wall-clock milliseconds are shown as p50 / p95; memory is guest-process RSS where available.
Timing changes in parentheses are versus the prior base-branch run; negative is faster.

Backend TTI p50 / p95 (ms) Warm execute (ms) Host tool (ms) 1 MiB input (ms) Guest RSS (MiB)
Local / in-process 0.03 / 0.10 (-40.7% / -70.6%) 0.01 / 0.03 (-34.4% / -11.9%) 0.02 / 0.02 (-38.4% / -50.0%) 0.02 / 0.05 (-46.9% / -11.1%) 108.4
Local / subprocess 22.32 / 22.38 0.08 / 0.10 0.14 / 0.18 5.18 / 6.66 12.0
Monty 1.95 / 3.23 (-14.1% / -9.9%) 0.05 / 0.08 (-28.7% / -13.8%) 0.10 / 0.14 (-24.1% / -16.4%) 1.76 / 1.83 (+27.7% / -38.0%)
DSPy Deno/Pyodide 1935.34 / 2420.51 (-21.9% / -17.3%) 2.61 / 3.83 (-12.1% / -10.0%) 2.78 / 4.22 (-10.3% / +4.9%) 49.24 / 55.98 (-16.9% / -20.3%)
IPython kernel subprocess 559.53 / 1086.11 (-8.8% / -3.2%) 3.69 / 3.84 (-14.9% / -15.3%) 4.42 / 54.95 (-11.3% / +945.6%) 307.27 / 319.17 (-25.5% / -24.6%) 62.6

TTI covers construction through the first completed execution. Raw samples, p95 values, host process-tree RSS, and environment metadata are in the uploaded interpreter-benchmark-report artifact.

@isaacbmiller
isaacbmiller merged commit f05f3bd into main Aug 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants