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,232 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/eval/io_profiler.py
|
|
3
|
+
|
|
4
|
+
Phase 1 validation tool: measures LookaheadRouter prediction accuracy
|
|
5
|
+
and temporal routing smoothness offline, without running full inference.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python -m chronos.eval.io_profiler \
|
|
9
|
+
--model_path /path/to/chronos_checkpoint \
|
|
10
|
+
--data_path /path/to/eval.jsonl \
|
|
11
|
+
--device cuda
|
|
12
|
+
|
|
13
|
+
Outputs:
|
|
14
|
+
- t+1 Top-1 accuracy (target: >= 85%)
|
|
15
|
+
- t+2 Top-1 accuracy (target: >= 75%)
|
|
16
|
+
- Mean L2 routing shift (lower = more cache-friendly)
|
|
17
|
+
- Cache hit rate estimate given VRAM budget
|
|
18
|
+
"""
|
|
19
|
+
import sys
|
|
20
|
+
import chronos.deps # ensure minimind on sys.path
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import torch
|
|
25
|
+
import torch.nn.functional as F
|
|
26
|
+
from torch.utils.data import DataLoader
|
|
27
|
+
from transformers import AutoTokenizer
|
|
28
|
+
|
|
29
|
+
from chronos.model.config import ChronosConfig
|
|
30
|
+
from chronos.model.model_chronos import ChronosForCausalLM
|
|
31
|
+
from chronos.model.moe_chronos import ChronosMOEFeedForward
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def collect_ground_truth_routing(model, input_ids, device):
|
|
35
|
+
"""
|
|
36
|
+
Run a full forward pass and collect per-layer, per-step top-1 expert indices.
|
|
37
|
+
Returns: [B, T, num_layers] int tensor of selected expert indices.
|
|
38
|
+
"""
|
|
39
|
+
model.eval()
|
|
40
|
+
input_ids = input_ids.to(device)
|
|
41
|
+
with torch.no_grad():
|
|
42
|
+
_, _ = model(input_ids, use_cache=False)
|
|
43
|
+
|
|
44
|
+
layers = [l for l in model.model.layers if isinstance(l.mlp, ChronosMOEFeedForward)]
|
|
45
|
+
if not layers:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
# last_router_probs: [B, S, E] per layer
|
|
49
|
+
probs_per_layer = [l.mlp.last_router_probs for l in layers] # list of [B, S, E]
|
|
50
|
+
stacked = torch.stack(probs_per_layer, dim=2) # [B, S, L, E]
|
|
51
|
+
top1 = stacked.argmax(dim=-1) # [B, S, L]
|
|
52
|
+
return top1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def evaluate_lookahead_accuracy(model, dataloader, device, lookahead_steps=2):
|
|
56
|
+
"""
|
|
57
|
+
For each sequence, compare LookaheadRouter's predicted expert at t+k
|
|
58
|
+
against the ground-truth expert selected at t+k.
|
|
59
|
+
|
|
60
|
+
Returns dict with accuracy per step and mean L2 routing shift.
|
|
61
|
+
"""
|
|
62
|
+
model.eval()
|
|
63
|
+
correct = {k: 0 for k in range(1, lookahead_steps + 1)}
|
|
64
|
+
total = 0
|
|
65
|
+
l2_shifts = []
|
|
66
|
+
|
|
67
|
+
for input_ids, _ in dataloader:
|
|
68
|
+
input_ids = input_ids.to(device)
|
|
69
|
+
B, T = input_ids.shape
|
|
70
|
+
if T < lookahead_steps + 2:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Ground truth routing for full sequence
|
|
74
|
+
gt_routing = collect_ground_truth_routing(model, input_ids, device)
|
|
75
|
+
if gt_routing is None:
|
|
76
|
+
continue
|
|
77
|
+
# gt_routing: [B, T, L] — use layer 0 as reference
|
|
78
|
+
gt_top1 = gt_routing[:, :, 0] # [B, T]
|
|
79
|
+
|
|
80
|
+
# LookaheadRouter predictions from block 0 hidden states
|
|
81
|
+
with torch.no_grad():
|
|
82
|
+
hidden_after_block0 = None
|
|
83
|
+
|
|
84
|
+
def _hook(module, inp, out):
|
|
85
|
+
nonlocal hidden_after_block0
|
|
86
|
+
hidden_after_block0 = out[0].detach()
|
|
87
|
+
|
|
88
|
+
handle = model.model.layers[0].register_forward_hook(_hook)
|
|
89
|
+
model(input_ids, use_cache=False)
|
|
90
|
+
handle.remove()
|
|
91
|
+
|
|
92
|
+
if hidden_after_block0 is None:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
lookahead_probs = model.model.lookahead_router(hidden_after_block0)
|
|
96
|
+
# lookahead_probs: [B, T, K+1, E], index 1..K = future predictions
|
|
97
|
+
|
|
98
|
+
for k in range(1, lookahead_steps + 1):
|
|
99
|
+
if T <= k:
|
|
100
|
+
continue
|
|
101
|
+
# Predicted expert at step t for future t+k
|
|
102
|
+
pred_top1 = lookahead_probs[:, :-k, k, :].argmax(dim=-1) # [B, T-k]
|
|
103
|
+
gt_at_tk = gt_top1[:, k:] # [B, T-k]
|
|
104
|
+
correct[k] += (pred_top1 == gt_at_tk).sum().item()
|
|
105
|
+
total_k = pred_top1.numel()
|
|
106
|
+
if k == 1:
|
|
107
|
+
total += total_k
|
|
108
|
+
|
|
109
|
+
# L2 routing shift between adjacent steps
|
|
110
|
+
# Use mean router probs across layers
|
|
111
|
+
from chronos.model.moe_chronos import ChronosMOEFeedForward
|
|
112
|
+
moe_layers = [l for l in model.model.layers if isinstance(l.mlp, ChronosMOEFeedForward)]
|
|
113
|
+
if moe_layers:
|
|
114
|
+
probs = torch.stack([l.mlp.last_router_probs for l in moe_layers], dim=2).mean(dim=2)
|
|
115
|
+
diff = probs[:, 1:, :] - probs[:, :-1, :]
|
|
116
|
+
l2_shifts.append((diff ** 2).sum(dim=-1).mean().item())
|
|
117
|
+
|
|
118
|
+
accuracies = {}
|
|
119
|
+
for k in range(1, lookahead_steps + 1):
|
|
120
|
+
denom = total if k == 1 else max(total, 1)
|
|
121
|
+
accuracies[f"top1_acc_t+{k}"] = correct[k] / max(denom, 1)
|
|
122
|
+
|
|
123
|
+
mean_l2 = sum(l2_shifts) / max(len(l2_shifts), 1)
|
|
124
|
+
return {**accuracies, "mean_l2_routing_shift": mean_l2}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def estimate_cache_hit_rate(model, dataloader, device, vram_budget_gb=4.0):
|
|
128
|
+
"""
|
|
129
|
+
Simulate a VRAM LRU cache of expert weights and estimate hit rate
|
|
130
|
+
given the LookaheadRouter's prefetch predictions.
|
|
131
|
+
"""
|
|
132
|
+
import collections
|
|
133
|
+
|
|
134
|
+
# Estimate single expert weight size in GB
|
|
135
|
+
moe_layers = [l for l in model.model.layers if isinstance(l.mlp, ChronosMOEFeedForward)]
|
|
136
|
+
if not moe_layers:
|
|
137
|
+
return {"cache_hit_rate": 1.0}
|
|
138
|
+
|
|
139
|
+
expert_params = sum(p.numel() for p in moe_layers[0].mlp.experts[0].parameters())
|
|
140
|
+
expert_size_gb = expert_params * 2 / (1024 ** 3) # fp16
|
|
141
|
+
num_experts = model.config.num_experts
|
|
142
|
+
cache_capacity = max(1, int(vram_budget_gb / expert_size_gb))
|
|
143
|
+
|
|
144
|
+
hits, total = 0, 0
|
|
145
|
+
for input_ids, _ in dataloader:
|
|
146
|
+
input_ids = input_ids.to(device)
|
|
147
|
+
gt_routing = collect_ground_truth_routing(model, input_ids, device)
|
|
148
|
+
if gt_routing is None:
|
|
149
|
+
continue
|
|
150
|
+
gt_top1 = gt_routing[:, :, 0] # [B, T]
|
|
151
|
+
|
|
152
|
+
lru = collections.OrderedDict()
|
|
153
|
+
for t in range(gt_top1.shape[1]):
|
|
154
|
+
for b in range(gt_top1.shape[0]):
|
|
155
|
+
eid = gt_top1[b, t].item()
|
|
156
|
+
if eid in lru:
|
|
157
|
+
lru.move_to_end(eid)
|
|
158
|
+
hits += 1
|
|
159
|
+
else:
|
|
160
|
+
if len(lru) >= cache_capacity:
|
|
161
|
+
lru.popitem(last=False)
|
|
162
|
+
lru[eid] = True
|
|
163
|
+
total += 1
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
"cache_hit_rate": hits / max(total, 1),
|
|
167
|
+
"cache_capacity_experts": cache_capacity,
|
|
168
|
+
"expert_size_gb": round(expert_size_gb, 4),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def main():
|
|
173
|
+
parser = argparse.ArgumentParser(description="Chronos Phase 1 Validation")
|
|
174
|
+
parser.add_argument("--model_path", type=str, default=None)
|
|
175
|
+
parser.add_argument("--data_path", type=str, required=True)
|
|
176
|
+
parser.add_argument("--device", type=str, default="cuda" if torch.cuda.is_available() else "cpu")
|
|
177
|
+
parser.add_argument("--batch_size", type=int, default=4)
|
|
178
|
+
parser.add_argument("--max_seq_len", type=int, default=256)
|
|
179
|
+
parser.add_argument("--vram_budget_gb", type=float, default=4.0)
|
|
180
|
+
parser.add_argument("--hidden_size", type=int, default=512)
|
|
181
|
+
parser.add_argument("--num_hidden_layers", type=int, default=8)
|
|
182
|
+
parser.add_argument("--num_experts", type=int, default=4)
|
|
183
|
+
args = parser.parse_args()
|
|
184
|
+
|
|
185
|
+
config = ChronosConfig(
|
|
186
|
+
hidden_size=args.hidden_size,
|
|
187
|
+
num_hidden_layers=args.num_hidden_layers,
|
|
188
|
+
num_experts=args.num_experts,
|
|
189
|
+
use_moe=True,
|
|
190
|
+
vram_budget_gb=args.vram_budget_gb,
|
|
191
|
+
)
|
|
192
|
+
model = ChronosForCausalLM(config).to(args.device)
|
|
193
|
+
|
|
194
|
+
if args.model_path:
|
|
195
|
+
weights = torch.load(args.model_path, map_location=args.device)
|
|
196
|
+
model.load_state_dict(weights, strict=False)
|
|
197
|
+
print(f"Loaded weights from {args.model_path}")
|
|
198
|
+
|
|
199
|
+
tokenizer = AutoTokenizer.from_pretrained(
|
|
200
|
+
chronos.deps.get_tokenizer_path()
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
from dataset.lm_dataset import PretrainDataset
|
|
204
|
+
dataset = PretrainDataset(args.data_path, tokenizer, max_length=args.max_seq_len)
|
|
205
|
+
loader = DataLoader(dataset, batch_size=args.batch_size, shuffle=False)
|
|
206
|
+
|
|
207
|
+
print("\n=== Phase 1 Validation: LookaheadRouter Accuracy ===")
|
|
208
|
+
acc_results = evaluate_lookahead_accuracy(model, loader, args.device)
|
|
209
|
+
for k, v in acc_results.items():
|
|
210
|
+
status = ""
|
|
211
|
+
if "t+1" in k:
|
|
212
|
+
status = "✓ PASS" if v >= 0.85 else "✗ FAIL (target: ≥85%)"
|
|
213
|
+
elif "t+2" in k:
|
|
214
|
+
status = "✓ PASS" if v >= 0.75 else "✗ FAIL (target: ≥75%)"
|
|
215
|
+
print(f" {k}: {v:.4f} {status}")
|
|
216
|
+
|
|
217
|
+
print("\n=== Cache Hit Rate Estimate ===")
|
|
218
|
+
cache_results = estimate_cache_hit_rate(model, loader, args.device, args.vram_budget_gb)
|
|
219
|
+
for k, v in cache_results.items():
|
|
220
|
+
print(f" {k}: {v}")
|
|
221
|
+
|
|
222
|
+
print("\n=== Summary ===")
|
|
223
|
+
t1 = acc_results.get("top1_acc_t+1", 0)
|
|
224
|
+
t2 = acc_results.get("top1_acc_t+2", 0)
|
|
225
|
+
phase1_pass = t1 >= 0.85 and t2 >= 0.75
|
|
226
|
+
print(f" Phase 1 Gate: {'PASS ✓' if phase1_pass else 'FAIL ✗'}")
|
|
227
|
+
print(f" t+1 acc={t1:.1%} t+2 acc={t2:.1%} "
|
|
228
|
+
f"L2 shift={acc_results['mean_l2_routing_shift']:.4f}")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
main()
|
chronos/io/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .expert_store import ExpertStore, LRUCache
|
|
2
|
+
from .async_prefetcher import AsyncPrefetcher, PrefetchScheduler
|
|
3
|
+
from .storage import ClusterStorage, ClusterManifest, MANIFEST_FILENAME
|
|
4
|
+
from .cluster_layout import (
|
|
5
|
+
collect_activation_log,
|
|
6
|
+
build_cooccurrence_matrix,
|
|
7
|
+
cluster_experts_greedy,
|
|
8
|
+
try_louvain_clustering,
|
|
9
|
+
build_cluster_layout,
|
|
10
|
+
repack_expert_weights,
|
|
11
|
+
repack_expert_weights_safetensors,
|
|
12
|
+
load_cluster_layout,
|
|
13
|
+
)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/io/async_prefetcher.py
|
|
3
|
+
|
|
4
|
+
Asynchronous expert weight prefetcher driven by LookaheadRouter predictions.
|
|
5
|
+
|
|
6
|
+
Architecture:
|
|
7
|
+
- Background daemon thread runs a prefetch loop
|
|
8
|
+
- Main thread submits prefetch requests via a queue
|
|
9
|
+
- Prefetcher loads SSD → RAM (pinned) ahead of time
|
|
10
|
+
- When main thread needs an expert, it's already warm in RAM → fast H2D
|
|
11
|
+
|
|
12
|
+
The key insight: LookaheadRouter predicts t+1/t+2 expert needs,
|
|
13
|
+
giving us 1-2 decode steps (~10-50ms) of lead time to hide SSD latency.
|
|
14
|
+
"""
|
|
15
|
+
import threading
|
|
16
|
+
import queue
|
|
17
|
+
import time
|
|
18
|
+
from typing import List, Optional, Callable
|
|
19
|
+
|
|
20
|
+
from chronos.io.expert_store import ExpertStore
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from chronos.runtime.metrics import safe_record as _metric
|
|
24
|
+
except Exception:
|
|
25
|
+
def _metric(name, value): # fallback
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AsyncPrefetcher:
|
|
30
|
+
"""
|
|
31
|
+
Background thread that prefetches expert weights from SSD → RAM
|
|
32
|
+
based on LookaheadRouter predictions.
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
prefetcher = AsyncPrefetcher(expert_store)
|
|
36
|
+
prefetcher.start()
|
|
37
|
+
|
|
38
|
+
# After each decode step, submit next predictions:
|
|
39
|
+
prefetcher.submit([expert_id_t1, expert_id_t2])
|
|
40
|
+
|
|
41
|
+
# Before using expert at step t:
|
|
42
|
+
prefetcher.wait_for(expert_id_t) # usually already done
|
|
43
|
+
|
|
44
|
+
prefetcher.stop()
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
expert_store: ExpertStore,
|
|
50
|
+
queue_depth: int = 8,
|
|
51
|
+
log_fn: Optional[Callable] = None,
|
|
52
|
+
):
|
|
53
|
+
self.store = expert_store
|
|
54
|
+
self.log_fn = log_fn or (lambda m: None)
|
|
55
|
+
self._request_queue: queue.Queue = queue.Queue(maxsize=queue_depth)
|
|
56
|
+
self._done_set: set = set()
|
|
57
|
+
self._done_lock = threading.Lock()
|
|
58
|
+
self._stop_event = threading.Event()
|
|
59
|
+
self._thread: Optional[threading.Thread] = None
|
|
60
|
+
self._stats = {"prefetch_hits": 0, "prefetch_misses": 0, "total_requests": 0}
|
|
61
|
+
|
|
62
|
+
def start(self):
|
|
63
|
+
self._stop_event.clear()
|
|
64
|
+
self._thread = threading.Thread(
|
|
65
|
+
target=self._prefetch_loop,
|
|
66
|
+
daemon=True,
|
|
67
|
+
name="chronos-prefetcher",
|
|
68
|
+
)
|
|
69
|
+
self._thread.start()
|
|
70
|
+
|
|
71
|
+
def stop(self):
|
|
72
|
+
self._stop_event.set()
|
|
73
|
+
# Unblock the queue
|
|
74
|
+
try:
|
|
75
|
+
self._request_queue.put_nowait(None)
|
|
76
|
+
except queue.Full:
|
|
77
|
+
pass
|
|
78
|
+
if self._thread:
|
|
79
|
+
self._thread.join(timeout=2.0)
|
|
80
|
+
|
|
81
|
+
def submit(self, expert_ids: List[int]):
|
|
82
|
+
"""
|
|
83
|
+
Submit a list of expert IDs to prefetch (non-blocking).
|
|
84
|
+
Called after each decode step with LookaheadRouter predictions.
|
|
85
|
+
"""
|
|
86
|
+
if not expert_ids:
|
|
87
|
+
return
|
|
88
|
+
try:
|
|
89
|
+
self._request_queue.put_nowait(expert_ids)
|
|
90
|
+
except queue.Full:
|
|
91
|
+
# Drop oldest request to make room
|
|
92
|
+
try:
|
|
93
|
+
self._request_queue.get_nowait()
|
|
94
|
+
self._request_queue.put_nowait(expert_ids)
|
|
95
|
+
except (queue.Empty, queue.Full):
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
def wait_for(self, expert_id: int, timeout: float = 0.05) -> bool:
|
|
99
|
+
"""
|
|
100
|
+
Wait until expert_id is in RAM (or VRAM). Returns True if ready.
|
|
101
|
+
In practice this should return immediately if prefetch worked.
|
|
102
|
+
"""
|
|
103
|
+
deadline = time.monotonic() + timeout
|
|
104
|
+
while time.monotonic() < deadline:
|
|
105
|
+
if (self.store.ram_lru.contains(expert_id) or
|
|
106
|
+
self.store.vram_lru.contains(expert_id)):
|
|
107
|
+
with self._done_lock:
|
|
108
|
+
self._stats["prefetch_hits"] += 1
|
|
109
|
+
return True
|
|
110
|
+
time.sleep(0.001)
|
|
111
|
+
with self._done_lock:
|
|
112
|
+
self._stats["prefetch_misses"] += 1
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
def _prefetch_loop(self):
|
|
116
|
+
while not self._stop_event.is_set():
|
|
117
|
+
try:
|
|
118
|
+
expert_ids = self._request_queue.get(timeout=0.01)
|
|
119
|
+
except queue.Empty:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
if expert_ids is None: # stop sentinel
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
with self._done_lock:
|
|
126
|
+
self._stats["total_requests"] += len(expert_ids)
|
|
127
|
+
|
|
128
|
+
# Filter out already-warm experts
|
|
129
|
+
to_fetch = [
|
|
130
|
+
eid for eid in expert_ids
|
|
131
|
+
if not self.store.ram_lru.contains(eid)
|
|
132
|
+
and not self.store.vram_lru.contains(eid)
|
|
133
|
+
]
|
|
134
|
+
if to_fetch:
|
|
135
|
+
try:
|
|
136
|
+
self.store.prefetch_to_ram(to_fetch)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
self.log_fn(f"[prefetcher] error fetching {to_fetch}: {e}")
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def stats(self) -> dict:
|
|
142
|
+
with self._done_lock:
|
|
143
|
+
s = dict(self._stats)
|
|
144
|
+
total = s["prefetch_hits"] + s["prefetch_misses"]
|
|
145
|
+
s["hit_rate"] = s["prefetch_hits"] / max(total, 1)
|
|
146
|
+
_metric("prefetch_hit_rate", s["hit_rate"])
|
|
147
|
+
return s
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class PrefetchScheduler:
|
|
151
|
+
"""
|
|
152
|
+
Integrates LookaheadRouter predictions with AsyncPrefetcher.
|
|
153
|
+
|
|
154
|
+
Called once per decode step to:
|
|
155
|
+
1. Extract predicted expert IDs from lookahead_probs
|
|
156
|
+
2. Submit them to the prefetcher
|
|
157
|
+
3. Promote the current-step expert from RAM → VRAM
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def __init__(self, prefetcher: AsyncPrefetcher, expert_store: ExpertStore):
|
|
161
|
+
self.prefetcher = prefetcher
|
|
162
|
+
self.store = expert_store
|
|
163
|
+
|
|
164
|
+
def step(
|
|
165
|
+
self,
|
|
166
|
+
lookahead_probs, # [B, S, K+1, E] from LookaheadRouter
|
|
167
|
+
current_step_expert_ids: List[int],
|
|
168
|
+
):
|
|
169
|
+
"""
|
|
170
|
+
Legacy single-call API: promote current experts (blocking), then
|
|
171
|
+
submit future predictions. Kept for backward compatibility.
|
|
172
|
+
Prefer `promote_current` + `prefetch_only` for the M3 pipeline.
|
|
173
|
+
"""
|
|
174
|
+
self.promote_current(current_step_expert_ids, blocking=True)
|
|
175
|
+
self.prefetch_only(lookahead_probs)
|
|
176
|
+
|
|
177
|
+
def promote_current(
|
|
178
|
+
self,
|
|
179
|
+
current_step_expert_ids: List[int],
|
|
180
|
+
blocking: bool = False,
|
|
181
|
+
) -> List[int]:
|
|
182
|
+
"""
|
|
183
|
+
Move the experts needed right now from RAM → VRAM.
|
|
184
|
+
|
|
185
|
+
With ``blocking=False`` (M3 default) the H2D copies are issued on
|
|
186
|
+
the dedicated stream and return immediately; the caller is
|
|
187
|
+
responsible for waiting via ``expert_store.wait_for_experts``.
|
|
188
|
+
Returns the list of experts that were actually newly promoted
|
|
189
|
+
(already-resident experts are skipped).
|
|
190
|
+
"""
|
|
191
|
+
newly = []
|
|
192
|
+
for eid in current_step_expert_ids:
|
|
193
|
+
if not self.store.vram_lru.contains(eid):
|
|
194
|
+
if self.store.promote_to_vram(eid, blocking=blocking):
|
|
195
|
+
newly.append(eid)
|
|
196
|
+
return newly
|
|
197
|
+
|
|
198
|
+
def prefetch_only(self, lookahead_probs) -> List[int]:
|
|
199
|
+
"""
|
|
200
|
+
Submit future expert predictions to the async prefetcher.
|
|
201
|
+
Does NOT touch VRAM. Safe to call *before* the forward pass so the
|
|
202
|
+
H2D pipeline can overlap with compute.
|
|
203
|
+
|
|
204
|
+
Returns the list of unique expert IDs submitted.
|
|
205
|
+
"""
|
|
206
|
+
if lookahead_probs is None:
|
|
207
|
+
return []
|
|
208
|
+
import torch
|
|
209
|
+
# lookahead_probs: [B, S, K+1, E] — take last token, steps 1..K.
|
|
210
|
+
# We submit t+1 first (highest priority), then t+2, etc., so the
|
|
211
|
+
# FIFO queue naturally orders them by recency.
|
|
212
|
+
last_token_probs = lookahead_probs[:, -1, 1:, :] # [B, K, E]
|
|
213
|
+
K = last_token_probs.shape[1]
|
|
214
|
+
all_submitted: List[int] = []
|
|
215
|
+
for k in range(K):
|
|
216
|
+
ids_k = last_token_probs[:, k, :].argmax(dim=-1).unique().cpu().tolist()
|
|
217
|
+
if ids_k:
|
|
218
|
+
self.prefetcher.submit(ids_k)
|
|
219
|
+
all_submitted.extend(ids_k)
|
|
220
|
+
return list(dict.fromkeys(all_submitted)) # preserve order, dedup
|