exo 如何配置并运行 prefill/decode 分离基准测试?instance-links 与 prefill-decode.toml 实战
【免费下载链接】exoRun frontier AI locally.项目地址: https://gitcode.com/GitHub_Trending/exo8/exo
如果你在 exo 集群上想让 prefill(提示词处理)和 decode(逐 token 生成)跑在不同机器上,并量化这种分离带来的延迟变化,仓库里的 bench/prefill_decode_bench.py 就是现成的工具:它在集群里启动两个 MLX 实例,通过/v1/instance-links接口把一个标记为 Prefill 源、另一个标记为 Decode 目标,然后向/bench/chat/completions端点发请求测速。本文围绕一条可照做的路径展开:先确认集群与模型就位,再配置 bench/prefill-decode.toml 或用命令行参数运行,最后核对输出并了解脚本的自动清理行为。
原理与边界:这个脚本在做什么
bench/prefill_decode_bench.py 的模块说明把整个流程交代得很清楚:
- 在集群上创建 prefill 实例和 decode 实例,并分别等待其就绪;
- 调用
POST /v1/instance-links,请求体为{"prefill_instances": [prefill_instance_id], "decode_instances": [decode_instance_id]},把两个实例建立链接; - 向 decode 实例发送 chat completion 请求。master 会把请求路由到 decode 实例并打上指向 prefill 实例的
prefill_endpoint标记,worker 按请求自行判断是否把 prefill 发到远端; - 每完成一轮,在
finally块中删除该 instance link 并删除两个实例——也就是说脚本结束时集群会恢复到没有这两个实例的状态,无需手工清理。
判断"是否真的走了远端 prefill"的阈值在引擎代码里:src/exo/worker/engines/mlx/generator/batch_generate.py 定义了REMOTE_PREFILL_MIN_TOKENS = 1000,未命中缓存的 token 数超过它才会触发远端 prefill。这也是脚本强制--pp必须大于 1000 的原因,直接传--pp 512会报错退出:
pp=512 must be >1000 (remote prefill triggers when uncached >1000)测试请求走的是 docs/METHODOLOGY.md 描述的/bench/chat/completions端点,与普通 chat completion 有三点不同:KV prefix cache 默认关闭,每个请求都从冷缓存开始,保证 prefill 计时不受先前请求影响;EOS token 被 logits processor 屏蔽,模型必须正好生成max_tokens个 token,避免提前收尾影响 TPS 对比;不解析模型输出,只拼接原始 token 文本。prompt 长度则是用 tokenizer 的apply_chat_template()做二分搜索,把重复的 atom 字符串凑到目标 token 数,实际 token 数会作为pp_tokens记录在每条结果里。由于 chat 模板的开销,非常小的 pp 可能构造不出精确长度。
前置条件
- 一个已在运行的 exo 集群,master API 可通过
http://<host>:52415访问(基准脚本默认端口 52415,可用--host、--port或环境变量EXO_HOST/EXO_PORT覆盖)。 - 要测试的模型已存在于集群的模型列表中,脚本会先通过
GET /models解析模型短名(name)或 HuggingFace 全名(hugging_face_id);都不匹配时直接抛Model not found in /models: <id>。模型不在列表中时,加--force-download会让脚本通过POST /models/add从 HuggingFace 添加并触发下载。 - 集群中至少有两个节点上存在该模型的有效 placement。脚本在同模型场景下会挑选两个节点互不相同的 placement 分别作为 prefill 与 decode;不满足时退出并打印
Need at least two distinct-node MLX placements for the same model.。 - placement 需要满足
--instance-meta(ring/jaccl/both,默认both)与--sharding(pipeline/tensor/both,默认both)的过滤条件,以及--min-nodes/--max-nodes(默认 1/4)的节点数范围。 - 运行环境支持
uv run python。脚本依赖仓库内的 tools/src/exo_tools 里的ExoClient与 harness 工具函数。
先用 --dry-run 确认 placement 选择
--dry-run只列出脚本选中(或即将选择)的 placement 就退出,不会创建实例,适合在正式跑之前确认:
uv run python bench/prefill_decode_bench.py \ --model mlx-community/gpt-oss-20b-MXFP4-Q8 \ --pp 4096 --tg 512 \ --instance-meta ring \ --sharding pipeline \ --min-nodes 1 --max-nodes 1 \ --dry-run脚本会打印两行关键日志,确认 prefill 与 decode 分别落在哪个节点、哪个 instance id 上(下面是脚本日志的格式示例,节点名与 id 以你的集群为准):
PREFILL: ring / nodes=[...] (mike) / gpt-oss-20b-MXFP4-Q8 (mlx-community/gpt-oss-20b-MXFP4-Q8) / instance_id=... DECODE: ring / nodes=[...] (james) / gpt-oss-20b-MXFP4-Q8 (mlx-community/gpt-oss-20b-MXFP4-Q8) / instance_id=...如果不指定--prefill-node/--decode-node(节点 friendly name,可用集群 state 中的nodeIdentities核对),脚本自动从同一模型的 placement 里挑两个节点不同的组合;只要指定了其中一个,另一个侧仍会从全量 placement 里选。
配置 prefill-decode.toml
仓库自带一份 bench/prefill-decode.toml,顶层键作用于整个基准,[prefill]与[decode]两节分别设置两侧的 placement 过滤条件(instance_meta、sharding、min_nodes、max_nodes)以及可选的按侧model。文件头部注释给出的用法就是:
uv run python bench/prefill_decode_bench.py --config bench/prefill-decode.toml仓库中这份配置的原样内容如下——注意node = "mike"/node = "james"、host = "james"都是作者集群的示例值,使用前要替换成你集群的实际节点名与 master 地址:
# Prefill/Decode disaggregation benchmark config. # # Top-level keys are bench-wide. [prefill] and [decode] sections set per-side # placement filters and (optionally) per-side model. host = "james" port = 52415 timeout = 7200.0 settle_timeout = 60.0 # Workload pp = [4096] tg = [512] repeat = 1 warmup = 0 json_out = "bench/prefill_decode_results.json" [prefill] model = "mlx-community/gpt-oss-20b-MXFP4-Q8" node = "mike" instance_meta = "ring" sharding = "pipeline" min_nodes = 1 max_nodes = 1 [decode] model = "mlx-community/gpt-oss-20b-MXFP4-Q8" node = "james" instance_meta = "ring" sharding = "pipeline" min_nodes = 1 max_nodes = 1各键的实际作用(对照 bench/prefill_decode_bench.py 的参数定义):
host/port/timeout/settle_timeout:API 地址、HTTP 超时(默认 7200 秒)、等待集群产出有效 placement 的最长秒数(默认 60 秒,0 表示只试一次)。集群刚启动、placement 还没稳定时,这个等待尤其有用。pp/tg:prompt token 提示长度与生成长度,支持列表;两个列表等长时按 zip 成对执行(tandem 模式),不等长时自动变成全组合(product 模式),也可用--all-combinations强制全组合。repeat/warmup:每个 (pp, tg) 对的重复次数(必须 ≥ 1)与预热次数,warmup 复用第一个 pp/tg 对,不计入结果。json_out:逐次运行原始结果的 JSON 输出路径,脚本默认值就是bench/prefill_decode_results.json;--stdout可改为直接打印。[prefill]/[decode]的model:两侧可以不同模型;node是节点 friendly name;instance_meta、sharding、min_nodes、max_nodes只作用于该侧。
配置与命令行的关系:CLI 参数优先于 toml。脚本先把 toml 里的model、pp、tg注入命令行参数以通过必填校验,再把其余顶层键合入参数(仅在参数取默认值时生效)。所以--pp、--tg、--repeat等直接传在命令行上时,会覆盖 toml 里的值,不用改文件。
运行基准并核对输出
一条最短主路径,全部用命令行参数:
uv run python bench/prefill_decode_bench.py \ --host <你的master地址> --port 52415 \ --model mlx-community/gpt-oss-20b-MXFP4-Q8 \ --prefill-node mike --decode-node james \ --pp 4096 --tg 512 --repeat 3 --warmup 1 \ --instance-meta ring --sharding pipeline \ --min-nodes 1 --max-nodes 1 \ --json-out bench/prefill_decode_results.json其中<你的master地址>、节点名mike/james和模型 id 都是需要按你的集群替换的值;模型 id 可用GET /models返回的短名或 HuggingFace 全名。
执行过程中按顺序会看到这些日志,可作为检查点:
PREFILL:/DECODE:两行 placement 摘要(与--dry-run输出相同);- Planning 阶段:脚本检查各节点磁盘空间、必要时触发模型下载并等待完成。模型已缓存则直接跳过。如果磁盘不足,脚本会报错提示
Insufficient disk on <node_id> ... Use --danger-delete-downloads to free space.; Creating prefill instance...→Prefill instance ready,随后Creating decode instance...→Decode instance ready;Creating prefill instance link之后打印Link created: <linkId>。如果链接没出现在集群 state 里,脚本报Link did not appear in state.并退出;=== phase: disaggregated ... ===,每个 (pp, tg) 对跑完后打印均值行:prompt_tps、gen_tps、prompt_tokens、gen_tokens、peak_memory、avg_elapsed。
结束时脚本在finally块中删除 instance link 并删除两个实例,等待其从 state 中消失后,把逐次运行结果写入--json-out指定的文件。JSON 中每条记录包含elapsed_s、output_text_preview(前 200 字符)、stats(prompt_tps、generation_tps、prompt_tokens、generation_tokens、peak_memory_usage)、pp_tokens实际 token 数、phase,以及两侧模型、instance id 与节点等元数据。
可选分支:用 --compare-baseline 对照 decode_alone
加--compare-baseline后,脚本会多跑基线:先跑prefill_alone阶段(把请求发到 prefill 模型自身),再在正式disaggregated阶段后删除 link 与 prefill 实例,跑decode_alone(decode 实例自己完成 prefill),最后打印三阶段对照表并计算相对加速比(文档示例格式,数值以你的运行结果为准):
──────────────────────────────────────────────────────────────── pp=4096 tg=512 ──────────────────────────────────────────────────────────────── phase elapsed prompt_tps gen_tps disaggregated 12.34s 331.9 42.10 decode_alone 20.87s 196.1 41.80 prefill_alone 15.02s 272.7 43.00 speedup vs decode_alone: 1.69x speedup vs prefill_alone: 1.22x ────────────────────────────────────────────────────────────────注意该分支会让脚本额外删除 prefill 实例(日志会打印Removing link and prefill instance to isolate decode_alone.),这是基线测量的必要步骤;不带该参数时 prefill 实例会一直存活到脚本收尾。
排查与限制
- 找不到 placement:
No placement on prefill node '...'/No placement on decode node '...'或No placement found for prefill model <id>,说明过滤条件(instance_meta/sharding/节点数范围/指定节点)在该侧没有命中。先放宽--instance-meta both --sharding both,或先跑--dry-run看脚本实际选中了什么。 - 磁盘不足:报错信息里会给出需要多少 GB、现有多少 GB。加
--danger-delete-downloads会按从小到大删除节点上已下载的模型腾空间,这会真实删除节点上的模型文件,只在确认可以丢这些模型时启用。 - pp 太小的请求不会触发远端 prefill:阈值是
REMOTE_PREFILL_MIN_TOKENS = 1000(见 src/exo/worker/engines/mlx/generator/batch_generate.py),脚本对pp <= 1000直接拒绝,跑分离场景时保持 pp 在 1000 以上。 - 模型解析失败:
Model not found in /models时检查模型名是否在GET /models列表里;不在列表且可联网时可用--force-download让脚本经POST /models/add拉取。 - 结果数值不要当固定预期:上文所有日志与表格均为脚本打印格式或文档示例,实际 TPS、耗时、内存以你的硬件与模型为准;bench/METHODOLOGY.md 中的 TPS 口径(
generation_tps = (completion_tokens - 1) / gen_span,首 token 不计入分子)也意味着 tg=1 不可用。 - 清理是自动的,但幂等性有限:脚本结束时删除 link 与实例是收尾动作的一部分,若中途被强杀,残留实例需要自行通过
DELETE /instance/{instance_id}清理。
继续深入
- bench/METHODOLOGY.md:prompt 构造、计时口径、prefix cache 模式与输出格式,写分析脚本前值得通读一遍。
- docs/api.md:
/instance、/instance/await、/bench/chat/completions等端点的请求与响应说明;instance-links 的四个端点(GET/POST/v1/instance-links、PUT/DELETE/v1/instance-links/{link_id})在 src/exo/api/main.py 中注册,状态结构定义在 src/exo/shared/types/instance_link.py。 - bench/exo_bench.py 是面向单实例各 placement 配置的通用基准工具,与本脚本共用
PromptSizer、parse_int_list等工具函数,可对照使用。
【免费下载链接】exoRun frontier AI locally.项目地址: https://gitcode.com/GitHub_Trending/exo8/exo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考