inductive-mlxrl 0.2.3__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.3.0/.github/scripts/audit_dependencies.py +52 -0
  2. inductive_mlxrl-0.3.0/.github/scripts/verify_release.py +78 -0
  3. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/CHANGELOG.md +36 -0
  4. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/CONTRIBUTING.md +31 -13
  5. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/DESIGN.md +18 -0
  6. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/PKG-INFO +55 -3
  7. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/README.md +52 -0
  8. inductive_mlxrl-0.3.0/SECURITY.md +35 -0
  9. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/THIRD_PARTY_LICENSES.md +9 -0
  10. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/external_baseline_worker.py +4 -1
  11. inductive_mlxrl-0.3.0/benchmarks/results/score_centering_4bit_2026-10-30.jsonl +505 -0
  12. inductive_mlxrl-0.3.0/benchmarks/results/score_centering_4bit_2026-10-30.md +102 -0
  13. inductive_mlxrl-0.3.0/benchmarks/results/score_centering_4bit_2026-10-30.summary.json +549 -0
  14. inductive_mlxrl-0.3.0/benchmarks/results/score_centering_capture_2026-10-30.jsonl +26 -0
  15. inductive_mlxrl-0.3.0/benchmarks/results/score_centering_existing_baselines_2026-10-30.jsonl +505 -0
  16. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/run_phase4.py +3 -1
  17. inductive_mlxrl-0.3.0/benchmarks/run_score_centering.py +463 -0
  18. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/compare.py +3 -1
  19. inductive_mlxrl-0.3.0/examples/qwen3_0_6b_score_centering.toml +25 -0
  20. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/__init__.py +1 -1
  21. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/algo/__init__.py +3 -0
  22. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/algo/grpo.py +4 -0
  23. inductive_mlxrl-0.3.0/mlxrl/algo/score_centering.py +97 -0
  24. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/algorithm.py +20 -0
  25. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/cli.py +20 -2
  26. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/config.py +18 -2
  27. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/data/rewards.py +15 -8
  28. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/env/reference.py +6 -4
  29. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/policy/logprobs.py +32 -7
  30. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/policy/model.py +3 -1
  31. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/rollout/naive.py +26 -0
  32. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/rollout/optimized.py +50 -9
  33. inductive_mlxrl-0.3.0/mlxrl/sampling.py +148 -0
  34. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/train/grpo.py +59 -16
  35. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/pyproject.toml +18 -5
  36. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_cli.py +16 -0
  37. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_config.py +15 -0
  38. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_policy_model.py +58 -0
  39. inductive_mlxrl-0.3.0/tests/test_release_policy.py +98 -0
  40. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_rewards.py +7 -0
  41. inductive_mlxrl-0.3.0/tests/test_score_centering.py +328 -0
  42. inductive_mlxrl-0.3.0/tests/test_security.py +67 -0
  43. inductive_mlxrl-0.3.0/uv.lock +4034 -0
  44. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/.gitignore +0 -0
  45. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/LICENSE +0 -0
  46. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/__init__.py +0 -0
  47. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/audit_phase4_config.py +0 -0
  48. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after.jsonl +0 -0
  49. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after.md +0 -0
  50. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after_final.jsonl +0 -0
  51. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after_final.md +0 -0
  52. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after_noprefixsync.jsonl +0 -0
  53. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_after_noprefixsync.md +0 -0
  54. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_before.jsonl +0 -0
  55. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate1_mlxrl_before.md +0 -0
  56. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate2_unified_smoke.jsonl +0 -0
  57. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate2_unified_smoke.md +0 -0
  58. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_mlx_tune_sync_256.jsonl +0 -0
  59. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_mlx_tune_sync_256.md +0 -0
  60. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_sync_blocked.jsonl +0 -0
  61. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_sync_blocked.md +0 -0
  62. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_sync_smoke.jsonl +0 -0
  63. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate3_sync_smoke.md +0 -0
  64. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate4_config_audit.json +0 -0
  65. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate4_config_audit.md +0 -0
  66. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate4_sampler_smoke.jsonl +0 -0
  67. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate4_sampler_smoke.md +0 -0
  68. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate5_full_reconciled.jsonl +0 -0
  69. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/gate5_full_reconciled.md +0 -0
  70. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_external_smoke.jsonl +0 -0
  71. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_external_smoke.md +0 -0
  72. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_full_all_targets.jsonl +0 -0
  73. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_full_all_targets.md +0 -0
  74. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.jsonl +0 -0
  75. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.md +0 -0
  76. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_mlx_lm_g4_smoke.jsonl +0 -0
  77. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/benchmarks/results/phase4_mlx_lm_g4_smoke.md +0 -0
  78. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/README.md +0 -0
  79. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/assets/terminal-results.jpg +0 -0
  80. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/assets/terminal-training.jpg +0 -0
  81. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/assets/ui-ranking.jpg +0 -0
  82. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/assets/ui-scorer-eval.jpg +0 -0
  83. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/config.toml +0 -0
  84. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/data/briefs.json +0 -0
  85. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/data/scorer.json +0 -0
  86. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/taste_reward.py +0 -0
  87. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/test_reward.py +0 -0
  88. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/personal_taste_grpo/train.py +0 -0
  89. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/qwen35_9b_g2_checkpoint.toml +0 -0
  90. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/qwen3_0_6b_gigpo_recurring.toml +0 -0
  91. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/examples/qwen3_0_6b_grpo.toml +0 -0
  92. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/algo/gigpo.py +0 -0
  93. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/data/__init__.py +0 -0
  94. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/data/gsm8k.py +0 -0
  95. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/echo.py +0 -0
  96. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/env/__init__.py +0 -0
  97. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/env/base.py +0 -0
  98. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/policy/__init__.py +0 -0
  99. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/policy/trajectory_logprobs.py +0 -0
  100. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/py.typed +0 -0
  101. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/rollout/__init__.py +0 -0
  102. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/rollout/agentic.py +0 -0
  103. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/train/__init__.py +0 -0
  104. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/train/reference.py +0 -0
  105. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/train/trajectory.py +0 -0
  106. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/mlxrl/trajectory.py +0 -0
  107. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_agentic_env.py +0 -0
  108. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_agentic_rollout.py +0 -0
  109. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_benchmark_metrics.py +0 -0
  110. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_cache_compatibility.py +0 -0
  111. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_echo.py +0 -0
  112. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_gigpo.py +0 -0
  113. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_grpo.py +0 -0
  114. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_import_direction.py +0 -0
  115. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_optimized_rollout.py +0 -0
  116. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_policy_logprobs.py +0 -0
  117. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_train_grpo.py +0 -0
  118. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_train_trajectory.py +0 -0
  119. {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.3.0}/tests/test_trajectory_logprobs.py +0 -0
