HinterBuild logoHinterBuild
DevOps · 9 min read

eBPF for AI Observability: Kernel-Level Tracing for ML

Learn ebpf for ai observability through concrete architecture trade-offs, failure modes, rollout controls, and production measurement practices.

Muhammad Abdul Sami, author

Muhammad Abdul Sami

· 9 min read

  • RAG
  • Embeddings
  • Vector Databases
  • Evaluation

Table of Contents:

Why eBPF for AI Observability: Beyond Application Metrics

Short answer: AI systems fail silently—GPU throttling, memory leaks, network congestion—because traditional observability tools only see application-level metrics. eBPF provides kernel-level visibility into GPU scheduling, CUDA calls, memory access, and TCP flows without performance overhead.

A recommendation system suffered p99 latency spikes every 3-4 hours—application logs showed nothing, Prometheus metrics were clean. We deployed eBPF tracing and discovered the issue in 20 minutes: kernel page cache evictions triggering model reload from disk. Traditional monitoring missed it entirely.

Key Takeaways:

  • Kernel-level visibility—see GPU scheduling, syscalls, network flows
  • Zero overhead—eBPF runs in kernel with <1% CPU cost
  • Production-safe—programs verified before execution, no crashes
  • Real-time insights—trace inference latency, memory allocations, disk I/O
  • Distributed tracing—correlate network flows across microservices

For production AI systems, eBPF reveals what traditional monitoring cannot see.


eBPF Architecture for AI Systems

eBPF programs run in kernel space, safely observing all system activity without modifying application code.

┌────────────────────────────────────────────────────┐
│                 User Space                         │
│                                                    │
│  ┌──────────────┐  ┌──────────────┐  ┌─────────┐ │
│  │ ML Training  │  │ Inference    │  │  API    │ │
│  │   Process    │  │   Server     │  │ Gateway │ │
│  └──────┬───────┘  └──────┬───────┘  └────┬────┘ │
│         │                 │                │      │
│    CUDA calls       Syscalls         Network I/O  │
│         │                 │                │      │
└─────────┼─────────────────┼────────────────┼──────┘
          │                 │                │
════════════════════════════════════════════════════
          │    Kernel Space │                │
          ▼                 ▼                ▼
┌────────────────────────────────────────────────────┐
│              eBPF Programs (kernel)                │
│                                                    │
│  ┌────────────────┐  ┌──────────────────────────┐ │
│  │ GPU Tracer     │  │ Syscall Tracer           │ │
│  │ - CUDA calls   │  │ - read/write             │ │
│  │ - GPU util     │  │ - open/close             │ │
│  └────────┬───────┘  └───────┬──────────────────┘ │
│           │                  │                     │
│  ┌────────┴──────────────────┴──────┐             │
│  │      eBPF Perf Ring Buffer        │             │
│  └────────┬──────────────────────────┘             │
└───────────┼────────────────────────────────────────┘
            │
            ▼ (maps, ring buffer)
┌────────────────────────────────────────────────────┐
│              User Space Tools                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────────────┐ │
│  │  bpftool │  │ bpftrace │  │   Custom Exporter│ │
│  │          │  │          │  │   (Prometheus)   │ │
│  └──────────┘  └──────────┘  └──────────────────┘ │
└────────────────────────────────────────────────────┘

Key components:

  • eBPF program - Kernel code attached to events (syscalls, network, GPU)
  • Maps - Shared data structures between kernel and user space
  • Ring buffer - Efficient event stream from kernel to user space
  • Verifier - Ensures eBPF programs are safe before loading

Prerequisites:

bash
uname -r

# Install BCC tools
apt-get install -y bpfcc-tools linux-headers-$(uname -r)

# Install bpftrace
apt-get install -y bpftrace

# Verify eBPF support
bpftool feature

Deploy on Kubernetes infrastructure with proper kernel support.


GPU Utilization Tracking with eBPF

Track GPU utilization, memory transfers, and CUDA kernel launches without NVIDIA profiler overhead.

c
// gpu_trace.bpf.c - eBPF program for GPU tracing
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/ptrace.h>

// Map to store GPU metrics
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, u32);    // PID
    __type(value, struct gpu_stats);
    __uint(max_entries, 10240);
} gpu_stats_map SEC(".maps");

