Branching¶
Branching enables creating independent environment copies from intermediate states. Given a state at step K, you can create N independent (environment, state) pairs that can each be stepped, branched further, or run to completion independently.
Use Cases¶
- Q-value estimation: Try many continuations from the same state to estimate expected reward
- Action exploration: Try different actions from a decision point and compare outcomes
- Success rate computation: Branch N times from a checkpoint and measure how often a strategy succeeds
- Recursive tree search: Branch from branches to build exploration trees
Strategy Tiers¶
llenvs auto-selects the best strategy for each environment:
| Rank | Strategy | Overhead | Fidelity | Applicability |
|---|---|---|---|---|
| 1 | Direct | Zero | Perfect | pure_step=True envs (ReasoningGym, HF, Verifiers ST, Dialogue, GEM) |
| 2 | ProcessFork | Fork cost (fast, COW) | Perfect | Any env on Unix (macOS/Linux) |
| 3 | ActionReplay | O(K) per branch | Perfect if deterministic | Envs with seed + task_index support |
Quick Start¶
from llenvs.core.branching import BranchManager
env = my_environment()
state, info = env.reset(seed=42, options={"task_index": 0})
# Step to an interesting state
actions = []
for i in range(5):
action = Action.from_text(f"step-{i}")
actions.append(action)
result = env.step(state, action)
state = result.next_state
# Checkpoint and branch
with BranchManager.create(env) as mgr:
mgr.checkpoint("step5", state, actions=tuple(actions),
reset_options={"seed": 42, "task_index": 0})
# Try 100 continuations
results = []
for _ in range(100):
branch_env, branch_state = mgr.branch("step5")
# ... run trajectory on branch_env from branch_state ...
Strategies¶
DirectStrategy¶
For environments with spec.pure_step=True. step() is a pure function, so the same environment instance handles all branches. Zero cost.
with BranchManager.create(env) as mgr: # auto-selects Direct
mgr.checkpoint("s0", state, actions=(), reset_options={})
e1, s1 = mgr.branch("s0") # e1 is env (same instance)
e2, s2 = mgr.branch("s0") # e2 is env (same instance)
ProcessForkStrategy¶
For any environment on Unix. Uses os.fork() to snapshot the entire process, including mutable backend state. Each branch gets its own process with copy-on-write memory.
with BranchManager.create(env, strategy="process_fork") as mgr:
mgr.checkpoint("s0", state, actions=(), reset_options={})
e1, s1 = mgr.branch("s0") # e1 is a ContainerEnvironment proxy
e2, s2 = mgr.branch("s0") # e2 is a separate proxy
Each branch creates a forked server process. Resources are cleaned up on release(), close(), or context manager exit.
ActionReplayStrategy¶
For deterministic environments with seed and task_index support. Creates a fresh environment, resets with the same parameters, and replays all stored actions.
with BranchManager.create(env, strategy="action_replay",
env_factory=MyEnvClass) as mgr:
mgr.checkpoint("s0", state, actions=tuple(actions),
reset_options={"seed": 42, "task_index": 0})
e1, s1 = mgr.branch("s0") # fresh env, replayed to step K
Requires env_factory (callable that creates a fresh env instance). Cost is O(K) per branch where K is the checkpoint depth.
BranchManager API¶
Creating¶
# Auto-resolve strategy
mgr = BranchManager.create(env)
# Explicit strategy
mgr = BranchManager.create(env, strategy="direct")
mgr = BranchManager.create(env, strategy="process_fork")
mgr = BranchManager.create(env, strategy="action_replay", env_factory=MyEnv)
Checkpointing¶
name: String identifier for laterbranch()/release()callsstate: TheStateto checkpointactions: Tuple ofActions taken to reach this state (used by ActionReplay)reset_options: Dict withseedandtask_indexused in the originalreset()call
Checkpointing the same name again overwrites the previous checkpoint.
Branching¶
Returns an independent (environment, state) pair. Can be called multiple times per checkpoint.
Resource Management¶
mgr.release("checkpoint_name") # Release one checkpoint
mgr.close() # Release all checkpoints
# Or use as context manager (recommended)
with BranchManager.create(env) as mgr:
... # All checkpoints released on exit
Recursive Branching¶
Branches can be branched further using nested BranchManager instances:
with BranchManager.create(env) as mgr:
mgr.checkpoint("A", state_a, actions_a, opts)
for i in range(3):
b_env, b_state = mgr.branch("A")
result = b_env.step(b_state, some_action)
with BranchManager.create(b_env) as sub_mgr:
sub_mgr.checkpoint("B", result.next_state, ..., ...)
for j in range(3):
bb_env, bb_state = sub_mgr.branch("B")
# 9 total branches at depth 2
Configuration¶
Set branching_strategy on EnvironmentConfig to specify a preference:
from llenvs.core.config import EnvironmentConfig
config = EnvironmentConfig(
name="webshop",
adapter="webshop",
branching_strategy="process_fork", # or "direct", "action_replay", None
)
When None (default), BranchManager.create() auto-resolves the best strategy.
Per-Adapter Capabilities¶
| Adapter | pure_step |
Direct | ProcessFork | ActionReplay |
|---|---|---|---|---|
| reasoning_gym | True | Yes | Yes | Yes |
| huggingface | True | Yes | Yes | Yes |
| gem | True | Yes | Yes | Yes |
| dialogue | True | Yes | Yes | Yes |
| verifiers (single-turn) | True | Yes | Yes | Yes |
gymnasium (pure_step=True) |
True | Yes | Yes | Yes |
| gymnasium (default) | False | No | Yes | Yes |
| verifiers (tool) | False | No | Yes | No |
| webshop | False | No | Yes | No |
| agentgym | False | No | Yes | No |
| openenv | False | No | Yes | No |
ProcessForkStrategy works for all environments on Unix, making it the universal fallback for mutable-backend environments.
Resource Management¶
ProcessForkStrategy creates server processes for each checkpoint and branch. These are cleaned up:
- On
mgr.release("name")for a specific checkpoint - On
mgr.close()or context manager exit for all checkpoints - Via
atexithandler as a safety net
Always use BranchManager as a context manager to ensure cleanup.