inductive-mlxrl 0.1.1__tar.gz → 0.2.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 (93) hide show
  1. inductive_mlxrl-0.2.2/CHANGELOG.md +87 -0
  2. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/CONTRIBUTING.md +26 -4
  3. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/DESIGN.md +38 -4
  4. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/PKG-INFO +92 -13
  5. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/README.md +88 -9
  6. inductive_mlxrl-0.2.2/examples/qwen35_9b_g2_checkpoint.toml +29 -0
  7. inductive_mlxrl-0.2.2/examples/qwen3_0_6b_gigpo_recurring.toml +33 -0
  8. inductive_mlxrl-0.2.2/examples/qwen3_0_6b_grpo.toml +29 -0
  9. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/__init__.py +1 -2
  10. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/algo/__init__.py +12 -0
  11. inductive_mlxrl-0.2.2/mlxrl/algo/gigpo.py +202 -0
  12. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/algo/grpo.py +36 -16
  13. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/algorithm.py +41 -1
  14. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/cli.py +176 -2
  15. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/config.py +64 -7
  16. inductive_mlxrl-0.2.2/mlxrl/echo.py +43 -0
  17. inductive_mlxrl-0.2.2/mlxrl/env/__init__.py +27 -0
  18. inductive_mlxrl-0.2.2/mlxrl/env/base.py +75 -0
  19. inductive_mlxrl-0.2.2/mlxrl/env/reference.py +104 -0
  20. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/policy/__init__.py +18 -0
  21. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/policy/logprobs.py +98 -38
  22. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/policy/model.py +16 -1
  23. inductive_mlxrl-0.2.2/mlxrl/policy/trajectory_logprobs.py +239 -0
  24. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/rollout/__init__.py +10 -0
  25. inductive_mlxrl-0.2.2/mlxrl/rollout/agentic.py +396 -0
  26. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/rollout/optimized.py +78 -34
  27. inductive_mlxrl-0.2.2/mlxrl/train/__init__.py +18 -0
  28. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/train/grpo.py +99 -29
  29. inductive_mlxrl-0.2.2/mlxrl/train/reference.py +25 -0
  30. inductive_mlxrl-0.2.2/mlxrl/train/trajectory.py +557 -0
  31. inductive_mlxrl-0.2.2/mlxrl/trajectory.py +179 -0
  32. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/pyproject.toml +5 -4
  33. inductive_mlxrl-0.2.2/tests/test_agentic_env.py +98 -0
  34. inductive_mlxrl-0.2.2/tests/test_agentic_rollout.py +217 -0
  35. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/tests/test_cli.py +37 -1
  36. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/tests/test_config.py +78 -0
  37. inductive_mlxrl-0.2.2/tests/test_echo.py +329 -0
  38. inductive_mlxrl-0.2.2/tests/test_gigpo.py +141 -0
  39. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/tests/test_grpo.py +36 -2
  40. inductive_mlxrl-0.2.2/tests/test_optimized_rollout.py +181 -0
  41. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/tests/test_policy_logprobs.py +47 -0
  42. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/tests/test_train_grpo.py +130 -1
  43. inductive_mlxrl-0.2.2/tests/test_train_trajectory.py +286 -0
  44. inductive_mlxrl-0.2.2/tests/test_trajectory_logprobs.py +89 -0
  45. inductive_mlxrl-0.1.1/CHANGELOG.md +0 -37
  46. inductive_mlxrl-0.1.1/mlxrl/train/__init__.py +0 -5
  47. inductive_mlxrl-0.1.1/tests/test_optimized_rollout.py +0 -76
  48. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/.gitignore +0 -0
  49. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/LICENSE +0 -0
  50. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/THIRD_PARTY_LICENSES.md +0 -0
  51. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/__init__.py +0 -0
  52. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/audit_phase4_config.py +0 -0
  53. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/external_baseline_worker.py +0 -0
  54. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after.jsonl +0 -0
  55. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after.md +0 -0
  56. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after_final.jsonl +0 -0
  57. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after_final.md +0 -0
  58. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after_noprefixsync.jsonl +0 -0
  59. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after_noprefixsync.md +0 -0
  60. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_before.jsonl +0 -0
  61. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_before.md +0 -0
  62. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate2_unified_smoke.jsonl +0 -0
  63. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate2_unified_smoke.md +0 -0
  64. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_mlx_tune_sync_256.jsonl +0 -0
  65. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_mlx_tune_sync_256.md +0 -0
  66. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_sync_blocked.jsonl +0 -0
  67. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_sync_blocked.md +0 -0
  68. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_sync_smoke.jsonl +0 -0
  69. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_sync_smoke.md +0 -0
  70. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate4_config_audit.json +0 -0
  71. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate4_config_audit.md +0 -0
  72. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate4_sampler_smoke.jsonl +0 -0
  73. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate4_sampler_smoke.md +0 -0
  74. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate5_full_reconciled.jsonl +0 -0
  75. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate5_full_reconciled.md +0 -0
  76. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_external_smoke.jsonl +0 -0
  77. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_external_smoke.md +0 -0
  78. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_full_all_targets.jsonl +0 -0
  79. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_full_all_targets.md +0 -0
  80. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.jsonl +0 -0
  81. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.md +0 -0
  82. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_mlx_lm_g4_smoke.jsonl +0 -0
  83. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_mlx_lm_g4_smoke.md +0 -0
  84. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/benchmarks/run_phase4.py +0 -0
  85. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/data/__init__.py +0 -0
  86. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/data/gsm8k.py +0 -0
  87. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/data/rewards.py +0 -0
  88. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/py.typed +0 -0
  89. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/mlxrl/rollout/naive.py +0 -0
  90. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/tests/test_benchmark_metrics.py +0 -0
  91. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/tests/test_import_direction.py +0 -0
  92. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/tests/test_policy_model.py +0 -0
  93. {inductive_mlxrl-0.1.1 → inductive_mlxrl-0.2.2}/tests/test_rewards.py +0 -0
