inductive-mlxrl 0.2.0__tar.gz → 0.2.1__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.0 → inductive_mlxrl-0.2.1}/CHANGELOG.md +10 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/PKG-INFO +1 -1
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/examples/qwen35_9b_g2_checkpoint.toml +1 -1
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/examples/qwen3_0_6b_grpo.toml +1 -2
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/__init__.py +1 -1
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/algo/grpo.py +36 -16
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/policy/logprobs.py +98 -38
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/policy/model.py +16 -1
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/policy/trajectory_logprobs.py +15 -23
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/rollout/agentic.py +1 -1
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/rollout/optimized.py +78 -34
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/train/grpo.py +95 -29
- inductive_mlxrl-0.2.1/mlxrl/train/reference.py +25 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/train/trajectory.py +41 -25
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/pyproject.toml +1 -1
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_agentic_rollout.py +77 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_grpo.py +36 -2
- inductive_mlxrl-0.2.1/tests/test_optimized_rollout.py +181 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_policy_logprobs.py +47 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_train_grpo.py +130 -1
- inductive_mlxrl-0.2.1/tests/test_train_trajectory.py +286 -0
- inductive_mlxrl-0.2.0/tests/test_optimized_rollout.py +0 -76
- inductive_mlxrl-0.2.0/tests/test_train_trajectory.py +0 -117
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/.gitignore +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/CONTRIBUTING.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/DESIGN.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/LICENSE +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/README.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/THIRD_PARTY_LICENSES.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/__init__.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/audit_phase4_config.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/external_baseline_worker.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after_final.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after_final.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after_noprefixsync.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after_noprefixsync.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_before.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_before.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate2_unified_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate2_unified_smoke.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_mlx_tune_sync_256.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_mlx_tune_sync_256.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_sync_blocked.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_sync_blocked.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_sync_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_sync_smoke.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate4_config_audit.json +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate4_config_audit.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate4_sampler_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate4_sampler_smoke.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate5_full_reconciled.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate5_full_reconciled.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_external_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_external_smoke.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_full_all_targets.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_full_all_targets.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_mlx_lm_g4_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_mlx_lm_g4_smoke.md +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/run_phase4.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/examples/qwen3_0_6b_gigpo_recurring.toml +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/algo/__init__.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/algo/gigpo.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/algorithm.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/cli.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/config.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/data/__init__.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/data/gsm8k.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/data/rewards.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/env/__init__.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/env/base.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/env/reference.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/policy/__init__.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/py.typed +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/rollout/__init__.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/rollout/naive.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/train/__init__.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/trajectory.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_agentic_env.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_benchmark_metrics.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_cli.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_config.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_gigpo.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_import_direction.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_policy_model.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_rewards.py +0 -0
- {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_trajectory_logprobs.py +0 -0
|
@@ -5,6 +5,16 @@ 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.1] - 2026-06-25
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Keep model-backed agentic/GiGPO decode tokens rank-2 during KV-cache decode,
|
|
13
|
+
fixing real Qwen3 model rollouts.
|
|
14
|
+
- Use valid repo-relative output paths in the committed Qwen smoke configs.
|
|
15
|
+
- Preserve eval-mode old-policy/reference logprob prep while restoring caller
|
|
16
|
+
train mode afterward.
|
|
17
|
+
|
|
8
18
|
## [0.2.0] - 2026-06-24
|
|
9
19
|
|
|
10
20
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: inductive-mlxrl
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -13,7 +13,7 @@ gradient_checkpointing = true
|
|
|
13
13
|
checkpoint_granularity = "per-layer"
|
|
14
14
|
iogpu_wired_limit_mb = 49152
|
|
15
15
|
micro_batch_size = 0
|
|
16
|
-
output = "/
|
|
16
|
+
output = "reference_outputs/mlxrl_qwen35_9b_g2_checkpoint.npz"
|
|
17
17
|
|
|
18
18
|
[optimizer]
|
|
19
19
|
name = "adam"
|
|
@@ -13,7 +13,7 @@ gradient_checkpointing = false
|
|
|
13
13
|
checkpoint_granularity = "per-layer"
|
|
14
14
|
iogpu_wired_limit_mb = 49152
|
|
15
15
|
micro_batch_size = 0
|
|
16
|
-
output = "/
|
|
16
|
+
output = "reference_outputs/mlxrl_config_smoke.npz"
|
|
17
17
|
|
|
18
18
|
[optimizer]
|
|
19
19
|
name = "adam"
|
|
@@ -27,4 +27,3 @@ top_k = 0
|
|
|
27
27
|
|
|
28
28
|
[algorithm]
|
|
29
29
|
name = "grpo"
|
|
30
|
-
|
|
@@ -494,17 +494,28 @@ def filter_zero_advantage_groups(batch: Any, group_size: int) -> Any:
|
|
|
494
494
|
raise ValueError("DAPO dynamic sampling dropped every group.")
|
|
495
495
|
|
|
496
496
|
indices = mx.array(keep_indices, dtype=mx.int32)
|
|
497
|
-
|
|
498
|
-
prompt_token_ids
|
|
499
|
-
completion_token_ids
|
|
497
|
+
filtered = {
|
|
498
|
+
"prompt_token_ids": tuple(batch.prompt_token_ids[index] for index in keep_indices),
|
|
499
|
+
"completion_token_ids": tuple(
|
|
500
500
|
batch.completion_token_ids[index] for index in keep_indices
|
|
501
501
|
),
|
|
502
|
-
rewards
|
|
503
|
-
advantages
|
|
504
|
-
old_policy_logprobs
|
|
505
|
-
reference_logprobs
|
|
506
|
-
mask
|
|
507
|
-
|
|
502
|
+
"rewards": batch.rewards[indices],
|
|
503
|
+
"advantages": batch.advantages[indices],
|
|
504
|
+
"old_policy_logprobs": batch.old_policy_logprobs[indices],
|
|
505
|
+
"reference_logprobs": batch.reference_logprobs[indices],
|
|
506
|
+
"mask": batch.mask[indices],
|
|
507
|
+
}
|
|
508
|
+
if hasattr(batch, "reference_is_policy"):
|
|
509
|
+
filtered["reference_is_policy"] = batch.reference_is_policy
|
|
510
|
+
logprob_inputs = getattr(batch, "logprob_inputs", None)
|
|
511
|
+
if logprob_inputs is not None:
|
|
512
|
+
filtered["logprob_inputs"] = type(logprob_inputs)(
|
|
513
|
+
input_ids=logprob_inputs.input_ids[indices],
|
|
514
|
+
target_ids=logprob_inputs.target_ids[indices],
|
|
515
|
+
gather_indices=logprob_inputs.gather_indices[indices],
|
|
516
|
+
mask=logprob_inputs.mask[indices],
|
|
517
|
+
)
|
|
518
|
+
return type(batch)(**filtered)
|
|
508
519
|
|
|
509
520
|
|
|
510
521
|
def _token_loss_metrics(
|
|
@@ -516,17 +527,26 @@ def _token_loss_metrics(
|
|
|
516
527
|
beta: float,
|
|
517
528
|
clipped_ratio: mx.array | None = None,
|
|
518
529
|
) -> AlgorithmLossMetrics:
|
|
519
|
-
|
|
520
|
-
|
|
530
|
+
mask = mask.astype(mx.float32)
|
|
531
|
+
denominator = mx.maximum(mx.sum(mask), mx.array(1.0, dtype=mx.float32))
|
|
532
|
+
policy_gradient_loss = mx.sum(token_policy_gradient * mask) / denominator
|
|
533
|
+
if beta == 0.0 and reference_logprobs is policy_logprobs:
|
|
534
|
+
kl = mx.array(0.0, dtype=mx.float32)
|
|
535
|
+
else:
|
|
536
|
+
token_kl = approximate_kl(policy_logprobs, reference_logprobs)
|
|
537
|
+
kl = mx.sum(token_kl * mask) / denominator
|
|
538
|
+
mean_ratio = mx.sum(ratio * mask) / denominator
|
|
521
539
|
if clipped_ratio is None:
|
|
522
540
|
clip_fraction = mx.array(0.0, dtype=mx.float32)
|
|
523
541
|
else:
|
|
524
|
-
clip_fraction =
|
|
542
|
+
clip_fraction = (
|
|
543
|
+
mx.sum(clip_indicator(ratio, clipped_ratio) * mask) / denominator
|
|
544
|
+
)
|
|
525
545
|
return AlgorithmLossMetrics(
|
|
526
|
-
loss=
|
|
527
|
-
policy_gradient_loss=
|
|
528
|
-
kl=
|
|
529
|
-
mean_ratio=
|
|
546
|
+
loss=policy_gradient_loss + beta * kl,
|
|
547
|
+
policy_gradient_loss=policy_gradient_loss,
|
|
548
|
+
kl=kl,
|
|
549
|
+
mean_ratio=mean_ratio,
|
|
530
550
|
clip_fraction=clip_fraction,
|
|
531
551
|
)
|
|
532
552
|
|
|
@@ -27,6 +27,16 @@ class CompletionLogprobs:
|
|
|
27
27
|
mask: mx.array
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class CompletionLogprobInputs:
|
|
32
|
+
"""Prepared padded tensors for a completion full-forward logprob pass."""
|
|
33
|
+
|
|
34
|
+
input_ids: mx.array
|
|
35
|
+
target_ids: mx.array
|
|
36
|
+
gather_indices: mx.array
|
|
37
|
+
mask: mx.array
|
|
38
|
+
|
|
39
|
+
|
|
30
40
|
@dataclass(frozen=True)
|
|
31
41
|
class DualLogprobs:
|
|
32
42
|
"""Policy and reference logprobs for the same completion tokens."""
|
|
@@ -61,6 +71,25 @@ def completion_logprobs(
|
|
|
61
71
|
) -> CompletionLogprobs:
|
|
62
72
|
"""Gather logprobs assigned to completion tokens in a full forward pass."""
|
|
63
73
|
|
|
74
|
+
inputs = prepare_completion_logprob_inputs(
|
|
75
|
+
prompt_token_ids,
|
|
76
|
+
completion_token_ids,
|
|
77
|
+
pad_token_id=pad_token_id,
|
|
78
|
+
)
|
|
79
|
+
return completion_logprobs_from_inputs(
|
|
80
|
+
model,
|
|
81
|
+
inputs,
|
|
82
|
+
use_checkpoint=use_checkpoint,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def prepare_completion_logprob_inputs(
|
|
87
|
+
prompt_token_ids: Sequence[Sequence[int]],
|
|
88
|
+
completion_token_ids: Sequence[Sequence[int]],
|
|
89
|
+
pad_token_id: int = 0,
|
|
90
|
+
) -> CompletionLogprobInputs:
|
|
91
|
+
"""Build reusable padded tensors for completion logprob gathering."""
|
|
92
|
+
|
|
64
93
|
if len(prompt_token_ids) != len(completion_token_ids):
|
|
65
94
|
raise ValueError("prompt_token_ids and completion_token_ids must have the same length.")
|
|
66
95
|
if not prompt_token_ids:
|
|
@@ -81,8 +110,7 @@ def completion_logprobs(
|
|
|
81
110
|
input_rows: list[list[int]] = []
|
|
82
111
|
target_rows: list[list[int]] = []
|
|
83
112
|
completion_mask_rows: list[list[float]] = []
|
|
84
|
-
|
|
85
|
-
completion_lengths: list[int] = []
|
|
113
|
+
gather_index_rows: list[list[int]] = []
|
|
86
114
|
|
|
87
115
|
for prompt, completion, sequence in zip(
|
|
88
116
|
prompt_token_ids,
|
|
@@ -100,38 +128,50 @@ def completion_logprobs(
|
|
|
100
128
|
mask_row = [0.0] * max_completion_len
|
|
101
129
|
for index in range(len(completion)):
|
|
102
130
|
mask_row[index] = 1.0
|
|
131
|
+
gather_index_rows.append(
|
|
132
|
+
[
|
|
133
|
+
completion_start + index if index < len(completion) else 0
|
|
134
|
+
for index in range(max_completion_len)
|
|
135
|
+
]
|
|
136
|
+
)
|
|
103
137
|
|
|
104
138
|
input_rows.append(input_row)
|
|
105
139
|
target_rows.append(target_row)
|
|
106
140
|
completion_mask_rows.append(mask_row)
|
|
107
|
-
completion_starts.append(completion_start)
|
|
108
|
-
completion_lengths.append(len(completion))
|
|
109
141
|
|
|
110
142
|
input_ids = mx.array(input_rows, dtype=mx.int32)
|
|
111
143
|
target_ids = mx.array(target_rows, dtype=mx.int32)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
gather_indices = mx.array(gather_index_rows, dtype=mx.int32)
|
|
145
|
+
mask = mx.array(completion_mask_rows, dtype=mx.float32)
|
|
146
|
+
return CompletionLogprobInputs(
|
|
147
|
+
input_ids=input_ids,
|
|
148
|
+
target_ids=target_ids,
|
|
149
|
+
gather_indices=gather_indices,
|
|
150
|
+
mask=mask,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def completion_logprobs_from_inputs(
|
|
155
|
+
model: nn.Module,
|
|
156
|
+
inputs: CompletionLogprobInputs,
|
|
157
|
+
use_checkpoint: bool = False,
|
|
158
|
+
) -> CompletionLogprobs:
|
|
159
|
+
"""Gather completion-token logprobs from prebuilt full-forward tensors."""
|
|
160
|
+
|
|
161
|
+
logits = _completion_forward(model, inputs.input_ids, use_checkpoint=use_checkpoint)
|
|
162
|
+
target_logprobs = target_logprobs_from_logits(logits, inputs.target_ids)
|
|
163
|
+
completion_logprob_values = mx.take_along_axis(
|
|
164
|
+
target_logprobs,
|
|
165
|
+
inputs.gather_indices,
|
|
166
|
+
axis=1,
|
|
167
|
+
)
|
|
168
|
+
completion_logprob_values = completion_logprob_values * inputs.mask.astype(
|
|
169
|
+
completion_logprob_values.dtype
|
|
170
|
+
)
|
|
131
171
|
|
|
132
172
|
return CompletionLogprobs(
|
|
133
|
-
logprobs=
|
|
134
|
-
mask=
|
|
173
|
+
logprobs=completion_logprob_values,
|
|
174
|
+
mask=inputs.mask,
|
|
135
175
|
)
|
|
136
176
|
|
|
137
177
|
|
|
@@ -186,16 +226,12 @@ def prefix_cached_completion_logprobs(
|
|
|
186
226
|
dtype=mx.int32,
|
|
187
227
|
)
|
|
188
228
|
logits = model(chunks, cache=batch_cache)
|
|
189
|
-
|
|
190
|
-
grouped_logprobs = mx.squeeze(
|
|
191
|
-
mx.take_along_axis(logprobs, targets[..., None], axis=-1),
|
|
192
|
-
axis=-1,
|
|
193
|
-
)
|
|
229
|
+
grouped_logprobs = target_logprobs_from_logits(logits, targets)
|
|
194
230
|
group_mask = mx.array(
|
|
195
231
|
[
|
|
196
232
|
[1.0] * len(completion_token_ids[index])
|
|
197
233
|
+ [0.0] * (max_completion_len - len(completion_token_ids[index]))
|
|
198
|
-
|
|
234
|
+
for index in row_indices
|
|
199
235
|
],
|
|
200
236
|
dtype=grouped_logprobs.dtype,
|
|
201
237
|
)
|
|
@@ -216,16 +252,24 @@ def dual_logprobs(
|
|
|
216
252
|
pad_token_id: int = 0,
|
|
217
253
|
use_checkpoint: bool = False,
|
|
218
254
|
compute_reference: bool = True,
|
|
255
|
+
prepared_inputs: CompletionLogprobInputs | None = None,
|
|
219
256
|
) -> DualLogprobs:
|
|
220
257
|
"""Compute policy logprobs and reference logprobs using one model object."""
|
|
221
258
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
259
|
+
if prepared_inputs is None:
|
|
260
|
+
policy = completion_logprobs(
|
|
261
|
+
model,
|
|
262
|
+
prompt_token_ids,
|
|
263
|
+
completion_token_ids,
|
|
264
|
+
pad_token_id,
|
|
265
|
+
use_checkpoint=use_checkpoint,
|
|
266
|
+
)
|
|
267
|
+
else:
|
|
268
|
+
policy = completion_logprobs_from_inputs(
|
|
269
|
+
model,
|
|
270
|
+
prepared_inputs,
|
|
271
|
+
use_checkpoint=use_checkpoint,
|
|
272
|
+
)
|
|
229
273
|
if not compute_reference:
|
|
230
274
|
return DualLogprobs(
|
|
231
275
|
policy=policy.logprobs,
|
|
@@ -287,6 +331,22 @@ def _completion_forward(
|
|
|
287
331
|
return model(input_ids)
|
|
288
332
|
|
|
289
333
|
|
|
334
|
+
def target_logprobs_from_logits(logits: mx.array, target_ids: mx.array) -> mx.array:
|
|
335
|
+
"""Gather target-token logprobs without materializing full log-softmax.
|
|
336
|
+
|
|
337
|
+
This is algebraically equivalent to gathering from
|
|
338
|
+
``logits - logsumexp(logits)`` but avoids a large [batch, seq, vocab]
|
|
339
|
+
intermediate on training forwards.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
target_logits = mx.squeeze(
|
|
343
|
+
mx.take_along_axis(logits, target_ids[..., None], axis=-1),
|
|
344
|
+
axis=-1,
|
|
345
|
+
)
|
|
346
|
+
normalizer = mx.squeeze(mx.logsumexp(logits, axis=-1, keepdims=True), axis=-1)
|
|
347
|
+
return target_logits - normalizer
|
|
348
|
+
|
|
349
|
+
|
|
290
350
|
def pad_token_id_from_tokenizer(tokenizer: Any) -> int:
|
|
291
351
|
"""Resolve a safe right-padding token id from a tokenizer."""
|
|
292
352
|
|
|
@@ -6,7 +6,8 @@ per-layer checkpoint helpers (MIT, Copyright Apple Inc.).
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from collections.abc import Iterable
|
|
9
|
+
from collections.abc import Iterable, Iterator
|
|
10
|
+
from contextlib import contextmanager
|
|
10
11
|
from dataclasses import dataclass
|
|
11
12
|
from typing import Any, cast
|
|
12
13
|
|
|
@@ -152,6 +153,20 @@ def enable_grad_checkpointing(model: nn.Module) -> bool:
|
|
|
152
153
|
return True
|
|
153
154
|
|
|
154
155
|
|
|
156
|
+
@contextmanager
|
|
157
|
+
def temporary_eval_mode(model: nn.Module) -> Iterator[None]:
|
|
158
|
+
"""Run non-gradient work in eval mode, then restore the caller's mode."""
|
|
159
|
+
|
|
160
|
+
was_training = bool(getattr(model, "training", False))
|
|
161
|
+
if was_training:
|
|
162
|
+
model.eval()
|
|
163
|
+
try:
|
|
164
|
+
yield
|
|
165
|
+
finally:
|
|
166
|
+
if was_training:
|
|
167
|
+
model.train()
|
|
168
|
+
|
|
169
|
+
|
|
155
170
|
def inject_lora_adapters(model: nn.Module, config: LoRAConfig) -> tuple[str, ...]:
|
|
156
171
|
"""Inject MLX-LM LoRA layers into all attention and MLP projections."""
|
|
157
172
|
|
|
@@ -8,7 +8,7 @@ from dataclasses import dataclass
|
|
|
8
8
|
import mlx.core as mx
|
|
9
9
|
import mlx.nn as nn
|
|
10
10
|
|
|
11
|
-
from mlxrl.policy.logprobs import adapters_disabled
|
|
11
|
+
from mlxrl.policy.logprobs import adapters_disabled, target_logprobs_from_logits
|
|
12
12
|
from mlxrl.trajectory import Trajectory
|
|
13
13
|
|
|
14
14
|
|
|
@@ -47,7 +47,7 @@ def trajectory_action_logprobs(
|
|
|
47
47
|
|
|
48
48
|
input_rows: list[list[int]] = []
|
|
49
49
|
target_rows: list[list[int]] = []
|
|
50
|
-
|
|
50
|
+
action_target_index_rows: list[list[int]] = []
|
|
51
51
|
mask_rows: list[list[float]] = []
|
|
52
52
|
target_width = max_sequence_len - 1
|
|
53
53
|
for trajectory in trajectories:
|
|
@@ -59,35 +59,27 @@ def trajectory_action_logprobs(
|
|
|
59
59
|
indices.extend(range(span.start - 1, span.end - 1))
|
|
60
60
|
input_rows.append(input_row)
|
|
61
61
|
target_rows.append(target_row)
|
|
62
|
-
|
|
62
|
+
action_target_index_rows.append(
|
|
63
|
+
[indices[index] if index < len(indices) else 0 for index in range(max_action_len)]
|
|
64
|
+
)
|
|
63
65
|
mask_rows.append([1.0] * len(indices) + [0.0] * (max_action_len - len(indices)))
|
|
64
66
|
|
|
65
67
|
input_ids = mx.array(input_rows, dtype=mx.int32)
|
|
66
68
|
target_ids = mx.array(target_rows, dtype=mx.int32)
|
|
67
69
|
logits = model(input_ids)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
target_logprobs = target_logprobs_from_logits(logits, target_ids)
|
|
71
|
+
action_indices = mx.array(action_target_index_rows, dtype=mx.int32)
|
|
72
|
+
mask = mx.array(mask_rows, dtype=mx.float32)
|
|
73
|
+
action_logprobs = mx.take_along_axis(
|
|
74
|
+
target_logprobs,
|
|
75
|
+
action_indices,
|
|
76
|
+
axis=1,
|
|
72
77
|
)
|
|
73
|
-
|
|
74
|
-
rows: list[mx.array] = []
|
|
75
|
-
for row_index, indices in enumerate(action_target_indices):
|
|
76
|
-
row_values = [target_logprobs[row_index, index] for index in indices]
|
|
77
|
-
row = mx.stack(row_values, axis=0)
|
|
78
|
-
if len(indices) < max_action_len:
|
|
79
|
-
row = mx.concatenate(
|
|
80
|
-
[
|
|
81
|
-
row,
|
|
82
|
-
mx.zeros((max_action_len - len(indices),), dtype=row.dtype),
|
|
83
|
-
],
|
|
84
|
-
axis=0,
|
|
85
|
-
)
|
|
86
|
-
rows.append(row)
|
|
78
|
+
action_logprobs = action_logprobs * mask.astype(action_logprobs.dtype)
|
|
87
79
|
|
|
88
80
|
return TrajectoryLogprobs(
|
|
89
|
-
logprobs=
|
|
90
|
-
mask=
|
|
81
|
+
logprobs=action_logprobs,
|
|
82
|
+
mask=mask,
|
|
91
83
|
)
|
|
92
84
|
|
|
93
85
|
|
|
@@ -295,7 +295,7 @@ def _generate_model_action(
|
|
|
295
295
|
if index > 0:
|
|
296
296
|
if current is None:
|
|
297
297
|
raise RuntimeError("Internal error: missing previous decode token.")
|
|
298
|
-
logits = model(current, cache=state.cache)
|
|
298
|
+
logits = model(current[None], cache=state.cache)
|
|
299
299
|
logprobs = logits[:, -1, :] - mx.logsumexp(logits[:, -1, :], axis=-1, keepdims=True)
|
|
300
300
|
next_token = sampler(logprobs)
|
|
301
301
|
sampled_logprob = sampled_token_logprobs(logprobs, next_token)
|
|
@@ -264,13 +264,6 @@ def generate_from_prefix_cache(
|
|
|
264
264
|
min_p=config.min_p,
|
|
265
265
|
top_k=config.top_k,
|
|
266
266
|
)
|
|
267
|
-
cache: list[Any]
|
|
268
|
-
if compile_decode_step:
|
|
269
|
-
cache = fixed_decode_cache_from_prefix(prefix.cache, config.max_tokens)
|
|
270
|
-
decode_logprobs = _compiled_decode_logprobs(model, cache)
|
|
271
|
-
else:
|
|
272
|
-
cache = clone_prompt_cache(prefix.cache)
|
|
273
|
-
decode_logprobs = _decode_logprobs(model, cache)
|
|
274
267
|
completion: list[int] = []
|
|
275
268
|
old_policy_logprobs: list[float] = []
|
|
276
269
|
|
|
@@ -290,6 +283,14 @@ def generate_from_prefix_cache(
|
|
|
290
283
|
decode_completion(tokenizer, completion),
|
|
291
284
|
)
|
|
292
285
|
|
|
286
|
+
cache: list[Any]
|
|
287
|
+
if compile_decode_step:
|
|
288
|
+
cache = fixed_decode_cache_from_prefix(prefix.cache, config.max_tokens)
|
|
289
|
+
decode_logprobs = _compiled_decode_logprobs(model, cache)
|
|
290
|
+
else:
|
|
291
|
+
cache = clone_prompt_cache(prefix.cache)
|
|
292
|
+
decode_logprobs = _decode_logprobs(model, cache)
|
|
293
|
+
|
|
293
294
|
current = next_token
|
|
294
295
|
for _ in range(1, config.max_tokens):
|
|
295
296
|
logprobs = decode_logprobs(current)
|
|
@@ -364,16 +365,6 @@ def generate_prompt_set_from_prefix_caches(
|
|
|
364
365
|
max_tokens=config.max_tokens,
|
|
365
366
|
)
|
|
366
367
|
|
|
367
|
-
cache: list[Any] = fixed_decode_cache_from_prefixes(
|
|
368
|
-
prefixes,
|
|
369
|
-
group_size=group_size,
|
|
370
|
-
max_tokens=config.max_tokens,
|
|
371
|
-
)
|
|
372
|
-
decode_logprobs = (
|
|
373
|
-
_compiled_decode_logprobs(model, cache)
|
|
374
|
-
if compile_decode_step
|
|
375
|
-
else _decode_logprobs(model, cache)
|
|
376
|
-
)
|
|
377
368
|
first_logprobs = mx.concatenate(
|
|
378
369
|
[
|
|
379
370
|
mx.broadcast_to(
|
|
@@ -409,6 +400,31 @@ def generate_prompt_set_from_prefix_caches(
|
|
|
409
400
|
active[row_index] = False
|
|
410
401
|
early_eos = config.max_tokens > 1
|
|
411
402
|
|
|
403
|
+
if early_eos and draw_keys is not None:
|
|
404
|
+
_set_random_state(start_random_state)
|
|
405
|
+
return _generate_prompt_set_sequentially(
|
|
406
|
+
model=model,
|
|
407
|
+
tokenizer=tokenizer,
|
|
408
|
+
prefixes=prefixes,
|
|
409
|
+
group_size=group_size,
|
|
410
|
+
config=config,
|
|
411
|
+
eos_token_ids=eos_token_ids,
|
|
412
|
+
compile_decode_step=compile_decode_step,
|
|
413
|
+
)
|
|
414
|
+
if not any(active):
|
|
415
|
+
return _rollout_rows_to_outputs(tokenizer, rows, old_policy_logprob_rows)
|
|
416
|
+
|
|
417
|
+
cache: list[Any] = fixed_decode_cache_from_prefixes(
|
|
418
|
+
prefixes,
|
|
419
|
+
group_size=group_size,
|
|
420
|
+
max_tokens=config.max_tokens,
|
|
421
|
+
)
|
|
422
|
+
decode_logprobs = (
|
|
423
|
+
_compiled_decode_logprobs(model, cache)
|
|
424
|
+
if compile_decode_step
|
|
425
|
+
else _decode_logprobs(model, cache)
|
|
426
|
+
)
|
|
427
|
+
|
|
412
428
|
for step in range(1, config.max_tokens):
|
|
413
429
|
logprobs = decode_logprobs(current)
|
|
414
430
|
next_token = _sample_batched_logprobs(
|
|
@@ -432,29 +448,57 @@ def generate_prompt_set_from_prefix_caches(
|
|
|
432
448
|
active[row_index] = False
|
|
433
449
|
early_eos = step < config.max_tokens - 1
|
|
434
450
|
current = next_token
|
|
451
|
+
if early_eos and draw_keys is not None:
|
|
452
|
+
_set_random_state(start_random_state)
|
|
453
|
+
return _generate_prompt_set_sequentially(
|
|
454
|
+
model=model,
|
|
455
|
+
tokenizer=tokenizer,
|
|
456
|
+
prefixes=prefixes,
|
|
457
|
+
group_size=group_size,
|
|
458
|
+
config=config,
|
|
459
|
+
eos_token_ids=eos_token_ids,
|
|
460
|
+
compile_decode_step=compile_decode_step,
|
|
461
|
+
)
|
|
435
462
|
if not any(active):
|
|
436
463
|
break
|
|
437
464
|
|
|
438
|
-
if early_eos:
|
|
439
|
-
_set_random_state(start_random_state)
|
|
440
|
-
outputs: list[tuple[tuple[int, ...], tuple[float, ...], str]] = []
|
|
441
|
-
for prefix in prefixes:
|
|
442
|
-
outputs.extend(
|
|
443
|
-
generate_from_prefix_cache(
|
|
444
|
-
model=model,
|
|
445
|
-
tokenizer=tokenizer,
|
|
446
|
-
prefix=prefix,
|
|
447
|
-
config=config,
|
|
448
|
-
eos_token_ids=eos_token_ids,
|
|
449
|
-
compile_decode_step=compile_decode_step,
|
|
450
|
-
)
|
|
451
|
-
for _ in range(group_size)
|
|
452
|
-
)
|
|
453
|
-
return tuple(outputs)
|
|
454
|
-
|
|
455
465
|
if final_random_state is not None:
|
|
456
466
|
_set_random_state(final_random_state)
|
|
457
467
|
|
|
468
|
+
return _rollout_rows_to_outputs(tokenizer, rows, old_policy_logprob_rows)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _generate_prompt_set_sequentially(
|
|
472
|
+
*,
|
|
473
|
+
model: nn.Module,
|
|
474
|
+
tokenizer: Any,
|
|
475
|
+
prefixes: Sequence[PrefixCache],
|
|
476
|
+
group_size: int,
|
|
477
|
+
config: SamplingConfig,
|
|
478
|
+
eos_token_ids: frozenset[int],
|
|
479
|
+
compile_decode_step: bool,
|
|
480
|
+
) -> tuple[tuple[tuple[int, ...], tuple[float, ...], str], ...]:
|
|
481
|
+
outputs: list[tuple[tuple[int, ...], tuple[float, ...], str]] = []
|
|
482
|
+
for prefix in prefixes:
|
|
483
|
+
outputs.extend(
|
|
484
|
+
generate_from_prefix_cache(
|
|
485
|
+
model=model,
|
|
486
|
+
tokenizer=tokenizer,
|
|
487
|
+
prefix=prefix,
|
|
488
|
+
config=config,
|
|
489
|
+
eos_token_ids=eos_token_ids,
|
|
490
|
+
compile_decode_step=compile_decode_step,
|
|
491
|
+
)
|
|
492
|
+
for _ in range(group_size)
|
|
493
|
+
)
|
|
494
|
+
return tuple(outputs)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _rollout_rows_to_outputs(
|
|
498
|
+
tokenizer: Any,
|
|
499
|
+
rows: Sequence[Sequence[int]],
|
|
500
|
+
old_policy_logprob_rows: Sequence[Sequence[float]],
|
|
501
|
+
) -> tuple[tuple[tuple[int, ...], tuple[float, ...], str], ...]:
|
|
458
502
|
return tuple(
|
|
459
503
|
(
|
|
460
504
|
tuple(row),
|