Skip to content

Commit b9ba04f

Browse files
theap06claude
andcommitted
[Feature] OfflineToOnlineTrainer + sota-implementation for offline->online RL
Follow-up to the OfflineToOnlineReplayBuffer PR: a config-driven SAC trainer that drives the offline-pretrain -> online-finetune transition, plus a sota-implementations script demonstrating it. - OfflineToOnlineTrainer (subclasses SACTrainer): routes collected experience to the online buffer (pre_epoch), samples a mixed offline/online batch (process_optim_batch), and anneals the offline fraction to zero over anneal_frames (post_steps). Two reusable hooks back it: OfflineToOnlineReplayBufferHook (projects online transitions onto the offline dataset schema so the mixed-batch concat stays valid) and OfflineToOnlineAnnealHook. - Hydra configs: OfflineToOnlineReplayBufferConfig and OfflineToOnlineTrainerConfig, registered under the replay_buffer / trainer groups (name "offline_to_online"). - sota-implementations/offline_to_online_trainer/: train.py + config.yaml (SAC on HalfCheetah, offline dataset via d4rl:/minari: string). - Tests: hook unit + flow tests, a gated functional train() run on Pendulum, and config-instantiation tests; OfflineToOnlineTrainer added to the auto_log_optim_steps invariant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6477e4a commit b9ba04f

10 files changed

Lines changed: 990 additions & 1 deletion

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Offline-to-online SAC fine-tuning for HalfCheetah.
2+
#
3+
# Warm-starts SAC on an offline dataset and fine-tunes online, annealing the
4+
# offline sampling fraction to zero over `trainer.anneal_frames` frames. Built
5+
# on the configurable trainer system (see sota-implementations/sac_trainer).
6+
7+
defaults:
8+
9+
- transform@transform0: step_counter
10+
- transform@transform1: double_to_float
11+
- transform@transform2: reward_sum
12+
13+
- env@training_env: batched_env
14+
- env@training_env.create_env_fn: transformed_env
15+
- env@training_env.create_env_fn.base_env: gym
16+
- transform@training_env.create_env_fn.transform: compose
17+
18+
- model@models.policy_model: tanh_normal
19+
- model@models.qvalue_model: value
20+
21+
- network@networks.policy_network: mlp
22+
- network@networks.qvalue_network: mlp
23+
24+
- collector@collector: sync
25+
26+
- replay_buffer@replay_buffer: offline_to_online
27+
- trainer@trainer: offline_to_online
28+
- optimizer@optimizer: adam
29+
- loss@loss: sac
30+
- target_net_updater@target_net_updater: soft
31+
- logger@logger: wandb
32+
- _self_
33+
34+
# Network configurations
35+
networks:
36+
policy_network:
37+
out_features: 12 # HalfCheetah action space is 6-dimensional (loc + scale)
38+
in_features: 17 # HalfCheetah observation space is 17-dimensional
39+
num_cells: [256, 256]
40+
41+
qvalue_network:
42+
out_features: 1 # Q-value output
43+
in_features: 23 # HalfCheetah observation space (17) + action space (6)
44+
num_cells: [256, 256]
45+
46+
# Model configurations
47+
models:
48+
policy_model:
49+
return_log_prob: true
50+
in_keys: ["observation"]
51+
param_keys: ["loc", "scale"]
52+
out_keys: ["action"]
53+
network: ${networks.policy_network}
54+
55+
qvalue_model:
56+
in_keys: ["observation", "action"]
57+
out_keys: ["state_action_value"]
58+
network: ${networks.qvalue_network}
59+
60+
transform0:
61+
max_steps: 1000
62+
step_count_key: "step_count"
63+
64+
transform1:
65+
# DoubleToFloatTransform - converts double precision to float to fix dtype mismatch
66+
in_keys: null
67+
out_keys: null
68+
69+
transform2:
70+
# RewardSumTransform - sums up the rewards
71+
in_keys: ["reward"]
72+
out_keys: ["reward_sum"]
73+
74+
training_env:
75+
num_workers: 1
76+
create_env_fn:
77+
base_env:
78+
env_name: HalfCheetah-v4
79+
transform:
80+
transforms:
81+
- ${transform0}
82+
- ${transform1}
83+
- ${transform2}
84+
_partial_: true
85+
86+
# Loss configuration
87+
loss:
88+
actor_network: ${models.policy_model}
89+
qvalue_network: ${models.qvalue_model}
90+
target_entropy: "auto"
91+
loss_function: l2
92+
alpha_init: 1.0
93+
delay_qvalue: true
94+
num_qvalue_nets: 2
95+
96+
target_net_updater:
97+
tau: 0.001
98+
99+
# Optimizer configuration
100+
optimizer:
101+
lr: 3.0e-4
102+
103+
# Collector configuration (sync: offline-to-online does not support async collection)
104+
collector:
105+
create_env_fn: ${training_env}
106+
policy: ${models.policy_model}
107+
total_frames: 1_000_000
108+
frames_per_batch: 1000
109+
init_random_frames: 0 # the offline dataset already warm-starts learning
110+
track_policy_version: true
111+
_partial_: true
112+
113+
# Replay buffer: immutable offline dataset + growing online buffer.
114+
# Swap offline_dataset for e.g. "minari:mujoco/halfcheetah/expert-v0".
115+
replay_buffer:
116+
offline_dataset: "d4rl:halfcheetah-medium-v2"
117+
online_capacity: 1_000_000
118+
offline_fraction: 0.5
119+
batch_size: 256
120+
121+
logger:
122+
exp_name: offline_to_online_halfcheetah_v4
123+
offline: false
124+
project: torchrl-sota-implementations
125+
126+
# Trainer configuration
127+
trainer:
128+
collector: ${collector}
129+
optimizer: ${optimizer}
130+
replay_buffer: ${replay_buffer}
131+
target_net_updater: ${target_net_updater}
132+
loss_module: ${loss}
133+
logger: ${logger}
134+
total_frames: ${collector.total_frames}
135+
# Anneal the offline sampling fraction to 0 over the first half of training.
136+
anneal_frames: 500_000
137+
batch_size: 256
138+
frame_skip: 1
139+
clip_grad_norm: false # SAC typically doesn't use gradient clipping
140+
clip_norm: null
141+
progress_bar: true
142+
seed: 42
143+
save_trainer_interval: 25000
144+
log_interval: 25000
145+
save_trainer_file: null
146+
optim_steps_per_batch: 64
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# This source code is licensed under the MIT license found in the
3+
# LICENSE file in the root directory of this source tree.
4+
"""Offline-to-online SAC fine-tuning via the configurable trainer system.
5+
6+
Warm-starts SAC on an offline dataset (D4RL/Minari) and fine-tunes it online,
7+
sampling a mixed offline/online batch whose offline fraction is annealed to zero
8+
over ``trainer.anneal_frames`` collected frames.
9+
10+
Run with::
11+
12+
python train.py
13+
python train.py replay_buffer.offline_dataset=minari:mujoco/halfcheetah/expert-v0
14+
15+
Requires the offline dataset's backend (``pip install d4rl`` or
16+
``pip install minari``) and the matching MuJoCo environment.
17+
"""
18+
19+
import hydra
20+
from torchrl.trainers.algorithms.configs import * # noqa: F401, F403
21+
22+
23+
@hydra.main(config_path="config", config_name="config", version_base="1.1")
24+
def main(cfg):
25+
trainer = hydra.utils.instantiate(cfg.trainer)
26+
trainer.train()
27+
28+
29+
if __name__ == "__main__":
30+
main()

