inductive-mlxrl 0.2.3__tar.gz → 0.2.4__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.
- inductive_mlxrl-0.2.4/.github/scripts/audit_dependencies.py +52 -0
- inductive_mlxrl-0.2.4/.github/scripts/verify_release.py +78 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/CHANGELOG.md +17 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/CONTRIBUTING.md +31 -13
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/PKG-INFO +6 -3
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/README.md +3 -0
- inductive_mlxrl-0.2.4/SECURITY.md +35 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/external_baseline_worker.py +4 -1
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/run_phase4.py +3 -1
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/compare.py +3 -1
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/__init__.py +1 -1
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/config.py +6 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/data/rewards.py +15 -8
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/env/reference.py +6 -4
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/policy/model.py +3 -1
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/pyproject.toml +18 -5
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_policy_model.py +58 -0
- inductive_mlxrl-0.2.4/tests/test_release_policy.py +98 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_rewards.py +7 -0
- inductive_mlxrl-0.2.4/tests/test_security.py +67 -0
- inductive_mlxrl-0.2.4/uv.lock +4036 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/.gitignore +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/DESIGN.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/LICENSE +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/THIRD_PARTY_LICENSES.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/__init__.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/audit_phase4_config.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate1_mlxrl_after.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate1_mlxrl_after.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate1_mlxrl_after_final.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate1_mlxrl_after_final.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate1_mlxrl_after_noprefixsync.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate1_mlxrl_after_noprefixsync.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate1_mlxrl_before.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate1_mlxrl_before.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate2_unified_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate2_unified_smoke.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate3_mlx_tune_sync_256.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate3_mlx_tune_sync_256.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate3_sync_blocked.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate3_sync_blocked.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate3_sync_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate3_sync_smoke.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate4_config_audit.json +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate4_config_audit.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate4_sampler_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate4_sampler_smoke.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate5_full_reconciled.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/gate5_full_reconciled.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/phase4_external_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/phase4_external_smoke.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/phase4_full_all_targets.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/phase4_full_all_targets.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/phase4_mlx_lm_g4_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/benchmarks/results/phase4_mlx_lm_g4_smoke.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/README.md +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/assets/terminal-results.jpg +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/assets/terminal-training.jpg +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/assets/ui-ranking.jpg +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/assets/ui-scorer-eval.jpg +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/config.toml +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/data/briefs.json +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/data/scorer.json +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/taste_reward.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/test_reward.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/personal_taste_grpo/train.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/qwen35_9b_g2_checkpoint.toml +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/qwen3_0_6b_gigpo_recurring.toml +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/examples/qwen3_0_6b_grpo.toml +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/algo/__init__.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/algo/gigpo.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/algo/grpo.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/algorithm.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/cli.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/data/__init__.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/data/gsm8k.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/echo.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/env/__init__.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/env/base.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/policy/__init__.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/policy/logprobs.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/policy/trajectory_logprobs.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/py.typed +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/rollout/__init__.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/rollout/agentic.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/rollout/naive.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/rollout/optimized.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/train/__init__.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/train/grpo.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/train/reference.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/train/trajectory.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/mlxrl/trajectory.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_agentic_env.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_agentic_rollout.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_benchmark_metrics.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_cache_compatibility.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_cli.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_config.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_echo.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_gigpo.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_grpo.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_import_direction.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_optimized_rollout.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_policy_logprobs.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_train_grpo.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/tests/test_train_trajectory.py +0 -0
- {inductive_mlxrl-0.2.3 → inductive_mlxrl-0.2.4}/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,23 @@ 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.2.4] - 2026-10-22
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- Require MLX-LM 0.32.0 or later with its MLX 0.32.2 minimum, and disable custom
|
|
13
|
+
model and tokenizer Python execution in library, example, and benchmark loaders. The older minimum
|
|
14
|
+
allowed the unsafe `model_file` behavior addressed by upstream's
|
|
15
|
+
[CVE-2026-5843 fix](https://github.com/ml-explore/mlx-lm/pull/1385).
|
|
16
|
+
- Parse answer and action tags in linear time to prevent malformed model
|
|
17
|
+
output from stalling reward evaluation and multi-turn rollouts.
|
|
18
|
+
- Reject CLI output paths whose symlinks resolve outside the working directory.
|
|
19
|
+
- Pin release actions to verified commits, freeze build and validation tools,
|
|
20
|
+
audit all locked dependencies in CI, and require release tags to match a
|
|
21
|
+
package version on main with successful push CI.
|
|
22
|
+
- Add security reporting guidance, ownership of release controls, and weekly
|
|
23
|
+
dependency and action updates.
|
|
24
|
+
|
|
8
25
|
## [0.2.3] - 2026-10-21
|
|
9
26
|
|
|
10
27
|
### 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
|
|
101
|
-
UV_CACHE_DIR=.uv-cache uv run
|
|
102
|
-
UV_CACHE_DIR=.uv-cache uv run
|
|
103
|
-
UV_CACHE_DIR=.uv-cache uv
|
|
104
|
-
UV_CACHE_DIR=.uv-cache uv run --no-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: inductive-mlxrl
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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.
|
|
23
|
-
Requires-Dist: mlx>=0.
|
|
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
|
|
@@ -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
|
|
@@ -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.
|
|
@@ -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(
|
|
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
|
|
@@ -536,7 +536,9 @@ def run_mlx_lm_worker(args: argparse.Namespace) -> BenchResult:
|
|
|
536
536
|
|
|
537
537
|
set_mlx_wired_limit(mx, args.wired_limit_mb)
|
|
538
538
|
mx.random.seed(args.seed)
|
|
539
|
-
loaded = mlx_lm.load(
|
|
539
|
+
loaded = mlx_lm.load(
|
|
540
|
+
args.model, tokenizer_config={"trust_remote_code": False}, trust_remote_code=False
|
|
541
|
+
)
|
|
540
542
|
model = loaded[0]
|
|
541
543
|
tokenizer = loaded[1]
|
|
542
544
|
sampler = make_sampler(
|
|
@@ -107,7 +107,9 @@ def main() -> None:
|
|
|
107
107
|
top_k=int(sampling_config["top_k"]),
|
|
108
108
|
)
|
|
109
109
|
prompt = naming_prompt(args.prompt)
|
|
110
|
-
base_model, base_tokenizer = load(
|
|
110
|
+
base_model, base_tokenizer = load(
|
|
111
|
+
str(run["model_id"]), tokenizer_config={"trust_remote_code": False}, trust_remote_code=False
|
|
112
|
+
)
|
|
111
113
|
if hasattr(base_model, "eval"):
|
|
112
114
|
base_model.eval()
|
|
113
115
|
base = sample_name(
|
|
@@ -247,6 +247,12 @@ def validate_output_path(value: str, field_name: str = "output") -> Path:
|
|
|
247
247
|
output_path = Path(value.strip())
|
|
248
248
|
if output_path.is_absolute() or ".." in output_path.parts:
|
|
249
249
|
raise ConfigError(f"{field_name} must be a relative path inside the working directory.")
|
|
250
|
+
try:
|
|
251
|
+
contained = output_path.resolve().is_relative_to(Path.cwd().resolve())
|
|
252
|
+
except (OSError, RuntimeError) as error:
|
|
253
|
+
raise ConfigError(f"{field_name} could not be resolved safely.") from error
|
|
254
|
+
if not contained:
|
|
255
|
+
raise ConfigError(f"{field_name} must be a relative path inside the working directory.")
|
|
250
256
|
return output_path
|
|
251
257
|
|
|
252
258
|
|
|
@@ -9,7 +9,8 @@ from typing import TypeAlias
|
|
|
9
9
|
RewardFn: TypeAlias = Callable[..., float]
|
|
10
10
|
|
|
11
11
|
_REWARD_REGISTRY: dict[str, RewardFn] = {}
|
|
12
|
-
|
|
12
|
+
_ANSWER_OPEN_RE = re.compile(r"<answer>", re.IGNORECASE)
|
|
13
|
+
_ANSWER_CLOSE_RE = re.compile(r"</answer>", re.IGNORECASE)
|
|
13
14
|
_NUMBER_RE = re.compile(r"[-+]?(?:\d[\d,]*)(?:\.\d+)?")
|
|
14
15
|
|
|
15
16
|
|
|
@@ -49,9 +50,8 @@ def extract_answer(text: str | None) -> str | None:
|
|
|
49
50
|
|
|
50
51
|
if text is None:
|
|
51
52
|
return None
|
|
52
|
-
|
|
53
|
-
if
|
|
54
|
-
answer = tagged.group(1).strip()
|
|
53
|
+
answer = _answer_tag_content(text)
|
|
54
|
+
if answer is not None:
|
|
55
55
|
return answer or None
|
|
56
56
|
if "####" in text:
|
|
57
57
|
answer = text.rsplit("####", maxsplit=1)[-1].strip()
|
|
@@ -94,8 +94,15 @@ def extract_answer_from_tag(text: str | None) -> str | None:
|
|
|
94
94
|
|
|
95
95
|
if text is None:
|
|
96
96
|
return None
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
return _answer_tag_content(text) or None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _answer_tag_content(text: str) -> str | None:
|
|
101
|
+
# Fixed delimiters avoid backtracking or repeated body rescans on malformed output.
|
|
102
|
+
opening = _ANSWER_OPEN_RE.search(text)
|
|
103
|
+
if opening is None:
|
|
104
|
+
return None
|
|
105
|
+
closing = _ANSWER_CLOSE_RE.search(text, opening.end())
|
|
106
|
+
if closing is None:
|
|
99
107
|
return None
|
|
100
|
-
|
|
101
|
-
return answer or None
|
|
108
|
+
return text[opening.end() : closing.start()].strip()
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import re
|
|
6
5
|
from collections.abc import Callable, Hashable, Mapping
|
|
7
6
|
from dataclasses import dataclass, field
|
|
8
7
|
from typing import Any
|
|
@@ -13,9 +12,12 @@ from mlxrl.env.base import StepResult
|
|
|
13
12
|
def default_action_parser(text: str) -> str:
|
|
14
13
|
"""Extract <action>...</action> when present, otherwise return stripped text."""
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
if
|
|
18
|
-
|
|
15
|
+
opening = text.find("<action>")
|
|
16
|
+
if opening != -1:
|
|
17
|
+
start = opening + len("<action>")
|
|
18
|
+
closing = text.find("</action>", start)
|
|
19
|
+
if closing != -1:
|
|
20
|
+
return text[start:closing].strip()
|
|
19
21
|
return text.strip()
|
|
20
22
|
|
|
21
23
|
|
|
@@ -288,7 +288,9 @@ def load_policy_with_lora(
|
|
|
288
288
|
"""Load a 4-bit MLX-LM policy and attach trainable LoRA adapters."""
|
|
289
289
|
|
|
290
290
|
lora_config = config or LoRAConfig()
|
|
291
|
-
loaded = load(
|
|
291
|
+
loaded = load(
|
|
292
|
+
model_id, tokenizer_config={"trust_remote_code": False}, trust_remote_code=False
|
|
293
|
+
)
|
|
292
294
|
model = loaded[0]
|
|
293
295
|
tokenizer = loaded[1]
|
|
294
296
|
target_keys = inject_lora_adapters(model, lora_config)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "inductive-mlxrl"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.4"
|
|
4
4
|
description = "Small single-process RL post-training for LLMs on Apple Silicon with MLX."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -18,8 +18,8 @@ classifiers = [
|
|
|
18
18
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
|
-
"mlx>=0.
|
|
22
|
-
"mlx-lm>=0.
|
|
21
|
+
"mlx>=0.32.2; python_version < '3.15' and platform_system == 'Darwin' and platform_machine == 'arm64'",
|
|
22
|
+
"mlx-lm>=0.32.0; python_version < '3.15' and platform_system == 'Darwin' and platform_machine == 'arm64'",
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
[project.urls]
|
|
@@ -39,17 +39,28 @@ dev = [
|
|
|
39
39
|
"pytest>=8.3.0",
|
|
40
40
|
"ruff>=0.8.0",
|
|
41
41
|
]
|
|
42
|
+
ci = [
|
|
43
|
+
"hatchling==1.32.4",
|
|
44
|
+
"pip-audit==2.10.1",
|
|
45
|
+
"pyright>=1.1.390",
|
|
46
|
+
"pytest>=8.3.0",
|
|
47
|
+
"ruff>=0.8.0",
|
|
48
|
+
"twine==7.0.0",
|
|
49
|
+
]
|
|
42
50
|
|
|
43
51
|
[build-system]
|
|
44
|
-
requires = ["hatchling"]
|
|
52
|
+
requires = ["hatchling==1.32.4"]
|
|
45
53
|
build-backend = "hatchling.build"
|
|
46
54
|
|
|
47
55
|
[tool.uv]
|
|
48
56
|
# mlx-tune 0.6 still pins datasets<4; use the patched series for CVE-2026-66007.
|
|
49
57
|
override-dependencies = ["datasets>=5.0.1,<6"]
|
|
58
|
+
# Newly patched security advisories require shorter cooldowns for these packages.
|
|
59
|
+
exclude-newer-package = { certifi = "1 day", pillow = "1 day" }
|
|
50
60
|
|
|
51
61
|
[tool.hatch.build.targets.sdist]
|
|
52
62
|
include = [
|
|
63
|
+
".github/scripts",
|
|
53
64
|
"benchmarks",
|
|
54
65
|
"examples",
|
|
55
66
|
"mlxrl",
|
|
@@ -59,8 +70,10 @@ include = [
|
|
|
59
70
|
"DESIGN.md",
|
|
60
71
|
"LICENSE",
|
|
61
72
|
"README.md",
|
|
73
|
+
"SECURITY.md",
|
|
62
74
|
"THIRD_PARTY_LICENSES.md",
|
|
63
75
|
"pyproject.toml",
|
|
76
|
+
"uv.lock",
|
|
64
77
|
]
|
|
65
78
|
|
|
66
79
|
[tool.hatch.build.targets.wheel]
|
|
@@ -82,7 +95,7 @@ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
|
82
95
|
pythonVersion = "3.11"
|
|
83
96
|
typeCheckingMode = "basic"
|
|
84
97
|
reportMissingTypeStubs = "none"
|
|
85
|
-
include = ["mlxrl", "tests", "benchmarks"]
|
|
98
|
+
include = ["mlxrl", "tests", "benchmarks", ".github/scripts"]
|
|
86
99
|
|
|
87
100
|
[tool.pytest.ini_options]
|
|
88
101
|
markers = [
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from typing import Any, cast
|
|
4
5
|
|
|
5
6
|
import mlx.core as mx
|
|
6
7
|
import mlx.nn as nn
|
|
7
8
|
import pytest
|
|
9
|
+
from mlx.utils import tree_flatten
|
|
10
|
+
from mlx_lm.models.llama import Model, ModelArgs
|
|
8
11
|
from mlx_lm.tuner.lora import LoRALinear
|
|
9
12
|
|
|
10
13
|
from mlxrl.policy.model import (
|
|
@@ -15,6 +18,7 @@ from mlxrl.policy.model import (
|
|
|
15
18
|
count_parameters,
|
|
16
19
|
enable_grad_checkpointing,
|
|
17
20
|
inject_lora_adapters,
|
|
21
|
+
load_policy_with_lora,
|
|
18
22
|
lora_module_counts_by_layer,
|
|
19
23
|
strict_lora_config,
|
|
20
24
|
)
|
|
@@ -22,6 +26,60 @@ from mlxrl.policy.model import (
|
|
|
22
26
|
pytestmark = pytest.mark.metal
|
|
23
27
|
|
|
24
28
|
|
|
29
|
+
def test_model_loading_rejects_custom_code_without_executing_it(tmp_path) -> None:
|
|
30
|
+
marker = tmp_path / "executed.txt"
|
|
31
|
+
(tmp_path / "config.json").write_text(json.dumps({"model_file": "custom.py"}))
|
|
32
|
+
(tmp_path / "custom.py").write_text(
|
|
33
|
+
"from pathlib import Path\n"
|
|
34
|
+
"Path(__file__).with_name('executed.txt').write_text('executed')\n"
|
|
35
|
+
"raise RuntimeError('custom model code ran')\n"
|
|
36
|
+
)
|
|
37
|
+
mx.save_safetensors(tmp_path / "model.safetensors", {"dummy": mx.array([0.0])})
|
|
38
|
+
|
|
39
|
+
with pytest.raises(ValueError, match="trust_remote_code"):
|
|
40
|
+
load_policy_with_lora(str(tmp_path))
|
|
41
|
+
|
|
42
|
+
assert not marker.exists()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_tokenizer_loading_rejects_custom_code_without_prompting(tmp_path, monkeypatch) -> None:
|
|
46
|
+
config = {
|
|
47
|
+
"model_type": "llama",
|
|
48
|
+
"hidden_size": 8,
|
|
49
|
+
"num_hidden_layers": 1,
|
|
50
|
+
"intermediate_size": 16,
|
|
51
|
+
"num_attention_heads": 2,
|
|
52
|
+
"rms_norm_eps": 1e-5,
|
|
53
|
+
"vocab_size": 16,
|
|
54
|
+
}
|
|
55
|
+
model = Model(ModelArgs.from_dict(config))
|
|
56
|
+
mx.save_safetensors(tmp_path / "model.safetensors", dict(tree_flatten(model.parameters())))
|
|
57
|
+
(tmp_path / "config.json").write_text(json.dumps(config))
|
|
58
|
+
(tmp_path / "tokenizer_config.json").write_text(
|
|
59
|
+
json.dumps(
|
|
60
|
+
{
|
|
61
|
+
"tokenizer_class": "CustomTokenizer",
|
|
62
|
+
"auto_map": {"AutoTokenizer": ["tokenization_custom.CustomTokenizer", None]},
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
marker = tmp_path / "tokenizer-executed.txt"
|
|
67
|
+
(tmp_path / "tokenization_custom.py").write_text(
|
|
68
|
+
"from pathlib import Path\n"
|
|
69
|
+
"Path(__file__).with_name('tokenizer-executed.txt').write_text('executed')\n"
|
|
70
|
+
"raise RuntimeError('custom tokenizer code ran')\n"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def reject_prompt(*args, **kwargs):
|
|
74
|
+
pytest.fail("Loading a model must not prompt to execute tokenizer code.")
|
|
75
|
+
|
|
76
|
+
monkeypatch.setattr("builtins.input", reject_prompt)
|
|
77
|
+
with pytest.raises(ValueError, match="trust_remote_code"):
|
|
78
|
+
load_policy_with_lora(str(tmp_path))
|
|
79
|
+
|
|
80
|
+
assert not marker.exists()
|
|
81
|
+
|
|
82
|
+
|
|
25
83
|
class ToyDenseAttention(nn.Module):
|
|
26
84
|
def __init__(self) -> None:
|
|
27
85
|
super().__init__()
|