- Posted on
- • Artificial Intelligence
Artificial Intelligence Observability for Linux
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence Observability for Linux: From GPU To Trace In 30 Minutes
AI workloads don’t fail quietly. They slow down because a GPU is starved by I/O, a kernel throttles CPUs, or a tiny refactor adds a surprise 200 ms to every inference. Without observability on Linux—the platform most of us run for training and inference—you’re flying blind.
This article shows a pragmatic, bash-first path to AI observability on Linux. You’ll set up host and GPU metrics, add app-level telemetry, and learn a couple of eBPF one-liners you’ll actually use when things go sideways. All commands are given for apt, dnf, and zypper where relevant.
Why observability for AI on Linux?
AI performance depends on the system: CPU scheduling, NUMA, PCIe bandwidth, disk throughput, and GPU memory. You need metrics beyond your framework logs.
Reproducibility: training runs and inference services behave differently across kernels, drivers, and containers. System and app telemetry closes the gap.
Cost-control: catching underutilized GPUs or memory thrash early can save real money.
What we’ll build (the 80/20 stack)
Host metrics with Prometheus + Node Exporter
GPU metrics via a tiny Python “gpu-exporter” that wraps
nvidia-smiApp-level metrics in your AI code (request latency, batch size, model-specific counters)
Deep-dive diagnostics with bpftrace (eBPF) one-liners
Optional: basic Prometheus queries you can paste into your dashboard
Everything runs on bare-metal Linux or VMs. No Kubernetes required.
Step 1 — Install Prometheus and Node Exporter
Prometheus scrapes metrics; Node Exporter exposes host metrics (CPU, memory, disk, network).
Install Prometheus:
- apt (Debian/Ubuntu):
sudo apt update
sudo apt install -y prometheus
- dnf (Fedora/RHEL/CentOS Stream; enable EPEL on RHEL derivatives if needed):
sudo dnf install -y prometheus
- zypper (openSUSE/SLE):
sudo zypper -n install prometheus
Install Node Exporter:
- apt:
sudo apt install -y prometheus-node-exporter
- dnf:
sudo dnf install -y node_exporter || sudo dnf install -y prometheus-node-exporter
- zypper:
sudo zypper -n install prometheus-node_exporter || sudo zypper -n install prometheus-node-exporter
Enable services:
# Prometheus
sudo systemctl enable --now prometheus
# Detect Node Exporter service name and enable it
NODE_SVC=$(systemctl list-unit-files | awk '/node.*exporter/ {print $1; exit}')
[ -n "$NODE_SVC" ] && sudo systemctl enable --now "$NODE_SVC" || echo "Node Exporter service not found"
Quick checks:
# Prometheus
curl -fsS localhost:9090/-/ready && echo "prometheus OK"
# Node Exporter (usually 9100)
curl -fsS localhost:9100/metrics | head
Step 2 — Add GPU metrics with a lightweight exporter
NVIDIA GPUs: we’ll publish metrics by parsing nvidia-smi and serving them on http://localhost:9400 for Prometheus to scrape.
Install Python tooling:
- apt:
sudo apt install -y python3 python3-pip
- dnf:
sudo dnf install -y python3 python3-pip
- zypper:
sudo zypper -n install python3 python3-pip
Install dependencies:
sudo pip3 install --upgrade prometheus_client
Create the exporter:
sudo tee /usr/local/bin/gpu_exporter.py >/dev/null <<'PY'
#!/usr/bin/env python3
import os
import time
import subprocess
from http.server import HTTPServer, BaseHTTPRequestHandler
from prometheus_client import CollectorRegistry, Gauge, generate_latest
HOST = os.environ.get("GPU_EXPORTER_HOST", "0.0.0.0")
PORT = int(os.environ.get("GPU_EXPORTER_PORT", "9400"))
def nvidia_metrics():
cmd = [
"nvidia-smi",
"--query-gpu=uuid,index,utilization.gpu,utilization.memory,memory.total,memory.used,temperature.gpu,power.draw",
"--format=csv,noheader,nounits",
]
try:
out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True, timeout=2.0)
except Exception:
return []
rows = []
for line in out.strip().splitlines():
parts = [p.strip() for p in line.split(",")]
if len(parts) < 8:
continue
rows.append({
"uuid": parts[0],
"index": parts[1],
"util_gpu": float(parts[2]),
"util_mem": float(parts[3]),
"mem_total": float(parts[4]),
"mem_used": float(parts[5]),
"temp": float(parts[6]),
"power": float(parts[7]),
})
return rows
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != "/metrics":
self.send_response(404); self.end_headers(); return
reg = CollectorRegistry()
g_util = Gauge("gpu_utilization_percent", "GPU utilization percent", ["gpu", "uuid"], registry=reg)
g_memu = Gauge("gpu_memory_used_bytes", "GPU memory used (bytes)", ["gpu", "uuid"], registry=reg)
g_memt = Gauge("gpu_memory_total_bytes", "GPU memory total (bytes)", ["gpu", "uuid"], registry=reg)
g_temp = Gauge("gpu_temperature_celsius", "GPU temperature C", ["gpu", "uuid"], registry=reg)
g_pwr = Gauge("gpu_power_watts", "GPU power draw W", ["gpu", "uuid"], registry=reg)
g_memu_pct = Gauge("gpu_memory_utilization_percent", "GPU memory utilization percent", ["gpu", "uuid"], registry=reg)
for row in nvidia_metrics():
labels = {"gpu": row["index"], "uuid": row["uuid"]}
g_util.labels(**labels).set(row["util_gpu"])
g_memu.labels(**labels).set(row["mem_used"] * 1024 * 1024)
g_memt.labels(**labels).set(row["mem_total"] * 1024 * 1024)
g_temp.labels(**labels).set(row["temp"])
g_pwr.labels(**labels).set(row["power"])
if row["mem_total"] > 0:
g_memu_pct.labels(**labels).set(100.0 * row["mem_used"] / row["mem_total"])
content = generate_latest(reg)
self.send_response(200)
self.send_header("Content-Type", "text/plain; version=0.0.4")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
def main():
server = HTTPServer((HOST, PORT), Handler)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()
PY
sudo chmod +x /usr/local/bin/gpu_exporter.py
Add a systemd service:
sudo tee /etc/systemd/system/gpu-exporter.service >/dev/null <<'UNIT'
[Unit]
Description=GPU Exporter (nvidia-smi -> Prometheus)
After=network.target
[Service]
ExecStart=/usr/bin/env python3 /usr/local/bin/gpu_exporter.py
User=nobody
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now gpu-exporter
curl -fsS localhost:9400/metrics | head
Update Prometheus scrape config:
Backup and replace a minimal config (adjust if you already have one):
sudo cp /etc/prometheus/prometheus.yml /etc/prometheus/prometheus.yml.bak.$(date +%s) 2>/dev/null || true
sudo tee /etc/prometheus/prometheus.yml >/dev/null <<'YAML'
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "node"
static_configs:
- targets: ["localhost:9100"]
- job_name: "gpu"
static_configs:
- targets: ["localhost:9400"]
YAML
sudo systemctl restart prometheus
Open Prometheus at http://localhost:9090 and try these queries:
GPU utilization:
gpu_utilization_percentGPU memory used (GiB):
gpu_memory_used_bytes / 1024^3CPU usage:
1 - avg by(instance)(irate(node_cpu_seconds_total{mode="idle"}[5m]))
Tip: If you run Prometheus on a different host, change targets to point at your AI host’s IP.
Step 3 — Add app-level metrics (Python example)
Expose latency, throughput, and batch size from your inference/training code.
Install in your project venv:
pip install prometheus-client
Drop-in snippet for a Flask-ish inference service:
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import time
# Start a Prometheus metrics endpoint on :9300
start_http_server(9300)
# Define metrics
inference_requests = Counter("inference_requests_total", "Total inference requests")
inference_errors = Counter("inference_errors_total", "Total inference errors")
inference_latency = Histogram("inference_latency_seconds", "Inference latency (s)", buckets=(0.01,0.05,0.1,0.2,0.5,1,2,5))
batch_size_gauge = Gauge("inference_batch_size", "Current batch size")
def run_inference(model, batch):
t0 = time.time()
inference_requests.inc()
batch_size_gauge.set(len(batch))
try:
out = model(batch) # your model call
return out
except Exception:
inference_errors.inc()
raise
finally:
inference_latency.observe(time.time() - t0)
Prometheus scrape job to add:
- job_name: "app"
static_configs:
- targets: ["localhost:9300"]
If you want distributed tracing later, instrument with OpenTelemetry and ship to Jaeger via a container; you don’t need to change your system packages for that.
Step 4 — Deep dives with eBPF (bpftrace)
When p99 latency spikes, the question is “why right now?” eBPF lets you answer without kernel rebuilds.
Install bpftrace:
- apt:
sudo apt install -y bpftrace bpfcc-tools
- dnf:
sudo dnf install -y bpftrace bcc-tools
- zypper:
sudo zypper -n install bpftrace bcc-tools
Useful one-liners:
- Which syscalls are hot for your PID (10 seconds):
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter /pid == PID/ { @[probe] = count(); } interval:s:10 { exit(); }'
- Run queue latency (are we CPU-saturated?):
sudo bpftrace -e 'hist:runqlat { @ = hist(arg0); } interval:s:10 { exit(); }'
- Off-CPU time by blocked stacks (why are threads sleeping? 10s sample):
sudo bpftrace -e 'profile:hz:99 /pid == PID/ { @off[ustack] = count(); } interval:s:10 { exit(); }'
Replace PID with your process ID. Use perf top as a quick alternative if eBPF is restricted.
Step 5 — A real-world triage example
Symptom: Your PyTorch inference p99 jumps from 40 ms to 180 ms under load.
What to check, fast:
1) GPU headroom:
- Query:
max(gpu_utilization_percent)— if it’s under ~70% but latency is high, GPU is waiting on something.
2) CPU saturation and run queue:
Query:
1 - avg by(instance)(irate(node_cpu_seconds_total{mode="idle"}[1m]))bpftrace:
runqlathistogram shows long scheduling delays => CPU bound or noisy neighbors.
3) Memory pressure / swapping:
- Query:
rate(node_vmstat_pswpin[5m]) + rate(node_vmstat_pswpout[5m])— any non-zero is red flag.
4) Disk I/O bottleneck:
Query:
irate(node_disk_read_bytes_total[5m])andnode_disk_io_time_seconds_totalIf high, look for dataset or model weights being fetched synchronously.
5) Fix patterns:
Increase CPU limits or pin service to isolated CPUs (cgroups).
Preload weights into GPU memory, use page-locked host memory.
Batch requests (watch your
inference_batch_sizemetric).If NUMA, bind process memory and CPUs to the closest GPU PCIe NUMA node.
You’ll see the impact immediately in your app and GPU metrics, not “after the incident.”
Common Prometheus queries you’ll reuse
- Node CPU usage:
100 * (1 - avg by(instance)(irate(node_cpu_seconds_total{mode="idle"}[5m])))
- Memory pressure:
(node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100
- GPU memory util percent:
gpu_memory_utilization_percent
- Inference error rate per minute:
rate(inference_errors_total[1m])
- p95 latency (if you use Histogram):
histogram_quantile(0.95, sum by (le) (rate(inference_latency_seconds_bucket[5m])))
Wrap-up and next steps
You now have:
Host metrics (Node Exporter)
GPU metrics (nvidia-smi exporter)
App metrics (latency, throughput, batch size)
eBPF tools for when the graph isn’t enough
Next steps:
Add Grafana for dashboards (install via your package manager or container) and wire Prometheus as a data source.
Containerize the gpu-exporter and your app for repeatability.
Add alerts: start with “GPU utilization < 30% for 10m while requests > X” and “p95 inference latency > target SLO.”
If you need traces, run a Jaeger all-in-one container and instrument with OpenTelemetry.
Have questions or want a ready-to-paste dashboard JSON? Tell me your distro and stack and I’ll tailor a Linux-first AI observability starter kit for you.