tinybaselines 0.1.0a2__tar.gz → 0.1.0a3__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 (55) hide show
  1. {tinybaselines-0.1.0a2/tinybaselines.egg-info → tinybaselines-0.1.0a3}/PKG-INFO +17 -5
  2. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/README.md +16 -4
  3. tinybaselines-0.1.0a3/examples/cartpole/results/cpu.jsonl +4 -0
  4. tinybaselines-0.1.0a3/examples/cartpole/train.py +33 -0
  5. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/pyproject.toml +1 -1
  6. tinybaselines-0.1.0a3/tests/test_cartpole.py +39 -0
  7. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tests/test_checkpoint.py +12 -0
  8. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tests/test_core.py +23 -0
  9. tinybaselines-0.1.0a3/tests/test_learning.py +89 -0
  10. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tinybaselines/__init__.py +1 -1
  11. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tinybaselines/policies.py +49 -0
  12. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tinybaselines/ppo.py +11 -3
  13. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tinybaselines/rollout.py +1 -1
  14. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3/tinybaselines.egg-info}/PKG-INFO +17 -5
  15. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tinybaselines.egg-info/SOURCES.txt +3 -0
  16. tinybaselines-0.1.0a2/tests/test_learning.py +0 -47
  17. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/LICENSE +0 -0
  18. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/MANIFEST.in +0 -0
  19. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/__init__.py +0 -0
  20. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/__init__.py +0 -0
  21. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/config.py +0 -0
  22. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/reference.py +0 -0
  23. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/requirements-reference.txt +0 -0
  24. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/requirements.txt +0 -0
  25. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-initial-tinygrad-thread4.jsonl +0 -0
  26. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-initial-torch.jsonl +0 -0
  27. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-protocol.json +0 -0
  28. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-tinygrad-thread10.jsonl +0 -0
  29. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-tinygrad-thread4-error.jsonl +0 -0
  30. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-torch-thread4.jsonl +0 -0
  31. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/protocol.json +0 -0
  32. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/tinygrad-cpu.jsonl +0 -0
  33. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/torch-cpu.jsonl +0 -0
  34. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/run_learning.py +0 -0
  35. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/lunar_lander/train.py +0 -0
  36. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/pendulum/__init__.py +0 -0
  37. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/pendulum/env.py +0 -0
  38. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/pendulum/requirements.txt +0 -0
  39. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/pendulum/results/cpu-2.jsonl +0 -0
  40. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/pendulum/results/cpu-3.jsonl +0 -0
  41. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/pendulum/results/cpu.jsonl +0 -0
  42. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/examples/pendulum/train.py +0 -0
  43. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/requirements.txt +0 -0
  44. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/setup.cfg +0 -0
  45. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tests/test_evaluation.py +0 -0
  46. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tests/test_learning_run.py +0 -0
  47. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tests/test_parity.py +0 -0
  48. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tests/test_pendulum.py +0 -0
  49. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tests/test_rollout.py +0 -0
  50. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tests/test_training.py +0 -0
  51. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tinybaselines/evaluation.py +0 -0
  52. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tinybaselines/training.py +0 -0
  53. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tinybaselines.egg-info/dependency_links.txt +0 -0
  54. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tinybaselines.egg-info/requires.txt +0 -0
  55. {tinybaselines-0.1.0a2 → tinybaselines-0.1.0a3}/tinybaselines.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinybaselines
3
- Version: 0.1.0a2
3
+ Version: 0.1.0a3
4
4
  Summary: PPO in tinygrad.
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -12,7 +12,7 @@ Dynamic: license-file
12
12
 
13
13
  # tinybaselines
14
14
 
15
- PPO in tinygrad. Continuous actions clipped to `[-1, 1]`.
15
+ PPO in tinygrad for discrete and continuous actions. Continuous actions are clipped to `[-1, 1]`.
16
16
 
17
17
  ## LunarLander from PyPI
18
18
 
@@ -21,7 +21,7 @@ Use Python 3.11 and clang. The library install does not include Gymnasium.
21
21
  ```sh
22
22
  python3.11 -m venv .venv
23
23
  source .venv/bin/activate
24
- python -m pip install tinybaselines==0.1.0a2
24
+ python -m pip install tinybaselines==0.1.0a3
25
25
  ```
26
26
 
27
27
  Install Gymnasium and Box2D to run this example:
@@ -72,10 +72,10 @@ actions by default; `deterministic=True` uses clipped policy means.
72
72
  The collector uses Gymnasium vector environments with same-step autoreset.
