Project-Chronos 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. {project_chronos-0.2.0 → project_chronos-0.2.1}/PKG-INFO +1 -1
  2. {project_chronos-0.2.0 → project_chronos-0.2.1}/Project_Chronos.egg-info/PKG-INFO +1 -1
  3. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/mlx/expert_store.py +21 -5
  4. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/mlx/inference.py +128 -46
  5. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/mlx/training/trainer.py +127 -0
  6. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/model/checkpoint.py +3 -0
  7. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/model/config.py +6 -3
  8. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/model/temporal_loss.py +121 -0
  9. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/chronos_trainer.py +15 -9
  10. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/distill_trainer.py +11 -7
  11. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/dpo_trainer.py +11 -9
  12. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/grpo_trainer.py +6 -4
  13. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/loss_mixin.py +35 -0
  14. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/orpo_trainer.py +11 -9
  15. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/sft_trainer.py +12 -8
  16. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/stage_utils.py +15 -2
  17. {project_chronos-0.2.0 → project_chronos-0.2.1}/pyproject.toml +1 -1
  18. {project_chronos-0.2.0 → project_chronos-0.2.1}/tests/test_smoke.py +378 -1
  19. {project_chronos-0.2.0 → project_chronos-0.2.1}/train_chronos.py +10 -18
  20. {project_chronos-0.2.0 → project_chronos-0.2.1}/train_chronos_distill.py +10 -2
  21. {project_chronos-0.2.0 → project_chronos-0.2.1}/train_chronos_dpo.py +10 -2
  22. {project_chronos-0.2.0 → project_chronos-0.2.1}/train_chronos_grpo.py +11 -3
  23. {project_chronos-0.2.0 → project_chronos-0.2.1}/train_chronos_orpo.py +10 -2
  24. {project_chronos-0.2.0 → project_chronos-0.2.1}/train_chronos_sft.py +10 -2
  25. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/presets.py +3 -2
  26. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/tabs/config_tab.py +6 -3
  27. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/tabs/inference_tab.py +157 -5
  28. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/tabs/train_tab.py +442 -72
  29. {project_chronos-0.2.0 → project_chronos-0.2.1}/LICENSE +0 -0
  30. {project_chronos-0.2.0 → project_chronos-0.2.1}/MANIFEST.in +0 -0
  31. {project_chronos-0.2.0 → project_chronos-0.2.1}/Project_Chronos.egg-info/SOURCES.txt +0 -0
  32. {project_chronos-0.2.0 → project_chronos-0.2.1}/Project_Chronos.egg-info/dependency_links.txt +0 -0
  33. {project_chronos-0.2.0 → project_chronos-0.2.1}/Project_Chronos.egg-info/entry_points.txt +0 -0
  34. {project_chronos-0.2.0 → project_chronos-0.2.1}/Project_Chronos.egg-info/requires.txt +0 -0
  35. {project_chronos-0.2.0 → project_chronos-0.2.1}/Project_Chronos.egg-info/top_level.txt +0 -0
  36. {project_chronos-0.2.0 → project_chronos-0.2.1}/README.md +0 -0
  37. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/__init__.py +0 -0
  38. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/_backend_legacy.py +0 -0
  39. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/app.py +0 -0
  40. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/backend/__init__.py +0 -0
  41. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/backend/dispatcher.py +0 -0
  42. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/backend/ext/__init__.py +0 -0
  43. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/backend/ext/opencl.py +0 -0
  44. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/backend/mac_diagnostics.py +0 -0
  45. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/cli.py +0 -0
  46. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/data/__init__.py +0 -0
  47. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/data/flexible_dataset.py +0 -0
  48. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/deps.py +0 -0
  49. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/eval/__init__.py +0 -0
  50. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/eval/benchmark.py +0 -0
  51. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/eval/io_profiler.py +0 -0
  52. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/export.py +0 -0
  53. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/io/__init__.py +0 -0
  54. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/io/async_prefetcher.py +0 -0
  55. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/io/cluster_layout.py +0 -0
  56. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/io/expert_store.py +0 -0
  57. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/io/io_simulator.py +0 -0
  58. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/io/storage.py +0 -0
  59. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/mlx/__init__.py +0 -0
  60. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/mlx/attention.py +0 -0
  61. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/mlx/model.py +0 -0
  62. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/mlx/moe.py +0 -0
  63. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/mlx/training/__init__.py +0 -0
  64. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/mlx/training/io.py +0 -0
  65. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/model/__init__.py +0 -0
  66. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/model/hf_io.py +0 -0
  67. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/model/hybrid_attention.py +0 -0
  68. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/model/lookahead_router.py +0 -0
  69. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/model/model_chronos.py +0 -0
  70. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/model/moe_chronos.py +0 -0
  71. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/router/__init__.py +0 -0
  72. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/router/expert_predictor.py +0 -0
  73. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/router/intent_classifier.py +0 -0
  74. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/router/prefill_scheduler.py +0 -0
  75. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/runtime/__init__.py +0 -0
  76. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/runtime/cache_manager.py +0 -0
  77. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/runtime/inference_engine.py +0 -0
  78. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/runtime/metrics.py +0 -0
  79. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/serving/__init__.py +0 -0
  80. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/serving/vllm_adapter.py +0 -0
  81. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/__init__.py +0 -0
  82. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/device_utils.py +0 -0
  83. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/optim_utils.py +0 -0
  84. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/trainer/reward.py +0 -0
  85. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/tuning/__init__.py +0 -0
  86. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/tuning/_base_tuner.py +0 -0
  87. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/tuning/chronos_auto_tuner.py +0 -0
  88. {project_chronos-0.2.0 → project_chronos-0.2.1}/chronos/verify.py +0 -0
  89. {project_chronos-0.2.0 → project_chronos-0.2.1}/requirements.txt +0 -0
  90. {project_chronos-0.2.0 → project_chronos-0.2.1}/setup.cfg +0 -0
  91. {project_chronos-0.2.0 → project_chronos-0.2.1}/setup.py +0 -0
  92. {project_chronos-0.2.0 → project_chronos-0.2.1}/tests/test_smoke_cuda.py +0 -0
  93. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/__init__.py +0 -0
  94. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/estimator.py +0 -0
  95. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/gradio_compat.py +0 -0
  96. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/i18n.py +0 -0
  97. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/tabs/__init__.py +0 -0
  98. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/tabs/autotune_tab.py +0 -0
  99. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/tabs/benchmark_tab.py +0 -0
  100. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/tabs/data_tab.py +0 -0
  101. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/tabs/export_tab.py +0 -0
  102. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/tabs/iomon_tab.py +0 -0
  103. {project_chronos-0.2.0 → project_chronos-0.2.1}/ui/tabs/pipeline_tab.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Project_Chronos
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: On-device low-latency lookahead dual-layer MoE inference — Lookahead Routing + Async Prefetch + Hybrid Attention
5
5
  License-Expression: Apache-2.0
