432 lines
25 KiB
Python
432 lines
25 KiB
Python
"""MoE 热路径块:路由 → 选专家 → 取专家权重 → 计算 → 加权合并。
|
||
|
||
包含两种块:
|
||
- `StreamingMoeBlock`:包住原生 MoE 块,专家权重常驻(switch_mlp),只激活选中专家。
|
||
- `FileStreamingMoeBlock`:专家权重从磁盘按需加载(流式),是低内存推理的核心热路径,
|
||
集成常驻池 acquire、native-fused-prefetch 预取、共享专家叠加等逻辑。
|
||
"""
|
||
import os
|
||
import time
|
||
|
||
import mlx.core as mx
|
||
|
||
from mlx_streaming import config
|
||
from mlx_streaming.core.moe import native_moe
|
||
from mlx_streaming.core import route_trace
|
||
from mlx_streaming.core.profiling import (
|
||
PROF, WINDOW_PROF, PREDICT_RECALL_PROF, MISS_ATTRIB, note_miss_attrib,
|
||
note_tprof, TPROF_ON, _PROF_ON, _tick, note_union, UNION_ON)
|
||
|
||
# decode/verify 热路径判据:seq 短(单 token decode=1、MTP verify=K≤几),与 prefill 长 seq 区分。
|
||
_DECODE_SEQ_MAX = 8
|
||
from mlx_streaming.core.moe.gate import _effective_top_k
|
||
from mlx_streaming.core.cache import autopin
|
||
from mlx_streaming.core.moe.compute import (
|
||
streaming_switch_glu_forward, PersistentSubGLU)
|
||
|
||
|
||
class StreamingMoeBlock:
|
||
"""包住原 Qwen3MoeSparseMoeBlock:路由器常驻,专家计算改为只算选中专家。
|
||
|
||
若提供 store(LruExpertStore),可进一步把专家权重从磁盘按需加载;否则直接在
|
||
常驻的 switch_mlp 上做 uniq 切片计算(仍只激活少数专家)。
|
||
"""
|
||
|
||
def __init__(self, orig_block, layer_idx: int, store=None):
|
||
self.gate = orig_block.gate # 路由器常驻(很小)
|
||
self.top_k = orig_block.top_k
|
||
self.norm_topk_prob = orig_block.norm_topk_prob
|
||
self.switch_mlp = orig_block.switch_mlp
|
||
self.store = store
|
||
self.layer_idx = layer_idx
|
||
|
||
def __call__(self, x: mx.array) -> mx.array:
|
||
gates = mx.softmax(self.gate(x), axis=-1, precise=True)
|
||
k = _effective_top_k(self.top_k)
|
||
inds = mx.argpartition(gates, kth=-k, axis=-1)[..., -k:]
|
||
scores = mx.take_along_axis(gates, inds, axis=-1)
|
||
if self.norm_topk_prob:
|
||
scores = scores / mx.sum(scores, axis=-1, keepdims=True)
|
||
mx.eval(inds) # 先物化路由结果,才能按结果取专家
|
||
y = streaming_switch_glu_forward(self.switch_mlp, x, inds)
|
||
return (y * scores[..., None]).sum(axis=-2)
|
||
|
||
|
||
class FileStreamingMoeBlock:
|
||
"""文件后端流式 MoE 块:路由器常驻,专家权重从磁盘按需加载(不持有堆叠 switch_mlp)。"""
|
||
|
||
def __init__(self, gate, top_k, norm_topk_prob, store, layer_idx,
|
||
hidden, moe_inter, group_size, bits,
|
||
proj_bits: dict | None = None,
|
||
shared_expert=None, shared_expert_gate=None):
|
||
self.gate = gate
|
||
self.top_k = top_k
|
||
self.norm_topk_prob = norm_topk_prob
|
||
self.store = store
|
||
self.layer_idx = layer_idx
|
||
self.hidden = hidden
|
||
self.moe_inter = moe_inter
|
||
self.group_size = group_size
|
||
self.bits = bits
|
||
# Qwen3-Next 等带共享专家的模型:共享专家恒激活、必须常驻(不流式),
|
||
# 输出叠加 sigmoid(shared_expert_gate(x)) * shared_expert(x)。None 则退化为纯路由(如 Qwen3-MoE)。
|
||
self.shared_expert = shared_expert
|
||
self.shared_expert_gate = shared_expert_gate
|
||
# 可选全流式 blob 源(STREAM_BLOB=1 时由 model_builder 注入),见 core/blob_loader.py。
|
||
self._blob = None
|
||
# 持久化子模块:跨 token 复用,避免每调用重建 QSL。
|
||
# proj_bits 非空时走混合精度(逐 proj 不同 bit)。
|
||
self._sub = PersistentSubGLU(hidden, moe_inter, group_size, bits,
|
||
proj_bits=proj_bits, layer_idx=layer_idx)
|
||
|
||
def __call__(self, x: mx.array) -> mx.array:
|
||
if _PROF_ON:
|
||
return self._call_prof(x)
|
||
gates = mx.softmax(self.gate(x), axis=-1, precise=True)
|
||
k = _effective_top_k(self.top_k)
|
||
inds = mx.argpartition(gates, kth=-k, axis=-1)[..., -k:]
|
||
scores = mx.take_along_axis(gates, inds, axis=-1)
|
||
if self.norm_topk_prob:
|
||
scores = scores / mx.sum(scores, axis=-1, keepdims=True)
|
||
if config.predict_recall_prof():
|
||
ps = getattr(self, "_predicted_set", None)
|
||
if ps is not None:
|
||
act = {int(i) for i in inds.reshape(-1).tolist()}
|
||
PREDICT_RECALL_PROF["hit"] += len(ps & act)
|
||
PREDICT_RECALL_PROF["routed"] += len(act)
|
||
PREDICT_RECALL_PROF["n"] += 1
|
||
if config.native_moe():
|
||
native_y = self._try_native_forward(x, inds, scores, gates.shape[-1])
|
||
if native_y is not None:
|
||
y = native_y
|
||
if self.shared_expert is not None:
|
||
y = y + mx.sigmoid(self.shared_expert_gate(x)) * self.shared_expert(x)
|
||
return y
|
||
if self._blob is not None and config.stream_blob():
|
||
# 全流式 blob 路径:每层按需并行读专家 → 复用 _sub.forward(MLX quantized_matmul)。
|
||
flat = [int(i) for i in inds.reshape(-1).tolist()]
|
||
pool_arrays, slots = self._blob.acquire(self.layer_idx, flat)
|
||
local = mx.array(slots, dtype=inds.dtype).reshape(inds.shape)
|
||
y = self._sub.forward(pool_arrays, len(set(flat)), x, local)
|
||
y = (y * scores[..., None]).sum(axis=-2)
|
||
if self.shared_expert is not None:
|
||
y = y + mx.sigmoid(self.shared_expert_gate(x)) * self.shared_expert(x)
|
||
return y
|
||
if config.probe_perlayer_sync():
|
||
# 诊断:每层强制一次 host 同步(模拟预测 barrier),不做任何预取。
|
||
# 若单这个就拖慢 ~30% → 税是 eval barrier,C++ 读统一内存也救不了。
|
||
_ = int(inds.reshape(-1)[:1].item())
|
||
if (config.stream_blob_bg()
|
||
and getattr(self.store, "_bg", None) is not None):
|
||
# 后台预取已物化的专家在此(主线程、acquire 前)写进常驻池槽 → 转 miss 为 hit。
|
||
if config.window_prof():
|
||
t0 = getattr(self, "_submit_t", None)
|
||
if t0 is not None:
|
||
WINDOW_PROF["sum_s"] += time.perf_counter() - t0
|
||
WINDOW_PROF["n"] += 1
|
||
self.store.promote_prefetched(self.layer_idx)
|
||
# native-fused-prefetch 的 promote(把回调预读好的专家写进池槽)下移到各 acquire 分支前:
|
||
# host/verify 路径已算出真实路由 uniq_set,可"只 promote 命中本层路由的专家",零额外同步地
|
||
# 丢弃假阳性 → 省掉无用 scatter + 池污染(这是 promote -3.2% 开销的主因)。
|
||
_stg_mgr = getattr(self.store, "_staging", None)
|
||
# 双源:投机专家留池侧区由 demand_dual 取,不 promote、不驱逐 → 关 promote。
|
||
_do_promote = (_stg_mgr is not None and not config.native_no_promote()
|
||
and not config.zerocopy_dual_source())
|
||
if config.route_trace_enabled():
|
||
flat_trace = [int(i) for i in inds.reshape(-1).tolist()]
|
||
resident = (self.store.resident_experts(self.layer_idx)
|
||
if hasattr(self.store, "resident_experts") else set())
|
||
rank = (self.store.resident_lru_scores(self.layer_idx)
|
||
if hasattr(self.store, "resident_lru_scores") else {})
|
||
route_trace.record(
|
||
self.layer_idx, flat_trace, set(flat_trace) - resident, resident, rank)
|
||
# native-fused-prefetch:在 seq 分支**之前**提交,使 decode(seq=1) 与 MTP verify(seq=K)
|
||
# 两条路径都触发预取。dummy 折进 inds(加 0):GPU 路径靠 acquire_gpu 的 n_miss eval、
|
||
# host 路径靠后面的 .tolist() eval,都会触发完成回调里的 pread。verify 时 x 为 K 个
|
||
# token(verify_in=[x, d_1..d_{K-1}]),预测的是"下一层这 K 个 token 的专家并集"(recall≈0.96 的口径)。
|
||
# 双源双缓冲:每块开头先推进/检测前向边界(必须在本块预取提交之前,保证同前向内 fill_gen/read_gen 恒定)。
|
||
if config.zerocopy_dual_source() and getattr(self, "_vpool", None) is not None:
|
||
self._vpool.begin_forward(self.layer_idx)
|
||
if (getattr(self.store, "_staging", None) is not None
|
||
and not config.native_no_submit()):
|
||
_dummy = self._native_fused_prefetch(x)
|
||
if _dummy is not None:
|
||
inds = inds + (_dummy.reshape(()).astype(inds.dtype) * 0)
|
||
layer_cap = self.store.cap_for(self.layer_idx)
|
||
# verify(小 seq)可走 GPU 重映射;prefill(大 seq)唯一专家可能超 cap,须留在 host 路径(有超容量 fetch 回退)。
|
||
if config.zerocopy_dual_source() and getattr(self, "_vpool", None) is not None:
|
||
# 双源两级缓存本就是为 MTP verify 建的:verify 走 dual 路径才能读到侧区,否则落 host
|
||
# 路径会白填侧区(预取填了却不读)→ 命中骤降、读盘翻倍。
|
||
#
|
||
# 但判据必须按 demand_dual 真实区**可安放容量**算,不能按 cap+侧区行:侧区行是预取只读的,
|
||
# miss 只能落真实区的 cap 槽,其中 pinned(AUTOPIN)永不可驱逐。设本次唯一专家数 U、
|
||
# 真实区 cap=C、pinned=P,可用槽 = C − P − (本次命中的非 pinned 常驻数),推得
|
||
# 「U ≤ C − P」是安放得下的充分条件。超了 C++ 分不到槽会把专家落 0 号槽 → 逐位错算
|
||
# (prefill chunk 大时曾整段 system+tools 都算错)。U 无同步上界即 inds.size = seq×top_k。
|
||
_verify_gpu = (x.shape[1] * k <= self._dual_placeable(layer_cap))
|
||
else:
|
||
_verify_gpu = (config.verify_gpu_remap() and x.shape[1] * k <= layer_cap)
|
||
if (config.resident_pool_enabled()
|
||
and config.gpu_remap_enabled()
|
||
and (x.shape[1] == 1 or _verify_gpu)):
|
||
# decode 热路径:GPU 侧 slot 重映射,命中层零 host 往返(消除每层 .tolist 同步),
|
||
# 仅一次 miss 标志同步;真 miss 才回退读盘。decode top-k(≤cap)恒装得下。
|
||
# 专家总数 = gate 输出维(gates 末维),无需依赖 gate.weight。
|
||
if UNION_ON:
|
||
# GPU 路径本不 .tolist 真实路由;仅 UNION_PROF=1 时付一次同步取并集大小。
|
||
note_union(x.shape[1], len({int(i) for i in inds.reshape(-1).tolist()}),
|
||
self.layer_idx)
|
||
# decode/verify GPU 重映射路径:host 无现成真实路由。开关开时用 GPU membership 仅对
|
||
# 预读候选现算 used(drain≤budget)过滤假阳性;关时退回整批写入(回退/对照基线)。
|
||
if _do_promote:
|
||
if config.gpu_remap_promote_filter():
|
||
_stg_mgr.promote(self.layer_idx, self.store,
|
||
route_inds=inds, num_experts=gates.shape[-1])
|
||
else:
|
||
_stg_mgr.promote(self.layer_idx, self.store)
|
||
if config.miss_attrib():
|
||
# 诊断专用:GPU remap 路径本不 .tolist 真实路由(那是它消栅栏的关键),
|
||
# 仅 MISS_ATTRIB=1 时付一次 .tolist 取真实路由,统计 decode 热路径 A/B 构成。
|
||
_uniq = {int(i) for i in inds.reshape(-1).tolist()}
|
||
_ps = getattr(self, "_predicted_set", None)
|
||
_res = (self.store.resident_experts(self.layer_idx)
|
||
if hasattr(self.store, "resident_experts") else set())
|
||
_rdy = _stg_mgr.last_ready.get(self.layer_idx) if _stg_mgr else None
|
||
note_miss_attrib(_uniq, _ps, _res, x.shape[1] <= _DECODE_SEQ_MAX, _rdy)
|
||
if config.zerocopy_dual_source():
|
||
# 双源双缓冲:统一走 VirtualPool.acquire(呈现「所有专家都在」视角,
|
||
# 内部真实区表 ∪ 侧区(读代) 单次 gather,返回 (pool, local, n_experts))。
|
||
pool_arrays, local, n_experts = self._vpool.acquire(
|
||
self.layer_idx, inds, gates.shape[-1],
|
||
seq_len=x.shape[1], layer_cap=layer_cap)
|
||
y = self._sub.forward(pool_arrays, n_experts, x, local)
|
||
else:
|
||
pool_arrays, local = self.store.acquire_gpu(
|
||
self.layer_idx, inds, gates.shape[-1])
|
||
if config.stg_verify():
|
||
# 诊断:消费侧字节校验(默认 off),定位混合 ahead 下池槽损坏。
|
||
self.store._resident.verify_acquire_bytes(
|
||
self.layer_idx, inds, _stg_mgr)
|
||
y = self._sub.forward(pool_arrays, layer_cap, x, local)
|
||
else:
|
||
# prefill/大批量(seq>1)或显式关闭 GPU_REMAP:走 host 路径。
|
||
# 只在此处对 inds 做一次 .tolist() 同步,uniq/local 全在 Python 里算。
|
||
flat = [int(i) for i in inds.reshape(-1).tolist()]
|
||
autopin.note(self.layer_idx, flat) # AUTOPIN 热度:搭已物化的 flat,零额外同步(AUTOPIN=0 立即返回)
|
||
uniq_set = set(flat)
|
||
if UNION_ON:
|
||
note_union(x.shape[1], len(uniq_set), self.layer_idx) # 本层路由专家并集(零额外同步,uniq_set 已算)
|
||
# promote 只写"预读好 ∩ 本层真实路由"的专家:复用已算的 uniq_set,零额外同步,
|
||
# 假阳性不进池 → 省 scatter、不污染池(acquire 前完成,命中转化生效)。
|
||
if _do_promote:
|
||
_stg_mgr.promote(self.layer_idx, self.store, used=uniq_set)
|
||
if config.miss_attrib():
|
||
# 在 promote 之后、acquire(读盘)之前统计:本层真实路由的命中构成。
|
||
# miss 拆成 A(预测到却没进池:budget丢/时序/驱逐) 与 B(没预测到:召回缺口)。
|
||
_ps = getattr(self, "_predicted_set", None)
|
||
_res = (self.store.resident_experts(self.layer_idx)
|
||
if hasattr(self.store, "resident_experts") else set())
|
||
_rdy = _stg_mgr.last_ready.get(self.layer_idx) if _stg_mgr else None
|
||
note_miss_attrib(uniq_set, _ps, _res, x.shape[1] <= _DECODE_SEQ_MAX, _rdy)
|
||
# 池只能同时容纳 ≤该层容量 个唯一专家;prefill 唯一数超容量时回退 stack。
|
||
# dual 模式(_vpool 存在)统一走 VirtualPool.acquire_host 收口 host+fetch;
|
||
# 非 dual(_vpool 为 None)保留内联逻辑(非目标路径,避免额外依赖)。
|
||
if (config.resident_pool_enabled()
|
||
and getattr(self, "_vpool", None) is not None):
|
||
pool_arrays, local, n_experts = self._vpool.acquire_host(
|
||
self.layer_idx, flat, inds.shape, inds.dtype, layer_cap)
|
||
y = self._sub.forward(pool_arrays, n_experts, x, local)
|
||
elif (config.resident_pool_enabled()
|
||
and len(uniq_set) <= layer_cap):
|
||
pool_arrays, slots = self.store.acquire(self.layer_idx, flat)
|
||
local = mx.array(slots, dtype=inds.dtype).reshape(inds.shape)
|
||
y = self._sub.forward(pool_arrays, layer_cap, x, local)
|
||
else:
|
||
uniq_sorted = sorted(uniq_set)
|
||
remap = {g: i for i, g in enumerate(uniq_sorted)}
|
||
local = mx.array([remap[i] for i in flat], dtype=inds.dtype).reshape(inds.shape)
|
||
fetched = self.store.fetch(self.layer_idx, uniq_sorted)
|
||
y = self._sub.forward(fetched, len(uniq_sorted), x, local)
|
||
y = (y * scores[..., None]).sum(axis=-2)
|
||
if self.shared_expert is not None: # 共享专家(常驻)叠加
|
||
y = y + mx.sigmoid(self.shared_expert_gate(x)) * self.shared_expert(x)
|
||
return y
|
||
|
||
def _dual_placeable(self, layer_cap: int) -> int:
|
||
"""demand_dual 真实区本层能安放的唯一专家上限 = cap − pinned。
|
||
|
||
pinned 集合只在启动 AUTOPIN 预热时写入、生成期恒定,故首次查到后缓存
|
||
(decode 每层每步都要判,不能每次都进 C++ 取锁)。
|
||
"""
|
||
n = getattr(self, "_pinned_n", None)
|
||
if n is None:
|
||
try:
|
||
import mlx_streaming.native_moe_ext as _N
|
||
n = int(_N.real_pinned_count(int(self.layer_idx)))
|
||
except Exception: # noqa: BLE001 取不到就按最保守的「全钉死」算,逼回 host 路径
|
||
n = int(layer_cap)
|
||
self._pinned_n = n
|
||
return max(1, int(layer_cap) - n)
|
||
|
||
def _native_fused_prefetch(self, x: mx.array):
|
||
"""搭车式预取:用下 AHEAD 层 gate 对 x 算预测 inds(lazy),挂 GPU 完成回调,
|
||
在 C++ 里(GPU 算完后)读 id + pread 预热下层专家字节。返回 dummy 张量(需被 eval 才触发)。
|
||
全程不加主线程 host 同步——靠折进 inds、搭 acquire_gpu 的 n_miss eval。
|
||
"""
|
||
model = getattr(self, "_prefetch_model_ref", None)
|
||
bl = getattr(self.store, "_blob_loader", None)
|
||
if model is None or bl is None:
|
||
return None
|
||
layers = getattr(model, "layers", [])
|
||
vp = getattr(self, "_vpool", None)
|
||
if vp is not None:
|
||
tgt = vp.target_for(self.layer_idx) # per-layer cutoff ahead
|
||
if not (self.layer_idx < tgt < len(layers)): # 0/越界/无前瞻 → 跳过
|
||
return None
|
||
else:
|
||
ahead = max(1, config.cross_layer_ahead(default=1))
|
||
tgt = self.layer_idx + ahead
|
||
if not (0 <= tgt < len(layers)):
|
||
return None
|
||
tmlp = getattr(layers[tgt], "mlp", None)
|
||
if not isinstance(tmlp, FileStreamingMoeBlock):
|
||
return None
|
||
try:
|
||
from mlx_streaming import native_moe_ext as _N
|
||
except Exception:
|
||
return None
|
||
# 方案B:预测"宽集合"(top-N, N=predict_width)只为高 recall,不占内存(仅一次 gate argpartition);
|
||
# 真正占 staging 的是 C++ 回调过滤常驻后、按分截到 staging budget 的"缺口"子集。
|
||
_tp0 = time.perf_counter() if TPROF_ON else 0.0
|
||
predict_width = config.cross_layer_predict_width()
|
||
# 默认(PREDICT_USE_X=1):用本层 MoE 输入 x 喂目标层 gate。x 含了本层 attention,离 L+1
|
||
# 近半层 → 比未归一化输入更新鲜,实测 recall 0.812→0.847(+3.6pp)、还省一次 norm。
|
||
# PREDICT_USE_X=0 回退旧路径:目标层 post_attention_layernorm 作用在本层未归一化输入上。
|
||
h = getattr(self, "_unnormed_input", None)
|
||
if config.predict_use_x():
|
||
g = tmlp.gate(x)
|
||
elif h is None:
|
||
h = x # 退化:无未归一化输入时用 x(norm 偏差,覆盖会差)
|
||
g = tmlp.gate(h)
|
||
else:
|
||
g = tmlp.gate(layers[tgt].post_attention_layernorm(h))
|
||
# 按 seq 维聚合成"该批 K 个 token 的专家并集"近似。聚合方式 PREDICT_AGG:
|
||
# - max(默认):任一 token 强烈想要即入选;mean:各 token 平均偏好;
|
||
# - union:每 token 各取 top-kk 再并集(与真实路由"并集"结构最一致,候选更多,由 handler 去重+截断)。
|
||
_agg = config.predict_agg()
|
||
kk = min(g.shape[-1], predict_width)
|
||
if g.ndim == 3 and _agg == "union":
|
||
# union 候选数 = seq × union_k。每 token 只取 top-union_k(独立于 predict_width),
|
||
# 调小 union_k 缩候选 → 减少缺口、缓解 staging/读盘洪水(每 token 真实只路由 top_k 个)。
|
||
uk = min(g.shape[-1], config.predict_union_k())
|
||
pred = mx.argpartition(g, kth=-uk, axis=-1)[..., -uk:].reshape(-1).astype(mx.uint32)
|
||
else:
|
||
if g.ndim == 3:
|
||
g = g.mean(axis=1) if _agg == "mean" else g.max(axis=1)
|
||
# 取 top-kk(argpartition,O(E) 不全排序)。实测:加宽 kk 不提升命中——staging 更多 = 后台
|
||
# pread 更多、到位更晚 + 抢带宽 → 反而更低;故 width=16 即最优,cap 截断极少触发、无需排序。
|
||
pred = mx.argpartition(g, kth=-kk, axis=-1)[..., -kk:].reshape(-1).astype(mx.uint32)
|
||
if config.predict_recall_prof() or config.miss_attrib():
|
||
try:
|
||
tmlp._predicted_set = {int(e) for e in pred.tolist()}
|
||
except Exception:
|
||
pass
|
||
if TPROF_ON:
|
||
note_tprof("predict_s", time.perf_counter() - _tp0, count_key="predict_n")
|
||
stg = getattr(self.store, "_staging", None)
|
||
if stg is not None:
|
||
resident = (self.store.resident_experts(tgt)
|
||
if hasattr(self.store, "resident_experts") else None)
|
||
if config.zerocopy_dual_source():
|
||
# 双源双缓冲:经 VirtualPool 向填代 submit,C++ 回调把预读段散写进该代侧区行。
|
||
rp = self.store._resident
|
||
if tgt not in rp._pools:
|
||
return None # 目标层池未建(首 token 预热) → 跳过本次
|
||
segs = stg.src._segs # (proj, tensor, dt, shape, nb),与池 key 顺序一致
|
||
pool_list = [rp._pools[tgt][f"{p}.{t}"] for p, t, *_ in segs]
|
||
import os as _os
|
||
if _os.environ.get("POOL_PTR_TRACE") is not None and tgt == int(_os.environ["POOL_PTR_TRACE"]):
|
||
try:
|
||
import mlx_streaming.native_moe_ext as _N
|
||
_fk = f"{segs[0][0]}.{segs[0][1]}"
|
||
print(f"[POOL_PTR] submit layer={tgt} key={_fk} obj_id={id(rp._pools[tgt][_fk])} "
|
||
f"ptr={hex(_N.array_data_ptr(rp._pools[tgt][_fk]))}", flush=True)
|
||
except Exception as _e:
|
||
print(f"[POOL_PTR] err {_e}", flush=True)
|
||
return self._vpool.prefetch(tgt, pred, resident, pool_list)
|
||
# miss→hit:回调按目标层常驻快照过滤,只把缺口 pread 进 staging(≤budget 行),promote 时写池。
|
||
if TPROF_ON:
|
||
_ts0 = time.perf_counter()
|
||
_r = stg.submit(tgt, pred, resident)
|
||
note_tprof("submit_s", time.perf_counter() - _ts0, count_key="submit_n")
|
||
return _r
|
||
return stg.submit(tgt, pred, resident)
|
||
# 仅预热字节(page cache)的轻量版。
|
||
path = os.path.join(bl.dir, f"layer{tgt:02d}.blob")
|
||
return _N.prefetch_on_complete(pred, path, int(bl.stride), True)
|
||
|
||
def _try_native_forward(self, x: mx.array, inds: mx.array, scores: mx.array, num_experts: int):
|
||
# native 第一版只支持三投影同 bit 的专家;其他情况保持现有 MLX 回退。
|
||
proj_bits = getattr(self._sub, "proj_bits", {})
|
||
bits_set = {int(proj_bits.get(name, self.bits)) for name in ("gate_proj", "up_proj", "down_proj")}
|
||
if len(bits_set) != 1:
|
||
return None
|
||
bits = bits_set.pop()
|
||
if not native_moe.can_native_moe(
|
||
self.layer_idx, self.hidden, self.moe_inter,
|
||
self.group_size, bits, num_experts):
|
||
return None
|
||
t_sync = time.perf_counter()
|
||
mx.eval(inds)
|
||
flat = [int(i) for i in inds.reshape(-1).tolist()]
|
||
native_moe.note_route_sync(time.perf_counter() - t_sync)
|
||
return native_moe.try_native_moe(
|
||
self.layer_idx, flat, x, scores,
|
||
self.hidden, self.moe_inter, self.group_size, bits, num_experts)
|
||
|
||
def _call_prof(self, x: mx.array) -> mx.array:
|
||
t = time.perf_counter()
|
||
gates = mx.softmax(self.gate(x), axis=-1, precise=True)
|
||
k = _effective_top_k(self.top_k)
|
||
inds = mx.argpartition(gates, kth=-k, axis=-1)[..., -k:]
|
||
scores = mx.take_along_axis(gates, inds, axis=-1)
|
||
if self.norm_topk_prob:
|
||
scores = scores / mx.sum(scores, axis=-1, keepdims=True)
|
||
mx.eval(inds, scores)
|
||
_tick("route", t); t = time.perf_counter()
|
||
|
||
flat = [int(i) for i in inds.reshape(-1).tolist()]
|
||
uniq_set = set(flat)
|
||
layer_cap = self.store.cap_for(self.layer_idx)
|
||
use_pool = (config.resident_pool_enabled()
|
||
and len(uniq_set) <= layer_cap)
|
||
if not use_pool:
|
||
uniq_sorted = sorted(uniq_set)
|
||
remap = {g: i for i, g in enumerate(uniq_sorted)}
|
||
local = mx.array([remap[i] for i in flat], dtype=inds.dtype).reshape(inds.shape)
|
||
mx.eval(local)
|
||
_tick("pyremap", t); t = time.perf_counter()
|
||
|
||
if use_pool:
|
||
pool_arrays, slots = self.store.acquire(self.layer_idx, flat)
|
||
local = mx.array(slots, dtype=inds.dtype).reshape(inds.shape)
|
||
mx.eval(local)
|
||
fetched, n_experts = pool_arrays, layer_cap
|
||
else:
|
||
fetched = self.store.fetch(self.layer_idx, uniq_sorted)
|
||
n_experts = len(uniq_sorted)
|
||
mx.eval(list(fetched.values()))
|
||
_tick("fetch", t); t = time.perf_counter()
|
||
|
||
y = self._sub.forward(fetched, n_experts, x, local)
|
||
mx.eval(y)
|
||
_tick("matmul", t); t = time.perf_counter()
|
||
|
||
y = (y * scores[..., None]).sum(axis=-2)
|
||
if self.shared_expert is not None:
|
||
y = y + mx.sigmoid(self.shared_expert_gate(x)) * self.shared_expert(x)
|
||
mx.eval(y)
|
||
_tick("combine", t)
|
||
PROF["n_calls"] += 1
|
||
return y
|