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
· 9 min read
- RAG
- Embeddings
- Vector Databases
- Evaluation
Table of Contents:
- Why eBPF for AI Observability
- eBPF Architecture Overview
- GPU Utilization Tracking
- Model Inference Latency Tracing
- Memory Access Patterns
- Network Performance Analysis
- Production Deployment
- Observability Stack Integration
- Frequently Asked Questions
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:
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.
// 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:
# 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:
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.
# 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.
# 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.
# 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.
# 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.
# 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):
{
"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:
- Agent Observability Tracing Decisions
- Rag Pipeline Observability Tracing
- Ai Cicd Pipeline Test Prompts Deploy
Primary references: official documentation, official documentation, official documentation, official documentation.
eBPF for AI Observability Decision Table
| Decision | Prefer the simpler path when | Add operational complexity when |
|---|---|---|
| Architecture | One component can own the contract and state | Independent scaling or fault isolation is required |
| Rollout | Offline replay covers the meaningful cases | Live behavior requires shadow traffic and a canary |
| Recovery | A failed operation is safe to repeat | Partial effects require idempotency or reconciliation |
| Measurement | One service objective represents user impact | Quality, 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:
- Observability & Monitoring
- Kubernetes Platform Engineering
- Cloud Infrastructure & DevOps
- AI Agent Development
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
Related articles
RAG Pipeline Observability & Tracing
Learn rag pipeline observability & tracing through concrete architecture trade-offs, failure modes, rollout controls, and production measurement practices.
Read post
Internal Developer Platform for AI Teams
Internal Developer Platform for AI Teams guidance for engineers: compare architecture choices, avoid failure modes, and ship a measurable, reliable.
Read post
Service Mesh: Do You Actually Need Istio in ?
Service Mesh guidance for engineers: compare architecture choices, avoid failure modes, and ship a measurable, reliable production implementation.
Read post
GitHub Actions vs GitLab CI: Comparison for Production
Learn github actions vs gitlab ci through concrete architecture trade-offs, failure modes, rollout controls, and production measurement practices.
Read post