@@ -0,0 +1,87 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project uses semantic versioning once releases are cut.
7
+
8
+ ## [0.2.2] - 2026-06-25
9
+
10
+ ### Added
11
+
12
+ - ECHO world-modeling support for tagged multi-turn trajectories, composing an
13
+ independently normalized ECHO SFT loss with the existing GiGPO action loss.
14
+ - `echo_alpha` scheduling with constant and linear-taper-to-zero modes, plus
15
+ separate `loss_echo` and `echo_accuracy` diagnostics.
16
+ - Real-Qwen smoke coverage for tagged ECHO batches on Qwen3-0.6B and
17
+ Qwen3.5-9B MLX 4-bit models.
18
+
19
+ ## [0.2.1] - 2026-06-25
20
+
21
+ ### Fixed
22
+
23
+ - Keep model-backed agentic/GiGPO decode tokens rank-2 during KV-cache decode,
24
+ fixing real Qwen3 model rollouts.
25
+ - Use valid repo-relative output paths in the committed Qwen smoke configs.
26
+ - Preserve eval-mode old-policy/reference logprob prep while restoring caller
27
+ train mode afterward.
28
+
29
+ ## [0.2.0] - 2026-06-24
30
+
31
+ ### Added
32
+
33
+ - Multi-turn agentic rollout path with Gym-style `Environment` / `EnvFactory`
34
+ protocols and deterministic reference environments.
35
+ - Trajectory data model with action-token spans and full-forward trajectory
36
+ logprob gathering for 4-bit policy, old-policy, and reference semantics.
37
+ - GiGPO (`Group-in-Group Policy Optimization`) with episode-level and
38
+ anchor-state step-level advantages, plus an exact `omega=0` reduction gate.
39
+ - Trajectory batch builder and optimizer step that preserve the existing
40
+ single-turn GRPO-family APIs.
41
+ - Config-driven GiGPO training route with `max_turns`, `rollout_mode`,
42
+ `env_name`, `max_observation_len`, `gigpo_omega`, `gigpo_gamma`, and
43
+ normalization knobs.
44
+
45
+ ### Changed
46
+
47
+ - Extend the memory estimator to label multi-turn configs as estimated
48
+ OOM-risk shapes while preserving the measured v0.1 single-turn anchors.
49
+
50
+ ## [0.1.2] - 2026-06-24
51
+
52
+ ### Fixed
53
+
54
+ - Move the Python `<3.15` ceiling from package-level `Requires-Python` to the
55
+ MLX runtime dependency markers so universal resolvers can add the package to
56
+ projects with broad Python ranges while skipping unsupported MLX splits.
57
+
58
+ ## [0.1.1] - 2026-06-24
59
+
60
+ ### Fixed
61
+
62
+ - Mark `mlx` and `mlx-lm` runtime dependencies as macOS arm64-only and bound the
63
+ package to Python 3.11 through 3.14 so universal resolvers do not try to solve
64
+ unsupported MLX platform or Python splits.
65
+
66
+ ## [0.1.0] - 2026-06-24
67
+
68
+ ### Added
69
+
70
+ - Single-process MLX GRPO+QLoRA training path for Apple Silicon.
71
+ - Prefix-cached grouped rollout engine with compiled decode support.
72
+ - Adapter-disabled reference logprob path on one model object.
73
+ - Full-forward old-policy logprob recompute for 4-bit correctness.
74
+ - Algorithm protocol with GRPO, Dr. GRPO, DAPO, GSPO, and RLOO.
75
+ - DAPO `filter_batch` hook for dynamic zero-advantage group filtering.
76
+ - Per-layer MLX-LM gradient checkpointing for DeltaNet/linear-attention models.
77
+ - Micro-batched gradient accumulation for token-mean losses.
78
+ - Typed config schema and memory preflight helper calibrated to measured anchors.
79
+ - Phase 4 benchmark harness for `mlxrl`, `mlx-lm`, `mlx-tune`, and `mlx-lm-lora`.
80
+ - Linux CPU-safe CI for docs/config/import-direction checks.
81
+
82
+ ### Notes
83
+
84
+ - `mlxrl` is pre-1.0; public APIs may change while the correctness gates settle.
85
+ - Full MLX/Metal tests require Apple Silicon or a self-hosted Mac runner.
86
+ - Published on PyPI as `inductive-mlxrl`; the import package and CLI command
87
+ remain `mlxrl`.
@@ -20,7 +20,9 @@ estimator anchor tests are part of the contract.
20
20
 
