☰
FreeToken MoE 离线推理基准完全指南:解码吞吐、权重加载与缓存拷贝的测量原理与实战用法
2026/11/2 1:18:30 网站建设 项目流程

FreeToken MoE 离线推理基准完全指南:解码吞吐、权重加载与缓存拷贝的测量原理与实战用法

本指南以仓库 benchmarks 目录下的三个基准脚本为主线,系统讲解 FreeToken 如何测量 MoE(Mixture-of-Experts)模型在 offload / cpu / hybrid 三种后端下的端到端解码吞吐、专家权重加载速度与逐层缓存拷贝成本,并介绍配套的ft bench bw带宽测试与后端自动选择机制。读完本文,你将掌握每个基准脚本的测量原理、全部命令行参数与输出字段的含义,并能在自己的机器上复现这些测量、利用结果指导 offload 缓存容量与 hybrid 取数策略的调优。

一、基准体系概览:四个工具的分工

FreeToken 的 MoE 离线推理性能基准由四个互相配合的工具构成,它们分别回答不同层面的性能问题:

工具脚本 / 命令测量对象是否需要 checkpoint
端到端解码吞吐bench_decode_moe.pybs=1 流式解码 tok/s,含完整 serving 路径是
权重加载时间bench_load_weight_generic.pyexpert-bank 从磁盘读入主机内存的时间是
缓存拷贝成本bench_offload_cache_copy.py每层解码ensure_experts+copy_missing的拷贝耗时否(合成数据)
带宽与后端选择ft bench bw(benchbw.py)主机 RAM / PCIe 带宽,生成引擎读取的 JSON 档案否

它们的共同运行约定是:从仓库根目录执行,设置PYTHONPATH=python:.,并固定到单张 GPU(CUDA_VISIBLE_DEVICES=0)。每个脚本的--help与模块 docstring 都包含完整的细节说明。

在深入各个脚本之前,先明确这些基准所处的架构背景:FreeToken 将 MoE 专家权重以"专家库(expert banks)"的形式驻留在主机内存(pinned memory),GPU 上只保留一个统一的专家槽缓存(slot cache)。解码时命中的专家从槽缓存直接计算,未命中的专家通过 PCIe 拷入缓存(copy_missing),或者交给 CPU 执行器计算——这正是 offload / cpu / hybrid 三种策略的差异所在。因此,解码吞吐、加载速度和拷贝成本构成了这条离线推理链路最核心的三个性能维度。

二、bench_decode_moe.py:真实 serving 路径下的单流解码吞吐

2.1 测量思路:客户端视角而非引擎视角

bench_decode_moe.py 测量的是已经部署好的 MoE 模型在单请求(bs=1)下的解码 tok/s。它不走引擎内部接口,而是对每个后端实际拉起一个ft serve进程,向/v1/chat/completions发送带stream=true的请求,并逐事件打时间戳。因此数字天然包含调度器(scheduler)、去分词器(detokenizer)和 HTTP/SSE 一跳的完整开销——即客户端真实感知的吞吐,而非裸引擎 forward 的数值。

在 bs=1 下,服务端每个解码步发出一个 delta 事件,最后一个 chunk(stream_options.include_usage)带有精确的 token 计数,于是吞吐按下式计算:

decode_tok_s = (completion_tokens - 1) / (t_last_event - t_first_event)

这个公式即使去分词器把多个 token 合并进一个事件(如多字节字符)也依然正确,因为窗口锚定在第一个与最后一个 token 的到达时刻。ignore_eos=True保证步数精确等于设定的D,不受采样影响。TTFT 是本次测量中热机后的首个 token 延迟(含模板渲染与 prefill)。

需要说明的是,引擎内部诊断信息(如 expert 缓存 miss rate、hybrid 取数拆分)不会通过 API 暴露,因此该脚本不报告这些指标;VRAM 取的是服务端/v1/stats的实时值。

2.2 命令行参数

参数默认值说明
--model必填checkpoint 目录(或.ftw)
--backendoffload逗号分隔的offload\|cpu\|hybrid,每个后端一个独立服务进程
--aime$FREETOKEN_AIME25_JSONL本地 jsonl 文件,替代从 Hub 下载 AIME-25 数据集
--problem00 起始的 AIME 题目索引
--decode256要测量的解码 token 数(D)
--cache0GPU 专家槽数;0= 根据空闲 VRAM 自动定容
--cache-rateNone缓存槽数取 L×E 的比例(--cache的互斥替代)
--hybrid-fetch-1hybrid 后端每层最大 PCIe 取数;-1= 自动(按实测 pcie/cpu 带宽比例)
--mem-ratio0.9目标 VRAM 利用率
--gpuNone服务所用 GPU(UUID 或 nvidia-smi 索引,同ft serve --gpu)
--no-graphFalse关闭 CUDA graph,改用 eager 解码
--greedyFalse强制 temperature 0(忽略 checkpoint 采样),使 token id 可跨后端比较
--server-timeout1800等待服务就绪的秒数
--jsonNone把结果行追加到指定 JSON 文件(JSONL)

