tinybaselines 0.1.0a1__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.0a1/tinybaselines.egg-info → tinybaselines-0.1.0a3}/PKG-INFO +49 -26
  2. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/README.md +48 -25
  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.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/train.py +1 -1
  6. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/pyproject.toml +1 -1
  7. tinybaselines-0.1.0a3/tests/test_cartpole.py +39 -0
  8. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_checkpoint.py +12 -0
  9. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_core.py +41 -0
  10. tinybaselines-0.1.0a3/tests/test_learning.py +89 -0
  11. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_parity.py +2 -1
  12. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_pendulum.py +9 -0
  13. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/__init__.py +1 -1
  14. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/policies.py +72 -0
  15. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/ppo.py +18 -17
  16. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/rollout.py +5 -1
  17. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3/tinybaselines.egg-info}/PKG-INFO +49 -26
  18. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines.egg-info/SOURCES.txt +3 -0
  19. tinybaselines-0.1.0a1/tests/test_learning.py +0 -47
  20. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/LICENSE +0 -0
  21. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/MANIFEST.in +0 -0
  22. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/__init__.py +0 -0
  23. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/__init__.py +0 -0
  24. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/config.py +0 -0
  25. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/reference.py +0 -0
  26. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/requirements-reference.txt +0 -0
  27. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/requirements.txt +0 -0
  28. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-initial-tinygrad-thread4.jsonl +0 -0
  29. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-initial-torch.jsonl +0 -0
  30. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-protocol.json +0 -0
  31. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-tinygrad-thread10.jsonl +0 -0
  32. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-tinygrad-thread4-error.jsonl +0 -0
  33. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/phase-torch-thread4.jsonl +0 -0
  34. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/protocol.json +0 -0
  35. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/tinygrad-cpu.jsonl +0 -0
  36. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/results/2026-10-26/torch-cpu.jsonl +0 -0
  37. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/lunar_lander/run_learning.py +0 -0
  38. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/__init__.py +0 -0
  39. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/env.py +0 -0
  40. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/requirements.txt +0 -0
  41. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/results/cpu-2.jsonl +0 -0
  42. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/results/cpu-3.jsonl +0 -0
  43. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/results/cpu.jsonl +0 -0
  44. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/examples/pendulum/train.py +0 -0
  45. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/requirements.txt +0 -0
  46. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/setup.cfg +0 -0
  47. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_evaluation.py +0 -0
  48. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_learning_run.py +0 -0
  49. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_rollout.py +0 -0
  50. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tests/test_training.py +0 -0
  51. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/evaluation.py +0 -0
  52. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines/training.py +0 -0
  53. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines.egg-info/dependency_links.txt +0 -0
  54. {tinybaselines-0.1.0a1 → tinybaselines-0.1.0a3}/tinybaselines.egg-info/requires.txt +0 -0
  55. {tinybaselines-0.1.0a1 → 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.0a1
3
+ Version: 0.1.0a3
4
4
  Summary: PPO in tinygrad.
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -12,44 +12,53 @@ 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
- Python 3.11 and clang.
17
+ ## LunarLander from PyPI
18
+
19
+ Use Python 3.11 and clang. The library install does not include Gymnasium.
18
20
 
19
21
  ```sh
20
22
  python3.11 -m venv .venv
21
23
  source .venv/bin/activate
22
- pip install -e . -r examples/lunar_lander/requirements.txt
24
+ python -m pip install tinybaselines==0.1.0a3
23
25
  ```
24
26
 
25
- Train, save, and load.
27
+ Install Gymnasium and Box2D to run this example:
28
+
29
+ ```sh
30
+ python -m pip install 'gymnasium[box2d]==1.4.0'
31
+ ```
32
+
33
+ Save this as `train_lander.py`. It runs one PPO update and evaluates the reloaded checkpoint.
26
34
 
27
35
  ```python
36
+ from contextlib import closing
28
37
  from statistics import mean
38
+
39
+ import gymnasium as gym
40
+
29
41
  from tinybaselines import GaussianActorCritic, PPO, evaluate, learn
30
- from examples.lunar_lander.config import OBS_SCALE
31
- from examples.lunar_lander.train import make_env
32
42
 
33
- env = make_env(4, asynchronous=False)
34
43
  policy = GaussianActorCritic(state_dim=8, action_dim=2, hidden_dim=128,
35
- actor_layers=4, critic_layers=4, observation_scale=OBS_SCALE)
44
+ actor_layers=4, critic_layers=4,
45
+ observation_scale=[10, 6.666, 5, 7.5, 1, 2.5, 1, 1])
36
46
  ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95,
37
47
  K_epochs=20, eps_clip=0.2, batch_size=10_000, vf_coef=1.0, entropy_coef=0.001)
38
48
 
39
- for step in learn(env, ppo, total_timesteps=2_000_000, rollout_steps=20_000):
40
- print(step.timesteps, step.rollout_seconds, step.update_seconds)
41
-
42
- ppo.save('ppo.safetensors')
43
- ppo = PPO.load('ppo.safetensors')
44
- policy = ppo.actor_critic
45
- observations, _ = env.reset()
46
- actions, raw_actions = policy.act(observations, deterministic=True)
47
- env.close()
49
+ with closing(gym.vector.SyncVectorEnv(
50
+ [lambda: gym.make('LunarLander-v3', continuous=True)] * 4,
51
+ autoreset_mode=gym.vector.AutoresetMode.SAME_STEP,
52
+ )) as env:
53
+ for step in learn(env, ppo, total_timesteps=20_000, rollout_steps=20_000):
54
+ print(step.timesteps, step.rollout_seconds, step.update_seconds)
55
+ ppo.save('ppo.safetensors')
56
+ policy = PPO.load('ppo.safetensors').actor_critic
57
+ print(mean(evaluate(env, policy, num_episodes=10)))
58
+ ```
48
59
 
49
- eval_env = make_env(4, asynchronous=False)
50
- returns = evaluate(eval_env, policy, num_episodes=100)
51
- eval_env.close()
52
- print(mean(returns))
60
+ ```sh
61
+ DEV=CPU BEAM=0 python train_lander.py
53
62
  ```
54
63
 
55
64
  `total_timesteps` and `rollout_steps` count transitions across all environments.
@@ -63,18 +72,32 @@ actions by default; `deterministic=True` uses clipped policy means.
63
72
  The collector uses Gymnasium vector environments with same-step autoreset.
64
73
  For time-limit bootstrapping, the collector reads pre-reset observations from `infos['final_obs']`.
65
74
 
66
- PPO checkpoints contain the policy, observation scale, Adam state, and PPO settings.
75
+ PPO checkpoints contain the policy, Adam state, and PPO settings.
67
76
  Continue training by passing the loaded PPO to `learn` with a new transition budget.
68
77
  Each `learn` call starts fresh episodes and a new transition counter.
69
- `policy.save(path)` exports a policy for inference.
78
+ `GaussianActorCritic.save(path)` exports a Gaussian policy for inference.
79
+
80
+ ## Repository examples
70
81
 
71
- The full LunarLander trainer, evaluation, and PyTorch reference live in
72
- `examples/lunar_lander`.
82
+ From a checkout, run the full LunarLander trainer:
73
83
 
74
84
  ```sh
85
+ pip install -e . -r examples/lunar_lander/requirements.txt
75
86
  python -m examples.lunar_lander.train
76
87
  ```
77
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
+
78
101
  Pendulum uses three observations and one action. Its example rescales policy actions
79
102
  from `[-1, 1]` to torque in `[-2, 2]`.
80
103
 
@@ -1,43 +1,52 @@
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
- Python 3.11 and clang.
5
+ ## LunarLander from PyPI
6
+
7
+ Use Python 3.11 and clang. The library install does not include Gymnasium.
6
8
 
7
9
  ```sh
8
10
  python3.11 -m venv .venv
9
11
  source .venv/bin/activate
10
- pip install -e . -r examples/lunar_lander/requirements.txt
12
+ python -m pip install tinybaselines==0.1.0a3
11
13
  ```
12
14
 
13
- Train, save, and load.
15
+ Install Gymnasium and Box2D to run this example:
16
+
17
+ ```sh
18
+ python -m pip install 'gymnasium[box2d]==1.4.0'
19
+ ```
20
+
21
+ Save this as `train_lander.py`. It runs one PPO update and evaluates the reloaded checkpoint.
14
22
 
15
23
  ```python
24
+ from contextlib import closing
16
25
  from statistics import mean
26
+
27
+ import gymnasium as gym
28
+
17
29
  from tinybaselines import GaussianActorCritic, PPO, evaluate, learn
18
- from examples.lunar_lander.config import OBS_SCALE
19
- from examples.lunar_lander.train import make_env
20
30
 
21
- env = make_env(4, asynchronous=False)
22
31
  policy = GaussianActorCritic(state_dim=8, action_dim=2, hidden_dim=128,
23
- actor_layers=4, critic_layers=4, observation_scale=OBS_SCALE)
32
+ actor_layers=4, critic_layers=4,
33
+ observation_scale=[10, 6.666, 5, 7.5, 1, 2.5, 1, 1])
24
34
  ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95,
25
35
  K_epochs=20, eps_clip=0.2, batch_size=10_000, vf_coef=1.0, entropy_coef=0.001)
26
36
 
27
- for step in learn(env, ppo, total_timesteps=2_000_000, rollout_steps=20_000):
28
- print(step.timesteps, step.rollout_seconds, step.update_seconds)
29
-
30
- ppo.save('ppo.safetensors')
31
- ppo = PPO.load('ppo.safetensors')
32
- policy = ppo.actor_critic
33
- observations, _ = env.reset()
34
- actions, raw_actions = policy.act(observations, deterministic=True)
35
- env.close()
37
+ with closing(gym.vector.SyncVectorEnv(
38
+ [lambda: gym.make('LunarLander-v3', continuous=True)] * 4,
39
+ autoreset_mode=gym.vector.AutoresetMode.SAME_STEP,
40
+ )) as env:
41
+ for step in learn(env, ppo, total_timesteps=20_000, rollout_steps=20_000):
42
+ print(step.timesteps, step.rollout_seconds, step.update_seconds)
43
+ ppo.save('ppo.safetensors')
44
+ policy = PPO.load('ppo.safetensors').actor_critic
45
+ print(mean(evaluate(env, policy, num_episodes=10)))
46
+ ```
36
47
 
37
- eval_env = make_env(4, asynchronous=False)
38
- returns = evaluate(eval_env, policy, num_episodes=100)
39
- eval_env.close()
40
- print(mean(returns))
48
+ ```sh
49
+ DEV=CPU BEAM=0 python train_lander.py
41
50
  ```
42
51
 
43
52
  `total_timesteps` and `rollout_steps` count transitions across all environments.
@@ -51,18 +60,32 @@ actions by default; `deterministic=True` uses clipped policy means.
51
60
  The collector uses Gymnasium vector environments with same-step autoreset.
52
61
  For time-limit bootstrapping, the collector reads pre-reset observations from `infos['final_obs']`.
53
62
 
54
- PPO checkpoints contain the policy, observation scale, Adam state, and PPO settings.
63
+ PPO checkpoints contain the policy, Adam state, and PPO settings.
55
64
  Continue training by passing the loaded PPO to `learn` with a new transition budget.
56
65
  Each `learn` call starts fresh episodes and a new transition counter.
57
- `policy.save(path)` exports a policy for inference.
66
+ `GaussianActorCritic.save(path)` exports a Gaussian policy for inference.
67
+
68
+ ## Repository examples
58
69
 
59
- The full LunarLander trainer, evaluation, and PyTorch reference live in
60
- `examples/lunar_lander`.
70
+ From a checkout, run the full LunarLander trainer:
61
71
 
62
72
  ```sh
73
+ pip install -e . -r examples/lunar_lander/requirements.txt
63
74
  python -m examples.lunar_lander.train
64
75
  ```
65
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
+
66
89
  Pendulum uses three observations and one action. Its example rescales policy actions
67
90
  from `[-1, 1]` to torque in `[-2, 2]`.
68
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()
@@ -12,7 +12,7 @@ from tinygrad import Tensor, TinyJit, nn, Context
12
12
  from tqdm import trange
13
13
  from .config import ENV_NAME, OBS_SCALE
14
14
  from tinybaselines import GaussianActorCritic, PPO, evaluate, learn
15
- from tinybaselines.ppo import clip_grad_norm, normal_log_prob
15
+ from tinybaselines.ppo import clip_grad_norm
16
16
  from tinybaselines.rollout import RolloutBatch, RolloutState, rollout, track_episode_returns
17
17
 
18
18
  warnings.filterwarnings('ignore', message='pkg_resources is deprecated')
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tinybaselines"
7
- version = "0.1.0a1"
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,53 @@ 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
+
38
+ def test_ppo_gets_actor_parameters_and_device_from_policy(self):
39
+ model = GaussianActorCritic(3, 1, 4, 1, 1)
40
+ weight = model.actor.layers[0].weight
41
+ policy = SimpleNamespace(actor_parameters=[weight], device=weight.device, critic=model.critic)
42
+ ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95, K_epochs=1,
43
+ eps_clip=0.2, batch_size=4, vf_coef=1.0, entropy_coef=0.0)
44
+ self.assertEqual(ppo.device, policy.device)
45
+ self.assertIs(ppo.pi_optimizer.params[0], weight)
46
+
47
+ def test_rejects_nonpositive_update_settings(self):
48
+ policy = GaussianActorCritic(3, 1, 4, 1, 1)
49
+ settings = dict(pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95, K_epochs=1,
50
+ eps_clip=0.2, batch_size=4, vf_coef=1.0, entropy_coef=0.0)
51
+ for name in ('K_epochs', 'batch_size'):
52
+ for value in (0, -1):
53
+ with self.subTest(name=name, value=value), self.assertRaisesRegex(ValueError, f'{name} must be positive'):
54
+ PPO(policy, **(settings | {name: value}))
55
+
15
56
  def test_terminals_do_not_require_final_observations(self):