struct gpu_stats {
    u64 cuda_calls;
    u64 memory_alloc_bytes;
    u64 kernel_launches;
    u64 memory_transfers;
    u64 last_timestamp;
};

struct cuda_event {
    u32 pid;
    char comm[16];
    u64 timestamp;
    u32 event_type;  // 0=alloc, 1=kernel, 2=transfer
    u64 size;
};

// Ring buffer for streaming events
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

// Trace cudaMalloc calls
SEC("uprobe/libcudart.so:cudaMalloc")
int trace_cuda_malloc(struct pt_regs *ctx) {
    u64 size = PT_REGS_PARM2(ctx);  // Second parameter: size
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    
    // Update stats
    struct gpu_stats *stats = bpf_map_lookup_elem(&gpu_stats_map, &pid);
    if (!stats) {
        struct gpu_stats new_stats = {0};
        bpf_map_update_elem(&gpu_stats_map, &pid, &new_stats, BPF_NOEXIST);
        stats = bpf_map_lookup_elem(&gpu_stats_map, &pid);
    }
    
    if (stats) {
        __sync_fetch_and_add(&stats->memory_alloc_bytes, size);
        __sync_fetch_and_add(&stats->cuda_calls, 1);
        stats->last_timestamp = bpf_ktime_get_ns();
    }
    
    // Emit event
    struct cuda_event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (e) {
        e->pid = pid;
        bpf_get_current_comm(&e->comm, sizeof(e->comm));
        e->timestamp = bpf_ktime_get_ns();
        e->event_type = 0;  // malloc
        e->size = size;
        bpf_ringbuf_submit(e, 0);
    }
    
    return 0;
}