6
6
  Keywords: llm,moe,inference,lookahead-routing,hybrid-attention,edge-ai
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Project_Chronos
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: On-device low-latency lookahead dual-layer MoE inference — Lookahead Routing + Async Prefetch + Hybrid Attention
5
5
  License-Expression: Apache-2.0
6
6
  Keywords: llm,moe,inference,lookahead-routing,hybrid-attention,edge-ai
@@ -72,6 +72,7 @@ class MLXExpertStore:
72
72
  self._warm_lru: collections.OrderedDict = collections.OrderedDict()
73
73
  self._cluster_manifest: ClusterManifest | None = None
74
74
  self._loaded_clusters: set[int] = set()
75
+ self._protected_experts: set[int] = set()
75
76
  self.storage_format = "npy"
76
77
  self.attach_cluster_manifest(ssd_dir)
77
78
  self._stats = collections.Counter()
@@ -296,18 +297,25 @@ class MLXExpertStore:
296
297
  self._warm_lru.move_to_end(expert_id)
297
298
  self._evict_warm_locked(protected_expert_id=protected_expert_id)
298
299
 
300
+ def set_protected_experts(self, expert_ids: List[int] | set[int] | tuple[int, ...]):
301
+ with self._lock:
302
+ self._protected_experts = {
303
+ int(eid) for eid in expert_ids
304
+ if 0 <= int(eid) < int(self.num_experts)
305
+ }
306
+
299
307
  def _evict_warm_locked(self, protected_expert_id: int | None = None):
300
308
  while len(self._warm_lru) > self._warm_capacity:
301
309
  evict_id = None
302
310
  for candidate in self._warm_lru.keys():
303
- if candidate == protected_expert_id:
311
+ if candidate == protected_expert_id or candidate in self._protected_experts:
304
312
  continue
305
313
  if candidate not in self._hot_lru:
306
314
  evict_id = candidate
307
315
  break
308
316
  if evict_id is None:
309
317
  for candidate in self._warm_lru.keys():
310
- if candidate != protected_expert_id:
318
+ if candidate != protected_expert_id and candidate not in self._protected_experts:
311
319
  evict_id = candidate
312
320
  break
313
321
  if evict_id is None:
@@ -455,9 +463,17 @@ class MLXExpertStore:
455
463
  self._hot_lru.move_to_end(expert_id)
456
464
  else:
457
465
  while len(self._hot_lru) >= self._capacity:
458
- evicted, _ = self._hot_lru.popitem(last=False)
459
- if evicted == expert_id:
460
- continue
466
+ evicted = None
467
+ for candidate in self._hot_lru.keys():
468
+ if candidate != expert_id and candidate not in self._protected_experts:
469
+ evicted = candidate
470
+ break
471
+ if evicted is None:
472
+ evicted, _ = self._hot_lru.popitem(last=False)
473
+ if evicted == expert_id:
474
+ continue
475
+ else:
476
+ self._hot_lru.pop(evicted, None)
461
477
  # Keep warm weights in unified memory, but drop the
