Performance and throughput¶
This page is about computational throughput (how fast the code runs), which is separate from the learning-quality numbers in Benchmarks. Reproduce it with:
python examples/benchmark_throughput.py --device cpu --steps 5000
The script reports, per algorithm:
- env steps/sec - pure environment stepping under a random policy, the Python simulation ceiling with no learning involved
- train steps/sec - the full training loop: act, step the environment, store the transition, run the gradient update
- predict calls/sec - deterministic inference throughput of the learned policy
Measured throughput¶
Single CPU thread (torch.set_num_threads(1)), PyTorch 2.9, small default networks:
| Algorithm | Environment | env steps/s | train steps/s | predict/s |
|---|---|---|---|---|
| PPO | CartPole | 94,000 | 1,500 | 9,600 |
| DQN | CartPole | 95,000 | 940 | 23,800 |
| SAC | Pendulum | 26,000 | 100 | 4,700 |
The gap between env steps/sec and train steps/sec is the cost of the gradient update. It is largest for SAC, which updates two critics, an actor, and the temperature on every step, so its per-step work is several backward passes rather than one.
Choosing a device¶
For the small networks these environments use, a single CPU thread is usually the fastest option for training. The per-step work is tiny, so throughput is dominated by Python-side environment stepping and per-call overhead rather than matrix math, and a GPU spends most of its time launching kernels instead of computing. A GPU becomes worth it when the networks or batch sizes are large enough that the matrix math dominates the launch overhead, for example wide multi-layer policies, image observations, or large off-policy batch sizes.
Select the device explicitly:
from decisionrl.algorithms import SAC
agent = SAC(env, device="cpu") # or "cuda", or "auto" (default)
Practical levers¶
- Threads. More CPU threads rarely helps small models and can hurt because of
synchronisation overhead. Set
torch.set_num_threads(1)for the tightest per-step latency, and raise it only for wide networks or large batches. - Batch size and update frequency. For off-policy agents,
train_freqandgradient_stepstrade sample efficiency against wall-clock throughput: fewer, larger updates run faster per environment step. torch.compile.decisionrl.utils.torch_utils.maybe_compilewraps a module withtorch.compilewhen a compiler backend is available and falls back to the eager module otherwise, so it is safe to call unconditionally. It helps most for larger networks; for the tiny defaults here the compile overhead is not repaid.
from decisionrl.utils.torch_utils import maybe_compile
agent.policy = maybe_compile(agent.policy, enabled=True)
- Inference. Exported ONNX policies (see Serving) run inference with onnxruntime alone and do not carry the PyTorch training stack, which lowers both latency and memory footprint for deployment.