73
73
  For time-limit bootstrapping, the collector reads pre-reset observations from `infos['final_obs']`.
74
74
 
75
- PPO checkpoints contain the policy, observation scale, Adam state, and PPO settings.
75
+ PPO checkpoints contain the policy, Adam state, and PPO settings.
76
76
  Continue training by passing the loaded PPO to `learn` with a new transition budget.
77
77
  Each `learn` call starts fresh episodes and a new transition counter.
78
- `policy.save(path)` exports a policy for inference.
78
+ `GaussianActorCritic.save(path)` exports a Gaussian policy for inference.
79
79
 
80
80
  ## Repository examples
81
81
 
@@ -86,6 +86,18 @@ pip install -e . -r examples/lunar_lander/requirements.txt
86
86
  python -m examples.lunar_lander.train
87
87
  ```
88
88
 
89
+ CartPole uses `CategoricalActorCritic`. The example trains, saves a checkpoint,
90
+ and evaluates the reloaded policy over 10 episodes.
91
+
92
+ ```sh
93
+ pip install -e . 'gymnasium==1.4.0'
94
+ DEV=CPU BEAM=0 python -m examples.cartpole.train --checkpoint cartpole.safetensors
95
+ ```
96
+
97
+ Three independent four-environment CPU runs averaged 500.00, 496.67, and 494.37
98
+ after 102,400 transitions each. All cleared 475 over 100 fresh stochastic episodes
99
+ per reloaded checkpoint. [Run records](examples/cartpole/results/cpu.jsonl).
100
+
89
101
  Pendulum uses three observations and one action. Its example rescales policy actions
90
102
  from `[-1, 1]` to torque in `[-2, 2]`.
91
103
 
@@ -1,6 +1,6 @@
1
1
  # tinybaselines
2
2
 
3
- PPO in tinygrad. Continuous actions clipped to `[-1, 1]`.
3
+ PPO in tinygrad for discrete and continuous actions. Continuous actions are clipped to `[-1, 1]`.
4
4
 
5
5
  ## LunarLander from PyPI
6
6
 
@@ -9,7 +9,7 @@ Use Python 3.11 and clang. The library install does not include Gymnasium.
9
9
  ```sh
10
10
  python3.11 -m venv .venv
11
11
  source .venv/bin/activate
12
- python -m pip install tinybaselines==0.1.0a2
12
+ python -m pip install tinybaselines==0.1.0a3
13
13
  ```
14
14
 
15
15
  Install Gymnasium and Box2D to run this example:
@@ -60,10 +60,10 @@ actions by default; `deterministic=True` uses clipped policy means.
60
60
  The collector uses Gymnasium vector environments with same-step autoreset.
61
61
  For time-limit bootstrapping, the collector reads pre-reset observations from `infos['final_obs']`.
62
62
 
63
- PPO checkpoints contain the policy, observation scale, Adam state, and PPO settings.
63
+ PPO checkpoints contain the policy, Adam state, and PPO settings.
64
64
  Continue training by passing the loaded PPO to `learn` with a new transition budget.
65
65
  Each `learn` call starts fresh episodes and a new transition counter.
66
- `policy.save(path)` exports a policy for inference.
66
+ `GaussianActorCritic.save(path)` exports a Gaussian policy for inference.
67
67
 
68
68
  ## Repository examples
69
69
 
@@ -74,6 +74,18 @@ pip install -e . -r examples/lunar_lander/requirements.txt
74
74
  python -m examples.lunar_lander.train
75
75
  ```
76
76
 
77
+ CartPole uses `CategoricalActorCritic`. The example trains, saves a checkpoint,
78
+ and evaluates the reloaded policy over 10 episodes.
79
+
80
+ ```sh
81
+ pip install -e . 'gymnasium==1.4.0'
82
+ DEV=CPU BEAM=0 python -m examples.cartpole.train --checkpoint cartpole.safetensors
83
+ ```
84
+
85
+ Three independent four-environment CPU runs averaged 500.00, 496.67, and 494.37
86
+ after 102,400 transitions each. All cleared 475 over 100 fresh stochastic episodes
87
+ per reloaded checkpoint. [Run records](examples/cartpole/results/cpu.jsonl).
88
+
77
89
  Pendulum uses three observations and one action. Its example rescales policy actions
78
90
  from `[-1, 1]` to torque in `[-2, 2]`.