462
478
  # executable live module so masked MoE cannot call an
463
479
  # expert outside the hot execution budget.
@@ -47,7 +47,11 @@ class ChronosMLXInferenceEngine:
47
47
  self.last_stats: dict = {}
48
48
  self._last_predicted: set[int] = set()
49
49
  self._stats_lock = threading.Lock()
50
- self._runtime_stats = {
50
+ self._runtime_stats = self._new_runtime_stats()
51
+
52
+ @staticmethod
53
+ def _new_runtime_stats(all_resident_fast_path: bool = False) -> dict:
54
+ return {
51
55
  "resident_hits": 0,
52
56
  "resident_misses": 0,
53
57
  "resident_vram_hits": 0,
@@ -60,6 +64,9 @@ class ChronosMLXInferenceEngine:
60
64
  "prefetch_wait_time_s": 0.0,
61
65
  "sync_ssd_loads": 0,
62
66
  "on_demand_load_time_s": 0.0,
67
+ "cold_prediction_hits": 0,
68
+ "cold_prediction_total": 0,
69
+ "all_resident_fast_path": bool(all_resident_fast_path),
63
70
  }
64
71
 
65
72
  def setup(self, warm_expert_ids: Optional[List[int]] = None):
@@ -90,6 +97,9 @@ class ChronosMLXInferenceEngine:
90
97
  if not was_hot and not was_warm:
91
98
  with self._stats_lock:
92
99
  self._runtime_stats["sync_ssd_loads"] += 1
100
+ self._runtime_stats["cold_prediction_total"] += 1
101
+ if eid in self._last_predicted:
102
+ self._runtime_stats["cold_prediction_hits"] += 1
93
103
  ok = self.store.promote_to_vram(eid)
94
104
  elapsed = time.monotonic() - t0
95
105
  with self._stats_lock:
@@ -129,6 +139,14 @@ class ChronosMLXInferenceEngine:
129
139
  moe.runtime_on_demand_loader = loader
130
140
  moe.runtime_touch_expert = touch
131
141
 
142
+ def _clear_runtime_hooks(self):
143
+ for layer in self.model.layers:
144
+ moe = getattr(layer, "mlp", None)
145
+ if isinstance(moe, ChronosMLXMOE):
146
+ moe.runtime_miss_policy = "sync_on_demand"
147
+ moe.runtime_on_demand_loader = None
148
+ moe.runtime_touch_expert = None
149
+
132
150
  # ── Prefetch background thread ────────────────────────────────
133
151
 
134
152
  def _prefetch_loop(self):
@@ -149,37 +167,63 @@ class ChronosMLXInferenceEngine:
149
167
  with self._stats_lock:
150
168
  self._runtime_stats["prefetch_queue_drops"] += 1
151
169
 
152
- def _prefetch_and_promote_window(self, expert_ids: List[int], timeout_s: float = 0.012) -> None:
153
- if not expert_ids or self.store.storage_format == "full_dram":
170
+ def _prefetch_and_promote_window(
171
+ self,
172
+ prefetch_ids: List[int],
173
+ promote_ids: Optional[List[int]] = None,
174
+ timeout_s: float = 0.012,
175
+ ) -> None:
176
+ if not prefetch_ids or self.store.storage_format == "full_dram":
154
177
  return
178
+ promote_ids = list(dict.fromkeys(int(eid) for eid in (promote_ids or prefetch_ids)))
155
179
  pending = []
156
- for eid in dict.fromkeys(int(eid) for eid in expert_ids):
180
+ for eid in dict.fromkeys(int(eid) for eid in prefetch_ids):
157
181
  with self.store._lock:
158
182
  if eid in self.store._hot_lru:
159
183
  continue
160
184
  if eid in self.store._warm and self.store._layer_states_complete(self.store._warm.get(eid)):
161
- pending.append(eid)
162
185
  continue
163
186
  pending.append(eid)
164
- if not pending:
165
- return
166
- self._schedule_prefetch(pending)
187
+ if pending:
188
+ self._schedule_prefetch(pending)
167
189
  deadline = time.monotonic() + max(0.0, float(timeout_s))
168
190
  while time.monotonic() < deadline:
169
- self._promote_ready(pending)
191
+ self._promote_ready(promote_ids)
170
192
  with self.store._lock:
171
193
  ready = all(
172
194
  eid in self.store._hot_lru
173
195
  or (eid in self.store._warm and self.store._layer_states_complete(self.store._warm.get(eid)))
174
- for eid in pending
196
+ for eid in promote_ids
175
197
  )
176
198
  if ready:
177
199
  break
178
200
  time.sleep(0.001)
179
- self._promote_ready(pending)
201
+ self._promote_ready(promote_ids)
180
202
  with self._stats_lock:
181
203
  self._runtime_stats["prefetch_wait_time_s"] += max(0.0, time.monotonic() - (deadline - max(0.0, float(timeout_s))))
182
204
 
205
+ def _all_resident_fast_path_enabled(self) -> bool:
206
+ if self.store.storage_format == "full_dram":
207
+ return True
208
+ num_experts = int(getattr(self.config, "num_experts", 0) or 0)
209
+ if num_experts <= 0:
210
+ return False
211
+ if int(getattr(self.store, "_capacity", 0) or 0) < num_experts:
212
+ return False
213
+ if int(getattr(self.store, "_warm_capacity", 0) or 0) < num_experts:
214
+ return False
215
+ return len(self.store.hot_expert_ids()) >= num_experts
216
+
217
+ def _protect_prediction_window(self, expert_ids: List[int]) -> None:
218
+ capacity = max(
219
+ 1,
220
+ int(getattr(self.store, "_warm_capacity", 1) or 1),
221
+ int(getattr(self.store, "_capacity", 1) or 1),
222
+ )
223
+ hot_ids = sorted(self.store.hot_expert_ids())
224
+ protected = list(dict.fromkeys([int(eid) for eid in expert_ids] + hot_ids))[:capacity]
225
+ self.store.set_protected_experts(protected)
226
+
183
227
  def stop(self):
184
228
  self._stop.set()
185
229
  try:
@@ -210,23 +254,15 @@ class ChronosMLXInferenceEngine:
210
254
  setup_mem = self._memory_snapshot()
211
255
  self._last_predicted = set()
212
256
  with self._stats_lock:
213
- self._runtime_stats = {
214
- "resident_hits": 0,
215
- "resident_misses": 0,
216
- "resident_vram_hits": 0,
217
- "resident_ram_hits": 0,
218
- "selection_hits": 0,
219
- "selection_misses": 0,
220
- "prediction_hits": 0,
221
- "prediction_total": 0,
222
- "prefetch_queue_drops": 0,
223
- "prefetch_wait_time_s": 0.0,
224
- "sync_ssd_loads": 0,
225
- "on_demand_load_time_s": 0.0,
226
- }
227
- self._install_runtime_hooks()
257
+ all_resident_fast_path = self._all_resident_fast_path_enabled()
258
+ self._runtime_stats = self._new_runtime_stats(all_resident_fast_path)
259
+ if all_resident_fast_path:
260
+ self._clear_runtime_hooks()
261
+ self.store.set_protected_experts([])
262
+ else:
263
+ self._install_runtime_hooks()
228
264
  # ── Prefill phase: front-load expert IO ───────────────────────────
229
- if scheduler is not None:
265
+ if scheduler is not None and not all_resident_fast_path:
230
266
  import torch, numpy as np
231
267
  ids_np = np.array(input_ids.tolist(), dtype=np.int64)
232
268
  ids_pt = torch.from_numpy(ids_np)
@@ -235,7 +271,7 @@ class ChronosMLXInferenceEngine:
235
271
 
236
272
  # Prefill forward
237
273
  prefill_t0 = time.monotonic()
238
- if self.store.storage_format == "full_dram":
274
+ if all_resident_fast_path:
239
275
  prefill_masks = None
240
276
  else:
241
277
  prefill_masks = self._build_avail_masks(None)
@@ -248,21 +284,41 @@ class ChronosMLXInferenceEngine:
248
284
  next_token = self._sample(logits[:, -1, :], temperature, top_p)
249
285
  activated_ids: List[int] = []
250
286
  tokens = 1
251
- if scheduler is None and lookahead_probs is not None:
252
- future_ids = self._predict_future_experts(lookahead_probs)
287
+ if not all_resident_fast_path and scheduler is None and lookahead_probs is not None:
288
+ future_ids = self._predict_future_experts(
289
+ lookahead_probs,
290
+ capacity=int(getattr(self.store, "_warm_capacity", self.config.num_experts) or self.config.num_experts),
291
+ )
292
+ immediate_ids = self._predict_future_experts(
293
+ lookahead_probs,
294
+ capacity=int(getattr(self.store, "_capacity", 1) or 1),
295
+ max_steps=1,
296
+ )
253
297
  self._last_predicted = set(int(eid) for eid in future_ids)
254
- self._prefetch_and_promote_window(future_ids, timeout_s=0.025)
298
+ self._protect_prediction_window(future_ids)
299
+ self._prefetch_and_promote_window(future_ids, promote_ids=immediate_ids, timeout_s=0.025)
255
300
  yield int(next_token.item())
256
301
 
257
302
  for _ in range(max_new_tokens - 1):
258
- if scheduler is not None:
303
+ if all_resident_fast_path:
304
+ avail_masks = None
305
+ elif scheduler is not None:
259
306
  avail_masks = self._build_avail_masks(next_token)
260
307
  else:
261
308
  # LookaheadRouter-driven prefetch
262
309
  if lookahead_probs is not None:
263
- future_ids = self._predict_future_experts(lookahead_probs)
310
+ future_ids = self._predict_future_experts(
311
+ lookahead_probs,
312
+ capacity=int(getattr(self.store, "_warm_capacity", self.config.num_experts) or self.config.num_experts),
313
+ )
314
+ immediate_ids = self._predict_future_experts(
315
+ lookahead_probs,
316
+ capacity=int(getattr(self.store, "_capacity", 1) or 1),
317
+ max_steps=1,
318
+ )
264
319
  self._last_predicted = set(int(eid) for eid in future_ids)
265
- self._prefetch_and_promote_window(future_ids)
320
+ self._protect_prediction_window(future_ids)
321
+ self._prefetch_and_promote_window(future_ids, promote_ids=immediate_ids)
266
322
  avail_masks = self._build_avail_masks(next_token)
267
323
 
268
324
  token_in = next_token.reshape(1, 1)
@@ -298,22 +354,39 @@ class ChronosMLXInferenceEngine:
298
354
  **self.store.stats(),
299
355
  }
