首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >用 eBPF 给 AI 推理服务做"CT":从系统调用到 GPU 瓶颈的全链路追踪

用 eBPF 给 AI 推理服务做"CT":从系统调用到 GPU 瓶颈的全链路追踪

原创
作者头像
七条猫
发布2026-07-18 18:32:41
发布2026-07-18 18:32:41
40
举报

这篇文章的起因是一个真实的线上故障。团队部署了一个 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 推理服务

AI 推理服务的性能调试,和传统 Web 服务有本质区别。传统服务的瓶颈通常在应用层——数据库查询慢、缓存 miss、网络抖动。但 AI 推理服务的性能链条要长得多:

代码语言:txt
复制
用户请求 → 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


二、eBPF 基础:AI 工程师需要知道的核心概念

如果你是 AI 工程师,可能没接触过 eBPF。这里只讲和 AI 推理调试最相关的核心概念,不展开无关细节。

2.1 eBPF 是什么

eBPF(Extended Berkeley Packet Filter)是 Linux 内核提供的一种可编程的观测机制。简单说:你写一段小程序,经过内核验证器检查安全性后,直接在内核态运行,不需要修改内核源码或加载内核模块。

它的核心优势:

特性

说明

对 AI 调试的意义

无侵入

不需要修改应用代码,不需要重启服务

可以直接挂到生产环境的推理服务上

低开销

探针执行时间在微秒级,CPU 占用通常 < 1%

不影响推理服务的延迟和吞吐

内核态运行

直接在内核中捕获事件

能看到传统工具看不到的底层行为

安全沙箱

验证器保证程序不会崩溃内核

可以放心在生产环境使用

2.2 四类探针:你需要关注什么

eBPF 有四类主要探针,每一类对 AI 推理调试都有不同的价值:

探针类型

挂载位置

AI 调试用途

示例

kprobe

内核函数入口/出口

追踪系统调用(read/write/recvfrom/sendto)、调度器、TCP 栈

追踪 NCCL 通信的 sendto/recvfrom 延迟

uprobe

用户态函数入口/出口

追踪推理框架的函数(vLLM 的请求处理、CUDA Runtime API)

追踪 cudaMemcpyAsync 的调用耗时

tracepoint

内核静态埋点

追踪调度事件、网络事件、文件系统事件

追踪 NVLink 传输事件、进程调度延迟

USDT

用户态静态埋点