79
91
 
@@ -0,0 +1,4 @@
1
+ {"event": "config", "source_commit": "536bfa324af5ce2793a2332844af36b4033a6cb2", "environment": "CartPole-v1", "num_envs": 4, "seed": null, "requested_timesteps": 102400, "rollout_steps": 1024, "evaluation_episodes": 100, "deterministic": false, "acceptance_mean": 475, "policy": {"state_dim": 4, "action_dim": 2, "hidden_dim": 64, "actor_layers": 2, "critic_layers": 2}, "ppo": {"pi_lr": 0.0010000000474974513, "vf_lr": 0.0010000000474974513, "gamma": 0.99, "lamda": 0.95, "K_epochs": 4, "eps_clip": 0.2, "batch_size": 256, "vf_coef": 0.5, "entropy_coef": 0.01}, "device": "CPU", "cpu_threads": 10, "beam": "0", "jit": "1", "python": "3.11.14", "platform": "macOS-27.0.1-arm64-arm-64bit", "versions": {"tinygrad": "0.14.0", "numpy": "2.4.6", "gymnasium": "1.1.1"}}
2
+ {"event": "final", "run": 1, "timesteps": 102400, "mean_return": 500.0, "episode_returns": [500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0], "passed": true, "training_seconds": null, "evaluation_seconds": 14.94911529199453, "checkpoint": "run-1.safetensors", "note": "Repeated evaluation from the same checkpoint after a logging error."}
3
+ {"event": "final", "run": 2, "timesteps": 102400, "mean_return": 496.67, "episode_returns": [500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 390.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 445.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 332.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0], "passed": true, "training_seconds": 56.7557016669889, "evaluation_seconds": 16.15585825004382, "checkpoint": "run-2.safetensors"}
4
+ {"event": "final", "run": 3, "timesteps": 102400, "mean_return": 494.37, "episode_returns": [500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 96.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 341.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0], "passed": true, "training_seconds": 55.03045945899794, "evaluation_seconds": 16.498802624992095, "checkpoint": "run-3.safetensors"}
@@ -0,0 +1,33 @@
1
+ import argparse
2
+ from contextlib import closing
3
+ from pathlib import Path
4
+ from statistics import mean
5
+
6
+ import gymnasium as gym
7
+
8
+ from tinybaselines import CategoricalActorCritic, PPO, evaluate, learn
9
+
10
+
11
+ def main():
12
+ parser = argparse.ArgumentParser(description='Train PPO on CartPole-v1.')
13
+ parser.add_argument('--timesteps', type=int, default=40_960)
14
+ parser.add_argument('--checkpoint', type=Path, required=True)
15
+ args = parser.parse_args()
16
+
17
+ policy = CategoricalActorCritic(4, 2, 64, 2, 2)
18
+ ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95, K_epochs=4,
19
+ eps_clip=0.2, batch_size=256, vf_coef=0.5, entropy_coef=0.01)
20
+ with closing(gym.vector.SyncVectorEnv(
21
+ [lambda: gym.make('CartPole-v1')] * 4,
22
+ autoreset_mode=gym.vector.AutoresetMode.SAME_STEP,
23
+ )) as env:
24
+ for step in learn(env, ppo, total_timesteps=args.timesteps, rollout_steps=1024):
25
+ print(f'{step.timesteps} transitions')
26
+ ppo.save(args.checkpoint)
27
+ policy = PPO.load(args.checkpoint).actor_critic
28
+ returns = evaluate(env, policy, num_episodes=10)
29
+ print(f'Mean return over {len(returns)} episodes: {mean(returns):.1f}')
30
+
31
+
32
+ if __name__ == '__main__':
33
+ main()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tinybaselines"
7
- version = "0.1.0a2"
7
+ version = "0.1.0a3"
8
8
  description = "PPO in tinygrad."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,39 @@