300
356
 
301
- def _predict_future_experts(self, lookahead_probs: mx.array) -> List[int]:
357
+ def _predict_future_experts(
358
+ self,
359
+ lookahead_probs: mx.array,
360
+ capacity: Optional[int] = None,
361
+ max_steps: Optional[int] = None,
362
+ ) -> List[int]:
302
363
  """Extract bounded top-k expert IDs for future steps."""
303
364
  # lookahead_probs: [B, S, K+1, E] — take last token, steps 1..K
304
365
  future = lookahead_probs[0, -1, 1:, :] # [K, E]
366
+ if max_steps is not None:
367
+ future = future[:max(0, int(max_steps))]
368
+ if future.shape[0] <= 0:
369
+ return []
370
+ num_experts = int(getattr(self.config, "num_experts", future.shape[-1]) or future.shape[-1])
305
371
  top_k = max(1, min(
306
372
  int(getattr(self.config, "num_experts_per_tok", 1) or 1),
307
- int(getattr(self.config, "num_experts", future.shape[-1]) or future.shape[-1]),
373
+ num_experts,
308
374
  ))
309
- ids: list[int] = []
310
- for k in range(future.shape[0]):
311
- step = future[k]
312
- if top_k == 1:
313
- ids.append(int(mx.argmax(step).item()))
314
- else:
315
- ids.extend(int(v) for v in mx.argpartition(-step, kth=top_k - 1, axis=-1)[:top_k].tolist())
316
- return list(dict.fromkeys(ids))
375
+ budget = num_experts if capacity is None else max(1, min(num_experts, int(capacity)))
376
+ scores = mx.zeros((num_experts,), dtype=mx.float32)
377
+ for offset in range(future.shape[0]):
378
+ step = future[offset].astype(mx.float32)
379
+ if top_k >= num_experts:
380
+ scores = scores + step / float(offset + 1)
381
+ continue
382
+ step_ids = mx.argpartition(-step, kth=top_k - 1, axis=-1)[:top_k]
383
+ step_vals = mx.take(step, step_ids) / float(offset + 1)
384
+ one_hot = (step_ids[:, None] == mx.arange(num_experts)).astype(mx.float32)
385
+ scores = scores + (one_hot * step_vals[:, None]).sum(axis=0)
386
+ mx.eval(scores)
387
+ scored = [(idx, float(value)) for idx, value in enumerate(scores.tolist()) if float(value) > 0.0]
388
+ scored.sort(key=lambda item: (-item[1], item[0]))
389
+ return [int(idx) for idx, _value in scored[:budget]]
317
390
 
