inductive-mlxrl 0.2.1__tar.gz → 0.2.2__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.1 → inductive_mlxrl-0.2.2}/CHANGELOG.md +11 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/PKG-INFO +22 -1
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/README.md +21 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/__init__.py +1 -1
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/algorithm.py +3 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/cli.py +34 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/config.py +16 -1
- inductive_mlxrl-0.2.2/mlxrl/echo.py +43 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/policy/__init__.py +8 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/policy/trajectory_logprobs.py +117 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/train/grpo.py +4 -0
- inductive_mlxrl-0.2.2/mlxrl/train/trajectory.py +557 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/trajectory.py +23 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/pyproject.toml +1 -1
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_config.py +12 -0
- inductive_mlxrl-0.2.2/tests/test_echo.py +329 -0
- inductive_mlxrl-0.2.1/mlxrl/train/trajectory.py +0 -292
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/.gitignore +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/CONTRIBUTING.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/DESIGN.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/LICENSE +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/THIRD_PARTY_LICENSES.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/__init__.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/audit_phase4_config.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/external_baseline_worker.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after_final.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after_final.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after_noprefixsync.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_after_noprefixsync.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_before.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate1_mlxrl_before.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate2_unified_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate2_unified_smoke.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_mlx_tune_sync_256.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_mlx_tune_sync_256.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_sync_blocked.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_sync_blocked.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_sync_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate3_sync_smoke.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate4_config_audit.json +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate4_config_audit.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate4_sampler_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate4_sampler_smoke.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate5_full_reconciled.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/gate5_full_reconciled.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_external_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_external_smoke.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_full_all_targets.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_full_all_targets.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_local_mlxrl_vs_mlx_lm.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_mlx_lm_g4_smoke.jsonl +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/results/phase4_mlx_lm_g4_smoke.md +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/benchmarks/run_phase4.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/examples/qwen35_9b_g2_checkpoint.toml +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/examples/qwen3_0_6b_gigpo_recurring.toml +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/examples/qwen3_0_6b_grpo.toml +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/algo/__init__.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/algo/gigpo.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/algo/grpo.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/data/__init__.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/data/gsm8k.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/data/rewards.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/env/__init__.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/env/base.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/env/reference.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/policy/logprobs.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/policy/model.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/py.typed +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/rollout/__init__.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/rollout/agentic.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/rollout/naive.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/rollout/optimized.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/train/__init__.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/mlxrl/train/reference.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_agentic_env.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_agentic_rollout.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_benchmark_metrics.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_cli.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_gigpo.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_grpo.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_import_direction.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_optimized_rollout.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_policy_logprobs.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_policy_model.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_rewards.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_train_grpo.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_train_trajectory.py +0 -0
- {inductive_mlxrl-0.2.1 → inductive_mlxrl-0.2.2}/tests/test_trajectory_logprobs.py +0 -0
|
@@ -5,6 +5,17 @@ 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.2] - 2026-06-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- ECHO world-modeling support for tagged multi-turn trajectories, composing an
|
|
13
|
+
independently normalized ECHO SFT loss with the existing GiGPO action loss.
|
|
14
|
+
- `echo_alpha` scheduling with constant and linear-taper-to-zero modes, plus
|
|
15
|
+
separate `loss_echo` and `echo_accuracy` diagnostics.
|
|
16
|
+
- Real-Qwen smoke coverage for tagged ECHO batches on Qwen3-0.6B and
|
|
17
|
+
Qwen3.5-9B MLX 4-bit models.
|
|
18
|
+
|
|
8
19
|
## [0.2.1] - 2026-06-25
|
|
9
20
|
|
|
10
21
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: inductive-mlxrl
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -300,6 +300,27 @@ currently exact for token-mean policy losses: base GRPO, DAPO, GSPO token mode,
|
|
|
300
300
|
RLOO, and Dr. GRPO with `loss_reduction="token_mean"`. Sequence-reduced losses
|
|
301
301
|
should keep `micro_batch_size=0`.
|
|
302
302
|
|
|
303
|
+
## ECHO World Modeling
|
|
304
|
+
|
|
305
|
+
ECHO is an additive GiGPO trainer feature for tagged multi-turn trajectories.
|
|
306
|
+
Producer code may attach `token_roles` to each `Trajectory`, aligned to
|
|
307
|
+
`full_token_ids`: `0 = MASKED`, `1 = ACTION`, `2 = ECHO`. ACTION tokens keep
|
|
308
|
+
the normal GiGPO policy-gradient loss. ECHO tokens add an independently
|
|
309
|
+
normalized SFT term, `alpha * NLL`, in the same forward/backward pass.
|
|
310
|
+
|
|
311
|
+
`echo_alpha` defaults to `0.0`, which leaves current GiGPO behavior untouched.
|
|
312
|
+
For early world-modeling, start small, roughly `0.005` to `0.05`, and consider
|
|
313
|
+
`echo_schedule = "linear_taper_to_zero"` with `echo_taper_steps = N` so training
|
|
314
|
+
returns to pure RL. `alpha` is model-size-sensitive; too-high values can
|
|
315
|
+
overfit tool responses or collapse RL.
|
|
316
|
+
|
|
317
|
+
ECHO is content-agnostic by design. `mlxrl` never inspects token text to decide
|
|
318
|
+
what is an environment or tool response. The rollout producer owns the tags:
|
|
319
|
+
MASKED tokens have zero gradient, ACTION tokens receive the RL advantage, and
|
|
320
|
+
ECHO tokens receive only the SFT loss with no KL penalty or importance ratio.
|
|
321
|
+
The trainer logs `loss_echo` and token-level `echo_acc` separately from the
|
|
322
|
+
ACTION loss.
|
|
323
|
+
|
|
303
324
|
## Policy Semantics
|
|
304
325
|
|
|
305
326
|
- The base model is frozen before LoRA injection.
|
|
@@ -275,6 +275,27 @@ currently exact for token-mean policy losses: base GRPO, DAPO, GSPO token mode,
|
|
|
275
275
|
RLOO, and Dr. GRPO with `loss_reduction="token_mean"`. Sequence-reduced losses
|
|
276
276
|
should keep `micro_batch_size=0`.
|
|
277
277
|
|
|
278
|
+
## ECHO World Modeling
|
|
279
|
+
|
|
280
|
+
ECHO is an additive GiGPO trainer feature for tagged multi-turn trajectories.
|
|
281
|
+
Producer code may attach `token_roles` to each `Trajectory`, aligned to
|
|
282
|
+
`full_token_ids`: `0 = MASKED`, `1 = ACTION`, `2 = ECHO`. ACTION tokens keep
|
|
283
|
+
the normal GiGPO policy-gradient loss. ECHO tokens add an independently
|
|
284
|
+
normalized SFT term, `alpha * NLL`, in the same forward/backward pass.
|
|
285
|
+
|
|
286
|
+
`echo_alpha` defaults to `0.0`, which leaves current GiGPO behavior untouched.
|
|
287
|
+
For early world-modeling, start small, roughly `0.005` to `0.05`, and consider
|
|
288
|
+
`echo_schedule = "linear_taper_to_zero"` with `echo_taper_steps = N` so training
|
|
289
|
+
returns to pure RL. `alpha` is model-size-sensitive; too-high values can
|
|
290
|
+
overfit tool responses or collapse RL.
|
|
291
|
+
|
|
292
|
+
ECHO is content-agnostic by design. `mlxrl` never inspects token text to decide
|
|
293
|
+
what is an environment or tool response. The rollout producer owns the tags:
|
|
294
|
+
MASKED tokens have zero gradient, ACTION tokens receive the RL advantage, and
|
|
295
|
+
ECHO tokens receive only the SFT loss with no KL penalty or importance ratio.
|
|
296
|
+
The trainer logs `loss_echo` and token-level `echo_acc` separately from the
|
|
297
|
+
ACTION loss.
|
|
298
|
+
|
|
278
299
|
## Policy Semantics
|
|
279
300
|
|
|
280
301
|
- The base model is frozen before LoRA injection.
|
|
@@ -38,6 +38,7 @@ from mlxrl.data.gsm8k import (
|
|
|
38
38
|
format_gsm8k_prompt,
|
|
39
39
|
)
|
|
40
40
|
from mlxrl.data.rewards import accuracy_reward, format_reward
|
|
41
|
+
from mlxrl.echo import EchoSchedule, EchoScheduleName
|
|
41
42
|
from mlxrl.env import EnvFactory, RecurringStateTextEnv, SingleTurnRewardEnv
|
|
42
43
|
from mlxrl.policy.logprobs import pad_token_id_from_tokenizer
|
|
43
44
|
from mlxrl.policy.model import (
|
|
@@ -481,6 +482,9 @@ def _namespace_from_train_config(config: TrainConfig) -> argparse.Namespace:
|
|
|
481
482
|
max_turns=config.max_turns,
|
|
482
483
|
rollout_mode=config.rollout_mode,
|
|
483
484
|
env_name=config.env_name,
|
|
485
|
+
echo_alpha=config.echo_alpha,
|
|
486
|
+
echo_schedule=config.echo_schedule,
|
|
487
|
+
echo_taper_steps=config.echo_taper_steps,
|
|
484
488
|
)
|
|
485
489
|
|
|
486
490
|
|
|
@@ -500,6 +504,12 @@ def _apply_train_overrides(config: TrainConfig, args: argparse.Namespace) -> Tra
|
|
|
500
504
|
replacements["seed"] = args.seed
|
|
501
505
|
if args.output is not None:
|
|
502
506
|
replacements["output"] = args.output
|
|
507
|
+
if args.echo_alpha is not None:
|
|
508
|
+
replacements["echo_alpha"] = args.echo_alpha
|
|
509
|
+
if args.echo_schedule is not None:
|
|
510
|
+
replacements["echo_schedule"] = args.echo_schedule
|
|
511
|
+
if args.echo_taper_steps is not None:
|
|
512
|
+
replacements["echo_taper_steps"] = args.echo_taper_steps
|
|
503
513
|
if args.algorithm is not None:
|
|
504
514
|
replacements["algorithm"] = replace(
|
|
505
515
|
config.algorithm,
|
|
@@ -575,6 +585,11 @@ def _phase_agentic_train(config: TrainConfig) -> int:
|
|
|
575
585
|
)
|
|
576
586
|
algorithm = _trajectory_algorithm_from_config(config)
|
|
577
587
|
env_factory = _env_factory_from_config(config)
|
|
588
|
+
echo_schedule = EchoSchedule(
|
|
589
|
+
alpha=config.echo_alpha,
|
|
590
|
+
schedule=cast(EchoScheduleName, config.echo_schedule),
|
|
591
|
+
taper_steps=config.echo_taper_steps,
|
|
592
|
+
)
|
|
578
593
|
reward_history: list[float] = []
|
|
579
594
|
kl_history: list[float] = []
|
|
580
595
|
final_batch = None
|
|
@@ -604,6 +619,7 @@ def _phase_agentic_train(config: TrainConfig) -> int:
|
|
|
604
619
|
use_checkpoint=config.gradient_checkpointing,
|
|
605
620
|
compute_reference=config.kl_beta != 0.0,
|
|
606
621
|
)
|
|
622
|
+
echo_alpha = echo_schedule.value(step)
|
|
607
623
|
metrics = optimizer_step_trajectory(
|
|
608
624
|
model=model,
|
|
609
625
|
optimizer=optimizer,
|
|
@@ -613,6 +629,7 @@ def _phase_agentic_train(config: TrainConfig) -> int:
|
|
|
613
629
|
algorithm=algorithm,
|
|
614
630
|
use_checkpoint=config.gradient_checkpointing,
|
|
615
631
|
micro_batch_size=config.micro_batch_size,
|
|
632
|
+
echo_alpha=echo_alpha,
|
|
616
633
|
)
|
|
617
634
|
reward_history.append(metrics.mean_reward)
|
|
618
635
|
kl_history.append(metrics.kl)
|
|
@@ -624,6 +641,9 @@ def _phase_agentic_train(config: TrainConfig) -> int:
|
|
|
624
641
|
f"kl={metrics.kl:.6f} "
|
|
625
642
|
f"ratio={metrics.mean_ratio:.6f} "
|
|
626
643
|
f"clip={metrics.clip_fraction:.6f} "
|
|
644
|
+
f"echo_alpha={metrics.echo_alpha:.6f} "
|
|
645
|
+
f"loss_echo={metrics.loss_echo:.6f} "
|
|
646
|
+
f"echo_acc={metrics.echo_accuracy:.6f} "
|
|
627
647
|
f"loss={metrics.loss:.6f}",
|
|
628
648
|
flush=True,
|
|
629
649
|
)
|
|
@@ -645,7 +665,14 @@ def _phase_agentic_train(config: TrainConfig) -> int:
|
|
|
645
665
|
old_policy_logprobs=final_batch.old_policy_logprobs,
|
|
646
666
|
reference_logprobs=final_batch.reference_logprobs,
|
|
647
667
|
action_mask=final_batch.action_mask,
|
|
668
|
+
echo_mask=(
|
|
669
|
+
final_batch.echo_mask
|
|
670
|
+
if final_batch.echo_mask is not None
|
|
671
|
+
else mx.zeros_like(final_batch.action_mask)
|
|
672
|
+
),
|
|
648
673
|
loss=mx.array(final_metrics.loss, dtype=mx.float32),
|
|
674
|
+
loss_echo=mx.array(final_metrics.loss_echo, dtype=mx.float32),
|
|
675
|
+
echo_accuracy=mx.array(final_metrics.echo_accuracy, dtype=mx.float32),
|
|
649
676
|
kl=mx.array(final_metrics.kl, dtype=mx.float32),
|
|
650
677
|
)
|
|
651
678
|
print(f"reference_output: {output}")
|
|
@@ -881,6 +908,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
881
908
|
train.add_argument("--available-memory-gb", type=float, default=None)
|
|
882
909
|
train.add_argument("--auto-fit", action="store_true")
|
|
883
910
|
train.add_argument("--dry-run", action="store_true")
|
|
911
|
+
train.add_argument("--echo-alpha", type=float, default=None)
|
|
912
|
+
train.add_argument(
|
|
913
|
+
"--echo-schedule",
|
|
914
|
+
choices=["constant", "linear_taper_to_zero"],
|
|
915
|
+
default=None,
|
|
916
|
+
)
|
|
917
|
+
train.add_argument("--echo-taper-steps", type=int, default=None)
|
|
884
918
|
train.set_defaults(func=_phase_train)
|
|
885
919
|
|
|
886
920
|
phase2 = subparsers.add_parser(
|
|
@@ -7,7 +7,9 @@ import re
|
|
|
7
7
|
import tomllib
|
|
8
8
|
from dataclasses import dataclass, field, replace
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
from mlxrl.echo import EchoSchedule, EchoScheduleName
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class ConfigError(ValueError):
|
|
@@ -107,6 +109,9 @@ class TrainConfig:
|
|
|
107
109
|
iogpu_wired_limit_mb: int | None = None
|
|
108
110
|
micro_batch_size: int = 0
|
|
109
111
|
use_chat_template: bool = False
|
|
112
|
+
echo_alpha: float = 0.0
|
|
113
|
+
echo_schedule: str = "constant"
|
|
114
|
+
echo_taper_steps: int | None = None
|
|
110
115
|
output: str = "reference_outputs/phase1_reference.npz"
|
|
111
116
|
optimizer: OptimizerConfig = field(default_factory=OptimizerConfig)
|
|
112
117
|
sampling: SamplingConfigData = field(default_factory=SamplingConfigData)
|
|
@@ -147,6 +152,16 @@ class TrainConfig:
|
|
|
147
152
|
raise ConfigError("iogpu_wired_limit_mb must be positive or null.")
|
|
148
153
|
if self.micro_batch_size < 0:
|
|
149
154
|
raise ConfigError("micro_batch_size must be non-negative.")
|
|
155
|
+
if self.echo_alpha < 0:
|
|
156
|
+
raise ConfigError("echo_alpha must be non-negative.")
|
|
157
|
+
try:
|
|
158
|
+
EchoSchedule(
|
|
159
|
+
alpha=self.echo_alpha,
|
|
160
|
+
schedule=cast(EchoScheduleName, self.echo_schedule),
|
|
161
|
+
taper_steps=self.echo_taper_steps,
|
|
162
|
+
)
|
|
163
|
+
except ValueError as error:
|
|
164
|
+
raise ConfigError(str(error)) from error
|
|
150
165
|
validate_output_path(self.output)
|
|
151
166
|
self.optimizer.validate()
|
|
152
167
|
self.sampling.validate()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""ECHO token-role helpers and alpha scheduling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
MASKED = 0
|
|
9
|
+
ACTION = 1
|
|
10
|
+
ECHO = 2
|
|
11
|
+
VALID_TOKEN_ROLES = frozenset({MASKED, ACTION, ECHO})
|
|
12
|
+
EchoScheduleName = Literal["constant", "linear_taper_to_zero"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class EchoSchedule:
|
|
17
|
+
"""Schedule ECHO's SFT strength without changing the RL objective."""
|
|
18
|
+
|
|
19
|
+
alpha: float = 0.0
|
|
20
|
+
schedule: EchoScheduleName = "constant"
|
|
21
|
+
taper_steps: int | None = None
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
if self.alpha < 0:
|
|
25
|
+
raise ValueError("ECHO alpha must be non-negative.")
|
|
26
|
+
if self.schedule not in {"constant", "linear_taper_to_zero"}:
|
|
27
|
+
raise ValueError("ECHO schedule must be 'constant' or 'linear_taper_to_zero'.")
|
|
28
|
+
if (
|
|
29
|
+
self.schedule == "linear_taper_to_zero"
|
|
30
|
+
and (self.taper_steps is None or self.taper_steps < 1)
|
|
31
|
+
):
|
|
32
|
+
raise ValueError("linear_taper_to_zero requires taper_steps >= 1.")
|
|
33
|
+
|
|
34
|
+
def value(self, step: int) -> float:
|
|
35
|
+
"""Return alpha for a zero-based optimizer step."""
|
|
36
|
+
|
|
37
|
+
if step < 0:
|
|
38
|
+
raise ValueError("step must be non-negative.")
|
|
39
|
+
if self.schedule == "constant" or self.alpha == 0.0:
|
|
40
|
+
return self.alpha
|
|
41
|
+
assert self.taper_steps is not None
|
|
42
|
+
fraction = max(0.0, 1.0 - (float(step) / float(self.taper_steps)))
|
|
43
|
+
return self.alpha * fraction
|
|
@@ -21,10 +21,14 @@ from mlxrl.policy.model import (
|
|
|
21
21
|
strict_lora_config,
|
|
22
22
|
)
|
|
23
23
|
from mlxrl.policy.trajectory_logprobs import (
|
|
24
|
+
TaggedTrajectoryDualLogprobs,
|
|
25
|
+
TaggedTrajectoryLogprobs,
|
|
24
26
|
TrajectoryDualLogprobs,
|
|
25
27
|
TrajectoryLogprobs,
|
|
26
28
|
trajectory_action_logprobs,
|
|
27
29
|
trajectory_dual_logprobs,
|
|
30
|
+
trajectory_tagged_dual_logprobs,
|
|
31
|
+
trajectory_tagged_logprobs,
|
|
28
32
|
)
|
|
29
33
|
|
|
30
34
|
__all__ = [
|
|
@@ -34,6 +38,8 @@ __all__ = [
|
|
|
34
38
|
"DualLogprobs",
|
|
35
39
|
"LoRAConfig",
|
|
36
40
|
"Phase0Report",
|
|
41
|
+
"TaggedTrajectoryDualLogprobs",
|
|
42
|
+
"TaggedTrajectoryLogprobs",
|
|
37
43
|
"TrajectoryDualLogprobs",
|
|
38
44
|
"TrajectoryLogprobs",
|
|
39
45
|
"adapters_disabled",
|
|
@@ -48,4 +54,6 @@ __all__ = [
|
|
|
48
54
|
"strict_lora_config",
|
|
49
55
|
"trajectory_action_logprobs",
|
|
50
56
|
"trajectory_dual_logprobs",
|
|
57
|
+
"trajectory_tagged_dual_logprobs",
|
|
58
|
+
"trajectory_tagged_logprobs",
|
|
51
59
|
]
|
|
@@ -8,6 +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.echo import ACTION, ECHO
|
|
11
12
|
from mlxrl.policy.logprobs import adapters_disabled, target_logprobs_from_logits
|
|
12
13
|
from mlxrl.trajectory import Trajectory
|
|
13
14
|
|
|
@@ -29,6 +30,28 @@ class TrajectoryDualLogprobs:
|
|
|
29
30
|
mask: mx.array
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class TaggedTrajectoryLogprobs:
|
|
35
|
+
"""Full-forward logprobs and masks for tagged trajectory target tokens."""
|
|
36
|
+
|
|
37
|
+
logprobs: mx.array
|
|
38
|
+
target_ids: mx.array
|
|
39
|
+
predictions: mx.array
|
|
40
|
+
action_mask: mx.array
|
|
41
|
+
echo_mask: mx.array
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class TaggedTrajectoryDualLogprobs:
|
|
46
|
+
"""Policy/reference logprobs for tagged trajectory target tokens."""
|
|
47
|
+
|
|
48
|
+
policy: mx.array
|
|
49
|
+
reference: mx.array
|
|
50
|
+
target_ids: mx.array
|
|
51
|
+
action_mask: mx.array
|
|
52
|
+
echo_mask: mx.array
|
|
53
|
+
|
|
54
|
+
|
|
32
55
|
def trajectory_action_logprobs(
|
|
33
56
|
model: nn.Module,
|
|
34
57
|
trajectories: Sequence[Trajectory],
|
|
@@ -83,6 +106,100 @@ def trajectory_action_logprobs(
|
|
|
83
106
|
)
|
|
84
107
|
|
|
85
108
|
|
|
109
|
+
def trajectory_tagged_logprobs(
|
|
110
|
+
model: nn.Module,
|
|
111
|
+
trajectories: Sequence[Trajectory],
|
|
112
|
+
pad_token_id: int = 0,
|
|
113
|
+
use_checkpoint: bool = False,
|
|
114
|
+
) -> TaggedTrajectoryLogprobs:
|
|
115
|
+
"""Gather logprobs for ACTION and ECHO tags from one full forward."""
|
|
116
|
+
|
|
117
|
+
del use_checkpoint
|
|
118
|
+
if not trajectories:
|
|
119
|
+
raise ValueError("At least one trajectory is required.")
|
|
120
|
+
max_sequence_len = max(len(trajectory.full_token_ids) for trajectory in trajectories)
|
|
121
|
+
if max_sequence_len < 2:
|
|
122
|
+
raise ValueError("A trajectory needs at least two tokens for logprobs.")
|
|
123
|
+
|
|
124
|
+
target_width = max_sequence_len - 1
|
|
125
|
+
input_rows: list[list[int]] = []
|
|
126
|
+
target_rows: list[list[int]] = []
|
|
127
|
+
action_mask_rows: list[list[float]] = []
|
|
128
|
+
echo_mask_rows: list[list[float]] = []
|
|
129
|
+
for trajectory in trajectories:
|
|
130
|
+
sequence = list(trajectory.full_token_ids)
|
|
131
|
+
roles = trajectory.token_roles_or_default()
|
|
132
|
+
target_roles = list(roles[1:])
|
|
133
|
+
pad_count = target_width - (len(sequence) - 1)
|
|
134
|
+
input_rows.append(sequence[:-1] + [pad_token_id] * pad_count)
|
|
135
|
+
target_rows.append(sequence[1:] + [pad_token_id] * pad_count)
|
|
136
|
+
action_mask_rows.append(
|
|
137
|
+
[1.0 if role == ACTION else 0.0 for role in target_roles]
|
|
138
|
+
+ [0.0] * pad_count
|
|
139
|
+
)
|
|
140
|
+
echo_mask_rows.append(
|
|
141
|
+
[1.0 if role == ECHO else 0.0 for role in target_roles]
|
|
142
|
+
+ [0.0] * pad_count
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
input_ids = mx.array(input_rows, dtype=mx.int32)
|
|
146
|
+
target_ids = mx.array(target_rows, dtype=mx.int32)
|
|
147
|
+
logits = model(input_ids)
|
|
148
|
+
logprobs = target_logprobs_from_logits(logits, target_ids)
|
|
149
|
+
predictions = mx.argmax(logits, axis=-1).astype(mx.int32)
|
|
150
|
+
return TaggedTrajectoryLogprobs(
|
|
151
|
+
logprobs=logprobs,
|
|
152
|
+
target_ids=target_ids,
|
|
153
|
+
predictions=predictions,
|
|
154
|
+
action_mask=mx.array(action_mask_rows, dtype=mx.float32),
|
|
155
|
+
echo_mask=mx.array(echo_mask_rows, dtype=mx.float32),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def trajectory_tagged_dual_logprobs(
|
|
160
|
+
model: nn.Module,
|
|
161
|
+
trajectories: Sequence[Trajectory],
|
|
162
|
+
pad_token_id: int = 0,
|
|
163
|
+
use_checkpoint: bool = False,
|
|
164
|
+
compute_reference: bool = True,
|
|
165
|
+
) -> TaggedTrajectoryDualLogprobs:
|
|
166
|
+
"""Compute tagged policy/reference logprobs using one model object."""
|
|
167
|
+
|
|
168
|
+
policy = trajectory_tagged_logprobs(
|
|
169
|
+
model,
|
|
170
|
+
trajectories,
|
|
171
|
+
pad_token_id=pad_token_id,
|
|
172
|
+
use_checkpoint=use_checkpoint,
|
|
173
|
+
)
|
|
174
|
+
if not compute_reference:
|
|
175
|
+
return TaggedTrajectoryDualLogprobs(
|
|
176
|
+
policy=policy.logprobs,
|
|
177
|
+
reference=mx.stop_gradient(policy.logprobs),
|
|
178
|
+
target_ids=policy.target_ids,
|
|
179
|
+
action_mask=policy.action_mask,
|
|
180
|
+
echo_mask=policy.echo_mask,
|
|
181
|
+
)
|
|
182
|
+
with adapters_disabled(model):
|
|
183
|
+
reference = trajectory_tagged_logprobs(
|
|
184
|
+
model,
|
|
185
|
+
trajectories,
|
|
186
|
+
pad_token_id=pad_token_id,
|
|
187
|
+
use_checkpoint=use_checkpoint,
|
|
188
|
+
)
|
|
189
|
+
mx.eval( # Reference sync: materialize tagged ref before restoring adapters.
|
|
190
|
+
reference.logprobs,
|
|
191
|
+
reference.action_mask,
|
|
192
|
+
reference.echo_mask,
|
|
193
|
+
)
|
|
194
|
+
return TaggedTrajectoryDualLogprobs(
|
|
195
|
+
policy=policy.logprobs,
|
|
196
|
+
reference=reference.logprobs,
|
|
197
|
+
target_ids=policy.target_ids,
|
|
198
|
+
action_mask=policy.action_mask,
|
|
199
|
+
echo_mask=policy.echo_mask,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
86
203
|
def trajectory_dual_logprobs(
|
|
87
204
|
model: nn.Module,
|
|
88
205
|
trajectories: Sequence[Trajectory],
|
|
@@ -50,6 +50,10 @@ class StepMetrics:
|
|
|
50
50
|
mean_ratio: float
|
|
51
51
|
clip_fraction: float
|
|
52
52
|
mean_reward: float
|
|
53
|
+
loss_action: float = 0.0
|
|
54
|
+
loss_echo: float = 0.0
|
|
55
|
+
echo_accuracy: float = 0.0
|
|
56
|
+
echo_alpha: float = 0.0
|
|
53
57
|
|
|
54
58
|
|
|
55
59
|
def _slice_batch(batch: GRPOBatch, start: int, end: int) -> GRPOBatch:
|