"""共享装配层:把"文件后端流式加载主模型 + 取 hidden + 贪心生成"等跨入口复用的逻辑 集中在这里,而不是藏在某个入口脚本(原 validate_mtp)里。 依赖关系:本模块位于 core(mem/expert_store/streaming_moe)与 mtp 之上, 是把它们粘合成可运行模型的装配层,被 cli/ 各入口与测试复用。 环境变量: MODEL 主模型路径(MLX 量化) EXPERT_DIR 拆分/重量化后的 per-expert safetensors 目录 EXPERT_SLOTS 每层常驻池容量 EXPERT_POOL_PROFILE 每层池预算 JSON(无损省内存,可选) HIDDEN_VARIANT pre_final_norm(默认)| post_final_norm(排错时切换) """ import json import os import mlx.core as mx from mlx_lm import load from mlx_lm.models.base import create_attention_mask, create_ssm_mask from mlx_streaming import config from mlx_streaming.core.cache.expert_store import FileExpertStore from mlx_streaming.core.prefetch.patch import patch_model_filebacked MODEL = config.model_path() EXPERT_DIR = config.expert_dir() EXPERT_SLOTS = config.expert_slots() # pre_final_norm(默认)| post_final_norm(排错时切换) HIDDEN_VARIANT = config.hidden_variant() # 默认 profile 文件名:放在 EXPERT_DIR 下随专家目录一起走,常用跑法自动启用(无损省内存) DEFAULT_PROFILE_NAME = "pool_profile.json" def load_pool_profile(expert_dir: str) -> "dict[int, int] | None": """解析每层池预算 profile,返回 layer_caps 或 None。 优先级:环境变量 EXPERT_POOL_PROFILE 显式指定路径 > {expert_dir}/pool_profile.json 默认。 EXPERT_POOL_PROFILE=none/0/off 显式关闭(回到 uniform capacity)。 profile 无损:仅按各层真实工作集分配,命中率/输出/吞吐不变(caps 仍被 capacity 上限钳制)。 """ p = config.expert_pool_profile() if p.lower() in ("none", "0", "off"): return None if not p: # 未显式指定 → 默认找专家目录下的 profile cand = os.path.join(expert_dir, DEFAULT_PROFILE_NAME) p = cand if os.path.exists(cand) else "" if p and os.path.exists(p): with open(p) as f: caps = json.load(f).get("layer_caps", {}) return {int(k): int(v) for k, v in caps.items()} return None def build_streaming_model(): """用文件后端流式 patch 加载主模型(32GB 机器装不下 41GB 非流式)。""" model, tok = load(MODEL, lazy=True) # 取首个 MoE 维度 dims = None for layer in model.layers: mlp = getattr(layer, "mlp", None) if mlp is not None and hasattr(mlp, "switch_mlp") and hasattr(mlp, "gate"): gp = mlp.switch_mlp.gate_proj dims = {"hidden": gp.input_dims, "moe_inter": gp.output_dims, "group_size": getattr(gp, "group_size", 64), "bits": getattr(gp, "bits", 4)} break bits, group, proj_bits, layer_proj_bits = ( dims["bits"], dims["group_size"], None, None) meta_path = os.path.join(EXPERT_DIR, "_split_meta.json") if os.path.exists(meta_path): with open(meta_path) as f: meta = json.load(f) ed = meta.get("dims", {}) bits = ed.get("bits", bits) group = ed.get("group_size", group) proj_bits = ed.get("proj_bits") if "per_layer_proj_bits" in ed: layer_proj_bits = {int(k): v for k, v in ed["per_layer_proj_bits"].items()} # 每层池预算 profile(pool_footprint 产出):默认从 {EXPERT_DIR}/pool_profile.json 自动启用, # 无损省内存(命中率/输出/吞吐不变,仅不再为低占用层预留满 capacity)。 layer_caps = load_pool_profile(EXPERT_DIR) store = FileExpertStore(EXPERT_DIR, capacity=EXPERT_SLOTS, layer_caps=layer_caps) if config.zerocopy_dual_source(): # 零拷贝双源双缓冲:常驻池换成侧区模式(预分配 cap+2*spec_slots 行、禁 grow),复用原池 loader/cap/profile。 from mlx_streaming.core.cache.resident_pool import ResidentExpertPool _old = store._resident # 默认单缓冲(持久 LFU,一份工作集,省一半侧区内存=生产路径);仅显式 legacy(SIDEREGION_LFU=0)用双缓冲。 _spec_gens = 1 if config.sideregion_lfu() else 2 store._resident = ResidentExpertPool( _old.capacity, loader=_old.loader, layer_caps=_old.layer_caps, spec_slots=config.pool_spec_slots(), spec_gens=_spec_gens) if config.stream_blob_loader(): # blob 接入常驻池 miss-loader:复用 GPU-remap 快路径,小 EXPERT_SLOTS 即低内存。 store._blob_loader = _make_blob_source(dims, group, bits) # 主动预取(native-fused-prefetch miss→hit):opt-in(NATIVE_FUSED_PREFETCH=1)。 # 经"promote 只写真实路由命中专家"修正后已是净正:易缓存基座上 +15.5% tok/s # (demand 11.86→13.70,hit 0.731→0.851,读盘 −45%;见 active-prefetch-turnaround-2026-06-17.md)。 # 默认关只因收益依赖场景(基座可缓存性/是否磁盘受限)且只影响速度不影响质量、多占少量 staging 内存, # 故作 opt-in 而非默认路径,落地配方见上述报告 §6。 if config.native_fused_prefetch() and getattr(store, "_blob_loader", None) is not None: try: import mlx_streaming.native_moe_ext # noqa: F401 确认扩展已编译 from mlx_streaming.core.prefetch.native_staging import NativeStagingManager _budget = (config.pool_spec_slots() if config.zerocopy_dual_source() else config.stream_blob_bg_budget(default=16)) store._staging = NativeStagingManager(store._blob_loader, budget=_budget) except Exception: store._staging = None # 扩展不可用 → 关闭,不影响主路径 # 零拷贝双源不变量:staging 侧区行数必须等于池 spec_slots,否则 C++ 会越界写池(静默损坏)。 if config.zerocopy_dual_source() and getattr(store, "_staging", None) is not None: assert store._staging.budget == store._resident.spec_slots, ( f"零拷贝双源要求 staging.budget({store._staging.budget}) " f"== 池 spec_slots({store._resident.spec_slots})") if config.stream_blob_bg(): # 后台预取池预填:bg 在独立 stream 物化预测专家,promote 写进池槽(需 CROSS_LAYER_PREFETCH=1)。 from mlx_streaming.core.prefetch.bg_prefetch import BackgroundExpertPrefetcher src = _make_blob_source(dims, group, bits) store._blob_loader = src store._bg = BackgroundExpertPrefetcher( src, window=config.stream_blob_window()) patch_model_filebacked(model, store, dims["hidden"], dims["moe_inter"], group, bits, proj_bits=proj_bits, layer_proj_bits=layer_proj_bits) # 双源双缓冲:构造一个共享 VirtualPool(gen 跨层全局、每前向 +1)挂到每个流式 MoE 块。 if config.zerocopy_dual_source() and getattr(store, "_staging", None) is not None: from mlx_streaming.core.cache.virtual_pool import VirtualPool from mlx_streaming.core.moe.block import FileStreamingMoeBlock # 双源模式仍需 ahead 调度:block._native_fused_prefetch 靠 target_for 选目标层, # 不传调度参数会让 target_for 恒返回 0(_num_layers=0)→ 预取全跳过、侧区永远空。 _vpool = VirtualPool(store._resident, store._staging, config.pool_spec_slots(), num_layers=len(model.layers), cutoff=config.cross_layer_cutoff(), ahead_lo=config.cross_layer_ahead_lo(), ahead_hi=config.cross_layer_ahead_hi(), store=store) for layer in model.layers: mlp = getattr(layer, "mlp", None) if isinstance(mlp, FileStreamingMoeBlock): mlp._vpool = _vpool # 主动预取(非 zerocopy):挂 per-layer ahead 调度器 vpool(cutoff),让晚层预读更早发起。 if (config.native_fused_prefetch() and not config.zerocopy_dual_source() and getattr(store, "_staging", None) is not None): from mlx_streaming.core.cache.virtual_pool import VirtualPool from mlx_streaming.core.moe.block import FileStreamingMoeBlock _sched = VirtualPool(num_layers=len(model.layers), cutoff=config.cross_layer_cutoff(), ahead_lo=config.cross_layer_ahead_lo(), ahead_hi=config.cross_layer_ahead_hi()) for layer in model.layers: mlp = getattr(layer, "mlp", None) if isinstance(mlp, FileStreamingMoeBlock): mlp._vpool = _sched if config.stream_blob(): _attach_blob_source(model, dims, group, bits) # KV 量化(IsoQuant K4/V3 + SO(4) 旋转):仅作用于 12 个全注意力层,128k KV 3.0→~0.68 GiB。 if config.kv_quant(): from mlx_streaming.core.cache.kv_quant_patch import patch_kv_quant patch_kv_quant(model, group_size=config.kv_group_size(), k_bits=config.kv_k_bits(), v_bits=config.kv_v_bits(), rotate=config.kv_rotate(), seed=config.kv_rot_seed()) # AUTOPIN 预热钉死(池 + blob_loader + staging 就绪后、return 前):按历史路由热度把每层 # top-N 热专家预填进常驻池并 pin 住(不参与任何驱逐),消灭冷启动慢热;usage 缺失则跳过 # (会话中计数仍在累计,落盘供下次启动)。AUTOPIN=0 零行为。 if config.autopin(): from mlx_streaming.core.cache import autopin as _autopin _ap = _autopin.warm_start_pin(store) if _ap["pinned"]: print(f"[AUTOPIN] 预热 pin: {_ap['layers']} 层共 {_ap['pinned']} 专家 " f"(frac={config.autopin_budget_frac()}, 耗时 {_ap['seconds']}s)" + (f", 跳过 {_ap['skipped_layers']} 层" if _ap["skipped_layers"] else ""), flush=True) # 把 store 挂到 model 上,供只拿到 model 的调用方(server /api/stats 读专家池健康度)取用。 # 非 array/dict/list/tuple 走 object 属性,不进 nn.Module 参数树。 model._expert_store = store return model, tok, store def _make_blob_source(dims, group, bits): from mlx_streaming.core.cache.blob_loader import BlobExpertSource blob_dir = config.blob_dir() or os.path.join(EXPERT_DIR, "blobs") workers = config.stream_blob_workers() nocache = config.stream_blob_nocache(default="0") num_experts = 512 idx_path = os.path.join(blob_dir, "blob_index.json") if os.path.exists(idx_path): with open(idx_path) as f: num_experts = int(json.load(f).get("num_experts", num_experts)) return BlobExpertSource(blob_dir, dims["hidden"], dims["moe_inter"], group, bits, num_experts=num_experts, workers=workers, nocache=nocache) def _attach_blob_source(model, dims, group, bits): """STREAM_BLOB=1:给每个流式 MoE 块注入共享 BlobExpertSource(全流式低内存路径)。""" from mlx_streaming.core.cache.blob_loader import BlobExpertSource from mlx_streaming.core.moe.block import FileStreamingMoeBlock blob_dir = config.blob_dir() or os.path.join(EXPERT_DIR, "blobs") workers = config.stream_blob_workers() nocache = config.stream_blob_nocache(default="1") num_experts = 512 idx_path = os.path.join(blob_dir, "blob_index.json") if os.path.exists(idx_path): with open(idx_path) as f: num_experts = int(json.load(f).get("num_experts", num_experts)) src = BlobExpertSource(blob_dir, dims["hidden"], dims["moe_inter"], group, bits, num_experts=num_experts, workers=workers, nocache=nocache) for layer in model.layers: mlp = getattr(layer, "mlp", None) if isinstance(mlp, FileStreamingMoeBlock): mlp._blob = src def capture_prenorm_hidden(model, input_ids: mx.array) -> mx.array: """跑主模型层循环但跳过最后的 model.norm,返回 last-layer hidden(norm 前)。 HIDDEN_VARIANT=post_final_norm 时返回 norm 之后(用于消歧排错)。 """ inner = model.model h = inner.embed_tokens(input_ids) layers = inner.layers if not layers: return h cache = model.make_cache() fa_idx = next((i for i, l in enumerate(layers) if not l.is_linear), 0) ssm_idx = next((i for i, l in enumerate(layers) if l.is_linear), 0) fa_mask = create_attention_mask(h, cache[fa_idx]) ssm_mask = create_ssm_mask(h, cache[ssm_idx]) for layer, c in zip(layers, cache): mask = ssm_mask if layer.is_linear else fa_mask h = layer(h, mask=mask, cache=c) if HIDDEN_VARIANT == "post_final_norm": h = inner.norm(h) return h def greedy(model, input_ids: mx.array, n: int) -> mx.array: """主模型贪心生成 n 个 token,返回拼接后的完整序列(用作自投机参考)。""" cache = model.make_cache() cur = input_ids out = [] for _ in range(n): logits = model(cur, cache=cache) nxt = mx.argmax(logits[:, -1, :], axis=-1, keepdims=True) out.append(nxt) cur = nxt mx.eval(nxt) return mx.concatenate([input_ids] + out, axis=1)