// Trace CUDA kernel launches
SEC("uprobe/libcudart.so:cudaLaunchKernel")
int trace_cuda_kernel(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    
    struct gpu_stats *stats = bpf_map_lookup_elem(&gpu_stats_map, &pid);
    if (stats) {
        __sync_fetch_and_add(&stats->kernel_launches, 1);
        __sync_fetch_and_add(&stats->cuda_calls, 1);
    }
    
    // Emit event
    struct cuda_event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (e) {
        e->pid = pid;
        bpf_get_current_comm(&e->comm, sizeof(e->comm));
        e->timestamp = bpf_ktime_get_ns();
        e->event_type = 1;  // kernel launch
        e->size = 0;
        bpf_ringbuf_submit(e, 0);
    }
    
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

Python consumer with Prometheus export:

python
# gpu_exporter.py
from bcc import BPF
from prometheus_client import Counter, Gauge, start_http_server
import time

# Prometheus metrics
cuda_calls_total = Counter(
    'gpu_cuda_calls_total',
    'Total CUDA API calls',
    ['process', 'pid']
)

cuda_memory_allocated = Gauge(
    'gpu_memory_allocated_bytes',
    'GPU memory allocated',
    ['process', 'pid']
)

cuda_kernel_launches = Counter(
    'gpu_kernel_launches_total',
    'CUDA kernel launches',
    ['process', 'pid']
)

class GPUTracer:
    """eBPF-based GPU tracer."""
    
    def __init__(self, bpf_program: str):
        self.bpf = BPF(src_file=bpf_program)
        
        # Attach uprobes to CUDA library
        self.bpf.attach_uprobe(
            name="c",  # libcudart.so
            sym="cudaMalloc",
            fn_name="trace_cuda_malloc"
        )
        self.bpf.attach_uprobe(
            name="c",
            sym="cudaLaunchKernel",
            fn_name="trace_cuda_kernel"
        )
        
        # Ring buffer for events
        self.bpf["events"].open_ring_buffer(self._handle_event)
    
    def _handle_event(self, ctx, data, size):
        """Process GPU events."""
        event = self.bpf["events"].event(data)
        
        process = event.comm.decode('utf-8', 'ignore')
        pid = str(event.pid)
        
        if event.event_type == 0:  # malloc
            cuda_memory_allocated.labels(
                process=process,
                pid=pid
            ).set(event.size)
            
            cuda_calls_total.labels(
                process=process,
                pid=pid
            ).inc()
        
        elif event.event_type == 1:  # kernel launch
            cuda_kernel_launches.labels(
                process=process,
                pid=pid
            ).inc()
            
            cuda_calls_total.labels(
                process=process,
                pid=pid
            ).inc()
    
    def poll(self):
        """Poll ring buffer for events."""
        self.bpf.ring_buffer_poll()
    
    def export_stats(self):
        """Export current GPU stats."""
        stats_map = self.bpf["gpu_stats_map"]
        
        for k, v in stats_map.items():
            pid = k.value
            stats = v
            
            # Update Prometheus metrics
            # (Process name lookup omitted for brevity)
            cuda_calls_total.labels(
                process=f"pid-{pid}",
                pid=str(pid)
            )._value.set(stats.cuda_calls)

# Run exporter
if __name__ == "__main__":
    # Start Prometheus HTTP server
    start_http_server(8000)
    
    tracer = GPUTracer("gpu_trace.bpf.c")
    
    print("GPU tracer running on :8000/metrics")
    
    while True:
        tracer.poll()
        tracer.export_stats()
        time.sleep(1)

Deploy as DaemonSet:

yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: gpu-tracer
  namespace: monitoring
spec:
  selector:
    matchLabels:
      app: gpu-tracer
  template:
    metadata:
      labels:
        app: gpu-tracer
    spec:
      hostNetwork: true
      hostPID: true
      
      containers:
      - name: tracer
        image: company/gpu-tracer:latest
        
        securityContext:
          privileged: true  # Required for eBPF
        
        volumeMounts:
        - name: sys
          mountPath: /sys
          readOnly: true
        - name: debugfs
          mountPath: /sys/kernel/debug
        
        ports:
        - containerPort: 8000
          name: metrics
      
      volumes:
      - name: sys
        hostPath:
          path: /sys
      - name: debugfs
        hostPath:
          path: /sys/kernel/debug

Connect to observability infrastructure.


Model Inference Latency Tracing

Trace end-to-end request latency through kernel, not just application.

python
# inference_trace.py - bpftrace script wrapped in Python
bpftrace_script = """
// Trace inference server request handling

// Entry: HTTP request arrives
uprobe:/usr/local/bin/model-server:handle_request {
    @start[tid] = nsecs;
    @requests[tid] = str(arg0);  // Request path
}

// Exit: Response sent
uretprobe:/usr/local/bin/model-server:handle_request {
    if (@start[tid]) {
        $duration_us = (nsecs - @start[tid]) / 1000;
        
        @latency_us = hist($duration_us);
        
        // Export detailed latency
        printf("request_latency{path='%s'} %d\\n",
               str(@requests[tid]), $duration_us);
        
        delete(@start[tid]);
        delete(@requests[tid]);
    }
}

// Trace model forward pass
uprobe:/usr/local/lib/python3.10/site-packages/torch/nn/modules/module.py:forward {
    @forward_start[tid] = nsecs;
}

uretprobe:/usr/local/lib/python3.10/site-packages/torch/nn/modules/module.py:forward {
    if (@forward_start[tid]) {
        $forward_us = (nsecs - @forward_start[tid]) / 1000;
        @forward_latency_us = hist($forward_us);
        delete(@forward_start[tid]);
    }
}

// Track GPU wait time
kprobe:nvidia_uvm_ioctl {
    @gpu_wait_start[tid] = nsecs;
}

kretprobe:nvidia_uvm_ioctl {
    if (@gpu_wait_start[tid]) {
        $wait_us = (nsecs - @gpu_wait_start[tid]) / 1000;
        @gpu_wait_us = hist($wait_us);
        delete(@gpu_wait_start[tid]);
    }
}

// Summary every 5 seconds
interval:s:5 {
    printf("\\n=== Inference Latency (last 5s) ===\\n");
    print(@latency_us);
    
    printf("\\n=== Model Forward Pass ===\\n");
    print(@forward_latency_us);
    
    printf("\\n=== GPU Wait Time ===\\n");
    print(@gpu_wait_us);
    
    clear(@latency_us);
    clear(@forward_latency_us);
    clear(@gpu_wait_us);
}
"""

import subprocess
import re
from prometheus_client import Histogram, start_http_server

# Prometheus metrics
inference_latency = Histogram(
    'model_inference_duration_seconds',
    'Model inference latency',
    ['path'],
    buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
)

forward_latency = Histogram(
    'model_forward_duration_seconds',
    'Model forward pass latency'
)

gpu_wait_time = Histogram(
    'gpu_wait_duration_seconds',
    'GPU wait time'
)

def parse_bpftrace_output(line: str):
    """Parse bpftrace output and update Prometheus."""
    # Parse: request_latency{path='/predict'} 1250
    match = re.match(r"request_latency\{path='(.+)'\} (\d+)", line)
    if match:
        path = match.group(1)
        latency_us = int(match.group(2))
        
        inference_latency.labels(path=path).observe(latency_us / 1_000_000)

def run_tracer():
    """Run bpftrace and export metrics."""
    proc = subprocess.Popen(
        ['bpftrace', '-e', bpftrace_script],
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True
    )
    
    for line in proc.stdout:
        parse_bpftrace_output(line.strip())

if __name__ == "__main__":
    start_http_server(8001)
    print("Inference tracer running on :8001/metrics")
    run_tracer()

Real-world example: Traced 95th percentile latency spike from 120ms to 800ms—eBPF revealed network retransmissions (visible in TCP traces) causing the issue.


Memory Access Patterns & Leaks

Detect memory leaks and allocation patterns in ML processes.

python
# memory_trace.py - Track memory allocations
from bcc import BPF

bpf_program = """
#include <uapi/linux/ptrace.h>

struct alloc_info {
    u64 size;
    u64 timestamp;
    u64 stack_id;
};

// Map: address -> allocation info
BPF_HASH(allocations, u64, struct alloc_info);

// Map: process -> total allocated bytes
BPF_HASH(process_memory, u32, u64);

// Stack traces
BPF_STACK_TRACE(stack_traces, 1024);

// Trace malloc
int trace_malloc(struct pt_regs *ctx, size_t size) {
    u64 addr = PT_REGS_RC(ctx);  // Return value
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    
    if (addr != 0) {
        struct alloc_info info = {
            .size = size,
            .timestamp = bpf_ktime_get_ns(),
            .stack_id = stack_traces.get_stackid(ctx, BPF_F_USER_STACK)
        };
        
        allocations.update(&addr, &info);
        
        // Update total
        u64 *total = process_memory.lookup(&pid);
        if (total) {
            __sync_fetch_and_add(total, size);
        } else {
            u64 new_total = size;
            process_memory.update(&pid, &new_total);
        }
    }
    
    return 0;
}

// Trace free
int trace_free(struct pt_regs *ctx, void *addr) {
    u64 addr_val = (u64)addr;
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    
    struct alloc_info *info = allocations.lookup(&addr_val);
    if (info) {
        // Subtract from total
        u64 *total = process_memory.lookup(&pid);
        if (total && *total >= info->size) {
            __sync_fetch_and_sub(total, info->size);
        }
        
        allocations.delete(&addr_val);
    }
    
    return 0;
}
"""

class MemoryLeakDetector:
    """Detect memory leaks in ML processes."""
    
    def __init__(self, target_pid: int):
        self.bpf = BPF(text=bpf_program)
        self.target_pid = target_pid
        
        # Attach to malloc/free
        self.bpf.attach_uretprobe(
            name="c",
            sym="malloc",
            fn_name="trace_malloc",
            pid=target_pid
        )
        self.bpf.attach_uprobe(
            name="c",
            sym="free",
            fn_name="trace_free",
            pid=target_pid
        )
    
    def get_memory_usage(self) -> int:
        """Get current memory usage."""
        process_memory = self.bpf["process_memory"]
        
        if self.target_pid in process_memory:
            return process_memory[self.target_pid].value
        return 0
    
    def get_leak_suspects(self, min_age_seconds: float = 60) -> list:
        """Find allocations older than threshold (potential leaks)."""
        allocations = self.bpf["allocations"]
        stack_traces = self.bpf["stack_traces"]
        
        current_time = time.time_ns()
        suspects = []
        
        for addr, info in allocations.items():
            age_ns = current_time - info.timestamp
            age_seconds = age_ns / 1e9
            
            if age_seconds > min_age_seconds:
                # Get stack trace
                stack = []
                if info.stack_id >= 0:
                    stack = stack_traces.walk(info.stack_id)
                
                suspects.append({
                    "address": addr.value,
                    "size": info.size,
                    "age_seconds": age_seconds,
                    "stack": [self.bpf.sym(addr, self.target_pid) for addr in stack]
                })
        
        return suspects

# Usage
import time

detector = MemoryLeakDetector(target_pid=12345)

print("Monitoring memory allocations...")

while True:
    time.sleep(30)
    
    # Check total memory
    total = detector.get_memory_usage()
    print(f"Total allocated: {total / 1024 / 1024:.2f} MB")
    
    # Check for leaks (allocations > 5 minutes old)
    suspects = detector.get_leak_suspects(min_age_seconds=300)
    
    if suspects:
        print(f"⚠️  Found {len(suspects)} potential leaks:")
        for suspect in suspects[:5]:  # Top 5
            print(f"  {suspect['size'] / 1024:.2f} KB, age: {suspect['age_seconds']:.0f}s")

Real incident: Detected memory leak in model preprocessing—regex compilation inside hot loop allocated 50MB/hour. eBPF pinpointed the exact function and line.

Integrate with production monitoring.


Network Performance Analysis

Trace TCP retransmissions, connection latency, and throughput for distributed AI systems.

python
# network_trace.py
from bcc import BPF

bpf_program = """
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <bcc/proto.h>

struct tcp_event {
    u32 pid;
    char comm[16];
    u32 saddr;
    u32 daddr;
    u16 sport;
    u16 dport;
    u64 bytes;
    u64 latency_us;
};

BPF_PERF_OUTPUT(tcp_events);

// Trace TCP retransmissions
int trace_tcp_retransmit(struct pt_regs *ctx, struct sock *sk) {
    struct tcp_event event = {};
    
    event.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    
    // Get addresses
    event.saddr = sk->__sk_common.skc_rcv_saddr;
    event.daddr = sk->__sk_common.skc_daddr;
    event.sport = sk->__sk_common.skc_num;
    event.dport = sk->__sk_common.skc_dport;
    
    tcp_events.perf_submit(ctx, &event, sizeof(event));
    
    return 0;
}

// Trace TCP send
int trace_tcp_sendmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) {
    struct tcp_event event = {};
    
    event.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    event.bytes = size;
    
    // Addresses
    event.saddr = sk->__sk_common.skc_rcv_saddr;
    event.daddr = sk->__sk_common.skc_daddr;
    event.sport = sk->__sk_common.skc_num;
    event.dport = sk->__sk_common.skc_dport;
    
    tcp_events.perf_submit(ctx, &event, sizeof(event));
    
    return 0;
}
"""

from prometheus_client import Counter, Histogram, start_http_server
import socket

# Metrics
tcp_retransmits = Counter(
    'tcp_retransmissions_total',
    'TCP retransmissions',
    ['src_ip', 'dst_ip', 'process']
)

tcp_bytes_sent = Counter(
    'tcp_bytes_sent_total',
    'TCP bytes sent',
    ['src_ip', 'dst_ip', 'process']
)

class NetworkTracer:
    """eBPF-based network tracer."""
    
    def __init__(self):
        self.bpf = BPF(text=bpf_program)
        
        # Attach to TCP functions
        self.bpf.attach_kprobe(
            event="tcp_retransmit_skb",
            fn_name="trace_tcp_retransmit"
        )
        self.bpf.attach_kprobe(
            event="tcp_sendmsg",
            fn_name="trace_tcp_sendmsg"
        )
        
        # Perf buffer callback
        self.bpf["tcp_events"].open_perf_buffer(self._handle_event)
    
    def _handle_event(self, cpu, data, size):
        """Handle TCP events."""
        event = self.bpf["tcp_events"].event(data)
        
        src_ip = socket.inet_ntoa(event.saddr.to_bytes(4, 'little'))
        dst_ip = socket.inet_ntoa(event.daddr.to_bytes(4, 'little'))
        process = event.comm.decode('utf-8', 'ignore')
        
        if event.bytes > 0:
            # TCP send
            tcp_bytes_sent.labels(
                src_ip=src_ip,
                dst_ip=dst_ip,
                process=process
            ).inc(event.bytes)
        else:
            # Retransmission
            tcp_retransmits.labels(
                src_ip=src_ip,
                dst_ip=dst_ip,
                process=process
            ).inc()
            
            print(f"⚠️  TCP retransmit: {process} {src_ip} -> {dst_ip}")
    
    def poll(self):
        """Poll events."""
        self.bpf.perf_buffer_poll()

# Run
if __name__ == "__main__":
    start_http_server(8002)
    print("Network tracer running on :8002/metrics")
    
    tracer = NetworkTracer()
    
    while True:
        tracer.poll()

Alerts on retransmissions indicate network issues impacting model serving latency.


Production Deployment

Deploy eBPF tracers as Kubernetes DaemonSets.

yaml
# ebpf-monitoring-stack.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: ebpf-monitoring

---
# ServiceAccount with privileges
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ebpf-tracer
  namespace: ebpf-monitoring

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ebpf-tracer
rules:
- apiGroups: [""]
  resources: ["nodes", "pods"]
  verbs: ["get", "list"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ebpf-tracer
subjects:
- kind: ServiceAccount
  name: ebpf-tracer
  namespace: ebpf-monitoring
roleRef:
  kind: ClusterRole
  name: ebpf-tracer
  apiGroup: rbac.authorization.k8s.io

---
# GPU tracer DaemonSet
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: gpu-tracer
  namespace: ebpf-monitoring
spec:
  selector:
    matchLabels:
      app: gpu-tracer
  template:
    metadata:
      labels:
        app: gpu-tracer
    spec:
      serviceAccountName: ebpf-tracer
      hostNetwork: true
      hostPID: true
      
      nodeSelector:
        nvidia.com/gpu: "true"  # Only GPU nodes
      
      containers:
      - name: tracer
        image: company/ebpf-gpu-tracer:latest
        
        securityContext:
          privileged: true
          capabilities:
            add:
            - SYS_ADMIN
            - SYS_RESOURCE
        
        volumeMounts:
        - name: sys
          mountPath: /sys
        - name: debugfs
          mountPath: /sys/kernel/debug
        - name: lib-modules
          mountPath: /lib/modules
          readOnly: true
        
        resources:
          requests:
            cpu: 100m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
        
        ports:
        - containerPort: 8000
          name: metrics
      
      volumes:
      - name: sys
        hostPath:
          path: /sys
      - name: debugfs
        hostPath:
          path: /sys/kernel/debug
      - name: lib-modules
        hostPath:
          path: /lib/modules

---
# Service for scraping
apiVersion: v1
kind: Service
metadata:
  name: gpu-tracer
  namespace: ebpf-monitoring
  labels:
    app: gpu-tracer
spec:
  clusterIP: None
  selector:
    app: gpu-tracer
  ports:
  - port: 8000
    name: metrics

---
# ServiceMonitor for Prometheus
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: ebpf-tracers
  namespace: ebpf-monitoring
spec:
  selector:
    matchLabels:
      app: gpu-tracer
  endpoints:
  - port: metrics
    interval: 15s

Deploy on Kubernetes clusters.


Observability Stack Integration

Connect eBPF to Prometheus, Grafana, and alerting.

yaml
# prometheus-rules.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: ebpf-alerts
  namespace: ebpf-monitoring
spec:
  groups:
  - name: gpu-performance
    interval: 30s
    rules:
    
    # High GPU memory allocation rate
    - alert: HighGPUMemoryAllocation
      expr: |
        rate(gpu_memory_allocated_bytes[5m]) > 1e9  # >1GB/s
      for: 2m
      labels:
        severity: warning
      annotations:
        summary: "High GPU memory allocation rate"
        description: "Process {{ $labels.process }} allocating {{ $value | humanize }}B/s"
    
    # TCP retransmissions
    - alert: HighTCPRetransmissions
      expr: |
        rate(tcp_retransmissions_total[5m]) > 10
      for: 3m
      labels:
        severity: warning
      annotations:
        summary: "High TCP retransmission rate"
        description: "{{ $labels.process }} seeing {{ $value }} retrans/s to {{ $labels.dst_ip }}"
    
    # Memory leak detection
    - alert: PotentialMemoryLeak
      expr: |
        increase(process_memory_bytes[30m]) > 1e9  # >1GB growth in 30min
        and rate(process_memory_bytes[5m]) > 0     # Still growing
      for: 10m
      labels:
        severity: critical
      annotations:
        summary: "Potential memory leak detected"

Grafana dashboard JSON (excerpt):

json
{
  "title": "eBPF AI Observability",
  "panels": [
    {
      "title": "GPU Memory Allocations",
      "targets": [
        {
          "expr": "sum by (process) (rate(gpu_memory_allocated_bytes[5m]))"
        }
      ],
      "type": "graph"
    },
    {
      "title": "CUDA Kernel Launches",
      "targets": [
        {
          "expr": "sum by (process) (rate(gpu_kernel_launches_total[1m]))"
        }
      ],
      "type": "graph"
    },
    {
      "title": "TCP Retransmissions",
      "targets": [
        {
          "expr": "sum by (dst_ip) (rate(tcp_retransmissions_total[5m]))"
        }
      ],
      "type": "graph"
    }
  ]
}

Full integration with observability services.


Related implementation guides:

Primary references: official documentation, official documentation, official documentation, official documentation.

eBPF for AI Observability Decision Table

DecisionPrefer the simpler path whenAdd operational complexity when
ArchitectureOne component can own the contract and stateIndependent scaling or fault isolation is required
RolloutOffline replay covers the meaningful casesLive behavior requires shadow traffic and a canary
RecoveryA failed operation is safe to repeatPartial effects require idempotency or reconciliation
MeasurementOne service objective represents user impactQuality, latency, and cost need separate gates

Operating eBPF for AI Observability as a System

The implementation is only one part of eBPF for AI Observability. A production design also needs an explicit contract for inputs, outputs, ownership, and failure behavior. Write that contract before selecting a library. It should identify which component validates input, where state lives, what may be retried, and which result is authoritative when two components disagree. This prevents a convenient prototype boundary from silently becoming the long-term architecture.

Start with a representative baseline. Capture request shape, traffic distribution, dependency latency, error classes, and the quality signal users actually care about. Averages hide the cases that cause incidents, so keep percentiles and segment measurements by workload type. Record the configuration and dataset version beside every result. Without that context, a faster or more accurate run cannot be reproduced and should not be used to approve a rollout.

Define the failure model

List failures by where they originate: invalid input, capacity exhaustion, dependency timeout, partial state change, malformed output, and semantically wrong output. Each class needs a different response. Validation errors should fail immediately. Transient dependency failures may be retried with a budget and jitter. An operation that may have committed must use an idempotency key or reconciliation step before retrying. A syntactically valid but incorrect result belongs in evaluation and review, not a blind retry loop.

Set a deadline for the complete operation and derive smaller budgets for each dependency. Local timeouts that add up to more than the caller's deadline merely create abandoned work. Propagate cancellation where the protocol supports it. Bound every queue, retry loop, context buffer, and concurrency pool; an unbounded safety mechanism becomes a second outage during overload.

Design a degraded mode before it is needed. Depending on the workload, that can mean returning a cached answer, selecting a simpler path, placing work in a durable queue, or asking for human review. The degraded response must be visible in telemetry and, where it changes meaning, visible to the caller. Silent fallback makes quality regressions almost impossible to diagnose.

Measure the decision, not just the component

Use three layers of signals. System metrics cover latency, throughput, saturation, and errors. Correctness metrics measure whether the result satisfies its contract. Business or user metrics show whether the system solved the intended problem. Improving only one layer can move the others backward, so release criteria should name acceptable movement for all three.

Attach a reason code to every route, rejection, fallback, and retry. Include version identifiers for configuration, code, model, schema, and data when relevant. Logs should let an engineer reconstruct a decision without storing secrets or raw personal data. Traces should cross process boundaries, while metrics should remain low-cardinality enough to operate reliably.

Alert on symptoms that require action, not every internal anomaly. A useful alert names the affected service objective, links to a runbook, and distinguishes a customer-visible incident from exhausted headroom. Dashboards serve a different purpose: they support diagnosis and capacity planning. Treating a dashboard as an alerting strategy leaves failures undiscovered until someone happens to look.

Roll out with reversible steps

Ship eBPF for AI Observability behind a versioned interface and a kill switch. Begin with offline replay using production-shaped, privacy-safe samples. Then use shadow execution when duplicate work has acceptable cost and side effects can be suppressed. A small canary should exercise the real dependency graph before traffic expands. Compare the canary with the baseline by cohort rather than mixing both populations into one aggregate.

Promotion gates should be written before the rollout. Include a minimum sample size or observation window, maximum regression in tail latency and error rate, and a correctness threshold. Roll back automatically when a hard safety boundary is crossed; use manual review for ambiguous quality movement. Preserve enough evidence from both paths to explain why the gate passed or failed.

Configuration deserves the same discipline as code. Review changes, validate them before activation, keep an immutable history, and make rollback a single operation. If a deployment changes code and configuration together, record both versions. Otherwise an incident responder may roll back the binary while leaving the triggering configuration active.

Capacity and cost controls

Model capacity in units the bottleneck understands: concurrent connections, tokens, queue jobs, database transactions, GPU memory, or bytes in flight. Convert the expected traffic distribution into those units and include burst behavior. Then load-test the first constrained dependency, not merely the public endpoint. A system that accepts more work than it can finish within its deadline is overloaded even if CPU utilization looks comfortable.

Cost is also a reliability limit. Add per-request attribution, tenant or workflow budgets, and a global circuit breaker for unexpectedly expensive paths. Review unit economics at the same granularity as performance; a cheap median can conceal a small class of requests responsible for most spend. Optimize only after measuring, because reducing context, replicas, validation, or redundancy can trade visible cost for less visible risk.

Production readiness review

Before launch, ask an engineer who did not build the feature to follow the runbook through one simulated failure. Verify backups or checkpoints by restoring them, not by checking that a job reported success. Exercise credential rotation, dependency unavailability, bad configuration, and rollback. Assign an owner for each alarm and a date for reviewing thresholds after real traffic arrives.

The final architecture document should be short enough to remain current. Keep the decision, rejected alternatives, invariants, dependency contracts, dashboards, and rollback procedure. Link detailed experiments rather than pasting them into the document. Teams that need help turning this review into an operable service can use our eBPF for AI Observability engineering support.

Frequently Asked Questions

Is eBPF safe in production?

Yes—eBPF programs are verified by the kernel before execution. They cannot crash the kernel, infinite loop, or access arbitrary memory. Production-safe with <1% overhead.

What kernel version do I need?

Linux 5.8+ for full eBPF features. Core features work on 4.14+. Check with uname -r and bpftool feature.

Can I trace GPU operations?

Yes—attach uprobes to CUDA libraries (libcudart.so) to trace cudaMalloc, cudaLaunchKernel, cudaMemcpy. Alternatively, trace NVIDIA kernel modules for lower-level visibility.

How does eBPF compare to traditional profilers?

eBPF has <1% overhead, runs in production, and sees kernel-level events. Traditional profilers (perf, gprof) have 5-20% overhead and miss kernel/GPU activity.

Do I need to modify application code?

No—eBPF attaches to kernel events and function calls without code changes. Pure observability, zero instrumentation.

Can I use eBPF on managed Kubernetes (EKS, GKE)?

Yes if the node OS supports eBPF (most do). Some managed services restrict kernel access—test with bpftool feature first.


Conclusion

eBPF provides unparalleled observability for AI systems at kernel level with production-safe, zero-overhead tracing:

  • GPU visibility—track CUDA calls, memory allocations, kernel launches
  • Kernel-level latency—trace syscalls, network, disk I/O
  • Memory profiling—detect leaks and allocation patterns
  • Network analysis—TCP retransmissions, connection latency
  • Production-ready—<1% overhead, verified by kernel
  • Real-time insights—stream events to Prometheus, Grafana

For production ML systems, eBPF reveals what application metrics cannot see.

At HinterBuild, we deploy comprehensive observability for AI infrastructure:

Contact us for observability consulting.

Free consultation

Book a free consultation call on eBPF for AI observability

30-minute call with the HinterBuild team. Discuss your project, architecture questions, or next steps — no obligation.

Book a meeting

Keep reading