python sdk
RL pools
Thousands of live environments behind one batched step. Environments stay warm and resident between steps — nothing is reloaded,
re-imported or re-serialised — and step() is a single request carrying every action and
returning every result. This is the one part of the SDK with no CLI equivalent.
why it stays fast (and lean)
Each environment is a long-lived sandboxed process. Code and interpreters are mounted read-only and shared across the pool, so memory stays low as N grows. Steps are fanned out in parallel on the worker. There is no snapshot store and no per-step container boot.
- Soft reset —
pool.reset()/reset(where=…)calls the env’s own reset in-process. Fast; state lives in your Python variables. - Hard reset —
pool.reset(hard=True)kills and respawns the sandboxes and wipes the workspace. Slower; the clean start when you need episode N+1 byte-identical to episode 1. - Not snapshots — we do not checkpoint process memory. Warm pools give you speed without holding a full image of each env in RAM.
the environment
from boltzlabs.env import serve
state = {"t": 0}
def reset(seed):
state["t"] = 0
return {"t": 0}
def step(action):
state["t"] += 1
return {"t": state["t"]}, float(action), state["t"] >= 100, {}
serve(reset=reset, step=step)serve() owns the three things that silently break
this channel: buffering, stray output on stdout, and exceptions. It is shipped into the sandbox with
your code automatically — you never copy a file.
the training loop
from boltzlabs import RLPool
with RLPool("./my_env", 1000) as pool:
obs = pool.reset(seed=0)
for t in range(max_steps):
actions = policy(obs)
obs, rewards, dones, infos = pool.step(actions) # ONE request
if dones.any():
obs = pool.reset(where=dones) # only the finished ones
print(pool.timing) # worker time and round trip, never conflated
print(pool.envs_per_gb()) # the number that decides the billrewards is np.float32[n] and dones is np.bool_[n], so it drops into a trainer as it is. obs and infos stay as they came — they are your JSON, and coercing
them into an array would be a guess about your observation space.
the rest of the pool
pool.timing.worker_ms
the batch, measured on the worker — excludes the network
pool.timing.roundtrip_ms
your process's wall clock around the whole call
pool.timing.stragglers
environments that missed their deadline; returned done with zero reward
pool.reset(where=dones)
partial reset — returns all n observations, spliced back in place
pool.reset(hard=True)
respawn the sandboxes: byte-identical starts, slower
BoltzLabsVecEnv(pool)
Gymnasium vectorized adapter — pip install boltzlabs[gym]
Pools whose reward is a timing measurement should be created with serialize_measurement=True: it funnels each step
through a single gate so exactly one environment's code runs at a time. Rollout stays concurrent
— only the measurement funnels.