tensor-optix 2.0.0__tar.gz → 2.0.2__tar.gz
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.
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/PKG-INFO +1 -1
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/pyproject.toml +1 -1
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/pytorch/torch_agent.py +16 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/flax_ppo.py +7 -8
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/tf_ppo.py +11 -7
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/tf_ppo_continuous.py +7 -6
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/tf_sac.py +7 -4
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/tf_td3.py +6 -3
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_ppo.py +30 -9
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_ppo_continuous.py +7 -8
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_sac.py +44 -4
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_td3.py +6 -3
- tensor_optix-2.0.2/tensor_optix/core/async_callback.py +131 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/base_agent.py +51 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/checkpoint_registry.py +12 -2
- tensor_optix-2.0.2/tensor_optix/core/resume_state.py +72 -0
- tensor_optix-2.0.2/tensor_optix/core/trajectory_buffer.py +137 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/types.py +6 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/factory.py +69 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/__init__.py +13 -13
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/callback.py +210 -210
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/provenance.py +75 -75
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/records.py +165 -165
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/schema.py +183 -183
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/store.py +579 -579
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/__init__.py +39 -39
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/abstention.py +118 -118
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/features.py +76 -76
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/pooling.py +100 -100
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/selector.py +234 -234
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/smalln.py +76 -76
- tensor_optix-2.0.2/tensor_optix/neuroevo/controller/topology_callback.py +66 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/controller/topology_controller.py +34 -5
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/pipeline/vector_pipeline.py +20 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/__init__.py +21 -21
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/baseline.py +182 -182
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/experiment_stage.py +131 -131
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/live_runtime.py +130 -130
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/observer.py +98 -98
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/promote.py +80 -80
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/__init__.py +15 -15
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/_mp_common.py +32 -32
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/budget.py +35 -35
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/dedup.py +17 -17
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/isolation.py +20 -20
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/pool.py +386 -386
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/retry.py +39 -39
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/worker.py +111 -111
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/selection/__init__.py +28 -28
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/selection/backfill.py +93 -93
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/selection/selector.py +174 -174
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/selection/uncertainty.py +43 -43
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/__init__.py +24 -24
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/candidate.py +51 -51
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/comparator.py +81 -81
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/grammar.py +489 -489
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/promotion.py +211 -197
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/runner.py +218 -218
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/worker.py +141 -141
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/PKG-INFO +1 -1
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/SOURCES.txt +3 -0
- tensor_optix-2.0.0/tensor_optix/core/trajectory_buffer.py +0 -82
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/README.md +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/setup.cfg +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/jax/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/jax/flax_agent.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/jax/flax_evaluator.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/pytorch/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/pytorch/torch_evaluator.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/tensorflow/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/tensorflow/tf_agent.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/tensorflow/tf_evaluator.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/tf_dqn.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_dqn.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_rainbow_dqn.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_recurrent_ppo.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/callbacks/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/callbacks/rich_dashboard.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/callbacks/tensorboard_callback.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/callbacks/wandb_callback.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/cli.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/config.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/backoff_scheduler.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/base_evaluator.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/base_optimizer.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/base_pipeline.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/checkpoint_confirmation.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/device.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/diagnostic_controller.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/ensemble_agent.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/her_buffer.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/loop_controller.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/meta_controller.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/noisy_linear.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/normalizers.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/policy_manager.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/regime_detector.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/replay_buffer.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/reward_shape.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/distributed/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/distributed/async_learner.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/distributed/vtrace.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/exploration/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/exploration/rnd.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ml/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ml/dataset_pipeline.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ml/loss_registry.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ml/ml_agent.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/agent/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/agent/graph_agent.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/brain_network.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/controller/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/graph/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/graph/neuron.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/graph/neuron_graph.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/graph/topology_ops.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/hebbian.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/neuromodulator.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/optimizer/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/optimizer/topology_aware_adam.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizer.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/adaptive_optimizer.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/backoff_optimizer.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/momentum_optimizer.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/pbt_optimizer.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/spsa_optimizer.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/orchestrator.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/pipeline/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/pipeline/batch_pipeline.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/pipeline/live_pipeline.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/simple.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/utils/__init__.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/utils/window_size.py +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/dependency_links.txt +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/entry_points.txt +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/requires.txt +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/top_level.txt +0 -0
- {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tests/test_ml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tensor-optix
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Autonomous training loop for any sequential learning model — PPO, DQN, SAC, TD3, Rainbow DQN, Recurrent PPO for TensorFlow, PyTorch, and JAX/Flax; distributed async actor-learner (IMPALA + V-trace)
|
|
5
5
|
Author: sup3rus3r
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tensor-optix"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.2"
|
|
8
8
|
description = "Autonomous training loop for any sequential learning model — PPO, DQN, SAC, TD3, Rainbow DQN, Recurrent PPO for TensorFlow, PyTorch, and JAX/Flax; distributed async actor-learner (IMPALA + V-trace)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -2,6 +2,7 @@ import os
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
|
|
4
4
|
from tensor_optix.core.base_agent import BaseAgent
|
|
5
|
+
from tensor_optix.core.resume_state import save_torch_resume_state, load_torch_resume_state
|
|
5
6
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
6
7
|
|
|
7
8
|
|
|
@@ -147,6 +148,21 @@ class TorchAgent(BaseAgent):
|
|
|
147
148
|
state = torch.load(os.path.join(path, "model.pt"), map_location=self._device)
|
|
148
149
|
self.model.load_state_dict(state)
|
|
149
150
|
|
|
151
|
+
@property
|
|
152
|
+
def resumable_components(self) -> frozenset:
|
|
153
|
+
return frozenset({"model", "optimizer", "rng"})
|
|
154
|
+
|
|
155
|
+
def save_full_state(self, path: str) -> None:
|
|
156
|
+
self.save_weights(path)
|
|
157
|
+
save_torch_resume_state(path, optimizer=self.optimizer)
|
|
158
|
+
|
|
159
|
+
def load_full_state(self, path: str) -> None:
|
|
160
|
+
self.load_weights(path)
|
|
161
|
+
load_torch_resume_state(path, optimizer=self.optimizer)
|
|
162
|
+
|
|
163
|
+
def get_weights(self) -> dict:
|
|
164
|
+
return {k: v.detach().clone() for k, v in self.model.state_dict().items()}
|
|
165
|
+
|
|
150
166
|
def teardown(self) -> None:
|
|
151
167
|
"""Move model to CPU and free CUDA memory."""
|
|
152
168
|
import torch
|
|
@@ -38,7 +38,7 @@ import numpy as np
|
|
|
38
38
|
|
|
39
39
|
from tensor_optix.core.base_agent import BaseAgent
|
|
40
40
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
41
|
-
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches
|
|
41
|
+
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches, resolve_gae_bootstrap
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
# ---------------------------------------------------------------------------
|
|
@@ -195,15 +195,14 @@ class FlaxPPOAgent(BaseAgent):
|
|
|
195
195
|
dones = episode_data.dones
|
|
196
196
|
|
|
197
197
|
# Bootstrap value when trajectory ends mid-episode.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
last_value = float(self._model.critic(obs_final)[0])
|
|
198
|
+
def _critic_value(obs):
|
|
199
|
+
obs_final = jnp.array(np.atleast_2d(obs), dtype=jnp.float32)
|
|
200
|
+
return float(self._model.critic(obs_final)[0])
|
|
201
|
+
|
|
202
|
+
last_value, n_envs = resolve_gae_bootstrap(episode_data, _critic_value)
|
|
204
203
|
|
|
205
204
|
advantages, returns = compute_gae(
|
|
206
|
-
rewards, val_arr, dones, gamma, gae_lambda, last_value
|
|
205
|
+
rewards, val_arr, dones, gamma, gae_lambda, last_value, n_envs=n_envs
|
|
207
206
|
)
|
|
208
207
|
if T > 1:
|
|
209
208
|
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
|
|
@@ -3,7 +3,7 @@ import numpy as np
|
|
|
3
3
|
import tensorflow as tf
|
|
4
4
|
from tensor_optix.core.base_agent import BaseAgent
|
|
5
5
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
6
|
-
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches
|
|
6
|
+
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches, resolve_gae_bootstrap
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class TFPPOAgent(BaseAgent):
|
|
@@ -149,14 +149,18 @@ class TFPPOAgent(BaseAgent):
|
|
|
149
149
|
self._reward_normalizer.reset()
|
|
150
150
|
rewards = list(self._reward_normalizer.normalize(np.array(rewards, dtype=np.float32)))
|
|
151
151
|
|
|
152
|
-
# Bootstrap: if
|
|
152
|
+
# Bootstrap: if a window ended mid-episode, V(s_T) from the critic
|
|
153
153
|
# corrects the TD target at the last step. Zero when terminal.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
# With VectorBatchPipeline (final_obs is a list), this resolves one
|
|
155
|
+
# bootstrap value per env, and n_envs tells compute_gae how the
|
|
156
|
+
# rewards/values array is interleaved across envs.
|
|
157
|
+
def _critic_value(obs):
|
|
158
|
+
obs_t = tf.cast(np.atleast_2d(obs), tf.float32)
|
|
159
|
+
return float(tf.squeeze(self._critic(obs_t, training=False)).numpy())
|
|
158
160
|
|
|
159
|
-
|
|
161
|
+
last_value, n_envs = resolve_gae_bootstrap(episode_data, _critic_value)
|
|
162
|
+
|
|
163
|
+
advantages, returns = compute_gae(rewards, values_arr, dones, gamma, gae_lambda, last_value, n_envs=n_envs)
|
|
160
164
|
|
|
161
165
|
# Normalize advantages across the rollout
|
|
162
166
|
if T > 1:
|
|
@@ -4,7 +4,7 @@ import tensorflow as tf
|
|
|
4
4
|
|
|
5
5
|
from tensor_optix.core.base_agent import BaseAgent
|
|
6
6
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
7
|
-
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches
|
|
7
|
+
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches, resolve_gae_bootstrap
|
|
8
8
|
|
|
9
9
|
LOG_STD_MIN = -5
|
|
10
10
|
LOG_STD_MAX = 2
|
|
@@ -193,12 +193,13 @@ class TFGaussianPPOAgent(BaseAgent):
|
|
|
193
193
|
self._reward_normalizer.reset()
|
|
194
194
|
rewards = list(self._reward_normalizer.normalize(np.array(rewards, dtype=np.float32)))
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
last_value = float(tf.squeeze(self._critic(final_obs_t, training=False)).numpy())
|
|
196
|
+
def _critic_value(obs):
|
|
197
|
+
obs_t = tf.cast(np.atleast_2d(obs), tf.float32)
|
|
198
|
+
return float(tf.squeeze(self._critic(obs_t, training=False)).numpy())
|
|
200
199
|
|
|
201
|
-
|
|
200
|
+
last_value, n_envs = resolve_gae_bootstrap(episode_data, _critic_value)
|
|
201
|
+
|
|
202
|
+
advantages, returns = compute_gae(rewards, val_arr, dones, gamma, gae_lambda, last_value, n_envs=n_envs)
|
|
202
203
|
if T > 1:
|
|
203
204
|
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
|
|
204
205
|
|
|
@@ -151,7 +151,7 @@ class TFSACAgent(BaseAgent):
|
|
|
151
151
|
gamma = float(hp.get("gamma", 0.99))
|
|
152
152
|
tau = float(hp.get("tau", 0.005))
|
|
153
153
|
batch_size = int(hp.get("batch_size", 256))
|
|
154
|
-
updates_per_step =
|
|
154
|
+
updates_per_step = float(hp.get("updates_per_step", 1))
|
|
155
155
|
|
|
156
156
|
obs_arr = np.array(episode_data.observations, dtype=np.float32)
|
|
157
157
|
act_arr = np.array(episode_data.actions, dtype=np.float32)
|
|
@@ -177,7 +177,10 @@ class TFSACAgent(BaseAgent):
|
|
|
177
177
|
"buffer_size": len(self._buffer),
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
# Truncate the product, not updates_per_step itself -- truncating
|
|
181
|
+
# a fractional updates_per_step (e.g. 0.5) to int first would zero
|
|
182
|
+
# it out regardless of T.
|
|
183
|
+
n_updates = int(T * updates_per_step)
|
|
181
184
|
total_actor_loss = 0.0
|
|
182
185
|
total_critic_loss = 0.0
|
|
183
186
|
|
|
@@ -187,8 +190,8 @@ class TFSACAgent(BaseAgent):
|
|
|
187
190
|
total_critic_loss += cl
|
|
188
191
|
|
|
189
192
|
return {
|
|
190
|
-
"actor_loss": total_actor_loss / n_updates,
|
|
191
|
-
"critic_loss": total_critic_loss / n_updates,
|
|
193
|
+
"actor_loss": total_actor_loss / max(n_updates, 1),
|
|
194
|
+
"critic_loss": total_critic_loss / max(n_updates, 1),
|
|
192
195
|
"alpha": float(tf.exp(self._log_alpha).numpy()),
|
|
193
196
|
"buffer_size": len(self._buffer),
|
|
194
197
|
}
|
|
@@ -131,7 +131,7 @@ class TFTDDAgent(BaseAgent):
|
|
|
131
131
|
gamma = float(hp.get("gamma", 0.99))
|
|
132
132
|
tau = float(hp.get("tau", 0.005))
|
|
133
133
|
batch_size = int(hp.get("batch_size", 256))
|
|
134
|
-
updates_per_step =
|
|
134
|
+
updates_per_step = float(hp.get("updates_per_step", 1))
|
|
135
135
|
policy_delay = int(hp.get("policy_delay", 2))
|
|
136
136
|
|
|
137
137
|
obs_arr = np.array(episode_data.observations, dtype=np.float32)
|
|
@@ -155,7 +155,10 @@ class TFTDDAgent(BaseAgent):
|
|
|
155
155
|
"policy_update": 0,
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
# Truncate the product, not updates_per_step itself -- truncating
|
|
159
|
+
# a fractional updates_per_step (e.g. 0.5) to int first would zero
|
|
160
|
+
# it out regardless of T.
|
|
161
|
+
n_updates = int(T * updates_per_step)
|
|
159
162
|
total_al = 0.0
|
|
160
163
|
total_cl = 0.0
|
|
161
164
|
n_actor_updates = 0
|
|
@@ -171,7 +174,7 @@ class TFTDDAgent(BaseAgent):
|
|
|
171
174
|
|
|
172
175
|
return {
|
|
173
176
|
"actor_loss": total_al / max(n_actor_updates, 1),
|
|
174
|
-
"critic_loss": total_cl / n_updates,
|
|
177
|
+
"critic_loss": total_cl / max(n_updates, 1),
|
|
175
178
|
"buffer_size": len(self._buffer),
|
|
176
179
|
"policy_update": int(n_actor_updates > 0),
|
|
177
180
|
}
|
|
@@ -2,8 +2,9 @@ import os
|
|
|
2
2
|
import numpy as np
|
|
3
3
|
|
|
4
4
|
from tensor_optix.core.base_agent import BaseAgent
|
|
5
|
+
from tensor_optix.core.resume_state import save_torch_resume_state, load_torch_resume_state
|
|
5
6
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
6
|
-
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches
|
|
7
|
+
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches, resolve_gae_bootstrap
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class TorchPPOAgent(BaseAgent):
|
|
@@ -126,17 +127,19 @@ class TorchPPOAgent(BaseAgent):
|
|
|
126
127
|
self._reward_normalizer.reset()
|
|
127
128
|
rewards = list(self._reward_normalizer.normalize(np.array(rewards, dtype=np.float32)))
|
|
128
129
|
|
|
129
|
-
# Bootstrap: if
|
|
130
|
+
# Bootstrap: if a window ended mid-episode, V(s_T) from the critic
|
|
130
131
|
# corrects the TD target at the last step. Zero when terminal.
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
# With VectorBatchPipeline (final_obs is a list), this resolves one
|
|
133
|
+
# bootstrap value per env, and n_envs tells compute_gae how the
|
|
134
|
+
# rewards/values array is interleaved across envs.
|
|
135
|
+
def _critic_value(obs):
|
|
133
136
|
with torch.no_grad():
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
).to(self._device)
|
|
137
|
-
last_value = float(self._critic(final_obs_t).squeeze().item())
|
|
137
|
+
obs_t = torch.as_tensor(np.atleast_2d(obs), dtype=torch.float32).to(self._device)
|
|
138
|
+
return float(self._critic(obs_t).squeeze().item())
|
|
138
139
|
|
|
139
|
-
|
|
140
|
+
last_value, n_envs = resolve_gae_bootstrap(episode_data, _critic_value)
|
|
141
|
+
|
|
142
|
+
advantages, returns = compute_gae(rewards, val_arr, dones, gamma, gae_lambda, last_value, n_envs=n_envs)
|
|
140
143
|
if T > 1:
|
|
141
144
|
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
|
|
142
145
|
|
|
@@ -248,6 +251,24 @@ class TorchPPOAgent(BaseAgent):
|
|
|
248
251
|
torch.load(os.path.join(path, "critic.pt"), map_location=self._device)
|
|
249
252
|
)
|
|
250
253
|
|
|
254
|
+
@property
|
|
255
|
+
def resumable_components(self) -> frozenset:
|
|
256
|
+
return frozenset({"model", "optimizer", "rng"})
|
|
257
|
+
|
|
258
|
+
def save_full_state(self, path: str) -> None:
|
|
259
|
+
self.save_weights(path)
|
|
260
|
+
save_torch_resume_state(path, optimizer=self._optimizer)
|
|
261
|
+
|
|
262
|
+
def load_full_state(self, path: str) -> None:
|
|
263
|
+
self.load_weights(path)
|
|
264
|
+
load_torch_resume_state(path, optimizer=self._optimizer)
|
|
265
|
+
|
|
266
|
+
def get_weights(self) -> dict:
|
|
267
|
+
return {
|
|
268
|
+
"actor": {k: v.detach().clone() for k, v in self._actor.state_dict().items()},
|
|
269
|
+
"critic": {k: v.detach().clone() for k, v in self._critic.state_dict().items()},
|
|
270
|
+
}
|
|
271
|
+
|
|
251
272
|
def average_weights(self, paths: list) -> None:
|
|
252
273
|
import torch
|
|
253
274
|
modules = ("actor", "critic")
|
|
@@ -3,7 +3,7 @@ import numpy as np
|
|
|
3
3
|
|
|
4
4
|
from tensor_optix.core.base_agent import BaseAgent
|
|
5
5
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
6
|
-
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches
|
|
6
|
+
from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches, resolve_gae_bootstrap
|
|
7
7
|
|
|
8
8
|
LOG_STD_MIN = -5
|
|
9
9
|
LOG_STD_MAX = 2
|
|
@@ -206,15 +206,14 @@ class TorchGaussianPPOAgent(BaseAgent):
|
|
|
206
206
|
self._reward_normalizer.reset()
|
|
207
207
|
rewards = list(self._reward_normalizer.normalize(np.array(rewards, dtype=np.float32)))
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
if not dones[-1] and episode_data.final_obs is not None:
|
|
209
|
+
def _critic_value(obs):
|
|
211
210
|
with torch.no_grad():
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
).to(self._device)
|
|
215
|
-
last_value = float(self._critic(final_obs_t).squeeze().item())
|
|
211
|
+
obs_t = torch.as_tensor(np.atleast_2d(obs), dtype=torch.float32).to(self._device)
|
|
212
|
+
return float(self._critic(obs_t).squeeze().item())
|
|
216
213
|
|
|
217
|
-
|
|
214
|
+
last_value, n_envs = resolve_gae_bootstrap(episode_data, _critic_value)
|
|
215
|
+
|
|
216
|
+
advantages, returns = compute_gae(rewards, val_arr, dones, gamma, gae_lambda, last_value, n_envs=n_envs)
|
|
218
217
|
if T > 1:
|
|
219
218
|
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
|
|
220
219
|
|
|
@@ -4,6 +4,7 @@ import copy
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
|
|
6
6
|
from tensor_optix.core.base_agent import BaseAgent
|
|
7
|
+
from tensor_optix.core.resume_state import save_torch_resume_state, load_torch_resume_state
|
|
7
8
|
from tensor_optix.core.types import EpisodeData, HyperparamSet
|
|
8
9
|
from tensor_optix.core.replay_buffer import PrioritizedReplayBuffer
|
|
9
10
|
|
|
@@ -145,7 +146,7 @@ class TorchSACAgent(BaseAgent):
|
|
|
145
146
|
gamma = float(hp.get("gamma", 0.99))
|
|
146
147
|
tau = float(hp.get("tau", 0.005))
|
|
147
148
|
batch_size = int(hp.get("batch_size", 256))
|
|
148
|
-
updates_per_step =
|
|
149
|
+
updates_per_step = float(hp.get("updates_per_step", 1))
|
|
149
150
|
|
|
150
151
|
obs_arr = np.array(episode_data.observations, dtype=np.float32)
|
|
151
152
|
act_arr = np.array(episode_data.actions, dtype=np.float32)
|
|
@@ -165,7 +166,10 @@ class TorchSACAgent(BaseAgent):
|
|
|
165
166
|
"buffer_size": len(self._buffer),
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
|
|
169
|
+
# Truncate the product, not updates_per_step itself -- truncating
|
|
170
|
+
# a fractional updates_per_step (e.g. 0.5) to int first would zero
|
|
171
|
+
# it out regardless of T.
|
|
172
|
+
n_updates = int(T * updates_per_step)
|
|
169
173
|
total_al = 0.0
|
|
170
174
|
total_cl = 0.0
|
|
171
175
|
for _ in range(n_updates):
|
|
@@ -174,8 +178,8 @@ class TorchSACAgent(BaseAgent):
|
|
|
174
178
|
total_cl += cl
|
|
175
179
|
|
|
176
180
|
return {
|
|
177
|
-
"actor_loss": total_al / n_updates,
|
|
178
|
-
"critic_loss": total_cl / n_updates,
|
|
181
|
+
"actor_loss": total_al / max(n_updates, 1),
|
|
182
|
+
"critic_loss": total_cl / max(n_updates, 1),
|
|
179
183
|
"alpha": float(self._log_alpha.exp().item()),
|
|
180
184
|
"buffer_size": len(self._buffer),
|
|
181
185
|
}
|
|
@@ -224,6 +228,42 @@ class TorchSACAgent(BaseAgent):
|
|
|
224
228
|
self._c1_tgt.load_state_dict(self._c1.state_dict())
|
|
225
229
|
self._c2_tgt.load_state_dict(self._c2.state_dict())
|
|
226
230
|
|
|
231
|
+
@property
|
|
232
|
+
def resumable_components(self) -> frozenset:
|
|
233
|
+
# Deliberately does NOT include "replay_buffer" — PrioritizedReplayBuffer
|
|
234
|
+
# has no state_dict()/load_state_dict(), so resuming from a saved
|
|
235
|
+
# checkpoint restarts with an EMPTY buffer. For an off-policy algorithm
|
|
236
|
+
# this is a real gap (see BaseAgent.is_on_policy docstring): the first
|
|
237
|
+
# batch_size steps after resume have no replay data to sample from.
|
|
238
|
+
# Claiming "replay_buffer" here would be dishonest about what's
|
|
239
|
+
# actually restored — see resume_state.py's module docstring.
|
|
240
|
+
return frozenset({"model", "optimizer", "rng"})
|
|
241
|
+
|
|
242
|
+
def save_full_state(self, path: str) -> None:
|
|
243
|
+
self.save_weights(path)
|
|
244
|
+
save_torch_resume_state(
|
|
245
|
+
path,
|
|
246
|
+
actor_opt=self._actor_opt,
|
|
247
|
+
critic_opt=self._critic_opt,
|
|
248
|
+
alpha_opt=self._alpha_opt,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def load_full_state(self, path: str) -> None:
|
|
252
|
+
self.load_weights(path)
|
|
253
|
+
load_torch_resume_state(
|
|
254
|
+
path,
|
|
255
|
+
actor_opt=self._actor_opt,
|
|
256
|
+
critic_opt=self._critic_opt,
|
|
257
|
+
alpha_opt=self._alpha_opt,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def get_weights(self) -> dict:
|
|
261
|
+
return {
|
|
262
|
+
"actor": {k: v.detach().clone() for k, v in self._actor.state_dict().items()},
|
|
263
|
+
"critic1": {k: v.detach().clone() for k, v in self._c1.state_dict().items()},
|
|
264
|
+
"critic2": {k: v.detach().clone() for k, v in self._c2.state_dict().items()},
|
|
265
|
+
}
|
|
266
|
+
|
|
227
267
|
def average_weights(self, paths: list) -> None:
|
|
228
268
|
import torch
|
|
229
269
|
n = len(paths)
|
|
@@ -174,7 +174,7 @@ class TorchTD3Agent(BaseAgent):
|
|
|
174
174
|
gamma = float(hp.get("gamma", 0.99))
|
|
175
175
|
tau = float(hp.get("tau", 0.005))
|
|
176
176
|
batch_size = int(hp.get("batch_size", 256))
|
|
177
|
-
updates_per_step =
|
|
177
|
+
updates_per_step = float(hp.get("updates_per_step", 1))
|
|
178
178
|
policy_delay = int(hp.get("policy_delay", 2))
|
|
179
179
|
|
|
180
180
|
obs_arr = np.array(episode_data.observations, dtype=np.float32)
|
|
@@ -198,7 +198,10 @@ class TorchTD3Agent(BaseAgent):
|
|
|
198
198
|
"policy_update": 0,
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
# Truncate the product, not updates_per_step itself -- truncating
|
|
202
|
+
# a fractional updates_per_step (e.g. 0.5, meaning "update every
|
|
203
|
+
# other step") to int first would zero it out regardless of T.
|
|
204
|
+
n_updates = int(T * updates_per_step)
|
|
202
205
|
total_al = 0.0
|
|
203
206
|
total_cl = 0.0
|
|
204
207
|
n_actor_updates = 0
|
|
@@ -214,7 +217,7 @@ class TorchTD3Agent(BaseAgent):
|
|
|
214
217
|
|
|
215
218
|
return {
|
|
216
219
|
"actor_loss": total_al / max(n_actor_updates, 1),
|
|
217
|
-
"critic_loss": total_cl / n_updates,
|
|
220
|
+
"critic_loss": total_cl / max(n_updates, 1),
|
|
218
221
|
"buffer_size": len(self._buffer),
|
|
219
222
|
"policy_update": int(n_actor_updates > 0),
|
|
220
223
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsyncCallbackAdapter — a non-blocking wrapper around any LoopCallback.
|
|
3
|
+
|
|
4
|
+
LoopController fires callbacks synchronously, inline, on the training
|
|
5
|
+
thread (see LoopController._fire) -- a slow callback (GUI update, network
|
|
6
|
+
I/O, an unbuffered disk write) stalls training for as long as it takes to
|
|
7
|
+
return. This adapter enqueues each event onto a bounded queue and lets a
|
|
8
|
+
single background thread drain it into the wrapped callback, so the
|
|
9
|
+
training thread's call into _fire() always returns immediately regardless
|
|
10
|
+
of how slow the wrapped callback is.
|
|
11
|
+
|
|
12
|
+
This does NOT change LoopController's own contract -- callbacks are still
|
|
13
|
+
fired synchronously by design (so a broken callback can't silently corrupt
|
|
14
|
+
shared state via unsynchronized concurrent access to the agent/graph). Use
|
|
15
|
+
this adapter only around callbacks that are safe to run on a separate
|
|
16
|
+
thread and don't need to observe training state synchronously.
|
|
17
|
+
"""
|
|
18
|
+
import logging
|
|
19
|
+
import queue
|
|
20
|
+
import threading
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from .loop_controller import LoopCallback
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AsyncCallbackAdapter(LoopCallback):
|
|
29
|
+
"""
|
|
30
|
+
Wraps `inner` so its hooks run on a background thread via a bounded
|
|
31
|
+
queue, never on the training thread.
|
|
32
|
+
|
|
33
|
+
Overflow policy: when the queue is full, the OLDEST pending event is
|
|
34
|
+
dropped to make room for the newest one -- a consumer showing live
|
|
35
|
+
state (e.g. a GUI) cares about keeping up with "now," not replaying
|
|
36
|
+
every historical event. Dropped-event counts are tracked and exposed
|
|
37
|
+
via `dropped_count` so a consumer can detect and report that it's
|
|
38
|
+
falling behind.
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
adapter = AsyncCallbackAdapter(MyGuiCallback())
|
|
42
|
+
controller = LoopController(..., callbacks=[adapter])
|
|
43
|
+
...
|
|
44
|
+
controller.run()
|
|
45
|
+
adapter.close() # stop the background thread once training is done
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, inner: LoopCallback, maxsize: int = 256):
|
|
49
|
+
self._inner = inner
|
|
50
|
+
self._queue: "queue.Queue" = queue.Queue(maxsize=maxsize)
|
|
51
|
+
self._dropped_count = 0
|
|
52
|
+
self._lock = threading.Lock()
|
|
53
|
+
self._stop = threading.Event()
|
|
54
|
+
self._thread = threading.Thread(target=self._drain_loop, daemon=True)
|
|
55
|
+
self._thread.start()
|
|
56
|
+
|
|
57
|
+
def _drain_loop(self) -> None:
|
|
58
|
+
while not self._stop.is_set() or not self._queue.empty():
|
|
59
|
+
try:
|
|
60
|
+
event, args = self._queue.get(timeout=0.1)
|
|
61
|
+
except queue.Empty:
|
|
62
|
+
continue
|
|
63
|
+
try:
|
|
64
|
+
getattr(self._inner, event)(*args)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.warning(
|
|
67
|
+
"AsyncCallbackAdapter: inner callback %s raised during %s: %s",
|
|
68
|
+
self._inner, event, e,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def _enqueue(self, event: str, *args) -> None:
|
|
72
|
+
try:
|
|
73
|
+
self._queue.put_nowait((event, args))
|
|
74
|
+
return
|
|
75
|
+
except queue.Full:
|
|
76
|
+
pass
|
|
77
|
+
# Drop the oldest pending event to make room for this one.
|
|
78
|
+
try:
|
|
79
|
+
self._queue.get_nowait()
|
|
80
|
+
with self._lock:
|
|
81
|
+
self._dropped_count += 1
|
|
82
|
+
except queue.Empty:
|
|
83
|
+
pass
|
|
84
|
+
try:
|
|
85
|
+
self._queue.put_nowait((event, args))
|
|
86
|
+
except queue.Full:
|
|
87
|
+
with self._lock:
|
|
88
|
+
self._dropped_count += 1
|
|
89
|
+
|
|
90
|
+
def on_loop_start(self) -> None:
|
|
91
|
+
self._enqueue("on_loop_start")
|
|
92
|
+
|
|
93
|
+
def on_loop_stop(self) -> None:
|
|
94
|
+
self._enqueue("on_loop_stop")
|
|
95
|
+
|
|
96
|
+
def on_episode_end(self, episode_id, eval_metrics) -> None:
|
|
97
|
+
self._enqueue("on_episode_end", episode_id, eval_metrics)
|
|
98
|
+
|
|
99
|
+
def on_improvement(self, snapshot) -> None:
|
|
100
|
+
self._enqueue("on_improvement", snapshot)
|
|
101
|
+
|
|
102
|
+
def on_plateau(self, episode_id, state) -> None:
|
|
103
|
+
self._enqueue("on_plateau", episode_id, state)
|
|
104
|
+
|
|
105
|
+
def on_dormant(self, episode_id) -> None:
|
|
106
|
+
self._enqueue("on_dormant", episode_id)
|
|
107
|
+
|
|
108
|
+
def on_degradation(self, episode_id, eval_metrics) -> None:
|
|
109
|
+
self._enqueue("on_degradation", episode_id, eval_metrics)
|
|
110
|
+
|
|
111
|
+
def on_hyperparam_update(self, old_params, new_params) -> None:
|
|
112
|
+
self._enqueue("on_hyperparam_update", old_params, new_params)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def dropped_count(self) -> int:
|
|
116
|
+
with self._lock:
|
|
117
|
+
return self._dropped_count
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def pending_count(self) -> int:
|
|
121
|
+
return self._queue.qsize()
|
|
122
|
+
|
|
123
|
+
def close(self, timeout: Optional[float] = None) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Stop the background thread after draining whatever is already
|
|
126
|
+
queued. Call once the owning LoopController.run() has returned
|
|
127
|
+
(e.g. from a different, synchronous callback's on_loop_stop, or
|
|
128
|
+
by the caller directly) to avoid leaking the thread indefinitely.
|
|
129
|
+
"""
|
|
130
|
+
self._stop.set()
|
|
131
|
+
self._thread.join(timeout=timeout)
|
|
@@ -56,6 +56,57 @@ class BaseAgent(ABC):
|
|
|
56
56
|
def load_weights(self, path: str) -> None:
|
|
57
57
|
"""Restore model weights from the given path."""
|
|
58
58
|
|
|
59
|
+
@property
|
|
60
|
+
def resumable_components(self) -> frozenset:
|
|
61
|
+
"""
|
|
62
|
+
Which pieces of full training state save_full_state() actually
|
|
63
|
+
captures, beyond weights — e.g. frozenset({"optimizer", "rng"}).
|
|
64
|
+
|
|
65
|
+
Default (empty frozenset): save_full_state()/load_full_state()
|
|
66
|
+
are equivalent to save_weights()/load_weights() — weights only,
|
|
67
|
+
no optimizer/scheduler/RNG continuity. CheckpointRegistry records
|
|
68
|
+
this at save time so a checkpoint can be honestly labeled exact,
|
|
69
|
+
partial, or weights-only rather than assuming every checkpoint
|
|
70
|
+
supports exact training continuation — weights alone do not
|
|
71
|
+
guarantee that.
|
|
72
|
+
|
|
73
|
+
Override together with save_full_state()/load_full_state() in a
|
|
74
|
+
framework-specific subclass. See TorchAgent for a reference
|
|
75
|
+
implementation built on core.resume_state.
|
|
76
|
+
"""
|
|
77
|
+
return frozenset()
|
|
78
|
+
|
|
79
|
+
def save_full_state(self, path: str) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Persist everything needed to resume training, not just weights.
|
|
82
|
+
|
|
83
|
+
Default: identical to save_weights() — a safe no-op for any agent
|
|
84
|
+
that hasn't opted into full resumability. Override alongside
|
|
85
|
+
resumable_components and load_full_state().
|
|
86
|
+
"""
|
|
87
|
+
self.save_weights(path)
|
|
88
|
+
|
|
89
|
+
def load_full_state(self, path: str) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Restore everything save_full_state() persisted.
|
|
92
|
+
Default: identical to load_weights().
|
|
93
|
+
"""
|
|
94
|
+
self.load_weights(path)
|
|
95
|
+
|
|
96
|
+
def get_weights(self) -> dict:
|
|
97
|
+
"""
|
|
98
|
+
Return an in-memory snapshot of the model's learnable parameters,
|
|
99
|
+
without a disk round-trip through save_weights()/load_weights().
|
|
100
|
+
|
|
101
|
+
Default: raises NotImplementedError. Override in framework-specific
|
|
102
|
+
subclasses that can read parameters directly (currently TorchAgent
|
|
103
|
+
and its subclasses that reuse TorchAgent's model attribute).
|
|
104
|
+
"""
|
|
105
|
+
raise NotImplementedError(
|
|
106
|
+
f"{type(self).__name__} does not support get_weights(). "
|
|
107
|
+
f"Use save_weights()/load_weights() for disk-based access."
|
|
108
|
+
)
|
|
109
|
+
|
|
59
110
|
@property
|
|
60
111
|
def is_on_policy(self) -> bool:
|
|
61
112
|
"""
|
|
@@ -46,7 +46,13 @@ class CheckpointRegistry:
|
|
|
46
46
|
weights_path = os.path.join(snapshot_dir, "weights")
|
|
47
47
|
os.makedirs(snapshot_dir, exist_ok=True)
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
# save_full_state() defaults to save_weights() for any agent that
|
|
50
|
+
# hasn't opted into full resumability. Fall back directly to
|
|
51
|
+
# save_weights() for duck-typed agents that don't subclass
|
|
52
|
+
# BaseAgent (and so don't inherit that default) at all.
|
|
53
|
+
save_fn = getattr(agent, "save_full_state", agent.save_weights)
|
|
54
|
+
save_fn(weights_path)
|
|
55
|
+
resumable_components = sorted(getattr(agent, "resumable_components", frozenset()))
|
|
50
56
|
|
|
51
57
|
snapshot = PolicySnapshot(
|
|
52
58
|
snapshot_id=snapshot_id,
|
|
@@ -54,6 +60,7 @@ class CheckpointRegistry:
|
|
|
54
60
|
hyperparams=hyperparams,
|
|
55
61
|
weights_path=weights_path,
|
|
56
62
|
episode_id=eval_metrics.episode_id,
|
|
63
|
+
resumable_components=frozenset(resumable_components),
|
|
57
64
|
)
|
|
58
65
|
|
|
59
66
|
metadata = {
|
|
@@ -64,6 +71,7 @@ class CheckpointRegistry:
|
|
|
64
71
|
"metrics": eval_metrics.metrics,
|
|
65
72
|
"hyperparams": hyperparams.params,
|
|
66
73
|
"weights_path": weights_path,
|
|
74
|
+
"resumable_components": resumable_components,
|
|
67
75
|
}
|
|
68
76
|
with open(os.path.join(snapshot_dir, "metadata.json"), "w") as f:
|
|
69
77
|
json.dump(metadata, f, indent=2)
|
|
@@ -141,7 +149,8 @@ class CheckpointRegistry:
|
|
|
141
149
|
self._best = self._load_snapshot_from_dir(best_entry["snapshot_dir"])
|
|
142
150
|
|
|
143
151
|
if self._best is not None:
|
|
144
|
-
agent.load_weights
|
|
152
|
+
load_fn = getattr(agent, "load_full_state", agent.load_weights)
|
|
153
|
+
load_fn(self._best.weights_path)
|
|
145
154
|
return self._best
|
|
146
155
|
|
|
147
156
|
@property
|
|
@@ -202,4 +211,5 @@ class CheckpointRegistry:
|
|
|
202
211
|
weights_path=data["weights_path"],
|
|
203
212
|
episode_id=data["episode_id"],
|
|
204
213
|
timestamp=data["timestamp"],
|
|
214
|
+
resumable_components=frozenset(data.get("resumable_components", [])),
|
|
205
215
|
)
|