追踪有 USDT 支持的库(如 NCCL 的 ncclSend/ncclRecv

精确捕获 AllReduce 的发送/接收时间戳

2.3 工具链选择

工具

特点

适合场景

bpftrace

单行命令式,快速原型

快速排查,临时追踪

BCC(BPF Compiler Collection)

Python + C 混合,功能丰富

编写复杂追踪脚本

libbpf / CO-RE

编译一次到处运行,生产级

部署到生产环境的长期监控

Parca / Pixie / DeepFlow

基于eBPF的可观测性平台

全链路可视化和持续监控

我的建议:排查问题时先用 bpftrace 快速验证假设,确认方向后用 BCC 写完整脚本,最后用 libbpf 固化为生产探针。


三、实战一:追踪推理请求的系统调用链

3.1 问题场景

推理服务 P99 延迟偶发性飙升,但应用层日志和 GPU 指标都正常。怀疑是系统调用层面有阻塞,但不知道是哪个调用、阻塞在哪里。

3.2 用 bpftrace 追踪单请求的系统调用

第一步:找到推理服务的 PID。

代码语言:bash
复制
# 找到 vLLM 进程
ps aux | grep vllm
# 假设 PID 是 12345

第二步:用 bpftrace 追踪该进程的所有系统调用及耗时。

代码语言:bash
复制
# 追踪所有系统调用的耗时
bpftrace -e '
tracepoint:raw_syscalls:sys_enter
/comm == "vllm" || comm == "python"/ {
  @[args->id] = count();
}
'

这会统计每种系统调用的调用次数。但你更关心的是耗时——哪些系统调用阻塞了最久:

代码语言:bash
复制
# 追踪系统调用耗时,按耗时排序
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 的系统调用。在推理服务上运行后,你可能会看到类似这样的输出:

代码语言:txt
复制
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 耗时这么长,很可能是推理框架在做大块显存分配。

3.3 系统调用层面的常见瓶颈模式

根据实战经验,AI 推理服务在系统调用层面最常出现的瓶颈有以下几种:

瓶颈模式

典型 syscall

根因

影响

显存分配阻塞

mmap/munmap

频繁分配/释放大块显存,触发内核内存整理

请求级延迟毛刺

网络通信阻塞

sendto/recvfrom

NCCL 通信卡在 TCP 栈或 RDMA 队列

多卡推理延迟飙升

文件 I/O 阻塞

read/pread64

模型权重加载、KV Cache 换出换入

冷启动慢或偶发卡顿

锁竞争

futex

多线程推理框架的锁争用

并发性能不线性扩展

调度延迟

sched_switch

GPU 计算线程被其他进程抢占

kernel 启动延迟波动

3.4 用 eBPF 追踪 futex 锁竞争

vLLM 等推理框架是多线程的,锁竞争是常见瓶颈。用 eBPF 可以精确追踪锁等待时间:

代码语言:bash
复制
# 追踪 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 的等待,说明锁竞争严重,需要优化推理框架的线程模型。


四、实战二:追踪 NCCL 通信延迟

多卡推理(Tensor Parallelism)的性能,很大程度上取决于 NCCL 通信效率。但 NCCL 的内部行为对用户是黑箱——你只知道 AllReduce 花了多长时间,不知道卡在哪里。

4.1 用 USDT 探针追踪 NCCL

NCCL 库内置了 USDT(User Statically Defined Tracing)探针,可以被 eBPF 直接挂载 $TRAE_REF

代码语言:python
复制
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 之间的通信延迟。

4.2 用 kprobe 追踪 TCP 栈延迟

如果 NCCL 通信走的是 TCP(而不是 RDMA),那 TCP 栈的延迟就是关键。用 kprobe 追踪 TCP 栈的关键路径:

代码语言:txt
复制
// 追踪 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

4.3 用 tracepoint 追踪 NVLink 传输

如果多卡之间走 NVLink,可以用 tracepoint 追踪 NVLink 的物理链路传输:

代码语言:txt
复制
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

通信线程被抢占导致的调度延迟

4.4 通信瓶颈定位实战

一个真实案例的定位过程:

步骤

发现

结论

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,这个问题几乎不可能在应用层定位到。


五、实战三:追踪 GPU kernel 启动与显存拷贝

5.1 GPU 侧的观测盲区

eBPF 运行在 CPU 的内核态,GPU 内部的行为它看不到。但 GPU 的调用链是这样的:

代码语言:txt
复制
推理框架 → CUDA Runtime API → CUDA Driver API → GPU 内核执行

eBPF 虽然不能直接看 GPU 内部,但可以追踪前两层——CUDA Runtime API 和 Driver API 的调用。这已经足够定位很多问题。

5.2 用 uprobe 追踪 CUDA API 调用

CUDA Runtime 库(libcudart.so)和 Driver 库(libcuda.so)是用户态动态库,可以用 uprobe 挂载:

代码语言:bash
复制
# 追踪 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,说明这个方向正在被产业界认真对待。

5.3 关键 CUDA API 的追踪清单

CUDA API

作用

追踪意义

cudaLaunchKernel

启动 GPU kernel

追踪 kernel 启动频率和延迟

cudaMemcpyAsync

异步显存拷贝

追踪 H2D/D2D/D2H 拷贝耗时

cudaMalloc / cudaFree

显存分配/释放

追踪显存碎片化和分配阻塞

cudaStreamSynchronize

流同步

追踪 GPU 计算流水线阻塞

cudaEventRecord / cudaEventQuery

事件记录/查询

追踪同步点的等待时间

5.4 kernel 启动延迟分析

GPU kernel 的启动本身有开销(通常 5-20μs),如果 kernel 太多太碎,启动开销会累积成显著瓶颈。用 uprobe 追踪:

代码语言:bash
复制
# 统计每秒 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(算子融合)优化。

5.5 GPU Kernel 中的 eBPF 风格探针

一个前沿方向是:在 GPU Kernel 内部构建类似 eBPF 风格的性能探针。腾讯新闻的报道指出,NVIDIA Nsight Compute 虽然提供了丰富的硬件计数器,但缺乏用户自定义探针的能力。研究者正在探索在 GPU Kernel 中注入可编程的性能探针,实现类似 eBPF 的灵活性 $TRAE_REF

这个方向目前还在研究阶段,但它代表了 GPU 可观测性的未来——一旦 GPU 也有了 eBPF 级别的可编程探针,AI 推理服务的全链路追踪就真正完整了。


六、生产环境部署:开销控制与工程化

6.1 eBPF 探针的性能开销

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

关键原则:生产环境只挂"核心路径"探针,采样率动态调整,不用时及时卸载。

6.2 探针分级策略

级别

探针范围

采样率

部署策略

L1 核心

HTTP 请求入口/出口、TCP 建连

100%

常驻

L2 关键

NCCL 通信、CUDA API 调用

10-50%

按需启用

L3 诊断

文件 I/O、调度器、futex

1-5%

仅排查问题时启用

6.3 与 Prometheus + Grafana 集成

eBPF 采集的数据可以通过 eBPF exporter 导入 Prometheus,实现持续监控。核心架构:

代码语言:txt
复制
eBPF 探针(内核态)
    │
    ▼
perf buffer / ring buffer
    │
    ▼
eBPF exporter(用户态)
    │
    ▼
Prometheus(时序存储)
    │
    ▼
Grafana(可视化 + 告警)

关键告警规则示例 $TRAE_REF

指标

PromQL 表达式

告警阈值

推理 P99 延迟

histogram_quantile(0.99, ...)

500ms

NCCL 通信延迟 P99

自定义 exporter 指标

2ms

GPU kernel 启动频率

自定义 exporter 指标

100K/s

futex 等待时间 P99

自定义 exporter 指标

5ms

6.4 DeepFlow:eBPF + AI 的零侵入全链路追踪

DeepFlow 是一个基于 eBPF 的开源可观测性平台,它的核心思路是:用 eBPF 在内核态同时抓取网络包、系统调用和 uprobe 数据,然后自动拼接跨进程的 span,不需要在代码里手动加 tracer.startSpan() $TRAE_REF

它的数据采集分层 $TRAE_REF

层级

采集方式

数据类型

网络层

cBPF(网卡流量)

网络流(Flow)

系统调用层

eBPF kprobe/tracepoint

应用会话(Session)

函数层

eBPF uprobe/USDT

函数级性能数据

三层数据聚合后,自动生成全栈性能指标和分布式追踪。对 AI 推理服务来说,这意味着你可以看到一条请求从 API 网关到 GPU kernel 的完整调用链,而且不需要改一行代码


七、AI 推理服务性能调优决策树

最后,把全文的内容整合成一棵决策树——当你面对一个"推理服务慢"的问题时,应该按什么顺序排查。

第一步:定位瓶颈层次

代码语言:txt
复制
推理服务慢
    │
    ├─ 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 通信延迟高 → 多卡通信瓶颈

第二步:按层次深入

瓶颈层次

深入排查方向

优化手段

显存分配

追踪 mmap 频率和大小

预分配显存池,减少运行时分配

网络通信

追踪 TCP 栈延迟、RDMA 队列深度

切换到 RDMA,调整 NCCL 通信拓扑

锁竞争

追踪 futex 等待时间和持有者

优化线程模型,减少共享状态

文件 I/O

追踪 read 的文件描述符和大小

预加载模型权重,使用 mmap 模式

显存拷贝

追踪 cudaMemcpyAsync 方向和大小

减少 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%


八、总结

三句话总结

  1. AI 推理服务的性能瓶颈往往在"层与层之间的缝隙"里——系统调用阻塞、网络栈延迟、NCCL 通信毛刺、CUDA API 调用开销——这些是传统工具的盲区,却是 eBPF 的主场。
  2. eBPF 的价值不是替代传统工具,而是补全观测链条的最后一环。 nvidia-smi 看 GPU 硬件,PyTorch Profiler 看模型算子,eBPF 看内核和用户态的底层行为——三者结合才是完整的可观测性方案。
  3. 从"能监控"到"能调试"是质变。 Prometheus 告诉你"P99 高了",eBPF 告诉你"是哪个系统调用、在哪个内核函数、阻塞了多久、为什么阻塞"。前者是预警,后者是诊断。没有诊断的预警,只是焦虑的来源。

AI 推理服务的性能工程,正在从"调模型参数"走向"调系统栈"。而 eBPF,就是打开系统栈黑箱的那把钥匙。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、为什么传统监控工具搞不定 AI 推理服务
  • 二、eBPF 基础:AI 工程师需要知道的核心概念
    • 2.1 eBPF 是什么
    • 2.2 四类探针:你需要关注什么
    • 2.3 工具链选择
  • 三、实战一:追踪推理请求的系统调用链
    • 3.1 问题场景
    • 3.2 用 bpftrace 追踪单请求的系统调用
    • 3.3 系统调用层面的常见瓶颈模式
    • 3.4 用 eBPF 追踪 futex 锁竞争
  • 四、实战二:追踪 NCCL 通信延迟
    • 4.1 用 USDT 探针追踪 NCCL
    • 4.2 用 kprobe 追踪 TCP 栈延迟
    • 4.3 用 tracepoint 追踪 NVLink 传输
    • 4.4 通信瓶颈定位实战
  • 五、实战三:追踪 GPU kernel 启动与显存拷贝
    • 5.1 GPU 侧的观测盲区
    • 5.2 用 uprobe 追踪 CUDA API 调用
    • 5.3 关键 CUDA API 的追踪清单
    • 5.4 kernel 启动延迟分析
    • 5.5 GPU Kernel 中的 eBPF 风格探针
  • 六、生产环境部署:开销控制与工程化
    • 6.1 eBPF 探针的性能开销
    • 6.2 探针分级策略
    • 6.3 与 Prometheus + Grafana 集成
    • 6.4 DeepFlow:eBPF + AI 的零侵入全链路追踪
  • 七、AI 推理服务性能调优决策树
    • 第一步:定位瓶颈层次
    • 第二步:按层次深入
    • 第三步:验证优化效果
  • 八、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档