Semantic Router 在 ROCm 上的模型运行时延迟实测:单块 MI325X 对比同机 CPU
Semantic Router 在 ROCm 上的模型运行时延迟实测:单块 MI325X 对比同机 CPU
本技术记录基于语义路由器(semantic-router)内建模型运行时(built-in model runtime)的 ROCm 请求路径,完整复现并解读「同一路由器的五个模型分别跑在单块 AMD Instinct MI325X(gfx942)GPU 与同一批主机核的 CPU 上」的端到端延迟对比实验:环境、方法、两套镜像的完整结果表、成因分析以及可复现的命令全部在文中。读完你将掌握该项目的 ROCm 延迟门禁测量流程、router_latency.py 工具四个子命令的用法、ROCm 与 CPU 两侧的性能差异边界,以及把 use_cpu: true 改为 use_cpu: false 将模型部署到 GPU 的完整操作。
记录背景:为什么需要一条 ROCm 延迟记录
路由器的模型支撑信号(domain、prompt guard、PII、fact-check、feedback 等)运行在内建模型运行时中,该运行时可以把模型放到 GPU 上执行。而旧版(legacy)路由器对这些信号没有 ROCm 请求路径,因此这条记录要回答的问题是:运行时路由器把模型放到一块 AMD Instinct MI325X 上之后,端到端路由延迟相比同一个路由器把模型放在 CPU 上,到底快多少、慢在哪、稳定性如何。
它与此前两份记录构成完整对比三角:
- 模型级 ROCm 对比(针对旧版 AMD recipe,即 ONNX Runtime MIGraphX / ROCm EP)见 vela1-performance.md;
- 与旧版路由器的 CPU 对比见 router-latency-cpu.md(本记录的 CPU 侧基线文件 router-latency-cpu.yaml 即出自该记录)。
这份记录本身不引入新结论性论断,而是把「同一路由实例、同一输入语料、同一镜像家族」下的 GPU 与 CPU 两种放置方式放在同一台机器上交替测量,给出带 95% 置信区间的配对差异,并完整公开原始数据(router-latency-rocm.json)。
实验环境与设置
机器与资源划分
- 日期:2026-10-05。
- 机器:一台装有 AMD EPYC 9575F vCPU 与 AMD Instinct MI325X GPU(gfx942)的虚拟机。
- 资源钉扎:路由器容器(路由器及其运行时进程)通过
docker run --cpuset-cpus钉到 12 个 vCPU 上,每一轮的有效 cpuset 都读到同样的 12 个核;ROCm 侧额外获得一块 GPU。驱动进程钉到另外 4 个 vCPU。其他工作流的任务运行在其他核上;每次结果都会给出节点的 1 分钟负载(程序会废弃负载超过 120 时取得的计时)。
两套镜像栈
两条镜像均由 Dockerfile.extproc 构建(目标 extproc,ACCELERATOR=rocm),取自精确的镜像源:
- Shipped(随发布版本出货的镜像):staging
a580be6b9。其 ROCm 阶段使用 PyTorch 2.12.0+git6bbd260(AOTriton 0.13.50),以及来自vllm/vllm-openai-rocm@sha256:1fd21abe…(发布镜像的基础镜像)的 ROCm 7.2.3 用户空间,另加 Triton 3.7.0、fla-core0.5.2 与causal-conv1d1.7.0。运行时与路由器都位于a580be6b9。 - Stack B(先测量的一套):官方 PyTorch 2.12.0+rocm7.2 wheel(
TORCH_ROCM_INDEX指向 rocm7.2 索引)、HIP 7.2.53211、Triton 3.7.0、fla-core0.5.2,不带causal-conv1d,位于efc68a81c:路由器 IP3 头a85c57f6e加上f3c09121d(编码器图以线程本地模式捕获)。
关于 shipped 镜像的 ROCm 软件栈为何这样搭配,rocm-router-image.md 有更完整的说明:PyTorch、ROCm 库与 vLLM 的 ROCm 镜像逐字节一致,causal-conv1d 的 gfx942 机器码与发布镜像逐字节一致,且该镜像中 torch 没有 CPU LAPACK,所以 Qwen3.5 系解码器无法在此镜像的 CPU 上运行(运行时会在加载权重前直接拒绝这种放置)。
模型、配置与输入
- 模型:Vela 1.0 的 Domain、Guard、PII、FactCheck 与 Feedback,全部采用各自固定的 registry 修订版本,与 router-latency-cpu.md 一致。
- 配置:
- CPU 侧直接使用 router-latency-cpu.yaml,路由器为五个模型各起一个 CPU 进程,每个进程分得 12 个核中的一份;
- ROCm 侧使用同一份文件,仅把五个模型的
use_cpu: true改为use_cpu: false。路由器把五个模型放进同一个运行时进程,挂在rocm:0上(exactprofile、native 引擎)。该进程同一时刻只执行一个模型的设备工作(per-device lock);每个模型按长度桶(length bucket)重放 HIP 图,图在该桶第二次使用时以线程本地模式捕获,长行(long rows)则走 eager 执行。
- 输入:CPU 记录的同款语料文件(539 条 prompt,sha256
dc81026ad0c9…)。注意自551436917修改了三条 cache-polarity 测试 prompt 后,router_latency.py corpus在当前提交生成的语料在这三条上会不同,因此两份记录统一使用较早生成的语料文件。
测量方法
测量方法与 CPU 记录相同:客户端向 POST /api/v1/routing/preview 发送请求(该端点计算全部信号与决策,但不产生上游调用),由客户端记录延迟。每一轮先发 20 个 warm-up 请求,然后把语料连发三遍:先顺序,再分别以并发度 4 与 16 发送。路由器和运行时的两级结果缓存均关闭,因此每个请求都真实计算。
- 每一轮都启动一个全新的路由器容器;
- 共五轮,ROCm 与 CPU 交替进行,每轮轮换起始顺序;
- 每个指标取五轮的中位数,并给出每轮 ROCm − CPU 的配对差异及其 95% t 区间(由
router_latency.py rounds计算); - 每轮的原始摘要(含各轮的负载、cpuset、GPU 内存、运行时重启与设备故障)保存在 router-latency-rocm.json。
这套方法由 tools/router_latency.py 驱动实现。该工具是纯标准库的延迟门禁驱动,包含四个子命令:corpus(从仓库 E2E 测试数据中提取所有不同的用户 prompt 并追加边界用例,写出输入集)、run(把每个输入发给运行中路由器的 preview 端点并记录客户端延迟与路由结果)、compare(对比两份 run 文件并输出 p50/p95/p99、吞吐与决策不同的输入)、rounds(读取交错轮次的 r<N>-<side>.json,报告每侧每轮每个指标的中位数以及「新 − 基准」的配对差异与 95% t 区间,双侧 t 分位数按自由度内置在脚本中)。工具运行时的默认参数为 20 个 warm-up、语料重复 3 次、并发度 [4, 16]。
结果一:shipped 镜像(a580be6b9)
在每一轮中,ROCm 路由器与 CPU 路由器对 539 个输入中的 539 个都给出相同的路由决策。30 轮 pass 无一个请求失败,无运行时进程重启,也没有记录任何设备故障。唯一的 GPU 进程加载全部五个模型后占用 12.9–13.2 GiB GPU 显存。节点负载安静:1 分钟负载保持在 5 到 24 之间,容器外进程在一轮 pass 期间仅占用路由器 12 个 vCPU 中的 0.2–0.4。
五轮中位数(毫秒与每秒请求数)以及带 95% 区间的差异:
| Pass | Metric | CPU | ROCm | ROCm − CPU [95% CI] | ROCm 加速比 |
|---|---|---|---|---|---|
| Sequential | p50 | 22.9 | 8.7 | −14.3 [−14.7, −13.9] | 2.6× |
| p95 | 90.1 | 13.7 | −77.8 [−81.2, −74.4] | 6.6× | |
| p99 | 218.3 | 32.6 | −197.0 [−224.4, −169.5] | 6.7× | |
| req/s | 23.3 | 100.5 | +77.4 [+76.8, +78.1] | 4.3× | |
| Concurrency 4 | p50 | 105.0 | 29.8 | −75.8 [−77.7, −74.0] | 3.5× |
| p95 | 400.8 | 60.6 | −348.2 [−372.2, −324.2] | 6.6× | |
| p99 | 1,448.6 | 163.1 | −1,281.2 [−1,332.8, −1,229.6] | 8.9× | |
| req/s | 24.1 | 118.3 | +94.7 [+93.6, +95.8] | 4.9× | |
| Concurrency 16 | p50 | 551.9 | 125.5 | −427.1 [−445.6, −408.6] | 4.4× |
| p95 | 1,667.1 | 184.6 | −1,484.0 [−1,501.6, −1,466.4] | 9.0× | |
| p99 | 2,563.6 | 345.4 | −2,170.8 [−2,313.2, −2,028.4] | 7.4× | |
| req/s | 24.0 | 117.2 | +93.0 [+92.2, +93.8] | 4.9× |
每一个置信区间都完全落在 ROCm 占优的一侧。逐轮看,顺序场景 p50 在 ROCm 上为 8.65–8.72 ms,在 CPU 上为 22.8–23.6 ms。全部 pass 的原始数据都保存在 JSON 的 shipped_image 段中。
结果二:Stack B 镜像(efc68a81c)
同样的五轮交错测量(先测),跑在 Stack B 镜像上。每一轮 539/539 个输入决策一致,30 轮 pass 无请求失败、无运行时重启、无设备故障;GPU 进程占用 13.2–13.4 GiB 显存。期间节点负载较高:1 分钟负载在 60 到 83 之间,容器外进程占用路由器的 vCPU 0.2–0.6。原始数据在 JSON 的 stack_b_image 段。
五轮中位数(毫秒与每秒请求数)以及带 95% 区间的差异:
| Pass | Metric | CPU | ROCm | ROCm − CPU [95% CI] | ROCm 加速比 |
|---|---|---|---|---|---|
| Sequential | p50 | 30.8 | 9.8 | −20.2 [−25.1, −15.4] | 3.1× |
| p95 | 81.4 | 16.3 | −66.3 [−73.4, −59.2] | 5.0× | |
| p99 | 167.3 | 31.8 | −133.2 [−147.8, −118.7] | 5.3× | |
| req/s | 23.4 | 90.4 | +67.7 [+63.8, +71.6] | 3.9× | |
| Concurrency 4 | p50 | 116.6 | 31.0 | −83.7 [−104.1, −63.3] | 3.8× |
| p95 | 314.2 | 67.2 | −248.1 [−298.2, −198.0] | 4.7× | |
| p99 | 692.9 | 187.3 | −526.0 [−628.5, −423.6] | 3.7× | |
| req/s | 25.9 | 109.4 | +79.2 [+62.6, +95.7] | 4.2× | |
| Concurrency 16 | p50 | 551.6 | 135.5 | −418.1 [−494.2, −342.1] | 4.1× |
| p95 | 1,216.6 | 200.3 | −985.5 [−1,220.2, −750.9] | 6.1× | |
| p99 | 1,472.2 | 364.4 | −1,110.2 [−1,387.6, −832.8] | 4.0× | |
| req/s | 26.1 | 107.8 | +82.5 [+77.5, +87.5] | 4.1× |
所有区间同样完全落在 ROCm 一侧。逐轮看,顺序 p50 在 CPU 上为 30.8 / 35.1 / 31.8 / 23.7 / 27.9 ms,在 ROCm 上为 10.1 / 9.8 / 10.0 / 9.0 / 9.1 ms——GPU 行几乎不随节点负载波动,而 CPU 行会。ROCm 吞吐从 4 个调用方起稳定在约 108 req/s:单个 GPU 进程同一时刻只跑一个模型的设备工作,更多调用方只会排队。
Stack B + causal-conv1d,以及节点其他 GPU 的影响
Stack B 五轮之后,同一个路由器用加了 causal-conv1d 1.7.0 且模型卷内带 MIOpen 数据库的 Stack B 镜像(staging af71d5e82)与 Stack B 镜像交替各跑三轮 ROCm。两套镜像在每一轮都对 539/539 个输入给出相同决策,且每个点估计都偏向 af71d5e82,但大多数区间跨过零(无显著差异)。af71d5e82 在节点安静时的表现与 Stack B 轮次一致:顺序 p50 10.3–10.9 ms,16 个调用方下 104–105 req/s。
此时该节点另外四块 GPU 上有作业在跑,且它们的主机线程与路由器共享同一个 NUMA 节点,两套镜像都出现了间歇性慢阶段:某些子轮中顺序 p95 上升到 35–199 ms(平时只有 15–18 ms),或 16 个调用方下的速率掉到 29–70 req/s,同时路由器容器每个请求消耗的 CPU 时间达到平时的 1.5–3.5 倍;原因未被进一步隔离。那些作业暂停后,Stack B 镜像再跑四轮就恢复到轮次水平(顺序 p50 10.4–10.9 ms,16 个调用方 101–104 req/s),且运行时中的诊断钩子记录到每轮 41 次图捕获、无一失败。
因此,两组五轮数据反映的是其他 GPU 大多空闲的节点上的 GPU 路径表现;一个与其他 GPU 作业共享主机的路由器应当预期这种方差。这些 pass 记录在 JSON 的 stack_b_with_causal_conv1d 段。
CPU 侧测量的是什么
CPU 侧用的是 ROCm 镜像里的 PyTorch 在 CPU 上跑模型(shipped 镜像为 2.12.0+git6bbd260,Stack B 为 2.12.0+rocm7.2)——即某模型在 ROCm 镜像中被部署为 use_cpu: true 时实际得到的执行路径。它不是 CPU 镜像,且与 router-latency-cpu.md(CPU 部署的权威参考)有两处关键差异:
- 跨请求批处理:在 CPU 上,
exactprofile 只对「加载时探测发现单独与混批结果一致」的模型做并发请求批处理(设计文档 §9 与 §11)。在任一套 ROCm 镜像的 PyTorch 下,Domain、Guard、FactCheck 与 Feedback 都通不过探测、只有 PII 通过,所以五个模型里有四个在负载下只能一次应答一个请求;而同一棵代码树构建的 CPU 镜像(PyTorch 2.10.0+cpu)五个模型全部通过探测。 - 节点:CPU 行会随节点负载变化。在安静的节点上,shipped 镜像的 CPU 侧顺序 p50 为 22.8–23.6 ms。在相同核上做的两轮诊断(Stack B 的 CPU 侧与 CPU 镜像轮换、顺序轮转)中,CPU 镜像的顺序 p50 为 30.3 与 24.0 ms,16 个调用方下速率为 31.8 与 45.3 req/s;Stack B 对应为 37.7 与 50.5 ms、17.7 与 15.5 req/s。由于 Stack B 的 pass 期间节点负载更高(87–90 对比 69–72),这两轮无法把镜像与负载因素分开;数据保存在 JSON 的
cpu_side段。
值得强调的是:本记录与 vela1-performance.md 中的模型级数字相互印证——后者在单块 MI325X 上测出 Vela 1.0 各编码器相对旧 AMD recipe(ORT MIGraphX/ROCm EP,固定 8,192-token session)的 p50 加速在 68×–130× 量级;本文的端到端路由延迟数据则是在此之上的整机表现。
复现步骤
在构建好镜像、五个模型已进入运行时缓存的 ROCm 节点上,每一轮都在钉住路由器核的全新容器中运行,并从其他核驱动:
python3 tools/router_latency.py corpus --repo . --out corpus.json # 其中三条 prompt 与记录文件不同
sed 's/^\(\s*\)use_cpu: true$/\1use_cpu: false/' router-latency-cpu.yaml > router-latency-rocm.yaml
# 每轮按轮转顺序:启动一个路由器容器,运行,再移除。
# ROCm 侧示例(CPU 侧去掉设备标志、服务 router-latency-cpu.yaml):
docker run -d --name router-rocm --network host --cpuset-cpus 128-139 \
--device /dev/kfd --device /dev/dri --group-add video -e HIP_VISIBLE_DEVICES=6 \
-v "$PWD":/work:ro --entrypoint /usr/local/bin/router <image> \
--config=/work/router-latency-rocm.yaml --api-port 18080
taskset -c 140-143 python3 tools/router_latency.py run --url http://127.0.0.1:18080 \
--corpus corpus.json --out rounds/r1-rocm.json --label rocm-r1
# ... 相同方式把 CPU 侧写入 rounds/r1-cpu.json ...
python3 tools/router_latency.py rounds --dir rounds --base cpu --new rocm
复现要点:
- 语料差异:当前提交运行
corpus会因551436917的三条 prompt 改动而略有不同;若要与记录文件逐字节对齐,需使用较早的语料文件(记录中 sha256dc81026ad0c9…的 539 条输入)。 - 关闭缓存:与 CPU 记录相同,需要设置
VLLM_SRUN_RESULT_CACHE=0,并通过VLLM_SRUN_COMMAND以--result-cache-entries 0启动运行时,才能测到无缓存的真实计算延迟。 - 设备透传:
--device /dev/kfd --device /dev/dri --group-add video是 ROCm 容器访问 GPU 的标准透传方式,HIP_VISIBLE_DEVICES=6用于选定具体 GPU 序号。 - 生成配置:
sed替换把 router-latency-cpu.yaml 中五个模型的use_cpu: true全部翻转为use_cpu: false,得到 ROCm 侧配置;CPU 侧仍然服务原文件。
关键结论速览
- ROCm 全面优于 CPU:shipped 镜像下顺序 p50 2.6×、p95 6.6×、p99 6.7×,吞吐 4.3×;并发 16 下 p95 达到 9.0×。所有配对区间都完全落在 ROCm 一侧,全部行通过「无回归」标准。
- 决策一致性:两套镜像、每一轮、539/539 输入与 CPU 决策一致,无请求失败、无重启、无设备故障——GPU 加速不改变路由行为。
- GPU 路径对节点负载不敏感:CPU 行的延迟随节点负载明显波动,GPU 行几乎不动。
- 单进程串行设备工作的吞吐上限:五个模型共享一个运行时进程,同一时刻只跑一个模型的设备工作,ROCm 吞吐从 4 个调用方起稳定在 ~108 req/s(Stack B 为 ~108、shipped 略高)。
- 共享主机的方差:节点上其他 GPU 作业会让两套镜像都出现间歇性慢阶段;结论只在其他 GPU 空闲的节点上成立。
- CPU 侧的真实含义:ROCm 镜像的 CPU 路径受批处理探测失败影响,四个模型在负载下串行应答;CPU 部署请以 router-latency-cpu.md 为权威参考。
对想要把路由器模型信号部署到 AMD GPU 的团队,直接路线就是:以 ACCELERATOR=rocm 构建 Dockerfile.extproc 的 extproc 目标(默认 CAUSAL_CONV1D_ARCHS 覆盖 gfx90a/gfx942/gfx950),在配置中把目标模型的 use_cpu 置为 false,然后按上文命令透传 GPU 设备并启动;ROCm 加速器状态在 design.md §11 中被标记为 validated(MI300X、MI325X),配套的 BF16 autocast、FLA gated delta、causal-conv1d、exact-shape HIP 图与 gfx942 逐位一致融合内核均已就位。