2.3 提示词与采样:保证路由代表性

提示词选用AIME-25 竞赛题,以聊天消息形式发送并开启 thinking——这是一个真实的推理负载,专家路由因此具有代表性。题目默认从 Hub 上的math-ai/aime25数据集(test.jsonl)下载到常规 HF 缓存,首次运行触发;--aime可指向本地 jsonl。load_problem()同时兼容 Hub 的problem字段与本地已预格式化 jsonl 的prompt字段;若文本中没有\boxed{},会自动追加"Please reason step by step, and put your final answer within \boxed{}."指令——boxed 答案也便于肉眼抽查结果。

采样参数优先采用 checkpoint 的generation_config.json推荐值,未指定的字段回退到FALLBACK_SAMPLING = {"temperature": 1.0, "top_p": 0.95, "top_k": 64}(见脚本 L61-L71)。为什么要在客户端解析并显式发送?因为服务端对未指定字段的默认是贪婪解码(temperature 0、无过滤),会让没有完整采样建议的 checkpoint 在推理负载下悄悄降级。resolve_sampling()还会处理两个边界:do_sample=False或temperature==0.0时返回贪婪参数;HF 用top_k=0表示"不过滤",而 API 用-1,需要换算(L143-L162)。--greedy则强制 temperature 0 / top_p 1.0 / top_k -1,用于更严格的跨后端对比。

2.4 服务端启动参数映射

每个后端由脚本以子进程拉起ft serve(serve_cmd()),关键映射如下:

  • --moe-backend <backend>:指定 offload / cpu / hybrid;
  • --max-running-requests 1:保证单流;
  • --max-seq-len-override 8192+D:为提示词与 D 个解码 token 留足序列长度;
  • --memory-ratio <mem_ratio>:目标 VRAM 利用率;
  • --cuda-graph-max-bs 1(--no-graph时为0):解码走 CUDA graph;
  • --moe-hybrid-max-fetch <hybrid_fetch>:hybrid 取数上限;
  • 缓存容量三选一:--moe-cache-size(--cache > 0)、--moe-cache-rate(--cache-rate指定)、--moe-cache-auto(默认)。

脚本先轮询/health直到maintenance == "serving",再查询/v1/models拿到 model id,然后执行两次stream_generate():第一次用于把 expert 缓存热到稳态解码工作集,第二次才是被测量的请求。

2.5 输出字段与跨后端校验

每个后端生成一行结果,关键字段包括:decode_tok_s(解码吞吐)、ms_per_token、event_ms_p50/p99(SSE 事件间隔分位数)、ttft_ms(热 TTFT)、prompt_tokens、vram_gib、output_sha1(输出文本前 12 位哈希)等(L350-L367)。

output_sha1是跨后端一致性检查:非贪婪模式下每个服务进程内是确定性的(全新服务 + 固定请求序列),因此同模型三个后端应产生相同哈希。不过 token id 不通过 API 暴露,这是比旧版进程内 id 哈希更弱的约束(脚本 docstring 中明确说明)。--json out.json将每行结果以 JSONL 追加,便于批量对比。

若指定多个后端,单个后端失败(包括解码中途服务崩溃导致的连接断开)不会中止其余后端,脚本会记录失败列表并最终以非零退出码返回;每个后端结束后通过SIGTERM(90 秒宽限)再SIGKILL(30 秒)整组进程,并额外 sleep 3 秒等待驱动回收 VRAM,避免污染下一个后端的测量(stop_server())。

# 单个后端 CUDA_VISIBLE_DEVICES=0 PYTHONPATH=python:. python benchmarks/bench_decode_moe.py \ --model /path/to/model # 三个后端各起一个服务,结果写入 JSON ... --model /path/to/model --backend offload,cpu,hybrid --json out.json

三、bench_load_weight_generic.py:专家库加载时间对比

3.1 三种加载模式

