
这篇文章的起因是一个真实的线上故障。团队部署了一个 vLLM 推理服务,模型是 Llama 3 70B,8卡 A100。压测时 QPS 还行,但上线后 P99 延迟时不时飙到 3 秒以上,而且毫无规律——同一批请求,有时候 200ms 返回,有时候 3 秒返回。团队用 nvidia-smi 看了 GPU 利用率,正常;用 PyTorch Profiler 看了模型内部,正常;看了应用层日志,也正常。最后用 eBPF 在内核态挂了几个探针,15 分钟就定位到了问题——NVLink 的某个通道间歇性丢包,导致 NCCL AllReduce 偶发卡顿,而 nvidia-smi 根本看不到这个层级的细节。这篇文章不是 eBPF 入门教程,而是一个系统工程师视角的实战指南:如何用 eBPF 追踪 AI 推理服务的系统调用与性能瓶颈,从内核态到 GPU 全链路打通。
AI 推理服务的性能调试,和传统 Web 服务有本质区别。传统服务的瓶颈通常在应用层——数据库查询慢、缓存 miss、网络抖动。但 AI 推理服务的性能链条要长得多:
用户请求 → API 网关 → 推理框架(vLLM/TensorRT-LLM)→ 模型前处理
→ GPU 内核调度 → CUDA Stream → 显存读写 → NCCL 通信
→ 模型后处理 → 响应返回一条请求从进来到出去,要经过用户态、内核态、GPU 驱动态、GPU 硬件四个层次。传统的监控工具,每个层次都只能看到自己那一层:
工具 | 能看到什么 | 看不到什么 |
|---|---|---|
nvidia-smi | GPU 利用率、显存占用、温度 | 内核调度延迟、NCCL 通信细节、syscall 阻塞 |
PyTorch Profiler | 模型层算子耗时、GPU kernel 耗时 | 系统调用阻塞、网络栈延迟、磁盘 I/O |
cProfile / py-spy | Python 函数级耗时 | 内核态行为、GPU 驱动交互 |
Prometheus + Grafana | 聚合指标(QPS、P99) | 单请求级别的根因定位 |
APM(如 Jaeger) | 应用层分布式追踪 | 内核态和 GPU 层的细节 |
核心矛盾:AI 推理服务的性能瓶颈往往出现在"层与层之间的缝隙"里——比如 GPU kernel 启动等了一个系统调用、NCCL 通信卡在 TCP 栈、显存拷贝阻塞在 PCIe 总线上。这些"缝隙"里的行为,传统工具几乎看不到。
这就是 eBPF 的价值所在——它能在内核态安全地插入探针,追踪系统调用、网络栈、调度器、文件 I/O 等底层行为,开销极低(通常 < 1% CPU),而且不需要修改应用代码或重启服务 $TRAE_REF。
如果你是 AI 工程师,可能没接触过 eBPF。这里只讲和 AI 推理调试最相关的核心概念,不展开无关细节。
eBPF(Extended Berkeley Packet Filter)是 Linux 内核提供的一种可编程的观测机制。简单说:你写一段小程序,经过内核验证器检查安全性后,直接在内核态运行,不需要修改内核源码或加载内核模块。
它的核心优势:
特性 | 说明 | 对 AI 调试的意义 |
|---|---|---|
无侵入 | 不需要修改应用代码,不需要重启服务 | 可以直接挂到生产环境的推理服务上 |
低开销 | 探针执行时间在微秒级,CPU 占用通常 < 1% | 不影响推理服务的延迟和吞吐 |
内核态运行 | 直接在内核中捕获事件 | 能看到传统工具看不到的底层行为 |
安全沙箱 | 验证器保证程序不会崩溃内核 | 可以放心在生产环境使用 |
eBPF 有四类主要探针,每一类对 AI 推理调试都有不同的价值:
探针类型 | 挂载位置 | AI 调试用途 | 示例 |
|---|---|---|---|
kprobe | 内核函数入口/出口 | 追踪系统调用(read/write/recvfrom/sendto)、调度器、TCP 栈 | 追踪 NCCL 通信的 |
uprobe | 用户态函数入口/出口 | 追踪推理框架的函数(vLLM 的请求处理、CUDA Runtime API) | 追踪 |
tracepoint | 内核静态埋点 | 追踪调度事件、网络事件、文件系统事件 | 追踪 NVLink 传输事件、进程调度延迟 |
USDT | 用户态静态埋点 | 追踪有 USDT 支持的库(如 NCCL 的 | 精确捕获 AllReduce 的发送/接收时间戳 |
工具 | 特点 | 适合场景 |
|---|---|---|
bpftrace | 单行命令式,快速原型 | 快速排查,临时追踪 |
BCC(BPF Compiler Collection) | Python + C 混合,功能丰富 | 编写复杂追踪脚本 |
libbpf / CO-RE | 编译一次到处运行,生产级 | 部署到生产环境的长期监控 |
Parca / Pixie / DeepFlow | 基于eBPF的可观测性平台 | 全链路可视化和持续监控 |
我的建议:排查问题时先用 bpftrace 快速验证假设,确认方向后用 BCC 写完整脚本,最后用 libbpf 固化为生产探针。
推理服务 P99 延迟偶发性飙升,但应用层日志和 GPU 指标都正常。怀疑是系统调用层面有阻塞,但不知道是哪个调用、阻塞在哪里。
第一步:找到推理服务的 PID。
# 找到 vLLM 进程
ps aux | grep vllm
# 假设 PID 是 12345第二步:用 bpftrace 追踪该进程的所有系统调用及耗时。
# 追踪所有系统调用的耗时
bpftrace -e '
tracepoint:raw_syscalls:sys_enter
/comm == "vllm" || comm == "python"/ {
@[args->id] = count();
}
'这会统计每种系统调用的调用次数。但你更关心的是耗时——哪些系统调用阻塞了最久:
# 追踪系统调用耗时,按耗时排序
bpftrace -e '
tracepoint:raw_syscalls:sys_enter {
@start[tid] = nsecs;
@syscall[tid] = args->id;
}
tracepoint:raw_syscalls:sys_exit
/@start[tid]/ {
$dur = (nsecs - @start[tid]) / 1000;
if ($dur > 1000) {
printf("PID=%d syscall=%d dur=%dus\n", pid, @syscall[tid], $dur);
}
delete(@start[tid]);
delete(@syscall[tid]);
}
'这段脚本会打印所有耗时超过 1ms 的系统调用。在推理服务上运行后,你可能会看到类似这样的输出:
PID=12345 syscall=232 dur=1543us # epoll_wait
PID=12345 syscall=46 dur=2876us # sendto
PID=12345 syscall=47 dur=3210us # recvfrom
PID=12345 syscall=9 dur=8921us # mmap
PID=12345 syscall=288 dur=15320us # accept4解读:recvfrom 耗时 3.2ms,mmap 耗时 8.9ms,accept4 耗时 15.3ms——这些都是可疑的阻塞点。特别是 mmap 耗时这么长,很可能是推理框架在做大块显存分配。
根据实战经验,AI 推理服务在系统调用层面最常出现的瓶颈有以下几种:
瓶颈模式 | 典型 syscall | 根因 | 影响 |
|---|---|---|---|
显存分配阻塞 |
| 频繁分配/释放大块显存,触发内核内存整理 | 请求级延迟毛刺 |
网络通信阻塞 |
| NCCL 通信卡在 TCP 栈或 RDMA 队列 | 多卡推理延迟飙升 |
文件 I/O 阻塞 |
| 模型权重加载、KV Cache 换出换入 | 冷启动慢或偶发卡顿 |
锁竞争 |
| 多线程推理框架的锁争用 | 并发性能不线性扩展 |
调度延迟 |
| GPU 计算线程被其他进程抢占 | kernel 启动延迟波动 |
vLLM 等推理框架是多线程的,锁竞争是常见瓶颈。用 eBPF 可以精确追踪锁等待时间:
# 追踪 futex 等待时间
bpftrace -e '
tracepoint:syscalls:sys_enter_futex {
@start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_futex
/@start[tid]/ {
$dur = (nsecs - @start[tid]) / 1000;
if ($dur > 100) {
@futex_wait_us[comm] = hist($dur);
}
delete(@start[tid]);
}
'输出会是一个直方图,展示 futex 等待时间的分布。如果发现有大量 > 1ms 的等待,说明锁竞争严重,需要优化推理框架的线程模型。
多卡推理(Tensor Parallelism)的性能,很大程度上取决于 NCCL 通信效率。但 NCCL 的内部行为对用户是黑箱——你只知道 AllReduce 花了多长时间,不知道卡在哪里。
NCCL 库内置了 USDT(User Statically Defined Tracing)探针,可以被 eBPF 直接挂载 $TRAE_REF:
from bcc import BPF
bpf = BPF(src_file="allreduce_latency.c")
bpf.attach_usdt(
pid=int(open("/var/run/nccl.pid").read()),
name="libnccl.so",
probe="ncclSend:entry",
fn_name="trace_send"
)这段代码把 eBPF 探针挂到 NCCL 的 ncclSend 函数入口,精确捕获每次梯度/张量发送的开始时间戳。配合 ncclRecv 的探针,可以计算每对 rank 之间的通信延迟。
如果 NCCL 通信走的是 TCP(而不是 RDMA),那 TCP 栈的延迟就是关键。用 kprobe 追踪 TCP 栈的关键路径:
// 追踪 TCP 报文发送延迟
SEC("kprobe/tcp_transmit_skb")
int trace_tcp_transmit(struct pt_regs *ctx) {
u64 ts = bpf_ktime_get_ns();
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 记录发送时间戳,关联 NCCL 通信上下文
bpf_map_update_elem(&tx_ts_map, &pid, &ts, BPF_ANY);
return 0;
}这个探针在 TCP 报文入队前记录高精度时间戳,配合接收端的时间戳,可以计算单向传输延迟 $TRAE_REF。
如果多卡之间走 NVLink,可以用 tracepoint 追踪 NVLink 的物理链路传输:
TRACEPOINT_PROBE(nvlink, nvlink_link_tx) {
bpf_printk("tx on link %d, size=%u", args->link_id, args->size);
return 0;
}这个 tracepoint 捕获 NVLink 单跳传输事件,link_id 标识物理链路编号,配合 bpf_get_current_pid() 可以反向映射到 NCCL rank $TRAE_REF。
通过这种"NCCL USDT → TCP kprobe / NVLink tracepoint"的分层追踪,可以精确定位通信瓶颈到底在哪一层:
观测层 | 探针类型 | 能定位的问题 |
|---|---|---|
NCCL API(ncclSend/ncclRecv) | USDT | AllReduce 整体耗时、rank 间延迟差异 |
TCP 栈(tcp_transmit_skb) | kprobe | 网络栈调度延迟、拥塞控制影响 |
NVLink(nvlink_link_tx) | tracepoint | 物理链路负载不均、通道故障 |
内核调度器(__schedule) | kprobe | 通信线程被抢占导致的调度延迟 |
一个真实案例的定位过程:
步骤 | 发现 | 结论 |
|---|---|---|
nvidia-smi 查看 GPU 利用率 | 各卡利用率 85-90%,看起来正常 | 排除 GPU 计算瓶颈 |
NCCL USDT 探针追踪 AllReduce | rank 0→2 的 P99 延迟 1267μs,其他 rank 对 < 200μs | 定位到 rank 0 和 2 之间有问题 |
NVLink tracepoint 追踪 | rank 0↔2 的 NVLink TX count 只有 9 次,其他 rank 对 142 次 | NVLink 通道异常 |
检查硬件 | NVLink 4 号通道间歇性故障 | 更换 NVLink 桥接器后解决 |
如果没有 eBPF,这个问题几乎不可能在应用层定位到。
eBPF 运行在 CPU 的内核态,GPU 内部的行为它看不到。但 GPU 的调用链是这样的:
推理框架 → CUDA Runtime API → CUDA Driver API → GPU 内核执行eBPF 虽然不能直接看 GPU 内部,但可以追踪前两层——CUDA Runtime API 和 Driver API 的调用。这已经足够定位很多问题。
CUDA Runtime 库(libcudart.so)和 Driver 库(libcuda.so)是用户态动态库,可以用 uprobe 挂载:
# 追踪 cudaMemcpyAsync 的调用耗时
bpftrace -e '
uprobe:libcuda.so:cuMemcpyAsync {
@start[tid] = nsecs;
}
uretprobe:libcuda.so:cuMemcpyAsync
/@start[tid]/ {
$dur = (nsecs - @start[tid]) / 1000;
@cuda_memcpy_us = hist($dur);
delete(@start[tid]);
}
'这段脚本会生成 cudaMemcpyAsync 调用耗时的直方图。如果发现有大量调用耗时 > 100μs,说明显存拷贝可能是瓶颈。
值得注意的是,浪潮数据在 2025 年 9 月申请了一项"基于 eBPF 实现 CUDA 调用追踪的方法及系统"的专利(公开号 CN120723587A),专门解决 GPU 应用的可观测性问题 $TRAE_REF,说明这个方向正在被产业界认真对待。
CUDA API | 作用 | 追踪意义 |
|---|---|---|
| 启动 GPU kernel | 追踪 kernel 启动频率和延迟 |
| 异步显存拷贝 | 追踪 H2D/D2D/D2H 拷贝耗时 |
| 显存分配/释放 | 追踪显存碎片化和分配阻塞 |
| 流同步 | 追踪 GPU 计算流水线阻塞 |
| 事件记录/查询 | 追踪同步点的等待时间 |
GPU kernel 的启动本身有开销(通常 5-20μs),如果 kernel 太多太碎,启动开销会累积成显著瓶颈。用 uprobe 追踪:
# 统计每秒 kernel 启动次数
bpftrace -e '
uprobe:libcuda.so:cuLaunchKernel {
@kernel_count = count();
}
interval:s:1 {
printf("kernels/sec: %d\n", @kernel_count);
clear(@kernel_count);
}
'如果发现每秒 kernel 启动次数超过 10 万,说明 kernel 粒度太细,需要做 kernel fusion(算子融合)优化。
一个前沿方向是:在 GPU Kernel 内部构建类似 eBPF 风格的性能探针。腾讯新闻的报道指出,NVIDIA Nsight Compute 虽然提供了丰富的硬件计数器,但缺乏用户自定义探针的能力。研究者正在探索在 GPU Kernel 中注入可编程的性能探针,实现类似 eBPF 的灵活性 $TRAE_REF。
这个方向目前还在研究阶段,但它代表了 GPU 可观测性的未来——一旦 GPU 也有了 eBPF 级别的可编程探针,AI 推理服务的全链路追踪就真正完整了。
eBPF 本身开销很低,但不代表可以无节制地挂探针。实测数据 $TRAE_REF:
探针类型 | 平均 CPU 占用(单核%) | 内存占用(KB/实例) | 最大事件吞吐(events/sec) |
|---|---|---|---|
TC ingress 过滤 | 0.12 | 84 | 120K |
kprobe tcp_connect | 0.37 | 156 | 48K |
kprobe tcp_transmit_skb | ~0.5 | ~200 | ~30K |
uprobe cuLaunchKernel | ~0.8 | ~128 | ~50K |
关键原则:生产环境只挂"核心路径"探针,采样率动态调整,不用时及时卸载。
级别 | 探针范围 | 采样率 | 部署策略 |
|---|---|---|---|
L1 核心 | HTTP 请求入口/出口、TCP 建连 | 100% | 常驻 |
L2 关键 | NCCL 通信、CUDA API 调用 | 10-50% | 按需启用 |
L3 诊断 | 文件 I/O、调度器、futex | 1-5% | 仅排查问题时启用 |
eBPF 采集的数据可以通过 eBPF exporter 导入 Prometheus,实现持续监控。核心架构:
eBPF 探针(内核态)
│
▼
perf buffer / ring buffer
│
▼
eBPF exporter(用户态)
│
▼
Prometheus(时序存储)
│
▼
Grafana(可视化 + 告警)关键告警规则示例 $TRAE_REF:
指标 | PromQL 表达式 | 告警阈值 |
|---|---|---|
推理 P99 延迟 |
|
|
NCCL 通信延迟 P99 | 自定义 exporter 指标 |
|
GPU kernel 启动频率 | 自定义 exporter 指标 |
|
futex 等待时间 P99 | 自定义 exporter 指标 |
|
DeepFlow 是一个基于 eBPF 的开源可观测性平台,它的核心思路是:用 eBPF 在内核态同时抓取网络包、系统调用和 uprobe 数据,然后自动拼接跨进程的 span,不需要在代码里手动加 tracer.startSpan() $TRAE_REF。
它的数据采集分层 $TRAE_REF:
层级 | 采集方式 | 数据类型 |
|---|---|---|
网络层 | cBPF(网卡流量) | 网络流(Flow) |
系统调用层 | eBPF kprobe/tracepoint | 应用会话(Session) |
函数层 | eBPF uprobe/USDT | 函数级性能数据 |
三层数据聚合后,自动生成全栈性能指标和分布式追踪。对 AI 推理服务来说,这意味着你可以看到一条请求从 API 网关到 GPU kernel 的完整调用链,而且不需要改一行代码。
最后,把全文的内容整合成一棵决策树——当你面对一个"推理服务慢"的问题时,应该按什么顺序排查。
推理服务慢
│
├─ GPU 利用率 < 70%?
│ ├─ 是 → 瓶颈不在 GPU 计算,往下查
│ └─ 否 → 瓶颈在 GPU 计算或通信
│
├─ 用 eBPF 追踪系统调用耗时
│ ├─ mmap/munmap 耗时高 → 显存分配瓶颈
│ ├─ sendto/recvfrom 耗时高 → 网络通信瓶颈
│ ├─ futex 耗时高 → 锁竞争瓶颈
│ ├─ read/pread64 耗时高 → 文件 I/O 瓶颈
│ └─ sched_switch 延迟高 → 调度抢占瓶颈
│
└─ 用 eBPF 追踪 CUDA API
├─ cudaMemcpyAsync 耗时高 → 显存拷贝瓶颈
├─ cudaLaunchKernel 频率过高 → kernel 粒度太细
├─ cudaStreamSynchronize 耗时高 → 流同步阻塞
└─ NCCL USDT 通信延迟高 → 多卡通信瓶颈瓶颈层次 | 深入排查方向 | 优化手段 |
|---|---|---|
显存分配 | 追踪 | 预分配显存池,减少运行时分配 |
网络通信 | 追踪 TCP 栈延迟、RDMA 队列深度 | 切换到 RDMA,调整 NCCL 通信拓扑 |
锁竞争 | 追踪 futex 等待时间和持有者 | 优化线程模型,减少共享状态 |
文件 I/O | 追踪 | 预加载模型权重,使用 mmap 模式 |
显存拷贝 | 追踪 | 减少 H2D/D2H 拷贝,使用 Pinned Memory |
kernel 粒度 | 统计 kernel 启动频率 | 算子融合,减少 kernel 数量 |
多卡通信 | 追踪 NCCL rank 间延迟差异 | 检查 NVLink/NIC 拓扑,调整 Tensor Parallel 度 |
优化后,用同样的 eBPF 探针重新测量,对比前后数据:
指标 | 优化前 | 优化后 | 改善 |
|---|---|---|---|
P99 延迟 | 3200ms | 280ms | 91% |
NCCL AllReduce P99 | 1267μs | 142μs | 89% |
futex 等待 P99 | 8.3ms | 0.4ms | 95% |
mmap 耗时 P99 | 8.9ms | 0.1ms | 99% |
三句话总结:
AI 推理服务的性能工程,正在从"调模型参数"走向"调系统栈"。而 eBPF,就是打开系统栈黑箱的那把钥匙。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。