inductive-mlxrl 0.2.4__tar.gz → 0.3.0__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 (119) hide show
  1. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/CHANGELOG.md +19 -0
  2. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/DESIGN.md +18 -0
  3. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/PKG-INFO +50 -1
  4. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/README.md +49 -0
  5. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/THIRD_PARTY_LICENSES.md +9 -0
  6. inductive_mlxrl-0.3.0/benchmarks/results/score_centering_4bit_2026-10-30.jsonl +505 -0
  7. inductive_mlxrl-0.3.0/benchmarks/results/score_centering_4bit_2026-10-30.md +102 -0
  8. inductive_mlxrl-0.3.0/benchmarks/results/score_centering_4bit_2026-10-30.summary.json +549 -0
  9. inductive_mlxrl-0.3.0/benchmarks/results/score_centering_capture_2026-10-30.jsonl +26 -0
  10. inductive_mlxrl-0.3.0/benchmarks/results/score_centering_existing_baselines_2026-10-30.jsonl +505 -0
  11. inductive_mlxrl-0.3.0/benchmarks/run_score_centering.py +463 -0
  12. inductive_mlxrl-0.3.0/examples/qwen3_0_6b_score_centering.toml +25 -0
  13. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/__init__.py +1 -1
  14. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/algo/__init__.py +3 -0
  15. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/algo/grpo.py +4 -0
  16. inductive_mlxrl-0.3.0/mlxrl/algo/score_centering.py +97 -0
  17. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/algorithm.py +20 -0
  18. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/cli.py +20 -2
  19. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/config.py +12 -2
  20. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/policy/logprobs.py +32 -7
  21. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/rollout/naive.py +26 -0
  22. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/rollout/optimized.py +50 -9
  23. inductive_mlxrl-0.3.0/mlxrl/sampling.py +148 -0
  24. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/train/grpo.py +59 -16
  25. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/pyproject.toml +1 -1
  26. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_cli.py +16 -0
  27. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_config.py +15 -0
  28. inductive_mlxrl-0.3.0/tests/test_score_centering.py +328 -0
  29. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/uv.lock +25 -27
  30. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/.github/scripts/audit_dependencies.py +0 -0
  31. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/.github/scripts/verify_release.py +0 -0
  32. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/.gitignore +0 -0
  33. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/CONTRIBUTING.md +0 -0
  34. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/LICENSE +0 -0
  35. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/SECURITY.md +0 -0
  36. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/__init__.py +0 -0
  37. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/audit_phase4_config.py +0 -0
  38. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/external_baseline_worker.py +0 -0
  39. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after.jsonl +0 -0
  40. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after.md +0 -0
  41. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after_final.jsonl +0 -0
  42. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after_final.md +0 -0
  43. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after_noprefixsync.jsonl +0 -0
  44. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after_noprefixsync.md +0 -0
  45. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_before.jsonl +0 -0
  46. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_before.md +0 -0
  47. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate2_unified_smoke.jsonl +0 -0
  48. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate2_unified_smoke.md +0 -0
  49. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_mlx_tune_sync_256.jsonl +0 -0
  50. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_mlx_tune_sync_256.md +0 -0
  51. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_sync_blocked.jsonl +0 -0
  52. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_sync_blocked.md +0 -0
  53. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_sync_smoke.jsonl +0 -0
  54. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_sync_smoke.md +0 -0
  55. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate4_config_audit.json +0 -0
  56. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate4_config_audit.md +0 -0
  57. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate4_sampler_smoke.jsonl +0 -0
  58. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate4_sampler_smoke.md +0 -0
  59. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate5_full_reconciled.jsonl +0 -0
  60. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/gate5_full_reconciled.md +0 -0
  61. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_external_smoke.jsonl +0 -0
  62. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_external_smoke.md +0 -0
  63. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_full_all_targets.jsonl +0 -0
  64. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_full_all_targets.md +0 -0
  65. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.jsonl +0 -0
  66. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.md +0 -0
  67. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_mlx_lm_g4_smoke.jsonl +0 -0
  68. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_mlx_lm_g4_smoke.md +0 -0
  69. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/benchmarks/run_phase4.py +0 -0
  70. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/README.md +0 -0
  71. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/assets/terminal-results.jpg +0 -0
  72. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/assets/terminal-training.jpg +0 -0
  73. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/assets/ui-ranking.jpg +0 -0
  74. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/assets/ui-scorer-eval.jpg +0 -0
  75. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/compare.py +0 -0
  76. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/config.toml +0 -0
  77. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/data/briefs.json +0 -0
  78. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/data/scorer.json +0 -0
  79. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/taste_reward.py +0 -0
  80. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/test_reward.py +0 -0
  81. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/train.py +0 -0
  82. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/qwen35_9b_g2_checkpoint.toml +0 -0
  83. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/qwen3_0_6b_gigpo_recurring.toml +0 -0
  84. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/examples/qwen3_0_6b_grpo.toml +0 -0
  85. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/algo/gigpo.py +0 -0
  86. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/data/__init__.py +0 -0
  87. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/data/gsm8k.py +0 -0
  88. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/data/rewards.py +0 -0
  89. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/echo.py +0 -0
  90. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/env/__init__.py +0 -0
  91. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/env/base.py +0 -0
  92. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/env/reference.py +0 -0
  93. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/policy/__init__.py +0 -0
  94. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/policy/model.py +0 -0
  95. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/policy/trajectory_logprobs.py +0 -0
  96. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/py.typed +0 -0
  97. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/rollout/__init__.py +0 -0
  98. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/rollout/agentic.py +0 -0
  99. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/train/__init__.py +0 -0
  100. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/train/reference.py +0 -0
  101. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/train/trajectory.py +0 -0
  102. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/mlxrl/trajectory.py +0 -0
  103. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_agentic_env.py +0 -0
  104. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_agentic_rollout.py +0 -0
  105. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_benchmark_metrics.py +0 -0
  106. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_cache_compatibility.py +0 -0
  107. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_echo.py +0 -0
  108. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_gigpo.py +0 -0
  109. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_grpo.py +0 -0
  110. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_import_direction.py +0 -0
  111. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_optimized_rollout.py +0 -0
  112. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_policy_logprobs.py +0 -0
  113. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_policy_model.py +0 -0
  114. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_release_policy.py +0 -0
  115. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_rewards.py +0 -0
  116. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_security.py +0 -0
  117. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_train_grpo.py +0 -0
  118. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_train_trajectory.py +0 -0
  119. {inductive_mlxrl-0.2.4 → inductive_mlxrl-0.3.0}/tests/test_trajectory_logprobs.py +0 -0