21
21
  ## Adding An Algorithm
22
22
 
23
- Use `RLOOAlgorithm` in `mlxrl/algo/grpo.py` as the smallest template.
23
+ Use `RLOOAlgorithm` in `mlxrl/algo/grpo.py` as the smallest single-turn
24
+ template. Use `GiGPOAlgorithm` in `mlxrl/algo/gigpo.py` as the trajectory
25
+ template.
24
26
 
25
27
  Required steps:
26
28
 
@@ -35,6 +37,23 @@ Required steps:
35
37
  If an algorithm needs to drop or reshape examples before the loss, use the
36
38
  `filter_batch` hook. Do not special-case it in the trainer.
37
39
 
40
+ Trajectory algorithms implement `TrajectoryAlgorithm`, ship a hand-computed
41
+ multi-turn loss/gradient test, and prove their reduction to an existing
42
+ objective when a degenerate config should do so. They may consume trajectory
43
+ metadata such as env state ids, but `rollout/`, `policy/`, and `train/` still
44
+ must not import concrete algorithms.
45
+
46
+ ## Adding An Environment
47
+
48
+ External envs integrate through `EnvFactory(task, seed, group_index) ->
49
+ Environment`. Implement `reset()`, `step(action)`, `max_turns`, and
50
+ `state_id(observation)`. The env owns the anchor-state definition; the rollout
51
+ engine only records ids, and GiGPO groups them later.
52
+
53
+ Keep envs deterministic under the provided seed when possible. Add a tiny test
54
+ that resets, steps, returns stable state ids, and exercises truncation or
55
+ termination. Do not add omega gym itself to this repo.
56
+
38
57
  ## MLX And CI
39
58
 
40
59
  GitHub-hosted Linux runners cannot run the MLX/Metal path. Linux CI gates the
@@ -47,9 +66,11 @@ would close this coverage gap.
47
66
 
48
67
  ## Package Compatibility
49
68
 
50
- The PyPI distribution supports Python 3.11 through 3.14. Keep the `mlx` and
51
- `mlx-lm` runtime dependencies marked for macOS arm64 so universal resolvers do
52
- not try to solve MLX wheels for unsupported platforms.
69
+ The PyPI distribution requires Python 3.11+. The MLX runtime path is supported
70
+ on Apple Silicon with Python 3.11 through 3.14. Keep the `mlx` and `mlx-lm`
71
+ runtime dependencies marked for macOS arm64 and Python `<3.15` so universal
72
+ resolvers do not try to solve MLX wheels for unsupported platform or Python
73
+ splits.
53
74
 
54
75
  ## Release Cycle
55
76
 
@@ -103,6 +124,7 @@ In scope:
103
124
  - single-process Apple Silicon RL post-training;
104
125
  - QLoRA on local MLX LLMs;
105
126
  - critic-free on-policy algorithms;
127
+ - multi-turn agentic environments through the small env protocol;
106
128
  - memory-conscious rollout and training paths.
107
129
 
108
130
  Out of scope:
@@ -7,10 +7,10 @@ fast enough that rollout is the main problem and the loss stays thin.
7
7
  ## Critic-Free By Design
8
8
 
9
9
  The library is intentionally limited to rollout-based policy-gradient methods:
10
- GRPO, Dr. GRPO, DAPO, GSPO, and RLOO. PPO is out of scope because it needs a
11
- critic/value model path, a different forward pass, and a different memory
12
- profile. DPO and ORPO are also out of scope because they are offline preference
13
- objectives rather than on-policy rollout algorithms.
10
+ GRPO, Dr. GRPO, DAPO, GSPO, RLOO, and GiGPO. PPO is out of scope because it
11
+ needs a critic/value model path, a different forward pass, and a different
12
+ memory profile. DPO and ORPO are also out of scope because they are offline
13
+ preference objectives rather than on-policy rollout algorithms.
14
14
 
15
15
  That boundary matters. `mlxrl` keeps one policy model object in memory, attaches
16
16
  QLoRA adapters to it, and computes reference logprobs by disabling those
@@ -32,6 +32,24 @@ stable while algorithms change. DAPO's dynamic sampling is the proof that the
32
32
  interface is more general than "GRPO with renamed constants": it drops
33
33
  zero-advantage groups through `filter_batch` without special trainer branches.
34
34
 
35
+ Multi-turn algorithms use the sibling `TrajectoryAlgorithm` protocol rather
36
+ than widening the single-turn `Algorithm` contract. GiGPO is the first
37
+ implementation: the rollout engine records env-provided state ids, while the
38
+ algorithm owns anchor-state grouping and credit assignment.
39
+
40
+ ## Agentic Environment Seam
41
+
42
+ Agentic rollout is built around `EnvFactory(task, seed, group_index) ->
43
+ Environment`. An environment exposes `reset()`, `step(action)`, `max_turns`,
44
+ and `state_id(observation)`. The state id is deliberately supplied by the env:
45
+ omega gym, WebShop, ALFWorld, or a toy env each decides what counts as the same
46
+ anchor state.
47
+
48
+ The engine records observations, actions, rewards, state ids, full token
49
+ history, and action-token spans. It does not compute anchor groups. This keeps
50
+ rollout reusable for future trajectory algorithms while GiGPO performs the
51
+ group-in-group advantage calculation offline from the same trajectories.
52
+
35
53
  ## Correctness Gates
