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.
- chronos/__init__.py +16 -0
- chronos/_backend_legacy.py +64 -0
- chronos/app.py +122 -0
- chronos/backend/__init__.py +40 -0
- chronos/backend/dispatcher.py +347 -0
- chronos/backend/ext/__init__.py +2 -0
- chronos/backend/ext/opencl.py +22 -0
- chronos/cli.py +62 -0
- chronos/data/__init__.py +0 -0
- chronos/data/flexible_dataset.py +309 -0
- chronos/deps.py +72 -0
- chronos/eval/__init__.py +2 -0
- chronos/eval/benchmark.py +159 -0
- chronos/eval/io_profiler.py +232 -0
- chronos/io/__init__.py +13 -0
- chronos/io/async_prefetcher.py +220 -0
- chronos/io/cluster_layout.py +317 -0
- chronos/io/expert_store.py +559 -0
- chronos/io/io_simulator.py +40 -0
- chronos/io/storage.py +237 -0
- chronos/mlx/__init__.py +32 -0
- chronos/mlx/attention.py +230 -0
- chronos/mlx/expert_store.py +155 -0
- chronos/mlx/inference.py +159 -0
- chronos/mlx/model.py +181 -0
- chronos/mlx/moe.py +102 -0
- chronos/model/__init__.py +6 -0
- chronos/model/config.py +54 -0
- chronos/model/hf_io.py +149 -0
- chronos/model/hybrid_attention.py +237 -0
- chronos/model/lookahead_router.py +55 -0
- chronos/model/model_chronos.py +271 -0
- chronos/model/moe_chronos.py +189 -0
- chronos/model/temporal_loss.py +124 -0
- chronos/router/__init__.py +12 -0
- chronos/router/expert_predictor.py +163 -0
- chronos/router/intent_classifier.py +193 -0
- chronos/router/prefill_scheduler.py +248 -0
- chronos/runtime/__init__.py +2 -0
- chronos/runtime/cache_manager.py +117 -0
- chronos/runtime/inference_engine.py +295 -0
- chronos/runtime/metrics.py +75 -0
- chronos/serving/__init__.py +13 -0
- chronos/serving/vllm_adapter.py +85 -0
- chronos/trainer/__init__.py +6 -0
- chronos/trainer/chronos_trainer.py +151 -0
- chronos/trainer/distill_trainer.py +193 -0
- chronos/trainer/dpo_trainer.py +190 -0
- chronos/trainer/grpo_trainer.py +288 -0
- chronos/trainer/loss_mixin.py +165 -0
- chronos/trainer/optim_utils.py +101 -0
- chronos/trainer/orpo_trainer.py +189 -0
- chronos/trainer/reward.py +130 -0
- chronos/trainer/sft_trainer.py +189 -0
- chronos/tuning/__init__.py +1 -0
- chronos/tuning/_base_tuner.py +284 -0
- chronos/tuning/chronos_auto_tuner.py +203 -0
- project_chronos-0.1.1.dist-info/METADATA +670 -0
- project_chronos-0.1.1.dist-info/RECORD +83 -0
- project_chronos-0.1.1.dist-info/WHEEL +5 -0
- project_chronos-0.1.1.dist-info/entry_points.txt +7 -0
- project_chronos-0.1.1.dist-info/licenses/LICENSE +177 -0
- project_chronos-0.1.1.dist-info/top_level.txt +8 -0
- train_chronos.py +133 -0
- train_chronos_distill.py +95 -0
- train_chronos_dpo.py +82 -0
- train_chronos_grpo.py +88 -0
- train_chronos_orpo.py +81 -0
- train_chronos_sft.py +85 -0
- ui/__init__.py +3 -0
- ui/estimator.py +204 -0
- ui/gradio_compat.py +72 -0
- ui/i18n.py +540 -0
- ui/presets.py +194 -0
- ui/tabs/__init__.py +3 -0
- ui/tabs/autotune_tab.py +210 -0
- ui/tabs/benchmark_tab.py +196 -0
- ui/tabs/config_tab.py +318 -0
- ui/tabs/data_tab.py +148 -0
- ui/tabs/inference_tab.py +496 -0
- ui/tabs/iomon_tab.py +37 -0
- ui/tabs/pipeline_tab.py +134 -0
- 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,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
|