|
| 1 | +--- |
| 2 | +title: Training with Large Batches |
| 3 | +--- |
| 4 | + |
| 5 | +# Training with Large Batches |
| 6 | + |
| 7 | +SuperDex Gym scales RL training by combining two-tier vectorization with scene sharing |
| 8 | +within each worker. See |
| 9 | +[`run_with_gymnasium_vectorization.py`](./running_examples.md#2-run_with_gymnasium_vectorizationpy) |
| 10 | +for a runnable example and [Benchmarking](./benchmarking.md) for measuring throughput |
| 11 | +on your hardware. |
| 12 | + |
| 13 | +## Example Pattern |
| 14 | + |
| 15 | +This minimal example shows how to batch training safely with `HybridVectorEnv`. |
| 16 | + |
| 17 | +```python |
| 18 | +from superdex.lab.gym.envs.benchmarks.cartpole_env import CartPoleEnv |
| 19 | +from superdex.lab.gym.utils.vector import HybridVectorEnv |
| 20 | + |
| 21 | +num_training_steps = 1000 |
| 22 | + |
| 23 | + |
| 24 | +def main(): |
| 25 | + # Environment factory functions, one per environment. |
| 26 | + env_creators = [lambda: CartPoleEnv({"render_mode": None}) for _ in range(1000)] |
| 27 | + |
| 28 | + # 8 async workers, each stepping 125 environments sequentially. |
| 29 | + with HybridVectorEnv(env_creators, num_envs_per_worker=125) as env: |
| 30 | + print(f"Created {env.num_envs} environments") |
| 31 | + print(f"Using {env.num_workers} async workers") |
| 32 | + print(f"Each worker manages {env.num_envs_per_worker} environments") |
| 33 | + |
| 34 | + observations, _ = env.reset(seed=list(range(env.num_envs))) |
| 35 | + for step in range(num_training_steps): |
| 36 | + actions = ... # your policy |
| 37 | + observations, rewards, terminated, truncated, info = env.step(actions) |
| 38 | + |
| 39 | + |
| 40 | +# Required: the wrapper spawns its workers, so each child re-imports this module. |
| 41 | +if __name__ == "__main__": |
| 42 | + main() |
| 43 | +``` |
| 44 | + |
| 45 | +Using `HybridVectorEnv` as a context manager closes all workers if any worker raises |
| 46 | +an exception during the training loop. |
| 47 | + |
| 48 | +:::warning The `__main__` guard is not optional |
| 49 | +Construct `HybridVectorEnv` inside a function called from an |
| 50 | +`if __name__ == "__main__":` block, or spawned workers may fail to start. |
| 51 | +::: |
| 52 | + |
| 53 | +### Output and batch shapes |
| 54 | + |
| 55 | +The example prints: |
| 56 | + |
| 57 | +```text |
| 58 | +Created 1000 environments |
| 59 | +Using 8 async workers |
| 60 | +Each worker manages 125 environments |
| 61 | +``` |
| 62 | + |
| 63 | +Inside the wrapper, the batch shape is |
| 64 | +`(num_workers, num_envs_per_worker, ...)`; the public API flattens it to |
| 65 | +`(num_envs, ...)`. For the example above, the internal leading dimensions are |
| 66 | +`(8, 125, ...)`, and the public batch dimension is `1000`: |
| 67 | + |
| 68 | +| Value | Expected shape or result | |
| 69 | +| --- | --- | |
| 70 | +| `observations` | `env.observation_space.shape`, beginning with `(1000, ...)` | |
| 71 | +| `actions` | Exactly `env.action_space.shape`, beginning with `(1000, ...)` | |
| 72 | +| `rewards` | `(1000,)` | |
| 73 | +| `terminated` | `(1000,)` | |
| 74 | +| `truncated` | `(1000,)` | |
| 75 | +| `info` | A dict of per-environment arrays each indexed by environment ID, keyed by information type, such as truncation or termination reasons. | |
| 76 | + |
| 77 | +### Environment properties |
| 78 | + |
| 79 | +| Property | Meaning | |
| 80 | +| --- | --- | |
| 81 | +| `num_envs` | Total environments (`num_workers × num_envs_per_worker`) | |
| 82 | +| `num_workers` | Async subprocesses | |
| 83 | +| `num_envs_per_worker` | Environments stepped sequentially per worker | |
| 84 | +| `observation_space` / `action_space` | Batched, shape `(num_envs, ...)` | |
| 85 | +| `single_observation_space` / `single_action_space` | Per-environment shape | |
| 86 | + |
| 87 | +`step()` requires a NumPy action array with shape `action_space.shape` and raises |
| 88 | +`ValueError` for any other input. It returns a `BatchedStepResult` named tuple of |
| 89 | +`(observation, reward, terminated, truncated, info)`. |
| 90 | + |
| 91 | +`info` includes arrays keyed by "terminated_reason" and "truncated_reason", and each array is indexed by global environment index. To read termination or truncation reasons from `info`: |
| 92 | + |
| 93 | +```python |
| 94 | +observations, rewards, terminated, truncated, info = env.step(actions) |
| 95 | +for i in range(env.num_envs): |
| 96 | + if terminated[i] or truncated[i]: |
| 97 | + key = "terminated_reason" if terminated[i] else "truncated_reason" |
| 98 | + print(i, info[key][i]) |
| 99 | +``` |
| 100 | + |
| 101 | +## How Batching Works |
| 102 | + |
| 103 | +Large-scale training uses two mechanisms: |
| 104 | + |
| 105 | +1. **`HybridVectorEnv`** combines async and sync execution. |
| 106 | +2. **Scene sharing** reduces memory use and startup time through shared physics |
| 107 | + scenes. |
| 108 | + |
| 109 | +### Two-tier vectorization |
| 110 | + |
| 111 | +`HybridVectorEnv` has two execution layers: |
| 112 | + |
| 113 | +- **Outer layer**: an `AsyncVectorEnv` distributes work across `num_workers` |
| 114 | + subprocesses. |
| 115 | +- **Inner layer**: each worker runs a `SyncVectorEnv` that steps |
| 116 | + `num_envs_per_worker` environments sequentially in-process. |
| 117 | + |
| 118 | +The wrapper derives the worker count as |
| 119 | +`num_envs // num_envs_per_worker`; it does not choose a count based on available CPU |
| 120 | +cores. More workers increase parallelism and inter-process communication, while more |
| 121 | +environments per worker can reduce load imbalance and improve scene sharing. |
| 122 | + |
| 123 | +### Common constructor options |
| 124 | + |
| 125 | +```python |
| 126 | +HybridVectorEnv( |
| 127 | + env_fns, # Sequence[Callable[[], Env]], one per environment |
| 128 | + num_envs_per_worker=1, |
| 129 | + autoreset_mode=AutoresetMode.NEXT_STEP, |
| 130 | +) |
| 131 | +``` |
| 132 | + |
| 133 | +`env_fns` contains one factory per environment. Autoreset defaults to `NEXT_STEP` and |
| 134 | +runs at the inner layer. The outer layer disables autoreset so the two layers do not |
| 135 | +compound it. |
| 136 | + |
| 137 | +### Shared-scene state isolation |
| 138 | + |
| 139 | +Within each process, `SceneManager` keeps one instance per environment class name. Each |
| 140 | +instance holds scenes by name with a reference count. Two environment classes never |
| 141 | +share a scene, even when both use the same scene name. |
| 142 | + |
| 143 | +Each environment that shares a scene keeps its own state snapshot. Before that |
| 144 | +environment steps, it restores its snapshot into the shared scene; afterward, it |
| 145 | +captures the updated state. Sequential stepping prevents one environment's state from |
| 146 | +leaking into another. |
| 147 | + |
| 148 | +`Scene.capture_state()` captures dynamic state only: poses, velocities, controller |
| 149 | +and constraint targets, external forces, the scene clock, and the step counter. |
| 150 | +Restoring a snapshot also rewinds simulation time. It does not capture topology, |
| 151 | +materials, joint limits, or gravity. Every configuration field that changes scene |
| 152 | +construction must be part of the scene name. Otherwise, environments with different |
| 153 | +structures incorrectly share one scene. |
| 154 | + |
| 155 | +Enable sharing through the environment configuration: |
| 156 | + |
| 157 | +```python |
| 158 | +config = { |
| 159 | + "use_shared_scenes": True, # Default. |
| 160 | + "control_frequency": 20, |
| 161 | + "simulation_frequency": 100, |
| 162 | + # ... other config options |
| 163 | +} |
| 164 | + |
| 165 | +env_creators = [lambda: YourCustomEnv(config) for _ in range(1000)] |
| 166 | +``` |
| 167 | + |
| 168 | +### Scene sharing in custom environments |
| 169 | + |
| 170 | +Route scene creation through `_load_scene()`. It reuses a scene with the requested |
| 171 | +name or registers a new scene. Include every configuration field that changes scene |
| 172 | +construction in that name. See |
| 173 | +[Authoring a Custom Environment](./environments.mdx#building-a-scene-from-a-prefab) |
| 174 | +for a complete example. |
| 175 | + |
| 176 | +The three built-in benchmark environments use this pattern. `CartPoleEnv` uses |
| 177 | +`uid_fields = (cfg.use_damping, cfg.use_gravity, cfg.free_pole)`, the three fields |
| 178 | +that alter its prefab. It excludes fields such as `actuate_on_pole` that change only |
| 179 | +how actions are applied. |
| 180 | + |
| 181 | +## Failure Modes |
| 182 | + |
| 183 | +### Construction and reset inputs |
| 184 | + |
| 185 | +- **Divisibility is required.** `len(env_fns) % num_envs_per_worker != 0` raises |
| 186 | + `ValueError: The total number of environments must be divisible by the number of |
| 187 | + environments per process.` |
| 188 | +- **The multiprocessing context is always `"spawn"`.** Passing another explicit |
| 189 | + context emits a `RuntimeWarning`, and the wrapper uses `"spawn"` instead. |
| 190 | +- **Spaces must be `Box`.** The wrapper reshapes `low` and `high` arrays directly. A |
| 191 | + `Dict` or `Discrete` space fails during space flattening. Every SuperDex Gym |
| 192 | + environment satisfies this contract because `MochiEnv` flattens its structured |
| 193 | + spaces to a single `Box`. |
| 194 | +- **There are no reset masks.** Passing `options={"reset_mask": ...}` raises |
| 195 | + `ValueError: Reset mask is not supported for this vectorization.` Use Gymnasium's |
| 196 | + `AsyncVectorEnv` or `SyncVectorEnv` directly for selective resets. |
| 197 | + |
| 198 | +:::warning Use per-environment seeds for independent rollouts |
| 199 | +A scalar `int` seed is expanded twice: once by the outer `AsyncVectorEnv` and again by |
| 200 | +each inner `SyncVectorEnv`. Environment `(w, k)` receives `seed + w + k`. Therefore, |
| 201 | +`W × K` environments receive only `W + K - 1` distinct seeds. With 8 workers and 125 |
| 202 | +environments per worker, 1000 environments receive only 132 distinct seeds. The |
| 203 | +wrapper emits no error or warning, and rollouts become silently correlated. |
| 204 | + |
| 205 | +```python |
| 206 | +# Wrong: 1000 environments, 132 distinct seeds. |
| 207 | +env.reset(seed=42) |
| 208 | + |
| 209 | +# Right: one seed per environment. |
| 210 | +env.reset(seed=list(range(env.num_envs))) |
| 211 | +``` |
| 212 | + |
| 213 | +Pass a list or tuple containing one seed per environment. `reset()` validates its |
| 214 | +length and distributes the values across workers so global environment `i` receives |
| 215 | +`seed[i]`. NumPy arrays do not take this sequence-chunking path and are unsupported. |
| 216 | +::: |
| 217 | + |
| 218 | +### Shared-scene correctness |
| 219 | + |
| 220 | +:::caution Scene sharing assumes sequential stepping |
| 221 | +`use_shared_scenes` defaults to `True`, so every environment opts in. Sharing is |
| 222 | +correct only when environments in the same process run one at a time. |
| 223 | +`HybridVectorEnv` provides this guarantee through each worker's inner `SyncVectorEnv`. |
| 224 | +Do not use shared scenes with a vectorizer that steps multiple environments |
| 225 | +concurrently in the same process; their state will be corrupted. |
| 226 | +::: |
| 227 | + |
| 228 | +State capture does not support scenes containing ROM actors. Attempting to capture |
| 229 | +one raises `"State capture is not supported for scenes with ROM actors."` |
| 230 | + |
| 231 | +If scene sharing is enabled but the scene was not created through `_load_scene()`, |
| 232 | +the environment emits a `RuntimeWarning` on reset. |
| 233 | + |
| 234 | +## Performance |
| 235 | + |
| 236 | +### Choose the async/sync split |
| 237 | + |
| 238 | +For a fixed total environment count, the worker split has two main tradeoffs: |
| 239 | + |
| 240 | +- **More workers with fewer environments each** increase CPU parallelism. Each worker |
| 241 | + adds a subprocess and a full physics context. Because every worker builds its own |
| 242 | + scene, this split reduces scene-sharing benefits. |
| 243 | +- **Fewer workers with more environments each** amortize scene construction and |
| 244 | + memory across each worker. Each worker steps environments sequentially, so its wall |
| 245 | + time generally increases with `num_envs_per_worker`. |
| 246 | + |
| 247 | +Use [the benchmark script](./benchmarking.md) to tune the split for the target |
| 248 | +environment and hardware. Start `num_workers` at up to the physical CPU core count, |
| 249 | +then optionally sweep up to the logical CPU count. Increase `num_envs_per_worker` from |
| 250 | +1 until throughput plateaus. The script sweeps `--num_workers` and |
| 251 | +`--num_envs_per_worker` combinations and reports control and simulation FPS. |
| 252 | + |
| 253 | +### Scene-sharing savings |
| 254 | + |
| 255 | +When environments use an identical scene configuration, sharing reduces memory use |
| 256 | +and startup time. Sequential stepping within each worker makes the optimization safe. |
| 257 | +Within each worker process, the SuperDex Physics file cache also avoids repeated |
| 258 | +model-loading work. |
0 commit comments