@@ -5,6 +5,25 @@ All notable changes to this project are documented here.
5
5
  The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project uses semantic versioning once releases are cut.
7
7
 
8
+ ## [0.3.0] - 2026-10-30
9
+
10
+ Score centering is experimental and disabled by default. Existing configurations
11
+ continue to use GRPO. Enable it explicitly with `[algorithm] name = "score-centering"`.
12
+ The local benchmarks do not establish a reliable quality or long-run stability gain.
13
+
14
+ ### Added
15
+
16
+ - Experimental, opt-in single-turn score centering with actual post-filter,
17
+ post-temperature sampler probabilities, detached top-k/tail correction,
18
+ fp32 trainer probabilities, and micro-batch support.
19
+ - Correctness gates for exact gradients, drift cancellation, sampling laws,
20
+ seeded capture parity across rollout engines, and padded optimizer updates.
21
+ - A paired multi-seed 4-bit QLoRA benchmark with raw training diagnostics.
22
+
23
+ ### Changed
24
+
25
+ - Update locked filelock to 4.2.1 and Ruff to 0.17.1, and pin setup-uv 10.3.2.
26
+
8
27
  ## [0.2.4] - 2026-10-22
9
28
 
10
29
  ### Security
@@ -37,6 +37,13 @@ than widening the single-turn `Algorithm` contract. GiGPO is the first
37
37
  implementation: the rollout engine records env-provided state ids, while the
38
38
  algorithm owns anchor-state grouping and credit assignment.
39
39
 
40
+ The experimental single-turn `SamplerAwareAlgorithm` sibling extends the loss
41
+ input with a recorded sampler head and corresponding full-forward trainer
42
+ logprobs. The engine dispatches through that protocol when a prepared batch has
43
+ sampler records; it still imports no algorithm implementations. The first user
44
+ is score centering, a token-mean REINFORCE objective with centered rewards.
45
+ Existing algorithms continue through the original protocol and probability path.
46
+
40
47
  ## Agentic Environment Seam
