Project-Chronos 0.3.0__tar.gz → 0.3.1a0__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.
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/CHANGELOG.md +41 -0
- {project_chronos-0.3.0/Project_Chronos.egg-info → project_chronos-0.3.1a0}/PKG-INFO +1 -1
- {project_chronos-0.3.0 → project_chronos-0.3.1a0/Project_Chronos.egg-info}/PKG-INFO +1 -1
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/Project_Chronos.egg-info/SOURCES.txt +5 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/__init__.py +1 -1
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/cli.py +71 -8
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/docs.py +23 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/finetune.py +9 -3
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/recommend.py +91 -5
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/runtime.py +73 -5
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/config_schema.py +1 -0
- project_chronos-0.3.1a0/chronos/expert_clusters.py +121 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/expert_store.py +35 -3
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/storage.py +40 -8
- project_chronos-0.3.1a0/chronos/lazy_experts.py +387 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/expert_store.py +85 -28
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/inference.py +207 -18
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/moe.py +2 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/trainer.py +39 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/checkpoint.py +9 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/config.py +13 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/planner.py +14 -1
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/temporal_loss.py +76 -0
- project_chronos-0.3.1a0/chronos/residency_planner.py +154 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/runtime/inference_engine.py +6 -2
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/ollama_server.py +8 -2
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/runtime.py +26 -13
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/loss_mixin.py +15 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/stage_utils.py +6 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/pyproject.toml +1 -1
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_bundle.py +31 -0
- project_chronos-0.3.1a0/tests/test_lazy_experts.py +285 -0
- project_chronos-0.3.1a0/tests/test_residency_cluster.py +265 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_smoke.py +4 -1
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos.py +6 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos_distill.py +2 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos_dpo.py +2 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos_grpo.py +2 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos_orpo.py +2 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos_sft.py +3 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/i18n.py +32 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/presets.py +7 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/presets_v3.json +1 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/config_tab.py +13 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/inference_tab.py +108 -350
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/LICENSE +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/MANIFEST.in +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/Project_Chronos.egg-info/dependency_links.txt +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/Project_Chronos.egg-info/entry_points.txt +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/Project_Chronos.egg-info/requires.txt +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/Project_Chronos.egg-info/top_level.txt +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/README.md +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/README_zh.md +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/THIRD_PARTY_NOTICES.md +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/_backend_legacy.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/app.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/backend/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/backend/dispatcher.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/backend/ext/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/backend/ext/opencl.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/backend/mac_diagnostics.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/builder.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/cli.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/clean.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/download.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/evalsets.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/pipeline.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/presets.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/readers.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/shards.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/sources.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/flexible_dataset.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/deps.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/eval/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/eval/benchmark.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/eval/io_profiler.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/export.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/async_prefetcher.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/cluster_layout.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/io_simulator.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/migrate.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/attention.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/kv_quant.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/model.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/moe_kernels.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/recurrent.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/speculative.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/inspect_checkpoint.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/io.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/longrun.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/optim.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/telemetry.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/factory.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/hf_io.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/hybrid_attention.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/kv_quant.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/lookahead_router.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/model_chronos.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/moe_chronos.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/multimodal.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/outputs.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/precision_ops.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/recurrent.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/evals.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/jsonl.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/process.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/server.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/snapshot.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/static/app.css +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/static/app.js +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/static/index.html +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/precision.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/router/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/router/expert_predictor.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/router/intent_classifier.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/router/prefill_scheduler.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/runtime/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/runtime/cache_manager.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/runtime/metrics.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/runtime/speculative.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/adapter.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/api_compat.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/vllm_adapter.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/__main__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/evaluate.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/extract.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/model.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/normalize.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/trainer.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/chronos_trainer.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/device_utils.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/distill_trainer.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/dpo_trainer.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/grpo_trainer.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/optim_utils.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/orpo_trainer.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/paged_store.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/precision_training.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/reward.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/sft_trainer.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tuning/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tuning/_base_tuner.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tuning/chronos_auto_tuner.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/verify.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/requirements.txt +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/setup.cfg +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/setup.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_interface_regressions.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_longrun.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_mlx_speculative.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_moe_dispatch.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_monitor.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_planner_ui.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_precision_chain.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_smoke_cuda.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_speculative.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_tokenizer_builder.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_ui_event_browser.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_ui_event_contract.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_ui_generic.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_v2_architecture.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_v3_inference_options.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_v3_kernel.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/estimator.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/event_contract.js +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/event_contract.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/gradio_compat.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/planner_panel.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/__init__.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/autotune_tab.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/benchmark_tab.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/data_tab.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/export_tab.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/iomon_tab.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/monitor_tab.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/pipeline_tab.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/serve_tab.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/tokenizer_tab.py +0 -0
- {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/train_tab.py +0 -0
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.3.1a0] — 2026-11-07 (beta)
|
|
6
|
+
|
|
7
|
+
Core half of 0.3.1: cluster-locality training on by default, inference that
|
|
8
|
+
adapts its expert paging to how the checkpoint was trained, and lazy experts in
|
|
9
|
+
the bundle. The rewritten Web UI follows in 0.3.1a1.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Cluster locality is on by default for new training: `train_chronos*.py --lambda_cluster_locality` defaults to 0.3, the planner and presets set λ = 0.3 for MoE models with more than 2k experts, and the window follows the data (`recommended_window`: power of two nearest the median SFT record, clamped to [16, seq_len]; 32 without record lengths). The `ChronosConfig` class default stays 0.0, so a stored recipe without the field keeps its original objective on resume, and checkpoints written before the field existed load as λ = 0. The λ = 0.3 default comes from the synthetic sweep below (from-scratch training); its effect on real data and on fine-tuning has not been measured.
|
|
13
|
+
- Lazy-expert inference picks its page layout from the checkpoint (`paging="auto"`): weights trained with λ > 0 are paged one training cluster per SSD read, other weights one expert per read. `paging="cluster"|"expert"` forces a layout. The layout is part of the expert-cache directory name (`…_mlx_c2`, `…_c2`), so a cache written for one layout is never reused for the other. Per-expert cache names are unchanged.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `chronos/lazy_experts.py`: one lazy-expert implementation for the Web UI, bundle and server — `LazyOptions` (paging, residency, decay, decode/verify waits, RAM ratio; validated), `page_layout`, `checkpoint_cache_dir`, the expert budget, and `build_mlx_lazy_engine` (trunk only in memory, cache built or reused).
|
|
17
|
+
- Torch offload writes cluster layouts as `cluster_<c>.ctsr` files with a v2 manifest (`ClusterStorage.write_clusters_from_states`); `setup_from_state_dict/_reader(clusters=…)`.
|
|
18
|
+
- MLX `generate(decode_wait_s=0.012, verify_wait_s=0.02, repetition_penalty=1.0)`. The penalty uses the CTRL rule and is rejected together with `speculative=True`, which needs penalty 1.0 to stay exact.
|
|
19
|
+
- `chronos bundle translate|eval --lazy-experts` with `--paging`, `--residency`, `--decay`, `--ram-ratio`, `--decode-wait`, `--verify-wait`. The cache lives outside the bundle (`$CHRONOS_EXPERT_CACHE`, default `~/.cache/chronos/expert_cache`); `eval` also gains `--backend` and `--dtype`.
|
|
20
|
+
- `chronos bundle finetune --[no-]cluster-locality --lambda-cluster --cluster-window --clusters` (on by default; dense models and models with fewer than two clusters stay off). The spec records where each value came from (`extra.cluster_locality_source`); the window comes from the median SFT record length of `--data`.
|
|
21
|
+
- `chronos serve --paging auto|cluster|expert`; `/api/ps` metadata reports the resolved layout.
|
|
22
|
+
- Web UI Inference tab: paging, planner decay and the two prefetch waits, with four-language labels.
|
|
23
|
+
|
|
24
|
+
- Next-use residency planning for lazy MLX experts (`chronos/residency_planner.py`, `generate(residency="planned")`). The planner records the page-access trace of every forward, keyed by (layer, loop iteration). The previous forward of the same kind, and in speculative decoding the queue of draft traces since the last verify, then predicts when each page is needed next. The store evicts the page needed latest (`MLXExpertStore.set_eviction_ranker`). The running dispatch's pending experts are never evicted. Pages needed early are prefetched only into spare slots. Greedy output is unchanged. The Web UI Inference tab has a switch for it, on by default (MLX lazy only); the stats table shows the eviction policy.
|
|
25
|
+
- Cluster-locality training constraint `lambda_cluster_locality`, with `cluster_locality_clusters` (0 = ⌈E/2k⌉) and `cluster_locality_window`. The loss is 1 − I(window; cluster)/log C over every MoE dispatch, recurrent iterations included. It keeps a stream inside few expert clusters and spreads different streams over different clusters. The partition (`chronos/expert_clusters.py`) is the one lazy inference pages by. Torch and MLX trainers and all stage scripts take it.
|
|
26
|
+
- `tools/bench_residency.py`: LRU window vs planned residency, plain and speculative, on a real checkpoint.
|
|
27
|
+
- `tools/bench_cluster_locality.py`: a tiny-model training A/B of the constraint, reporting held-out CE, cluster entropies, topic purity and lazy-decode cluster reads.
|
|
28
|
+
|
|
29
|
+
### Measured (reports/residency_20261107/)
|
|
30
|
+
- 50M s13996 checkpoint, 16 FLORES+ dev prompts, greedy, CPU, 12 hot / 12 RAM pages.
|
|
31
|
+
- Plain decoding: planned residency needs 4438 synchronous SSD loads instead of 6876 (−35%). Throughput is 22.47 vs 23.17 tok/s.
|
|
32
|
+
- Speculative decoding: 6258 vs 9297 loads (−33%), and 18.96 vs 16.03 tok/s.
|
|
33
|
+
- All outputs are identical to plain greedy (16/16).
|
|
34
|
+
- The checkpoint was trained without the constraint and routes near-uniformly over all 36 experts within a stream.
|
|
35
|
+
- Tiny model on synthetic 4-topic streams, 300 steps, λ=0.3 vs 0.
|
|
36
|
+
- Window cluster entropy falls from 0.49 to 0.005; topic→cluster purity rises from 0.81 to 0.999.
|
|
37
|
+
- Marginal cluster entropy rises from 0.71 to 0.99, and expert-usage entropy is unchanged (0.87).
|
|
38
|
+
- Held-out CE is 1.712 vs 1.703.
|
|
39
|
+
- Cluster-paged lazy decoding needs 0.023 vs 1.48 cluster reads per token.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
- MLX lazy store: an expert evicted by the prefetch thread between an SSD read and promotion raised "could not be loaded on demand". Reads now return the page they loaded.
|
|
43
|
+
|
|
3
44
|
## [0.3.0] — 2026-11-06
|
|
4
45
|
|
|
5
46
|
v3 kernel: recurrent-depth MoE. Quality results for the 50M en→zh demo are not
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Project_Chronos
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1a0
|
|
4
4
|
Summary: Storage-aware recurrent-depth MoE: lookahead expert prefetch, SSD-lazy experts, hybrid MLA/SWA attention, self-speculative decoding
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/FonaTech/Project_Chronos
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Project_Chronos
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1a0
|
|
4
4
|
Summary: Storage-aware recurrent-depth MoE: lookahead expert prefetch, SSD-lazy experts, hybrid MLA/SWA attention, self-speculative decoding
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: Homepage, https://github.com/FonaTech/Project_Chronos
|
|
@@ -25,9 +25,12 @@ chronos/app.py
|
|
|
25
25
|
chronos/cli.py
|
|
26
26
|
chronos/config_schema.py
|
|
27
27
|
chronos/deps.py
|
|
28
|
+
chronos/expert_clusters.py
|
|
28
29
|
chronos/export.py
|
|
30
|
+
chronos/lazy_experts.py
|
|
29
31
|
chronos/migrate.py
|
|
30
32
|
chronos/precision.py
|
|
33
|
+
chronos/residency_planner.py
|
|
31
34
|
chronos/verify.py
|
|
32
35
|
chronos/backend/__init__.py
|
|
33
36
|
chronos/backend/dispatcher.py
|
|
@@ -144,12 +147,14 @@ chronos/tuning/_base_tuner.py
|
|
|
144
147
|
chronos/tuning/chronos_auto_tuner.py
|
|
145
148
|
tests/test_bundle.py
|
|
146
149
|
tests/test_interface_regressions.py
|
|
150
|
+
tests/test_lazy_experts.py
|
|
147
151
|
tests/test_longrun.py
|
|
148
152
|
tests/test_mlx_speculative.py
|
|
149
153
|
tests/test_moe_dispatch.py
|
|
150
154
|
tests/test_monitor.py
|
|
151
155
|
tests/test_planner_ui.py
|
|
152
156
|
tests/test_precision_chain.py
|
|
157
|
+
tests/test_residency_cluster.py
|
|
153
158
|
tests/test_smoke.py
|
|
154
159
|
tests/test_smoke_cuda.py
|
|
155
160
|
tests/test_speculative.py
|
|
@@ -12,6 +12,43 @@ def _bundle_dir(value: str | None) -> str:
|
|
|
12
12
|
return os.path.abspath(value or os.environ.get("CHRONOS_BUNDLE") or ".")
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def _add_lazy_args(parser) -> None:
|
|
16
|
+
"""SSD-lazy expert options (MLX backend); defaults are the engine defaults."""
|
|
17
|
+
from chronos.lazy_experts import (
|
|
18
|
+
DEFAULT_DECODE_WAIT_S, DEFAULT_RAM_RATIO, DEFAULT_RESIDENCY, DEFAULT_RESIDENCY_DECAY,
|
|
19
|
+
DEFAULT_VERIFY_WAIT_S, RESIDENCY_MODES,
|
|
20
|
+
)
|
|
21
|
+
g = parser.add_argument_group("lazy experts (MLX)")
|
|
22
|
+
g.add_argument("--lazy-experts", action="store_true",
|
|
23
|
+
help="keep only the trunk in memory and page routed experts from an SSD cache")
|
|
24
|
+
g.add_argument("--paging", default="auto", choices=["auto", "cluster", "expert"],
|
|
25
|
+
help="page layout; auto = by training cluster if the model was trained with cluster "
|
|
26
|
+
"locality, else per expert (default: auto)")
|
|
27
|
+
g.add_argument("--residency", default=DEFAULT_RESIDENCY, choices=list(RESIDENCY_MODES),
|
|
28
|
+
help=f"planned = next-use eviction, window = LRU + protected window (default: {DEFAULT_RESIDENCY})")
|
|
29
|
+
g.add_argument("--decay", type=float, default=DEFAULT_RESIDENCY_DECAY,
|
|
30
|
+
help=f"planned residency: weight of older forwards, [0, 1) (default: {DEFAULT_RESIDENCY_DECAY})")
|
|
31
|
+
g.add_argument("--ram-ratio", type=float, default=DEFAULT_RAM_RATIO,
|
|
32
|
+
help=f"expert pages kept loaded, × the routed working set (default: {DEFAULT_RAM_RATIO})")
|
|
33
|
+
g.add_argument("--decode-wait", type=float, default=DEFAULT_DECODE_WAIT_S,
|
|
34
|
+
help=f"seconds a decode step waits for prefetched pages (default: {DEFAULT_DECODE_WAIT_S})")
|
|
35
|
+
g.add_argument("--verify-wait", type=float, default=DEFAULT_VERIFY_WAIT_S,
|
|
36
|
+
help=f"seconds a speculative verify waits for prefetched pages (default: {DEFAULT_VERIFY_WAIT_S})")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _lazy_options(a):
|
|
40
|
+
from chronos.lazy_experts import LazyOptions
|
|
41
|
+
return LazyOptions(paging=a.paging, residency=a.residency, residency_decay=a.decay, ram_ratio=a.ram_ratio,
|
|
42
|
+
decode_wait_s=a.decode_wait, verify_wait_s=a.verify_wait).validated()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _bundle_model(bundle: str, a):
|
|
46
|
+
from .runtime import BundleModel
|
|
47
|
+
backend = "mlx" if a.lazy_experts else a.backend
|
|
48
|
+
return BundleModel(bundle, device=a.device, dtype=a.dtype, backend=backend, lazy_experts=a.lazy_experts,
|
|
49
|
+
lazy_options=_lazy_options(a) if a.lazy_experts else None)
|
|
50
|
+
|
|
51
|
+
|
|
15
52
|
def main(argv=None) -> int:
|
|
16
53
|
p = argparse.ArgumentParser(prog="chronos bundle", description=__doc__)
|
|
17
54
|
sub = p.add_subparsers(dest="cmd", required=True)
|
|
@@ -56,14 +93,18 @@ def main(argv=None) -> int:
|
|
|
56
93
|
help="recurrent-depth self-speculative decoding (same greedy output, fewer full-depth forwards)")
|
|
57
94
|
t.add_argument("--draft-depth", type=int, default=1)
|
|
58
95
|
t.add_argument("--gamma", type=int, default=4, help="initial draft length (adapts between 1 and 8)")
|
|
96
|
+
_add_lazy_args(t)
|
|
59
97
|
|
|
60
98
|
e = sub.add_parser("eval", help="BLEU/chrF on the bundled FLORES+ split")
|
|
61
99
|
e.add_argument("--bundle", default=None)
|
|
62
100
|
e.add_argument("--split", default="devtest", choices=["dev", "devtest"])
|
|
63
101
|
e.add_argument("-n", type=int, default=0, help="first n sentences (0 = all)")
|
|
64
102
|
e.add_argument("--repetition-penalty", type=float, default=1.2)
|
|
103
|
+
e.add_argument("--backend", default="torch", choices=["torch", "mlx"])
|
|
65
104
|
e.add_argument("--device", default="auto")
|
|
105
|
+
e.add_argument("--dtype", default="float32")
|
|
66
106
|
e.add_argument("--out", default=None, help="write the report JSON here")
|
|
107
|
+
_add_lazy_args(e)
|
|
67
108
|
|
|
68
109
|
d = sub.add_parser("prepare-data", help="your parallel data → shards tokenized with the bundle tokenizer")
|
|
69
110
|
d.add_argument("inputs", nargs="+", help=".jsonl/.csv/.tsv/.parquet files with zh and en columns")
|
|
@@ -84,6 +125,16 @@ def main(argv=None) -> int:
|
|
|
84
125
|
f.add_argument("--hours", type=float, default=None, help="override the tier's wall-clock budget")
|
|
85
126
|
f.add_argument("--seed", type=int, default=None)
|
|
86
127
|
f.add_argument("--dry-run", action="store_true", help="write the spec, do not start training")
|
|
128
|
+
from chronos.expert_clusters import DEFAULT_LAMBDA
|
|
129
|
+
f.add_argument("--cluster-locality", action=argparse.BooleanOptionalAction, default=True,
|
|
130
|
+
help="train with the expert-cluster locality constraint so lazy inference can page "
|
|
131
|
+
"by cluster (default: on)")
|
|
132
|
+
f.add_argument("--lambda-cluster", type=float, default=None,
|
|
133
|
+
help=f"constraint strength (default: {DEFAULT_LAMBDA}, from a measured sweep)")
|
|
134
|
+
f.add_argument("--cluster-window", type=int, default=None,
|
|
135
|
+
help="tokens per locality window (default: nearest power of two to the median SFT "
|
|
136
|
+
"record length of --data, clamped to [16, seq_len])")
|
|
137
|
+
f.add_argument("--clusters", type=int, default=None, help="number of clusters (default: ceil(E / 2k))")
|
|
87
138
|
|
|
88
139
|
s = sub.add_parser("serve", help="Ollama/OpenAI-protocol server over the bundle's model/ (loopback only)")
|
|
89
140
|
s.add_argument("--bundle", default=None)
|
|
@@ -120,7 +171,6 @@ def _run(a) -> int:
|
|
|
120
171
|
_print(builder.record_quality_probe(_bundle_dir(a.bundle), a.n, device=a.device))
|
|
121
172
|
return 0
|
|
122
173
|
if a.cmd == "translate":
|
|
123
|
-
from .runtime import BundleModel
|
|
124
174
|
lines = list(a.text) and [" ".join(a.text)]
|
|
125
175
|
if a.file:
|
|
126
176
|
with open(a.file, encoding="utf-8") as fh:
|
|
@@ -128,19 +178,30 @@ def _run(a) -> int:
|
|
|
128
178
|
elif not lines:
|
|
129
179
|
lines = [ln.strip() for ln in sys.stdin if ln.strip()]
|
|
130
180
|
rp = a.repetition_penalty if a.repetition_penalty is not None else (1.0 if a.speculative else 1.2)
|
|
131
|
-
bm =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
181
|
+
bm = _bundle_model(_bundle_dir(a.bundle), a)
|
|
182
|
+
try:
|
|
183
|
+
for hyp in bm.translate(lines, a.direction, max_new_tokens=a.max_new_tokens, repetition_penalty=rp,
|
|
184
|
+
speculative=a.speculative, draft_depth=a.draft_depth, gamma=a.gamma):
|
|
185
|
+
print(hyp, flush=True)
|
|
186
|
+
finally:
|
|
187
|
+
bm.close()
|
|
188
|
+
if bm.last_stats.get("lazy"):
|
|
189
|
+
print(json.dumps({"lazy": bm.last_stats["lazy"]}, ensure_ascii=False), file=sys.stderr)
|
|
135
190
|
if bm.last_stats.get("speculative"):
|
|
136
191
|
print(f"[speculative] {json.dumps(bm.last_stats['speculative'])}", file=sys.stderr)
|
|
137
192
|
return 0
|
|
138
193
|
if a.cmd == "eval":
|
|
139
|
-
from .runtime import
|
|
194
|
+
from .runtime import eval_pairs, score
|
|
140
195
|
bundle = _bundle_dir(a.bundle)
|
|
141
196
|
pairs = eval_pairs(bundle, a.split)
|
|
142
197
|
pairs = pairs[: a.n] if a.n else pairs
|
|
143
|
-
|
|
198
|
+
bm = _bundle_model(bundle, a)
|
|
199
|
+
try:
|
|
200
|
+
report = score(bm, pairs, repetition_penalty=a.repetition_penalty)
|
|
201
|
+
finally:
|
|
202
|
+
bm.close()
|
|
203
|
+
if bm.last_stats.get("lazy"):
|
|
204
|
+
report["lazy"] = bm.last_stats["lazy"]
|
|
144
205
|
report["set"] = f"flores_plus_{a.split}"
|
|
145
206
|
_print(report)
|
|
146
207
|
if a.out:
|
|
@@ -157,7 +218,9 @@ def _run(a) -> int:
|
|
|
157
218
|
data = os.path.abspath(a.data)
|
|
158
219
|
shards = os.path.join(data, "shards") if os.path.isdir(os.path.join(data, "shards")) else data
|
|
159
220
|
spec = write_finetune_spec(_bundle_dir(a.bundle), shards, a.run_dir, tier=a.tier, mode=a.mode,
|
|
160
|
-
budget_hours=a.hours, seed=a.seed
|
|
221
|
+
budget_hours=a.hours, seed=a.seed, cluster_locality=a.cluster_locality,
|
|
222
|
+
lambda_cluster=a.lambda_cluster, cluster_window=a.cluster_window,
|
|
223
|
+
clusters=a.clusters)
|
|
161
224
|
print(f"[finetune] spec: {spec}")
|
|
162
225
|
if a.dry_run:
|
|
163
226
|
print(f"[finetune] start it with: chronos longrun --spec {spec}")
|
|
@@ -157,6 +157,13 @@ def _readme_en(m: dict, recs: dict) -> str:
|
|
|
157
157
|
"`--speculative` (recurrent-depth self-speculative decoding) gives the same greedy output as plain "
|
|
158
158
|
"decoding and reports its acceptance rate on stderr. It needs `--repetition-penalty 1.0`, its default. "
|
|
159
159
|
"On CPU at this model size it was measured slower than plain decoding.", "",
|
|
160
|
+
"`--lazy-experts` (MLX) keeps only the trunk in memory and pages the routed experts from an SSD cache "
|
|
161
|
+
"(`~/.cache/chronos/expert_cache`, or `$CHRONOS_EXPERT_CACHE`). The greedy output is the same. The page "
|
|
162
|
+
"layout adapts to the weights: one file per expert cluster when the model was trained with the "
|
|
163
|
+
"cluster-locality constraint, one per expert otherwise (`--paging auto|cluster|expert`). "
|
|
164
|
+
"`--residency planned` (default) evicts the page needed latest; `window` keeps LRU order. "
|
|
165
|
+
"`--decay`, `--ram-ratio`, `--decode-wait` and `--verify-wait` tune it; `--help` lists the defaults. "
|
|
166
|
+
"The page counts are printed to stderr.", "",
|
|
160
167
|
"`bin/chronos` sets `CHRONOS_BUNDLE` and `CHRONOS_MINIMIND_DIR` (the vendored minimind in "
|
|
161
168
|
"`third_party/`), so nothing is fetched from the network. Without `bin/`, export both variables and call "
|
|
162
169
|
"`chronos bundle …` directly.", "",
|
|
@@ -176,6 +183,12 @@ def _readme_en(m: dict, recs: dict) -> str:
|
|
|
176
183
|
"Monitor a run with `bin/chronos monitor --runs-dir work`. The fine-tuned checkpoint lands in "
|
|
177
184
|
"`work/run/*_ft_sft.best.pth`; rebuild a bundle from it with `chronos bundle build --run-dir work/run "
|
|
178
185
|
"--phase sft`.", "",
|
|
186
|
+
"Fine-tuning trains with the expert-cluster locality constraint by default, so the result can be paged "
|
|
187
|
+
"by cluster at inference (`--no-cluster-locality` turns it off). Defaults: `--lambda-cluster 0.3` (from "
|
|
188
|
+
"a measured sweep on a synthetic task with from-scratch training; its effect on a fine-tune has not "
|
|
189
|
+
"been measured) and `--cluster-window` = the power of two nearest the median SFT record length of "
|
|
190
|
+
"your data. The spec records where each value came from (`extra.cluster_locality_source`). Models "
|
|
191
|
+
"with fewer than two clusters (E ≤ 2k) train without it.", "",
|
|
179
192
|
"Tiers (from `recommended.json`; every value is labelled with its source):", "",
|
|
180
193
|
"| Tier | Hours | SFT peak LR | Tokens / step | Warmup | Use |", "|---|---|---|---|---|---|",
|
|
181
194
|
*_tier_rows(recs), "",
|
|
@@ -238,6 +251,12 @@ def _readme_zh(m: dict, recs: dict) -> str:
|
|
|
238
251
|
"```", "",
|
|
239
252
|
"`--speculative`(循环深度自推测解码)的 greedy 输出与普通解码一致,接受率打印到 stderr。"
|
|
240
253
|
"它要求 `--repetition-penalty 1.0`(此时的默认值)。在这个规模的模型上用 CPU 实测,它比普通解码慢。", "",
|
|
254
|
+
"`--lazy-experts`(MLX)只把主干留在内存里,路由专家从 SSD 缓存按需分页加载"
|
|
255
|
+
"(`~/.cache/chronos/expert_cache`,或 `$CHRONOS_EXPERT_CACHE`),greedy 输出不变。分页方式按权重自适应:"
|
|
256
|
+
"模型训练时开了专家簇局部性约束就按簇读文件,否则按单个专家读(`--paging auto|cluster|expert`)。"
|
|
257
|
+
"`--residency planned`(默认)优先淘汰最晚才会用到的页,`window` 保持 LRU 顺序。"
|
|
258
|
+
"可用 `--decay`、`--ram-ratio`、`--decode-wait`、`--verify-wait` 调整,默认值见 `--help`。"
|
|
259
|
+
"页读取统计打印到 stderr。", "",
|
|
241
260
|
"`bin/chronos` 会设置 `CHRONOS_BUNDLE` 和 `CHRONOS_MINIMIND_DIR`(指向 `third_party/` 里随包附带的 minimind),"
|
|
242
261
|
"全程不需要联网。不用 `bin/` 时,手动导出这两个变量后直接调用 `chronos bundle …`。", "",
|
|
243
262
|
"## 下一阶段:SFT 微调(Apple silicon / MLX)", "",
|
|
@@ -255,6 +274,10 @@ def _readme_zh(m: dict, recs: dict) -> str:
|
|
|
255
274
|
"`--mode pretrain` 在新文本上继续预训练;`--mode pretrain+sft` 两者都做(时间按 40/60 分配)。"
|
|
256
275
|
"用 `bin/chronos monitor --runs-dir work` 监控训练。微调后的检查点在 `work/run/*_ft_sft.best.pth`,"
|
|
257
276
|
"可用 `chronos bundle build --run-dir work/run --phase sft` 重新打包。", "",
|
|
277
|
+
"微调默认带专家簇局部性约束训练,这样得到的模型在推理时可以按簇分页(`--no-cluster-locality` 关闭)。"
|
|
278
|
+
"默认值:`--lambda-cluster 0.3`(来自合成任务上从头训练的实测扫描,对微调的效果尚未实测);"
|
|
279
|
+
"`--cluster-window` 取最接近你数据中位 SFT 记录长度的 2 的幂。spec 中记录了每个值的来源"
|
|
280
|
+
"(`extra.cluster_locality_source`)。簇数少于 2(E ≤ 2k)的模型不加这项约束。", "",
|
|
258
281
|
"档位(来自 `recommended.json`,每个数值都标注了来源):", "",
|
|
259
282
|
"| 档位 | 小时 | SFT 峰值 LR | 每步 token | Warmup | 用途 |", "|---|---|---|---|---|---|",
|
|
260
283
|
*_tier_rows(recs), "",
|
|
@@ -102,14 +102,20 @@ def _check_shards(shards_dir: str, phases: list[dict]) -> None:
|
|
|
102
102
|
|
|
103
103
|
|
|
104
104
|
def write_finetune_spec(bundle_dir: str, shards_dir: str, run_dir: str, *, tier: str = "quick", mode: str = "sft",
|
|
105
|
-
budget_hours: float | None = None, seed: int | None = None
|
|
106
|
-
|
|
105
|
+
budget_hours: float | None = None, seed: int | None = None, cluster_locality: bool = True,
|
|
106
|
+
lambda_cluster: float | None = None, cluster_window: int | None = None,
|
|
107
|
+
clusters: int | None = None) -> str:
|
|
108
|
+
"""Write ``<run_dir>/finetune_spec.json`` for ``chronos longrun --spec``.
|
|
109
|
+
|
|
110
|
+
The expert-cluster locality constraint is on by default; see
|
|
111
|
+
``recommend.cluster_locality_config`` for the defaults and their sources."""
|
|
107
112
|
from .recommend import longrun_spec
|
|
108
113
|
bundle_dir = os.path.abspath(bundle_dir)
|
|
109
114
|
with open(os.path.join(bundle_dir, "recipes", "source_run_spec.json"), encoding="utf-8") as fh:
|
|
110
115
|
recipe = json.load(fh)
|
|
111
116
|
spec = longrun_spec(bundle_dir, recipe, run_dir=run_dir, shards_dir=shards_dir, tier=tier, mode=mode,
|
|
112
|
-
budget_hours=budget_hours, seed=seed
|
|
117
|
+
budget_hours=budget_hours, seed=seed, cluster_locality=cluster_locality,
|
|
118
|
+
lambda_cluster=lambda_cluster, cluster_window=cluster_window, clusters=clusters)
|
|
113
119
|
_check_shards(spec["shards_dir"], spec["phases"])
|
|
114
120
|
os.makedirs(spec["run_dir"], exist_ok=True)
|
|
115
121
|
path = os.path.join(spec["run_dir"], "finetune_spec.json")
|
|
@@ -98,19 +98,100 @@ def tier_phases(recipe: dict, tier: str, mode: str = "sft") -> list[dict]:
|
|
|
98
98
|
raise ValueError(f"unknown mode {mode!r}; choose sft, pretrain or pretrain+sft")
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
def median_record_tokens(shards_dir: str, max_files: int = 8) -> float | None:
|
|
102
|
+
"""Median SFT record length (tokens) from the ``.idx`` offsets under ``<shards>/sft/train``.
|
|
103
|
+
|
|
104
|
+
Reads at most ``max_files`` index files (offsets only, no token data).
|
|
105
|
+
Returns None when the shards have no SFT records (packed pretrain text
|
|
106
|
+
has no record boundaries)."""
|
|
107
|
+
import glob
|
|
108
|
+
import numpy as np
|
|
109
|
+
files = sorted(glob.glob(os.path.join(shards_dir, "sft", "train", "*.idx")))[:max_files]
|
|
110
|
+
lengths = [np.diff(np.fromfile(f, dtype=np.int64)) for f in files]
|
|
111
|
+
lengths = [x for x in lengths if x.size]
|
|
112
|
+
if not lengths:
|
|
113
|
+
return None
|
|
114
|
+
return float(np.median(np.concatenate(lengths)))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def cluster_locality_config(model_config: dict, shards_dir: str | None, *, enabled: bool = True,
|
|
118
|
+
lambda_cluster: float | None = None, window: int | None = None,
|
|
119
|
+
clusters: int | None = None, seq_len: int | None = None) -> dict:
|
|
120
|
+
"""Cluster-locality fields for a fine-tune and where each value comes from.
|
|
121
|
+
|
|
122
|
+
On by default at ``DEFAULT_LAMBDA`` (measured sweep on a synthetic
|
|
123
|
+
multi-topic task, from-scratch training; its effect on a fine-tune of
|
|
124
|
+
weights trained without the constraint has not been measured). Off for
|
|
125
|
+
dense models and when there are fewer than two clusters."""
|
|
126
|
+
from chronos.expert_clusters import DEFAULT_LAMBDA, recommended_window
|
|
127
|
+
from chronos.model.config import ChronosConfig
|
|
128
|
+
# The values training will use: longrun builds ChronosConfig(**model_config).
|
|
129
|
+
cfg = ChronosConfig(**{k: v for k, v in model_config.items() if v is not None})
|
|
130
|
+
e = int(getattr(cfg, "num_experts", 0) or 0)
|
|
131
|
+
k = int(getattr(cfg, "num_experts_per_tok", 1) or 1)
|
|
132
|
+
seq = int(seq_len or getattr(cfg, "max_position_embeddings", 512) or 512)
|
|
133
|
+
usable = bool(getattr(cfg, "use_moe", True)) and e > 2 * k
|
|
134
|
+
lam = 0.0 if not (enabled and usable) else float(DEFAULT_LAMBDA if lambda_cluster is None else lambda_cluster)
|
|
135
|
+
if lam < 0:
|
|
136
|
+
raise ValueError("lambda_cluster must be >= 0")
|
|
137
|
+
median = median_record_tokens(shards_dir) if (shards_dir and window is None) else None
|
|
138
|
+
win = int(window) if window is not None else recommended_window(median, seq)
|
|
139
|
+
if win < 0:
|
|
140
|
+
raise ValueError("cluster_window must be >= 0 (0 = whole sequence)")
|
|
141
|
+
values = {"lambda_cluster_locality": lam, "cluster_locality_window": win,
|
|
142
|
+
"cluster_locality_clusters": int(clusters or 0)}
|
|
143
|
+
if not enabled:
|
|
144
|
+
why = "off (--no-cluster-locality)"
|
|
145
|
+
elif not usable:
|
|
146
|
+
why = "off: dense model or fewer than two clusters"
|
|
147
|
+
elif lambda_cluster is None:
|
|
148
|
+
why = f"default {DEFAULT_LAMBDA} (measured sweep, synthetic task; not measured for fine-tuning)"
|
|
149
|
+
else:
|
|
150
|
+
why = "user value"
|
|
151
|
+
if window is not None:
|
|
152
|
+
wsrc = "user value"
|
|
153
|
+
elif median is not None:
|
|
154
|
+
wsrc = f"rule: power of two nearest the median SFT record ({median:.0f} tokens), clamped to [16, {seq}]"
|
|
155
|
+
else:
|
|
156
|
+
wsrc = "default (no SFT record lengths in the data)"
|
|
157
|
+
return {"values": values, "source": {"lambda_cluster_locality": why, "cluster_locality_window": wsrc,
|
|
158
|
+
"cluster_locality_clusters": "user value" if clusters else "ceil(E / 2k)"}}
|
|
159
|
+
|
|
160
|
+
|
|
101
161
|
def longrun_spec(bundle_dir: str, recipe: dict, *, run_dir: str, shards_dir: str, tier: str,
|
|
102
|
-
mode: str = "sft", budget_hours: float | None = None, seed: int | None = None
|
|
103
|
-
|
|
162
|
+
mode: str = "sft", budget_hours: float | None = None, seed: int | None = None,
|
|
163
|
+
cluster_locality: bool = True, lambda_cluster: float | None = None,
|
|
164
|
+
cluster_window: int | None = None, clusters: int | None = None) -> dict:
|
|
165
|
+
"""A ``chronos longrun`` RunSpec (as a dict) that starts from the bundle weights.
|
|
166
|
+
|
|
167
|
+
The cluster-locality constraint is on by default (``cluster_locality_config``)."""
|
|
104
168
|
from .builder import load_bundle_manifest
|
|
105
169
|
manifest = load_bundle_manifest(bundle_dir)
|
|
106
170
|
bundle_dir = os.path.abspath(bundle_dir)
|
|
107
171
|
smoke = tier == "smoke"
|
|
172
|
+
model_config = copy.deepcopy(recipe["model_config"])
|
|
173
|
+
phases = tier_phases(recipe, tier, mode)
|
|
174
|
+
seq_len = min((int(p["seq_len"]) for p in phases if p.get("seq_len")), default=None)
|
|
175
|
+
# Decide from the weights being fine-tuned: the recipe may omit fields that
|
|
176
|
+
# took class defaults when it ran.
|
|
177
|
+
topology = dict(model_config)
|
|
178
|
+
try:
|
|
179
|
+
from chronos.model.checkpoint import read_checkpoint_config
|
|
180
|
+
saved, _ = read_checkpoint_config(os.path.join(bundle_dir, manifest["model"]["checkpoint"]))
|
|
181
|
+
topology.update({k: saved[k] for k in ("num_experts", "num_experts_per_tok", "use_moe",
|
|
182
|
+
"max_position_embeddings") if saved and k in saved})
|
|
183
|
+
except (OSError, ValueError):
|
|
184
|
+
pass
|
|
185
|
+
locality = cluster_locality_config(topology, shards_dir, enabled=cluster_locality,
|
|
186
|
+
lambda_cluster=lambda_cluster, window=cluster_window, clusters=clusters,
|
|
187
|
+
seq_len=seq_len)
|
|
188
|
+
model_config.update(locality["values"])
|
|
108
189
|
spec = {
|
|
109
190
|
"run_dir": os.path.abspath(run_dir),
|
|
110
191
|
"shards_dir": os.path.abspath(shards_dir),
|
|
111
192
|
"tokenizer_dir": os.path.join(bundle_dir, manifest["tokenizer"]["dir"]),
|
|
112
|
-
"model_config":
|
|
113
|
-
"phases":
|
|
193
|
+
"model_config": model_config,
|
|
194
|
+
"phases": phases,
|
|
114
195
|
"budget_hours": float(budget_hours if budget_hours is not None else TIERS[tier]["hours"]),
|
|
115
196
|
"eval_reserve_fraction": 0.1 if smoke else 0.04,
|
|
116
197
|
"log_every_steps": 2 if smoke else 20,
|
|
@@ -126,7 +207,7 @@ def longrun_spec(bundle_dir: str, recipe: dict, *, run_dir: str, shards_dir: str
|
|
|
126
207
|
"init_checkpoint": os.path.join(bundle_dir, manifest["model"]["checkpoint"]),
|
|
127
208
|
"export_prefix": manifest["name"].replace("-", "_") + "_ft",
|
|
128
209
|
"extra": {"raw_root": os.path.join(bundle_dir, "eval"), "from_bundle": manifest["name"],
|
|
129
|
-
"tier": tier, "mode": mode},
|
|
210
|
+
"tier": tier, "mode": mode, "cluster_locality_source": locality["source"]},
|
|
130
211
|
}
|
|
131
212
|
return spec
|
|
132
213
|
|
|
@@ -158,6 +239,11 @@ def build_recommendations(recipe: dict, planner_preset: dict | None, decode: dic
|
|
|
158
239
|
for p in recipe.get("phases", [])}, "recipe"),
|
|
159
240
|
"model_lambdas": _labelled({k: v for k, v in recipe.get("model_config", {}).items()
|
|
160
241
|
if k.startswith(("lambda_", "ponder", "fallback_mask"))}, "recipe"),
|
|
242
|
+
"cluster_locality": _labelled(
|
|
243
|
+
cluster_locality_config(recipe.get("model_config", {}), None)["values"], "rule",
|
|
244
|
+
"on by default for `chronos bundle finetune` (--no-cluster-locality to disable); lambda from "
|
|
245
|
+
"a measured sweep on a synthetic task (from-scratch training), window from the median SFT "
|
|
246
|
+
"record length of the fine-tune data when it has one"),
|
|
161
247
|
},
|
|
162
248
|
"torch_stages": torch_stages,
|
|
163
249
|
"data": _labelled({
|
|
@@ -7,6 +7,11 @@ falls into on long inputs. ``speculative=True`` uses recurrent-depth
|
|
|
7
7
|
self-speculative decoding (same greedy output, fewer full-depth forwards);
|
|
8
8
|
it needs ``repetition_penalty == 1`` because the penalty changes the target
|
|
9
9
|
distribution position by position.
|
|
10
|
+
|
|
11
|
+
``lazy_experts=True`` (MLX) keeps only the trunk in memory and pages routed
|
|
12
|
+
experts from an SSD cache (``chronos.lazy_experts``): by training cluster when
|
|
13
|
+
the checkpoint was trained with cluster locality, per expert otherwise, with
|
|
14
|
+
next-use planned residency by default. The greedy output is the same.
|
|
10
15
|
"""
|
|
11
16
|
from __future__ import annotations
|
|
12
17
|
|
|
@@ -20,7 +25,8 @@ LANG_TAGS = {"en_zh": ("<|en|>", "<|zh|>"), "zh_en": ("<|zh|>", "<|en|>")}
|
|
|
20
25
|
class BundleModel:
|
|
21
26
|
"""Weights + tokenizer of one bundle, ready for greedy translation."""
|
|
22
27
|
|
|
23
|
-
def __init__(self, bundle_dir: str, *, device: str = "cpu", dtype: str = "float32", backend: str = "torch"
|
|
28
|
+
def __init__(self, bundle_dir: str, *, device: str = "cpu", dtype: str = "float32", backend: str = "torch",
|
|
29
|
+
lazy_experts: bool = False, lazy_options=None, cache_root: str | None = None):
|
|
24
30
|
import torch
|
|
25
31
|
from tokenizers import Tokenizer
|
|
26
32
|
from chronos.model.checkpoint import chronos_config_from_checkpoint, load_checkpoint_into_model
|
|
@@ -35,6 +41,13 @@ class BundleModel:
|
|
|
35
41
|
self.config, _ = chronos_config_from_checkpoint(self.checkpoint)
|
|
36
42
|
if self.tokenizer.get_vocab_size() > int(self.config.vocab_size):
|
|
37
43
|
raise ValueError("bundle tokenizer is larger than the model vocabulary")
|
|
44
|
+
self.prompt_format = self.manifest.get("prompt_format", "pretrain_tags")
|
|
45
|
+
tid = self.tokenizer.token_to_id
|
|
46
|
+
self.stop_ids = {tid(t) for t in STOP_TOKENS if tid(t) is not None}
|
|
47
|
+
self.engine, self.lazy_info = None, {}
|
|
48
|
+
if lazy_experts:
|
|
49
|
+
self._init_lazy(backend, device, dtype, lazy_options, cache_root)
|
|
50
|
+
return
|
|
38
51
|
model = ChronosForCausalLM(self.config)
|
|
39
52
|
load_checkpoint_into_model(model, self.checkpoint, map_location="cpu")
|
|
40
53
|
self.backend, self.last_stats = backend, {}
|
|
@@ -59,9 +72,34 @@ class BundleModel:
|
|
|
59
72
|
self.device = device
|
|
60
73
|
else:
|
|
61
74
|
raise ValueError(f"backend must be 'torch' or 'mlx', not {backend!r}")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
|
|
76
|
+
def _init_lazy(self, backend, device, dtype, lazy_options, cache_root):
|
|
77
|
+
import mlx.core as mx
|
|
78
|
+
from chronos.lazy_experts import LazyOptions, build_mlx_lazy_engine, warm_ids
|
|
79
|
+
if backend != "mlx":
|
|
80
|
+
raise ValueError("lazy_experts needs backend='mlx'")
|
|
81
|
+
if not getattr(self.config, "use_moe", True):
|
|
82
|
+
raise ValueError("lazy_experts needs a MoE checkpoint")
|
|
83
|
+
if device == "cpu":
|
|
84
|
+
mx.set_default_device(mx.cpu)
|
|
85
|
+
elif device not in ("auto", "gpu"):
|
|
86
|
+
raise ValueError(f"MLX backend runs on 'gpu' or 'cpu', not {device!r}")
|
|
87
|
+
self.lazy_options = (lazy_options or LazyOptions()).validated()
|
|
88
|
+
# Outside the bundle (it may be read-only and its files are hashed).
|
|
89
|
+
root = cache_root or os.environ.get("CHRONOS_EXPERT_CACHE") or os.path.join(
|
|
90
|
+
os.path.expanduser("~"), ".cache", "chronos", "expert_cache")
|
|
91
|
+
self.engine, self.lazy_info = build_mlx_lazy_engine(self.checkpoint, self.config, self.lazy_options,
|
|
92
|
+
cache_root=root, dtype=dtype)
|
|
93
|
+
self.engine.store.warm_up(warm_ids(self.config, self.lazy_info))
|
|
94
|
+
self.model, self.backend, self.last_stats = self.engine.model, "mlx", {}
|
|
95
|
+
self.precision = None
|
|
96
|
+
self.device = "cpu" if device == "cpu" else "gpu"
|
|
97
|
+
|
|
98
|
+
def close(self) -> None:
|
|
99
|
+
"""Stop the lazy engine's prefetch thread (no-op without lazy experts)."""
|
|
100
|
+
if self.engine is not None:
|
|
101
|
+
self.engine.stop()
|
|
102
|
+
self.engine = None
|
|
65
103
|
|
|
66
104
|
# ---- prompts ---------------------------------------------------------------
|
|
67
105
|
def directions(self) -> list[str]:
|
|
@@ -85,7 +123,9 @@ class BundleModel:
|
|
|
85
123
|
if speculative and repetition_penalty != 1.0:
|
|
86
124
|
raise ValueError("speculative decoding needs repetition_penalty=1.0 (the penalty changes the target "
|
|
87
125
|
"distribution at every position)")
|
|
88
|
-
if
|
|
126
|
+
if self.engine is not None:
|
|
127
|
+
out = self._lazy(prompt, limit, repetition_penalty, speculative, draft_depth, gamma)
|
|
128
|
+
elif speculative:
|
|
89
129
|
out = self._speculative(prompt, limit, draft_depth, gamma)
|
|
90
130
|
elif self.backend == "mlx":
|
|
91
131
|
out = self._greedy_mlx(prompt, limit, repetition_penalty)
|
|
@@ -138,6 +178,29 @@ class BundleModel:
|
|
|
138
178
|
logits, _, cache = self.model(mx.array([[nxt]], dtype=mx.int32), cache=cache)
|
|
139
179
|
return out
|
|
140
180
|
|
|
181
|
+
def _lazy(self, prompt, limit, repetition_penalty, speculative, draft_depth, gamma):
|
|
182
|
+
import mlx.core as mx
|
|
183
|
+
kw = dict(speculative=True, draft_depth=draft_depth, gamma=gamma) if speculative else {}
|
|
184
|
+
out = []
|
|
185
|
+
for tok in self.engine.generate(mx.array([prompt], dtype=mx.int32), max_new_tokens=limit, temperature=0.0,
|
|
186
|
+
top_p=1.0, eos_token_id=sorted(self.stop_ids),
|
|
187
|
+
repetition_penalty=float(repetition_penalty),
|
|
188
|
+
**self.lazy_options.generate_kwargs(), **kw):
|
|
189
|
+
if tok in self.stop_ids:
|
|
190
|
+
break
|
|
191
|
+
out.append(int(tok))
|
|
192
|
+
st = dict(self.engine.last_stats or {})
|
|
193
|
+
agg = self.last_stats.setdefault("lazy", {"tokens": 0, "sync_ssd_loads": 0, "cluster_loads": 0})
|
|
194
|
+
agg["tokens"] += len(out)
|
|
195
|
+
agg["sync_ssd_loads"] += int(st.get("sync_ssd_loads", 0))
|
|
196
|
+
agg["cluster_loads"] = int(self.engine.store.stats().get("cluster_loads", 0))
|
|
197
|
+
if speculative and st.get("speculative"):
|
|
198
|
+
spec = self.last_stats.setdefault("speculative", {"proposed": 0, "accepted": 0, "verify_calls": 0,
|
|
199
|
+
"draft_calls": 0, "generated": 0})
|
|
200
|
+
for k in spec:
|
|
201
|
+
spec[k] += int(st["speculative"].get(k, 0))
|
|
202
|
+
return out
|
|
203
|
+
|
|
141
204
|
def _speculative(self, prompt, limit, draft_depth, gamma):
|
|
142
205
|
eos = sorted(self.stop_ids)
|
|
143
206
|
if self.backend == "mlx":
|
|
@@ -169,6 +232,11 @@ class BundleModel:
|
|
|
169
232
|
gen = self.greedy(self.prompt_ids(src, direction), limit, repetition_penalty, speculative=speculative,
|
|
170
233
|
draft_depth=draft_depth, gamma=gamma)
|
|
171
234
|
out.append(self.tokenizer.decode(gen, skip_special_tokens=True))
|
|
235
|
+
lazy = self.last_stats.get("lazy")
|
|
236
|
+
if lazy:
|
|
237
|
+
lazy.update({k: self.lazy_info[k] for k in ("paging_resolved", "num_pages", "trained_with_clusters",
|
|
238
|
+
"effective_expert_budget") if k in self.lazy_info})
|
|
239
|
+
lazy["residency"] = self.lazy_options.residency
|
|
172
240
|
spec = self.last_stats.get("speculative")
|
|
173
241
|
if spec:
|
|
174
242
|
spec["acceptance_rate"] = round(spec["accepted"] / spec["proposed"], 4) if spec["proposed"] else 0.0
|
|
@@ -37,6 +37,7 @@ MODEL_FIELDS = (
|
|
|
37
37
|
"lookahead_steps", "layer_aware_lookahead", "layer_aware_cache",
|
|
38
38
|
"lambda_balance", "lambda_temporal", "lambda_lookahead",
|
|
39
39
|
"lambda_lookahead_topk", "lambda_lookahead_union", "lambda_router_locality",
|
|
40
|
+
"lambda_cluster_locality", "cluster_locality_clusters", "cluster_locality_window",
|
|
40
41
|
"lambda_router_anchor", "fallback_mask_prob", "offload_miss_policy_default",
|
|
41
42
|
"use_hybrid_attention", "kv_latent_dim", "rope_dim", "rope_layout", "sliding_window_size",
|
|
42
43
|
"kv_cache_dtype", "kv_quant_group_size", "mla_decode", "mla_cache_rope", "router_scope",
|