36
54
 
37
55
  Speed changes are allowed only behind equivalence gates. The original Phase 1
@@ -40,6 +58,11 @@ token-for-token at fixed seed and match loss within tolerance. The protocol
40
58
  refactor was checked against the pre-refactor GRPO and Dr. GRPO losses with
41
59
  zero loss and adapter-gradient difference.
42
60
 
61
+ GiGPO adds two reduction gates: `omega=0` must match episode-level GRPO over the
62
+ same trajectories, and single-turn one-step trajectories with `omega=0` must
63
+ match the v0.1 GRPO path through loss and adapter gradient. Toy multi-turn
64
+ cases also carry hand-computed advantage, loss, and gradient checks.
65
+
43
66
  Old-policy changes use a stronger gate than naive equality. At 4-bit, the
44
67
  rollout-time cached decode realization and the full-forward realization are not
45
68
  numerically identical. Stored rollout logprobs are attractive because they are
@@ -56,6 +79,11 @@ for gradient-bearing or importance-weighting quantities. Anything used in the
56
79
  loss denominator, KL, or adapter gradient must come from the full-forward path
57
80
  unless a dedicated stability gate proves otherwise.
58
81
 
82
+ For multi-turn trajectories, this means policy, old-policy, and reference
83
+ logprobs are gathered from a full forward over the complete token history, then
84
+ masked down to action-token spans only. Rollout-time cached logprobs remain
85
+ diagnostic data, not training data.
86
+
59
87
  ## Memory As A First-Class Constraint
60
88
 
61
89
  Apple Silicon's unified memory is the deployment target, not an afterthought.
@@ -70,3 +98,9 @@ anchors and labels long-sequence uncheckpointed hybrid configs as OOM-risk
70
98
  estimates, not measurements. Its purpose is to nudge users toward the knob that
71
99
  will most likely make a run fit: enable checkpointing, reduce G, then reduce
72
100
  completion length.
101
+
102
+ Multi-turn configs add trajectory length as `prompt + turns * action_tokens +
103
+ (turns - 1) * observation_tokens`. Parallel-per-turn rollout keeps G
104
+ per-trajectory cache/state objects resident; sequential mode is the lower-memory
105
+ fallback. Until more measured anchors exist, multi-turn estimates are labeled as
106
+ OOM-risk estimates rather than confident measurements.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: inductive-mlxrl
3
- Version: 0.1.1
3
+ Version: 0.2.2
4
4
  Summary: Small single-process RL post-training for LLMs on Apple Silicon with MLX.
5
5
  Project-URL: Homepage, https://github.com/inductiveML/mlxrl
6
6
  Project-URL: Repository, https://github.com/inductiveML/mlxrl
@@ -18,23 +18,25 @@ Classifier: Programming Language :: Python :: 3.12
18
18
  Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Programming Language :: Python :: 3.14
20
20
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
- Requires-Python: <3.15,>=3.11
22
- Requires-Dist: mlx-lm>=0.31.3; platform_system == 'Darwin' and platform_machine == 'arm64'
23
- Requires-Dist: mlx>=0.31.2; platform_system == 'Darwin' and platform_machine == 'arm64'
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: mlx-lm>=0.31.3; python_version < '3.15' and platform_system == 'Darwin' and platform_machine == 'arm64'
23
+ Requires-Dist: mlx>=0.31.2; python_version < '3.15' and platform_system == 'Darwin' and platform_machine == 'arm64'
24
24
  Description-Content-Type: text/markdown
25
25
 
26
26
  # mlxrl
27
27
 
28
- Fast on-policy MLX RL for Apple Silicon; not a general RL framework, not
29
- preference tuning, and not distributed training.
28
+ Fast on-policy MLX RL for Apple Silicon, including single-turn GRPO-family RL
29
+ and multi-turn GiGPO; not a general RL framework, not preference tuning, and
30
+ not distributed training.
30
31
 
31
32
  `mlxrl` is a small, single-process RL post-training library for LLMs on Apple
32
33
  Silicon. It is built around one idea: GRPO on MLX should be a fast batched
33
34
  rollout path with a thin loss and optimizer step on top, not a framework.
34
35
 
35
- The current implementation targets QLoRA GRPO on local 4-bit MLX models. It
36
- reuses `mlx-lm` model loading, LoRA layers, KV caches, and sampling utilities,
37
- and keeps generation and training in one Python process with one model object.
36
+ The current implementation targets QLoRA GRPO/GiGPO on local 4-bit MLX models.
37
+ It reuses `mlx-lm` model loading, LoRA layers, KV caches, and sampling
38
+ utilities, and keeps generation and training in one Python process with one
39
+ model object.
38
40
 
39
41
  `mlxrl` is pre-1.0. The correctness gates are stable, but import APIs and config
40
42
  fields may change before a 1.0 release.
@@ -66,6 +68,9 @@ UV_CACHE_DIR=.uv-cache uv run mlxrl train \
66
68
  - Adapter-disabled reference policy on the same model object.