1
+ from contextlib import closing
2
+ from pathlib import Path
3
+ from tempfile import TemporaryDirectory
4
+ import unittest
5
+
6
+ import gymnasium as gym
7
+ import numpy as np
8
+
9
+ from tinybaselines import CategoricalActorCritic, PPO, evaluate, learn
10
+
11
+
12
+ class CartPoleTest(unittest.TestCase):
13
+ def test_categorical_policy_trains_with_ppo(self):
14
+ policy = CategoricalActorCritic(4, 2, 16, 1, 1)
15
+ ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95, K_epochs=2,
16
+ eps_clip=0.2, batch_size=64, vf_coef=0.5, entropy_coef=0.01)
17
+ weight = policy.actor.layers[-1].weight
18
+ before = weight.numpy().copy()
19
+ with closing(gym.vector.SyncVectorEnv([lambda: gym.make('CartPole-v1')] * 4,
20
+ autoreset_mode=gym.vector.AutoresetMode.SAME_STEP)) as env:
21
+ actions, _ = policy.act(env.reset()[0])
22
+ self.assertTrue(env.action_space.contains(actions))
23
+ steps = list(learn(env, ppo, total_timesteps=384, rollout_steps=128))
24
+ self.assertEqual(steps[-1].timesteps, 384)
25
+ self.assertFalse(np.array_equal(weight.numpy(), before))
26
+ observations, _ = env.reset()
27
+ with TemporaryDirectory() as directory:
28
+ path = Path(directory) / 'ppo.safetensors'
29
+ ppo.save(path)
30
+ restored = PPO.load(path)
31
+ self.assertIsInstance(restored.actor_critic, CategoricalActorCritic)
32
+ np.testing.assert_array_equal(restored.actor_critic.act(observations, deterministic=True)[0],
33
+ policy.act(observations, deterministic=True)[0])
34
+ state = restored.state_dict()
35
+ self.assertEqual(state.keys(), ppo.state_dict().keys())
36
+ for name, tensor in ppo.state_dict().items():
37
+ np.testing.assert_array_equal(state[name].numpy(), tensor.numpy())
38
+ self.assertEqual(len(evaluate(env, restored.actor_critic, num_episodes=8)), 8)
39
+ self.assertEqual(next(learn(env, restored, total_timesteps=128, rollout_steps=128)).timesteps, 128)
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import tempfile
2
3
  import unittest
3
4
  from pathlib import Path
@@ -83,6 +84,17 @@ class CheckpointTest(unittest.TestCase):
83
84
  np.testing.assert_array_equal(first.actor_critic.log_std.numpy(), before)
84
85
  np.testing.assert_array_equal(second.actor_critic.log_std.numpy(), [0.25])
85
86
 
87
+ def test_loads_gaussian_checkpoint_without_policy_type(self):
88
+ ppo = self.make_ppo()
89
+ metadata = dict(format='tinybaselines.ppo.v1', policy_config=json.dumps(ppo.actor_critic.config),
90
+ ppo_config=json.dumps(ppo.config))
91
+ with tempfile.TemporaryDirectory() as directory:
92
+ path = Path(directory) / 'legacy.safetensors'
93
+ lander.nn.state.safe_save(ppo.state_dict(), path, metadata=metadata)
94
+ restored = PPO.load(path)
95
+ self.assertIsInstance(restored.actor_critic, GaussianActorCritic)
96
+ np.testing.assert_array_equal(restored.actor_critic.observation_scale.numpy(), [2, 3, 4])
97
+
86
98
  def test_failed_save_preserves_previous_checkpoint(self):
87
99
  ppo = self.make_ppo()
88
100
 
@@ -6,12 +6,35 @@ import unittest
6
6
  from unittest.mock import patch
7
7
 
8
8
  import numpy as np
9
+ from tinygrad import Tensor
9
10
 
10
11
  from tinybaselines import GaussianActorCritic, PPO, rollout
11
12
  from tinybaselines.rollout import RolloutBatch
12
13
 
13
14
 
14
15
  class CoreTest(unittest.TestCase):
16
+ def test_action_dtype_and_shape_survive_minibatching(self):
17
+ observations = np.zeros((4, 3), np.float32)
18
+ done = np.zeros(4, bool)
19
+ env = SimpleNamespace(num_envs=4, reset=lambda: (observations, {}),
20
+ step=lambda actions: (observations, np.ones(4), done, done, {}))
21
+ policy = GaussianActorCritic(3, 2, 4, 1, 1)
22
+ ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95, K_epochs=1,
23
+ eps_clip=0.2, batch_size=4, vf_coef=1.0, entropy_coef=0.0)
24
+ values = Tensor.zeros(8, 1, device=policy.device)
25
+ for actions in (np.arange(4, dtype=np.int32), np.arange(8, dtype=np.float32).reshape(4, 2) / 8):
26
+ with self.subTest(dtype=actions.dtype, shape=actions.shape):
27
+ with patch.object(policy, 'act', return_value=(actions, actions)):
28
+ batch, _ = rollout(env, policy, num_steps=8)
29
+ self.assertEqual(batch.raw_actions.dtype, actions.dtype)
30
+ np.testing.assert_array_equal(batch.raw_actions, np.stack([actions, actions]))
31
+ with patch.object(policy, 'evaluate_actions', return_value=(values, values.squeeze(-1), None)), \
32
+ patch.object(ppo, 'train_step') as step, patch('numpy.random.permutation', return_value=np.arange(8)):
33
+ ppo.update(batch)
34
+ minibatches = np.concatenate([call.args[1].numpy() for call in step.call_args_list])
35
+ self.assertEqual(minibatches.dtype, actions.dtype)
36
+ np.testing.assert_array_equal(minibatches, np.concatenate([actions, actions]))
37
+
15
38
  def test_ppo_gets_actor_parameters_and_device_from_policy(self):
