moe-l2 0.2.0__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.
- moe_l2/__init__.py +28 -0
- moe_l2/cache.py +549 -0
- moe_l2/cli.py +230 -0
- moe_l2/data/domain_expert_map.json +22027 -0
- moe_l2/data/generate_expert_map.py +239 -0
- moe_l2/data/prototypes.py +114 -0
- moe_l2/gguf_reader.py +228 -0
- moe_l2/predictor.py +414 -0
- moe_l2/proxy.py +219 -0
- moe_l2/semantic_predictor.py +73 -0
- moe_l2-0.2.0.dist-info/LICENSE +21 -0
- moe_l2-0.2.0.dist-info/METADATA +192 -0
- moe_l2-0.2.0.dist-info/RECORD +16 -0
- moe_l2-0.2.0.dist-info/WHEEL +5 -0
- moe_l2-0.2.0.dist-info/entry_points.txt +2 -0
- moe_l2-0.2.0.dist-info/top_level.txt +1 -0
moe_l2/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""moe-l2: MoE inference L2 hot-cache scheduler."""
|
|
2
|
+
|
|
3
|
+
from .predictor import (
|
|
4
|
+
DOMAINS,
|
|
5
|
+
domain_to_expert_ids,
|
|
6
|
+
enable_semantic,
|
|
7
|
+
get_backbone_experts,
|
|
8
|
+
get_layer_specificity,
|
|
9
|
+
get_preload_set,
|
|
10
|
+
is_semantic_available,
|
|
11
|
+
load_mapping,
|
|
12
|
+
predict,
|
|
13
|
+
predict_hybrid,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.2.0"
|
|
17
|
+
__all__ = [
|
|
18
|
+
"DOMAINS",
|
|
19
|
+
"load_mapping",
|
|
20
|
+
"predict",
|
|
21
|
+
"predict_hybrid",
|
|
22
|
+
"domain_to_expert_ids",
|
|
23
|
+
"get_preload_set",
|
|
24
|
+
"get_backbone_experts",
|
|
25
|
+
"get_layer_specificity",
|
|
26
|
+
"enable_semantic",
|
|
27
|
+
"is_semantic_available",
|
|
28
|
+
]
|
moe_l2/cache.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""L2 hot-cache manager for moe-l2.
|
|
2
|
+
|
|
3
|
+
Manages a shared-memory pool of preloaded expert weights with:
|
|
4
|
+
- mmap-backed shared memory in /dev/shm/ for zero-copy access
|
|
5
|
+
- Per-layer LRU eviction with pinned expert support
|
|
6
|
+
- Asynchronous preloading from domain predictions
|
|
7
|
+
- Thread-safe operations (protected by RLock)
|
|
8
|
+
- Hit/miss statistics tracking
|
|
9
|
+
|
|
10
|
+
Architecture:
|
|
11
|
+
L2 (RAM) sits between L3 (SSD/GGUF file) and L0 (GPU pool).
|
|
12
|
+
Experts that are likely to be needed are preloaded into L2
|
|
13
|
+
based on domain predictions, so they can be memcpy'd into
|
|
14
|
+
GPU pool in ~1150 µs instead of ~6500 µs from SSD.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
cache = L2Cache()
|
|
18
|
+
cache.pin_experts(0, [41, 72, 89]) # backbone experts
|
|
19
|
+
|
|
20
|
+
# Preload after prediction
|
|
21
|
+
cache.preload_domain("codegen", expert_map)
|
|
22
|
+
|
|
23
|
+
# On each token's expert selection
|
|
24
|
+
hot = cache.request(layer=15, expert_id=42)
|
|
25
|
+
|
|
26
|
+
# Check stats
|
|
27
|
+
print(cache.stats())
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import threading
|
|
34
|
+
import time
|
|
35
|
+
from concurrent.futures import ThreadPoolExecutor, Future
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Optional
|
|
38
|
+
|
|
39
|
+
from .gguf_reader import MoEGGUFReader
|
|
40
|
+
|
|
41
|
+
# ── Defaults ──
|
|
42
|
+
DEFAULT_N_LAYERS = 40
|
|
43
|
+
DEFAULT_SLOTS_PER_LAYER = 96
|
|
44
|
+
DEFAULT_EXPERT_SIZE = 1_010_000 # ~1.01 MB (fallback when no model_path)
|
|
45
|
+
DEFAULT_L2_DIR = Path("/dev/shm/moe_l2")
|
|
46
|
+
DEFAULT_LOADER_WORKERS = 2
|
|
47
|
+
|
|
48
|
+
# Sentinels for slot tracking
|
|
49
|
+
_EMPTY = -1 # slot is free
|
|
50
|
+
_RESERVED = -2 # slot is being loaded (between evict and write)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class L2Cache:
|
|
54
|
+
"""Per-layer LRU cache of expert weights in POSIX shared memory.
|
|
55
|
+
|
|
56
|
+
Each slot is an mmap'd file under {l2_dir}/. Filename: L{layer}_S{slot}.
|
|
57
|
+
Content: raw float16 expert weights (expert_size bytes).
|
|
58
|
+
|
|
59
|
+
Design choices (from LRU simulation + 8-domain expert analysis):
|
|
60
|
+
- 96 slots/layer: 84.4% hit rate across 8 domains (near ceiling)
|
|
61
|
+
- Per-layer independent LRU: layer variances were only ~7pp
|
|
62
|
+
- Pinned backbone/domain experts: small benefit at 96-slots,
|
|
63
|
+
critical at smaller capacities
|
|
64
|
+
- Async loading: non-blocking L3→L2 during user thinking time
|
|
65
|
+
|
|
66
|
+
Thread safety: all public methods use RLock internally.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
n_layers: Optional[int] = None,
|
|
72
|
+
slots_per_layer: int = DEFAULT_SLOTS_PER_LAYER,
|
|
73
|
+
expert_size: Optional[int] = None,
|
|
74
|
+
l2_dir: Optional[Path] = None,
|
|
75
|
+
loader_workers: int = DEFAULT_LOADER_WORKERS,
|
|
76
|
+
expert_data_dir: Optional[Path] = None,
|
|
77
|
+
model_path: Optional[str | Path] = None,
|
|
78
|
+
):
|
|
79
|
+
# ── Auto-detect from GGUF model ──
|
|
80
|
+
self._gguf_reader: Optional[MoEGGUFReader] = None
|
|
81
|
+
if model_path is not None:
|
|
82
|
+
self._gguf_reader = MoEGGUFReader(model_path)
|
|
83
|
+
self.n_layers = n_layers or self._gguf_reader.num_layers
|
|
84
|
+
self.expert_size = (
|
|
85
|
+
expert_size or self._gguf_reader.per_expert_size()
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
self.n_layers = n_layers or DEFAULT_N_LAYERS
|
|
89
|
+
self.expert_size = expert_size or DEFAULT_EXPERT_SIZE
|
|
90
|
+
|
|
91
|
+
self.slots_per_layer = slots_per_layer
|
|
92
|
+
self.l2_dir = l2_dir or DEFAULT_L2_DIR
|
|
93
|
+
self.expert_data_dir = expert_data_dir
|
|
94
|
+
|
|
95
|
+
# Create shared memory directory
|
|
96
|
+
self.l2_dir.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
|
|
98
|
+
# ── Slot state ──
|
|
99
|
+
# _slots[layer][slot] -> expert_id or _EMPTY
|
|
100
|
+
# _lru_order[layer] -> list[expert_id]; most recent at end
|
|
101
|
+
# _pinned[layer] -> set[expert_id]; never evictable
|
|
102
|
+
# _domain_pinned[layer] -> set[expert_id]; pinned by active domain
|
|
103
|
+
self._slots: list[list[int]] = [
|
|
104
|
+
[_EMPTY] * slots_per_layer for _ in range(self.n_layers)
|
|
105
|
+
]
|
|
106
|
+
self._lru_order: list[list[int]] = [[] for _ in range(self.n_layers)]
|
|
107
|
+
self._pinned: list[set[int]] = [set() for _ in range(self.n_layers)]
|
|
108
|
+
self._domain_pinned: list[set[int]] = [set() for _ in range(self.n_layers)]
|
|
109
|
+
|
|
110
|
+
# ── Statistics ──
|
|
111
|
+
self._hits: int = 0
|
|
112
|
+
self._misses: int = 0
|
|
113
|
+
self._loads_completed: int = 0
|
|
114
|
+
|
|
115
|
+
# ── Threading ──
|
|
116
|
+
self._lock = threading.RLock()
|
|
117
|
+
self._loader = ThreadPoolExecutor(
|
|
118
|
+
max_workers=loader_workers,
|
|
119
|
+
thread_name_prefix="moe-l2-loader",
|
|
120
|
+
)
|
|
121
|
+
# Track in-flight loads: (layer, expert_id) -> Future
|
|
122
|
+
self._pending_loads: dict[tuple[int, int], Future] = {}
|
|
123
|
+
|
|
124
|
+
# ── Active domain tracking ──
|
|
125
|
+
self._active_domain: Optional[str] = None
|
|
126
|
+
|
|
127
|
+
# ═══════════════════════════════════════════════════════════════
|
|
128
|
+
# Core API
|
|
129
|
+
# ═══════════════════════════════════════════════════════════════
|
|
130
|
+
|
|
131
|
+
def request(self, layer: int, expert_id: int) -> bool:
|
|
132
|
+
"""Request an expert. Returns True if cached (hot), False if miss.
|
|
133
|
+
|
|
134
|
+
On hit: marks the expert as MRU (moved to LRU tail).
|
|
135
|
+
On miss: submits async L3→L2 load if not already pending.
|
|
136
|
+
"""
|
|
137
|
+
with self._lock:
|
|
138
|
+
if self._is_cached(layer, expert_id):
|
|
139
|
+
self._touch(layer, expert_id)
|
|
140
|
+
self._hits += 1
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
self._misses += 1
|
|
144
|
+
if (layer, expert_id) not in self._pending_loads:
|
|
145
|
+
future = self._loader.submit(
|
|
146
|
+
self._load_expert, layer, expert_id
|
|
147
|
+
)
|
|
148
|
+
self._pending_loads[(layer, expert_id)] = future
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
def request_batch(self, requests: list[tuple[int, int]]) -> dict:
|
|
152
|
+
"""Request multiple experts at once.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
requests: list of (layer, expert_id) pairs.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
dict with keys: hits, misses, hit_count, miss_count
|
|
159
|
+
"""
|
|
160
|
+
hits: list = []
|
|
161
|
+
misses: list = []
|
|
162
|
+
with self._lock:
|
|
163
|
+
for layer, expert_id in requests:
|
|
164
|
+
if self._is_cached(layer, expert_id):
|
|
165
|
+
self._touch(layer, expert_id)
|
|
166
|
+
self._hits += 1
|
|
167
|
+
hits.append((layer, expert_id))
|
|
168
|
+
else:
|
|
169
|
+
self._misses += 1
|
|
170
|
+
if (layer, expert_id) not in self._pending_loads:
|
|
171
|
+
future = self._loader.submit(
|
|
172
|
+
self._load_expert, layer, expert_id
|
|
173
|
+
)
|
|
174
|
+
self._pending_loads[(layer, expert_id)] = future
|
|
175
|
+
misses.append((layer, expert_id))
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"hits": hits,
|
|
179
|
+
"misses": misses,
|
|
180
|
+
"hit_count": len(hits),
|
|
181
|
+
"miss_count": len(misses),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def preload_domain(
|
|
185
|
+
self, domain: str, expert_map: dict
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Preload all experts for a predicted domain.
|
|
188
|
+
|
|
189
|
+
Loads the per-layer top expert IDs from the domain→expert mapping.
|
|
190
|
+
Domain-preferred experts are pinned while this domain is active.
|
|
191
|
+
Old domain pins are cleared first.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
domain: Domain label (e.g. "codegen", "math").
|
|
195
|
+
expert_map: Domain→expert mapping dict (from predictor._get_map()).
|
|
196
|
+
Structure: {"domains": {domain: {"per_layer_top": {layer: [ids]}}}}
|
|
197
|
+
"""
|
|
198
|
+
domain_data = expert_map.get("domains", {}).get(domain, {})
|
|
199
|
+
per_layer_top = domain_data.get("per_layer_top", {})
|
|
200
|
+
if not per_layer_top:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
with self._lock:
|
|
204
|
+
# Clear previous domain pins
|
|
205
|
+
for layer_idx in range(self.n_layers):
|
|
206
|
+
self._domain_pinned[layer_idx].clear()
|
|
207
|
+
self._active_domain = domain
|
|
208
|
+
|
|
209
|
+
# Collect loads and pin
|
|
210
|
+
to_load: list[tuple[int, int]] = []
|
|
211
|
+
for layer_str, expert_ids in per_layer_top.items():
|
|
212
|
+
layer = int(layer_str)
|
|
213
|
+
if not (0 <= layer < self.n_layers):
|
|
214
|
+
continue
|
|
215
|
+
for eid in expert_ids:
|
|
216
|
+
if not self._is_cached(layer, eid):
|
|
217
|
+
to_load.append((layer, eid))
|
|
218
|
+
# All domain-preferred experts get domain-pinned
|
|
219
|
+
self._domain_pinned[layer].add(eid)
|
|
220
|
+
|
|
221
|
+
# Submit async loads
|
|
222
|
+
for layer, eid in to_load:
|
|
223
|
+
if (layer, eid) not in self._pending_loads:
|
|
224
|
+
f = self._loader.submit(self._load_expert, layer, eid)
|
|
225
|
+
self._pending_loads[(layer, eid)] = f
|
|
226
|
+
|
|
227
|
+
def preload_batch(self, pairs: list[tuple[int, int]]) -> None:
|
|
228
|
+
"""Preload a specific list of (layer, expert_id) pairs."""
|
|
229
|
+
with self._lock:
|
|
230
|
+
for layer, expert_id in pairs:
|
|
231
|
+
if not self._is_cached(layer, expert_id) \
|
|
232
|
+
and (layer, expert_id) not in self._pending_loads:
|
|
233
|
+
f = self._loader.submit(self._load_expert, layer, expert_id)
|
|
234
|
+
self._pending_loads[(layer, expert_id)] = f
|
|
235
|
+
|
|
236
|
+
def contains(self, layer: int, expert_id: int) -> bool:
|
|
237
|
+
"""Check if an expert is already in L2 cache."""
|
|
238
|
+
with self._lock:
|
|
239
|
+
return self._is_cached(layer, expert_id)
|
|
240
|
+
|
|
241
|
+
def pin_experts(self, layer: int, expert_ids: list[int]) -> None:
|
|
242
|
+
"""Permanently pin backbone/universal experts (never evicted).
|
|
243
|
+
|
|
244
|
+
These are experts activated across ALL domains — they should
|
|
245
|
+
never be evicted from cache.
|
|
246
|
+
"""
|
|
247
|
+
with self._lock:
|
|
248
|
+
self._pinned[layer].update(expert_ids)
|
|
249
|
+
|
|
250
|
+
def unpin_experts(self, layer: int, expert_ids: list[int]) -> None:
|
|
251
|
+
"""Remove permanent pin status from experts."""
|
|
252
|
+
with self._lock:
|
|
253
|
+
self._pinned[layer].difference_update(expert_ids)
|
|
254
|
+
|
|
255
|
+
def clear_domain_pins(self) -> None:
|
|
256
|
+
"""Remove all domain-level pins (called on domain switch)."""
|
|
257
|
+
with self._lock:
|
|
258
|
+
for layer in range(self.n_layers):
|
|
259
|
+
self._domain_pinned[layer].clear()
|
|
260
|
+
self._active_domain = None
|
|
261
|
+
|
|
262
|
+
def wait_for_pending(self, timeout: float = 30.0) -> int:
|
|
263
|
+
"""Wait for all pending expert loads to complete.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
timeout: Max seconds to wait.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Number of completed loads.
|
|
270
|
+
"""
|
|
271
|
+
with self._lock:
|
|
272
|
+
pending = list(self._pending_loads.values())
|
|
273
|
+
|
|
274
|
+
done = 0
|
|
275
|
+
for f in pending:
|
|
276
|
+
try:
|
|
277
|
+
f.result(timeout=timeout)
|
|
278
|
+
done += 1
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
return done
|
|
282
|
+
|
|
283
|
+
def get_slot_path(self, layer: int, slot: int) -> Optional[Path]:
|
|
284
|
+
"""Get the shared memory file path for a slot.
|
|
285
|
+
|
|
286
|
+
Returns None if invalid layer/slot indices.
|
|
287
|
+
"""
|
|
288
|
+
with self._lock:
|
|
289
|
+
if 0 <= layer < self.n_layers and 0 <= slot < self.slots_per_layer:
|
|
290
|
+
return self.l2_dir / f"L{layer}_S{slot}"
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
def get_expert_slot(self, layer: int, expert_id: int) -> Optional[int]:
|
|
294
|
+
"""Get the slot index for a cached expert, or None if not cached."""
|
|
295
|
+
with self._lock:
|
|
296
|
+
for slot, eid in enumerate(self._slots[layer]):
|
|
297
|
+
if eid == expert_id:
|
|
298
|
+
return slot
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
def stats(self) -> dict:
|
|
302
|
+
"""Return cache statistics.
|
|
303
|
+
|
|
304
|
+
Includes: hits, misses, hit rate, utilization, memory usage,
|
|
305
|
+
pending loads, active domain, and per-layer breakdown.
|
|
306
|
+
"""
|
|
307
|
+
with self._lock:
|
|
308
|
+
total = self._hits + self._misses
|
|
309
|
+
hit_rate = round(self._hits / total * 100, 1) if total > 0 else 0.0
|
|
310
|
+
|
|
311
|
+
total_slots = self.n_layers * self.slots_per_layer
|
|
312
|
+
used_slots = sum(
|
|
313
|
+
1 for ls in self._slots for s in ls if s != _EMPTY
|
|
314
|
+
)
|
|
315
|
+
pinned_count = sum(
|
|
316
|
+
len(self._pinned[l] | self._domain_pinned[l])
|
|
317
|
+
for l in range(self.n_layers)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Per-layer breakdown (compact)
|
|
321
|
+
layer_stats = []
|
|
322
|
+
for layer in range(self.n_layers):
|
|
323
|
+
used = sum(1 for s in self._slots[layer] if s != _EMPTY)
|
|
324
|
+
pinned = len(
|
|
325
|
+
self._pinned[layer] | self._domain_pinned[layer]
|
|
326
|
+
)
|
|
327
|
+
layer_stats.append(
|
|
328
|
+
f" L{layer:2d}: {used:3d}/{self.slots_per_layer:3d} slots "
|
|
329
|
+
f"({pinned:3d} pinned)"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
"hits": self._hits,
|
|
334
|
+
"misses": self._misses,
|
|
335
|
+
"total_requests": total,
|
|
336
|
+
"hit_rate_pct": hit_rate,
|
|
337
|
+
"total_slots": total_slots,
|
|
338
|
+
"used_slots": used_slots,
|
|
339
|
+
"pinned_experts": pinned_count,
|
|
340
|
+
"pending_loads": len(self._pending_loads),
|
|
341
|
+
"loads_completed": self._loads_completed,
|
|
342
|
+
"utilization_pct": (
|
|
343
|
+
round(used_slots / total_slots * 100, 1)
|
|
344
|
+
if total_slots else 0.0
|
|
345
|
+
),
|
|
346
|
+
"memory_usage_mb": round(
|
|
347
|
+
used_slots * self.expert_size / (1024 * 1024), 1
|
|
348
|
+
),
|
|
349
|
+
"active_domain": self._active_domain,
|
|
350
|
+
"per_layer_lines": layer_stats,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
def close(self) -> None:
|
|
354
|
+
"""Clean up all shared memory regions and thread pool."""
|
|
355
|
+
self._loader.shutdown(wait=False)
|
|
356
|
+
|
|
357
|
+
# Clean up shm files
|
|
358
|
+
for layer in range(self.n_layers):
|
|
359
|
+
for slot in range(self.slots_per_layer):
|
|
360
|
+
path = self._get_shm_path(layer, slot)
|
|
361
|
+
if path.exists():
|
|
362
|
+
path.unlink()
|
|
363
|
+
|
|
364
|
+
# Remove directory if empty
|
|
365
|
+
try:
|
|
366
|
+
self.l2_dir.rmdir()
|
|
367
|
+
except OSError:
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
def __enter__(self):
|
|
371
|
+
return self
|
|
372
|
+
|
|
373
|
+
def __exit__(self, *args):
|
|
374
|
+
self.close()
|
|
375
|
+
|
|
376
|
+
# ═══════════════════════════════════════════════════════════════
|
|
377
|
+
# Internal helpers
|
|
378
|
+
# ═══════════════════════════════════════════════════════════════
|
|
379
|
+
|
|
380
|
+
def _get_shm_path(self, layer: int, slot: int) -> Path:
|
|
381
|
+
"""Absolute path to a slot's shared memory file."""
|
|
382
|
+
return self.l2_dir / f"L{layer}_S{slot}"
|
|
383
|
+
|
|
384
|
+
def _is_cached(self, layer: int, expert_id: int) -> bool:
|
|
385
|
+
"""Check if expert weights are in shared memory.
|
|
386
|
+
|
|
387
|
+
NOTE: caller MUST hold self._lock.
|
|
388
|
+
"""
|
|
389
|
+
if not (0 <= layer < self.n_layers):
|
|
390
|
+
return False
|
|
391
|
+
return expert_id in self._slots[layer]
|
|
392
|
+
|
|
393
|
+
def _touch(self, layer: int, expert_id: int) -> None:
|
|
394
|
+
"""Mark expert as MRU — move to end of LRU list.
|
|
395
|
+
|
|
396
|
+
NOTE: caller MUST hold self._lock.
|
|
397
|
+
Pinned experts are not moved in LRU order.
|
|
398
|
+
"""
|
|
399
|
+
if expert_id in self._pinned[layer] or expert_id in self._domain_pinned[layer]:
|
|
400
|
+
return # pinned: don't track in LRU
|
|
401
|
+
lru = self._lru_order[layer]
|
|
402
|
+
if expert_id in lru:
|
|
403
|
+
lru.remove(expert_id)
|
|
404
|
+
lru.append(expert_id) # most recent = end
|
|
405
|
+
|
|
406
|
+
def _find_evictable_slot(self, layer: int) -> Optional[int]:
|
|
407
|
+
"""Find a slot to use for loading.
|
|
408
|
+
|
|
409
|
+
Priority: empty slot > LRU unpinned slot.
|
|
410
|
+
Skips _RESERVED slots (in-flighting loads).
|
|
411
|
+
Returns slot index, or None if all slots pinned/reserved.
|
|
412
|
+
|
|
413
|
+
NOTE: caller MUST hold self._lock.
|
|
414
|
+
"""
|
|
415
|
+
slots = self._slots[layer]
|
|
416
|
+
pinned = self._pinned[layer] | self._domain_pinned[layer]
|
|
417
|
+
lru = self._lru_order[layer]
|
|
418
|
+
|
|
419
|
+
# 1. Prefer an already-empty slot (not in-flight)
|
|
420
|
+
for i, eid in enumerate(slots):
|
|
421
|
+
if eid == _EMPTY:
|
|
422
|
+
return i
|
|
423
|
+
|
|
424
|
+
# 2. Evict LRU unpinned expert (front of list = least recently used)
|
|
425
|
+
for eid in lru:
|
|
426
|
+
if eid not in pinned:
|
|
427
|
+
for i, seid in enumerate(slots):
|
|
428
|
+
if seid == eid:
|
|
429
|
+
return i
|
|
430
|
+
|
|
431
|
+
return None # all pinned/reserved — can't load anything new
|
|
432
|
+
|
|
433
|
+
def _evict_slot(self, layer: int, slot: int) -> None:
|
|
434
|
+
"""Reserve a slot for loading. Caller MUST hold self._lock.
|
|
435
|
+
|
|
436
|
+
Sets the slot to _RESERVED to prevent concurrent loaders
|
|
437
|
+
from taking it. The actual expert_id is set after I/O completes.
|
|
438
|
+
"""
|
|
439
|
+
old_eid = self._slots[layer][slot]
|
|
440
|
+
if old_eid not in (_EMPTY, _RESERVED):
|
|
441
|
+
# Remove from LRU tracking
|
|
442
|
+
lru = self._lru_order[layer]
|
|
443
|
+
if old_eid in lru:
|
|
444
|
+
lru.remove(old_eid)
|
|
445
|
+
# Remove the shm file
|
|
446
|
+
path = self._get_shm_path(layer, slot)
|
|
447
|
+
if path.exists():
|
|
448
|
+
path.unlink()
|
|
449
|
+
self._slots[layer][slot] = _RESERVED
|
|
450
|
+
|
|
451
|
+
def _load_expert(self, layer: int, expert_id: int) -> None:
|
|
452
|
+
"""Load expert weights from disk into a shared memory slot.
|
|
453
|
+
|
|
454
|
+
Runs in background thread pool. Sequence:
|
|
455
|
+
1. Reserve a slot (find or evict), set to _RESERVED
|
|
456
|
+
2. Read weights from disk (I/O, outside lock)
|
|
457
|
+
3. Write to shared memory file
|
|
458
|
+
4. Update slot from _RESERVED to expert_id (inside lock)
|
|
459
|
+
|
|
460
|
+
On failure: revert slot to _EMPTY.
|
|
461
|
+
"""
|
|
462
|
+
slot: Optional[int] = None
|
|
463
|
+
try:
|
|
464
|
+
# Step 1: reserve a slot
|
|
465
|
+
with self._lock:
|
|
466
|
+
# Double-check: loaded while we were queued
|
|
467
|
+
if self._is_cached(layer, expert_id):
|
|
468
|
+
self._pending_loads.pop((layer, expert_id), None)
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
slot = self._find_evictable_slot(layer)
|
|
472
|
+
if slot is None:
|
|
473
|
+
# All slots pinned/reserved — silently skip
|
|
474
|
+
self._pending_loads.pop((layer, expert_id), None)
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
# Evict occupant → set slot to _RESERVED
|
|
478
|
+
self._evict_slot(layer, slot)
|
|
479
|
+
|
|
480
|
+
# Step 2-3: I/O outside lock
|
|
481
|
+
data = self._read_expert_weights(layer, expert_id)
|
|
482
|
+
shm_path = self._get_shm_path(layer, slot)
|
|
483
|
+
with open(shm_path, "wb") as f:
|
|
484
|
+
f.write(data)
|
|
485
|
+
|
|
486
|
+
# Step 4: finalize (inside lock)
|
|
487
|
+
with self._lock:
|
|
488
|
+
# Only overwrite if still _RESERVED (not stolen)
|
|
489
|
+
if self._slots[layer][slot] == _RESERVED:
|
|
490
|
+
self._slots[layer][slot] = expert_id
|
|
491
|
+
self._touch(layer, expert_id)
|
|
492
|
+
self._loads_completed += 1
|
|
493
|
+
self._pending_loads.pop((layer, expert_id), None)
|
|
494
|
+
|
|
495
|
+
except Exception:
|
|
496
|
+
# On failure: free the slot
|
|
497
|
+
with self._lock:
|
|
498
|
+
self._pending_loads.pop((layer, expert_id), None)
|
|
499
|
+
if slot is not None \
|
|
500
|
+
and 0 <= layer < self.n_layers \
|
|
501
|
+
and 0 <= slot < self.slots_per_layer \
|
|
502
|
+
and self._slots[layer][slot] == _RESERVED:
|
|
503
|
+
self._slots[layer][slot] = _EMPTY
|
|
504
|
+
|
|
505
|
+
def _read_expert_weights(self, layer: int, expert_id: int) -> bytes:
|
|
506
|
+
"""Read expert weights from disk.
|
|
507
|
+
|
|
508
|
+
Priority:
|
|
509
|
+
1. GGUF model (via MoEGGUFReader) — if model_path was provided
|
|
510
|
+
2. Pre-extracted .bin file: {expert_data_dir}/L{layer}_E{expert_id}.bin
|
|
511
|
+
3. Zero-filled placeholder (fallback)
|
|
512
|
+
|
|
513
|
+
Returns exactly expert_size bytes.
|
|
514
|
+
"""
|
|
515
|
+
# Priority 1: read from GGUF model
|
|
516
|
+
if self._gguf_reader is not None:
|
|
517
|
+
data = self._gguf_reader.read_expert_weights(layer, expert_id)
|
|
518
|
+
if len(data) >= self.expert_size:
|
|
519
|
+
return data[:self.expert_size]
|
|
520
|
+
return data + b"\x00" * (self.expert_size - len(data))
|
|
521
|
+
|
|
522
|
+
# Priority 2: pre-extracted .bin files
|
|
523
|
+
if self.expert_data_dir:
|
|
524
|
+
bin_path = self.expert_data_dir / f"L{layer}_E{expert_id}.bin"
|
|
525
|
+
if bin_path.exists():
|
|
526
|
+
with open(bin_path, "rb") as f:
|
|
527
|
+
data = f.read()
|
|
528
|
+
if len(data) >= self.expert_size:
|
|
529
|
+
return data[:self.expert_size]
|
|
530
|
+
# Pad if shorter
|
|
531
|
+
return data + b"\x00" * (self.expert_size - len(data))
|
|
532
|
+
|
|
533
|
+
# Priority 3: placeholder zeros
|
|
534
|
+
return b"\x00" * self.expert_size
|
|
535
|
+
|
|
536
|
+
def _pin_backbone_from_map(self, expert_map: dict) -> None:
|
|
537
|
+
"""Pin backbone/universal experts from the domain→expert mapping.
|
|
538
|
+
|
|
539
|
+
Backbone experts are those activated in ALL tested domains.
|
|
540
|
+
In Qwen3.6 data, the 10 backbone experts are:
|
|
541
|
+
[41, 72, 89, 95, 112, 127, 191, 217, 221, 231]
|
|
542
|
+
(These appear in the top-15 of all 8 domains.)
|
|
543
|
+
|
|
544
|
+
This is called once at init time if auto_pin_backbone is set.
|
|
545
|
+
"""
|
|
546
|
+
backbone = expert_map.get("backbone_experts", [])
|
|
547
|
+
if backbone:
|
|
548
|
+
for layer in range(self.n_layers):
|
|
549
|
+
self.pin_experts(layer, backbone)
|