"""AUTOPIN:专家热度持久化 + 启动预热钉死。 两部分: - **热度计数与持久化**:搭已有的物化点零同步计频——block host 路径的 flat、 acquire_gpu 的 LFU piggyback(全命中)与 miss 回退 flat;dual decode/verify 无 Python 物化点,由 C++ g_real 的 LFU freq 在落盘时按增量差分合并。节流写 {EXPERT_DIR}/pool_usage.json(同目录临时文件 + rename 防半写;读不到/损坏静默从零)。 - **启动预热钉死**:build_streaming_model 末尾按历史热度把每层 top-N 热专家 (N = floor(cap_for × AUTOPIN_BUDGET_FRAC))经 blob 并行读预填进常驻池并 pin 住 (不参与任何驱逐),消灭冷启动慢热。 覆盖率口径:prefill/host 路径与 GPU remap miss 回退全计;非 dual 的 decode 全命中 快路径搭 LFU piggyback 计(inds 已物化,零额外同步);dual decode/verify 靠 C++ freq 兜底(仅 lfu 策略计频)。EVICT_POLICY≠lfu 时两条 GPU 快路径均不计——绝不为计数给 decode 热路径新增 GPU→host 同步。 """ import atexit import json import os import tempfile from collections import Counter from typing import Dict, List from mlx_streaming import config _USAGE_NAME = "pool_usage.json" def usage_path() -> str: """热度文件路径:AUTOPIN_USAGE_FILE 覆盖,缺省 {EXPERT_DIR}/pool_usage.json。""" p = config.autopin_usage_file() return p or os.path.join(config.expert_dir(), _USAGE_NAME) def load_usage(path: str) -> "Dict[int, Counter]": """读热度文件;不存在/损坏/结构非法一律静默返回空(从零开始)。""" try: with open(path) as f: raw = json.load(f) return {int(l): Counter({int(e): int(c) for e, c in d.items()}) for l, d in raw.items()} except Exception: return {} def save_usage(path: str, counts: "Dict[int, Counter]") -> None: """原子写:先写同目录临时文件再 rename,防半写损坏。失败静默(不阻断推理)。""" try: d = os.path.dirname(path) or "." fd, tmp = tempfile.mkstemp(dir=d, prefix=".pool_usage.", suffix=".tmp") with os.fdopen(fd, "w") as f: json.dump({str(l): {str(e): c for e, c in cnt.items()} for l, cnt in counts.items()}, f) os.replace(tmp, path) except Exception: pass class HeatCounter: """进程内热度累计器:note() 增量计频,节流 + atexit 落盘。 前向边界用「MoE 层号回绕」近似(与 VirtualPool.begin_forward 同判据):MoE 块按层号 递增被调,出现 layer <= 上次即新前向。路径在创建时快照,避免进程退出时 env 已变 导致 atexit 写错位置。 """ def __init__(self, path: str): self.path = path self.counts: "Dict[int, Counter]" = {} self._last_layer = -1 self._forwards_since_save = 0 # C++ 累计频次的已合并基线(real_freq_dump 是进程累计值,按差分合并防重复计) self._cpp_baseline: "Dict[int, Dict[int, int]]" = {} def note(self, layer: int, ids: List[int]) -> None: layer = int(layer) if layer <= self._last_layer: self._forwards_since_save += 1 self._last_layer = layer c = self.counts.get(layer) if c is None: c = self.counts[layer] = Counter() for e in ids: c[int(e)] += 1 if self._forwards_since_save >= config.autopin_save_every(): self.flush() def tick(self) -> None: """显式前向边界(dual decode 无 Python 计频点,由 begin_forward 驱动周期落盘)。""" self._forwards_since_save += 1 if self._forwards_since_save >= config.autopin_save_every(): self.flush() def merge_cpp_freq(self) -> None: """把 dual decode 的 C++ LFU 累计频次按增量差分合并进来。""" try: import mlx_streaming.native_moe_ext as _N flat = _N.real_freq_dump() except Exception: return cur: "Dict[int, Dict[int, int]]" = {} for i in range(0, len(flat), 3): l, e, c = int(flat[i]), int(flat[i + 1]), int(flat[i + 2]) cur.setdefault(l, {})[e] = c for l, em in cur.items(): base = self._cpp_baseline.get(l, {}) cnt = self.counts.setdefault(l, Counter()) for e, c in em.items(): b = base.get(e, 0) cnt[e] += c - b if c >= b else c # real_reset 后 c None: self.merge_cpp_freq() save_usage(self.path, self.counts) self._forwards_since_save = 0 _counter: "HeatCounter | None" = None def _get_counter() -> HeatCounter: global _counter if _counter is None: _counter = HeatCounter(usage_path()) atexit.register(_counter.flush) return _counter def note(layer: int, ids: List[int]) -> None: """路由热度计数入口(热路径调用):AUTOPIN=0 立即返回,零行为。""" if not config.autopin(): return _get_counter().note(layer, ids) def tick() -> None: """前向边界入口(virtual_pool begin_forward 调用):AUTOPIN=0 立即返回。""" if not config.autopin(): return _get_counter().tick() def counter() -> "HeatCounter | None": """测试/诊断用:当前计数器(未触发过计数则为 None)。""" return _counter def warm_start_pin(store) -> "dict": """启动预热钉死:按 usage 文件每层取 top-N 热专家,经 store.pin_batch 预填进池并 pin 住。 N = floor(该层 cap_for × AUTOPIN_BUDGET_FRAC)。pin 加载由 pin_batch 收口:有 blob_loader 走并行 pread(生产唯一数据源),无则退回逐专家 per-expert 文件(兼容旧部署)。 usage 缺失/为空 → 返回零摘要(后台计数仍在累计,供下次启动用);单层失败不阻断整体。 """ summary = {"layers": 0, "pinned": 0, "skipped_layers": 0, "seconds": 0.0} usage = load_usage(usage_path()) if not usage: return summary import time t0 = time.perf_counter() frac = config.autopin_budget_frac() for layer in sorted(usage): n = int(store.cap_for(layer) * frac) if n <= 0: continue top = [e for e, _ in usage[layer].most_common(n)] try: summary["pinned"] += store.pin_batch(layer, top) summary["layers"] += 1 except Exception: summary["skipped_layers"] += 1 summary["seconds"] = round(time.perf_counter() - t0, 2) return summary