16
39
  model = GaussianActorCritic(3, 1, 4, 1, 1)
17
40
  weight = model.actor.layers[0].weight
@@ -0,0 +1,89 @@
1
+ import unittest
2
+
3
+ import numpy as np
4
+ from tinygrad import Tensor
5
+
6
+ from tinybaselines import CategoricalActorCritic, GaussianActorCritic, PPO, learn
7
+
8
+
9
+ class IdentityEnv:
10
+ num_envs = 4
11
+
12
+ def __init__(self, seed):
13
+ self.rng = np.random.default_rng(seed)
14
+
15
+ def reset(self):
16
+ self.observations = self.rng.uniform(-0.75, 0.75, (self.num_envs, 1)).astype(np.float32)
17
+ return self.observations, {}
18
+
19
+ def step(self, actions):
20
+ rewards = -np.square(actions[:, 0] - self.observations[:, 0])
21
+ observations, _ = self.reset()
22
+ return observations, rewards, np.ones(self.num_envs, bool), np.zeros(self.num_envs, bool), {}
23
+
24
+
25
+ def identity_errors(seed):
26
+ Tensor.manual_seed(seed)
27
+ np.random.seed(seed)
28
+ policy = GaussianActorCritic(1, 1, 16, 1, 1)
29
+ ppo = PPO(policy, pi_lr=0.01, vf_lr=0.01, gamma=0.99, lamda=0.95, K_epochs=4,
30
+ eps_clip=0.2, batch_size=256, vf_coef=0.5, entropy_coef=0.0)
31
+ observations = np.linspace(-0.75, 0.75, 65, dtype=np.float32)[:, None]
32
+
33
+ def error():
34
+ actions, _ = policy.act(observations, deterministic=True)
35
+ return np.square(actions - observations).mean()
36
+
37
+ before = error()
38
+ for _ in learn(IdentityEnv(seed), ppo, total_timesteps=2048, rollout_steps=256):
39
+ pass
40
+ return before, error(), np.square(observations).mean()
41
+
42
+
43
+ class DiscreteIdentityEnv:
44
+ num_envs = 4
45
+ observations = np.eye(4, dtype=np.float32)
46
+
47
+ def __init__(self, seed):
48
+ self.rng = np.random.default_rng(seed)
49
+
50
+ def reset(self):
51
+ self.targets = self.rng.integers(4, size=self.num_envs)
52
+ return self.observations[self.targets], {}
53
+
54
+ def step(self, actions):
55
+ rewards = (actions == self.targets).astype(np.float32)
56
+ observations, _ = self.reset()
57
+ return observations, rewards, np.ones(self.num_envs, bool), np.zeros(self.num_envs, bool), {}
58
+
59
+
60
+ def discrete_identity_scores(seed):
61
+ Tensor.manual_seed(seed)
62
+ np.random.seed(seed)
63
+ env = DiscreteIdentityEnv(seed)
64
+ policy = CategoricalActorCritic(4, 4, 16, 1, 1)
65
+ ppo = PPO(policy, pi_lr=0.02, vf_lr=0.01, gamma=0.99, lamda=0.95, K_epochs=4,
66
+ eps_clip=0.2, batch_size=64, vf_coef=0.5, entropy_coef=0.0)
67
+ states = policy.preprocess(env.observations)
68
+ targets = Tensor(np.arange(4, dtype=np.int32), device=policy.device)
69
+
70
+ def score():
71
+ _, log_probs, _ = policy.evaluate_actions(states, targets)
72
+ return log_probs.exp().mean().item()
73
+
74
+ before = score()
75
+ for _ in learn(env, ppo, total_timesteps=1024, rollout_steps=128):
76
+ pass
77
+ return before, score()
78
+
79
+
80
+ class LearningTest(unittest.TestCase):
81
+ def test_learns_identity(self):
82
+ self.addCleanup(np.random.set_state, np.random.get_state())
83
+ before, after, zero = identity_errors(seed=0)
84
+ self.assertLess(after, 0.25 * min(before, zero), (before, after, zero))
85
+
86
+ def test_learns_discrete_identity(self):
87
+ self.addCleanup(np.random.set_state, np.random.get_state())
88
+ before, after = discrete_identity_scores(seed=0)
89
+ self.assertGreater(after, max(before, 0.9), (before, after))
@@ -1,5 +1,5 @@
1
1
  from .ppo import PPO
