Appearance
How to Choose Frameworks and Tools
In a nutshell: this page helps you answer "which framework should I actually build my RL code on" — a side-by-side comparison of six options (Stable-Baselines3, Ray RLlib, Tianshou, CleanRL, Brax, and rolling your own), plus one piece of repeatedly validated engineering advice: "copy CleanRL first, then modify." By the end you'll have a concrete selection, instead of hopping back and forth between six READMEs.
First, a splash of cold water: framework choice matters far less than you think. The success of an RL project hinges on the environment, the reward, evaluation, and hyperparameters — not on which library you picked (the "simplicity first" rule in RL design principles says exactly this). What really determines your productivity is understanding the fundamentals — and the best vehicle for understanding is a clean, readable reference implementation. So this page's conclusion leans toward "CleanRL for learning, SB3 for production, RLlib/Brax for scale," rather than handing you a single "one true answer."
1. The Three Dimensions of Framework Choice
Every framework trades off along these three dimensions — figure out what you actually need first:
Dimension 1 Maturity (How complete are the algorithm implementations? Are the
pits patched? How large is the community?)
Dimension 2 Flexibility (Can you modify internals? Roll a custom algorithm?)
Dimension 3 Parallelism (Single machine, single GPU → multi-machine distributed —
how far can it scale?)
Choosing = finding your spot on the Pareto frontier of these three| Dimension | Questions to ask | Implication |
|---|---|---|
| Maturity | How many production deployments? Is the issue tracker active? Have the algorithm implementations been vetted by the community? | More mature means more peace of mind, but more "black box" |
| Flexibility | Do I need to change network architectures, define custom losses, invent new algorithms? | More flexible means more effort, but more malleable |
| Parallelism | Is a single process not enough? How many environments in parallel? Do I need GPU/multi-machine? | More parallel means more complex |
For the vast majority of personal projects and small-to-medium business projects, the parallelism requirement is "one GPU is plenty" — which eliminates most options, leaving essentially just "SB3 or CleanRL."
2. The Six Options, One by One
1. Stable-Baselines3 (SB3) — production-grade, works out of the box
- In one sentence: The most mature, best-documented, most "foolproof" RL library in the PyTorch ecosystem.
- Strengths: Unified API (
PPO(env).learn()), broad built-in algorithm coverage (A2C/DQN/PPO/SAC/TD3/DDPG), high-quality docs and tutorials, seamless Gymnasium integration. - Good for: A business team that needs a reliable algorithm that "just runs"; teaching and comparison; your default starting point for every project.
- The price: Poor flexibility — changing networks or losses means hacking internals; parallelism relies on
SubprocVecEnv, which caps out at modest scale.
python
from stable_baselines3 import PPO
from stable_baselines3.common.env_util import make_vec_env
env = make_vec_env("CartPole-v1", n_envs=4)
model = PPO("MlpPolicy", env, learning_rate=3e-4, seed=0, verbose=1)
model.learn(total_timesteps=50_000)
model.save("ppo_cartpole") # save the model2. Ray RLlib — the distributed veteran
- In one sentence: An industrial-strength framework built for "large-scale distributed training."
- Strengths: Distributed scheduling on top of Ray, multi-machine multi-GPU support, massive environment parallelism,
Tunefor automated hyperparameter search; broad algorithm coverage. - Good for: Large-scale environment sampling, multi-machine clusters, teams already invested in the Ray ecosystem (
Tune,Serve,Datasets). - The price: A steep learning curve, deep abstraction layers, and massive overkill for small projects; the API evolves fast, so online tutorials go stale quickly.
3. Tianshou — the academic's flexible "Swiss Army knife"
- In one sentence: A high-level PyTorch RL library led by the Chinese research community, with flexibility approaching hand-written code.
- Strengths: Modular design (policies / collectors / environments / trainers compose freely), extremely friendly to research newcomers, complete built-in algorithm coverage (including newer ones like QRDQN, IQN).
- Good for: Research, ablations, and rapidly validating new ideas in academic/lab settings.
- The price: Flexible = you must understand its abstractions; docs and community are smaller than SB3's.
4. CleanRL — readability-first teaching material and engineering blueprint
- In one sentence: Compresses a complete PPO implementation into a single file plus a matching write-up — code as paper.
- Strengths: Single Python file, zero abstraction,
argparsefor hyperparameters; every algorithm ships with an explanation doc aligned to the paper; full coverage of PPO/SAC/DQN/DDPG and more. - Good for: Learning the fundamentals (highly recommended), a self-research starting point that is "readable, modifiable, auditable," and paper reproduction.
- The price: It's a "reference implementation," not a "general-purpose library" — no distributed training, no scheduling, you wire up logging yourself.
python
# CleanRL's PPO: single file, richly commented (skeleton excerpt)
# python cleanrl/ppo.py --env-id CartPole-v1 --total-timesteps 50000
class Agent(nn.Module):
def __init__(self, envs):
super().__init__()
self.critic = nn.Sequential(...)
self.actor = nn.Sequential(...)
def get_value(self, x): ...
def get_action_and_value(self, x, action=None): ...
# Main loop: rollout → GAE → update → logging, all laid flat in one file5. Brax / PureJaxRL — GPU parallelism and the JAX ecosystem
- In one sentence: Write your environments in differentiable JAX, run entire batches of them directly on GPU/TPU, and leave CPU sampling in the dust.
- Strengths: Brax provides JAX versions of physics environments (Ant, Humanoid, HalfCheetah, etc.); PureJaxRL builds minimal pure-JAX PPO/SAC implementations on top, finishing massively parallel training within a few thousand lines.
- Good for: Tight sample budgets, extreme-throughput ambitions, research/frontier engineering that wants "environment + algorithm all on GPU," and anyone seriously pushing training scale.
- The price: JAX has a steep learning curve, a poor debugging experience (abstract error messages), and an ecosystem still evolving at high speed.
6. Rolling your own — the last resort, and the best teacher
- In one sentence: Write the training loop, the algorithm, the replay buffer, and the logging entirely yourself.
- Good for: Learning (every RL practitioner should hand-write it once — see the progressive tutorial); or requirements no existing framework can meet (custom environment protocols, exotic parallelism, differentiable environments).
- The price: You step into every pit yourself — debugging costs are enormous, and you'll likely end up re-implementing an unmaintained SB3.
The right way to "roll your own"
Rolling your own ≠ reinventing every wheel from zero. Copy CleanRL first, then modify: take a readable reference implementation as the skeleton, and gradually replace parts with what you need. Writing everything yourself = spending two or three months on engineering details someone else already solved. This advice is the most important output of this page — see section 5.
3. The Six Options, Side by Side
| Dimension | SB3 | RLlib | Tianshou | CleanRL | Brax/PureJaxRL | Roll your own |
|---|---|---|---|---|---|---|
| Language/backend | PyTorch | PyTorch/TF | PyTorch | PyTorch | JAX | Anything |
| Algorithm coverage | Broad and stable | Broadest | Broad and current | Mainstream algorithms | Mostly PPO/SAC | Up to you |
| Works out of the box | ★★★★★ | ★★★★ | ★★★★ | ★★★ | ★★★ | ★ |
| Flexibility | ★★ | ★★★ | ★★★★★ | ★★★★ | ★★★★ | ★★★★★ |
| Distributed/parallel | Single-machine vector | Multi-machine, large scale | Single-machine | Single-machine | GPU-scale | Up to you |
| Learning cost | Low | High | Medium | Low | High | Highest |
| Docs/community | Best | Good | Medium | Excellent (one doc per file) | Medium | — |
| Best for | Business/beginners | Large-scale teams | Researchers | Learners/self-research starting point | Scaling researchers | Geeks/special needs |
4. Practical Advice: Choose by Scenario
| Scenario | Recommendation | Why |
|---|---|---|
| Learning the fundamentals, writing RL for the first time | CleanRL + SB3 | CleanRL for understanding every number, SB3 as a quick cross-check |
| The business team wants a policy that "runs, reliably" | SB3 | Complete docs, few pits, low maintenance |
| Need to modify networks / reward shaping on top of SB3 | SB3 + custom MlpExtractor / callbacks | Most changes fit SB3's extension points |
| Need large-scale parallelism / multi-machine training | RLlib (CPU cluster) or Brax (GPU) | Pick by hardware |
| Doing research, ablations, trying new ideas | Tianshou | The most ergonomic modularity |
| Deep customization (differentiable envs, custom protocols) | Roll your own + copy CleanRL | Copy first, modify after |
| Reproducing a specific paper | That paper's official implementation | Official code is closer to the paper than any framework |
An often-overlooked option: just use the paper's official implementation
To reproduce PPO/DQN/RLHF, the first choice is always the paper's repo (CleanRL often maintains a corresponding replica as well). Framework implementations are wrapped in abstraction, and many details (reward normalization, the exact advantage computation) may differ from the paper — this is exactly the origin of "check implementation differences before reproducing" in tuning and hyperparameter optimization.
5. The Boundary with Rolling Your Own: "Copy CleanRL First, Then Modify"
This advice deserves its own section. It has three progressive layers:
text
Layer 1 Copy: Clone CleanRL's ppo.py and understand every line of it
Layer 2 Modify: Swap in your environment, your network, your reward, your logging
Layer 3 Grow: Bring in components from SB3/RLlib (callbacks, vectorization,
logging) or migrate wholesale — only when neededWhen should you graduate from "copying CleanRL" to "a bigger framework"?
| Trigger signal | Action |
|---|---|
| Environment sampling becomes the bottleneck (single-process CPU too slow) | Add gymnasium.vector / SubprocVecEnv first; RLlib/Brax only if that's not enough |
| Need multi-machine training | Go straight to RLlib or Brax — don't struggle on your own stack |
| Need integration with the company's scheduling/storage systems | Start with SB3 (easy ecosystem integration), or evaluate RLlib directly |
| The team will maintain this code for 2+ years | Prefer SB3 (the community maintains it for you) |
| Just validating an idea, no long-term maintenance | CleanRL / your own fork is fine |
The true cost of rolling your own
The biggest hidden cost of rolling your own isn't development — it's maintenance and reproducibility: six months later your "quick prototype" has grown into technical debt nobody understands, while the SB3 community has fixed hundreds of edge-case bugs for you. Unless you have a clear reason (learning, special protocols, performance), "copy CleanRL first, then modify; switch to SB3 the moment you can."
Extension-point examples: "controlled self-research" on top of SB3
Many people think choosing SB3 means losing control. In fact, SB3's extension points cover most "I just want to change one thing" needs. Four levels, from lightest to heaviest:
| Level | Need | SB3's entry point |
|---|---|---|
| 1 | Change network architecture | Custom MlpExtractor / features_extractor_class |
| 2 | Change training-loop behavior | callbacks (saving, evaluation, LR scheduling, custom logging) |
| 3 | Change reward/observation | Gymnasium wrappers (RewardWrapper/ObservationWrapper) |
| 4 | Change core algorithm logic | Subclass PPO/SAC and override methods (at this point you're near "copy and modify") |
A typical level-2 example — anneal the learning rate dynamically during training and evaluate periodically:
python
from stable_baselines3 import PPO
from stable_baselines3.common.callbacks import BaseCallback
class TrainCallback(BaseCallback):
"""After each rollout: linearly decay the learning rate + print eval return periodically."""
def _on_step(self):
if self.n_calls % self.model.n_steps == 0:
# Linearly decay the learning rate to 1/10
progress = self.num_timesteps / self.model.total_timesteps
new_lr = 3e-4 * (1.0 - 0.9 * progress)
self.model.lr_schedule = lambda _: new_lr
return True
model = PPO("MlpPolicy", "CartPole-v1", learning_rate=3e-4, verbose=0)
model.learn(total_timesteps=100_000, callback=TrainCallback())A typical level-1 example — give the policy a feature extractor (pass observations through a custom network first):
python
import torch.nn as nn
from stable_baselines3 import PPO
from stable_baselines3.common.torch_layers import BaseFeaturesExtractor
class ObsNormExtractor(BaseFeaturesExtractor):
def __init__(self, observation_space, features_dim=128):
super().__init__(observation_space, features_dim)
self.net = nn.Sequential(
nn.Linear(observation_space.shape[0], 128), nn.ReLU(),
)
def forward(self, observations):
return self.net(observations)
model = PPO("MlpPolicy", "CartPole-v1",
policy_kwargs={"features_extractor_class": ObsNormExtractor})When "extending SB3" turns into "must roll your own"
SB3's extension points solve 90% of needs; the remaining 10% concentrate on: needing a custom training loop (non-standard update rhythms), needing a differentiable environment (the environment participates in backprop), and needing custom multi-machine scheduling. When those show up, it's not too late to start "copying CleanRL and modifying."
6. Frameworks in Concert with the Toolchain
A framework is always just the middle layer. The full toolchain looks like this:
text
┌──────────────────────────────────────────────────────────────┐
│ Environment layer Gymnasium (unified interface) │
│ ├─ MuJoCo (continuous control) │
│ ├─ Atari (pixel observations) │
│ ├─ Brax (JAX environments) │
│ └─ Custom environments │
├──────────────────────────────────────────────────────────────┤
│ Sampling layer SubprocVecEnv / gymnasium.vector / │
│ multi-machine │
├──────────────────────────────────────────────────────────────┤
│ Algorithm layer SB3 / RLlib / Tianshou / CleanRL / DIY │
├──────────────────────────────────────────────────────────────┤
│ Config layer Hydra (config management) │
│ Logging layer W&B / TensorBoard / CSV │
│ Tuning layer Optuna (Bayesian optimization) │
│ Evaluation layer rliable / custom eval scripts │
└──────────────────────────────────────────────────────────────┘Choose the framework with the layers above and below in mind:
- Environment compatibility: Is your environment Gymnasium-interfaced? Brax environments are available via
gymnasium.make(braxprovides Gymnasium wrappers); Atari/MuJoCo both have Gymnasium integrations. Non-Gymnasium environments (some industrial simulators) instantly eliminate most framework options. - Observation preprocessing: Do pixel observations (Atari) need
frame_stack/grayscale? SB3 hasAtariPreprocessingbuilt in; CleanRL has ready-made flags — both are painless on image tasks. - Toolchain integration: SB3 has official
EvalCallbackandtensorboardintegration; CleanRL usesargparse+ manual logging and can plug into W&B; RLlib hasTunefor tuning andServefor deployment built in.
Quick Selection Flowchart
text
Start
├─ Writing RL for the first time? --------------------→ CleanRL tutorials + SB3 cross-check
├─ Business needs a stable policy? ------------------→ SB3
├─ Single-machine parallelism not enough? -----------→ RLlib or Brax (by CPU/GPU)
├─ Doing research / ablations? ----------------------→ Tianshou
└─ Want full control of everything? -----------------→ Copy CleanRL first, then decide whether to roll your own7. One-Sentence Summary for Your Team
CleanRL for teaching, SB3 for production, RLlib/Brax for scale, and for special needs — copy CleanRL first, then modify.
The framework is scaffolding, not the work. Your work is the whole experimental system of "environment + reward + evaluation + tuning" — no framework can help you with that part; you have to build it yourself following the pipeline in build an RL project from scratch.
Further Reading
- Progressive tutorial: three Gymnasium versions up and running — understand what the framework is doing for you, with minimal skeletons
- Build an RL project from scratch — the project engineering beyond frameworks: environment, evaluation, deployment, monitoring
- Datasets and tools file — the complete index of environment libraries (Gymnasium/MuJoCo/Brax) and the training toolchain (Hydra/Optuna/W&B)
- The Actor-Critic family — the theoretical foundations of the algorithms built into frameworks: the PPO/SAC/DDPG lineage
- Tuning and hyperparameter optimization — after choosing a framework, how to run batch experiments and tune
References
- Stable-Baselines3 documentation: stable-baselines3.readthedocs.io; source: github.com/DLR-RM/stable-baselines3
- Ray RLlib documentation: docs.ray.io/en/latest/rllib/
- Tianshou documentation: tianshou.org; source: github.com/thu-ml/tianshou
- CleanRL documentation: docs.cleanrl.dev; source: github.com/vwxyzjn/cleanrl
- Brax (Google, JAX physics engine): github.com/google/brax
- PureJaxRL (minimal RL in pure JAX): github.com/luchris429/purejaxrl
- Gymnasium (Farama Foundation): gymnasium.farama.org; MuJoCo: mujoco.org
- Hydra: hydra.cc; Optuna: optuna.org; Weights & Biases: wandb.ai