16
57
  observations = np.zeros((2, 3), np.float32)
17
58
  reset_observations = np.full((2, 3), 100, np.float32)
@@ -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))
@@ -5,6 +5,7 @@ import numpy as np
5
5
  import torch
6
6
 
7
7
  from examples.lunar_lander import train as lander, reference as reference_lander
8
+ from tinybaselines.policies import normal_log_prob
8
9
 
9
10
 
10
11
  class ParityTest(unittest.TestCase):
@@ -205,7 +206,7 @@ class ParityTest(unittest.TestCase):
205
206
  torch_mean = torch.tensor(means, requires_grad=True)
206
207
  torch_log_std = torch.tensor(log_std, requires_grad=True)
207
208
  with lander.Context(TRAINING=1):
208
- actual = lander.normal_log_prob(tiny_mean, tiny_log_std, lander.Tensor(actions))
209
+ actual = normal_log_prob(tiny_mean, tiny_log_std, lander.Tensor(actions))
209
210
  actual.sum().backward()
210
211
  expected = torch.distributions.Normal(torch_mean, torch_log_std.exp()).log_prob(torch.tensor(actions)).sum(-1)
211
212
  expected.sum().backward()
@@ -1,6 +1,7 @@
1
1
  from types import SimpleNamespace
2
2
  import unittest
