1"""Build a TITAN model from an MLX checkpoint: the host architecture (attention, norms, router) is2mlx-lm's own module tree; every layer's `switch_mlp` is replaced by a TitanSwitchGLU over a slot table.3Expert weights are never materialised from the checkpoint by mlx-lm: they are read on demand by the4ExpertStore. `capacity` = number of resident experts per layer (num_experts = everything resident).5"""6from __future__ import annotations78import os9from typing import Dict, List, Optional1011import mlx.core as mx12import mlx.nn as nn13from mlx_lm.utils import load_model, load_tokenizer1415from .moe import TitanMoEBlock, TitanSwitchGLU16from .prefetch import Prefetcher17from . import predictors as _pred18from .registry import ModelSpec, load_spec19from .slots import EvictionPolicy, LFUPolicy, SlotTable20from .store import ExpertStore2122POLICIES = {"lru": EvictionPolicy, "lfu": LFUPolicy}232425class TitanModel:26 def __init__(self, model_dir: str, capacity: Optional[int] = None, policy: str = "lru", nocache: bool = False,27 on_route=None, predictor: str = "none", m: int = 8, prefetch_workers: int = 2, traces_dir: Optional[str] = None):28 self.model_dir = os.path.realpath(model_dir)29 self.spec: ModelSpec = load_spec(self.model_dir)30 self.store = ExpertStore(self.spec, nocache=nocache)31 self.capacity = capacity or self.spec.num_experts32 self.policy_name = policy33 # Load the host model lazily so the stacked expert tensors are never evaluated.34 from pathlib import Path35 self.model, cfg = load_model(Path(self.model_dir), lazy=True)36 self.tokenizer = load_tokenizer(Path(self.model_dir))37 q = cfg.get("quantization") or cfg.get("quantization_config")38 self.tables: List[SlotTable] = []39 self.blocks: List[TitanSwitchGLU] = []40 for l, layer in enumerate(self.model.model.layers):41 table = SlotTable(l, self.capacity, self.store, POLICIES[policy]())42 block = TitanSwitchGLU(table, q["group_size"], q["bits"], on_route=on_route)43 # drop the lazy stacked weights and splice in the TITAN block44 mlp = layer.mlp45 del mlp.switch_mlp # removes the lazy stacked SwitchGLU (never evaluated)46 mlp.switch_mlp = block # plain attribute: not an nn.Module, not in parameters()47 self.tables.append(table)48 self.blocks.append(block)49 mx.eval(self.model.parameters()) # dense weights only50 # optional predictive prefetcher: replaces each layer's MoE block with the hooked exact-routing block51 self.prefetcher = None52 self.predictor_name = predictor53 if predictor != "none":54 pred = _pred.make(predictor, self.spec.num_layers, self.spec.num_experts)55 if traces_dir is not None and hasattr(pred, "fit"):56 pred.fit(_load_fit_traces(traces_dir))57 self.prefetcher = Prefetcher(self.tables, self.store, pred, m=m, workers=prefetch_workers)58 for l, layer in enumerate(self.model.model.layers):59 self.blocks[l].prefetcher = self.prefetcher60 layer.mlp = TitanMoEBlock(layer.mlp, self.blocks[l], self.spec.model_type, self.prefetcher)6162 # convenience -----------------------------------------------------------------63 @property64 def layers(self):65 return self.model.layers6667 def __call__(self, inputs, cache=None):68 return self.model(inputs, cache=cache)6970 def warm_all(self):71 """Load every expert (only meaningful when capacity == num_experts)."""72 for t in self.tables:73 t.ensure(list(range(self.spec.num_experts)))74 t.reset_stats()75 self.store.reset_stats()7677 def cache_stats(self) -> Dict:78 per = [t.stats() for t in self.tables]79 if self.prefetcher is not None:80 per_pf = self.prefetcher.stats()81 else:82 per_pf = None83 hits = sum(p["hits"] + p["hits_device"] for p in per)84 misses = sum(p["misses"] for p in per)85 return {"layers": per, "hits": hits, "misses": misses,86 "hit_rate": hits / (hits + misses) if hits + misses else None,87 "stall_time_s": sum(p["stall_time_s"] for p in per), "store": self.store.stats(), "prefetch": per_pf}8889 def reset_stats(self):90 for t in self.tables:91 t.reset_stats()92 self.store.reset_stats()93 if self.prefetcher is not None:94 self.prefetcher.reset_stats()959697def _load_fit_traces(traces_dir: str):98 import json, numpy as np99 idx = json.load(open(os.path.join(traces_dir, "index.json")))100 out = []101 for p in idx["prompts"]:102 if p["split"] != "fit":103 continue104 z = np.load(os.path.join(traces_dir, p["name"] + ".npz"))105 out.append((z["topk"].astype(np.int64), z["probs"]))106 return out107