omnirl-framework 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. omnirl/__init__.py +1 -0
  2. omnirl/algorithms/__init__.py +1 -0
  3. omnirl/algorithms/a2c/__init__.py +8 -0
  4. omnirl/algorithms/a2c/learner.py +319 -0
  5. omnirl/algorithms/a2c/policy.py +100 -0
  6. omnirl/algorithms/c51_dqn/__init__.py +8 -0
  7. omnirl/algorithms/c51_dqn/learner.py +276 -0
  8. omnirl/algorithms/c51_dqn/policy.py +94 -0
  9. omnirl/algorithms/ddpg/__init__.py +8 -0
  10. omnirl/algorithms/ddpg/learner.py +271 -0
  11. omnirl/algorithms/ddpg/policy.py +91 -0
  12. omnirl/algorithms/ddqn/__init__.py +8 -0
  13. omnirl/algorithms/ddqn/learner.py +231 -0
  14. omnirl/algorithms/ddqn/policy.py +84 -0
  15. omnirl/algorithms/dqn/__init__.py +8 -0
  16. omnirl/algorithms/dqn/learner.py +229 -0
  17. omnirl/algorithms/dqn/policy.py +84 -0
  18. omnirl/algorithms/dueling_dqn/__init__.py +8 -0
  19. omnirl/algorithms/dueling_dqn/learner.py +231 -0
  20. omnirl/algorithms/dueling_dqn/policy.py +84 -0
  21. omnirl/algorithms/impala/__init__.py +8 -0
  22. omnirl/algorithms/impala/learner.py +334 -0
  23. omnirl/algorithms/impala/policy.py +109 -0
  24. omnirl/algorithms/per_dqn/__init__.py +8 -0
  25. omnirl/algorithms/per_dqn/learner.py +239 -0
  26. omnirl/algorithms/per_dqn/policy.py +84 -0
  27. omnirl/algorithms/ppo/__init__.py +8 -0
  28. omnirl/algorithms/ppo/learner.py +377 -0
  29. omnirl/algorithms/ppo/policy.py +109 -0
  30. omnirl/algorithms/qr_dqn/__init__.py +8 -0
  31. omnirl/algorithms/qr_dqn/learner.py +253 -0
  32. omnirl/algorithms/qr_dqn/policy.py +88 -0
  33. omnirl/algorithms/sac/__init__.py +8 -0
  34. omnirl/algorithms/sac/learner.py +409 -0
  35. omnirl/algorithms/sac/policy.py +114 -0
  36. omnirl/algorithms/td3/__init__.py +8 -0
  37. omnirl/algorithms/td3/learner.py +322 -0
  38. omnirl/algorithms/td3/policy.py +91 -0
  39. omnirl/algorithms/tqc/__init__.py +8 -0
  40. omnirl/algorithms/tqc/learner.py +403 -0
  41. omnirl/algorithms/tqc/policy.py +95 -0
  42. omnirl/buffers/__init__.py +7 -0
  43. omnirl/buffers/prioritized_replay_buffer.py +161 -0
  44. omnirl/buffers/replay_buffer.py +116 -0
  45. omnirl/buffers/rollout_buffer.py +146 -0
  46. omnirl/envs/__init__.py +1 -0
  47. omnirl/envs/gymnasium/__init__.py +14 -0
  48. omnirl/envs/gymnasium/adapter.py +38 -0
  49. omnirl/envs/gymnasium/builder.py +16 -0
  50. omnirl/envs/gymnasium/collection.py +54 -0
  51. omnirl/envs/gymnasium/evaluation.py +115 -0
  52. omnirl/envs/gymnasium/spec.py +47 -0
  53. omnirl/envs/vizdoom/__init__.py +14 -0
  54. omnirl/envs/vizdoom/adapter.py +74 -0
  55. omnirl/envs/vizdoom/builder.py +59 -0
  56. omnirl/envs/vizdoom/collection.py +67 -0
  57. omnirl/envs/vizdoom/evaluation.py +121 -0
  58. omnirl/envs/vizdoom/spec.py +21 -0
  59. omnirl/networks/__init__.py +27 -0
  60. omnirl/networks/actors.py +116 -0
  61. omnirl/networks/base/__init__.py +7 -0
  62. omnirl/networks/base/cnn.py +54 -0
  63. omnirl/networks/base/mlp.py +26 -0
  64. omnirl/networks/base/resnet.py +123 -0
  65. omnirl/networks/critics.py +298 -0
  66. omnirl/runtime/__init__.py +7 -0
  67. omnirl/runtime/evaluator.py +57 -0
  68. omnirl/runtime/file_writer.py +83 -0
  69. omnirl/runtime/logger.py +16 -0
  70. omnirl/runtime/runner.py +428 -0
  71. omnirl/runtime/sampler.py +66 -0
  72. omnirl/runtime/weight_store.py +29 -0
  73. omnirl/utils/__init__.py +5 -0
  74. omnirl/utils/seed.py +18 -0
  75. omnirl_framework-0.1.0.dist-info/METADATA +371 -0
  76. omnirl_framework-0.1.0.dist-info/RECORD +79 -0
  77. omnirl_framework-0.1.0.dist-info/WHEEL +5 -0
  78. omnirl_framework-0.1.0.dist-info/licenses/LICENSE +201 -0
  79. omnirl_framework-0.1.0.dist-info/top_level.txt +1 -0
