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.
Files changed (191) hide show
  1. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/CHANGELOG.md +41 -0
  2. {project_chronos-0.3.0/Project_Chronos.egg-info → project_chronos-0.3.1a0}/PKG-INFO +1 -1
  3. {project_chronos-0.3.0 → project_chronos-0.3.1a0/Project_Chronos.egg-info}/PKG-INFO +1 -1
  4. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/Project_Chronos.egg-info/SOURCES.txt +5 -0
  5. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/__init__.py +1 -1
  6. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/cli.py +71 -8
  7. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/docs.py +23 -0
  8. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/finetune.py +9 -3
  9. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/recommend.py +91 -5
  10. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/runtime.py +73 -5
  11. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/config_schema.py +1 -0
  12. project_chronos-0.3.1a0/chronos/expert_clusters.py +121 -0
  13. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/expert_store.py +35 -3
  14. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/storage.py +40 -8
  15. project_chronos-0.3.1a0/chronos/lazy_experts.py +387 -0
  16. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/expert_store.py +85 -28
  17. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/inference.py +207 -18
  18. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/moe.py +2 -0
  19. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/trainer.py +39 -0
  20. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/checkpoint.py +9 -0
  21. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/config.py +13 -0
  22. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/planner.py +14 -1
  23. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/temporal_loss.py +76 -0
  24. project_chronos-0.3.1a0/chronos/residency_planner.py +154 -0
  25. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/runtime/inference_engine.py +6 -2
  26. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/ollama_server.py +8 -2
  27. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/runtime.py +26 -13
  28. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/loss_mixin.py +15 -0
  29. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/stage_utils.py +6 -0
  30. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/pyproject.toml +1 -1
  31. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_bundle.py +31 -0
  32. project_chronos-0.3.1a0/tests/test_lazy_experts.py +285 -0
  33. project_chronos-0.3.1a0/tests/test_residency_cluster.py +265 -0
  34. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_smoke.py +4 -1
  35. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos.py +6 -0
  36. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos_distill.py +2 -0
  37. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos_dpo.py +2 -0
  38. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos_grpo.py +2 -0
  39. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos_orpo.py +2 -0
  40. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/train_chronos_sft.py +3 -0
  41. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/i18n.py +32 -0
  42. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/presets.py +7 -0
  43. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/presets_v3.json +1 -0
  44. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/config_tab.py +13 -0
  45. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/inference_tab.py +108 -350
  46. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/LICENSE +0 -0
  47. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/MANIFEST.in +0 -0
  48. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/Project_Chronos.egg-info/dependency_links.txt +0 -0
  49. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/Project_Chronos.egg-info/entry_points.txt +0 -0
  50. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/Project_Chronos.egg-info/requires.txt +0 -0
  51. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/Project_Chronos.egg-info/top_level.txt +0 -0
  52. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/README.md +0 -0
  53. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/README_zh.md +0 -0
  54. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/THIRD_PARTY_NOTICES.md +0 -0
  55. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/_backend_legacy.py +0 -0
  56. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/app.py +0 -0
  57. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/backend/__init__.py +0 -0
  58. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/backend/dispatcher.py +0 -0
  59. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/backend/ext/__init__.py +0 -0
  60. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/backend/ext/opencl.py +0 -0
  61. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/backend/mac_diagnostics.py +0 -0
  62. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/__init__.py +0 -0
  63. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/bundle/builder.py +0 -0
  64. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/cli.py +0 -0
  65. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/__init__.py +0 -0
  66. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/__init__.py +0 -0
  67. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/clean.py +0 -0
  68. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/download.py +0 -0
  69. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/evalsets.py +0 -0
  70. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/pipeline.py +0 -0
  71. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/presets.py +0 -0
  72. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/readers.py +0 -0
  73. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/shards.py +0 -0
  74. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/corpus/sources.py +0 -0
  75. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/data/flexible_dataset.py +0 -0
  76. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/deps.py +0 -0
  77. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/eval/__init__.py +0 -0
  78. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/eval/benchmark.py +0 -0
  79. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/eval/io_profiler.py +0 -0
  80. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/export.py +0 -0
  81. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/__init__.py +0 -0
  82. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/async_prefetcher.py +0 -0
  83. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/cluster_layout.py +0 -0
  84. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/io/io_simulator.py +0 -0
  85. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/migrate.py +0 -0
  86. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/__init__.py +0 -0
  87. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/attention.py +0 -0
  88. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/kv_quant.py +0 -0
  89. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/model.py +0 -0
  90. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/moe_kernels.py +0 -0
  91. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/recurrent.py +0 -0
  92. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/speculative.py +0 -0
  93. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/__init__.py +0 -0
  94. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/inspect_checkpoint.py +0 -0
  95. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/io.py +0 -0
  96. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/longrun.py +0 -0
  97. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/optim.py +0 -0
  98. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/mlx/training/telemetry.py +0 -0
  99. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/__init__.py +0 -0
  100. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/factory.py +0 -0
  101. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/hf_io.py +0 -0
  102. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/hybrid_attention.py +0 -0
  103. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/kv_quant.py +0 -0
  104. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/lookahead_router.py +0 -0
  105. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/model_chronos.py +0 -0
  106. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/moe_chronos.py +0 -0
  107. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/multimodal.py +0 -0
  108. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/outputs.py +0 -0
  109. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/precision_ops.py +0 -0
  110. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/model/recurrent.py +0 -0
  111. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/__init__.py +0 -0
  112. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/evals.py +0 -0
  113. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/jsonl.py +0 -0
  114. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/process.py +0 -0
  115. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/server.py +0 -0
  116. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/snapshot.py +0 -0
  117. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/static/app.css +0 -0
  118. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/static/app.js +0 -0
  119. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/monitor/static/index.html +0 -0
  120. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/precision.py +0 -0
  121. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/router/__init__.py +0 -0
  122. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/router/expert_predictor.py +0 -0
  123. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/router/intent_classifier.py +0 -0
  124. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/router/prefill_scheduler.py +0 -0
  125. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/runtime/__init__.py +0 -0
  126. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/runtime/cache_manager.py +0 -0
  127. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/runtime/metrics.py +0 -0
  128. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/runtime/speculative.py +0 -0
  129. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/__init__.py +0 -0
  130. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/adapter.py +0 -0
  131. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/api_compat.py +0 -0
  132. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/serving/vllm_adapter.py +0 -0
  133. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/__init__.py +0 -0
  134. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/__main__.py +0 -0
  135. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/evaluate.py +0 -0
  136. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/extract.py +0 -0
  137. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/model.py +0 -0
  138. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/normalize.py +0 -0
  139. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tokenizer/trainer.py +0 -0
  140. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/__init__.py +0 -0
  141. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/chronos_trainer.py +0 -0
  142. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/device_utils.py +0 -0
  143. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/distill_trainer.py +0 -0
  144. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/dpo_trainer.py +0 -0
  145. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/grpo_trainer.py +0 -0
  146. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/optim_utils.py +0 -0
  147. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/orpo_trainer.py +0 -0
  148. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/paged_store.py +0 -0
  149. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/precision_training.py +0 -0
  150. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/reward.py +0 -0
  151. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/trainer/sft_trainer.py +0 -0
  152. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tuning/__init__.py +0 -0
  153. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tuning/_base_tuner.py +0 -0
  154. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/tuning/chronos_auto_tuner.py +0 -0
  155. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/chronos/verify.py +0 -0
  156. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/requirements.txt +0 -0
  157. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/setup.cfg +0 -0
  158. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/setup.py +0 -0
  159. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_interface_regressions.py +0 -0
  160. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_longrun.py +0 -0
  161. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_mlx_speculative.py +0 -0
  162. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_moe_dispatch.py +0 -0
  163. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_monitor.py +0 -0
  164. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_planner_ui.py +0 -0
  165. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_precision_chain.py +0 -0
  166. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_smoke_cuda.py +0 -0
  167. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_speculative.py +0 -0
  168. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_tokenizer_builder.py +0 -0
  169. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_ui_event_browser.py +0 -0
  170. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_ui_event_contract.py +0 -0
  171. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_ui_generic.py +0 -0
  172. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_v2_architecture.py +0 -0
  173. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_v3_inference_options.py +0 -0
  174. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/tests/test_v3_kernel.py +0 -0
  175. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/__init__.py +0 -0
  176. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/estimator.py +0 -0
  177. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/event_contract.js +0 -0
  178. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/event_contract.py +0 -0
  179. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/gradio_compat.py +0 -0
  180. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/planner_panel.py +0 -0
  181. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/__init__.py +0 -0
  182. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/autotune_tab.py +0 -0
  183. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/benchmark_tab.py +0 -0
  184. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/data_tab.py +0 -0
  185. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/export_tab.py +0 -0
  186. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/iomon_tab.py +0 -0
  187. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/monitor_tab.py +0 -0
  188. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/pipeline_tab.py +0 -0
  189. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/serve_tab.py +0 -0
  190. {project_chronos-0.3.0 → project_chronos-0.3.1a0}/ui/tabs/tokenizer_tab.py +0 -0
  191. {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.0
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.0
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
@@ -1,5 +1,5 @@
1
1
  # Project Chronos: On-device low-latency lookahead dual-layer MoE inference
2
- __version__ = "0.3.0"
2
+ __version__ = "0.3.1a0"
3
3
 
4
4
  # Bootstrap minimind dependency before any submodule imports
5
5
  from chronos.deps import ensure_minimind as _ensure_minimind
@@ -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 = BundleModel(_bundle_dir(a.bundle), device=a.device, dtype=a.dtype, backend=a.backend)
132
- for hyp in bm.translate(lines, a.direction, max_new_tokens=a.max_new_tokens, repetition_penalty=rp,
133
- speculative=a.speculative, draft_depth=a.draft_depth, gamma=a.gamma):
134
- print(hyp, flush=True)
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 BundleModel, eval_pairs, score
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
- report = score(BundleModel(bundle, device=a.device), pairs, repetition_penalty=a.repetition_penalty)
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) -> str:
106
- """Write ``<run_dir>/finetune_spec.json`` for ``chronos longrun --spec``."""
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) -> dict:
103
- """A ``chronos longrun`` RunSpec (as a dict) that starts from the bundle weights."""
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": copy.deepcopy(recipe["model_config"]),
113
- "phases": tier_phases(recipe, tier, mode),
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
- self.prompt_format = self.manifest.get("prompt_format", "pretrain_tags")
63
- tid = self.tokenizer.token_to_id
64
- self.stop_ids = {tid(t) for t in STOP_TOKENS if tid(t) is not None}
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 speculative:
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",