13 KiB
Pregel Execution Engine Specification
Overview
Pregel is the foundational execution engine for LangGraph, implementing a Bulk Synchronous Parallel (BSP) computation model. Inspired by Google's original Pregel system, it provides a framework for executing computational graphs with stateful communication between nodes while maintaining strict invariants around execution order, state updates, and error handling.
Conceptual Model
Pregel follows the Bulk Synchronous Parallel model, where computation proceeds in a series of synchronized steps called "supersteps." Each superstep consists of three distinct phases:
- Plan: Determine which actors to execute based on pending channel updates
- Execute: Run selected actors in parallel, collecting their outputs
- Update: Apply all updates to channels atomically at the end of the step
This model ensures several critical properties:
- Determinism: Given the same input, execution produces the same output
- Isolation: Node executions within a superstep cannot observe each other's updates until the next superstep
- Atomicity: All channel updates from a superstep are applied at once
- Checkpoint-ability: The system state can be captured at superstep boundaries
Core Components
PregelProtocol
Abstract interface defining the contract for all Pregel implementations:
class PregelProtocol(Protocol):
def invoke(self, input: Any, config: Optional[dict] = None, **kwargs: Any) -> Any: ...
def stream(
self,
input: Any,
config: Optional[dict] = None,
stream_mode: Optional[StreamMode] = None,
**kwargs: Any,
) -> Iterator[Any]: ...
def get_state(self, thread_id: Optional[str] = None) -> Any: ...
def update_state(self, thread_id: str, state: Any) -> None: ...
def get_state_history(self, thread_id: str) -> list[Any]: ...
PregelNode
Represents an actor in the system with the following properties:
class PregelNode:
def __init__(
self,
name: str,
action: PregelExecutable,
*,
subscribe: Optional[Collection[str]] = None,
trigger: Optional[str] = None,
writers: Optional[Collection[str]] = None,
retry_policy: Optional[RetryPolicy] = None,
) -> None: ...
Key properties:
- name: Unique identifier for the node
- action: Function to execute when the node is triggered
- subscribe: Channel names this node listens to for updates
- trigger: Special condition for node execution
- writers: Channels this node can write to (for validation)
- retry_policy: Strategy for handling execution failures
Channels
Communication paths that store values and propagate them between nodes:
class Channel(Protocol):
def get_value(self) -> Any: ...
def update(self, value: Any) -> bool: ...
def checkpoint(self) -> Any: ...
def from_checkpoint(self, value: Any) -> None: ...
Each channel type implements this interface with specific behaviors:
- LastValue: Stores only the most recent value; rejects multiple updates in a single step
- AnyValue: Permits multiple updates within a step, storing the last one
- EphemeralValue: Temporary storage that clears after being read
- UntrackedValue: Like LastValue but excluded from checkpoints
- NamedBarrierValue: Synchronization mechanism requiring all named values to be received
- BinaryOperatorAggregate: Applies operations to combine values (sum, join, etc.)
- Topic: PubSub topic supporting multiple subscribers and values
Tasks
Units of work representing computations to execute:
class PregelTask:
def __init__(
self,
node: str,
trigger: Optional[str] = None,
retry_policy: Optional[RetryPolicy] = None,
) -> None: ...
class PregelExecutableTask:
def __init__(
self,
task: PregelTask,
inputs: dict[str, Any],
context: dict[str, Any],
) -> None: ...
Checkpoints
Snapshots of execution state at superstep boundaries:
class Checkpoint:
def __init__(self, channel_values: dict[str, Any]) -> None: ...
def get_values(self) -> dict[str, Any]: ...
def update(self, channel_values: dict[str, Any]) -> None: ...
Execution Flow
When invoke() or stream() is called, Pregel performs the following steps:
-
Initialization:
- Create or retrieve a checkpoint for the specified thread
- Load initial channel values from the checkpoint
- Transform input into channel updates
-
Superstep Loop:
- Plan: Identify nodes to execute based on channel updates
- Execute: Run selected nodes in parallel, collecting updates
- Update: Apply all updates to channels atomically
- Checkpoint: Save the current state if checkpointing is enabled
- Repeat until no more nodes are active or an END token is received
-
Termination:
- Extract output from designated output channels
- Return result or stream updates based on the stream mode
This process is depicted in the diagram below:
┌──────────────────────────────────┐
│ INPUT │
└──────────────────┬───────────────┘
│
▼
┌──────────────────────────────────┐
│ INITIALIZE STATE │
└──────────────────┬───────────────┘
│
▼
┌──────────────────────────────────┐
│ SUPERSTEP LOOP │
│ ┌────────────────────────────┐ │
│ │ PLAN │ │
│ │ (Identify active nodes) │ │
│ └────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ EXECUTE │ │
│ │ (Run nodes in parallel) │ │
│ └────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ UPDATE │ │
│ │ (Apply channel updates) │ │
│ └────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ CHECKPOINT │ │
│ │ (Save current state) │ │
│ └────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ [More nodes active?] │
│ / \ │
│ Yes No │
│ │ │ │
│ └──────────────────┘ │
└──────────────────┬───────────────┘
│
▼
┌──────────────────────────────────┐
│ OUTPUT │
└──────────────────────────────────┘
Stream Modes
Pregel supports different streaming options to provide visibility into execution:
- values: Stream the complete state after each superstep
- updates: Stream state deltas after each node execution
- debug: Stream comprehensive execution information for debugging
Each mode has different performance characteristics and use cases.
Critical Invariants
Based on LangGraph's test suite, Pregel maintains the following invariants:
-
Execution Determinism:
- Given the same input and thread ID, execution produces identical results
- Channel update order within a superstep does not affect the final state
-
State Isolation:
- Updates from one node are not visible to other nodes within the same superstep
- Nodes cannot observe partial updates from incomplete executions
-
Checkpoint Consistency:
- Checkpoints capture the complete system state at superstep boundaries
- Restored checkpoints resume execution with the exact same state
-
Task Management:
- Tasks execute in a deterministic order based on node dependencies
- Parallel execution optimizes performance but maintains consistency
-
Error Handling:
- Node execution failures can be handled with retry policies
- Unrecoverable errors propagate without corrupting the execution state
- Failures during checkpointing do not corrupt previous checkpoints
-
Termination Guarantees:
- Execution always terminates for acyclic graphs
- Cyclic graphs require explicit exit conditions to ensure termination
- Execution timeouts prevent infinite loops
Implementation Notes
Type Safety
Pregel enforces type safety through:
- Input/output schema validation
- Channel type checking for updates
- Runtime validation of node return values
Concurrency Model
Pregel balances parallelism with determinism:
- Nodes within a superstep can execute in parallel
- Channel updates are collected and applied sequentially
- Execution order is deterministic despite parallel processing
Performance Optimizations
- Eager Planning: Pregel identifies all active nodes at the start of a superstep
- Task Batching: Similar tasks can be batched for efficient execution
- Lazy Checkpointing: Only modified channels are included in checkpoints
- Channel-specific optimizations: Different channel types use specialized storage strategies
Testing Approach
The test suite for Pregel focuses on:
- Functional correctness: Ensuring proper node execution and state updates
- Concurrency safety: Verifying parallel execution does not affect determinism
- Error handling: Confirming failures are properly managed
- Checkpoint fidelity: Validating checkpoint creation and restoration
- Edge cases: Testing unusual graph topologies and execution patterns
Reimplementation Guidance
When reimplementing Pregel from scratch, consider the following approach:
- Start with a simplified sequential execution model that maintains basic invariants
- Add channel implementations one at a time, focusing on correctness
- Implement the checkpoint system with proper serialization
- Add parallel execution with careful attention to update ordering
- Implement error handling and retry policies
- Optimize for performance and resource usage
The most challenging aspects are:
- Maintaining determinism with parallel execution
- Ensuring checkpoint consistency
- Properly handling error cases
- Managing memory usage for large state objects
Example Usage
# Define node functions
def process_input(state):
# Process input data
return {"output_channel": processed_data}
def make_decision(state):
# Make a decision based on processed data
return {"decision_channel": decision}
# Create PregelNodes
input_node = PregelNode(
name="input_processor",
action=process_input,
subscribe=["input_channel"],
writers=["output_channel"]
)
decision_node = PregelNode(
name="decision_maker",
action=make_decision,
subscribe=["output_channel"],
writers=["decision_channel"]
)
# Create channels
channels = {
"input_channel": LastValue(),
"output_channel": LastValue(),
"decision_channel": LastValue()
}
# Create Pregel instance
pregel = Pregel(
nodes={"input_processor": input_node, "decision_maker": decision_node},
channels=channels,
checkpoint_factories={"memory": memory_checkpointer_factory}
)
# Invoke the graph
result = pregel.invoke(
{"input_channel": input_data},
config={"thread_id": "conversation_123"}
)
# Stream execution with updates
for update in pregel.stream(
{"input_channel": input_data},
config={"thread_id": "conversation_456"},
stream_mode="updates"
):
print(update)
Related Components
Pregel interacts closely with:
- StateGraph: High-level API that compiles to Pregel
- Channels: Communication primitives used by Pregel
- Checkpoint System: Provides persistence for Pregel execution
- Human-in-the-Loop: Uses Pregel's checkpointing for interruption/resumption