bench_load_weight_generic.py 测量 MoE 启动阶段的主导成本——把离线专家库从磁盘读入主机内存——并对比三种方式:

  • baseline(串行):load_expert_banks(...)默认串行加载器(scattered read + pin);
  • parallel(并行 O_DIRECT):load_expert_banks(..., parallel=True),对原始 checkpoint做分块多线程 O_DIRECT 读取,无需重打包;
  • ftw:一次性把模型转换为 FTW(FreeToken 统一的整模型格式,含稠密权重与专家),之后从重打包后的 FTW 读取(内部走 load_ftw_banks);转换构建成本单独报告。

三种模式都走框架真实的load_expert_banks代码路径,没有脚本内自实现的读取器,且返回相同的ExpertBanks.sources,校验和必须与 baseline 一致。该基准是 Linux-only:FTW 默认暂存在/var/tmp/ftw_<模型名>(--ftw-dir可覆盖,体积约为 checkpoint 大小),放在/var/tmp是因为磁盘空间大且不受/tmp清理器清扫;目录按模型名隔离,避免不同模型并发基准相互覆盖。

3.2 关键设计:子进程隔离与冷读保证

每个模式跑在独立的子进程中,这样其约一个 bank 大小的 pinned 内存会在下一个模式开始前被回收——两套完整 bank 同时驻留会导致 OOM。每个 worker 通过 stdout 输出@@RESULT@@前缀的 JSON 行,stderr 继承终端以便 tqdm 进度条与 INFO 日志实时可见(L239-L254)。

默认情况下每个模式在读取前先用fadvise DONTNEED驱逐页缓存(_evict_cache(),无需 root),确保每次读取测的是真实磁盘——否则 baseline 的 mmap 会命中热缓存,显得虚快。--no-drop-cache可关闭驱逐以做热缓存对比。

内存指标由后台MemSampler线程采样:进程峰值 RSS(读/proc/self/status的 VmRSS)与系统MemAvailable最低水位(读/proc/meminfo)。

3.3 校验和:位置加权采样哈希

_checksum()对每个 bank 以全局字节偏移加权采样约 100 万字节并折叠进哈希(L103-L133)。加权是必要的:普通求和是置换不变的,而并行 / FTW 读取器恰恰可能在专家重排上出错。偏移跨每层张量全局连续,因此哈希与 bank 的切分方式无关(一个扁平[L*E, ...]张量还是逐层列表),且计算时不物化拼接副本,不会抬高本基准报告的峰值 RSS。脚本 docstring 也坦承这是抽样而非证明:此规模下跨步达数十 KiB,小于该跨步的局部损坏可能漏检。

3.4 命令行参数与结果表格

参数默认值说明
--model必填原始 checkpoint 路径
--modesparallel,ftw逗号分隔的待测模式(baseline 总是作为参考先跑)
--workers8并行读取线程数
--chunk-mib8并行读取的分块大小(MiB)
--shard-gib8.0FTW 构建的 shard 大小上限(GiB)
--ftw-dir""FTW 暂存目录(默认/var/tmp/ftw_<model>)
--no-drop-cacheFalse不驱逐页缓存(热比较)
--keep-ftwFalse跨次运行保留并复用已转换的 FTW 目录
--gpu首块可见 GPUGPU UUID 或 nvidia-smi 索引

结果输出为一张对比表:mode / load_s / read GiB/s / peakRSS / speedup / bytes。speedup 相对 baseline 计算,每个非 baseline 行的 checksum 与 baseline 比较,不一致标记为BAD并最终以非零退出码结束——保证脚本化运行(或 CI)不会把字节不一致当作成功。--keep-ftw未指定时,结束时 FTW 目录会被删除。

PYTHONPATH=python:. python benchmarks/bench_load_weight_generic.py --model /path/to/model ... --modes parallel,ftw # 子集模式(baseline 始终作为参考) ... --keep-ftw # 跨次运行保留转换后的 FTW

3.5 底层加载器逻辑

load_expert_banks的实现在 python/freetoken/moe/expert_banks.py#L275-L368,它提供两条路径:

  • FTW 快速路径:若model_path是已转换的 FTW checkpoint,直接连续分块 O_DIRECT 读取重打包后的 banks,不做自动转换;
  • 慢速路径(原始 checkpoint):自动选择parallel(当专家以小张量散存时,串行读取很慢;该判断来自experts_scattered())或serial(专家已打包时串行即可饱和,并行只会带来读放大)。parallel=None表示自动选择(生产默认),True/False强制指定(供本基准与转换器使用)。并行读取器在低内存机器上会因额外的整 shard 匿名缓冲而 OOM,因此自动模式还会检查空闲内存是否足以覆盖 banks 加一个 shard 的瞬时占用,不足则回退串行(--expert-load serial/parallel可强制)。

