Read-only inference failure analysis · vLLM · Triton · DCGM Exporter
TTFT p95 — capacity-queued capture
warning31,999.999999999993ms
target ≤ 1,000 ms — exceeded by 30,999.999999999993 ms
In plain terms: the server took 32 seconds to produce a first token against a one-second target, because requests were queueing behind a scheduler limited to two concurrent sequences.
- Source
- vllm:time_to_first_token_seconds
- Runtime · Cluster
- vLLM 0.28.0 · kind, Kubernetes 1.31
- Rule
- IFA-LAT-002
A signal you did not measure is not zero.
A runtime exposing no GPU metric is not a GPU idling at 0%. A server that has served nothing has no p95, not a p95 of zero. ifa is built to keep that distinction everywhere it reports a number — including on this page.
Try it
Three ways in, ordered by how little you need ready.
A default helm install now resolves — the image is public and a release is tagged. Start with the one that asks the least of you.
01 — The demo
A terminal. No cluster, no GPU, no inference server.
git clone https://github.com/p95labs/ifa && cd ifa
make demoSeven simulated workloads serving vLLM-format Prometheus exposition over a real socket, scraped by the real collector, evaluated by the real rule engine. Nothing about the output is pre-canned — it produces roughly fifteen findings.
02 — Point it at a server you run
A vLLM server you already have. Nothing to install.
go build -o bin/ifa ./cmd/ifa
bin/ifa check http://your-vllm:8000/metrics -runtime vllm -model your/modelReports which metrics ifa needs, which are present, and which are missing. It is the fastest way to find out whether this is useful to you, and it is read-only against an endpoint you already have.
03 — Install into a cluster
A Kubernetes cluster and Helm.
helm install ifa deploy/helm/ifa \
--namespace ifa --create-namespace \
--set config.kubernetes.namespace=inferenceThe chart is not published to a Helm repository yet, so this installs from a clone — there is no helm repo add. The published image is ghcr.io/p95labs/ifa, tagged 0.2.0-alpha.1 with no v prefix, even though the git tag has one.
The validation ledger
What is validated, and what is not.
Rows that say not validated carry the same visual weight as rows that say validated. That is the point — this is a record, not a pitch.
| Component | Status | Detail |
|---|---|---|
| vLLM adapter | Validated | Validated against live vLLM 0.28.0 on CPU and NVIDIA L4 hardware, in two separate L4 sessions (JarvisLabs and E2E Networks), including genuine KV-cache exhaustion, queueing, active preemptions and recovery. Multiple timestamped captures from the E2E run establish independently measured counter rates. internal/runtime/vllm/testdata/e2e/README.md |
| Kubernetes path | Validated — CI (kind 1.31) | Validated end to end in a real cluster in CI (Kubernetes 1.31), using synthetic and CPU-backed workloads. scripts/kind-integration.sh |
| Rule engine | Validated | Validated in-cluster at shipped defaults, including suppression. internal/recommender |
| Counter-rate and queue-growth rules | Integration validated | Validated through deterministic dynamic multi-scrape integration tests running in a real kind Kubernetes cluster. Tests cover counter deltas, queue growth, preemption rates, low token throughput, counter resets, missing telemetry and recovery. These tests use synthetic metrics, not a live GPU. Separately, IFA-CAP-003 was subsequently observed firing through the actual IFA REST API during a live GPU Kubernetes experiment. scripts/kind-dynamic-integration.sh |
| High KV-cache pressure | Validated | Captured on NVIDIA L4 hardware at 99.9% KV-cache occupancy (JarvisLabs) and 100% (E2E Networks), with the preemption counter measured climbing at roughly 280–300 per second in the E2E run. internal/runtime/vllm/testdata |
| Triton adapter | Validated | Validated against live Triton 25.12 on the CPU backend. internal/runtime/triton/testdata |
| GPU-backed Triton | Validated — NVIDIA L4 GPU | NVIDIA Triton Inference Server 25.12 was validated on a physical NVIDIA L4 using a TorchScript GPU matrix-multiplication model with the LibTorch backend. IFA successfully parsed live GPU metrics, including utilization and memory, and its utilization normalization was independently corroborated against DCGM.
|
| DCGM adapter | Validated against the official NVIDIA exporter | Validated against NVIDIA's official dcgm-exporter on an NVIDIA L4 hosted by E2E Networks, in addition to earlier validation against a JarvisLabs DCGM-compatible exporter. The same official exporter was also deployed as a DaemonSet in the K3s experiment below. internal/runtime/dcgm/testdata/e2e/README.md |
| Kubernetes live GPU end-to-end | Validated — Real GPU Kubernetes E2E | IFA was deployed using Helm in a real GPU-enabled K3s cluster. It automatically discovered a GPU-backed vLLM workload, scraped live vLLM and official NVIDIA DCGM exporter metrics, and generated diagnostic findings through its REST API. Single node, single GPU. The CI row above remains the synthetic, CPU-backed validation. scripts/gpu-validation |
| TimescaleDB history | Implemented — real database integration testing pending | Implemented, bounded, non-blocking — no integration test against a real TimescaleDB. internal/storage/timescale |
GPU validation covers NVIDIA L4 cards in two separate sessions (JarvisLabs and E2E Networks): vLLM, the DCGM adapter, GPU-backed Triton, and a real GPU Kubernetes deployment on a single node with one GPU. Not yet validated: a real TimescaleDB, comprehensive webhook delivery reliability, behaviour across multiple physical GPUs, per-pod GPU attribution on shared nodes, per-workload diagnostic thresholds (they are global), API authentication and production hardening, KV-cache preemption reproduced inside Kubernetes (it was measured on an L4 outside the cluster, but not reproduced in the Kubernetes run), and any external customer or design-partner validation. If you run any of these, ifa check <url> produces the report that would close it.
Stage
Solo-built, pre-users, alpha. No customers, no traction, no team. An alpha release, v0.2.0-alpha.1, is published, which makes the stage datable, not further along. No "trusted by," no logo wall, no invented metrics.
Latest published release: v0.2.0-alpha.1. Latest validation evidence is merged into main through PR #51 (8271d4c), so the published alpha does not contain every commit described on this page.
Rule taxonomy
19 rules across 7 families.
Nineteen rules is a real taxonomy, not a marketing number — presented here as one. Every rule reports evidence: the metric, the observed value, the threshold, and the comparison.
| Family | Covers | Rules |
|---|---|---|
| IFA-CAP | Capacity and queueing internal/recommender/rules.go | 4 |
| IFA-ERR | Errors internal/recommender/rules.go | 2 |
| IFA-KV | KV cache internal/recommender/rules.go | 2 |
| IFA-SCL | Scaling internal/recommender/rules.go | 2 |
| IFA-LAT | Latency internal/recommender/rules.go | 4 |
| IFA-OBS | Telemetry health internal/recommender/rules.go | 2 |
| IFA-EFF | Efficiency internal/recommender/rules.go | 3 |
| Total | 19 | |
The pair worth surfacing
A deep queue with a saturated GPU and a deep queue with an idle GPU have opposite fixes, and a dashboard cannot tell you which one you have. That distinction is the product.
Live validation
Real backends, real captures.
The vLLM and Triton adapters have been run against live CPU-backed servers. The vLLM, Triton and DCGM adapters have been run against NVIDIA L4 hardware, and IFA itself has been deployed into a real GPU Kubernetes cluster. Each run parsed real /metrics payloads, and verbatim captures are committed to the repository as regression fixtures.
vLLM 0.28.0 — CPU backend
Model facebook/opt-125m, official ARM64 CPU image. The CLI parsed the real /metrics endpoint: 51 KB payload, 122 metric families, 0 unparseable lines, 13 of 13 required metrics found. Three verbatim captures are committed to the repository as regression fixtures.
| Capture | Running | Waiting | Waiting (capacity) | KV cache | TTFT p95 |
|---|---|---|---|---|---|
| Idle | 0 | 0 | 0 | 0% | 39.9 ms |
| Under load | 10 | 0 | 0 | 2.86% | — |
| Capacity-queued | 2 | 12 | 12 | 0.46% | 32,000 ms |
vLLM reports KV utilisation as a 0–1 fraction — its own HELP text says "1 means 100 percent usage" — and the adapter converts it. That was checked against the live payload, because a fraction read as a percentage means the KV rules can never fire, silently, forever.
vLLM 0.28.0 — NVIDIA L4 (JarvisLabs)
The first L4 session: one capture, with the server driven into KV-cache exhaustion. This is the failure vLLM is best known for, and the one the KV rules exist to catch.
| Capture | Running | Waiting | Waiting (capacity) | KV cache | Preemptions |
|---|---|---|---|---|---|
| KV-exhausted | 84 | 16 | 16 | 99.9% | Counter active |
The highest KV occupancy reachable on CPU was 2.86%. The KV rules fire around 90%, so until this capture they could only be evaluated against synthetic data. The 16 waiting requests are all labelled capacity, and IFA-KV-001 was observed firing during the live run.
The preemption counter is active, and that is all this capture claims. It is a single snapshot, and a rate needs two time-separated scrapes, so no preemption rate was measured in this session. The E2E Networks run below supplies that measurement.
NVIDIA L4 — E2E Networks validation
A second, independent L4 on a different provider, with a different driver. Timestamped verbatim captures were taken at idle, under sustained load, once settled, and after the load stopped.
| Measurement | Result |
|---|---|
| GPU | NVIDIA L4, 24 GB class |
| Driver | 570.133.20 |
| CUDA | 12.8 |
| Inference server | vLLM 0.28.0 |
| Model | facebook/opt-125m |
| KV-cache size | Constrained to 16,384 tokens so exhaustion is reachable |
| Concurrent load | 80 workers |
| Completed requests | 2,890 |
| Request errors | 0 |
| KV-cache occupancy | 100% |
| Active preemption rate | Approximately 280–300/s |
| GPU utilization under load | 100% |
| Recovery | Queue and KV-cache occupancy returned to zero after load stopped |
The preemption rates were calculated independently from consecutive raw captures taken 14 seconds apart, not read from IFA's own output: 285.9, 297.2, 294.3 and 278.5 preemptions per second across four intervals. The KV cache was deliberately constrained to make exhaustion reachable on a 24 GB card, and the model is a small one, so these figures describe this configuration, not typical production behaviour.
These captures validate the vLLM and DCGM adapters against live GPU metrics, outside Kubernetes. The Kubernetes-hosted IFA API path against a GPU workload was validated in a later run, described below.
Official NVIDIA DCGM exporter
NVIDIA's official exporter image, nvcr.io/nvidia/k8s/dcgm-exporter:3.3.9-3.6.1-ubuntu22.04, was run successfully on the same L4 alongside the vLLM load.
| Capture | GPU utilization | GPU memory used | Temperature |
|---|---|---|---|
| Idle | 0% | 0 MiB | 33 °C |
| Under load | 100% | 1,548 MiB | 46 °C |
| Recovery | 0% | 1,548 MiB | 50 °C |
All six fields the IFA DCGM adapter reads were present and correctly formatted. GPU utilization reached 100% within the first 14-second sampling window, and no 18–20 second refresh delay was observed in the E2E sampling windows. That interval is coarse, so this does not show the exporter has no refresh latency, only that the earlier delay did not appear. The earlier delay was seen with the JarvisLabs bundled exporter and should not be generalized to NVIDIA's official one.
Triton 25.12 — CPU backend
Image nvcr.io/nvidia/tritonserver:25.12-pyt-python-py3, Python backend with a minimal echo model, KIND_CPU instance group. Three verbatim captures are committed to the repository: idle, loaded, and one with summary latencies enabled.
| Capture | Failed requests | GPU utilization | Compute latency |
|---|---|---|---|
| Idle | 0 | — | — |
| Loaded | 0 | — | — |
| Summary latencies enabled | 0 | — | 4.117 ms |
Triton exposes a failure counter, nv_inference_request_failure, with a reason label — vLLM does not, which is why ifa deliberately reports no error rate for the vLLM adapter. That asymmetry is a real design decision driven by what each runtime actually publishes, not a template applied to both.
A CPU-only Triton server publishes no nv_gpu_* metrics at all, which makes these captures a real test of the site's central claim: an absent signal is reported as unmeasured, not as zero. The vLLM captures could not test that — vLLM on CPU still reports a KV cache.
GPU-backed Triton — NVIDIA L4
The same adapter, run against a Triton server that is actually using a GPU. The model is pinned to the GPU with no CPU fallback, and the Triton startup log confirms it was loaded on GPU device 0.
- Server
- Triton 25.12, official NVIDIA container
- GPU
- NVIDIA L4, driver 570.133.20, CUDA 12.8
- Model
- TorchScript GPU matrix multiplication
- Backend
- LibTorch (PyTorch)
- Load
- 40 concurrent workers, about 90 seconds
- Completed requests
- 44,432
- Request errors
- 0
- ifa check
- 37 families, all 10 required found, 0 unparseable lines
| Capture | Requests succeeded | Pending | nv_gpu_utilization | IFA reads | GPU memory used |
|---|---|---|---|---|---|
| Idle | 0 | 0 | 0 | 0% | 258 MiB |
| Under load (~38 s in) | 30,637 | 30 | 0.02 | 2% | 292 MiB |
| Recovery (~10 s after) | 44,434 | 0 | 0 | 0% | 292 MiB |
Triton reports nv_gpu_utilization as a 0–1 fraction. It reported 0.02, IFA converts that to 2%, and DCGM independently reported 2% at the same moment. This corroborates the conversion; it is not a high-utilization result. The model is lightweight and the L4 was nowhere near saturated, so these captures say nothing about Triton behaviour at 90% GPU load. The server-side counter ended at 44,434 against the load generator's 44,432.
Not validated here: TensorRT or ONNX Runtime backends, dynamic batching behaviour, multiple GPUs, multiple models on one GPU server, and IFA's diagnostic rules against a Triton workload.
Kubernetes validation — CI (kind)
A real kind cluster, Kubernetes 1.31, node image pinned by tag and digest. The full path, end to end:
Helm install → ServiceAccount and RBAC → informer-based discovery → in-cluster Service networking → Prometheus scrape → vLLM parse → telemetry store → sustained rule evaluation → suppression → REST API response
Six assertions, zero semantic threshold overrides — the shipped defaults are active, including the 1,000 ms TTFT target and the 50% queue-share threshold. The only overrides are collection interval and sustain window, both purely to keep CI under four minutes.
Actual API response from the cluster run
{
"code": "IFA-LAT-002",
"severity": "warning",
"window_seconds": 10,
"evidence": [
{ "metric": "ttft_p95_ms", "source": "vllm:time_to_first_token_seconds",
"observed": 31999.999999999993, "threshold": 1000, "comparison": ">", "unit": "ms" },
{ "metric": "queue_time_share_of_ttft",
"observed": 44.531250000000014, "threshold": 50, "comparison": "<", "unit": "percent" }
]
}Suppression, not duplication
The test also asserts that IFA-LAT-003 is absent — the specific diagnosis suppresses the generic symptom, which is behaviour a rule engine either has or fakes.
An unrounded float
31999.999999999993 is a real float from a real histogram interpolation, not a rounded marketing number. It is more convincing than any claim.
Real GPU. Real Kubernetes. Real Diagnoses.
IFA was deployed into a GPU-enabled Kubernetes cluster running vLLM on an NVIDIA L4. The collector discovered the workload, read genuine inference and GPU telemetry, and generated actionable diagnostic findings through the REST API.
Single-node K3s with the NVIDIA device plugin, vLLM 0.28.0 serving facebook/opt-125m, NVIDIA's official DCGM exporter as a DaemonSet, and IFA installed with its Helm chart. The IFA image was built from main, not from the published release.
| Finding | Live evidence | Result |
|---|---|---|
| IFA-CAP-001 | GPU utilization 91%, 63 requests waiting | Confirmed |
| IFA-CAP-003 | Waiting queue increased by 63 requests over the evaluation window | Confirmed |
| IFA-EFF-003 | GPU memory usage approximately 92–94% | Confirmed |
| IFA-LAT-001 | TTFT p95 approximately 888 ms, queue share 89% | Confirmed |
| IFA-KV-001 | KV preemption not reproduced with this configuration | Not reproduced |
| Recovery | Queue findings cleared after load stopped | Confirmed |
| Pod restart | Kubernetes rediscovery and telemetry recovery | Confirmed |
IFA-KV-001 is deliberately not marked validated in this run. With a 125M-parameter model, KV-cache usage peaked near 0.23% of the L4's cache, so preemption never occurred. That is a separate result from the earlier E2E Networks session above, where preemption counters were measured climbing at roughly 280–300 per second on an L4 with a deliberately constrained cache. Preemption has been measured on real hardware; it has not yet been reproduced through IFA inside Kubernetes.
What broke, and why
A real model name broke the watcher. Here is the account.
The Kubernetes integration test was added to cover a path that had only ever been tested with a fake client. On its first run against a real API server, it failed — and the failure was not in the test.
The watcher read the model name from a Kubernetes label, inference.io/model. Kubernetes label values cannot contain /. Every real model name is a Hugging Face repo id:
- meta-llama/Llama-3.1-8B-Instruct
- mistralai/Mistral-7B-v0.3
- facebook/opt-125m
So the documented convention would have been rejected by the API server for every production model, and the project's own chart output demonstrated the broken form.
The unit tests passed because client-go's fake clientset does not validate label values. Only a real API server does. The fix moved model identity to an annotation, which accepts arbitrary values; runtime stayed a label, because vllm and triton are valid label values and selecting on runtime is useful. It shipped as its own change before the integration test merged, so the test covers the corrected convention rather than encoding a workaround.
Before — rejected by a real API server
# invalid: label values cannot contain '/'
metadata:
labels:
inference.io/model: meta-llama/Llama-3.1-8B-InstructAfter — model identity moved to an annotation
metadata:
annotations:
inference.io/model: meta-llama/Llama-3.1-8B-Instruct
labels:
inference.io/runtime: vllmNot written as a humble-brag, and not as "we take quality seriously" — this is a short account of what broke, why the existing tests allowed it, and what changed. A project that publishes its own defects is more trustworthy than one that claims to have none.
Measured on hardware
One bundled GPU exporter read up to twenty seconds late.
While capturing DCGM output on a JarvisLabs NVIDIA L4, the bundled DCGM-compatible exporter's GPU readings were measured lagging by 18–20 seconds. A matched pair of captures is committed to the repository, one taken while the reading was stale and one after it settled, and the lag was confirmed to be a property of that exporter rather than a defect in ifa.
For an operator of that exporter, a GPU utilisation figure can be up to about twenty seconds behind reality, which matters when a rule correlates GPU state against queue state sampled at a different instant. NVIDIA's official dcgm-exporter was later run on a separate L4, and no such delay appeared in its sampling windows. The 18–20 second figure belongs to the bundled exporter and is kept here as historical evidence, not as a claim about NVIDIA's.