test/test_configs.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1130,7 +1130,6 @@ def test_collector_config(self, factory, collector):
11301130

11311131
# Define cfg_cls and kwargs based on collector type
11321132
if collector == "async":
1133-
11341133
cfg_cls = AsyncCollectorConfig
11351134
kwargs = {"create_env_fn": env_cfg, "frames_per_batch": 10}
11361135
elif collector == "multi_sync":
@@ -1869,6 +1868,59 @@ def test_td3_trainer_config(self):
18691868
assert cfg.policy_update_delay == 2
18701869
assert cfg.clip_grad_norm is True
18711870

1871+
@pytest.mark.skipif(not _has_gymnasium, reason="Gymnasium is not installed")
1872+
def test_offline_to_online_trainer_config(self):
1873+
from torchrl.trainers.algorithms.configs.trainers import (
1874+
OfflineToOnlineTrainerConfig,
1875+
)
1876+
1877+
cfg = OfflineToOnlineTrainerConfig(
1878+
collector=None,
1879+
total_frames=600,
1880+
optim_steps_per_batch=2,
1881+
loss_module=None,
1882+
optimizer=None,
1883+
logger=None,
1884+
save_trainer_file=None,
1885+
replay_buffer=None,
1886+
anneal_frames=300,
1887+
)
1888+
1889+
assert (
1890+
cfg._target_
1891+
== "torchrl.trainers.algorithms.configs.trainers._make_offline_to_online_trainer"
1892+
)
1893+
assert cfg.total_frames == 600
1894+
assert cfg.anneal_frames == 300
1895+
# defaults inherited from the SAC-style config
1896+
assert cfg.frame_skip == 1
1897+
assert cfg.auto_log_optim_steps is True
1898+
assert cfg.actor_network is None
1899+
assert cfg.critic_network is None
1900+
1901+
@pytest.mark.skipif(not _has_hydra, reason="Hydra is not installed")
1902+
def test_offline_to_online_replay_buffer_config_instantiation(self):
1903+
from hydra.utils import instantiate
1904+
1905+
from torchrl.data import OfflineToOnlineReplayBuffer
1906+
from torchrl.trainers.algorithms.configs.data import (
1907+
LazyTensorStorageConfig,
1908+
OfflineToOnlineReplayBufferConfig,
1909+
ReplayBufferConfig,
1910+
)
1911+
1912+
cfg = OfflineToOnlineReplayBufferConfig(
1913+
offline_dataset=ReplayBufferConfig(
1914+
storage=LazyTensorStorageConfig(max_size=100)
1915+
),
1916+
online_capacity=200,
1917+
offline_fraction=0.25,
1918+
batch_size=32,
1919+
)
1920+
rb = instantiate(cfg)
1921+
assert isinstance(rb, OfflineToOnlineReplayBuffer)
1922+
assert rb.offline_fraction == 0.25
1923+
18721924

18731925
@pytest.mark.skipif(not _has_hydra, reason="Hydra is not installed")
18741926
@pytest.mark.skipif(not _has_gymnasium, reason="Gymnasium is not installed")

0 commit comments

Comments
 (0)