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.
Files changed (90) hide show
  1. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/CHANGELOG.md +10 -0
  2. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/PKG-INFO +1 -1
  3. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/examples/qwen35_9b_g2_checkpoint.toml +1 -1
  4. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/examples/qwen3_0_6b_grpo.toml +1 -2
  5. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/__init__.py +1 -1
  6. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/algo/grpo.py +36 -16
  7. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/policy/logprobs.py +98 -38
  8. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/policy/model.py +16 -1
  9. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/policy/trajectory_logprobs.py +15 -23
  10. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/rollout/agentic.py +1 -1
  11. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/rollout/optimized.py +78 -34
  12. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/train/grpo.py +95 -29
  13. inductive_mlxrl-0.2.1/mlxrl/train/reference.py +25 -0
  14. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/train/trajectory.py +41 -25
  15. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/pyproject.toml +1 -1
  16. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_agentic_rollout.py +77 -0
  17. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_grpo.py +36 -2
  18. inductive_mlxrl-0.2.1/tests/test_optimized_rollout.py +181 -0
  19. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_policy_logprobs.py +47 -0
  20. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_train_grpo.py +130 -1
  21. inductive_mlxrl-0.2.1/tests/test_train_trajectory.py +286 -0
  22. inductive_mlxrl-0.2.0/tests/test_optimized_rollout.py +0 -76
  23. inductive_mlxrl-0.2.0/tests/test_train_trajectory.py +0 -117
  24. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/.gitignore +0 -0
  25. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/CONTRIBUTING.md +0 -0
  26. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/DESIGN.md +0 -0
  27. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/LICENSE +0 -0
  28. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/README.md +0 -0
  29. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/THIRD_PARTY_LICENSES.md +0 -0
  30. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/__init__.py +0 -0
  31. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/audit_phase4_config.py +0 -0
  32. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/external_baseline_worker.py +0 -0
  33. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after.jsonl +0 -0
  34. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after.md +0 -0
  35. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after_final.jsonl +0 -0
  36. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after_final.md +0 -0
  37. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after_noprefixsync.jsonl +0 -0
  38. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_after_noprefixsync.md +0 -0
  39. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_before.jsonl +0 -0
  40. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate1_mlxrl_before.md +0 -0
  41. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate2_unified_smoke.jsonl +0 -0
  42. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate2_unified_smoke.md +0 -0
  43. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_mlx_tune_sync_256.jsonl +0 -0
  44. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_mlx_tune_sync_256.md +0 -0
  45. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_sync_blocked.jsonl +0 -0
  46. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_sync_blocked.md +0 -0
  47. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_sync_smoke.jsonl +0 -0
  48. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate3_sync_smoke.md +0 -0
  49. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate4_config_audit.json +0 -0
  50. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate4_config_audit.md +0 -0
  51. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate4_sampler_smoke.jsonl +0 -0
  52. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate4_sampler_smoke.md +0 -0
  53. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate5_full_reconciled.jsonl +0 -0
  54. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/gate5_full_reconciled.md +0 -0
  55. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_external_smoke.jsonl +0 -0
  56. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_external_smoke.md +0 -0
  57. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_full_all_targets.jsonl +0 -0
  58. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_full_all_targets.md +0 -0
  59. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.jsonl +0 -0
  60. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.md +0 -0
  61. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_mlx_lm_g4_smoke.jsonl +0 -0
  62. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/results/phase4_mlx_lm_g4_smoke.md +0 -0
  63. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/benchmarks/run_phase4.py +0 -0
  64. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/examples/qwen3_0_6b_gigpo_recurring.toml +0 -0
  65. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/algo/__init__.py +0 -0
  66. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/algo/gigpo.py +0 -0
  67. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/algorithm.py +0 -0
  68. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/cli.py +0 -0
  69. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/config.py +0 -0
  70. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/data/__init__.py +0 -0
  71. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/data/gsm8k.py +0 -0
  72. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/data/rewards.py +0 -0
  73. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/env/__init__.py +0 -0
  74. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/env/base.py +0 -0
  75. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/env/reference.py +0 -0
  76. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/policy/__init__.py +0 -0
  77. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/py.typed +0 -0
  78. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/rollout/__init__.py +0 -0
  79. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/rollout/naive.py +0 -0
  80. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/train/__init__.py +0 -0
  81. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/mlxrl/trajectory.py +0 -0
  82. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_agentic_env.py +0 -0
  83. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_benchmark_metrics.py +0 -0
  84. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_cli.py +0 -0
  85. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_config.py +0 -0
  86. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_gigpo.py +0 -0
  87. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_import_direction.py +0 -0
  88. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_policy_model.py +0 -0
  89. {inductive_mlxrl-0.2.0 → inductive_mlxrl-0.2.1}/tests/test_rewards.py +0 -0
  90. {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.0
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 = "/tmp/mlxrl_qwen35_9b_g2_checkpoint.npz"
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 = "/tmp/mlxrl_config_smoke.npz"
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
-
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.2.1"
@@ -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
- return type(batch)(
498
- prompt_token_ids=tuple(batch.prompt_token_ids[index] for index in keep_indices),
499
- completion_token_ids=tuple(
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=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
- )
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
- token_kl = approximate_kl(policy_logprobs, reference_logprobs)
520
- loss = masked_mean(token_policy_gradient + beta * token_kl, mask)
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 = masked_mean(clip_indicator(ratio, clipped_ratio), mask)
542
+ clip_fraction = (
543
+ mx.sum(clip_indicator(ratio, clipped_ratio) * mask) / denominator
544
+ )
525
545
  return AlgorithmLossMetrics(
526
- loss=loss,
527
- policy_gradient_loss=masked_mean(token_policy_gradient, mask),
528
- kl=masked_mean(token_kl, mask),
529
- mean_ratio=masked_mean(ratio, mask),
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
- completion_starts: list[int] = []
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
- logits = _completion_forward(model, input_ids, use_checkpoint=use_checkpoint)
113
- all_logprobs = logits - mx.logsumexp(logits, axis=-1, keepdims=True)
114
- target_logprobs = mx.take_along_axis(all_logprobs, target_ids[..., None], axis=-1)
115
- target_logprobs = mx.squeeze(target_logprobs, axis=-1)
116
-
117
- rows: list[mx.array] = []
118
- for row_index, (start, length) in enumerate(
119
- zip(completion_starts, completion_lengths, strict=True)
120
- ):
121
- row = target_logprobs[row_index, start : start + length]
122
- if length < max_completion_len:
123
- row = mx.concatenate(
124
- [
125
- row,
126
- mx.zeros((max_completion_len - length,), dtype=row.dtype),
127
- ],
128
- axis=0,
129
- )
130
- rows.append(row)
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=mx.stack(rows, axis=0),
134
- mask=mx.array(completion_mask_rows, dtype=mx.float32),
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
- logprobs = logits - mx.logsumexp(logits, axis=-1, keepdims=True)
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
- for index in row_indices
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
- policy = completion_logprobs(
223
- model,
224
- prompt_token_ids,
225
- completion_token_ids,
226
- pad_token_id,
227
- use_checkpoint=use_checkpoint,
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
- action_target_indices: list[list[int]] = []
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
- action_target_indices.append(indices)
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
- all_logprobs = logits - mx.logsumexp(logits, axis=-1, keepdims=True)
69
- target_logprobs = mx.squeeze(
70
- mx.take_along_axis(all_logprobs, target_ids[..., None], axis=-1),
71
- axis=-1,
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=mx.stack(rows, axis=0),
90
- mask=mx.array(mask_rows, dtype=mx.float32),
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),