3
3
 
4
+ import gymnasium as gym
4
5
  import numpy as np
5
6
 
6
7
  from examples.pendulum.env import make_env
@@ -8,6 +9,14 @@ from tinybaselines import GaussianActorCritic, PPO, learn, rollout
8
9
 
9
10
 
10
11
  class PendulumTest(unittest.TestCase):
12
+ def test_rejects_default_vector_autoreset(self):
13
+ env = gym.vector.SyncVectorEnv([lambda: gym.make('Pendulum-v1')] * 4)
14
+ self.addCleanup(env.close)
15
+ actions = np.zeros((4, 1), np.float32)
16
+ policy = SimpleNamespace(act=lambda obs, deterministic: (actions, actions))
17
+ with self.assertRaisesRegex(ValueError, 'autoreset_mode=SAME_STEP'):
18
+ rollout(env, policy, num_steps=8)
19
+
11
20
  def test_normalized_actions_cover_torque_range(self):
12
21
  env = make_env(4)
13
22
  try:
@@ -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
@@ -3,6 +3,14 @@ import json
3
3
  import numpy as np
4
4
  from tinygrad import Tensor, TinyJit, nn
5
5
 
6
+ LOG_2PI = float(np.log(2.0 * np.pi))
7
+ NORMAL_ENTROPY_CONSTANT = float(0.5 * np.log(2.0 * np.pi * np.e))
8
+
9
+
10
+ def normal_log_prob(action_mean, log_std, actions):
11
+ normalized = (actions - action_mean) / log_std.exp()
12
+ return (-0.5 * (normalized.square() + 2.0 * log_std + LOG_2PI)).sum(axis=-1)
13
+
6
14
 
