Project-Chronos 0.1.1__py3-none-any.whl

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 (83) hide show
  1. chronos/__init__.py +16 -0
  2. chronos/_backend_legacy.py +64 -0
  3. chronos/app.py +122 -0
  4. chronos/backend/__init__.py +40 -0
  5. chronos/backend/dispatcher.py +347 -0
  6. chronos/backend/ext/__init__.py +2 -0
  7. chronos/backend/ext/opencl.py +22 -0
  8. chronos/cli.py +62 -0
  9. chronos/data/__init__.py +0 -0
  10. chronos/data/flexible_dataset.py +309 -0
  11. chronos/deps.py +72 -0
  12. chronos/eval/__init__.py +2 -0
  13. chronos/eval/benchmark.py +159 -0
  14. chronos/eval/io_profiler.py +232 -0
  15. chronos/io/__init__.py +13 -0
  16. chronos/io/async_prefetcher.py +220 -0
  17. chronos/io/cluster_layout.py +317 -0
  18. chronos/io/expert_store.py +559 -0
  19. chronos/io/io_simulator.py +40 -0
  20. chronos/io/storage.py +237 -0
  21. chronos/mlx/__init__.py +32 -0
  22. chronos/mlx/attention.py +230 -0
  23. chronos/mlx/expert_store.py +155 -0
  24. chronos/mlx/inference.py +159 -0
  25. chronos/mlx/model.py +181 -0
  26. chronos/mlx/moe.py +102 -0
  27. chronos/model/__init__.py +6 -0
  28. chronos/model/config.py +54 -0
  29. chronos/model/hf_io.py +149 -0
  30. chronos/model/hybrid_attention.py +237 -0
  31. chronos/model/lookahead_router.py +55 -0
  32. chronos/model/model_chronos.py +271 -0
  33. chronos/model/moe_chronos.py +189 -0
  34. chronos/model/temporal_loss.py +124 -0
  35. chronos/router/__init__.py +12 -0
  36. chronos/router/expert_predictor.py +163 -0
  37. chronos/router/intent_classifier.py +193 -0
  38. chronos/router/prefill_scheduler.py +248 -0
  39. chronos/runtime/__init__.py +2 -0
  40. chronos/runtime/cache_manager.py +117 -0
  41. chronos/runtime/inference_engine.py +295 -0
  42. chronos/runtime/metrics.py +75 -0
  43. chronos/serving/__init__.py +13 -0
  44. chronos/serving/vllm_adapter.py +85 -0
  45. chronos/trainer/__init__.py +6 -0
  46. chronos/trainer/chronos_trainer.py +151 -0
  47. chronos/trainer/distill_trainer.py +193 -0
  48. chronos/trainer/dpo_trainer.py +190 -0
  49. chronos/trainer/grpo_trainer.py +288 -0
  50. chronos/trainer/loss_mixin.py +165 -0
  51. chronos/trainer/optim_utils.py +101 -0
  52. chronos/trainer/orpo_trainer.py +189 -0
  53. chronos/trainer/reward.py +130 -0
  54. chronos/trainer/sft_trainer.py +189 -0
  55. chronos/tuning/__init__.py +1 -0
  56. chronos/tuning/_base_tuner.py +284 -0
  57. chronos/tuning/chronos_auto_tuner.py +203 -0
  58. project_chronos-0.1.1.dist-info/METADATA +670 -0
  59. project_chronos-0.1.1.dist-info/RECORD +83 -0
  60. project_chronos-0.1.1.dist-info/WHEEL +5 -0
  61. project_chronos-0.1.1.dist-info/entry_points.txt +7 -0
  62. project_chronos-0.1.1.dist-info/licenses/LICENSE +177 -0
  63. project_chronos-0.1.1.dist-info/top_level.txt +8 -0
  64. train_chronos.py +133 -0
  65. train_chronos_distill.py +95 -0
  66. train_chronos_dpo.py +82 -0
  67. train_chronos_grpo.py +88 -0
  68. train_chronos_orpo.py +81 -0
  69. train_chronos_sft.py +85 -0
  70. ui/__init__.py +3 -0
  71. ui/estimator.py +204 -0
  72. ui/gradio_compat.py +72 -0
  73. ui/i18n.py +540 -0
  74. ui/presets.py +194 -0
  75. ui/tabs/__init__.py +3 -0
  76. ui/tabs/autotune_tab.py +210 -0
  77. ui/tabs/benchmark_tab.py +196 -0
  78. ui/tabs/config_tab.py +318 -0
  79. ui/tabs/data_tab.py +148 -0
  80. ui/tabs/inference_tab.py +496 -0
  81. ui/tabs/iomon_tab.py +37 -0
  82. ui/tabs/pipeline_tab.py +134 -0
  83. ui/tabs/train_tab.py +1688 -0