omnirl/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """OmniRL: a distributed reinforcement learning framework."""
@@ -0,0 +1 @@
1
+ """Reinforcement learning algorithm implementations."""
@@ -0,0 +1,8 @@
1
+ """A2C algorithm components."""
2
+
3
+ from omnirl.buffers import RolloutBuffer as Memory
4
+
5
+ from .learner import Learner
6
+ from .policy import Policy
7
+
8
+ __all__ = ["Learner", "Memory", "Policy"]
@@ -0,0 +1,319 @@
1
+ import os
2
+
3
+ import ray
4
+ import torch
5
+ import torch.distributed as torch_dist
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ import torch.optim as optim
9
+ from torch.distributions import Categorical, Normal
10
+ from torch.nn.parallel import DistributedDataParallel as DDP
11
+
12
+ from omnirl.networks.actors import StochasticActorNetwork
13
+ from omnirl.networks.critics import ValueCriticNetwork
14
+ from omnirl.utils.seed import set_global_seed
15
+
16
+
17
+ @ray.remote
18
+ class Learner:
19
+ """
20
+ A2C learner agent.
21
+
22
+ Args:
23
+ state_dim: Dimension of the environment state space.
24
+ action_dim: Dimension of the environment action space.
25
+ is_continuous: Whether the action space is continuous.
26
+ network_kwargs: Keyword arguments used to build the networks.
27
+ ckpt_dir: Directory path to save model checkpoints.
28
+ gamma: Discount factor.
29
+ lambda_: GAE lambda used for generalized advantage estimation.
30
+ ent_coef: Entropy regularization coefficient used to encourage exploration.
31
+ vf_coef: Coefficient for the critic loss in the total optimization objective.
32
+ actor_lr: Learning rate for the actor optimizer.
33
+ critic_lr: Learning rate for the critic optimizer.
34
+ max_grad_norm: Maximum gradient norm used for gradient clipping.
35
+ device: Torch device to run the networks on.
36
+ seed: Random seed used to ensure consistent results across runs.
37
+ rank: Distributed learner rank.
38
+ world_size: Total number of distributed learners.
39
+ master_addr: Master address used to initialize distributed training.
40
+ master_port: Master port used to initialize distributed training.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ state_dim: int | tuple[int, int, int],
46
+ action_dim: int,
47
+ is_continuous: bool = False,
48
+ network_kwargs: dict | None = None,
49
+ ckpt_dir: str = "checkpoints",
50
+ gamma: float = 0.99,
51
+ lambda_: float = 0.95,
52
+ ent_coef: float = 0.02,
53
+ vf_coef: float = 0.5,
54
+ actor_lr: float = 0.0003,
55
+ critic_lr: float = 0.0003,
56
+ max_grad_norm: float = 0.5,
57
+ device: str | None = None,
58
+ seed: int | None = None,
59
+ rank: int = 0,
60
+ world_size: int = 1,
61
+ master_addr: str = "127.0.0.1",
62
+ master_port: int = 29500,
63
+ ):
64
+ # Limit PyTorch CPU threads
65
+ torch.set_num_threads(1)
66
+ torch.set_num_interop_threads(1)
67
+
68
+ # Initialize random seed
69
+ if seed is not None:
70
+ set_global_seed(seed)
71
+
72
+ # Create checkpoint directory
73
+ self.ckpt_dir = ckpt_dir
74
+ os.makedirs(self.ckpt_dir, exist_ok=True)
75
+
76
+ # Initialize device
77
+ if device is None:
78
+ self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
79
+ else:
80
+ self.device = torch.device("cuda:0" if device == "gpu" else "cpu")
81
+
82
+ # Initialize distributed training
83
+ self.rank = rank
84
+ is_distributed = world_size > 1
85
+
86
+ if is_distributed and not torch_dist.is_initialized():
87
+ backend = "nccl" if self.device.type == "cuda" else "gloo"
88
+ torch_dist.init_process_group(
89
+ backend=backend,
90
+ init_method=f"tcp://{master_addr}:{master_port}",
91
+ rank=self.rank,
92
+ world_size=world_size,
93
+ )
94
+
95
+ # Initialize parameters
96
+ self.gamma = gamma
97
+ self.lambda_ = lambda_
98
+ self.ent_coef = ent_coef
99
+ self.vf_coef = vf_coef
100
+ self.is_continuous = is_continuous
101
+ self.max_grad_norm = max_grad_norm
102
+
103
+ # Initialize actor and critic networks
104
+ self.version = 0
105
+ network_kwargs = dict(network_kwargs or {})
106
+ actor_kwargs = dict(network_kwargs.get("actor", network_kwargs))
107
+ critic_kwargs = dict(network_kwargs.get("critic", network_kwargs))
108
+ actor_layer_type = actor_kwargs.pop("layer_type")
109
+ critic_layer_type = critic_kwargs.pop("layer_type")
110
+ log_std_min = actor_kwargs.pop("log_std_min", -20.0)
111
+ log_std_max = actor_kwargs.pop("log_std_max", 2.0)
112
+
113
+ actor = StochasticActorNetwork(
114
+ state_dim=state_dim,
115
+ action_dim=action_dim,
116
+ is_continuous=is_continuous,
117
+ layer_type=actor_layer_type,
118
+ log_std_min=log_std_min,
119
+ log_std_max=log_std_max,
120
+ network_kwargs=actor_kwargs,
121
+ )
122
+ critic = ValueCriticNetwork(
123
+ state_dim=state_dim,
124
+ layer_type=critic_layer_type,
125
+ network_kwargs=critic_kwargs,
126
+ )
127
+
128
+ self.actor = actor.to(self.device)
129
+ self.critic = critic.to(self.device)
130
+
131
+ self.actor.train()
132
+ self.critic.train()
133
+
134
+ if is_distributed:
135
+ ddp_kwargs = {"device_ids": [0]} if self.device.type == "cuda" else {}
136
+ self.train_actor = DDP(self.actor, **ddp_kwargs)
137
+ self.train_critic = DDP(self.critic, **ddp_kwargs)
138
+ else:
139
+ self.train_actor = self.actor
140
+ self.train_critic = self.critic
141
+
142
+ # Initialize optimizer
143
+ self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=actor_lr)
144
+ self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=critic_lr)
145
+
146
+ def _state_dict(self, model):
147
+ """Return a detached CPU copy of model state_dict."""
148
+ return {k: v.detach().cpu() for k, v in model.state_dict().items()}
149
+
150
+ def get_weights(self):
151
+ """Export actor network weights."""
152
+ return self._state_dict(self.actor)
153
+
154
+ def get_version(self):
155
+ """Export the current learner model version."""
156
+ return self.version
157
+
158
+ def _evaluate_action(self, states_tensor, actions_tensor):
159
+ """Evaluate action log probabilities and entropies under the current policy."""
160
+ if self.is_continuous:
161
+ action_mean, action_log_std = self.train_actor(states_tensor)
162
+
163
+ action_std = action_log_std.exp()
164
+ dist = Normal(action_mean, action_std)
165
+
166
+ dist_entropy = dist.entropy().sum(dim=-1)
167
+ log_probs = dist.log_prob(actions_tensor).sum(dim=-1)
168
+ else:
169
+ action_logits = self.train_actor(states_tensor)
170
+
171
+ dist = Categorical(logits=action_logits)
172
+
173
+ dist_entropy = dist.entropy()
174
+ log_probs = dist.log_prob(actions_tensor)
175
+
176
+ state_values = self.train_critic(states_tensor)
177
+
178
+ return {
179
+ "log_probs": log_probs.view(-1),
180
+ "dist_entropy": dist_entropy.view(-1),
181
+ "state_values": state_values.view(-1),
182
+ }
183
+
184
+ def learn(self, batch):
185
+ """Sample a batch from rollout buffer and update actor and critic networks."""
186
+ states_tensor = torch.tensor(batch["states"], device=self.device)
187
+ actions_tensor = torch.tensor(batch["actions"], device=self.device)
188
+ rewards_tensor = torch.tensor(batch["rewards"], device=self.device)
189
+ next_states_tensor = torch.tensor(batch["next_states"], device=self.device)
190
+ terminals_tensor = torch.tensor(batch["terminals"], device=self.device)
191
+ dones_tensor = torch.tensor(batch["dones"], device=self.device)
192
+
193
+ states_tensor = states_tensor.float()
194
+ next_states_tensor = next_states_tensor.float()
195
+ terminals_tensor = terminals_tensor.float()
196
+ dones_tensor = dones_tensor.float()
197
+
198
+ B, T = rewards_tensor.shape
199
+
200
+ # Compute GAE
201
+ with torch.no_grad():
202
+ flat_states = states_tensor.view(B * T, *states_tensor.shape[2:])
203
+ flat_next_states = next_states_tensor.view(B * T, *states_tensor.shape[2:])
204
+
205
+ state_values = self.critic(flat_states).squeeze(-1)
206
+ next_state_values = self.critic(flat_next_states).squeeze(-1)
207
+
208
+ state_values = state_values.view(B, T)
209
+ next_state_values = next_state_values.view(B, T)
210
+
211
+ next_state_values = next_state_values * (1.0 - terminals_tensor)
212
+ td_target = rewards_tensor + self.gamma * next_state_values
213
+ deltas = td_target - state_values
214
+
215
+ advantages = torch.zeros_like(rewards_tensor, device=self.device)
216
+ gae = torch.zeros(B, device=self.device)
217
+
218
+ for t in reversed(range(T)):
219
+ gamma_lambda = self.gamma * self.lambda_ * (1.0 - dones_tensor[:, t])
220
+ gae = deltas[:, t] + gamma_lambda * gae
221
+ advantages[:, t] = gae
222
+
223
+ returns = advantages + state_values
224
+ advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
225
+
226
+ flat_states = states_tensor.view(B * T, *states_tensor.shape[2:])
227
+ if self.is_continuous:
228
+ flat_actions = actions_tensor.view(B * T, -1)
229
+ else:
230
+ flat_actions = actions_tensor.reshape(B * T)
231
+ flat_advantages = advantages.reshape(B * T)
232
+ flat_returns = returns.reshape(B * T)
233
+
234
+ # Compute action log-probabilities for the sampled actions.
235
+ results = self._evaluate_action(flat_states, flat_actions)
236
+ log_probs = results["log_probs"]
237
+ dist_entropy = results["dist_entropy"]
238
+ state_values = results["state_values"]
239
+
240
+ actor_loss = -(log_probs * flat_advantages).mean()
241
+ critic_loss = F.mse_loss(state_values, flat_returns)
242
+ entropy_loss = dist_entropy.mean()
243
+
244
+ loss = actor_loss + self.vf_coef * critic_loss - self.ent_coef * entropy_loss
245
+
246
+ value_error_var = torch.var(flat_returns - state_values.detach())
247
+ return_var = torch.var(flat_returns)
248
+ explained_var = 1 - value_error_var / (return_var + 1e-8)
249
+
250
+ self.actor_optimizer.zero_grad()
251
+ self.critic_optimizer.zero_grad()
252
+ loss.backward()
253
+ nn.utils.clip_grad_norm_(self.actor.parameters(), self.max_grad_norm)
254
+ nn.utils.clip_grad_norm_(self.critic.parameters(), self.max_grad_norm)
255
+ self.actor_optimizer.step()
256
+ self.critic_optimizer.step()
257
+
258
+ # Update model version
259
+ self.version += 1
260
+
261
+ return {
262
+ "log": {
263
+ "actor_loss": actor_loss.item(),
264
+ "critic_loss": critic_loss.item(),
265
+ "entropy": entropy_loss.item(),
266
+ "explained_var": explained_var.item(),
267
+ },
268
+ "memory": {"version": self.version},
269
+ "version": self.get_version(),
270
+ "trained_samples": int(B * T),
271
+ }
272
+
273
+ def save_checkpoint(self):
274
+ """Save actor, critic and optimizer into checkpoint file."""
275
+ if self.rank != 0:
276
+ return
277
+
278
+ path = os.path.join(self.ckpt_dir, "checkpoint.ckpt")
279
+
280
+ ckpt = {
281
+ "actor": self._state_dict(self.actor),
282
+ "critic": self._state_dict(self.critic),
283
+ "actor_optimizer": self.actor_optimizer.state_dict(),
284
+ "critic_optimizer": self.critic_optimizer.state_dict(),
285
+ "version": self.version,
286
+ }
287
+ torch.save(ckpt, path)
288
+
289
+ eval_path = os.path.join(self.ckpt_dir, f"agent_{self.version}.pth")
290
+ torch.save(self._state_dict(self.actor), eval_path)
291
+
292
+ def load_checkpoint(self):
293
+ """Load actor, critic and optimizer from checkpoint file."""
294
+ path = os.path.join(self.ckpt_dir, "checkpoint.ckpt")
295
+ ckpt = torch.load(path, map_location=self.device)
296
+
297
+ # Load actor and critic networks
298
+ self.actor.load_state_dict(ckpt["actor"])
299
+ self.critic.load_state_dict(ckpt["critic"])
300
+
301
+ # Load optimizer state
302
+ self.actor_optimizer.load_state_dict(ckpt["actor_optimizer"])
303
+ self.critic_optimizer.load_state_dict(ckpt["critic_optimizer"])
304
+
305
+ # Load model version
306
+ self.version = ckpt["version"]
307
+
308
+ return {"version": self.get_version(), "weights": self.get_weights()}
309
+
310
+ def load_model(self, model_path):
311
+ """Load actor from pre-trained model weights."""
312
+ path = os.path.join(self.ckpt_dir, model_path)
313
+ state_dict = torch.load(path, map_location=self.device)
314
+
315
+ # Load actor network
316
+ self.actor.load_state_dict(state_dict)
317
+
318
+ # Load model version
319
+ self.version = int(model_path.removeprefix("agent_").removesuffix(".pth"))
@@ -0,0 +1,100 @@
1
+ import numpy as np
2
+ import torch
3
+ from torch.distributions import Categorical, Normal
4
+
5
+ from omnirl.networks.actors import StochasticActorNetwork
6
+
7
+
8
+ class Policy:
9
+ """
10
+ A2C policy used for local inference.
11
+
12
+ Args:
13
+ state_dim: Dimension of the environment state space.
14
+ action_dim: Dimension of the environment action space.
15
+ is_continuous: Whether the action space is continuous.
16
+ action_low: Lower bounds of the continuous action space.
17
+ action_high: Upper bounds of the continuous action space.
18
+ network_kwargs: Keyword arguments used to build the actor network.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ state_dim: int | tuple[int, int, int],
24
+ action_dim: int,
25
+ is_continuous: bool = False,
26
+ action_low: np.ndarray | None = None,
27
+ action_high: np.ndarray | None = None,
28
+ network_kwargs: dict | None = None,
29
+ ):
30
+ self.device = torch.device("cpu")
31
+ self.is_continuous = is_continuous
32
+ self.version = -1
33
+
34
+ if self.is_continuous:
35
+ action_low = np.asarray(action_low, dtype=np.float32)
36
+ action_high = np.asarray(action_high, dtype=np.float32)
37
+
38
+ action_low = torch.tensor(action_low, device=self.device)
39
+ action_high = torch.tensor(action_high, device=self.device)
40
+ self.action_scale = (action_high - action_low) / 2.0
41
+ self.action_bias = (action_high + action_low) / 2.0
42
+
43
+ network_kwargs = dict(network_kwargs or {})
44
+ actor_kwargs = dict(network_kwargs.get("actor", network_kwargs))
45
+ layer_type = actor_kwargs.pop("layer_type")
46
+ log_std_min = actor_kwargs.pop("log_std_min", -20.0)
47
+ log_std_max = actor_kwargs.pop("log_std_max", 2.0)
48
+ actor = StochasticActorNetwork(
49
+ state_dim=state_dim,
50
+ action_dim=action_dim,
51
+ is_continuous=is_continuous,
52
+ layer_type=layer_type,
53
+ log_std_min=log_std_min,
54
+ log_std_max=log_std_max,
55
+ network_kwargs=actor_kwargs,
56
+ )
57
+
58
+ self.actor = actor.to(self.device)
59
+ self.actor.eval()
60
+
61
+ def set_weights(self, weights, version=None):
62
+ """Load external network weights into the actor network."""
63
+ self.actor.load_state_dict(weights)
64
+ if version is not None:
65
+ self.version = version
66
+
67
+ def choose_action(self, observation):
68
+ """Select a deterministic action for evaluation."""
69
+ observation = np.asarray(observation, dtype=np.float32)
70
+ state = torch.as_tensor(observation, device=self.device).unsqueeze(0)
71
+
72
+ with torch.no_grad():
73
+ if self.is_continuous:
74
+ action_mean, _ = self.actor(state)
75
+ action = torch.tanh(action_mean)
76
+ action = action * self.action_scale + self.action_bias
77
+ return action.squeeze(0).cpu().numpy()
78
+ else:
79
+ action_logits = self.actor(state)
80
+ action = torch.argmax(action_logits, dim=-1)
81
+ return int(action.item())
82
+
83
+ def sample_action(self, observation):
84
+ """Select a probabilistic action during training."""
85
+ observation = np.asarray(observation, dtype=np.float32)
86
+ state = torch.as_tensor(observation, device=self.device).unsqueeze(0)
87
+
88
+ with torch.no_grad():
89
+ if self.is_continuous:
90
+ action_mean, action_log_std = self.actor(state)
91
+ action = Normal(action_mean, action_log_std.exp()).sample()
92
+ env_action = torch.tanh(action) * self.action_scale + self.action_bias
93
+ action = action.squeeze(0).cpu().numpy()
94
+ env_action = env_action.squeeze(0).cpu().numpy()
95
+ else:
96
+ action_logits = self.actor(state)
97
+ action = int(Categorical(logits=action_logits).sample().item())
98
+ env_action = action
99
+
100
+ return {"env_action": env_action, "data": {"action": action}}
@@ -0,0 +1,8 @@
1
+ """C51 DQN algorithm components."""
2
+
3
+ from omnirl.buffers import ReplayBuffer as Memory
4
+
5
+ from .learner import Learner
6
+ from .policy import Policy
7
+
8
+ __all__ = ["Learner", "Memory", "Policy"]