2
- from .policies import GaussianActorCritic
2
+ from .policies import CategoricalActorCritic, GaussianActorCritic
3
3
  from .rollout import RolloutBatch, RolloutState, rollout
4
4
  from .training import TrainingStep, learn
5
5
  from .evaluation import evaluate
@@ -25,6 +25,8 @@ class MLP:
25
25
 
26
26
 
27
27
  class GaussianActorCritic:
28
+ _checkpoint_type = 'gaussian'
29
+
28
30
  def __init__(self, state_dim, action_dim, hidden_dim, actor_layers, critic_layers, observation_scale=None):
29
31
  self.config = dict(state_dim=state_dim, action_dim=action_dim, hidden_dim=hidden_dim,
30
32
  actor_layers=actor_layers, critic_layers=critic_layers)
@@ -96,3 +98,50 @@ class GaussianActorCritic:
96
98
  self.action_std = np.exp(self.log_std.clip(-5, 2).numpy())
97
99
  if not np.isfinite(self.action_std).all():
98
100
  raise FloatingPointError('non-finite policy standard deviation')
101
+
102
+
103
+ class CategoricalActorCritic:
104
+ """Actor-critic for a single discrete action per observation."""
105
+
106
+ _checkpoint_type = 'categorical'
107
+
108
+ def __init__(self, state_dim, action_dim, hidden_dim, actor_layers, critic_layers):
109
+ self.config = dict(state_dim=state_dim, action_dim=action_dim, hidden_dim=hidden_dim,
110
+ actor_layers=actor_layers, critic_layers=critic_layers)
111
+ self.actor = MLP(state_dim, action_dim, hidden_dim, actor_layers)
112
+ self.critic = MLP(state_dim, 1, hidden_dim, critic_layers)
113
+ self.actor_jit = TinyJit(lambda states: self.actor(states).realize())
114
+ self.jit_shape = None
115
+
116
+ @property
117
+ def actor_parameters(self):
118
+ return nn.state.get_parameters(self.actor)
119
+
120
+ @property
121
+ def device(self):
122
+ return self.actor.layers[0].weight.device
123
+
124
+ def preprocess(self, observations):
125
+ return Tensor(observations.astype(np.float32), device=self.device)
126
+
127
+ def evaluate_actions(self, states, actions):
128
+ log_probs = self.actor(states).log_softmax(axis=-1)
129
+ selected = log_probs.gather(-1, actions.unsqueeze(-1)).squeeze(-1)
130
+ entropy = -(log_probs.exp() * log_probs).sum(axis=-1).mean()
131
+ return self.critic(states), selected, entropy
132
+
133
+ def act(self, state, deterministic=False):
134
+ states = self.preprocess(state)
135
+ if self.jit_shape != states.shape:
136
+ self.refresh_inference()
137
+ self.jit_shape = states.shape
138
+ logits = self.actor_jit(states).numpy()
139
+ scores = logits if deterministic else logits + np.random.gumbel(size=logits.shape)
140
+ actions = scores.argmax(axis=-1).astype(np.int32)
141
+ return actions, actions
142
+
143
+ def refresh_inference(self):
144
+ self.actor_jit.reset()
145
+
146
+ def state_dict(self):
147
+ return nn.state.get_state_dict(dict(actor=self.actor, critic=self.critic))
@@ -5,7 +5,10 @@ from tempfile import TemporaryDirectory
5
5
  import numpy as np
6
6
  from tinygrad import Context, Tensor, TinyJit, nn
7
7
 