@@ -0,0 +1,248 @@
1
+ """
2
+ chronos/router/prefill_scheduler.py
3
+
4
+ Orchestrates the dual-layer pipeline:
5
+
6
+ Prefill phase:
7
+ 1. Run IntentClassifier on full prompt → IntentVector
8
+ 2. ExpertPredictor converts IntentVector → ExpertSet
9
+ 3. Batch-load all predicted experts to VRAM (or Metal) BEFORE
10
+ generation starts — so generation sees zero IO latency
11
+
12
+ Generation phase:
13
+ 4. Pass per-layer avail masks to model.forward()
14
+ 5. After generation, update ExpertPredictor frequency table
15
+
16
+ This is the key architectural difference from per-token LookaheadRouter:
17
+ IO is fully front-loaded into prefill, not spread across generation steps.
18
+
19
+ Usage (with IntentClassifier trained):
20
+ scheduler = PrefillScheduler(
21
+ classifier=intent_clf,
22
+ predictor=expert_pred,
23
+ expert_store=store, # ExpertStore or MLXExpertStore
24
+ )
25
+
26
+ # Called once before generation
27
+ expert_set = scheduler.prepare(prompt_ids, device)
28
+
29
+ # Get masks to pass to model.forward()
30
+ masks = scheduler.avail_masks(device)
31
+
32
+ # After generation
33
+ scheduler.record_activation(activated_expert_ids)
34
+
35
+ Usage (without IntentClassifier — fallback mode):
36
+ scheduler = PrefillScheduler(
37
+ classifier=None, # No classifier
38
+ predictor=expert_pred,
39
+ expert_store=store,
40
+ )
41
+ # Falls back to frequency-based heuristic selection
42
+ """
43
+ import time
44
+ import threading
45
+ from typing import List, Optional, Set, TYPE_CHECKING
46
+
47
+ import torch
48
+
49
+ if TYPE_CHECKING:
50
+ from chronos.router.expert_predictor import ExpertSet
51
+
52
+
53
+ class PrefillScheduler:
54
+ """
55
+ Dual-layer prefill: intent classify → expert predict → batch preload.
56
+
57
+ classifier: IntentClassifier or None (disables Layer 1, uses heuristic)
58
+ predictor: ExpertPredictor (always required)
59
+ expert_store: ExpertStore (PyTorch) or MLXExpertStore
60
+ async_load: if True, preloads in a background thread while the main
61
+ model is still running the LM head on the prompt tokens
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ classifier, # IntentClassifier | None
67
+ predictor, # ExpertPredictor
68
+ expert_store,
69
+ async_load: bool = True,
70
+ ):
71
+ self.classifier = classifier
72
+ self.predictor = predictor
73
+ self.store = expert_store
74
+ self.async_load = async_load
75
+
76
+ self._current_expert_set = None
77
+ self._load_thread: Optional[threading.Thread] = None
78
+ self._load_done = threading.Event()
79
+
80
+ # ── Public API ────────────────────────────────────────────────
81
+
82
+ def prepare(
83
+ self,
84
+ prompt_ids: torch.Tensor, # [1, S] token ids
85
+ device: str = "cpu",
86
+ classifier_device: str = "cpu",
87
+ ) -> "ExpertSet":
88
+ """
89
+ Run prefill scheduling. Call this BEFORE model.generate().
90
+
91
+ 1. Run IntentClassifier (or use heuristic if classifier=None)
92
+ 2. Determine ExpertSet
93
+ 3. Kick off background loading (or synchronous if async=False)
94
+
95
+ Returns ExpertSet immediately (loading may still be in progress
96
+ if async_load=True — call wait() before first forward pass).
97
+ """
98
+ t0 = time.monotonic()
99
+
100
+ if self.classifier is not None:
101
+ self.classifier.eval()
102
+ with torch.no_grad():
103
+ ids = prompt_ids.to(classifier_device)
104
+ intent = self.classifier(ids)
105
+ expert_set = self.predictor.predict(intent)
106
+ else:
107
+ # No classifier: build a synthetic IntentVector from frequency table
108
+ from chronos.router.intent_classifier import IntentVector
109
+ freq = self.predictor._frequency
110
+ per_layer = [freq.clone()
111
+ for _ in range(self.predictor.num_moe_layers)]
112
+ intent = IntentVector(per_layer_probs=per_layer, global_probs=freq)
113
+ expert_set = self.predictor.predict(intent)
114
+
115
+ self._current_expert_set = expert_set
116
+ elapsed_classify = (time.monotonic() - t0) * 1000
117
+
118
+ # Trigger expert loading
119
+ self._load_done.clear()
120
+ if self.async_load:
121
+ self._load_thread = threading.Thread(
122
+ target=self._load_experts,
123
+ args=(expert_set.expert_ids,),
124
+ daemon=True,
125
+ name="prefill-loader",
126
+ )
127
+ self._load_thread.start()
128
+ else:
129
+ self._load_experts(expert_set.expert_ids)
130
+
131
+ mode = "async" if self.async_load else "sync"
132
+ print(
133
+ f"[PrefillScheduler] classify={elapsed_classify:.1f}ms "
134
+ f"experts={sorted(expert_set.expert_ids)} "
135
+ f"confidence={expert_set.confidence:.2f} "
136
+ f"fallback={expert_set.fallback} load={mode}"
137
+ )
138
+ return expert_set
139
+
140
+ def wait(self):
141
+ """
142
+ Block until background expert loading is complete.
143
+ Call this just before the first generation forward pass.
144
+ """
145
+ self._load_done.wait()
146
+ # Sync H2D stream on CUDA backends
147
+ if hasattr(self.store, "sync_h2d"):
148
+ self.store.sync_h2d()
149
+
150
+ def avail_masks(self, device=None) -> List[torch.Tensor]:
151
+ """
152
+ Return per-layer float availability masks to pass to model.forward().
153
+ Returns all-ones masks if no expert set has been computed yet
154
+ (safe default: treat all experts as available).
155
+ """
156
+ if self._current_expert_set is None:
157
+ num_layers = self.predictor.num_moe_layers
158
+ num_experts = self.predictor.num_experts
159
+ return [torch.ones(num_experts).to(device or "cpu")
160
+ for _ in range(num_layers)]
161
+ return self.predictor.avail_masks_float(
162
+ self._current_expert_set, device=device
163
+ )
164
+
165
+ def record_activation(self, activated_expert_ids: List[int]):
166
+ """
167
+ Call after generation completes with the list of expert IDs
168
+ that were actually used. Updates the frequency heuristic.
169
+ """
170
+ self.predictor.update_frequency(activated_expert_ids)
171
+
172
+ # ── Internal ─────────────────────────────────────────────────
173
+
174
+ def _load_experts(self, expert_ids: Set[int]):
175
+ """
176
+ SSD → RAM → VRAM for the predicted expert set.
177
+ Runs in background thread when async_load=True.
178
+ """
179
+ try:
180
+ id_list = list(expert_ids)
181
+ # Step 1: SSD → pinned RAM (bulk)
182
+ self.store.prefetch_to_ram(id_list)
183
+ # Step 2: RAM → VRAM (promote each)
184
+ for eid in id_list:
185
+ self.store.promote_to_vram(eid)
186
+ finally:
187
+ self._load_done.set()
188
+
189
+ # ── Integration helpers ───────────────────────────────────────
190
+
191
+ @classmethod
192
+ def build(
193
+ cls,
194
+ main_model,
195
+ config,
196
+ expert_store,
197
+ classifier_path: Optional[str] = None,
198
+ device: str = "cpu",
199
+ async_load: bool = True,
200
+ ) -> "PrefillScheduler":
201
+ """
202
+ Factory: build a PrefillScheduler from a main model + config.
203
+ Loads IntentClassifier from disk if path provided; otherwise
204
+ creates an untrained one (will use heuristic fallback until trained).
205
+
206
+ classifier_path: path to .pt checkpoint, or None to skip classifier
207
+ """
208
+ from chronos.router.intent_classifier import IntentClassifier
209
+ from chronos.router.expert_predictor import ExpertPredictor
210
+
211
+ from chronos.model.moe_chronos import ChronosMOEFeedForward
212
+ num_moe_layers = sum(
213
+ 1 for l in main_model.model.layers
214
+ if isinstance(l.mlp, ChronosMOEFeedForward)
215
+ )
216
+
217
+ predictor = ExpertPredictor(
218
+ num_experts=config.num_experts,
219
+ num_moe_layers=num_moe_layers,
220
+ vram_capacity=expert_store.vram_capacity
221
+ if hasattr(expert_store, "vram_capacity") else config.num_experts,
222
+ )
223
+
224
+ classifier = None
225
+ if classifier_path is not None and __import__("os").path.exists(classifier_path):
226
+ classifier = IntentClassifier.load(
227
+ classifier_path,
228
+ vocab_size=config.vocab_size,
229
+ num_experts=config.num_experts,
230
+ num_moe_layers=num_moe_layers,
231
+ ).to(device)
232
+ print(f"[PrefillScheduler] Loaded IntentClassifier from {classifier_path}")
233
+ elif classifier_path is not None:
234
+ # Path provided but file doesn't exist yet → create untrained
235
+ classifier = IntentClassifier(
236
+ vocab_size=config.vocab_size,
237
+ num_experts=config.num_experts,
238
+ num_moe_layers=num_moe_layers,
239
+ ).to(device)
240
+ print("[PrefillScheduler] IntentClassifier not found — "
241
+ "using heuristic fallback (train with chronos train --mode intent)")
242
+
243
+ return cls(
244
+ classifier=classifier,
245
+ predictor=predictor,
246
+ expert_store=expert_store,
247
+ async_load=async_load,
248
+ )
@@ -0,0 +1,2 @@
1
+ from .cache_manager import CacheManager
2
+ from .inference_engine import ChronosInferenceEngine
@@ -0,0 +1,117 @@
1
+ """
2
+ chronos/runtime/cache_manager.py
3
+
4
+ Unified VRAM/RAM cache manager for Project Chronos inference.
5
+
6
+ Wraps ExpertStore + AsyncPrefetcher into a single interface used by
7
+ the inference engine. Provides:
8
+ - availability_mask() for soft gating
9
+ - step() to advance prefetch schedule after each decode step
10
+ - stats() for monitoring
11
+ """
12
+ from typing import List, Optional
13
+ import os
14
+ import torch
15
+
16
+ from chronos.io.expert_store import ExpertStore
17
+ from chronos.io.async_prefetcher import AsyncPrefetcher, PrefetchScheduler
18
+ from chronos.io.storage import ClusterStorage, MANIFEST_FILENAME
19
+
20
+
21
+ class CacheManager:
22
+ """
23
+ Single entry point for all expert caching operations during inference.
24
+
25
+ Lifecycle:
26
+ mgr = CacheManager(model, config, ssd_dir="./expert_cache")
27
+ mgr.start()
28
+ for each decode step:
29
+ mask = mgr.availability_mask() # pass to model.forward
30
+ outputs, lp = model(x, available_expert_masks=[mask]*L)
31
+ mgr.step(lp, current_expert_ids) # schedule next prefetch
32
+ mgr.stop()
33
+ """
34
+
35
+ def __init__(self, model, config, ssd_dir: str = "./expert_cache"):
36
+ self.config = config
37
+ self.expert_store = ExpertStore(model, config, ssd_dir=ssd_dir)
38
+ self.prefetcher = AsyncPrefetcher(self.expert_store)
39
+ self.scheduler = PrefetchScheduler(self.prefetcher, self.expert_store)
40
+ self._num_layers = len(self.expert_store.moe_layers)
41
+
42
+ # Auto-attach any cluster manifest present on disk. Also supports an
43
+ # explicit override via config.cluster_manifest_path pointing to a
44
+ # different layout (e.g. a post-calibration Louvain repack).
45
+ manifest_override = getattr(config, "cluster_manifest_path", None)
46
+ if manifest_override and ClusterStorage.has_manifest(manifest_override):
47
+ self.expert_store.attach_cluster_manifest(manifest_override)
48
+ elif ClusterStorage.has_manifest(ssd_dir):
49
+ self.expert_store.attach_cluster_manifest(ssd_dir)
50
+
51
+ def start(self):
52
+ """Start background prefetch thread."""
53
+ self.prefetcher.start()
54
+
55
+ def stop(self):
56
+ """Stop background prefetch thread."""
57
+ self.prefetcher.stop()
58
+
59
+ def warm_up(self, initial_expert_ids: Optional[List[int]] = None):
60
+ """
61
+ Pre-load a set of experts into VRAM before generation starts.
62
+ Defaults to experts 0..vram_capacity-1.
63
+ """
64
+ if initial_expert_ids is None:
65
+ initial_expert_ids = list(range(
66
+ min(self.config.num_experts, self.expert_store.vram_capacity)
67
+ ))
68
+ self.expert_store.prefetch_to_ram(initial_expert_ids)
69
+ for eid in initial_expert_ids:
70
+ self.expert_store.promote_to_vram(eid)
71
+ # Ensure all H2D copies are complete before model sees the weights
72
+ self.expert_store.sync_h2d()
73
+
74
+ def availability_mask(self) -> torch.Tensor:
75
+ """[num_experts] bool — True if expert is currently in VRAM."""
76
+ return self.expert_store.vram_availability_mask()
77
+
78
+ def availability_masks_all_layers(self) -> List[torch.Tensor]:
79
+ """Returns the same mask replicated for each MoE layer."""
80
+ mask = self.availability_mask()
81
+ return [mask] * self._num_layers
82
+
83
+ def step(self, lookahead_probs, current_expert_ids: List[int]):
84
+ """
85
+ Legacy single-call API (M1+M2 compatible): blocking promote of
86
+ current experts, then async submit of future predictions, then
87
+ full sync. Use the M3 split below to get pipeline overlap.
88
+ """
89
+ self.scheduler.step(lookahead_probs, current_expert_ids)
90
+ self.expert_store.sync_h2d()
91
+
92
+ # ── M3: split prefetch from blocking sync ────────────────────
93
+
94
+ def prefetch_for_next_step(self, lookahead_probs) -> List[int]:
95
+ """Fire off async prefetch for the LookaheadRouter's predictions.
96
+ Non-blocking. Should be called BEFORE the next forward so H2D and
97
+ compute overlap. Returns the submitted expert IDs."""
98
+ return self.scheduler.prefetch_only(lookahead_probs)
99
+
100
+ def ensure_resident(self, expert_ids: List[int]) -> List[int]:
101
+ """Promote `expert_ids` to VRAM (non-blocking H2D) and then have the
102
+ compute stream wait on *only those* events. Returns the list newly
103
+ promoted. After this call returns, the named experts are guaranteed
104
+ visible to the next forward without globally syncing other in-flight
105
+ H2D copies."""
106
+ newly = self.scheduler.promote_current(expert_ids, blocking=False)
107
+ # The compute stream waits on each newly-promoted expert's event;
108
+ # already-resident experts contribute no event, so this only blocks
109
+ # on the truly necessary transfers.
110
+ self.expert_store.wait_for_experts(newly)
111
+ return newly
112
+
113
+ def stats(self) -> dict:
114
+ return {
115
+ **self.expert_store.stats(),
116
+ **self.prefetcher.stats,
117
+ }
@@ -0,0 +1,295 @@
1
+ """
2
+ chronos/runtime/inference_engine.py
3
+
4
+ End-to-end Chronos inference engine with:
5
+ - Async prefetch driven by LookaheadRouter
6
+ - Soft gating fallback via CacheManager availability mask
7
+ - Token/s throughput measurement
8
+ - Optional streamer support
9
+
10
+ Usage:
11
+ engine = ChronosInferenceEngine(model, config, ssd_dir="./expert_cache")
12
+ engine.setup() # offload experts to SSD, warm up cache
13
+ output_ids = engine.generate(input_ids, max_new_tokens=200)
14
+ print(engine.last_stats)
15
+ """
16
+ import sys
17
+ import chronos.deps # ensure minimind on sys.path
18
+
19
+ import time
20
+ from typing import List, Optional
21
+
22
+ import torch
23
+ import torch.nn.functional as F
24
+
25
+ from chronos.model.config import ChronosConfig
26
+ from chronos.model.model_chronos import ChronosForCausalLM
27
+ from chronos.model.moe_chronos import ChronosMOEFeedForward
28
+ from chronos.runtime.cache_manager import CacheManager
29
+ from chronos.router.prefill_scheduler import PrefillScheduler
30
+
31
+
32
+ class ChronosInferenceEngine:
33
+ """
34
+ Wraps ChronosForCausalLM with the full async prefetch + soft gating pipeline.
35
+
36
+ The decode loop:
37
+ 1. Get availability mask from CacheManager
38
+ 2. Forward pass with mask → soft gating handles cache misses
39
+ 3. Extract LookaheadRouter probs from output
40
+ 4. Submit future expert predictions to prefetcher (non-blocking)
41
+ 5. Repeat
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ model: ChronosForCausalLM,
47
+ config: ChronosConfig,
48
+ ssd_dir: str = "./expert_cache",
49
+ ):
50
+ self.model = model
51
+ self.config = config
52
+ self.device = next(model.parameters()).device
53
+ self.cache_manager = CacheManager(model, config, ssd_dir=ssd_dir)
54
+ self.last_stats: dict = {}
55
+
56
+ def setup(self, warm_expert_ids: Optional[List[int]] = None):
57
+ """
58
+ One-time setup: serialize experts to SSD, start prefetcher, warm cache.
59
+ Call once before the first generate().
60
+ """
61
+ print("[Chronos] Offloading expert weights to SSD...")
62
+ self.cache_manager.expert_store.offload_all_to_ssd()
63
+ self.cache_manager.start()
64
+ print("[Chronos] Warming up VRAM cache...")
65
+ self.cache_manager.warm_up(warm_expert_ids)
66
+ print(f"[Chronos] Ready. Cache stats: {self.cache_manager.stats()}")
67
+
68
+ def setup_from_state_dict(
69
+ self,
70
+ state_dict: dict,
71
+ warm_expert_ids: Optional[List[int]] = None,
72
+ ):
73
+ """Setup path for real lazy expert loading on CPU/MPS.
74
+
75
+ The caller loads base/non-expert weights into the live model, then
76
+ hands the full checkpoint here so ExpertStore can shard only expert
77
+ tensors to SSD and replace live experts with placeholders.
78
+ """
79
+ print("[Chronos] Offloading expert weights to SSD from checkpoint...")
80
+ self.cache_manager.expert_store.offload_from_state_dict(state_dict)
81
+ self.cache_manager.start()
82
+ print("[Chronos] Warming up VRAM cache...")
83
+ self.cache_manager.warm_up(warm_expert_ids)
84
+ print(f"[Chronos] Ready. Cache stats: {self.cache_manager.stats()}")
85
+
86
+ def teardown(self):
87
+ self.cache_manager.stop()
88
+
89
+ def _get_current_expert_ids(self) -> List[int]:
90
+ """Extract top-1 expert IDs from the last forward pass (layer 0)."""
91
+ moe_layers = [
92
+ l.mlp for l in self.model.model.layers
93
+ if isinstance(l.mlp, ChronosMOEFeedForward)
94
+ ]
95
+ if not moe_layers or moe_layers[0].last_router_probs is None:
96
+ return []
97
+ probs = moe_layers[0].last_router_probs # [B, S, E]
98
+ return probs[:, -1, :].argmax(dim=-1).unique().cpu().tolist()
99
+
100
+ def _plan_prefill_experts(
101
+ self,
102
+ input_ids: torch.Tensor,
103
+ attention_mask: Optional[torch.Tensor] = None,
104
+ ) -> List[int]:
105
+ """Probe prompt routing with shared fallback, then load hot experts.
106
+
107
+ The live model contains placeholder experts after lazy setup. A cold
108
+ first prefill would therefore run entirely through shared fallback and
109
+ can diverge from the real model before decode starts. This probe pass
110
+ does not materialize expert modules; it only records gate choices, then
111
+ returns the most frequent expert IDs bounded by the active cache budget.
112
+ The caller reruns the real prefill after those experts are promoted.
113
+ """
114
+ if not getattr(self.config, "use_moe", True):
115
+ return []
116
+ num_experts = int(getattr(self.config, "num_experts", 0) or 0)
117
+ if num_experts <= 0:
118
+ return []
119
+
120
+ cold_mask = torch.zeros(num_experts, dtype=torch.bool, device=self.device)
121
+ masks = [cold_mask] * len(self.cache_manager.expert_store.moe_layers)
122
+ self.model(
123
+ input_ids,
124
+ attention_mask=attention_mask,
125
+ past_key_values=None,
126
+ use_cache=False,
127
+ available_expert_masks=masks,
128
+ )
129
+
130
+ counts = torch.zeros(num_experts, dtype=torch.long)
131
+ top_k = int(getattr(self.config, "num_experts_per_tok", 1) or 1)
132
+ top_k = max(1, min(top_k, num_experts))
133
+ for layer in self.model.model.layers:
134
+ moe = getattr(layer, "mlp", None)
135
+ if not isinstance(moe, ChronosMOEFeedForward) or moe.last_router_probs is None:
136
+ continue
137
+ probs = moe.last_router_probs.reshape(-1, num_experts)
138
+ expert_ids = torch.topk(probs, k=top_k, dim=-1).indices.reshape(-1).cpu()
139
+ counts += torch.bincount(expert_ids, minlength=num_experts)
140
+
141
+ if not torch.any(counts):
142
+ return []
143
+ capacity = max(1, min(
144
+ int(getattr(self.cache_manager.expert_store, "vram_capacity", 1) or 1),
145
+ num_experts,
146
+ ))
147
+ ordered = torch.argsort(counts, descending=True).tolist()
148
+ return [int(eid) for eid in ordered if int(counts[eid]) > 0][:capacity]
149
+
150
+ @torch.inference_mode()
151
+ def generate(
152
+ self,
153
+ input_ids: torch.Tensor,
154
+ attention_mask: Optional[torch.Tensor] = None,
155
+ max_new_tokens: int = 256,
156
+ temperature: float = 0.85,
157
+ top_p: float = 0.85,
158
+ top_k: int = 50,
159
+ eos_token_id: int = 2,
160
+ streamer=None,
161
+ do_sample: bool = True,
162
+ repetition_penalty: float = 1.0,
163
+ scheduler: Optional[PrefillScheduler] = None,
164
+ ) -> torch.Tensor:
165
+ input_ids = input_ids.to(self.device)
166
+ if attention_mask is not None:
167
+ attention_mask = attention_mask.to(self.device)
168
+
169
+ # ── Prefill phase: front-load expert IO before generation ──────────
170
+ if scheduler is not None:
171
+ scheduler.prepare(input_ids, device=str(self.device))
172
+ scheduler.wait() # block until SSD→VRAM load complete
173
+ prefill_expert_ids: List[int] = []
174
+ else:
175
+ prefill_expert_ids = self._plan_prefill_experts(input_ids, attention_mask)
176
+ if prefill_expert_ids:
177
+ self.cache_manager.ensure_resident(prefill_expert_ids)
178
+
179
+ past_key_values = None
180
+ finished = torch.zeros(input_ids.shape[0], dtype=torch.bool, device=self.device)
181
+ if streamer:
182
+ streamer.put(input_ids.cpu())
183
+
184
+ t0 = time.monotonic()
185
+ tokens_generated = 0
186
+ cache_hits, cache_misses = 0, 0
187
+ activated_expert_ids: List[int] = []
188
+ # M3: holds last step's lookahead predictions; we submit them as
189
+ # prefetch BEFORE this step's forward so the H2D stream overlaps
190
+ # with compute. None on the first iteration (no predictions yet).
191
+ prev_lookahead_probs = None
192
+ prev_current_ids: List[int] = []
193
+
194
+ for step in range(max_new_tokens):
195
+ # ── M3 pipeline: fire async prefetch BEFORE forward ──
196
+ if scheduler is None:
197
+ if prev_lookahead_probs is not None:
198
+ # Submit next-token predictions to the async SSD→RAM thread.
199
+ self.cache_manager.prefetch_for_next_step(prev_lookahead_probs)
200
+ if prev_current_ids:
201
+ # Ensure the experts we actually need NOW are resident in
202
+ # VRAM, waiting only on their own H2D events.
203
+ self.cache_manager.ensure_resident(prev_current_ids)
204
+
205
+ # Get availability masks AFTER ensure_resident so the mask
206
+ # reflects experts that just became available.
207
+ if scheduler is not None:
208
+ masks = scheduler.avail_masks(device=str(self.device))
209
+ else:
210
+ masks = self.cache_manager.availability_masks_all_layers()
211
+
212
+ past_len = past_key_values[0][0].shape[1] if past_key_values else 0
213
+ outputs, lookahead_probs = self.model(
214
+ input_ids[:, past_len:],
215
+ attention_mask=attention_mask,
216
+ past_key_values=past_key_values,
217
+ use_cache=True,
218
+ available_expert_masks=masks,
219
+ )
220
+
221
+ if attention_mask is not None:
222
+ attention_mask = torch.cat(
223
+ [attention_mask, attention_mask.new_ones(attention_mask.shape[0], 1)], -1
224
+ )
225
+
226
+ # Track cache hit/miss from mask
227
+ mask_tensor = masks[0] if masks else None
228
+ current_ids = self._get_current_expert_ids()
229
+ if mask_tensor is not None:
230
+ for eid in current_ids:
231
+ if eid < len(mask_tensor) and mask_tensor[eid]:
232
+ cache_hits += 1
233
+ else:
234
+ cache_misses += 1
235
+ activated_expert_ids.extend(current_ids)
236
+
237
+ # Stash predictions for the NEXT iteration's prefetch.
238
+ prev_lookahead_probs = lookahead_probs
239
+ prev_current_ids = current_ids
240
+
241
+ # Sampling
242
+ logits = outputs.logits[:, -1, :] / temperature
243
+ if repetition_penalty != 1.0:
244
+ for i in range(input_ids.shape[0]):
245
+ logits[i, torch.unique(input_ids[i])] /= repetition_penalty
246
+ if top_k > 0:
247
+ logits[logits < torch.topk(logits, top_k)[0][..., -1, None]] = -float('inf')
248
+ if top_p < 1.0:
249
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
250
+ mask_p = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1) > top_p
251
+ mask_p[..., 1:] = mask_p[..., :-1].clone()
252
+ mask_p[..., 0] = 0
253
+ logits[mask_p.scatter(1, sorted_indices, mask_p)] = -float('inf')
254
+
255
+ next_token = (
256
+ torch.multinomial(F.softmax(logits, dim=-1), num_samples=1)
257
+ if do_sample else torch.argmax(logits, dim=-1, keepdim=True)
258
+ )
259
+ if eos_token_id is not None:
260
+ next_token = torch.where(
261
+ finished.unsqueeze(-1),
262
+ next_token.new_full((next_token.shape[0], 1), eos_token_id),
263
+ next_token,
264
+ )
265
+
266
+ input_ids = torch.cat([input_ids, next_token], dim=-1)
267
+ past_key_values = outputs.past_key_values
268
+ tokens_generated += 1
269
+
270
+ if streamer:
271
+ streamer.put(next_token.cpu())
272
+
273
+ if eos_token_id is not None:
274
+ finished |= next_token.squeeze(-1).eq(eos_token_id)
275
+ if finished.all():
276
+ break
277
+
278
+ # ── Post-generation: update frequency heuristic ───────────────────
279
+ if scheduler is not None:
280
+ scheduler.record_activation(list(set(activated_expert_ids)))
281
+
282
+ elapsed = time.monotonic() - t0
283
+ total_cache = cache_hits + cache_misses
284
+ self.last_stats = {
285
+ "tokens_generated": tokens_generated,
286
+ "elapsed_s": round(elapsed, 3),
287
+ "tokens_per_sec": round(tokens_generated / max(elapsed, 1e-6), 2),
288
+ "cache_hit_rate": round(cache_hits / max(total_cache, 1), 4),
289
+ "prefill_expert_ids": prefill_expert_ids,
290
+ **self.cache_manager.stats(),
291
+ }
292
+
293
+ if streamer:
294
+ streamer.end()
295
+ return input_ids