Overview
Bypass the GIL with Worker Processes
Parallel Python (pp) provides a clean, robust framework
for executing Python code concurrently across multi-core systems (SMP) and networked
compute clusters. It is lightweight, zero-dependency, and designed to integrate
smoothly into any Python application.
While multi-threading is standard for I/O-bound tasks in Python, CPython's Global Interpreter Lock (GIL) prevents CPU-bound threads from executing in true parallel fashion. Because only one bytecode instruction runs at any given moment, multi-threaded computational workloads remain serialized regardless of how many CPU cores your hardware provides.
Parallel Python breaks through the GIL by orchestrating isolated worker processes
over high-performance inter-process communication (IPC). All low-level process lifecycles,
bidirectional pipes, network sockets, serialization, and scheduling are handled automatically.
Your application simply submits jobs and retrieves results. The exact same tasks run
interchangeably on local CPU cores or on remote ppserver cluster nodes, with
intelligent, dynamic load balancing across all available resources.
SMP & Cluster Scaling
Scale identically across multi-core workstations and distributed cluster networks using a single unified API.
Effortless Parallelization
A minimalist job-based model makes transforming existing sequential code into parallel tasks intuitive and fast.
Auto-Tuned Workers
Automatically detects available CPU cores and provisions the optimal number of worker processes out of the box.
Dynamic Pool Resizing
Adjust worker process pools on the fly with set_ncpus() at runtime without stopping or restarting the server.
Content-Addressed Caching
Functions and dependencies are transmitted once and cached by content hash, eliminating serialization overhead for repeated tasks.
Dynamic Load Balancing
Real-time scheduling dynamically dispatches jobs to the next available local or remote worker to maximize cluster throughput.
Resilient & Fault Tolerant
Worker subprocess crashes and remote network disconnections are handled gracefully with automatic task rescheduling.
Zero-Config Auto-Discovery
Cluster nodes announce availability via UDP broadcast, letting clients automatically discover and utilize compute pools.
Secure Network Protocol
Remote cluster communication requires SHA-1 challenge-response authentication using a shared secret passphrase.
Cross-Platform & Multi-Arch
Runs smoothly across Linux, macOS, and Windows on x86_64, ARM64, and heterogeneous multi-node environments.
Zero Dependencies
Pure Python implementation built exclusively on the standard library. Runs anywhere Python 3.10+ is installed.
Permissive Open Source
Licensed under Apache 2.0 with explicit patent protection and full commercial readiness.
Architecture
How It Works
Your application interacts with a single pp.Server instance. Under the hood,
the server manages an in-memory job queue and an intelligent FIFO/load-balanced scheduler,
dispatching tasks across local worker subprocesses (via stdio IPC pipes) and remote cluster
nodes (via TCP sockets to ppserver) without exposing any low-level networking
or process plumbing to your code.
pp.Server dispatches jobs concurrently to local stdio-piped worker processes and remote TCP cluster nodes running ppserver.
-
Local Workers (SMP): Local workers run as dedicated Python subprocesses
(
python -m pp._worker) communicating over high-throughput standard I/O pipes. Defaulting to one worker per logical CPU core, local workers operate with zero network overhead and no socket port contention. -
Remote Cluster Nodes: Remote workers run under the
ppserverdaemon on external physical or virtual machines. Clients establish TCP connections (default port60000) using an 8-byte length-framed binary protocol with content-addressed caching and SHA-1 challenge-response authentication. - Content-Addressed Code Shipping: Functions, user-defined classes, and dependency modules are extracted and transmitted on first invocation. Remote and local nodes cache the compiled code by cryptographic content hash, ensuring subsequent submissions transfer only argument payloads.
- Mutual Challenge-Response Authentication: Remote cluster communication is protected by a shared secret passphrase. The server issues a cryptographic challenge verified with a salted SHA-1 digest before any remote command or code execution is permitted.
-
Dynamic Resource Auto-Discovery: When
ppserver -ais enabled, nodes broadcast their presence over UDP. Clients configured withppservers=("*",)detect active nodes in real time, automatically expanding the compute pool and pruning unreachable hosts. - Standard Output Redirection: Worker processes capture standard output during task execution. When a job completes, its return value and captured stdout are returned to the client, reproducing console output faithfully on the host application.
-
Fine-Grained Execution Control: Dynamically scale worker pools with
set_ncpus(), synchronize task batches by group tag withwait("group_name"), attach asynchronous completion callbacks, or leveragepp.Templatefor maximum batch submission throughput.
Quick Start
Get Started in Five Lines
pip install pp
Requires Python 3.10+ — pure Python, zero dependencies. Automatically provides the
pp module and the ppserver command-line executable.
1. Multi-Core Execution on One Machine (SMP)
import pp
def square(x):
return x * x
# Initialize the server (defaults to 1 worker per CPU core)
job_server = pp.Server()
# Submit tasks asynchronously for parallel execution
f1 = job_server.submit(square, (10,))
f2 = job_server.submit(square, (20,))
f3 = job_server.submit(square, (30,))
# Retrieve results (calling f() blocks until the task completes)
print(f1(), f2(), f3())
# Clean up worker processes and pipes when finished
job_server.destroy()
2. Distributed Execution on a Cluster
Launch the ppserver daemon on each remote computational node:
node-1$ ppserver
node-2$ ppserver
node-3$ ppserver
Connect from your client script by supplying the node hostnames or IP addresses:
import pp
ppservers = ("node-1", "node-2", "node-3:60000")
job_server = pp.Server(ppservers=ppservers)
# Submit jobs with helper functions and imported modules
f1 = job_server.submit(func1, args1, depfuncs=(helper1,), modules=("math",))
f2 = job_server.submit(func2, args2, depfuncs=(helper2,), modules=("numpy",))
# Retrieve results from whichever nodes finish first
print(f1(), f2())
job_server.destroy()
3. Cluster with UDP Auto-Discovery
Instead of hardcoding a static list of IP addresses, let cluster nodes announce themselves automatically.
Start each node with -a and use a wildcard on the client:
ppserver -a
import pp
# Auto-discover all available nodes on the local network
job_server = pp.Server(ppservers=("*",))
# On subnets where broadcast is restricted, specify the broadcast target:
# job_server = pp.Server(ppservers=("*",), broadcast="192.168.1.255")
4. High-Throughput Batch Submissions with Templates
Use pp.Template to compile and package a function and its dependencies once,
eliminating redundant serialization overhead when submitting large volumes of tasks:
import pp
def multiply(a, b):
return a * b
job_server = pp.Server()
template = pp.Template(job_server, multiply)
# Submit 1,000 tasks with minimal serialization overhead
tasks = [template.submit(i, i * 2) for i in range(1000)]
results = [t() for t in tasks]
print("Sum of results:", sum(results))
job_server.destroy()
5. Asynchronous Callbacks and Job Groups
import pp
job_server = pp.Server()
def on_complete(job_id, result):
print(f"[{job_id}] completed with result: {result}")
# The callback is invoked as on_complete(*callbackargs, result)
job = job_server.submit(my_task, (data,),
callback=on_complete,
callbackargs=("job-42",),
group="batch-1")
# Wait synchronously for all jobs in a specific group
job_server.wait("batch-1")
job_server.destroy()
Reference
Module API Reference
pp.Server
pp.Server(ncpus="autodetect", ppservers=(), secret=None, restart=False, proto=4, socket_timeout=3600, loglevel=None, broadcast=None)Instantiates a Parallel Python execution server managing local worker processes and/or remote cluster connections.
| Parameter | Default | Description |
|---|---|---|
| ncpus | "autodetect" | Number of local worker processes. Defaults to "autodetect", which queries os.cpu_count() to spawn one worker per logical CPU core. Set to 0 for cluster-only execution or specify a custom integer count. |
| ppservers | () | Tuple of remote ppserver addresses (e.g. "node-1", "192.168.1.10:60000"). Wildcard strings (e.g. "*", "192.168.1.*") activate background UDP auto-discovery. |
| secret | None | Shared secret passphrase for authenticating network connections to remote ppserver nodes. Always specify a custom secret on shared or production networks. |
| restart | False | When True, worker subprocesses are cleanly recycled after completing each task, preventing memory accumulation in tasks with leaks. |
| proto | 4 | Pickle protocol version used for serializing jobs, arguments, and return values on the wire. |
| socket_timeout | 3600 | Socket timeout in seconds (default: 1 hour). Bounds the maximum allowable duration for remote job execution and network round-trips. |
| loglevel | None | Logging level for the internal pp logger (e.g. logging.DEBUG, logging.INFO, logging.WARNING). |
| broadcast | None | Target broadcast or unicast address for UDP auto-discovery packets (defaults to 255.255.255.255 for "*"). Useful when broadcast traffic is restricted across subnets. |
pp.Server Methods
| Method | Description |
|---|---|
| submit(func, args=(), depfuncs=(), modules=(), callback=None, callbackargs=(), group='default', globals=None) | Submits a task asynchronously to the execution queue and returns a callable task handle. Parameters: args (arguments tuple), depfuncs (tuple of helper functions called by func), modules (tuple of module names to import in the worker), callback (function invoked on completion as callback(*callbackargs, result)), group (string tag for batch synchronization), and globals (dictionary, e.g. globals(), to resolve module-level functions/classes). Invoking the returned task handle task() blocks until completion and returns the evaluated result. |
| wait(group=None) | Blocks until all queued and executing jobs in the specified group finish. If group is None, blocks until all jobs across all groups complete. |
| set_ncpus(ncpus="autodetect") | Dynamically adjusts the local worker process pool size at runtime without restarting the server or dropping queued tasks. |
| get_ncpus() | Returns the current count of configured local worker processes. |
| get_active_nodes() | Returns a dictionary mapping active compute nodes to their worker counts (e.g. {"local": 8, "192.168.1.10:60000": 16}). |
| get_stats() | Returns a dictionary mapping node names to statistics objects with attributes ncpus (worker count), njobs (completed job count), and time (cumulative CPU execution time). |
| print_stats() | Prints a formatted tabular breakdown of execution statistics and cluster throughput to standard output. |
| destroy() | Gracefully terminates local worker processes, closes network transports, and releases system resources. Pending jobs that are interrupted raise DestroyedServerError. |
pp.Template
pp.Template(job_server, func, depfuncs=(), modules=(), callback=None, callbackargs=(), group='default', globals=None)Pre-compiles and serializes a function definition, dependency hierarchy, and import list once. Subsequent invocations via template.submit(*args) dispatch tasks with minimal overhead, maximizing throughput when fanning out large batches of identical computations.
pp.DestroyedServerError
Exception raised when attempting to retrieve the result of a task (task()) if the underlying pp.Server instance was destroyed before the computation finished.
Job Semantics & Best Practices
- Return Values & Output: Invoking
job()blocks and returns the evaluated result. Standard output written by the worker during execution is captured and mirrored on the client console. - Fault Resilience: If a worker subprocess exits unexpectedly or a remote node disconnects mid-computation, the server logs the incident, recycles the worker, and automatically reschedules uncompleted tasks.
- Named Lambdas & Functions: Functions defined inline as named assignments (e.g.
f = lambda x: x * 2) and top-level functions are supported. Unnamed inline lambdas (e.g.submit(lambda x: x, ...)) cannot be resolved by name and raise an immediateValueError. - Serialization: All task arguments and return values are serialized with standard Python
pickle. Arguments should be free of non-serializable objects (such as open file descriptors, network sockets, or mutex locks).
Remote Compute
ppserver Command-Line Daemon
The ppserver command turns any machine into a high-performance Parallel Python compute node.
Start it on remote hosts, then reference them directly via pp.Server(ppservers=...)
or let UDP auto-discovery connect them automatically.
# Start with 8 workers and UDP auto-discovery enabled
ppserver -w 8 -a
# Listen on a specific network interface and port with a custom secret
ppserver -i 192.168.1.50 -p 60000 -s "SuperSecretPassphrase"
# Write a PID file and inspect real-time stats using SIGUSR1
ppserver -P /var/run/ppserver.pid
kill -USR1 "$(cat /var/run/ppserver.pid)"
| Option | Description |
|---|---|
| -h, --help | Show the command-line help message and exit. |
| -a | Enable the UDP auto-discovery service (announces availability to clients). |
| -b BROADCAST | Broadcast address for auto-discovery announcements (default: 255.255.255.255). |
| -c PATH | Load configuration parameters from an INI file ([general] and [network] sections). |
| -d | Enable debug logging level for verbose diagnostic output. |
| -f FORMAT | Custom format string for log messages. |
| -i INTERFACE | Network IP interface to bind and listen on (e.g. 0.0.0.0 or 192.168.1.50). |
| -k SECONDS | Socket timeout in seconds (also bounds the maximum execution time of any single remote job). |
| -n PROTO | Pickle protocol version used on the wire (default: 4). |
| -p PORT | TCP port to listen on (default: 60000). |
| -P PID_FILE | Path to write the server process PID file. |
| -q | Enable quiet mode: suppresses startup banner and sets log level to error (only errors are printed). |
| -r | Restart worker processes after completing each task to maintain clean state. |
| -s SECRET | Secret passphrase for client authentication. Overrides the PP_SECRET environment variable. |
| -t SECONDS | Automatic idle timeout: exit if no client connections remain active for this duration. |
| -w NWORKERS | Number of worker subprocesses to start (defaults to the number of detected CPU cores). |
On Unix-like systems, sending SIGUSR1 to the server process
(kill -USR1 <pid>) prints real-time job execution statistics to standard output
on the next event loop iteration without interrupting active connections.
Examples
A Complete Distributed Example
This example calculates the sum of prime numbers below multiple thresholds concurrently.
The helper function isprime is passed via depfuncs, and the
math module is imported in worker namespaces via modules:
import math
import pp
def isprime(n):
"""Returns True if n is prime, False otherwise."""
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
for i in range(3, int(math.isqrt(n)) + 1, 2):
if n % i == 0:
return False
return True
def sum_primes(n):
"""Calculates the sum of all prime numbers below n."""
return sum(x for x in range(2, n) if isprime(x))
# Start the execution server with one worker per CPU core
job_server = pp.Server()
# Submit one job per input; isprime and math are shipped automatically
inputs = (100_000, 100_100, 100_200, 100_300)
jobs = [
job_server.submit(sum_primes, (i,), depfuncs=(isprime,), modules=("math",))
for i in inputs
]
# Collect and display results as they complete
for i, job in zip(inputs, jobs):
print(f"Sum of primes below {i:,} is {job():,}")
# Print execution breakdown across all workers
job_server.print_stats()
job_server.destroy()
The examples/ directory in the source repository contains complete runnable scripts
covering prime summation, brute-force MD5 cracking, dynamic worker pool resizing,
asynchronous completion callbacks, automatic differentiation, and cluster throughput benchmarking.
Compatibility
Tested Platforms & Runtimes
Parallel Python is pure Python with zero external dependencies and no compiled C-extensions.
Built strictly on standard library primitives (subprocess, socket, pickle,
threading, and struct), it installs effortlessly and runs reliably across an exceptionally
broad spectrum of Python versions, operating systems, hardware architectures, and endiannesses.
Zero C-Extensions
Pure Python standard library implementation. No compilation toolchains, platform-specific wheels, or third-party packages required.
Endian-Neutral Framing
Fixed 8-byte big-endian framing (!Q) ensures seamless interoperability between little-endian (x86, ARM, RISC-V) and big-endian (s390x) nodes.
Heterogeneous Clusters
Cluster clients, local workers, and remote ppserver daemons running across different OSes and CPU architectures interoperate smoothly.
1. Python Interpreters & Runtimes
| Runtime | Versions Tested | Status |
|---|---|---|
| CPython (Standard) | 3.10, 3.11, 3.12, 3.13, 3.14 |
✔ CI Verified |
| CPython Free-Threaded (No-GIL) | 3.13t, 3.14t |
✔ CI Verified |
| PyPy | PyPy 3.10 |
✔ CI Verified |
2. Operating Systems & C Libraries
| Operating System | Distributions & Environments | Status |
|---|---|---|
| Linux (glibc) | Ubuntu, Debian, Fedora, RHEL, CentOS, Arch | ✔ CI Verified |
| Linux (musl libc) | Alpine Linux, minimal scratch containers | ✔ CI Verified |
| macOS | Apple Silicon (ARM64) & Intel (x86_64) | ✔ CI Verified |
| Windows | Windows 10, Windows 11, Windows Server | ✔ CI Verified |
| FreeBSD | FreeBSD 14.1+ | ✔ CI Verified |
| OpenBSD | OpenBSD 7.5+ | ✔ CI Verified |
| NetBSD | NetBSD 10.0 / 11.0 | ✔ CI Verified |
3. Hardware Architectures & Endianness
| Architecture | Common Platforms & Silicon | Endianness | Status |
|---|---|---|---|
| x86_64 / AMD64 | Intel Core/Xeon, AMD Ryzen/EPYC workstations & cloud VMs | Little-Endian | ✔ CI Verified |
| AArch64 / ARM64 | Apple M-Series, AWS Graviton, Ampere Altra, Raspberry Pi 4/5 | Little-Endian | ✔ CI Verified |
| i686 / i386 | 32-bit x86 legacy servers, embedded systems, 32-bit Docker | Little-Endian | ✔ CI Verified |
| ARMv7 / armhf | 32-bit ARM, Raspberry Pi 2/3, BeagleBone, edge IoT hardware | Little-Endian | ✔ CI Verified |
RISC-V 64 (riscv64) |
RISC-V development boards, emulators, server silicon | Little-Endian | ✔ CI Verified |
IBM Z (s390x) |
IBM z/Architecture mainframe compute infrastructure | Big-Endian | ✔ CI Verified |