sparkle/mlx_streaming/core/profiling.py
fiser_jun 4745f264b2
2026-08-04 14:34:00 +08:00

132 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""热路径计时与诊断埋点(从 streaming_moe 抽出,集中管理)。
- PROF细粒度分段计时STREAM_PROF=1probe 读取。
- WINDOW_PROF同层 submit→promote 的 attention/GDN 窗口时间WINDOW_PROF=1
- PREDICT_RECALL_PROF运行时预测对实际路由的覆盖率分诊预取覆盖问题PREDICT_RECALL_PROF=1
这些 dict 为共享可变对象:各模块 import 后原地累加外部run_mtp_spec读同一对象。
"""
import time
import mlx.core as mx
from mlx_streaming import config
_PROF_ON = config.stream_prof()
PROF = {"route": 0.0, "pyremap": 0.0, "fetch": 0.0, "matmul": 0.0, "combine": 0.0,
"n_calls": 0}
WINDOW_PROF = {"sum_s": 0.0, "n": 0}
PREDICT_RECALL_PROF = {"hit": 0, "routed": 0, "n": 0}
# 主动预取路径 host 墙钟探针PREFETCH_TPROF=1量主线程"不可与 GPU 重叠"的 CPU 时间。
# 注意MLX 惰性——gate matmul / pool scatter 的 GPU 计算落在前向末尾统一 eval本探针看不到
# 那部分用消融吞吐口径(NATIVE_NO_SUBMIT/NATIVE_NO_PROMOTE 差值)测。本探针专测 host 段:
# - predict_sblock._native_fused_prefetch 里建预测图(gate(x)+argpartition[+predicted_set tolist])
# - submit_s stg.submit(prefetch_into_staging 的 host 调用,注册 GPU 完成回调)
# - promote_spromote 总时长,再细分:
# · take_s prefetch_staging_takeC++ 锁读已就绪记录)
# · route_sroute_used_subset 的成员测试 .tolist()GPU→host 同步drain≤budget
# · place_s_place_expert 循环(建惰性切片 + 池 scatter 入图,不含其 GPU 执行)
# *_n 为各段调用次数(按"层·前向"计)。关时各调用点直接跳过,零开销。
PREFETCH_TPROF = {
"predict_s": 0.0, "predict_n": 0,
"submit_s": 0.0, "submit_n": 0,
"promote_s": 0.0, "promote_n": 0,
"take_s": 0.0, "route_s": 0.0, "place_s": 0.0, "place_experts": 0,
}
TPROF_ON = config.prefetch_tprof()
def note_tprof(seg: str, dt: float, *, count_key: "str | None" = None, count: int = 1) -> None:
"""累加预取某段 host 墙钟。dt 为秒;可选 count_key 同步累加调用/专家计数。"""
PREFETCH_TPROF[seg] += dt
if count_key is not None:
PREFETCH_TPROF[count_key] += count
def tprof_reset() -> None:
for k in PREFETCH_TPROF:
PREFETCH_TPROF[k] = 0 if isinstance(PREFETCH_TPROF[k], int) else 0.0
# miss 归因MISS_ATTRIB=1promote 之后 / acquire 之前统计本层真实路由的命中构成):
# - resident_hitacquire 前已驻留LRU 历史 + 本次 promote 命中)
# - miss_A_predictedmiss 且"在预测集里"——预测对了但没进池budget 丢/时序没到/被驱逐)
# - miss_B_unpredictedmiss 且"不在预测集里"——预测器召回缺口(根本没预测到)
# dec_* 为 decode/verify 热路径专用桶seq 短,与 prefill 长 seq 分离):原 miss_attrib
# 仅埋在 host 分支、被 prefill 样本主导,无法反映 decode 热路径的真实 A/B 构成。
MISS_ATTRIB = {"routed": 0, "resident_hit": 0, "miss_A_predicted": 0,
"miss_B_unpredicted": 0, "n": 0,
"dec_routed": 0, "dec_resident_hit": 0, "dec_miss_A": 0,
"dec_miss_B": 0, "dec_n": 0,
# miss_A 细分decode 桶):时序=就绪集里没有它pread 没完成);
# 驱逐=就绪过pread 完成)但 acquire 前已不在池(被驱逐/没写进)。
"dec_miss_A_timing": 0, "dec_miss_A_evicted": 0}
def note_miss_attrib(uniq, predicted, resident, is_decode: bool, ready=None) -> None:
"""统计本层真实路由 uniq 的命中构成,分 prefill / decode 两桶累加。
uniq本层真实路由专家集合predicted本层预测集residentacquire 前驻留集;
ready本层最近 promote take 到的"已就绪(pread 完成)"专家集(用于细分 miss_A
供 host 路径与 GPU remap 路径共用,确保两条路径口径一致。
"""
pset = predicted or set()
rset = ready or set()
for e in uniq:
MISS_ATTRIB["routed"] += 1
if is_decode:
MISS_ATTRIB["dec_routed"] += 1
if e in resident:
MISS_ATTRIB["resident_hit"] += 1
if is_decode:
MISS_ATTRIB["dec_resident_hit"] += 1
elif e in pset:
MISS_ATTRIB["miss_A_predicted"] += 1
if is_decode:
MISS_ATTRIB["dec_miss_A"] += 1
# 就绪集里有它=pread 完成过却仍 miss → 驱逐;否则 pread 没完成 → 时序。
if e in rset:
MISS_ATTRIB["dec_miss_A_evicted"] += 1
else:
MISS_ATTRIB["dec_miss_A_timing"] += 1
else:
MISS_ATTRIB["miss_B_unpredicted"] += 1
if is_decode:
MISS_ATTRIB["dec_miss_B"] += 1
MISS_ATTRIB["n"] += 1
if is_decode:
MISS_ATTRIB["dec_n"] += 1
# 并集专家数探针(UNION_PROF=1):按本次前向的 seq 长度分桶,记每层路由专家"去重并集"大小。
# seq=K(MTP verify,如 K=3)的桶即"K 个 token 的专家并集",决定池 cap 下限;seq=1 为 decode、
# seq=chunk 为分块 prefill。值为 {seq: [sum_union, n_layer_calls]}。默认关、零开销。
UNION_PROF: "dict[int, list]" = {}
# 分层原始样本(UNION_PROF=1){seq: {layer_idx: [本层每次前向的并集大小...]}}。
# 仅聚合 [sum,n] 无法给出 cap 下限所需的 U_max/p99/分层分布,故额外留每层每次前向的原始值。
# 样本量极小(≤层数×token数),仅采集期占用,退出前汇总。
UNION_SAMPLES: "dict[int, dict[int, list]]" = {}
UNION_ON = config.union_prof()
def note_union(seq: int, union_count: int, layer_idx: int = -1) -> None:
seq = int(seq)
union_count = int(union_count)
e = UNION_PROF.setdefault(seq, [0, 0])
e[0] += union_count
e[1] += 1
if layer_idx >= 0:
UNION_SAMPLES.setdefault(seq, {}).setdefault(int(layer_idx), []).append(union_count)
def union_reset() -> None:
UNION_PROF.clear()
UNION_SAMPLES.clear()
def prof_reset():
for k in PROF:
PROF[k] = 0.0
def _tick(seg, t0):
mx.eval # noqa (占位,避免误用)
PROF[seg] += time.perf_counter() - t0