Semantic Router 在 ROCm 上的模型运行时延迟实测:单块 MI325X 对比同机 CPU

原创2026-10-09 16:31:511,445 阅读
文章标签:后端API网关模型推理服务AI Agent

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 上,到底快多少、慢在哪、稳定性如何。

它与此前两份记录构成完整对比三角:

这份记录本身不引入新结论性论断,而是把「同一路由实例、同一输入语料、同一镜像家族」下的 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-core 0.5.2 与 causal-conv1d 1.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-core 0.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 上(exact profile、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 上,exact profile 只对「加载时探测发现单独与混批结果一致」的模型做并发请求批处理(设计文档 §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 改动而略有不同;若要与记录文件逐字节对齐,需使用较早的语料文件(记录中 sha256 dc81026ad0c9… 的 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 侧仍然服务原文件。

关键结论速览

  1. ROCm 全面优于 CPU:shipped 镜像下顺序 p50 2.6×、p95 6.6×、p99 6.7×,吞吐 4.3×;并发 16 下 p95 达到 9.0×。所有配对区间都完全落在 ROCm 一侧,全部行通过「无回归」标准。
  2. 决策一致性:两套镜像、每一轮、539/539 输入与 CPU 决策一致,无请求失败、无重启、无设备故障——GPU 加速不改变路由行为。
  3. GPU 路径对节点负载不敏感:CPU 行的延迟随节点负载明显波动,GPU 行几乎不动。
  4. 单进程串行设备工作的吞吐上限:五个模型共享一个运行时进程,同一时刻只跑一个模型的设备工作,ROCm 吞吐从 4 个调用方起稳定在 ~108 req/s(Stack B 为 ~108、shipped 略高)。
  5. 共享主机的方差:节点上其他 GPU 作业会让两套镜像都出现间歇性慢阶段;结论只在其他 GPU 空闲的节点上成立。
  6. 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 逐位一致融合内核均已就位。

登录后查看全文
semantic-router