7
15
  class MLP:
8
16
  def __init__(self, input_dim, output_dim, hidden_dim, layer_count):
@@ -17,6 +25,8 @@ class MLP:
17
25
 
18
26
 
19
27
  class GaussianActorCritic:
28
+ _checkpoint_type = 'gaussian'
29
+
20
30
  def __init__(self, state_dim, action_dim, hidden_dim, actor_layers, critic_layers, observation_scale=None):
21
31
  self.config = dict(state_dim=state_dim, action_dim=action_dim, hidden_dim=hidden_dim,
22
32
  actor_layers=actor_layers, critic_layers=critic_layers)
@@ -31,12 +41,27 @@ class GaussianActorCritic:
31
41
  self.actor_jit = TinyJit(lambda state: self.actor(state).realize())
32
42
  self.refresh_inference()
33
43
 
44
+ @property
45
+ def actor_parameters(self):
46
+ return nn.state.get_parameters(self.actor) + [self.log_std]
47
+
48
+ @property
49
+ def device(self):
50
+ return self.log_std.device
51
+
34
52
  def __call__(self, state):
35
53
  action_mean = self.actor(state)
36
54
  log_std = self.log_std.clip(-5, 2)
37
55
  value = self.critic(state)
38
56
  return action_mean, log_std, value