67
69
  - GRPO, Dr. GRPO, DAPO, and GSPO loss variants.
68
70
  - RLOO (REINFORCE Leave-One-Out) as a critic-free rollout objective.
71
+ - Multi-turn agentic rollout through an `Environment` / `EnvFactory` protocol.
72
+ - GiGPO with episode-level and anchor-state step-level advantages.
73
+ - Full-forward trajectory action-logprob recompute for 4-bit correctness.
69
74
  - QLoRA injection on dense and heterogeneous/hybrid layer stacks.
70
75
  - Qwen3.5-style hybrid support via MLX-LM auto LoRA targeting, including
71
76
  DeltaNet `linear_attn.in_proj_*` and dense attention `q/k/v/o_proj`.
@@ -101,10 +106,11 @@ Run commands through the local environment:
101
106
  UV_CACHE_DIR=.uv-cache uv run mlxrl --help
102
107
  ```
103
108
 
104
- Python 3.11 through 3.14 is supported. Runtime dependencies are intentionally
105
- small: `mlx` and `mlx-lm` on macOS arm64, matching the MLX wheel support window.
106
- Development dependencies include `pytest`, `ruff`, `pyright`, `mlx-tune`, and
107
- `mlx-lm-lora` for comparison benchmarks.
109
+ Python 3.11+ is required. The MLX runtime path is supported on Apple Silicon
110
+ with Python 3.11 through 3.14, matching the current MLX wheel support window.
111
+ Runtime dependencies are intentionally small: `mlx` and `mlx-lm` on macOS
112
+ arm64. Development dependencies include `pytest`, `ruff`, `pyright`,
113
+ `mlx-tune`, and `mlx-lm-lora` for comparison benchmarks.
108
114
  The PyPI distribution name is `inductive-mlxrl`; the import package and console
109
115
  script remain `mlxrl`.
110
116
 
@@ -158,6 +164,14 @@ UV_CACHE_DIR=.uv-cache uv run mlxrl train \
158
164
  --available-memory-gb 48
159
165
  ```
160
166
 
167
+ Reference multi-turn GiGPO run against the built-in recurring-state env:
168
+
169
+ ```bash
170
+ UV_CACHE_DIR=.uv-cache uv run mlxrl train \
171
+ --config examples/qwen3_0_6b_gigpo_recurring.toml \
172
+ --available-memory-gb 48
173
+ ```
174
+
161
175
  The config schema validates model id, quant bits, group size, completion/prompt
162
176
  lengths, checkpointing granularity, `iogpu.wired_limit_mb`, optimizer settings,
163
177
  algorithm hyperparameters, KL beta, and seed before a model is loaded. CLI
@@ -242,11 +256,71 @@ metrics = optimizer_step(
242
256
  )
243
257
  ```
244
258
 
259
+ Multi-turn GiGPO uses the additive trajectory path:
260
+
261
+ ```python
262
+ from mlxrl.algo import GiGPOAlgorithm
263
+ from mlxrl.env import RecurringStateTextEnv
264
+ from mlxrl.rollout import generate_agentic_trajectories
265
+ from mlxrl.train import batch_from_trajectories, optimizer_step_trajectory
266
+
267
+ algorithm = GiGPOAlgorithm(omega=1.0, gamma=1.0)
268
+ trajectories = generate_agentic_trajectories(
269
+ model=model,
270
+ tokenizer=tokenizer,
271
+ env_factory=lambda task, seed, group: RecurringStateTextEnv(
272
+ task=str(task),
273
+ max_turns=3,
274
+ ),
275
+ tasks=["finish the task"],
276
+ group_size=4,
277
+ sampling=sampling,
278
+ rollout_mode="parallel_per_turn",
279
+ )
280
+ batch = batch_from_trajectories(
281
+ model=model,
282
+ trajectories=trajectories,
283
+ group_size=4,
284
+ pad_token_id=pad_token_id,
285
+ algorithm=algorithm,
286
+ compute_reference=beta != 0.0,
287
+ )
288
+ metrics = optimizer_step_trajectory(
289
+ model=model,
290
+ optimizer=optimizer,
291
+ batch=batch,
292
+ beta=beta,
293
+ pad_token_id=pad_token_id,
294
+ algorithm=algorithm,
295
+ )
296
+ ```
297
+
245
298
  `micro_batch_size=0` keeps the original whole-batch path. Micro-batching is
246
299
  currently exact for token-mean policy losses: base GRPO, DAPO, GSPO token mode,
247
300
  RLOO, and Dr. GRPO with `loss_reduction="token_mean"`. Sequence-reduced losses
248
301
  should keep `micro_batch_size=0`.
249
302
 
303
+ ## ECHO World Modeling
304
+
305
+ ECHO is an additive GiGPO trainer feature for tagged multi-turn trajectories.
306
+ Producer code may attach `token_roles` to each `Trajectory`, aligned to
307
+ `full_token_ids`: `0 = MASKED`, `1 = ACTION`, `2 = ECHO`. ACTION tokens keep
308
+ the normal GiGPO policy-gradient loss. ECHO tokens add an independently
309
+ normalized SFT term, `alpha * NLL`, in the same forward/backward pass.
310
+
311
+ `echo_alpha` defaults to `0.0`, which leaves current GiGPO behavior untouched.
312
+ For early world-modeling, start small, roughly `0.005` to `0.05`, and consider
313
+ `echo_schedule = "linear_taper_to_zero"` with `echo_taper_steps = N` so training
314
+ returns to pure RL. `alpha` is model-size-sensitive; too-high values can
315
+ overfit tool responses or collapse RL.
316
+
317
+ ECHO is content-agnostic by design. `mlxrl` never inspects token text to decide
318
+ what is an environment or tool response. The rollout producer owns the tags:
319
+ MASKED tokens have zero gradient, ACTION tokens receive the RL advantage, and
320
+ ECHO tokens receive only the SFT loss with no KL penalty or importance ratio.
321
+ The trainer logs `loss_echo` and token-level `echo_acc` separately from the
322
+ ACTION loss.
323
+
250
324
  ## Policy Semantics
251
325
 
252
326
  - The base model is frozen before LoRA injection.
@@ -277,6 +351,7 @@ import concrete algorithm implementations.
277
351
  | DAPO | asymmetric low/high clipping plus optional dynamic zero-advantage group filtering |
278
352
  | GSPO | sequence-level, length-normalized importance ratio and clipping |
279
353
  | RLOO | leave-one-out group baseline, no critic, no std-normalized advantage |
354
+ | GiGPO | multi-turn episode advantage plus anchor-state step advantage |
280
355
 
281
356
  ## Memory Preflight
282
357
 
@@ -295,6 +370,9 @@ Qwen3.5-9B/G2/seq609/per-layer-checkpointed, `45.9 GB` for
295
370
  Qwen3.5-9B/G4/seq609/per-layer-checkpointed, and `36 GB` for
296
371
  Qwen3.5-9B/G2/seq128/no-checkpoint. For hybrid 9B no-checkpoint long-sequence
297
372
  configs, it reports an OOM-risk lower bound rather than a fake precise peak.
373
+ Multi-turn configs include `max_turns` and `max_observation_len` in the
374
+ estimated trajectory length and are labeled as estimated OOM-risk shapes until
375
+ new measured anchors are added.
298
376
  For an obviously too-large Qwen3.5-9B/G8/prompt97/T512/no-checkpoint config on
299
377
  48 GB, it flags the run and suggests the measured-boundary fallback around
300
378
  G4/T512/checkpointed.
@@ -374,6 +452,7 @@ updates, or benchmark timing boundaries.
374
452
  ```text
