eBPF × LLM 推理:零侵入全链路可观测性工程实践
传统 APM 工具在面对大规模 LLM 推理集群时力不从心——高采样率下的 CPU 开销、复杂的异步调用链追踪、GPU 与 CPU 协同视野的缺失。本文深入探讨如何利用 eBPF 技术,在不修改推理引擎源码的前提下,构建一套覆盖请求入口、Token 调度、GPU 计算、内存分配的全链路可观测平台。
一、AI 推理可观测性的核心挑战
现代 LLM 推理引擎(vLLM、TensorRT-LLM、TGI)的架构远比传统 Web 服务复杂:
- 多阶段流水线:HTTP 请求 → Tokenizer → Scheduler → Model Forward → Detokenizer → Streaming Response,每个阶段的延迟开销呈现高度动态性。
- 连续批处理(Continuous Batching)的追踪困境:同一时刻一个 batch 中可能混合了处于 Prefill 阶段的新请求和处于 Decode 阶段的持续生成请求,传统方法难以区分两种模式的计算特征。
- CPU-GPU 异步边界模糊:CUDA Kernel 提交与 GPU 实际计算之间存在排队延迟,常规工具无法在时间轴上精确对齐 CPU 侧调度事件和 GPU 侧执行事件。
- KV Cache 内存管理的微观行为:PageAttention 引入的分页机制使得内存分配/释放呈现出碎片化模式,这种内存在传统指标中完全隐形。
- 流量层:QPS、并发序列数、输入/输出 Token 速率
- 延迟层:TTFT(Time-To-First-Token)、TPOT P50/P99、E2E P99
- 错误层:OOM 事件、CUDA OOM、超时中断
- 饱和度层:KV Cache 使用率、CUDA 队列深度、显存带宽利用率
- BTF 兼容性确保:部署目标需开启
CONFIG_DEBUG_INFO_BTF=y,建议内核 5.4+。可携带vmlinux.h处理跨内核兼容。 - eBPF 验证器边界:复杂循环需显式设置
#pragma unroll,避免验证器拒绝加载。 - TLS 终止点处理:若使用 TLS 加密流量,需在应用层解密后追踪,或利用 eBPF 的
BPF_PROG_TYPE_SK_LOOKUP在 Socket 层追踪明文(仅限本机通信)。 - CUDA Driver API 链接注意:
libcudart.so与libcuda.so不同函数入口需完整覆盖。 - Ring Buffer 溢出处理:高负载场景下增加 buffer 大小或采用 BPF_MAP_TYPE_PERF_EVENT_ARRAY 配合批量消费。
- BPF 程序热加载:利用
bpf_link的独立 attach,实现不停机更新探针逻辑。 - 用户态无状态设计:Ring Buffer 数据丢失不影响推理引擎本身运行,仅影响可观测数据完整性。
- 降级模式:eBPF 加载失败时,自动回退到基于日志的近似追踪(解析推理引擎结构化日志)。
- GPU Profiling 与 eBPF 的时间轴融合:将 CUPTI Activity Record 与 eBPF 事件在统一时间轴上呈现,实现真正的 CPU-GPU 全链路可视化。
- 智能异常检测:基于 eBPF 事件流训练轻量级异常检测模型,自动识别 KV Cache 碎片化、Prefill/Decode 调度失衡、GPU 显存带宽退化等微妙异常。
- 可观测性驱动的智能调度:将 eBPF 采集的实时 KV Cache 压力指标反馈给推理引擎调度器,实现基于系统全局最优的批处理决策——这是我们当前正在探索的下一个方向。
eBPF 的零侵入、内核级别粒度、安全沙箱特性,恰好切中这些痛点。
二、eBPF 程序架构设计
2.1 探针布局策略
我们设计了三层探针体系:
┌─────────────────────────────────────────────────────────┐
│ 用户态聚合器 │
├──────────┬──────────────────┬─────────────────────────┤
│ 网络追踪 │ 系统调用追踪 │ 内核函数追踪 │
│ (Kprobe) │ (Tracepoint) │ (Tracepoint/Kprobe) │
├──────────┼──────────────────┼─────────────────────────┤
│ tcp_send │ sys_read/write │ sched_switch │
│ tcp_recv │ sys_mmap/munmap │ mm_page_alloc/free │
│ │ sys_epoll_wait │ cuda_driver_* (uprobe) │
│ │ sys_futex │ io_uring_submit │
└──────────┴──────────────────┴─────────────────────────┘
关键选择:对推理引擎进程使用 uprobe 追踪 CUDA Driver API 调用(cuLaunchKernel、cuMemcpyHtoD),对内核关键路径使用 Tracepoint 以最小化开销。
2.2 Map 数据结构设计
// eBPF Map: 推理请求上下文
struct request_ctx {
__u64 trace_id; // 关联 OpenTelemetry Trace ID
__u32 pid; // 进程 ID
__u8 stage; // PREFILL / DECODE / TRANSFER
__u64 enter_ts; // 阶段进入时间戳 (ns)
__u64 tokens_in; // 输入 token 数
__u64 tokens_out; // 已输出 token 数
};
// eBPF Map: KV Cache 操作事件
struct kv_cache_event {
__u64 ts; // 时间戳
__u32 npages_alloc; // 分配的页数
__u32 npages_free; // 释放的页数
__u32 block_size; // 每页 token 数
__u16 seq_group_id; // 序列组 ID
};
// 提交给内核 Ring Buffer 的事件
struct observability_event {
__u32 type; // 事件类型枚举
__u64 timestamp;
__u64 trace_id;
union {
struct request_ctx req;
struct kv_cache_event kv;
};
};
三、核心追踪场景实现
3.1 Prefill/Decode 阶段切割
vLLM 的 SequenceGroup 调度器在每次迭代时区分 Prefill 和 Decode 两类任务。我们通过解析 SequenceGroup 的 is_prefill 字段来确定当前执行模式。
// 追踪 vLLM 调度器迭代
SEC("uprobe/vllm_engine_iterate")
int trace_schedule(struct pt_regs *ctx) {
struct seq_group_t *sg = (struct seq_group_t *)PT_REGS_PARM1(ctx);
__u8 is_prefload = 0;
bpf_probe_read_user(&is_prefload, sizeof(__u8),
&sg->is_prefill);
struct request_ctx rctx = {};
rctx.stage = is_prefload ? STAGE_PREFILL : STAGE_DECODE;
rctx.pid = bpf_get_current_pid_tgid() >> 32;
rctx.enter_ts = bpf_ktime_get_ns();
bpf_probe_read_user(&rctx.tokens_in, sizeof(__u64),
&sg->prompt_token_ids->size);
__u64 key = bpf_get_current_pid_tgid();
bpf_map_update_elem(&request_map, &key, &rctx, BPF_ANY);
return 0;
}
3.2 Token Generation 每 Token 延迟追踪
每生成一个 Token 涉及:GPU 完成最后一层 Attention 计算 → 取对数概率 → 采样 → 将结果放回 CPU 事件循环。关键指标"Time Per Output Token"(TPOT)的精确度直接决定用户体验。
// 追踪推理引擎每 token 生成完成事件
SEC("uprobe/generate_next_token")
int trace_token_emit(struct pt_regs *ctx) {
__u64 ts = bpf_ktime_get_ns();
__u64 pid_tgid = bpf_get_current_pid_tgid();
struct request_ctx *rctx = bpf_map_lookup_elem(&request_map, &pid_tgid);
if (!rctx) return 0;
// 提交 Token 间隔事件到用户态
struct token_event evt = {};
evt.emit_ts = ts;
evt.stage = rctx->stage;
evt.token_pos = __sync_fetch_and_add(&rctx->tokens_out, 1);
evt.trace_id = rctx->trace_id;
bpf_ringbuf_submit(&ringbuf, &evt, sizeof(evt), 0);
return 0;
}
3.3 CUDA Kernel 追踪与 CPU-GPU 时间对齐
// 追踪 cuLaunchKernel 调用
SEC("uprobe/cuLaunchKernel")
int trace_cuda_launch(struct pt_regs *ctx) {
struct cuda_launch_event evt = {};
evt.host_ts = bpf_ktime_get_ns();
// 读取 kernel 维度参数
bpf_probe_read_user(&evt.grid_x, sizeof(unsigned int),
(void *)PT_REGS_PARM5(ctx));
bpf_probe_read_user(&evt.block_x, sizeof(unsigned int),
(void *)PT_REGS_PARM6(ctx));
evt.pid = bpf_get_current_pid_tgid() >> 32;
// 记录到 pending map,等待对应事件
__u64 key = (__u64)evt.pid << 32 | bpf_get_current_pid_tgid();
bpf_map_update_elem(&cuda_pending, &key, &event, BPF_ANY);
return 0;
}
在用户态,通过 CUPTI(CUDA Profiling Tools Interface)的回调获取 GPU 侧 timestamp,建立 CPU 提交时间到 GPU 执行时间的映射关系。
3.4 KV Cache 分配的细粒度追踪
对 PageAttention 实现(基于 block_manager)进行追踪:
// 追踪 KV Block 分配
SEC("uprobe/allocate_kv_block")
int trace_kv_alloc(struct pt_regs *ctx) {
struct kv_cache_event evt = {};
evt.ts = bpf_ktime_get_ns();
// 读取分配结果
bpf_probe_read_user(&evt.npages_alloc, sizeof(__u32),
(void *)PT_REGS_RC(ctx));
evt.block_size = 16; // vLLM 默认每页 16 tokens
// 关联到当前序列组
__u64 pid_tgid = bpf_get_current_pid_tgid();
struct request_ctx *rctx = bpf_map_lookup_elem(&request_map, &pid_tgid);
if (rctx) {
evt.seq_group_id = rctx->pid;
bpf_ringbuf_submit(&ringbuf, &evt, sizeof(evt), 0);
}
return 0;
}
四、用户态聚合:从事件流到 Trace 图谱
4.1 Ring Buffer 用户态消费
# 使用 libbpf-py 消费内核 Ring Buffer
import ctypes
from bcc import BPF
class LLMInferenceTracer:
def __init__(self, bpf_prog_path):
self.bpf = BPF(src_file=bpf_prog_path)
self.bpf["ringbuf"].open_ring_buffer(self._process_event)
self.traces = {} # trace_id -> TraceBuilder
def _process_event(self, ctx, data, size):
event = ctypes.cast(data, ctypes.POINTER(ObservabilityEvent)).contents
if event.type == EVENT_REQUEST_ENTER:
trace = self.traces.setdefault(event.trace_id,
TraceBuilder(event.trace_id))
trace.add_stage(event.timestamp, event.req.stage)
elif event.type == EVENT_TOKEN_EMIT:
trace = self.traces[event.trace_id]
trace.record_token_emit(event.token_pos, event.timestamp)
elif event.type == EVENT_KV_CACHE:
trace = self.traces[event.trace_id]
trace.record_kv_usage(event.kv.npages_alloc)
def poll(self):
self.bpf.ring_buffer_poll(timeout_ms=10)
4.2 与 OpenTelemetry 集成
将 eBPF 采集的低层事件关联到 OpenTelemetry Trace 系统,实现从 HTTP 请求到 CUDA Kernel 调用的全栈联通:
# 在 FastAPI/Wsgi 中间层注入 eBPF Trace Context
class eBPFOTelMiddleware:
def __init__(self, app, tracer):
self.app = app
self.tracer = tracer
async def __call__(self, scope, receive, send):
# 从 HTTP Header 提取 OTel Trace ID
trace_id = self._extract_trace_id(scope)
# 向 eBPF Map 写入 PID -> TraceID 映射
pid = os.getpid()
self.tracer.bpf["pid_trace_map"].update(
ctypes.c_uint64(pid),
ctypes.c_uint64(trace_id)
)
await self.app(scope, receive, send)
五、关键性能指标(KPI)导出
5.1 Prometheus 指标向量
# Prometheus 导出器
INFERENCE_LATENCY = Histogram(
'llm_inference_duration_seconds',
'End-to-end request latency',
buckets=[0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60]
)
TOKEN_GENERATION_LATENCY = Histogram(
'llm_token_generation_seconds',
'Per-token generation time',
buckets=[0.005, 0.01, 0.02, 0.05, 0.1, 0.25]
)
KV_CACHE_USAGE = Gauge(
'llm_kv_cache_pages_used',
'KV Cache pages in use',
['model', 'instance']
)
PREFILL_DECODE_RATIO = Counter(
'llm_schedule_mode_total',
'Schedule mode counter',
['stage'] # 'prefill' or 'decode'
)
CUDA_QUEUE_DEPTH = Gauge(
'llm_cuda_pending_kernels',
'CUDA kernels queued but not yet executing',
['gpu_id']
)
5.2 Grafana Dashboard 面板布局
推荐按"四个黄金信号"组织:
六、性能开销与运维关注点
6.1 BPF 程序本身的开销
实测数据(A100×8, vLLM, Llama-2-70B):
| 探针类型 | 事件频率 | CPU 核占用 | 吞吐量影响 |
|---|---|---|---|
| Tracepoint (sched, mm) | ~10K/s | <0.3 核 | <0.2% |
| Uprobe (vllm 调度器) | ~2K/s | <0.1 核 | <0.1% |
| Uprobe (CUDA driver) | ~5K/s | <0.2 核 | <0.1% |
| Ring Buffer 提交 | ~15K/s | <0.2 核 | - |
| 用户态聚合 | - | <1 核 | - |
总体增加约 1.8 核 CPU 开销,对推理吞吐量影响不到 0.4%——比传统 APM Agent 的 5-10% 开销低一个数量级。
6.2 关键运维注意事项
七、生产级部署模式
7.1 DaemonSet 模式(Kubernetes 环境)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: llm-observability-agent
spec:
selector:
matchLabels:
app: llm-ebpf-observability
template:
spec:
hostPID: true
hostNetwork: true
containers:
- name: ebpx-observability
image: ybb-registry/llm-observability:v2.1
securityContext:
privileged: true
resources:
requests:
cpu: "500m"
memory: "256Mi"
limits:
cpu: "2"
memory: "1Gi"
volumeMounts:
- name: bpffs
mountPath: /sys/fs/bpf
- name: modules
mountPath: /lib/modules
readOnly: true
volumes:
- name: bpffs
hostPath:
path: /sys/fs/bpf
- name: modules
hostPath:
path: /lib/modules
7.2 高可用与容错
八、未来方向
总结:eBPF 为 LLM 推理可观测性打开了一扇新窗口。它让我们能在不触碰一行推理引擎源码的前提下,窥探从 HTTP 请求到 CUDA Kernel 这一整条链路的微观行为。对于已在生产环境运行 vLLM 或 TensorRT-LLM 的团队,这将显著降低定位推理延迟抖动、KV Cache 异常、GPU 利用率不足等疑难问题的排查成本。

发表评论 取消回复