@@ -0,0 +1,52 @@
1
+ """Audit every registry version in uv.lock, including other-platform dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import tempfile
7
+ import tomllib
8
+ from pathlib import Path
9
+
10
+
11
+ def main() -> None:
12
+ lock = tomllib.loads(Path("uv.lock").read_text())
13
+ groups: list[dict[str, str]] = []
14
+ for package in lock["package"]:
15
+ if package["source"] == {"editable": "."}:
16
+ continue
17
+ if package["source"].get("registry") != "https://pypi.org/simple":
18
+ raise ValueError(f"Unrecognized dependency source for {package['name']}.")
19
+ hashes = {wheel["hash"] for wheel in package.get("wheels", [])}
20
+ if "sdist" in package:
21
+ hashes.add(package["sdist"]["hash"])
22
+ if not hashes:
23
+ raise ValueError(f"Missing locked hashes for {package['name']}.")
24
+ requirement = f"{package['name']}=={package['version']} " + " ".join(
25
+ f"--hash={digest}" for digest in sorted(hashes)
26
+ )
27
+ # pip-audit rejects duplicate names even when versions target other Pythons.
28
+ for group in groups:
29
+ if package["name"] not in group:
30
+ group[package["name"]] = requirement
31
+ break
32
+ else:
33
+ groups.append({package["name"]: requirement})
34
+ with tempfile.TemporaryDirectory(prefix="mlxrl-dependency-audit-") as directory:
35
+ for index, group in enumerate(groups):
36
+ requirements = Path(directory) / f"requirements-{index}.txt"
37
+ requirements.write_text("\n".join(group.values()) + "\n")
38
+ subprocess.run(
39
+ [
40
+ "pip-audit",
41
+ "--strict",
42
+ "--disable-pip",
43
+ "--require-hashes",
44
+ "-r",
45
+ str(requirements),
46
+ ],
47
+ check=True,
48
+ )
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
@@ -0,0 +1,78 @@
1
+ """Require versioned tags to identify a main commit with successful push CI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import json
7
+ import os
8
+ import re
9
+ import subprocess
10
+ import tomllib
11
+ import urllib.parse
12
+ import urllib.request
13
+ from pathlib import Path
14
+
15
+
16
+ def verify_release(ref: str, sha: str, version: str, repository: str, token: str) -> None:
17
+ if not ref.startswith("refs/tags/"):
18
+ return
19
+ if not re.fullmatch(r"refs/tags/v[0-9]+\.[0-9]+\.[0-9]+", ref):
20
+ raise ValueError("Release tags must use vX.Y.Z.")
21
+ if ref != f"refs/tags/v{version}":
22
+ raise ValueError("Release tag must match the package version.")
23
+ if not re.fullmatch(r"[0-9a-f]{40}", sha):
24
+ raise ValueError("Release commit must be a full SHA.")
25
+ tagged = subprocess.check_output(["git", "rev-parse", f"{ref}^{{commit}}"], text=True).strip()
26
+ if tagged != sha:
27
+ raise ValueError("Release tag and workflow commit differ.")
28
+ if subprocess.run(["git", "merge-base", "--is-ancestor", sha, "origin/main"]).returncode:
29
+ raise ValueError("Release commit must already be on origin/main.")
30
+ query = urllib.parse.urlencode(
31
+ {"head_sha": sha, "branch": "main", "event": "push", "status": "success"}
32
+ )
33
+ request = urllib.request.Request(
34
+ f"https://api.github.com/repos/{repository}/actions/workflows/ci.yml/runs?{query}",
35
+ headers={
36
+ "Authorization": f"Bearer {token}",
37
+ "Accept": "application/vnd.github+json",
38
+ "X-GitHub-Api-Version": "2022-11-28",
39
+ },
40
+ )
41
+ with urllib.request.urlopen(request, timeout=30) as response:
42
+ runs = json.load(response)["workflow_runs"]
43
+ if not any(
44
+ run["head_sha"] == sha
45
+ and run["head_branch"] == "main"
46
+ and run["event"] == "push"
47
+ and run["status"] == "completed"
48
+ and run["conclusion"] == "success"
49
+ and run["path"] == ".github/workflows/ci.yml"
50
+ for run in runs
51
+ ):
52
+ raise ValueError("Release commit needs successful push CI on main.")
53
+
54
+
55
+ def main() -> None:
56
+ version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
57
+ module = ast.parse(Path("mlxrl/__init__.py").read_text())
58
+ package_version = next(
59
+ ast.literal_eval(node.value)
60
+ for node in module.body
61
+ if isinstance(node, ast.Assign)
62
+ and any(
63
+ isinstance(target, ast.Name) and target.id == "__version__" for target in node.targets
64
+ )
65
+ )
66
+ if package_version != version:
67
+ raise ValueError("Package and distribution versions must match.")
68
+ verify_release(
69
+ os.environ["GITHUB_REF"],
70
+ os.environ["GITHUB_SHA"],
71
+ version,
72
+ os.environ["GITHUB_REPOSITORY"],
73
+ os.environ.get("GH_TOKEN", ""),
74
+ )
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()
@@ -5,6 +5,42 @@ 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
+
27
+ ## [0.2.4] - 2026-10-22
28
+
29
+ ### Security
30
+
31
+ - Require MLX-LM 0.32.0 or later with its MLX 0.32.2 minimum, and disable custom
32
+ model and tokenizer Python execution in library, example, and benchmark loaders. The older minimum
33
+ allowed the unsafe `model_file` behavior addressed by upstream's
34
+ [CVE-2026-5843 fix](https://github.com/ml-explore/mlx-lm/pull/1385).
35
+ - Parse answer and action tags in linear time to prevent malformed model
36
+ output from stalling reward evaluation and multi-turn rollouts.
37
+ - Reject CLI output paths whose symlinks resolve outside the working directory.
38
+ - Pin release actions to verified commits, freeze build and validation tools,
39
+ audit all locked dependencies in CI, and require release tags to match a
40
+ package version on main with successful push CI.
41
+ - Add security reporting guidance, ownership of release controls, and weekly
42
+ dependency and action updates.
43
+
8
44
  ## [0.2.3] - 2026-10-21
9
45
 
10
46
  ### Changed
@@ -77,6 +77,10 @@ with `datasets>=5.0.1,<6` to avoid CVE-2026-66007. This only affects the externa
77
77
  benchmark tools; the published package depends on MLX and MLX-LM. Remove the
78
78
  override when mlx-tune accepts a patched datasets release.
79
79
 
80
+ Certifi and Pillow use a one-day package-age cutoff so their October 2026
81
+ security patches can be installed promptly. Other dependencies retain the
82
+ configured default cutoff.
83
+
80
84
  ## Release Cycle
81
85
 
82
86
  Releases are tag-driven and publish to PyPI through Trusted Publishing. Do not
@@ -90,21 +94,34 @@ One-time PyPI setup:
90
94
  - set environment name to `pypi`;
91
95
  - require manual approval on the GitHub `pypi` environment before publishing.
92
96
 
97
+ Restrict the environment to tags matching `v*`, disable administrator bypass
98
+ for environment approval, and protect release-tag creation, updates, and
99
+ deletion so only repository administrators can perform them. Require the
100
+ `Linux CPU-safe checks` status on main and retain pull-request review.
101
+
102
+ The publisher runs the official PyPA container by digest. When updating it,
103
+ resolve the upstream release's commit-tagged image from
104
+ `ghcr.io/pypa/gh-action-pypi-publish`, verify its content digest and source
105
+ metadata, and update the digest and source-commit comment together. Other
106
+ actions use commit pins maintained by Dependabot.
107
+
93
108
  For each release:
94
109
 
95
- 1. Update the version in `pyproject.toml`.
110
+ 1. Update the version in `pyproject.toml` and `mlxrl/__init__.py`, then run `uv lock`.
96
111
  2. Add the release notes to `CHANGELOG.md`.
97
112
  3. Run the local gates:
98
113
 
99
114
  ```bash
100
- UV_CACHE_DIR=.uv-cache uv run pytest
101
- UV_CACHE_DIR=.uv-cache uv run ruff check .
102
- UV_CACHE_DIR=.uv-cache uv run pyright
103
- UV_CACHE_DIR=.uv-cache uv build
104
- UV_CACHE_DIR=.uv-cache uv run --no-project --python 3.11 --with twine twine check dist/*
115
+ UV_CACHE_DIR=.uv-cache uv sync --frozen --all-groups
116
+ UV_CACHE_DIR=.uv-cache uv run --no-sync pytest
117
+ UV_CACHE_DIR=.uv-cache uv run --no-sync ruff check .
118
+ UV_CACHE_DIR=.uv-cache uv run --no-sync pyright
119
+ UV_CACHE_DIR=.uv-cache uv run --no-sync python .github/scripts/audit_dependencies.py
120
+ UV_CACHE_DIR=.uv-cache uv build --no-build-isolation --python .venv/bin/python
121
+ UV_CACHE_DIR=.uv-cache uv run --no-sync twine check dist/*
105
122
  ```
106
123
 
107
- 4. Commit and merge the release prep.
124
+ 4. Commit and merge the release prep, then wait for main's push CI to pass.
108
125
  5. Tag the release from `main`:
109
126
 
110
127
  ```bash
@@ -112,12 +129,13 @@ git tag -a vX.Y.Z -m "Release vX.Y.Z"
112
129
  git push origin vX.Y.Z
113
130
  ```
114
131
 
115
- The `Release` workflow builds the source distribution and wheel, checks package
116
- metadata with Twine, stores the artifacts, publishes them to PyPI from the
117
- `pypi` environment, and creates a GitHub Release with the built distributions
118
- attached. Running the workflow manually builds and checks artifacts without
119
- publishing because the publish and GitHub Release jobs only run for `v*.*.*`
120
- tags.
132
+ The `Release` workflow first checks that the tag matches the package version,
133
+ identifies a commit already on main, and has successful main push CI. It builds
134
+ with frozen validation and build tools, checks metadata with Twine, stores the
135
+ artifacts, and publishes signed distributions from the protected `pypi`
136
+ environment after maintainer approval. The GitHub Release receives the same
137
+ files. Manual runs on branches only build and check artifacts; manual runs on
138
+ version tags must satisfy all publication controls.
121
139
 
122
140
  The PyPI distribution name is `inductive-mlxrl`; the Python import package and
123
141
  CLI command remain `mlxrl`.
@@ -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.3
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
@@ -19,8 +19,8 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Programming Language :: Python :: 3.14
20
20
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
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'
22
+ Requires-Dist: mlx-lm>=0.32.0; python_version < '3.15' and platform_system == 'Darwin' and platform_machine == 'arm64'
23
+ Requires-Dist: mlx>=0.32.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
@@ -43,6 +43,9 @@ fields may change before a 1.0 release.
43
43
 
44
44
  ## Quickstart
45
45
 
46
+ Requires Apple Silicon, Python 3.11–3.14, MLX 0.32.2+, and MLX-LM 0.32.0+.
47
+ See [SECURITY.md](SECURITY.md) for model trust boundaries and private reporting.
48
+
46
49
  ```bash
47
50
  git clone https://github.com/inductiveML/mlxrl.git
48
51
  cd mlxrl
@@ -352,6 +355,55 @@ import concrete algorithm implementations.
352
355
  | GSPO | sequence-level, length-normalized importance ratio and clipping |
353
356
  | RLOO | leave-one-out group baseline, no critic, no std-normalized advantage |
354
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).
355
407
 
356
408
  ## Memory Preflight
357
409
 
@@ -18,6 +18,9 @@ fields may change before a 1.0 release.
18
18
 
19
19
  ## Quickstart
20
20
 
21
+ Requires Apple Silicon, Python 3.11–3.14, MLX 0.32.2+, and MLX-LM 0.32.0+.
22
+ See [SECURITY.md](SECURITY.md) for model trust boundaries and private reporting.
23
+
21
24
  ```bash
22
25
  git clone https://github.com/inductiveML/mlxrl.git
23
26
  cd mlxrl
@@ -327,6 +330,55 @@ import concrete algorithm implementations.
327
330
  | GSPO | sequence-level, length-normalized importance ratio and clipping |
328
331
  | RLOO | leave-one-out group baseline, no critic, no std-normalized advantage |
329
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).
330
382
 