375
453
  mlxrl/
376
454
  rollout/ # batched group generation
455
+ env/ # agentic environment protocols and reference envs
377
456
  policy/ # model loading, LoRA setup, logprob passes
378
457
  algo/ # GRPO-family advantages and losses
379
458
  train/ # value_and_grad and optimizer integration
@@ -1,15 +1,17 @@
1
1
  # mlxrl
2
2
 
3
- Fast on-policy MLX RL for Apple Silicon; not a general RL framework, not
4
- preference tuning, and not distributed training.
3
+ Fast on-policy MLX RL for Apple Silicon, including single-turn GRPO-family RL
4
+ and multi-turn GiGPO; not a general RL framework, not preference tuning, and
5
+ not distributed training.
5
6
 
6
7
  `mlxrl` is a small, single-process RL post-training library for LLMs on Apple
7
8
  Silicon. It is built around one idea: GRPO on MLX should be a fast batched
8
9
  rollout path with a thin loss and optimizer step on top, not a framework.
9
10
 
10
- The current implementation targets QLoRA GRPO on local 4-bit MLX models. It
11
- reuses `mlx-lm` model loading, LoRA layers, KV caches, and sampling utilities,
12
- and keeps generation and training in one Python process with one model object.
11
+ The current implementation targets QLoRA GRPO/GiGPO on local 4-bit MLX models.
12
+ It reuses `mlx-lm` model loading, LoRA layers, KV caches, and sampling
13
+ utilities, and keeps generation and training in one Python process with one
14
+ model object.
13
15
 
14
16
  `mlxrl` is pre-1.0. The correctness gates are stable, but import APIs and config
15
17
  fields may change before a 1.0 release.
@@ -41,6 +43,9 @@ UV_CACHE_DIR=.uv-cache uv run mlxrl train \
41
43
  - Adapter-disabled reference policy on the same model object.
42
44
  - GRPO, Dr. GRPO, DAPO, and GSPO loss variants.
43
45
  - RLOO (REINFORCE Leave-One-Out) as a critic-free rollout objective.
46
+ - Multi-turn agentic rollout through an `Environment` / `EnvFactory` protocol.
47
+ - GiGPO with episode-level and anchor-state step-level advantages.
48
+ - Full-forward trajectory action-logprob recompute for 4-bit correctness.
44
49
  - QLoRA injection on dense and heterogeneous/hybrid layer stacks.
45
50
  - Qwen3.5-style hybrid support via MLX-LM auto LoRA targeting, including
46
51
  DeltaNet `linear_attn.in_proj_*` and dense attention `q/k/v/o_proj`.
@@ -76,10 +81,11 @@ Run commands through the local environment:
76
81
  UV_CACHE_DIR=.uv-cache uv run mlxrl --help