39
57
 
58
+ def evaluate_actions(self, states, actions):
59
+ """Return values, action log probabilities, and mean entropy for preprocessed states."""
60
+ means, log_std, values = self(states)
61
+ log_probs = normal_log_prob(means, log_std, actions)
62
+ entropy = (log_std + NORMAL_ENTROPY_CONSTANT).sum()
63
+ return values, log_probs, entropy
64
+
40
65
  def preprocess(self, observations):
41
66
  return Tensor(observations.astype(np.float32), device=self.observation_scale.device) * self.observation_scale
42
67
 
@@ -73,3 +98,50 @@ class GaussianActorCritic:
73
98
  self.action_std = np.exp(self.log_std.clip(-5, 2).numpy())
74
99
  if not np.isfinite(self.action_std).all():
75
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,15 +5,9 @@ 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
9
 
10
- LOG_2PI = float(np.log(2.0 * np.pi))
11
- NORMAL_ENTROPY_CONSTANT = float(0.5 * np.log(2.0 * np.pi * np.e))
12
-
13
-
14
- def normal_log_prob(action_mean, log_std, actions):
15
- normalized = (actions - action_mean) / log_std.exp()
16
- return (-0.5 * (normalized.square() + 2.0 * log_std + LOG_2PI)).sum(axis=-1)
10
+ _CHECKPOINT_POLICIES = {policy._checkpoint_type: policy for policy in (GaussianActorCritic, CategoricalActorCritic)}
17
11
 
18
12
 
19
13
  def clip_grad_norm(parameters, max_norm):
@@ -29,9 +23,13 @@ def clip_grad_norm(parameters, max_norm):
29
23
 
30
24
  class PPO:
31
25
  def __init__(self, actor_critic, pi_lr, vf_lr, gamma, lamda, K_epochs, eps_clip, batch_size, vf_coef, entropy_coef):
26
+ if K_epochs <= 0:
27
+ raise ValueError('K_epochs must be positive')
28
+ if batch_size <= 0:
29
+ raise ValueError('batch_size must be positive')
32
30
  self.actor_critic = actor_critic
33
- self.device = actor_critic.log_std.device
34
- self.actor_parameters = nn.state.get_parameters(actor_critic.actor) + [actor_critic.log_std]
31
+ self.device = actor_critic.device
32
+ self.actor_parameters = actor_critic.actor_parameters
35
33
  self.critic_parameters = nn.state.get_parameters(actor_critic.critic)
36
34
  self.pi_optimizer = nn.optim.Adam(self.actor_parameters, lr=pi_lr)
37
35
  self.vf_optimizer = nn.optim.Adam(self.critic_parameters, lr=vf_lr)
@@ -57,6 +55,7 @@ class PPO:
57
55
  def save(self, path):
58
56
  path = Path(path)
59
57
  metadata = dict(format='tinybaselines.ppo.v1', policy_config=json.dumps(self.actor_critic.config),
58
+ policy_type=self.actor_critic._checkpoint_type,
60
59
  ppo_config=json.dumps(self.config))
61
60
  with TemporaryDirectory(dir=path.parent) as directory:
62
61
  temporary = Path(directory) / path.name
