eBPF × LLM 推理:零侵入全链路可观测性工程实践

传统 APM 工具在面对大规模 LLM 推理集群时力不从心——高采样率下的 CPU 开销、复杂的异步调用链追踪、GPU 与 CPU 协同视野的缺失。本文深入探讨如何利用 eBPF 技术,在不修改推理引擎源码的前提下,构建一套覆盖请求入口、Token 调度、GPU 计算、内存分配的全链路可观测平台。

一、AI 推理可观测性的核心挑战

现代 LLM 推理引擎(vLLM、TensorRT-LLM、TGI)的架构远比传统 Web 服务复杂:

  1. 多阶段流水线:HTTP 请求 → Tokenizer → Scheduler → Model Forward → Detokenizer → Streaming Response,每个阶段的延迟开销呈现高度动态性。
    1. 连续批处理(Continuous Batching)的追踪困境:同一时刻一个 batch 中可能混合了处于 Prefill 阶段的新请求和处于 Decode 阶段的持续生成请求,传统方法难以区分两种模式的计算特征。
      1. CPU-GPU 异步边界模糊:CUDA Kernel 提交与 GPU 实际计算之间存在排队延迟,常规工具无法在时间轴上精确对齐 CPU 侧调度事件和 GPU 侧执行事件。
        1. KV Cache 内存管理的微观行为:PageAttention 引入的分页机制使得内存分配/释放呈现出碎片化模式,这种内存在传统指标中完全隐形。
        2. 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 面板布局

          推荐按"四个黄金信号"组织:

          • 流量层:QPS、并发序列数、输入/输出 Token 速率
          • 延迟层:TTFT(Time-To-First-Token)、TPOT P50/P99、E2E P99
          • 错误层:OOM 事件、CUDA OOM、超时中断
          • 饱和度层:KV Cache 使用率、CUDA 队列深度、显存带宽利用率

          六、性能开销与运维关注点

          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 关键运维注意事项

          1. BTF 兼容性确保:部署目标需开启 CONFIG_DEBUG_INFO_BTF=y,建议内核 5.4+。可携带 vmlinux.h 处理跨内核兼容。
            1. eBPF 验证器边界:复杂循环需显式设置 #pragma unroll,避免验证器拒绝加载。
              1. TLS 终止点处理:若使用 TLS 加密流量,需在应用层解密后追踪,或利用 eBPF 的 BPF_PROG_TYPE_SK_LOOKUP 在 Socket 层追踪明文(仅限本机通信)。
                1. CUDA Driver API 链接注意:libcudart.so 与 libcuda.so 不同函数入口需完整覆盖。
                  1. Ring Buffer 溢出处理:高负载场景下增加 buffer 大小或采用 BPF_MAP_TYPE_PERF_EVENT_ARRAY 配合批量消费。
                  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 高可用与容错

                    • BPF 程序热加载:利用 bpf_link 的独立 attach,实现不停机更新探针逻辑。
                    • 用户态无状态设计:Ring Buffer 数据丢失不影响推理引擎本身运行,仅影响可观测数据完整性。
                    • 降级模式:eBPF 加载失败时,自动回退到基于日志的近似追踪(解析推理引擎结构化日志)。

                    八、未来方向

                    1. GPU Profiling 与 eBPF 的时间轴融合:将 CUPTI Activity Record 与 eBPF 事件在统一时间轴上呈现,实现真正的 CPU-GPU 全链路可视化。
                      1. 智能异常检测:基于 eBPF 事件流训练轻量级异常检测模型,自动识别 KV Cache 碎片化、Prefill/Decode 调度失衡、GPU 显存带宽退化等微妙异常。
                        1. 可观测性驱动的智能调度:将 eBPF 采集的实时 KV Cache 压力指标反馈给推理引擎调度器,实现基于系统全局最优的批处理决策——这是我们当前正在探索的下一个方向。

                        2. 总结:eBPF 为 LLM 推理可观测性打开了一扇新窗口。它让我们能在不触碰一行推理引擎源码的前提下,窥探从 HTTP 请求到 CUDA Kernel 这一整条链路的微观行为。对于已在生产环境运行 vLLM 或 TensorRT-LLM 的团队,这将显著降低定位推理延迟抖动、KV Cache 异常、GPU 利用率不足等疑难问题的排查成本。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部