77
82
  ```
78
83
 
79
- Python 3.11 through 3.14 is supported. Runtime dependencies are intentionally
80
- small: `mlx` and `mlx-lm` on macOS arm64, matching the MLX wheel support window.
81
- Development dependencies include `pytest`, `ruff`, `pyright`, `mlx-tune`, and
82
- `mlx-lm-lora` for comparison benchmarks.
84
+ Python 3.11+ is required. The MLX runtime path is supported on Apple Silicon
85
+ with Python 3.11 through 3.14, matching the current MLX wheel support window.
86
+ Runtime dependencies are intentionally small: `mlx` and `mlx-lm` on macOS
87
+ arm64. Development dependencies include `pytest`, `ruff`, `pyright`,
88
+ `mlx-tune`, and `mlx-lm-lora` for comparison benchmarks.
83
89
  The PyPI distribution name is `inductive-mlxrl`; the import package and console
84
90
  script remain `mlxrl`.
85
91
 
@@ -133,6 +139,14 @@ UV_CACHE_DIR=.uv-cache uv run mlxrl train \
133
139
  --available-memory-gb 48
134
140
  ```
135
141
 
142
+ Reference multi-turn GiGPO run against the built-in recurring-state env:
143
+
144
+ ```bash
145
+ UV_CACHE_DIR=.uv-cache uv run mlxrl train \
146
+ --config examples/qwen3_0_6b_gigpo_recurring.toml \
147
+ --available-memory-gb 48
148
+ ```
149
+
136
150
  The config schema validates model id, quant bits, group size, completion/prompt
137
151
  lengths, checkpointing granularity, `iogpu.wired_limit_mb`, optimizer settings,
138
152
  algorithm hyperparameters, KL beta, and seed before a model is loaded. CLI
@@ -217,11 +231,71 @@ metrics = optimizer_step(
217
231
  )
218
232
  ```
219
233
 
234
+ Multi-turn GiGPO uses the additive trajectory path:
235
+
236
+ ```python
237
+ from mlxrl.algo import GiGPOAlgorithm
238
+ from mlxrl.env import RecurringStateTextEnv
239
+ from mlxrl.rollout import generate_agentic_trajectories
240
+ from mlxrl.train import batch_from_trajectories, optimizer_step_trajectory
241
+
242
+ algorithm = GiGPOAlgorithm(omega=1.0, gamma=1.0)
243
+ trajectories = generate_agentic_trajectories(
244
+ model=model,
245
+ tokenizer=tokenizer,
246
+ env_factory=lambda task, seed, group: RecurringStateTextEnv(
247
+ task=str(task),
248
+ max_turns=3,
249
+ ),
250
+ tasks=["finish the task"],
251
+ group_size=4,
252
+ sampling=sampling,
253
+ rollout_mode="parallel_per_turn",
254
+ )
255
+ batch = batch_from_trajectories(
256
+ model=model,
257
+ trajectories=trajectories,
258
+ group_size=4,
259
+ pad_token_id=pad_token_id,
260
+ algorithm=algorithm,
261
+ compute_reference=beta != 0.0,
262
+ )
263
+ metrics = optimizer_step_trajectory(
264
+ model=model,
265
+ optimizer=optimizer,
266
+ batch=batch,
267
+ beta=beta,
268
+ pad_token_id=pad_token_id,
269
+ algorithm=algorithm,
270
+ )
271
+ ```
272
+
220
273
  `micro_batch_size=0` keeps the original whole-batch path. Micro-batching is
221
274
  currently exact for token-mean policy losses: base GRPO, DAPO, GSPO token mode,
222
275
  RLOO, and Dr. GRPO with `loss_reduction="token_mean"`. Sequence-reduced losses
223
276
  should keep `micro_batch_size=0`.
224
277
 
278
+ ## ECHO World Modeling
279
+
280
+ ECHO is an additive GiGPO trainer feature for tagged multi-turn trajectories.
281
+ Producer code may attach `token_roles` to each `Trajectory`, aligned to
282
+ `full_token_ids`: `0 = MASKED`, `1 = ACTION`, `2 = ECHO`. ACTION tokens keep
283
+ the normal GiGPO policy-gradient loss. ECHO tokens add an independently
284
+ normalized SFT term, `alpha * NLL`, in the same forward/backward pass.
285
+
286
+ `echo_alpha` defaults to `0.0`, which leaves current GiGPO behavior untouched.
287
+ For early world-modeling, start small, roughly `0.005` to `0.05`, and consider
288
+ `echo_schedule = "linear_taper_to_zero"` with `echo_taper_steps = N` so training
289
+ returns to pure RL. `alpha` is model-size-sensitive; too-high values can
290
+ overfit tool responses or collapse RL.
291
+
292
+ ECHO is content-agnostic by design. `mlxrl` never inspects token text to decide
293
+ what is an environment or tool response. The rollout producer owns the tags:
294
+ MASKED tokens have zero gradient, ACTION tokens receive the RL advantage, and
295
+ ECHO tokens receive only the SFT loss with no KL penalty or importance ratio.
296
+ The trainer logs `loss_echo` and token-level `echo_acc` separately from the
297
+ ACTION loss.
298
+
225
299
  ## Policy Semantics
226
300
 
227
301
  - The base model is frozen before LoRA injection.
@@ -252,6 +326,7 @@ import concrete algorithm implementations.
252
326
  | DAPO | asymmetric low/high clipping plus optional dynamic zero-advantage group filtering |
253
327
  | GSPO | sequence-level, length-normalized importance ratio and clipping |
254
328
  | RLOO | leave-one-out group baseline, no critic, no std-normalized advantage |
329
+ | GiGPO | multi-turn episode advantage plus anchor-state step advantage |
255
330
 
256
331
  ## Memory Preflight
257
332
 
@@ -270,6 +345,9 @@ Qwen3.5-9B/G2/seq609/per-layer-checkpointed, `45.9 GB` for
270
345
  Qwen3.5-9B/G4/seq609/per-layer-checkpointed, and `36 GB` for
271
346
  Qwen3.5-9B/G2/seq128/no-checkpoint. For hybrid 9B no-checkpoint long-sequence
272
347
  configs, it reports an OOM-risk lower bound rather than a fake precise peak.
348
+ Multi-turn configs include `max_turns` and `max_observation_len` in the
349
+ estimated trajectory length and are labeled as estimated OOM-risk shapes until
350
+ new measured anchors are added.
273
351
  For an obviously too-large Qwen3.5-9B/G8/prompt97/T512/no-checkpoint config on
274
352
  48 GB, it flags the run and suggests the measured-boundary fallback around
275
353
  G4/T512/checkpointed.
@@ -349,6 +427,7 @@ updates, or benchmark timing boundaries.
349
427
  ```text
