1. Qwen3-32B 推理服务 TTFT 偏高问题到底出在哪Qwen3-32B 这类 32B 级别的稠密模型在单机多卡推理时首 token 返回时延TTFT往往不是被算力卡住的而是被 Host 侧的调度和内存拓扑拖了后腿。我最近在 Atlas 800I A2 上用 TP4 跑 Qwen3-32B基线 TTFT 平均 250ms偶发冲到 270ms 以上端到端体验明显发涩。排查下来瓶颈集中在 prefill 阶段的快慢卡现象部分卡算子执行连续部分卡出现大量空泡通信时间线上慢卡成为短板。这类问题的典型特征是NPU 计算利用率看着不低但通信等待占比高Host 侧任务下发不均衡。换句话说不是模型算不动而是任务没被均匀、就近地喂给每张卡。要解决它得从两个角度切入——CPU 绑核让推理线程不被其他进程抢走NUMA 内存调度让每张卡访问的内存尽量落在本地节点减少跨节点迁移。这篇内容适合正在自建 Qwen3-32B 推理服务、被 TTFT 波动困扰的工程师。我会给出可复制的启动参数骨架、numactl 配置模板以及一套压测对比方法让你在自己的环境里复现从 250ms 到 223ms 的调优过程。核心检索词就三个Qwen3-32B、TTFT、NUMA 绑核。2. 动手前先把 TaoToken 的 Key 和接入信息准备好调优过程中免不了要反复验证模型输出是否正常、对比不同参数下的响应质量。与其每次手动起服务再 curl不如先用一个稳定的 API 入口做基准验证。我习惯用 TaoToken 来做这件事——它提供统一的模型对话接口验证 Qwen3-32B 的输出一致性很方便不用每次都把本地服务拉起来。你需要先拿到 API Key。访问控制台创建即可控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI Key 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content拿到 Key 之后模型对话的验证入口在模型对话https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content如果你后续要做长期的编码或 Agent 类任务可以关注 Coding PlanCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content接入文档在这里参数细节以文档为准接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基础地址是https://taotoken.net/api注意这个地址不带 UTM 参数直接用于代码里的 base_url。官网首页是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。注意TaoToken 在这里的角色是模型输出验证和接口联调的入口不是用来替代你本地推理引擎的。本地 Qwen3-32B 的 TTFT 调优仍然在你的 NPU 机器上完成。3. 可复制的绑核与 NUMA 配置骨架这一节是全文的核心。先讲清楚环境拓扑再给配置模板。3.1 先摸清 NUMA 与 NPU 的亲和关系在 Atlas 800I A2 上4 张 NPU 卡的亲和 NUMA 节点并不一定是 0 和 1。我这次的环境里系统共 8 个 NUMA 节点每个节点 24 个 CPUNPU 亲和的是 NUMA 0 和 2。问题在于 NUMA0 和 NUMA1 的可用内存低于 4GB而每张卡的推理线程需要超过 4GB 内存其中约 1GB 是多卡共享、3GB 是单卡独占。共享内存要在多个 NUMA 节点间迁移跨节点访问延迟直接拉高了 TTFT。先用命令确认拓扑# 查看 NUMA 节点与 CPU 分布 numactl --hardware # 查看 NPU 设备与 NUMA 亲和 npu-smi info -t topo # 查看各 NUMA 节点内存余量 numastat -mnumactl --hardware会输出每个节点的 CPU 列表和内存大小。重点看node X free这一行如果某个节点 free 内存低于 4GB就不要把推理任务往那个节点上放。3.2 任务迁移到内存充足的 NUMA 节点识别出 NUMA0/1 内存不足后把推理任务切到前 4 张 NPU 卡亲和 NUMA 4 和 6。这一步是基线改善的关键TTFT 从 250ms 降到 243ms。# 假设推理进程 PID 为 $PID将其绑定到 NUMA 4 和 6 的 CPU numactl --cpunodebind4,6 --membind4,6 --physcpubind96-143 启动命令这里的--cpunodebind指定 CPU 节点--membind指定内存分配节点--physcpubind进一步细化到物理核。NUMA4 和 6 各 24 核合计 48 核足够 TP4 的推理线程使用。3.3 绑核脚本骨架光靠 numactl 还不够推理引擎内部线程也需要显式绑核避免线程在节点间漂移。下面是一个可复用的绑核脚本骨架#!/bin/bash # bind_cores.sh - Qwen3-32B TP4 绑核脚本 # 用法: ./bind_cores.sh 推理进程PID PID$1 if [ -z $PID ]; then echo usage: $0 pid exit 1 fi # NUMA4 的 CPU 范围 96-119NUMA6 的 CPU 范围 144-167 # 每张卡分配 12 个物理核4 卡共 48 核 CARD0_CPUS96-107 CARD1_CPUS108-119 CARD2_CPUS144-155 CARD3_CPUS156-167 # 获取推理进程的所有线程 TIDS$(ls /proc/$PID/task) i0 for tid in $TIDS; do case $((i % 4)) in 0) taskset -pc $CARD0_CPUS $tid /dev/null 21 ;; 1) taskset -pc $CARD1_CPUS $tid /dev/null 21 ;; 2) taskset -pc $CARD2_CPUS $tid /dev/null 21 ;; 3) taskset -pc $CARD3_CPUS $tid /dev/null 21 ;; esac i$((i 1)) done echo bound $i threads of pid $PID这个脚本把推理进程的线程轮流绑到 4 张卡对应的 CPU 核上。实测下来绑核后 TTFT 从 243ms 进一步降到 223ms波动控制在 240ms 以内整体提升 8.2%。3.4 启动参数模板把 numactl 和绑核脚本串起来完整的启动流程如下# 1. 启动推理服务先用 numactl 限定 NUMA 范围 numactl --cpunodebind4,6 --membind4,6 \ python -m vllm.entrypoints.openai.api_server \ --model Qwen3-32B \ --tensor-parallel-size 4 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --port 8000 # 2. 记录 PID VLLM_PID$! # 3. 等待服务就绪后执行绑核 sleep 60 ./bind_cores.sh $VLLM_PID注意--membind一定要和--cpunodebind保持一致否则内存可能被分配到远端节点绑核效果会被抵消。这是我在第一次尝试时踩过的坑——只绑了 CPU 没绑内存TTFT 几乎没变化。4. 验证请求与成功结果对比配置改完得用压测数据说话。我用的验证方式是固定 prompt 长度、固定并发对比优化前后的 TTFT 分布。4.1 压测脚本# bench_ttft.py - TTFT 压测脚本 import time import requests import statistics API_URL http://localhost:8000/v1/completions PROMPT 请用 200 字解释什么是 NUMA 架构。 * 20 # 构造较长 prefill N 50 # 请求次数 ttfts [] for i in range(N): payload { model: Qwen3-32B, prompt: PROMPT, max_tokens: 1, # 只取首 token stream: True, } start time.time() with requests.post(API_URL, jsonpayload, streamTrue) as r: for line in r.iter_lines(): if line: ttfts.append((time.time() - start) * 1000) break print(favg TTFT: {statistics.mean(ttfts):.1f} ms) print(fp95 TTFT: {statistics.quantiles(ttfts, n20)[18]:.1f} ms) print(fmax TTFT: {max(ttfts):.1f} ms)4.2 优化前后对比阶段平均 TTFT波动上限说明原始基线250ms270msNUMA0/1 内存不足跨节点迁移任务迁移至前 4 卡243ms260ms避开低内存节点迁移 绑核223ms240ms线程与内存均本地化从 250ms 到 223ms降幅 8.2%。这个数字看起来不大但在高并发下TTFT 的波动收窄比均值下降更有价值——波动从 270ms 压到 240ms 以内意味着尾延迟更可控。4.3 用 TaoToken 验证输出一致性调优只影响性能不应该改变模型输出。为了确认绑核和 NUMA 调整没有引入输出异常我用 TaoToken 的模型对话接口做了一组对照import requests resp requests.post( https://taotoken.net/api/v1/chat/completions, headers{Authorization: Bearer 你的API_KEY}, json{ model: Qwen3-32B, messages: [{role: user, content: 用一句话说明 NUMA 对推理延迟的影响。}], }, ) print(resp.json()[choices][0][message][content])把本地服务的输出和 TaoToken 的输出做语义对比确认调优前后模型行为一致。这一步能帮你排除“性能上去了但输出崩了”的情况。5. 本篇常见错排查调优过程中最容易卡住的几个点我按出现频率排一下。5.1 绑核后 TTFT 没变化最常见的原因是只绑了 CPU 没绑内存。taskset只控制 CPU 亲和不控制内存分配节点。如果推理线程访问的内存在远端 NUMA 节点跨节点延迟依然存在。解决方法是 numactl 的--membind和绑核脚本配合使用两者缺一不可。另一个可能是绑核脚本执行太晚。推理服务启动时会预分配内存和线程池如果绑核在初始化之后才执行部分线程已经跑偏了。建议在服务就绪后尽快执行或者把绑核逻辑写进启动脚本的 wrapper 里。5.2 numactl 报错 “no such node”说明你指定的 NUMA 节点号在当前机器上不存在。先用numactl --hardware确认实际节点列表。不同型号的机器 NUMA 编号不一样Atlas 800I A2 是 8 节点但有些配置可能是 4 节点或 2 节点。节点号写错会直接启动失败。5.3 关闭透明大页和 NUMA 平衡后无改善我试过这两个系统参数echo never /sys/kernel/mm/transparent_hugepage/enabled sysctl -w kernel.numa_balancing0验证状态确实生效了[never]和0但 TTFT 没有明显变化。这说明瓶颈不在系统级调度策略而在资源拓扑本身。这两个操作可以作为排除项但不要指望它们能解决跨 NUMA 共享内存迁移的问题。5.4 共享内存跨 NUMA 迁移无法软件层解决Atlas 800I A2 架构下NPU 亲和 NUMA 跨节点较多约 1GB 的多卡共享内存会被各卡频繁访问在 NUMA4 和 NUMA6 之间迁移。这部分在当前代码逻辑下无法通过软件层进一步优化。如果你遇到类似情况建议在部署阶段就规划好内存分布把共享内存尽量放在同一个 NUMA 节点内。5.5 压测结果波动大如果每次压测的 TTFT 差异超过 10%先检查是否有其他进程在抢 CPU。用top -H看线程级 CPU 占用确认推理线程没有被其他任务挤占。另外压测时关闭其他非必要服务避免网络和磁盘 IO 干扰。6. 后续接入与长期编码任务的分流建议调优做完接下来通常是两条路一是把验证好的服务接入业务二是做长期的编码或 Agent 任务。接入和排障相关的建议直接看 API Keys 和接入文档API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content需要反复验证模型输出、对比不同参数效果的用模型对话入口最方便模型对话https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content如果你在做长期的编码辅助或 Agent 类项目Coding Plan 会更合适Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content最后说一个实操细节绑核脚本里的 CPU 范围一定要根据你自己机器的numactl --hardware输出改不要直接抄我这里的 96-167。不同机器的 CPU 编号起点不一样抄错会导致绑核到不存在的核上脚本静默失败你还以为优化没效果。先跑numactl --hardware把节点和 CPU 范围记下来再填进脚本。这一步花两分钟能省掉后面半小时的排查。