8
- from .policies import GaussianActorCritic
8
+ from .policies import CategoricalActorCritic, GaussianActorCritic
9
+
10
+ _CHECKPOINT_POLICIES = {policy._checkpoint_type: policy for policy in (GaussianActorCritic, CategoricalActorCritic)}
11
+
9
12
 
10
13
  def clip_grad_norm(parameters, max_norm):
11
14
  gradients = [parameter.grad for parameter in parameters if parameter.grad is not None]
@@ -52,6 +55,7 @@ class PPO:
52
55
  def save(self, path):
53
56
  path = Path(path)
54
57
  metadata = dict(format='tinybaselines.ppo.v1', policy_config=json.dumps(self.actor_critic.config),
58
+ policy_type=self.actor_critic._checkpoint_type,
55
59
  ppo_config=json.dumps(self.config))
56
60
  with TemporaryDirectory(dir=path.parent) as directory:
57
61
  temporary = Path(directory) / path.name
@@ -64,7 +68,11 @@ class PPO:
64
68
  metadata = header.get('__metadata__', {})
65
69
  if metadata.get('format') != 'tinybaselines.ppo.v1':
66
70
  raise ValueError('unsupported PPO checkpoint format')
67
- policy = GaussianActorCritic(**json.loads(metadata['policy_config']))
71
+ policy_type = metadata.get('policy_type', 'gaussian')
72
+ policy_class = _CHECKPOINT_POLICIES.get(policy_type)
73
+ if policy_class is None:
74
+ raise ValueError(f'unsupported PPO checkpoint policy type: {policy_type!r}')
75
+ policy = policy_class(**json.loads(metadata['policy_config']))
68
76
  ppo = cls(policy, **json.loads(metadata['ppo_config']))
69
77
  nn.state.load_state_dict(ppo.state_dict(), nn.state.safe_load(path), verbose=False)
70
78
  policy.refresh_inference()
@@ -114,7 +122,7 @@ class PPO:
114
122
  def prepare(self, batch):
115
123
  T, N = batch.rewards.shape
116
124
  obs_flat = self.actor_critic.preprocess(batch.observations.reshape(T * N, -1))
117
- raw_act_np = batch.raw_actions.reshape(T * N, -1).astype(np.float32)
125
+ raw_act_np = batch.raw_actions.reshape(T * N, *batch.raw_actions.shape[2:])
118
126
  raw_act_flat = Tensor(raw_act_np, device=self.device)
119
127
  old_value, old_logprobs, _ = self.actor_critic.evaluate_actions(obs_flat, raw_act_flat)
120
128
  old_logprobs_np = old_logprobs.numpy()