318
391
  def _build_avail_masks(self, _token) -> List[set[int]] | None:
319
392
  """Promote prefetched experts and return per-layer availability masks."""
@@ -339,9 +412,14 @@ class ChronosMLXInferenceEngine:
339
412
  stats = dict(self._runtime_stats)
340
413
  total = int(stats.get("selection_hits", 0)) + int(stats.get("selection_misses", 0))
341
414
  pred_total = int(stats.get("prediction_total", 0))
415
+ cold_pred_total = int(stats.get("cold_prediction_total", 0))
416
+ all_resident_fast_path = bool(stats.get("all_resident_fast_path", False))
417
+ resident_hit_rate = 1.0 if all_resident_fast_path else (
418
+ float(stats.get("selection_hits", 0)) / max(total, 1)
419
+ )
342
420
  return {
343
- "resident_hit_rate": round(float(stats.get("selection_hits", 0)) / max(total, 1), 4),
344
- "cache_hit_rate": round(float(stats.get("selection_hits", 0)) / max(total, 1), 4),
421
+ "resident_hit_rate": round(resident_hit_rate, 4),
422
+ "cache_hit_rate": round(resident_hit_rate, 4),
345
423
  "cache_hits": int(stats.get("selection_hits", 0)),
346
424
  "cache_misses": int(stats.get("selection_misses", 0)),
347
425
  "expert_selection_hits": int(stats.get("selection_hits", 0)),
@@ -349,6 +427,10 @@ class ChronosMLXInferenceEngine:
349
427
  "prediction_hit_rate": round(float(stats.get("prediction_hits", 0)) / max(pred_total, 1), 4),
350
428
  "prediction_hits": int(stats.get("prediction_hits", 0)),
351
429
  "prediction_total": pred_total,
430
+ "cold_miss_predict_hit_rate": round(float(stats.get("cold_prediction_hits", 0)) / max(cold_pred_total, 1), 4),
431
+ "cold_prediction_hits": int(stats.get("cold_prediction_hits", 0)),
432
+ "cold_prediction_total": cold_pred_total,
433
+ "all_resident_fast_path": all_resident_fast_path,
352
434
  "resident_vram_hits": int(stats.get("resident_vram_hits", 0)),
353
435
  "resident_ram_hits": int(stats.get("resident_ram_hits", 0)),
354
436
  "prefetch_queue_drops": int(stats.get("prefetch_queue_drops", 0)),
@@ -101,6 +101,19 @@ def _temporal_locality_loss(router_mean: mx.array) -> mx.array:
101
101
  return (diff * diff).sum(axis=-1).mean()
102
102
 
103
103
 
104
+ def _router_locality_loss(router_mean: mx.array, num_experts_per_tok: int) -> mx.array:
105
+ if router_mean.shape[1] <= 1:
106
+ return mx.zeros(())
107
+ B, _S, E = router_mean.shape
108
+ top_k = max(1, min(int(num_experts_per_tok or 1), E))
109
+ prev = mx.stop_gradient(router_mean[:, :-1, :])
110
+ cur = router_mean[:, 1:, :]
111
+ target_ids = mx.argpartition(-prev, kth=top_k - 1, axis=-1)[:, :, :top_k]
112
+ target_mask = (target_ids[..., None] == mx.arange(E)).astype(cur.dtype).sum(axis=-2)
113
+ mass = (cur * target_mask).sum(axis=-1)
114
+ return (-mx.log(mx.maximum(mass / float(top_k), 1e-9))).mean()
115
+
116
+
104
117
  def _load_balance_loss(router_4d: mx.array, num_experts_per_tok: int) -> mx.array:
105
118
  B, S, L, E = router_4d.shape
106
119
  top_k = max(1, min(int(num_experts_per_tok or 1), E))
@@ -171,6 +184,96 @@ def _lookahead_topk_hit_loss(
171
184
  return total / max(terms, 1)
172
185
 
173
186
 
187
+ def _lookahead_union_loss(
188
+ lookahead_probs: mx.array | None,
189
+ teacher_router_probs: mx.array | None,
190
+ lookahead_steps: int,
191
+ num_experts_per_tok: int,
192
+ ) -> mx.array:
193
+ if lookahead_probs is None:
194
+ return mx.zeros(())
195
+ if teacher_router_probs is None:
196
+ return mx.zeros((), dtype=lookahead_probs.dtype)
197
+ B, S, Kp1, E = lookahead_probs.shape
198
+ K = min(int(lookahead_steps or 0), Kp1 - 1)
199
+ top_k = max(1, min(int(num_experts_per_tok or 1), E))
200
+ if K <= 0 or S <= K:
201
+ return mx.zeros((), dtype=lookahead_probs.dtype)
202
+ teacher = mx.stop_gradient(teacher_router_probs.astype(mx.float32))
203
+ union_target = mx.zeros((B, S - K, E), dtype=mx.float32)
204
+ for k in range(1, K + 1):
205
+ target_ids = mx.argpartition(-teacher[:, k:k + S - K, :], kth=top_k - 1, axis=-1)[:, :, :top_k]
206
+ target_mask = (target_ids[..., None] == mx.arange(E)).astype(mx.float32).sum(axis=-2)
207
+ union_target = mx.maximum(union_target, target_mask)
208
+ pred_union = lookahead_probs[:, :S - K, 1:K + 1, :].astype(mx.float32).max(axis=2)
209
+ mass = (pred_union * union_target).sum(axis=-1)
210
+ target_count = mx.maximum(union_target.sum(axis=-1), 1.0)
211
+ return (-mx.log(mx.maximum(mass / target_count, 1e-9))).mean()
212
+
213
+
214
+ def _offload_training_metrics(
215
+ router_mean: mx.array | None,
216
+ lookahead_probs: mx.array | None,
217
+ lookahead_steps: int,
218
+ num_experts_per_tok: int,
219
+ ) -> dict[str, float]:
220
+ if router_mean is None:
221
+ return {}
222
+ B, S, E = router_mean.shape
223
+ top_k = max(1, min(int(num_experts_per_tok or 1), E))
224
+ top_ids = mx.argpartition(-router_mean, kth=top_k - 1, axis=-1)[:, :, :top_k]
225
+ mx.eval(top_ids)
226
+ import numpy as np
227
+
228
+ ids_np = np.array(top_ids)
229
+ out = {
230
+ "expert_working_set": float(len(set(int(v) for v in ids_np.reshape(-1)))),
231
+ "router_adjacent_jaccard": 1.0,
232
+ "lookahead_topk_recall": 0.0,
233
+ "lookahead_union_recall": 0.0,
234
+ }
235
+ if S > 1:
236
+ vals = []
237
+ for b in range(B):
238
+ for t in range(S - 1):
239
+ a = set(int(v) for v in ids_np[b, t])
240
+ c = set(int(v) for v in ids_np[b, t + 1])
241
+ vals.append(len(a & c) / max(len(a | c), 1))
242
+ out["router_adjacent_jaccard"] = float(sum(vals) / max(len(vals), 1))
243
+ if lookahead_probs is not None and lookahead_steps > 0 and S > 1:
244
+ K = min(int(lookahead_steps), int(lookahead_probs.shape[2]) - 1)
245
+ pred = lookahead_probs.astype(mx.float32)
246
+ mx.eval(pred)
247
+ pred_np = np.array(pred)
248
+ recalls = []
249
+ union_recalls = []
250
+ pred_budget = max(top_k, min(E, top_k * max(1, K)))
251
+ for k in range(1, K + 1):
252
+ if S - k <= 0:
253
+ continue
254
+ pred_ids = np.argpartition(-pred_np[:, :-k, k, :], kth=top_k - 1, axis=-1)[:, :, :top_k]
255
+ for b in range(B):
256
+ for t in range(S - k):
257
+ target = set(int(v) for v in ids_np[b, t + k])
258
+ got = set(int(v) for v in pred_ids[b, t])
259
+ recalls.append(len(target & got) / float(top_k))
260
+ if K > 0 and S - K > 0:
261
+ pred_union_ids = np.argpartition(-pred_np[:, :S - K, 1:K + 1, :].max(axis=2),
262
+ kth=pred_budget - 1, axis=-1)[:, :, :pred_budget]
263
+ for b in range(B):
264
+ for t in range(S - K):
265
+ target = set()
266
+ for k in range(1, K + 1):
267
+ target.update(int(v) for v in ids_np[b, t + k])
268
+ got = set(int(v) for v in pred_union_ids[b, t])
269
+ union_recalls.append(len(target & got) / max(len(target), 1))
270
+ if recalls:
271
+ out["lookahead_topk_recall"] = float(sum(recalls) / len(recalls))
272
+ if union_recalls:
273
+ out["lookahead_union_recall"] = float(sum(union_recalls) / len(union_recalls))
274
+ return out
275
+
276
+
174
277
  def _router_kl_anchor(current: mx.array, reference: mx.array | None, lambda_anchor: float) -> mx.array:
175
278
  if reference is None or lambda_anchor <= 0.0:
176
279
  return mx.zeros((), dtype=current.dtype)
@@ -184,6 +287,7 @@ def _router_kl_anchor(current: mx.array, reference: mx.array | None, lambda_anch
184
287
 
185
288
 
186
289
  def _chronos_regularized_loss(model, base_loss: mx.array, lookahead_probs: mx.array | None, config, router_ref=None) -> mx.array:
290
+ setattr(model, "_last_lookahead_probs", lookahead_probs)
187
291
  loss = base_loss.astype(mx.float32)
188
292
  router_4d = _collect_router_probs(model)
189
293
  if router_4d is None:
@@ -202,6 +306,12 @@ def _chronos_regularized_loss(model, base_loss: mx.array, lookahead_probs: mx.ar
202
306
  lambda_temporal = float(getattr(config, "lambda_temporal", 0.0) or 0.0)
203
307
  if lambda_temporal > 0.0:
204
308
  loss = loss + lambda_temporal * _temporal_locality_loss(router_mean)
309
+ lambda_locality = float(getattr(config, "lambda_router_locality", 0.0) or 0.0)
310
+ if lambda_locality > 0.0:
311
+ loss = loss + lambda_locality * _router_locality_loss(
312
+ router_mean,
313
+ int(getattr(config, "num_experts_per_tok", 1) or 1),
314
+ )
205
315
 
206
316
  lookahead_steps = int(getattr(config, "lookahead_steps", 0) or 0)
207
317
  lambda_lookahead = float(getattr(config, "lambda_lookahead", 0.0) or 0.0)
@@ -220,6 +330,14 @@ def _chronos_regularized_loss(model, base_loss: mx.array, lookahead_probs: mx.ar
220
330
  lookahead_steps,
221
331
  int(getattr(config, "num_experts_per_tok", 1) or 1),
222
332
  )
333
+ lambda_union = float(getattr(config, "lambda_lookahead_union", 0.0) or 0.0)
334
+ if lambda_union > 0.0:
335
+ loss = loss + lambda_union * _lookahead_union_loss(
336
+ lookahead_probs,
337
+ teacher,
338
+ lookahead_steps,
339
+ int(getattr(config, "num_experts_per_tok", 1) or 1),
340
+ )
223
341
 
224
342
  loss = loss + _router_kl_anchor(
225
343
  router_mean,
@@ -824,6 +942,14 @@ def run_mlx_stage(
824
942
  tps = delta_steps / max(now - last_log_t, 1e-6)
825
943
  last_log_t = now
826
944
  last_log_step = steps
945
+ router_4d = _collect_router_probs(model)
946
+ router_mean = router_4d.mean(axis=2) if router_4d is not None else None
947
+ offload_metrics = _offload_training_metrics(
948
+ router_mean,
949
+ getattr(model, "_last_lookahead_probs", None),
950
+ int(getattr(config, "lookahead_steps", 0) or 0),
951
+ int(getattr(config, "num_experts_per_tok", 1) or 1),
952
+ )
827
953
  _emit({
828
954
  "event": "step",
829
955
  "stage": stage,
@@ -840,6 +966,7 @@ def run_mlx_stage(
840
966
  "dtype": dtype_name,
841
967
  "lr": float(optimizer.learning_rate.item()) if "learning_rate" in optimizer.state else base_lr * lr_scale,
842
968
  "rollbacks": rollback_count,
969
+ **offload_metrics,
843
970
  })
844
971
  if max_steps is not None and steps >= int(max_steps):
845
972
  break
@@ -48,10 +48,13 @@ CHECKPOINT_CONFIG_FIELDS = (
48
48
  "lambda_temporal",
49
49
  "lambda_lookahead",
50
50
  "lambda_lookahead_topk",
51
+ "lambda_lookahead_union",
52
+ "lambda_router_locality",
51
53
  "lambda_router_anchor",
52
54
  "fallback_mask_prob",
53
55
  "offload_miss_policy_default",
54
56
  "recommended_resident_experts",
57
+ "offload_working_set_experts",
55
58
  "vram_budget_gb",
56
59
  "pinned_memory_max_fraction",
57
60
  "storage_format",
@@ -19,7 +19,7 @@ class ChronosConfig(MiniMindConfig):
19
19
  self.lookahead_steps: int = kwargs.pop("lookahead_steps", 2)
20
20
  self.num_shared_experts: int = kwargs.pop("num_shared_experts", 1)
21
21
  self.lambda_balance: float = kwargs.pop("lambda_balance", 5e-4)
22
- self.lambda_temporal: float = kwargs.pop("lambda_temporal", 1e-3)
22
+ self.lambda_temporal: float = kwargs.pop("lambda_temporal", 3e-3)
23
23
  self.vram_budget_gb: float = kwargs.pop("vram_budget_gb", 4.0)
24
24
  self.prefetch_depth: int = kwargs.pop("prefetch_depth", 2)
25
25
  # Hybrid attention params
@@ -42,7 +42,9 @@ class ChronosConfig(MiniMindConfig):
42
42
  # Auxiliary hit-rate loss for the lookahead head. The soft CE above
43
43
  # learns distribution shape; this term directly rewards future top-k
44
44
  # expert-set recall, which is what offload prefetch needs.
45
- self.lambda_lookahead_topk: float = kwargs.pop("lambda_lookahead_topk", 0.05)
45
+ self.lambda_lookahead_topk: float = kwargs.pop("lambda_lookahead_topk", 0.15)
46
+ self.lambda_lookahead_union: float = kwargs.pop("lambda_lookahead_union", 0.05)
47
+ self.lambda_router_locality: float = kwargs.pop("lambda_router_locality", 0.02)
46
48
  # M4: weight on the router KL anchor (alignment stages only).
47
49
  # Prevents SFT/DPO/ORPO/GRPO gradients from drifting routing away
48
50
  # from the pretrained distribution that the cluster layout was
@@ -52,7 +54,7 @@ class ChronosConfig(MiniMindConfig):
52
54
  # Optional training-time miss simulation. When >0, MoE layers randomly
53
55
  # route selected experts through the shared fallback while still adding
54
56
  # the shared residual, matching the SSD/DRAM offload inference formula.
55
- self.fallback_mask_prob: float = kwargs.pop("fallback_mask_prob", 0.0)
57
+ self.fallback_mask_prob: float = kwargs.pop("fallback_mask_prob", 0.08)
56
58
  # Runtime offload policy. "on_demand" is the predictive low-latency
57
59
  # path: cold SSD misses are queued for RAM and covered by shared
58
60
  # fallback this token. "sync_on_demand" is the diagnostic path that
@@ -60,6 +62,7 @@ class ChronosConfig(MiniMindConfig):
60
62
  # "fallback_diagnostic" keeps the pure shared-fallback stress path.
61
63
  self.offload_miss_policy_default: str = kwargs.pop("offload_miss_policy_default", "on_demand")
62
64
  self.recommended_resident_experts: int | None = kwargs.pop("recommended_resident_experts", None)
65
+ self.offload_working_set_experts: int | None = kwargs.pop("offload_working_set_experts", None)
63
66
  # M7: regularization defaults. MiniMindConfig defaults dropout=0.0
64
67
  # and weight_decay isn't even a config field — both major causes
65
68
  # of overfitting on small corpora. Override here.