350
428
  mlxrl/
351
429
  rollout/ # batched group generation
430
+ env/ # agentic environment protocols and reference envs
352
431
  policy/ # model loading, LoRA setup, logprob passes
353
432
  algo/ # GRPO-family advantages and losses
354
433
  train/ # value_and_grad and optimizer integration
@@ -0,0 +1,29 @@
1
+ model_id = "mlx-community/Qwen3.5-9B-MLX-4bit"
2
+ quant_bits = 4
3
+ group_size = 2
4
+ max_prompt_len = 97
5
+ max_completion_len = 512
6
+ steps = 1
7
+ seed = 7
8
+ kl_beta = 0.0
9
+ rank = 16
10
+ scale = 2.0
11
+ dropout = 0.0
12
+ gradient_checkpointing = true
13
+ checkpoint_granularity = "per-layer"
14
+ iogpu_wired_limit_mb = 49152
15
+ micro_batch_size = 0
16
+ output = "reference_outputs/mlxrl_qwen35_9b_g2_checkpoint.npz"
17
+
18
+ [optimizer]
19
+ name = "adam"
20
+ learning_rate = 0.00001
21
+
22
+ [sampling]
23
+ temperature = 0.7
24
+ top_p = 0.95
25
+ min_p = 0.0
26
+ top_k = 0
27
+
28
+ [algorithm]
29
+ name = "grpo"
@@ -0,0 +1,33 @@
1
+ model_id = "mlx-community/Qwen3-0.6B-4bit"
2
+ group_size = 4
3
+ max_prompt_len = 512
4
+ max_completion_len = 32
5
+ max_turns = 3
6
+ max_observation_len = 64
7
+ rollout_mode = "parallel_per_turn"
8
+ env_name = "recurring-text"
9
+ steps = 5
10
+ seed = 7
11
+ kl_beta = 0.0
12
+ rank = 8
13
+ scale = 20.0
14
+ dropout = 0.0
15
+ gradient_checkpointing = false
16
+ micro_batch_size = 0
17
+ output = "reference_outputs/gigpo_recurring_reference.npz"
18
+
19
+ [optimizer]
20
+ name = "adam"
21
+ learning_rate = 0.00001
22
+
23
+ [sampling]
24
+ temperature = 0.7
25
+ top_p = 0.95
26
+ min_p = 0.0
27
+ top_k = 0
28
+
29
+ [algorithm]
30
+ name = "gigpo"
31
+ gigpo_omega = 1.0
32
+ gigpo_gamma = 1.0
33
+ gigpo_normalization = "std"
@@ -0,0 +1,29 @@
1
+ model_id = "mlx-community/Qwen3-0.6B-4bit"
2
+ quant_bits = 4
3
+ group_size = 2
4
+ max_completion_len = 4
5
+ max_prompt_len = 4096
6
+ steps = 1
7
+ seed = 7
8
+ kl_beta = 0.0
9
+ rank = 8
10
+ scale = 20.0
11
+ dropout = 0.0
12
+ gradient_checkpointing = false
13
+ checkpoint_granularity = "per-layer"
14
+ iogpu_wired_limit_mb = 49152
15
+ micro_batch_size = 0
16
+ output = "reference_outputs/mlxrl_config_smoke.npz"
17
+
18
+ [optimizer]
19
+ name = "adam"
20
+ learning_rate = 0.00001
21
+
22
+ [sampling]
23
+ temperature = 0.7
24
+ top_p = 0.95
25
+ min_p = 0.0
26
+ top_k = 0
27
+
28
+ [algorithm]
29
+ name = "grpo"
@@ -2,5 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.1.0"
6
-
5
+ __version__ = "0.2.2"