331
383
  ## Memory Preflight
332
384
 
@@ -0,0 +1,35 @@
1
+ # Security policy
2
+
3
+ Security fixes target the latest released version. Use inductive-mlxrl 0.2.4 or
4
+ later and MLX-LM 0.32.0 or later. Earlier supported MLX-LM versions could execute
5
+ Python from a model's `model_file` configuration without an explicit trust flag.
6
+ See the [upstream fix](https://github.com/ml-explore/mlx-lm/pull/1385).
7
+
8
+ Report vulnerabilities through the repository's
9
+ [private vulnerability reporting form](https://github.com/inductiveML/mlxrl/security/advisories/new).
10
+ Include the affected version, a minimal reproduction, and the expected impact.
11
+ Keep credentials and private training data out of reports and public issues.
12
+
13
+ ## Trust boundaries
14
+
15
+ Model completions and action text are untrusted data. Built-in tag parsers scan
16
+ fixed delimiters without regex backtracking. Model loading explicitly disables
17
+ custom remote Python code.
18
+
19
+ Training configurations, custom Python reward functions, environment callbacks,
20
+ benchmark command adapters, model repositories, and adapter files must come from
21
+ sources you trust. The library runs in the caller's process and does not provide
22
+ a sandbox for these extensions or for native tensor parsers. CLI output paths
23
+ must resolve inside the working directory; this check does not isolate a
24
+ process from another user who can concurrently change its files or symlinks.
25
+
26
+ ## Release controls
27
+
28
+ Release tags must match the package version, identify a commit already on main,
29
+ and have successful main push CI. CI audits every locked dependency version,
30
+ including packages for other platforms and Python versions. Release actions
31
+ use verified commit pins and build tools come from the frozen lockfile.
32
+
33
+ GitHub's `pypi` environment restricts publication to protected release tags and
34
+ requires a maintainer's approval. PyPI publication uses OIDC Trusted Publishing
35
+ and signed attestations. Local Metal tests remain a release requirement.
@@ -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.
@@ -317,7 +317,9 @@ def run_mlx_lm_lora(args: argparse.Namespace) -> BenchResult:
317
317
  prompt_token_lengths = [
318
318
  encoded_prompt_token_count(tokenizer, row["prompt"]) for row in rows
319
319
  ]
320
- ref_loaded = load(args.model)
320
+ ref_loaded = load(
321
+ args.model, tokenizer_config={"trust_remote_code": False}, trust_remote_code=False
322
+ )
321
323
  ref_model = ref_loaded[0]
322
324
  ref_model.freeze()
323
325
  optimizer = optim.Adam(learning_rate=args.learning_rate)
@@ -453,6 +455,7 @@ def run_mlx_tune(args: argparse.Namespace) -> BenchResult:
453
455
  model_name=args.model,
454
456
  max_seq_length=args.max_context,
455
457
  load_in_4bit=True,
458
+ trust_remote_code=False,
456
459
  )
457
460
  prompt_token_lengths = [
458
461
  encoded_prompt_token_count(tokenizer, row["prompt"]) for row in rows