@@ -58,7 +58,7 @@ def rollout(env, actor_critic, num_steps=None, num_episodes=None, deterministic=
58
58
  actions, raw_actions = actor_critic.act(states, deterministic=deterministic)
59
59
  if collect:
60
60
  if t == 0:
61
- raw_act = np.empty((T, *raw_actions.shape), dtype=np.float32)
61
+ raw_act = np.empty((T, *raw_actions.shape), dtype=raw_actions.dtype)
62
62
  obs[t], raw_act[t] = states, raw_actions
63
63
 
64
64
  states, rewards, terminated, truncated, infos = env.step(actions)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinybaselines
3
- Version: 0.1.0a2
3
+ Version: 0.1.0a3
4
4
  Summary: PPO in tinygrad.
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -12,7 +12,7 @@ Dynamic: license-file
12
12
 
13
13
  # tinybaselines
14
14
 
15
- PPO in tinygrad. Continuous actions clipped to `[-1, 1]`.
15
+ PPO in tinygrad for discrete and continuous actions. Continuous actions are clipped to `[-1, 1]`.
16
16
 
17
17
  ## LunarLander from PyPI
18
18
 
@@ -21,7 +21,7 @@ Use Python 3.11 and clang. The library install does not include Gymnasium.
21
21
  ```sh
22
22
  python3.11 -m venv .venv
23
23
  source .venv/bin/activate
24
- python -m pip install tinybaselines==0.1.0a2
24
+ python -m pip install tinybaselines==0.1.0a3
25
25
  ```
26
26
 
27
27
  Install Gymnasium and Box2D to run this example:
@@ -72,10 +72,10 @@ actions by default; `deterministic=True` uses clipped policy means.
72
72
  The collector uses Gymnasium vector environments with same-step autoreset.
73
73
  For time-limit bootstrapping, the collector reads pre-reset observations from `infos['final_obs']`.
74
74
 
75
- PPO checkpoints contain the policy, observation scale, Adam state, and PPO settings.
75
+ PPO checkpoints contain the policy, Adam state, and PPO settings.
76
76
  Continue training by passing the loaded PPO to `learn` with a new transition budget.
77
77
  Each `learn` call starts fresh episodes and a new transition counter.
78
- `policy.save(path)` exports a policy for inference.
78
+ `GaussianActorCritic.save(path)` exports a Gaussian policy for inference.
79
79
 
80
80
  ## Repository examples
81
81
 
@@ -86,6 +86,18 @@ pip install -e . -r examples/lunar_lander/requirements.txt
86
86
  python -m examples.lunar_lander.train
87
87
  ```
88
88
 
89
+ CartPole uses `CategoricalActorCritic`. The example trains, saves a checkpoint,
90
+ and evaluates the reloaded policy over 10 episodes.
91
+
92
+ ```sh
93
+ pip install -e . 'gymnasium==1.4.0'
94
+ DEV=CPU BEAM=0 python -m examples.cartpole.train --checkpoint cartpole.safetensors
95
+ ```
96
+
97
+ Three independent four-environment CPU runs averaged 500.00, 496.67, and 494.37
98
+ after 102,400 transitions each. All cleared 475 over 100 fresh stochastic episodes
99
+ per reloaded checkpoint. [Run records](examples/cartpole/results/cpu.jsonl).
100
+
89
101
  Pendulum uses three observations and one action. Its example rescales policy actions
90
102
  from `[-1, 1]` to torque in `[-2, 2]`.
91
103
 
@@ -4,6 +4,8 @@ README.md
4
4
  pyproject.toml
5
5
  requirements.txt
6
6
  examples/__init__.py
7
+ examples/cartpole/train.py
8
+ examples/cartpole/results/cpu.jsonl
7
9
  examples/lunar_lander/__init__.py
8
10
  examples/lunar_lander/config.py
9
11
  examples/lunar_lander/reference.py
@@ -27,6 +29,7 @@ examples/pendulum/train.py
27
29
  examples/pendulum/results/cpu-2.jsonl
28
30
  examples/pendulum/results/cpu-3.jsonl
29
31
  examples/pendulum/results/cpu.jsonl
32
+ tests/test_cartpole.py
30
33
  tests/test_checkpoint.py
31
34
  tests/test_core.py
32
35
  tests/test_evaluation.py
@@ -1,47 +0,0 @@
1
- import unittest
2
-
3
- import numpy as np
4
- from tinygrad import Tensor
5
-
6
- from tinybaselines import GaussianActorCritic, PPO, learn
7
-
8
-
9
- class IdentityEnv:
10
- num_envs = 4
11
-
12
- def __init__(self, seed):
13
- self.rng = np.random.default_rng(seed)
14
-
15
- def reset(self):
16
- self.observations = self.rng.uniform(-0.75, 0.75, (self.num_envs, 1)).astype(np.float32)
17
- return self.observations, {}
18
-
19
- def step(self, actions):
20
- rewards = -np.square(actions[:, 0] - self.observations[:, 0])
21
- observations, _ = self.reset()
22
- return observations, rewards, np.ones(self.num_envs, bool), np.zeros(self.num_envs, bool), {}
23
-
24
-
25
- def identity_errors(seed):
26
- Tensor.manual_seed(seed)
27
- np.random.seed(seed)
28
- policy = GaussianActorCritic(1, 1, 16, 1, 1)
29
- ppo = PPO(policy, pi_lr=0.01, vf_lr=0.01, gamma=0.99, lamda=0.95, K_epochs=4,
30
- eps_clip=0.2, batch_size=256, vf_coef=0.5, entropy_coef=0.0)
31
- observations = np.linspace(-0.75, 0.75, 65, dtype=np.float32)[:, None]
32
-
33
- def error():
34
- actions, _ = policy.act(observations, deterministic=True)
35
- return np.square(actions - observations).mean()
36
-
37
- before = error()
38
- for _ in learn(IdentityEnv(seed), ppo, total_timesteps=2048, rollout_steps=256):
39
- pass
40
- return before, error(), np.square(observations).mean()
41
-
42
-
43
- class LearningTest(unittest.TestCase):
44
- def test_learns_identity(self):
45
- self.addCleanup(np.random.set_state, np.random.get_state())
46
- before, after, zero = identity_errors(seed=0)
47
- self.assertLess(after, 0.25 * min(before, zero), (before, after, zero))
File without changes