四、bench_offload_cache_copy.py:合成数据下的逐层解码拷贝成本

4.1 目的:聚焦拷贝路径本身

bench_offload_cache_copy.py 不需要任何 checkpoint——它用合成张量直接驱动 MoE 缓存的真实内核,测量每层解码的完整移动成本:ensure_experts(LRU 槽重映射 + 逐 bank 的fast_index_copy_jitgather)。它把 batch size 与 miss rate 作为变量扫描,tok_ms列把单层拷贝成本外推到整个模型——假设所有 MoE 层具有相同的 miss profile。

CUDA_VISIBLE_DEVICES=0 PYTHONPATH=python:. python benchmarks/bench_offload_cache_copy.py

4.2 模型档案与 bank 布局

脚本内置一组模型档案(L36-L50),覆盖多种量化格式与几何形状:

档案层数 L专家数 Etop-k量化格式H / I
qwen3.5-35B402568bf162048 / 512
qwen3-30B481288bf162048 / 768
gemma4-26B301288bf162816 / 704
minimax-m2.5-marlin622568nvfp4_marlin3072 / 1536
minimax-m2.5-triton622568nvfp43072 / 1536
glm4.7-nvfp4891608nvfp45120 / 1536
gpt-oss-20b24324mxfp4_triton2880 / 2880
gpt-oss-120b361284mxfp4_triton2880 / 2880

每个档案的逐 bank 形状由量化格式的 bank schema 推导(bank_specs()),schema 的唯一权威定义在 offload_cache.py 的_BANK_SCHEMAS:例如 bf16 是(gate_up, down)两个 bank;nvfp4 是六个 bank(packed 码 + 每 16 块 e4m3 scale + 每输出行 fp16 global);mxfp4_triton 是六个转置 bank(_blocks/_scales/_bias)。缓存机制本身与布局无关,只负责搬行——这正是脚本可以按 schema 合成任意格式张量的原因。

4.3 状态准备与计时方法

prepare_state()构造"每个槽都归其他层所有"的初始状态(id 空间是扁平的:id == layer_id * num_experts + expert),保证驱逐要做真实工作而本层从不命中。miss 数按active_unique × miss_rate取整,其中active_unique = min(E, batch_size × topk)。计时用 CUDA event 夹住ensure_experts+copy_missing,每档重复 25 次取中位数(--repeat)。

4.4 参数与输出

参数默认值说明
--gpu首块可见 GPUGPU UUID 或 nvidia-smi 索引
--repeat25每档重复次数(取中位数)
--models全部档案待测档案名列表
--cache-slots[E, 0.4·L·E]绝对槽数(默认两档:单层专家数 E,以及 0.4×L×E 的目标场景)
--batch-sizes[1, 4, 16, 32]批量大小扫描
--miss-rates[0.0, 0.25, 0.5, 1.0]miss rate 扫描

每档输出一行:bs / active / miss_rate / misses / time_ms / copy_MiB / bw_GBps / tok_ms,其中copy_MiB = misses × 每专家字节数,bw_GBps是拷贝带宽,tok_ms = time_ms × L外推整个模型每个解码 token 的拷贝成本。

值得注意的是脚本仅实际驱动LAYER_ID=0,但缓存接线完全按真实契约(每个 bank 的bank_sources是逐层张量列表,只填充索引 0),因此测到的是真实内核路径。底层对应OffloadMoeCache(python/freetoken/moe/offload_cache.py)的ensure_experts与copy_missing;拷贝还可通过环境变量FREETOKEN_FUSED_COPY=0强制关闭多 bank 融合拷贝,走逐 bank 旧路径做 A/B 对比。

五、ft bench bw:主机带宽测量与 offload / hybrid 后端决策

5.1 为什么需要它

解码 miss 时专家走 PCIe(offload 路径)还是 CPU 计算(hybrid 路径)是一个带宽主导的决策。ft bench bw(python/freetoken/moe/benchbw.py)在本机实测两条路径的真实带宽,并把结果写成 JSON 档案供引擎读取,从而在--moe-strategy auto下自动选择后端、并为 hybrid 设定取数拆分比例。

5.2 两层测量

  • 上限测量(纯硬件、与模型无关):STREAM 风格的 CPU DRAM 读取,以及线性 pinned↔device 拷贝,给出快速上界;
  • 真实内核测量(按工作负载 + 专家 dtype):实际 CPU MoE GEMV(CpuMoeExecutor,即 hybrid 在 CPU 上计算的内容)与实际 PCIe gather(OffloadMoeCache.copy_missing/fast_index_copy,即 offload 在解码 miss 时流式传输的内容)。两者都可用时,还会重叠测量(并发运行、争抢主机 DRAM),这两条达成带宽即设定 hybrid 的逐步取数拆分(load_hybrid_fetch_fraction)。