@@ -69,7 +68,11 @@ class PPO:
69
68
  metadata = header.get('__metadata__', {})
70
69
  if metadata.get('format') != 'tinybaselines.ppo.v1':
71
70
  raise ValueError('unsupported PPO checkpoint format')
72
- 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']))
73
76
  ppo = cls(policy, **json.loads(metadata['ppo_config']))
74
77
  nn.state.load_state_dict(ppo.state_dict(), nn.state.safe_load(path), verbose=False)
75
78
  policy.refresh_inference()
@@ -89,14 +92,12 @@ class PPO:
89
92
  return advantages.reshape(-1), returns.reshape(-1)
90
93
 
91
94
  def compute_loss(self, batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns):
92
- action_means, log_std, state_values = self.actor_critic(batch_states)
93
- action_logprobs = normal_log_prob(action_means, log_std, batch_actions)
95
+ state_values, action_logprobs, entropy = self.actor_critic.evaluate_actions(batch_states, batch_actions)
94
96
  ratios = (action_logprobs - batch_logprobs).exp()
95
97
  unclipped = ratios * batch_advantages
96
98
  clipped = ratios.clip(1 - self.eps_clip, 1 + self.eps_clip) * batch_advantages
97
99
  actor_loss = -unclipped.minimum(clipped).mean()
98
100
  critic_loss = (state_values.squeeze(-1) - batch_returns).square().mean()
99
- entropy = (log_std + NORMAL_ENTROPY_CONSTANT).sum()
100
101
  return actor_loss + self.vf_coef * critic_loss - self.entropy_coef * entropy
101
102
 
102
103
  def train_step(self, batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns):
@@ -121,10 +122,10 @@ class PPO:
121
122
  def prepare(self, batch):
122
123
  T, N = batch.rewards.shape
123
124
  obs_flat = self.actor_critic.preprocess(batch.observations.reshape(T * N, -1))
124
- 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:])
125
126
  raw_act_flat = Tensor(raw_act_np, device=self.device)
126
- old_mean, old_log_std, old_value = self.actor_critic(obs_flat)
127
- old_logprobs_np = normal_log_prob(old_mean, old_log_std, raw_act_flat).numpy()
127
+ old_value, old_logprobs, _ = self.actor_critic.evaluate_actions(obs_flat, raw_act_flat)
128
+ old_logprobs_np = old_logprobs.numpy()
128
129
  old_values_np = old_value.numpy().reshape(T, N)
129
130
  next_obs_t = self.actor_critic.preprocess(batch.next_observations.reshape(T * N, -1))
130
131
  next_values_np = self.actor_critic.critic(next_obs_t).numpy().reshape(T, N)
