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.
Files changed (144) hide show
  1. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/PKG-INFO +1 -1
  2. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/pyproject.toml +1 -1
  3. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/pytorch/torch_agent.py +16 -0
  4. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/flax_ppo.py +7 -8
  5. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/tf_ppo.py +11 -7
  6. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/tf_ppo_continuous.py +7 -6
  7. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/tf_sac.py +7 -4
  8. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/tf_td3.py +6 -3
  9. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_ppo.py +30 -9
  10. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_ppo_continuous.py +7 -8
  11. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_sac.py +44 -4
  12. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_td3.py +6 -3
  13. tensor_optix-2.0.2/tensor_optix/core/async_callback.py +131 -0
  14. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/base_agent.py +51 -0
  15. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/checkpoint_registry.py +12 -2
  16. tensor_optix-2.0.2/tensor_optix/core/resume_state.py +72 -0
  17. tensor_optix-2.0.2/tensor_optix/core/trajectory_buffer.py +137 -0
  18. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/types.py +6 -0
  19. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/factory.py +69 -0
  20. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/__init__.py +13 -13
  21. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/callback.py +210 -210
  22. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/provenance.py +75 -75
  23. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/records.py +165 -165
  24. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/schema.py +183 -183
  25. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ledger/store.py +579 -579
  26. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/__init__.py +39 -39
  27. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/abstention.py +118 -118
  28. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/features.py +76 -76
  29. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/pooling.py +100 -100
  30. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/selector.py +234 -234
  31. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/nanonets/smalln.py +76 -76
  32. tensor_optix-2.0.2/tensor_optix/neuroevo/controller/topology_callback.py +66 -0
  33. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/controller/topology_controller.py +34 -5
  34. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/pipeline/vector_pipeline.py +20 -0
  35. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/__init__.py +21 -21
  36. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/baseline.py +182 -182
  37. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/experiment_stage.py +131 -131
  38. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/live_runtime.py +130 -130
  39. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/observer.py +98 -98
  40. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/runtime/promote.py +80 -80
  41. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/__init__.py +15 -15
  42. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/_mp_common.py +32 -32
  43. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/budget.py +35 -35
  44. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/dedup.py +17 -17
  45. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/isolation.py +20 -20
  46. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/pool.py +386 -386
  47. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/retry.py +39 -39
  48. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/scheduler/worker.py +111 -111
  49. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/selection/__init__.py +28 -28
  50. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/selection/backfill.py +93 -93
  51. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/selection/selector.py +174 -174
  52. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/selection/uncertainty.py +43 -43
  53. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/__init__.py +24 -24
  54. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/candidate.py +51 -51
  55. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/comparator.py +81 -81
  56. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/grammar.py +489 -489
  57. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/promotion.py +211 -197
  58. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/runner.py +218 -218
  59. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/strategy/worker.py +141 -141
  60. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/PKG-INFO +1 -1
  61. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/SOURCES.txt +3 -0
  62. tensor_optix-2.0.0/tensor_optix/core/trajectory_buffer.py +0 -82
  63. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/README.md +0 -0
  64. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/setup.cfg +0 -0
  65. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/__init__.py +0 -0
  66. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/__init__.py +0 -0
  67. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/jax/__init__.py +0 -0
  68. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/jax/flax_agent.py +0 -0
  69. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/jax/flax_evaluator.py +0 -0
  70. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/pytorch/__init__.py +0 -0
  71. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/pytorch/torch_evaluator.py +0 -0
  72. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/tensorflow/__init__.py +0 -0
  73. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/tensorflow/tf_agent.py +0 -0
  74. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/adapters/tensorflow/tf_evaluator.py +0 -0
  75. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/__init__.py +0 -0
  76. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/tf_dqn.py +0 -0
  77. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_dqn.py +0 -0
  78. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_rainbow_dqn.py +0 -0
  79. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/algorithms/torch_recurrent_ppo.py +0 -0
  80. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/callbacks/__init__.py +0 -0
  81. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/callbacks/rich_dashboard.py +0 -0
  82. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/callbacks/tensorboard_callback.py +0 -0
  83. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/callbacks/wandb_callback.py +0 -0
  84. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/cli.py +0 -0
  85. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/config.py +0 -0
  86. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/__init__.py +0 -0
  87. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/backoff_scheduler.py +0 -0
  88. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/base_evaluator.py +0 -0
  89. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/base_optimizer.py +0 -0
  90. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/base_pipeline.py +0 -0
  91. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/checkpoint_confirmation.py +0 -0
  92. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/device.py +0 -0
  93. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/diagnostic_controller.py +0 -0
  94. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/ensemble_agent.py +0 -0
  95. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/her_buffer.py +0 -0
  96. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/loop_controller.py +0 -0
  97. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/meta_controller.py +0 -0
  98. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/noisy_linear.py +0 -0
  99. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/normalizers.py +0 -0
  100. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/policy_manager.py +0 -0
  101. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/regime_detector.py +0 -0
  102. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/replay_buffer.py +0 -0
  103. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/core/reward_shape.py +0 -0
  104. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/distributed/__init__.py +0 -0
  105. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/distributed/async_learner.py +0 -0
  106. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/distributed/vtrace.py +0 -0
  107. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/exploration/__init__.py +0 -0
  108. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/exploration/rnd.py +0 -0
  109. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ml/__init__.py +0 -0
  110. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ml/dataset_pipeline.py +0 -0
  111. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ml/loss_registry.py +0 -0
  112. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/ml/ml_agent.py +0 -0
  113. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/__init__.py +0 -0
  114. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/agent/__init__.py +0 -0
  115. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/agent/graph_agent.py +0 -0
  116. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/brain_network.py +0 -0
  117. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/controller/__init__.py +0 -0
  118. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/graph/__init__.py +0 -0
  119. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/graph/neuron.py +0 -0
  120. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/graph/neuron_graph.py +0 -0
  121. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/graph/topology_ops.py +0 -0
  122. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/hebbian.py +0 -0
  123. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/neuromodulator.py +0 -0
  124. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/optimizer/__init__.py +0 -0
  125. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/neuroevo/optimizer/topology_aware_adam.py +0 -0
  126. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizer.py +0 -0
  127. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/__init__.py +0 -0
  128. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/adaptive_optimizer.py +0 -0
  129. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/backoff_optimizer.py +0 -0
  130. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/momentum_optimizer.py +0 -0
  131. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/pbt_optimizer.py +0 -0
  132. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/optimizers/spsa_optimizer.py +0 -0
  133. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/orchestrator.py +0 -0
  134. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/pipeline/__init__.py +0 -0
  135. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/pipeline/batch_pipeline.py +0 -0
  136. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/pipeline/live_pipeline.py +0 -0
  137. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/simple.py +0 -0
  138. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/utils/__init__.py +0 -0
  139. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix/utils/window_size.py +0 -0
  140. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/dependency_links.txt +0 -0
  141. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/entry_points.txt +0 -0
  142. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/requires.txt +0 -0
  143. {tensor_optix-2.0.0 → tensor_optix-2.0.2}/tensor_optix.egg-info/top_level.txt +0 -0
  144. {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.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.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
- last_value = 0.0
199
- if not dones[-1] and episode_data.final_obs is not None:
200
- obs_final = jnp.array(
201
- np.atleast_2d(episode_data.final_obs), dtype=jnp.float32
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 the window ended mid-episode, V(s_T) from the critic
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
- last_value = 0.0
155
- if not dones[-1] and episode_data.final_obs is not None:
156
- final_obs_t = tf.cast(np.atleast_2d(episode_data.final_obs), tf.float32)
157
- last_value = float(tf.squeeze(self._critic(final_obs_t, training=False)).numpy())
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
- advantages, returns = compute_gae(rewards, values_arr, dones, gamma, gae_lambda, last_value)
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
- last_value = 0.0
197
- if not dones[-1] and episode_data.final_obs is not None:
198
- final_obs_t = tf.cast(np.atleast_2d(episode_data.final_obs), tf.float32)
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
- advantages, returns = compute_gae(rewards, val_arr, dones, gamma, gae_lambda, last_value)
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 = int(hp.get("updates_per_step", 1))
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
- n_updates = T * updates_per_step
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 = int(hp.get("updates_per_step", 1))
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
- n_updates = T * updates_per_step
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 the window ended mid-episode, V(s_T) from the critic
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
- last_value = 0.0
132
- if not dones[-1] and episode_data.final_obs is not None:
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
- final_obs_t = torch.as_tensor(
135
- np.atleast_2d(episode_data.final_obs), dtype=torch.float32
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
- advantages, returns = compute_gae(rewards, val_arr, dones, gamma, gae_lambda, last_value)
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
- last_value = 0.0
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
- final_obs_t = torch.as_tensor(
213
- np.atleast_2d(episode_data.final_obs), dtype=torch.float32
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
- advantages, returns = compute_gae(rewards, val_arr, dones, gamma, gae_lambda, last_value)
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 = int(hp.get("updates_per_step", 1))
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
- n_updates = T * updates_per_step
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 = int(hp.get("updates_per_step", 1))
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
- n_updates = T * updates_per_step
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
- agent.save_weights(weights_path)
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(self._best.weights_path)
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
  )