决策规则:按(工作负载, 格式),当 CPU MoE 内核带宽超过threshold倍(默认 2x)的 PCIe gather 带宽时推荐hybrid,否则offload。没有 CPU MoE 权重路径的格式(如 block-fp8)无法在 CPU 上计算,恒定为offload。

5.3 用法与输出

ft bench bw # 默认:全部格式的 per-dtype 调优基准 ft bench bw --dtype nvfp4,bf16 # 只测这些格式 ft bench bw --model qwen3.6-moe # 改为按模型几何测量 ft bench bw --dtype all --model all # 两者都做 ft bench bw --gpu GPU-2f3a... # 指定 GPU(UUID 或 nvidia-smi 索引)

结果写入每 GPU 一个的 JSON 档案,默认路径$XDG_CACHE_HOME/freetoken/benchbw/<gpu-uuid>.json(多 GPU 机器每卡一份,旧版无 uuid 的benchbw.json亦被兼容)。读取侧是无 torch 依赖的 bench_profile.py,引擎通过它加载档案。ft bench bw还支持--dtype/--model/--threshold等参数透传(daemon 的/benchmark/start端点也会在引擎主机上运行它以生成档案,见 daemon/app.py)。相关 CLI 入口在 cli.py。

5.4 档案如何驱动引擎配置

引擎在 engine/config.py 与 engine.py 中消费该档案:

  • moe_strategy="auto"将 MoE 模型解析到 offload 家族,当ft bench bw档案推荐 hybrid 时采用 hybrid;resident 的fused专家则必须显式请求(server/args.py 的 --moe-strategy);
  • --moe-hybrid-max-fetch -1(默认)时,每步按档案中实测的 pcie_bw/cpu_bw 比例取数:理想重叠要求"PCIe 取数专家数 : CPU 计算专家数 = pcie_bw : (cpu_bw - pcie_bw)"(engine.py L747-L764);没有可用档案时回退到固定上限 1;
  • 缓存的档案还可以把配置从 offload 升级到 hybrid(engine.py L1617-L1659),前提是 CPU MoE 执行器可用。

此外,offload_cache.py 中的 hybrid 字段(hybrid_max_fetch、hybrid_fetch_fraction)正是上述决策在运行时的落点:ensure_experts_hybrid只给本步最多hybrid_max_fetch个 miss 专家分配槽位并调度拷贝,溢出部分改写为-1交给 CPU 执行器,num_indices与num_missing_full分别记录取数上限内与未封顶的 miss 数供统计。

六、组合使用建议

四个工具正好覆盖"运行前、启动时、解码中"三个性能阶段,可串成一条完整的调优流水线:

  1. 先用ft bench bw建立本机带宽基线:结果写入每 GPU 的 JSON 档案后,--moe-strategy auto即可自动决定 offload / hybrid,并让--moe-hybrid-max-fetch -1得到正确的取数比例;
  2. 用bench_load_weight_generic.py评估启动成本:对比串行、并行 O_DIRECT 与 FTW 三种加载路径,若 FTW 明显更快且可接受一次性转换开销(--keep-ftw复用),可考虑在生产中以 FTW 格式存放模型;校验和机制保证任何重排错误都会被当场捕获;
  3. 用bench_offload_cache_copy.py选缓存容量与理解拷贝瓶颈:不需要 checkpoint 即可扫描 batch size × miss rate × 槽数组合,tok_ms直接给出 miss 对单 token 延迟的贡献,指导--moe-cache-size/--moe-cache-rate的取舍(默认槽数E与0.4·L·E两档即是轻量级与目标场景的代表);
  4. 最后用bench_decode_moe.py做端到端验收:对每个后端起真实服务、以 AIME-25 推理负载测量客户端可见的 tok/s 与 TTFT,--json落盘、output_sha1校验跨后端一致性,--greedy可做更严格的对照。

需要提醒的边界条件:加载基准仅限 Linux(依赖 O_DIRECT 与 fadvise);bench_decode_moe.py的 AIME 数据首次运行需联网下载(可用--aime指向本地 jsonl 规避);bench_offload_cache_copy.py与ft bench bw需要可用 CUDA 设备。所有基准都应固定 GPU 并确保无人共享该卡,否则吞吐与带宽数字不具备可比性。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询