@@ -33,6 +33,10 @@ def rollout(env, actor_critic, num_steps=None, num_episodes=None, deterministic=
33
33
  if num_episodes is not None and num_episodes <= 0:
34
34
  raise ValueError('num_episodes must be positive')
35
35
 
36
+ mode = (getattr(env, 'metadata', None) or {}).get('autoreset_mode')
37
+ if getattr(mode, 'value', mode) not in (None, 'SameStep'):
38
+ raise ValueError(f'Expected autoreset_mode=SAME_STEP, got {mode!r}')
39
+
36
40
  N = env.num_envs
37
41
  if state is None:
38
42
  state = RolloutState(env.reset()[0], np.zeros(N))
@@ -54,7 +58,7 @@ def rollout(env, actor_critic, num_steps=None, num_episodes=None, deterministic=
54
58
  actions, raw_actions = actor_critic.act(states, deterministic=deterministic)
55
59
  if collect:
56
60
  if t == 0:
57
- raw_act = np.empty((T, *raw_actions.shape), dtype=np.float32)
61
+ raw_act = np.empty((T, *raw_actions.shape), dtype=raw_actions.dtype)
58
62
  obs[t], raw_act[t] = states, raw_actions
59
63
 
60
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.0a1
3
+ Version: 0.1.0a3
4
4
  Summary: PPO in tinygrad.
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.11
@@ -12,44 +12,53 @@ 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
- Python 3.11 and clang.
17
+ ## LunarLander from PyPI
18
+
19
+ Use Python 3.11 and clang. The library install does not include Gymnasium.
18
20
 
19
21
  ```sh
20
22
  python3.11 -m venv .venv
21
23
  source .venv/bin/activate
22
- pip install -e . -r examples/lunar_lander/requirements.txt
24
+ python -m pip install tinybaselines==0.1.0a3
23
25
  ```
24
26
 
25
- Train, save, and load.
27
+ Install Gymnasium and Box2D to run this example:
28
+
29
+ ```sh
30
+ python -m pip install 'gymnasium[box2d]==1.4.0'
31
+ ```
32
+
33
+ Save this as `train_lander.py`. It runs one PPO update and evaluates the reloaded checkpoint.
26
34
 
27
35
  ```python
36
+ from contextlib import closing
28
37
  from statistics import mean
38
+
39
+ import gymnasium as gym
40
+
29
41
  from tinybaselines import GaussianActorCritic, PPO, evaluate, learn
30
- from examples.lunar_lander.config import OBS_SCALE
31
- from examples.lunar_lander.train import make_env
32
42
 
33
- env = make_env(4, asynchronous=False)
34
43
  policy = GaussianActorCritic(state_dim=8, action_dim=2, hidden_dim=128,
35
- actor_layers=4, critic_layers=4, observation_scale=OBS_SCALE)
44
+ actor_layers=4, critic_layers=4,
45
+ observation_scale=[10, 6.666, 5, 7.5, 1, 2.5, 1, 1])
36
46
  ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95,
37
47
  K_epochs=20, eps_clip=0.2, batch_size=10_000, vf_coef=1.0, entropy_coef=0.001)
38
48
 
39
- for step in learn(env, ppo, total_timesteps=2_000_000, rollout_steps=20_000):
40
- print(step.timesteps, step.rollout_seconds, step.update_seconds)
41
-
42
- ppo.save('ppo.safetensors')
43
- ppo = PPO.load('ppo.safetensors')
44
- policy = ppo.actor_critic
45
- observations, _ = env.reset()
46
- actions, raw_actions = policy.act(observations, deterministic=True)
47
- env.close()
49
+ with closing(gym.vector.SyncVectorEnv(
50
+ [lambda: gym.make('LunarLander-v3', continuous=True)] * 4,
51
+ autoreset_mode=gym.vector.AutoresetMode.SAME_STEP,
52
+ )) as env:
53
+ for step in learn(env, ppo, total_timesteps=20_000, rollout_steps=20_000):
54
+ print(step.timesteps, step.rollout_seconds, step.update_seconds)
55
+ ppo.save('ppo.safetensors')
56
+ policy = PPO.load('ppo.safetensors').actor_critic
57
+ print(mean(evaluate(env, policy, num_episodes=10)))
58
+ ```
48
59
 
49
- eval_env = make_env(4, asynchronous=False)
50
- returns = evaluate(eval_env, policy, num_episodes=100)
51
- eval_env.close()
52
- print(mean(returns))
60
+ ```sh
61
+ DEV=CPU BEAM=0 python train_lander.py
53
62
  ```
54
63
 
55
64
  `total_timesteps` and `rollout_steps` count transitions across all environments.
@@ -63,18 +72,32 @@ actions by default; `deterministic=True` uses clipped policy means.
63
72
  The collector uses Gymnasium vector environments with same-step autoreset.
64
73
  For time-limit bootstrapping, the collector reads pre-reset observations from `infos['final_obs']`.
65
74
 
66
- PPO checkpoints contain the policy, observation scale, Adam state, and PPO settings.
75
+ PPO checkpoints contain the policy, Adam state, and PPO settings.
67
76
  Continue training by passing the loaded PPO to `learn` with a new transition budget.
68
77
  Each `learn` call starts fresh episodes and a new transition counter.
69
- `policy.save(path)` exports a policy for inference.
78
+ `GaussianActorCritic.save(path)` exports a Gaussian policy for inference.
79
+
80
+ ## Repository examples
70
81
 
71
- The full LunarLander trainer, evaluation, and PyTorch reference live in
72
- `examples/lunar_lander`.
82
+ From a checkout, run the full LunarLander trainer:
73
83
 
74
84
  ```sh
85
+ pip install -e . -r examples/lunar_lander/requirements.txt
75
86
  python -m examples.lunar_lander.train
76
87
  ```
77
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
+
78
101
  Pendulum uses three observations and one action. Its example rescales policy actions
79
102
  from `[-1, 1]` to torque in `[-2, 2]`.
80
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