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,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)