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,559 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/io/expert_store.py
|
|
3
|
+
|
|
4
|
+
Three-tier expert weight storage: VRAM (hot) → RAM pinned (warm) → SSD (cold).
|
|
5
|
+
|
|
6
|
+
Responsibilities:
|
|
7
|
+
- Load expert weights from SSD into RAM pinned memory (async-friendly)
|
|
8
|
+
- Promote RAM-resident weights to VRAM on demand
|
|
9
|
+
- Evict VRAM weights via LRU when budget is exceeded
|
|
10
|
+
- Track per-expert residency state for soft-gating mask generation
|
|
11
|
+
"""
|
|
12
|
+
import sys
|
|
13
|
+
import chronos.deps # ensure minimind on sys.path
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import os
|
|
17
|
+
import threading
|
|
18
|
+
import collections
|
|
19
|
+
from typing import Dict, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
import torch
|
|
22
|
+
import torch.nn as nn
|
|
23
|
+
|
|
24
|
+
from chronos.io.storage import ClusterStorage, MANIFEST_FILENAME
|
|
25
|
+
from chronos.io.io_simulator import maybe_sleep as _maybe_sleep_ssd
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from chronos.runtime.metrics import safe_record as _metric
|
|
29
|
+
except Exception:
|
|
30
|
+
def _metric(name, value): # fallback
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LRUCache:
|
|
35
|
+
"""Thread-safe LRU cache tracking which expert IDs are in a tier."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, capacity: int):
|
|
38
|
+
self.capacity = capacity
|
|
39
|
+
self._cache: collections.OrderedDict = collections.OrderedDict()
|
|
40
|
+
self._lock = threading.Lock()
|
|
41
|
+
|
|
42
|
+
def contains(self, key: int) -> bool:
|
|
43
|
+
with self._lock:
|
|
44
|
+
return key in self._cache
|
|
45
|
+
|
|
46
|
+
def touch(self, key: int):
|
|
47
|
+
with self._lock:
|
|
48
|
+
if key in self._cache:
|
|
49
|
+
self._cache.move_to_end(key)
|
|
50
|
+
|
|
51
|
+
def put(self, key: int) -> Optional[int]:
|
|
52
|
+
"""Insert key; returns evicted key if capacity exceeded, else None."""
|
|
53
|
+
with self._lock:
|
|
54
|
+
if key in self._cache:
|
|
55
|
+
self._cache.move_to_end(key)
|
|
56
|
+
return None
|
|
57
|
+
evicted = None
|
|
58
|
+
if len(self._cache) >= self.capacity:
|
|
59
|
+
evicted, _ = self._cache.popitem(last=False)
|
|
60
|
+
self._cache[key] = True
|
|
61
|
+
return evicted
|
|
62
|
+
|
|
63
|
+
def remove(self, key: int):
|
|
64
|
+
with self._lock:
|
|
65
|
+
self._cache.pop(key, None)
|
|
66
|
+
|
|
67
|
+
def keys(self) -> List[int]:
|
|
68
|
+
with self._lock:
|
|
69
|
+
return list(self._cache.keys())
|
|
70
|
+
|
|
71
|
+
def __len__(self):
|
|
72
|
+
with self._lock:
|
|
73
|
+
return len(self._cache)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ExpertStore:
|
|
77
|
+
"""
|
|
78
|
+
Manages three-tier storage for MoE expert weights.
|
|
79
|
+
|
|
80
|
+
Tier 0 — VRAM: active experts as nn.Module parameters on GPU
|
|
81
|
+
Tier 1 — RAM: pinned-memory tensors ready for fast H2D transfer
|
|
82
|
+
Tier 2 — SSD: full expert weights saved as .pt files
|
|
83
|
+
|
|
84
|
+
Usage:
|
|
85
|
+
store = ExpertStore(model, config, ssd_dir="./expert_cache")
|
|
86
|
+
store.offload_all_to_ssd() # serialize experts to SSD
|
|
87
|
+
store.prefetch_to_ram([0, 2]) # load experts 0,2 into RAM
|
|
88
|
+
store.promote_to_vram(0) # move expert 0 RAM→VRAM
|
|
89
|
+
mask = store.vram_availability_mask() # [num_experts] bool tensor
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, model, config, ssd_dir: str = "./expert_cache"):
|
|
93
|
+
self.config = config
|
|
94
|
+
self.ssd_dir = ssd_dir
|
|
95
|
+
self.model = model
|
|
96
|
+
self.device = next(model.parameters()).device
|
|
97
|
+
os.makedirs(ssd_dir, exist_ok=True)
|
|
98
|
+
|
|
99
|
+
# Dedicated CUDA stream for H2D weight transfers.
|
|
100
|
+
# Runs concurrently with the default compute stream so weight copies
|
|
101
|
+
# never stall matrix multiplications on the current token.
|
|
102
|
+
if torch.cuda.is_available() and str(self.device).startswith("cuda"):
|
|
103
|
+
self._h2d_stream: Optional[torch.cuda.Stream] = torch.cuda.Stream(device=self.device)
|
|
104
|
+
else:
|
|
105
|
+
self._h2d_stream = None
|
|
106
|
+
|
|
107
|
+
# Collect all MoE layers and their expert modules
|
|
108
|
+
from chronos.model.moe_chronos import ChronosMOEFeedForward
|
|
109
|
+
self.moe_layers: List[ChronosMOEFeedForward] = [
|
|
110
|
+
layer.mlp for layer in model.model.layers
|
|
111
|
+
if isinstance(layer.mlp, ChronosMOEFeedForward)
|
|
112
|
+
]
|
|
113
|
+
self.num_experts = config.num_experts
|
|
114
|
+
self.num_layers = len(self.moe_layers)
|
|
115
|
+
|
|
116
|
+
# Estimate VRAM capacity in number of experts
|
|
117
|
+
expert_bytes = self._expert_size_bytes()
|
|
118
|
+
self._expert_bytes = expert_bytes
|
|
119
|
+
self._expert_dtype_cached = self._expert_dtype()
|
|
120
|
+
vram_bytes = config.vram_budget_gb * (1024 ** 3)
|
|
121
|
+
# Reserve 50% headroom for shared experts + attention KV cache
|
|
122
|
+
self.vram_capacity = max(1, int(vram_bytes * 0.5 / max(expert_bytes, 1)))
|
|
123
|
+
|
|
124
|
+
# Pinned RAM capacity: bounded by physical RAM safety limit
|
|
125
|
+
# Never exceed pinned_memory_max_fraction of total physical RAM
|
|
126
|
+
max_fraction = getattr(config, 'pinned_memory_max_fraction', 0.25)
|
|
127
|
+
physical_ram_bytes = self._physical_ram_bytes()
|
|
128
|
+
safe_pinned_bytes = physical_ram_bytes * max_fraction
|
|
129
|
+
# Each expert occupies expert_bytes * num_layers in RAM
|
|
130
|
+
per_expert_ram = max(expert_bytes * self.num_layers, 1)
|
|
131
|
+
ram_capacity_by_safety = max(1, int(safe_pinned_bytes / per_expert_ram))
|
|
132
|
+
# Also cap at 4x VRAM capacity (original heuristic)
|
|
133
|
+
self.ram_capacity = min(self.vram_capacity * 4, ram_capacity_by_safety)
|
|
134
|
+
|
|
135
|
+
self.vram_lru = LRUCache(capacity=self.vram_capacity)
|
|
136
|
+
self.ram_lru = LRUCache(capacity=self.ram_capacity)
|
|
137
|
+
|
|
138
|
+
# RAM pinned buffers: {expert_id: {layer_idx: tensor_dict}}
|
|
139
|
+
self._ram_buffers: Dict[int, Dict[int, Dict[str, torch.Tensor]]] = {}
|
|
140
|
+
self._ram_lock = threading.Lock()
|
|
141
|
+
self._offloaded = False
|
|
142
|
+
|
|
143
|
+
# M3: per-expert H2D events. Every promote_to_vram records an event
|
|
144
|
+
# on _h2d_stream and stores it here; wait_for_experts synchronizes
|
|
145
|
+
# only on the needed ones so the compute stream never blocks on
|
|
146
|
+
# unrelated transfers.
|
|
147
|
+
self._pending_events: Dict[int, "torch.cuda.Event"] = {}
|
|
148
|
+
self._events_lock = threading.Lock()
|
|
149
|
+
|
|
150
|
+
# Storage backend: "safetensors" (clustered) or "pt" (legacy, one file per expert)
|
|
151
|
+
self.storage_format = getattr(config, "storage_format", "safetensors")
|
|
152
|
+
self._cluster_storage: Optional[ClusterStorage] = None
|
|
153
|
+
if self.storage_format == "safetensors" and ClusterStorage.has_manifest(ssd_dir):
|
|
154
|
+
# Manifest already exists on disk — load it up front so prefetch
|
|
155
|
+
# can run cluster-aware from step 0.
|
|
156
|
+
self._cluster_storage = ClusterStorage(ssd_dir)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _physical_ram_bytes() -> int:
|
|
160
|
+
"""Query available (not total) RAM at call time for dynamic safety."""
|
|
161
|
+
try:
|
|
162
|
+
import psutil
|
|
163
|
+
vm = psutil.virtual_memory()
|
|
164
|
+
# Use available memory so we respect current system load
|
|
165
|
+
return vm.available
|
|
166
|
+
except ImportError:
|
|
167
|
+
return 8 * (1024 ** 3) # conservative 8 GB fallback
|
|
168
|
+
|
|
169
|
+
def _safe_ram_capacity(self) -> int:
|
|
170
|
+
"""Recompute RAM capacity based on current available memory."""
|
|
171
|
+
max_fraction = getattr(self.config, 'pinned_memory_max_fraction', 0.25)
|
|
172
|
+
available_bytes = self._physical_ram_bytes()
|
|
173
|
+
safe_bytes = available_bytes * max_fraction
|
|
174
|
+
per_expert_ram = max(self._expert_size_bytes() * self.num_layers, 1)
|
|
175
|
+
return max(1, min(self.vram_capacity * 4, int(safe_bytes / per_expert_ram)))
|
|
176
|
+
|
|
177
|
+
def _expert_size_bytes(self) -> int:
|
|
178
|
+
if not self.moe_layers:
|
|
179
|
+
return 0
|
|
180
|
+
expert = self.moe_layers[0].experts[0]
|
|
181
|
+
size = sum(p.numel() * 2 for p in expert.parameters()) # fp16
|
|
182
|
+
if size:
|
|
183
|
+
return size
|
|
184
|
+
cached = getattr(self, "_expert_bytes", 0)
|
|
185
|
+
if cached:
|
|
186
|
+
return cached
|
|
187
|
+
# Lazy placeholders deliberately have no parameters. Fall back to the
|
|
188
|
+
# Chronos FeedForward topology so cache capacities stay meaningful
|
|
189
|
+
# when experts are replaced before model.to(device).
|
|
190
|
+
hidden = int(getattr(self.config, "hidden_size", 0) or 0)
|
|
191
|
+
intermediate = int(getattr(self.config, "moe_intermediate_size", 0) or 0)
|
|
192
|
+
if hidden and intermediate:
|
|
193
|
+
return 3 * hidden * intermediate * 2 # gate/up/down, fp16 bytes
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
def _expert_dtype(self) -> torch.dtype:
|
|
197
|
+
if not self.moe_layers:
|
|
198
|
+
return torch.float16
|
|
199
|
+
expert = self.moe_layers[0].experts[0]
|
|
200
|
+
for p in expert.parameters():
|
|
201
|
+
return p.dtype
|
|
202
|
+
return getattr(self, "_expert_dtype_cached", torch.float16)
|
|
203
|
+
|
|
204
|
+
def _build_live_expert(self):
|
|
205
|
+
from model.model_minimind import FeedForward
|
|
206
|
+
return FeedForward(self.config, intermediate_size=self.config.moe_intermediate_size).to(self.device)
|
|
207
|
+
|
|
208
|
+
def _build_placeholder_expert(self):
|
|
209
|
+
from chronos.model.moe_chronos import LazyExpertPlaceholder
|
|
210
|
+
return LazyExpertPlaceholder(
|
|
211
|
+
self.config.hidden_size,
|
|
212
|
+
self.config.moe_intermediate_size,
|
|
213
|
+
self._expert_dtype(),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _replace_expert_module(self, layer_idx: int, expert_id: int, module: nn.Module):
|
|
217
|
+
self.moe_layers[layer_idx].experts[expert_id] = module
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _expert_state_from_checkpoint(
|
|
221
|
+
state_dict: Dict[str, torch.Tensor],
|
|
222
|
+
layer_idx: int,
|
|
223
|
+
expert_id: int,
|
|
224
|
+
) -> Dict[str, torch.Tensor]:
|
|
225
|
+
prefix = f"model.layers.{layer_idx}.mlp.experts.{expert_id}."
|
|
226
|
+
state = {}
|
|
227
|
+
for key, val in state_dict.items():
|
|
228
|
+
if key.startswith(prefix):
|
|
229
|
+
state[key[len(prefix):]] = val.detach().to(dtype=torch.float16, device="cpu").contiguous()
|
|
230
|
+
return state
|
|
231
|
+
|
|
232
|
+
def offload_from_state_dict(
|
|
233
|
+
self,
|
|
234
|
+
state_dict: Dict[str, torch.Tensor],
|
|
235
|
+
clusters: Optional[List[List[int]]] = None,
|
|
236
|
+
replace_live: bool = True,
|
|
237
|
+
):
|
|
238
|
+
"""Write expert weights to SSD directly from a full checkpoint.
|
|
239
|
+
|
|
240
|
+
This is the real lazy-load path for CPU/MPS inference: we avoid
|
|
241
|
+
materializing MoE expert weights into the live model before setup.
|
|
242
|
+
"""
|
|
243
|
+
if self.storage_format == "safetensors" and clusters is None:
|
|
244
|
+
# A raw .pth checkpoint has no co-activation cluster layout.
|
|
245
|
+
# Falling back to one .pt shard per (layer, expert) preserves
|
|
246
|
+
# true lazy loading; writing a single all-expert .ctsr cluster
|
|
247
|
+
# would make the first miss pull every expert into RAM.
|
|
248
|
+
self.storage_format = "pt"
|
|
249
|
+
self._cluster_storage = None
|
|
250
|
+
|
|
251
|
+
for li in range(self.num_layers):
|
|
252
|
+
for ei in range(self.num_experts):
|
|
253
|
+
path = self._ssd_path(ei, li)
|
|
254
|
+
if os.path.exists(path):
|
|
255
|
+
continue
|
|
256
|
+
state = self._expert_state_from_checkpoint(state_dict, li, ei)
|
|
257
|
+
if state:
|
|
258
|
+
torch.save(state, path)
|
|
259
|
+
|
|
260
|
+
if replace_live:
|
|
261
|
+
self.replace_live_experts_with_placeholders()
|
|
262
|
+
|
|
263
|
+
def replace_live_experts_with_placeholders(self):
|
|
264
|
+
"""Drop live expert modules from the active model after SSD offload."""
|
|
265
|
+
for li in range(self.num_layers):
|
|
266
|
+
for ei in range(self.num_experts):
|
|
267
|
+
self._replace_expert_module(li, ei, self._build_placeholder_expert())
|
|
268
|
+
self._offloaded = True
|
|
269
|
+
|
|
270
|
+
def _ssd_path(self, expert_id: int, layer_idx: int) -> str:
|
|
271
|
+
return os.path.join(self.ssd_dir, f"expert_l{layer_idx}_e{expert_id}.pt")
|
|
272
|
+
|
|
273
|
+
# ── SSD operations ────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
def offload_all_to_ssd(self, clusters: Optional[List[List[int]]] = None):
|
|
276
|
+
"""
|
|
277
|
+
Serialize all expert weights to SSD.
|
|
278
|
+
|
|
279
|
+
- storage_format == "safetensors" (default): writes one .ctsr file per
|
|
280
|
+
cluster + cluster_manifest.json. If `clusters` is None, falls back
|
|
281
|
+
to a single all-experts cluster (still mmap-friendly, just no
|
|
282
|
+
co-occurrence reordering benefit).
|
|
283
|
+
- storage_format == "pt" (legacy): one .pt pickle per (expert, layer).
|
|
284
|
+
"""
|
|
285
|
+
if self.storage_format == "safetensors":
|
|
286
|
+
self._offload_safetensors(clusters)
|
|
287
|
+
else:
|
|
288
|
+
self._offload_legacy_pt()
|
|
289
|
+
|
|
290
|
+
def _offload_legacy_pt(self):
|
|
291
|
+
for li, moe in enumerate(self.moe_layers):
|
|
292
|
+
for ei, expert in enumerate(moe.experts):
|
|
293
|
+
path = self._ssd_path(ei, li)
|
|
294
|
+
if not os.path.exists(path):
|
|
295
|
+
state = {k: v.half().cpu() for k, v in expert.state_dict().items()}
|
|
296
|
+
torch.save(state, path)
|
|
297
|
+
self.replace_live_experts_with_placeholders()
|
|
298
|
+
|
|
299
|
+
def _offload_safetensors(self, clusters: Optional[List[List[int]]]):
|
|
300
|
+
if clusters is None:
|
|
301
|
+
# Trivial single-cluster layout — better than per-expert pickles
|
|
302
|
+
# because we still get one mmap-friendly contiguous file.
|
|
303
|
+
clusters = [list(range(self.num_experts))]
|
|
304
|
+
ClusterStorage.write_clusters(
|
|
305
|
+
moe_layers=self.moe_layers,
|
|
306
|
+
clusters=clusters,
|
|
307
|
+
output_dir=self.ssd_dir,
|
|
308
|
+
num_layers=self.num_layers,
|
|
309
|
+
num_experts=self.num_experts,
|
|
310
|
+
dtype=torch.float16,
|
|
311
|
+
)
|
|
312
|
+
self._cluster_storage = ClusterStorage(self.ssd_dir)
|
|
313
|
+
self.replace_live_experts_with_placeholders()
|
|
314
|
+
|
|
315
|
+
def attach_cluster_manifest(self, manifest_dir: Optional[str] = None):
|
|
316
|
+
"""Load (or reload) a cluster_manifest.json. Called by CacheManager
|
|
317
|
+
after an offline repack, or to point ExpertStore at a different layout."""
|
|
318
|
+
target = manifest_dir or self.ssd_dir
|
|
319
|
+
if ClusterStorage.has_manifest(target):
|
|
320
|
+
self._cluster_storage = ClusterStorage(target)
|
|
321
|
+
self.storage_format = "safetensors"
|
|
322
|
+
|
|
323
|
+
def _load_from_ssd(self, expert_id: int) -> Dict[int, Dict[str, torch.Tensor]]:
|
|
324
|
+
"""Load expert weights for all layers from SSD into CPU tensors.
|
|
325
|
+
|
|
326
|
+
Legacy .pt path. The clustered safetensors path uses
|
|
327
|
+
`_load_cluster_into_ram` instead.
|
|
328
|
+
"""
|
|
329
|
+
layer_states: Dict[int, Dict[str, torch.Tensor]] = {}
|
|
330
|
+
for li in range(self.num_layers):
|
|
331
|
+
path = self._ssd_path(expert_id, li)
|
|
332
|
+
if os.path.exists(path):
|
|
333
|
+
layer_states[li] = torch.load(path, map_location="cpu")
|
|
334
|
+
return layer_states
|
|
335
|
+
|
|
336
|
+
# ── RAM operations ────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
def prefetch_to_ram(self, expert_ids: List[int]):
|
|
339
|
+
"""
|
|
340
|
+
Synchronously load expert weights from SSD into pinned RAM.
|
|
341
|
+
Call this from a background thread for async prefetch.
|
|
342
|
+
|
|
343
|
+
Cluster-aware path: when a safetensors manifest is attached, each
|
|
344
|
+
request expands to its full Louvain cluster so co-occurring experts
|
|
345
|
+
arrive in one sequential mmap — this is where we actually convert
|
|
346
|
+
random reads into sequential reads. Requests that land in the same
|
|
347
|
+
cluster are deduplicated.
|
|
348
|
+
"""
|
|
349
|
+
if not expert_ids:
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
if self._cluster_storage is not None:
|
|
353
|
+
self._prefetch_clustered(expert_ids)
|
|
354
|
+
else:
|
|
355
|
+
self._prefetch_legacy(expert_ids)
|
|
356
|
+
|
|
357
|
+
def _prefetch_clustered(self, expert_ids: List[int]):
|
|
358
|
+
"""Group requests by cluster, load each cluster at most once."""
|
|
359
|
+
cs = self._cluster_storage
|
|
360
|
+
seen_clusters: set = set()
|
|
361
|
+
clusters_to_load: List[int] = []
|
|
362
|
+
for eid in expert_ids:
|
|
363
|
+
if eid not in cs.manifest.expert_to_cluster:
|
|
364
|
+
continue
|
|
365
|
+
if self.ram_lru.contains(eid):
|
|
366
|
+
self.ram_lru.touch(eid)
|
|
367
|
+
continue
|
|
368
|
+
cid = cs.cluster_for_expert(eid)
|
|
369
|
+
if cid in seen_clusters:
|
|
370
|
+
continue
|
|
371
|
+
seen_clusters.add(cid)
|
|
372
|
+
clusters_to_load.append(cid)
|
|
373
|
+
|
|
374
|
+
for cid in clusters_to_load:
|
|
375
|
+
# Simulate SSD read latency when CHRONOS_SIM_SSD_MS is set
|
|
376
|
+
_maybe_sleep_ssd()
|
|
377
|
+
# One mmap read pulls the whole cluster — all experts, all layers.
|
|
378
|
+
nested = cs.load_cluster(cid, dtype=torch.float16)
|
|
379
|
+
# nested: {expert_id: {layer_id: {param_name: Tensor}}}
|
|
380
|
+
dynamic_cap = self._safe_ram_capacity()
|
|
381
|
+
self.ram_lru.capacity = dynamic_cap
|
|
382
|
+
for eid, layer_states in nested.items():
|
|
383
|
+
if self.ram_lru.contains(eid):
|
|
384
|
+
self.ram_lru.touch(eid)
|
|
385
|
+
continue
|
|
386
|
+
pinned = self._pin_layer_states(layer_states)
|
|
387
|
+
with self._ram_lock:
|
|
388
|
+
evicted = self.ram_lru.put(eid)
|
|
389
|
+
if evicted is not None:
|
|
390
|
+
self._ram_buffers.pop(evicted, None)
|
|
391
|
+
self._ram_buffers[eid] = pinned
|
|
392
|
+
|
|
393
|
+
def _prefetch_legacy(self, expert_ids: List[int]):
|
|
394
|
+
for eid in expert_ids:
|
|
395
|
+
if self.ram_lru.contains(eid):
|
|
396
|
+
self.ram_lru.touch(eid)
|
|
397
|
+
continue
|
|
398
|
+
# Simulate SSD read latency
|
|
399
|
+
_maybe_sleep_ssd()
|
|
400
|
+
dynamic_cap = self._safe_ram_capacity()
|
|
401
|
+
self.ram_lru.capacity = dynamic_cap
|
|
402
|
+
|
|
403
|
+
layer_states = self._load_from_ssd(eid)
|
|
404
|
+
if not layer_states:
|
|
405
|
+
continue
|
|
406
|
+
pinned = self._pin_layer_states(layer_states)
|
|
407
|
+
with self._ram_lock:
|
|
408
|
+
evicted = self.ram_lru.put(eid)
|
|
409
|
+
if evicted is not None:
|
|
410
|
+
self._ram_buffers.pop(evicted, None)
|
|
411
|
+
self._ram_buffers[eid] = pinned
|
|
412
|
+
|
|
413
|
+
@staticmethod
|
|
414
|
+
def _pin_layer_states(
|
|
415
|
+
layer_states: Dict[int, Dict[str, torch.Tensor]]
|
|
416
|
+
) -> Dict[int, Dict[str, torch.Tensor]]:
|
|
417
|
+
pinned: Dict[int, Dict[str, torch.Tensor]] = {}
|
|
418
|
+
for li, state in layer_states.items():
|
|
419
|
+
pinned[li] = {}
|
|
420
|
+
for k, v in state.items():
|
|
421
|
+
try:
|
|
422
|
+
pinned[li][k] = v.pin_memory() if not v.is_pinned() else v
|
|
423
|
+
except RuntimeError:
|
|
424
|
+
pinned[li][k] = v.cpu()
|
|
425
|
+
return pinned
|
|
426
|
+
|
|
427
|
+
# ── VRAM operations ───────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
def promote_to_vram(self, expert_id: int, blocking: bool = False) -> bool:
|
|
430
|
+
"""
|
|
431
|
+
Move expert from RAM → VRAM. Returns True if the H2D copy was issued.
|
|
432
|
+
|
|
433
|
+
blocking=False (default, M3 path): issues H2D on _h2d_stream, records
|
|
434
|
+
an event, and returns immediately. Caller must wait on the event (via
|
|
435
|
+
``wait_for_experts``) before the expert is used by compute.
|
|
436
|
+
|
|
437
|
+
blocking=True (legacy path): synchronizes _h2d_stream before return,
|
|
438
|
+
so the expert is guaranteed visible to the compute stream.
|
|
439
|
+
"""
|
|
440
|
+
if not self.ram_lru.contains(expert_id):
|
|
441
|
+
self.prefetch_to_ram([expert_id])
|
|
442
|
+
|
|
443
|
+
with self._ram_lock:
|
|
444
|
+
if expert_id not in self._ram_buffers:
|
|
445
|
+
return False
|
|
446
|
+
layer_states = self._ram_buffers[expert_id]
|
|
447
|
+
|
|
448
|
+
evicted = self.vram_lru.put(expert_id)
|
|
449
|
+
if evicted is not None:
|
|
450
|
+
self._evict_from_vram(evicted)
|
|
451
|
+
|
|
452
|
+
ctx = (
|
|
453
|
+
torch.cuda.stream(self._h2d_stream)
|
|
454
|
+
if self._h2d_stream is not None
|
|
455
|
+
else _null_context()
|
|
456
|
+
)
|
|
457
|
+
with ctx:
|
|
458
|
+
for li, state in layer_states.items():
|
|
459
|
+
expert = self.moe_layers[li].experts[expert_id]
|
|
460
|
+
if not isinstance(expert, nn.Module) or len(list(expert.parameters())) == 0:
|
|
461
|
+
expert = self._build_live_expert()
|
|
462
|
+
self._replace_expert_module(li, expert_id, expert)
|
|
463
|
+
expert.load_state_dict({k: v.to(self.device, non_blocking=True) for k, v in state.items()}, strict=False)
|
|
464
|
+
# Record an event on the H2D stream so consumers can wait on
|
|
465
|
+
# *just this expert* instead of the whole stream.
|
|
466
|
+
if self._h2d_stream is not None:
|
|
467
|
+
evt = torch.cuda.Event(blocking=False)
|
|
468
|
+
evt.record(self._h2d_stream)
|
|
469
|
+
with self._events_lock:
|
|
470
|
+
self._pending_events[expert_id] = evt
|
|
471
|
+
|
|
472
|
+
if blocking:
|
|
473
|
+
self.sync_h2d()
|
|
474
|
+
_metric("vram_experts", len(self.vram_lru))
|
|
475
|
+
_metric("ram_experts", len(self.ram_lru))
|
|
476
|
+
return True
|
|
477
|
+
|
|
478
|
+
def wait_for_experts(self, expert_ids: List[int]) -> None:
|
|
479
|
+
"""Block the current (compute) stream until the named experts' H2D
|
|
480
|
+
copies are done. No-op on CPU. Only syncs the relevant events; other
|
|
481
|
+
in-flight transfers keep running."""
|
|
482
|
+
if self._h2d_stream is None:
|
|
483
|
+
return
|
|
484
|
+
current = torch.cuda.current_stream(self.device)
|
|
485
|
+
with self._events_lock:
|
|
486
|
+
events = [self._pending_events.pop(eid, None) for eid in expert_ids]
|
|
487
|
+
for evt in events:
|
|
488
|
+
if evt is not None:
|
|
489
|
+
# Make the compute stream wait for this event rather than
|
|
490
|
+
# calling evt.synchronize() on the host, which would block
|
|
491
|
+
# the CPU too.
|
|
492
|
+
current.wait_event(evt)
|
|
493
|
+
|
|
494
|
+
def sync_h2d(self):
|
|
495
|
+
"""Wait for *all* pending H2D weight transfers to complete (legacy)."""
|
|
496
|
+
if self._h2d_stream is not None:
|
|
497
|
+
self._h2d_stream.synchronize()
|
|
498
|
+
with self._events_lock:
|
|
499
|
+
self._pending_events.clear()
|
|
500
|
+
|
|
501
|
+
def _evict_from_vram(self, expert_id: int):
|
|
502
|
+
"""Write VRAM expert back to RAM buffer before eviction."""
|
|
503
|
+
for li, moe in enumerate(self.moe_layers):
|
|
504
|
+
if expert_id < len(moe.experts):
|
|
505
|
+
state = {}
|
|
506
|
+
for k, v in moe.experts[expert_id].state_dict().items():
|
|
507
|
+
cpu_v = v.detach().to(dtype=torch.float16, device="cpu").contiguous()
|
|
508
|
+
try:
|
|
509
|
+
state[k] = cpu_v.pin_memory()
|
|
510
|
+
except RuntimeError:
|
|
511
|
+
# MPS/CPU-only torch builds may reject pinned storage.
|
|
512
|
+
# Keeping a normal CPU tensor is still correct; it only
|
|
513
|
+
# loses the CUDA non_blocking copy optimization.
|
|
514
|
+
state[k] = cpu_v
|
|
515
|
+
with self._ram_lock:
|
|
516
|
+
if expert_id not in self._ram_buffers:
|
|
517
|
+
self._ram_buffers[expert_id] = {}
|
|
518
|
+
self._ram_buffers[expert_id][li] = state
|
|
519
|
+
self._replace_expert_module(li, expert_id, self._build_placeholder_expert())
|
|
520
|
+
self.vram_lru.remove(expert_id)
|
|
521
|
+
|
|
522
|
+
# ── Mask generation ───────────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
def vram_availability_mask(self) -> torch.Tensor:
|
|
525
|
+
"""Returns [num_experts] bool tensor: True if expert is in VRAM."""
|
|
526
|
+
mask = torch.zeros(self.num_experts, dtype=torch.bool)
|
|
527
|
+
for eid in self.vram_lru.keys():
|
|
528
|
+
if eid < self.num_experts:
|
|
529
|
+
mask[eid] = True
|
|
530
|
+
return mask
|
|
531
|
+
|
|
532
|
+
def stats(self) -> dict:
|
|
533
|
+
available_ram_gb = self._physical_ram_bytes() / (1024 ** 3)
|
|
534
|
+
pinned_used_gb = (
|
|
535
|
+
len(self.ram_lru) * self._expert_size_bytes() * self.num_layers / (1024 ** 3)
|
|
536
|
+
)
|
|
537
|
+
dynamic_cap = self._safe_ram_capacity()
|
|
538
|
+
return {
|
|
539
|
+
"vram_experts": len(self.vram_lru),
|
|
540
|
+
"vram_capacity": self.vram_capacity,
|
|
541
|
+
"ram_experts": len(self.ram_lru),
|
|
542
|
+
"ram_capacity_dynamic": dynamic_cap,
|
|
543
|
+
"expert_size_kb": self._expert_size_bytes() // 1024,
|
|
544
|
+
"pinned_ram_used_gb": round(pinned_used_gb, 3),
|
|
545
|
+
"available_ram_gb": round(available_ram_gb, 1),
|
|
546
|
+
"pinned_ram_fraction": round(pinned_used_gb / max(available_ram_gb, 1), 4),
|
|
547
|
+
"h2d_stream": "dedicated" if self._h2d_stream is not None else "default",
|
|
548
|
+
"storage_format": self.storage_format,
|
|
549
|
+
"cluster_aware": self._cluster_storage is not None,
|
|
550
|
+
"num_clusters": (
|
|
551
|
+
len(self._cluster_storage.manifest.clusters)
|
|
552
|
+
if self._cluster_storage is not None else 0
|
|
553
|
+
),
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
@contextlib.contextmanager
|
|
558
|
+
def _null_context():
|
|
559
|
+
yield
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chronos/runtime/io_simulator.py
|
|
3
|
+
|
|
4
|
+
Optional SSD-latency injection for the M3 double-stream pipeline benchmark.
|
|
5
|
+
|
|
6
|
+
In production, expert weight loads block on real NVMe reads (10–50ms typical
|
|
7
|
+
for a clustered safetensors mmap on a consumer SSD). On a CPU smoke test the
|
|
8
|
+
load is essentially free, which makes the difference between sequential and
|
|
9
|
+
overlapped pipelines invisible.
|
|
10
|
+
|
|
11
|
+
Setting ``CHRONOS_SIM_SSD_MS=30`` causes ``ExpertStore.prefetch_to_ram`` to
|
|
12
|
+
sleep for 30ms per cluster load, simulating SSD-bound conditions so we can
|
|
13
|
+
verify on CPU that the new ``prefetch_for_next_step + ensure_resident`` path
|
|
14
|
+
actually overlaps with ``forward()``.
|
|
15
|
+
|
|
16
|
+
Defaults to 0 (no injection). The simulator is a no-op when unset, so it
|
|
17
|
+
incurs no overhead in normal use.
|
|
18
|
+
"""
|
|
19
|
+
import os
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def simulated_ssd_delay_ms() -> float:
|
|
24
|
+
"""Return the configured simulated SSD latency in milliseconds (0 if off)."""
|
|
25
|
+
raw = os.environ.get("CHRONOS_SIM_SSD_MS", "0")
|
|
26
|
+
try:
|
|
27
|
+
return max(0.0, float(raw))
|
|
28
|
+
except ValueError:
|
|
29
|
+
return 0.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def maybe_sleep(per_unit_ms: float = None) -> None:
|
|
33
|
+
"""Sleep for the configured SSD-simulation duration if non-zero.
|
|
34
|
+
|
|
35
|
+
`per_unit_ms` allows the caller to scale by units fetched (e.g. multiply
|
|
36
|
+
by number of clusters). When None, sleeps once for the configured value.
|
|
37
|
+
"""
|
|
38
|
+
delay = simulated_ssd_delay_ms() if per_unit_ms is None else per_unit_ms
|
|
39
|
+
if delay > 0:
|
|
40
|
+
time.sleep(delay / 1000.0)
|