41
48
 
42
49
  Agentic rollout is built around `EnvFactory(task, seed, group_index) ->
@@ -86,6 +93,17 @@ diagnostic data, not training data.
86
93
 
87
94
  ## Memory As A First-Class Constraint
88
95
 
96
+ Experimental score centering captures the actual categorical sampling law after
97
+ filtering and temperature, rather than treating raw model logprobs as sampler
98
+ probabilities. Records contain the selected-token probability and a top-k head;
99
+ the head is not renormalized. Persistent capture storage scales with completion
100
+ tokens times k, rather than vocabulary size. All gradient-bearing candidate
101
+ logprobs come from the trainer's full-forward path with fp32 normalization.
102
+ The detached tail correction follows equations 8--10 of arXiv:2609.20807, using
103
+ an exact zero sampler tail when its head exhausts all mass and a 1e-6 floor on
104
+ the trainer tail for numerical stability. This path is opt-in, single-turn only,
105
+ and requires separate measured memory and stability gates.
106
+
89
107
  Apple Silicon's unified memory is the deployment target, not an afterthought.
90
108
  The code supports per-layer MLX-LM gradient checkpointing because DeltaNet and
91
109
  linear-attention models otherwise keep O(sequence length) recurrent state live
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: inductive-mlxrl
3
- Version: 0.2.4
3
+ Version: 0.3.0
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
@@ -355,6 +355,55 @@ import concrete algorithm implementations.
355
355
  | GSPO | sequence-level, length-normalized importance ratio and clipping |
356
356
  | RLOO | leave-one-out group baseline, no critic, no std-normalized advantage |
357
357
  | GiGPO | multi-turn episode advantage plus anchor-state step advantage |
358
+ | Score centering (experimental) | centered rewards, token-mean REINFORCE with sampler-score correction |
359
+
360
+ ### Experimental score centering
361
+
362
+ Score centering is an explicit single-turn opt-in based on
363
+ [Marek and Ryabinin, 2026](https://arxiv.org/abs/2609.20807). Existing algorithm
364
+ defaults and old-policy recompute semantics stay unchanged.
365
+
366
+ ```bash
367
+ UV_CACHE_DIR=.uv-cache uv run mlxrl train \
368
+ --config examples/qwen3_0_6b_score_centering.toml \
369
+ --available-memory-gb 48
370
+ ```
371
+
372
+ The config selects the objective and the number of sampler candidates to retain:
373
+
374
+ ```toml
375
+ [algorithm]
376
+ name = "score-centering"
377
+ score_centering_top_k = 128
378
+ ```
379
+
380
+ This objective uses group-centered rewards without standard-deviation scaling.
381
+ The sampler records probabilities **after top-p, min-p, top-k, and temperature**,
382
+ including the selected token's probability even when it falls outside the
383
+ recorded head. Greedy sampling is recorded as a point mass. Candidate
384
+ probabilities retain their original mass; the trainer approximates the remaining
385
+ tail as described in the paper. The correction coefficients are detached, and
386
+ trainer logprobs are normalized in fp32. `kl_beta` remains the independent
387
+ adapter-disabled reference-model penalty.
388
+
389
+ For the import API, construct `ScoreCenteringAlgorithm(sampler_top_k=128)` from
390
+ `mlxrl.algo` and use `SamplingConfig(record_sampler_top_k=128)` for generation.
391
+ Pass the resulting completions and that algorithm to `batch_from_rollouts` and
392
+ `optimizer_step`. Missing sampler records are rejected. `Completion.sampler_trace`
393
+ contains the selected-token and candidate probabilities; config-driven runs also
394
+ save `sampler_logprobs`, `sampler_topk_ids`, and `sampler_topk_logprobs` in the
395
+ output NPZ. Ordinary `old_policy_logprobs` remain separate.
396
+
397
+ Capture works with naive, prefix-cached, batched, and compiled single-turn
398
+ rollouts and preserves seeded token draws. The objective supports token-weighted
399
+ micro-batching. Multi-turn/ECHO training is outside this initial experiment.
400
+ The memory preflight estimates have not been calibrated for this objective.
401
+ Its benefit and overhead on a particular model must be measured; the paper's
402
+ large-model results do not establish a benefit for MLX QLoRA.
403
+
404
+ The reproducible paired benchmark is `benchmarks/run_score_centering.py`; its
405
+ protocol and measured results are in
406
+ [the 4-bit benchmark report](benchmarks/results/score_centering_4bit_2026-10-30.md).
358
407
 
359
408
  ## Memory Preflight
360
409
 
@@ -330,6 +330,55 @@ import concrete algorithm implementations.
330
330
  | GSPO | sequence-level, length-normalized importance ratio and clipping |
331
331
  | RLOO | leave-one-out group baseline, no critic, no std-normalized advantage |
332
332
  | GiGPO | multi-turn episode advantage plus anchor-state step advantage |
333
+ | Score centering (experimental) | centered rewards, token-mean REINFORCE with sampler-score correction |
334
+
335
+ ### Experimental score centering
336
+
337
+ Score centering is an explicit single-turn opt-in based on
338
+ [Marek and Ryabinin, 2026](https://arxiv.org/abs/2609.20807). Existing algorithm
339
+ defaults and old-policy recompute semantics stay unchanged.
340
+
341
+ ```bash
342
+ UV_CACHE_DIR=.uv-cache uv run mlxrl train \
343
+ --config examples/qwen3_0_6b_score_centering.toml \
344
+ --available-memory-gb 48
345
+ ```
346
+
347
+ The config selects the objective and the number of sampler candidates to retain:
348
+
349
+ ```toml
350
+ [algorithm]
351
+ name = "score-centering"
352
+ score_centering_top_k = 128
353
+ ```
354
+
355
+ This objective uses group-centered rewards without standard-deviation scaling.
356
+ The sampler records probabilities **after top-p, min-p, top-k, and temperature**,
357
+ including the selected token's probability even when it falls outside the
358
+ recorded head. Greedy sampling is recorded as a point mass. Candidate
359
+ probabilities retain their original mass; the trainer approximates the remaining
360
+ tail as described in the paper. The correction coefficients are detached, and
361
+ trainer logprobs are normalized in fp32. `kl_beta` remains the independent
362
+ adapter-disabled reference-model penalty.
363
+
364
+ For the import API, construct `ScoreCenteringAlgorithm(sampler_top_k=128)` from
365
+ `mlxrl.algo` and use `SamplingConfig(record_sampler_top_k=128)` for generation.
366
+ Pass the resulting completions and that algorithm to `batch_from_rollouts` and
367
+ `optimizer_step`. Missing sampler records are rejected. `Completion.sampler_trace`
368
+ contains the selected-token and candidate probabilities; config-driven runs also
369
+ save `sampler_logprobs`, `sampler_topk_ids`, and `sampler_topk_logprobs` in the
370
+ output NPZ. Ordinary `old_policy_logprobs` remain separate.
371
+
372
+ Capture works with naive, prefix-cached, batched, and compiled single-turn
373
+ rollouts and preserves seeded token draws. The objective supports token-weighted
374
+ micro-batching. Multi-turn/ECHO training is outside this initial experiment.
375
+ The memory preflight estimates have not been calibrated for this objective.
376
+ Its benefit and overhead on a particular model must be measured; the paper's
377
+ large-model results do not establish a benefit for MLX QLoRA.
378
+
379
+ The reproducible paired benchmark is `benchmarks/run_score_centering.py`; its
380
+ protocol and measured results are in
381
+ [the 4-bit benchmark report](benchmarks/results/score_centering_4bit_2026-10-30.md).
333
382
 
334
383
  ## Memory Preflight
335
384
 
@@ -44,3 +44,12 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
44
44
  - License: MIT per installed package metadata.
45
45
  - Used for: benchmark configuration inspection and external baseline adapter
46
46
  patterns only. No `mlx-lm-lora` implementation code is vendored in `mlxrl`.
47
+
48
+ ## Score centering research
49
+
50
+ - Martin Marek and Max Ryabinin, *Score Centering Stabilizes Off-policy
51
+ Reinforcement Learning* (2026): https://arxiv.org/abs/2609.20807.
52
+ - The experimental MLX objective implements the mathematics in equations 8--10
53
+ with independently written MLX code and explicit gradient-equivalence tests.
54
+ The paper is a research reference, not an installed dependency. This objective
55
+ is distinct from Tinker's StableReinforce service implementation.