Read-only inference failure analysis · vLLM · Triton · DCGM Exporter

TTFT p95 — capacity-queued capture

warning

31,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

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 demo

Seven 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/model

Reports 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=inference

The 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.

ComponentStatusDetail
vLLM adapterValidated

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 pathValidated — 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 engineValidated

Validated in-cluster at shipped defaults, including suppression.

internal/recommender
Counter-rate and queue-growth rulesIntegration 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 pressureValidated

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 adapterValidated

Validated against live Triton 25.12 on the CPU backend.

internal/runtime/triton/testdata
GPU-backed TritonValidated — 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.

  • 44,432 successful requests, zero request errors.
  • 37 metric families observed; all 10 required IFA metric families present.
  • nv_gpu_utilization fraction-to-percentage conversion verified (0.02 → 2%, matching DCGM). This is a conversion check, not a high-utilization measurement.
  • Authentic idle, loaded and recovery captures committed; GPU regression tests passing.
internal/runtime/triton/testdata/e2e
DCGM adapterValidated 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-endValidated — 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 historyImplemented — 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.

FamilyCoversRules
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
Total19

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.

IFA-CAP-001

Requests queueing while the GPU is saturated

internal/recommender/rules.go

IFA-CAP-002

Requests queueing while the GPU is idle

internal/recommender/rules.go

IFA-LAT-001

Time to first token dominated by queueing

internal/recommender/rules.go

IFA-LAT-002

Time to first token dominated by prefill

internal/recommender/rules.go

IFA-KV-001

KV cache exhausted: requests are being preempted

internal/recommender/rules.go

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.

CaptureRunningWaitingWaiting (capacity)KV cacheTTFT p95
Idle0000%39.9 ms
Under load10002.86%—
Capacity-queued212120.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.

CaptureRunningWaitingWaiting (capacity)KV cachePreemptions
KV-exhausted84161699.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.

MeasurementResult
GPUNVIDIA L4, 24 GB class
Driver570.133.20
CUDA12.8
Inference servervLLM 0.28.0
Modelfacebook/opt-125m
KV-cache sizeConstrained to 16,384 tokens so exhaustion is reachable
Concurrent load80 workers
Completed requests2,890
Request errors0
KV-cache occupancy100%
Active preemption rateApproximately 280–300/s
GPU utilization under load100%
RecoveryQueue 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.

CaptureGPU utilizationGPU memory usedTemperature
Idle0%0 MiB33 °C
Under load100%1,548 MiB46 °C
Recovery0%1,548 MiB50 °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.

CaptureFailed requestsGPU utilizationCompute latency
Idle0——
Loaded0——
Summary latencies enabled0—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
CaptureRequests succeededPendingnv_gpu_utilizationIFA readsGPU memory used
Idle0000%258 MiB
Under load (~38 s in)30,637300.022%292 MiB
Recovery (~10 s after)44,434000%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.

FindingLive evidenceResult
IFA-CAP-001GPU utilization 91%, 63 requests waitingConfirmed
IFA-CAP-003Waiting queue increased by 63 requests over the evaluation windowConfirmed
IFA-EFF-003GPU memory usage approximately 92–94%Confirmed
IFA-LAT-001TTFT p95 approximately 888 ms, queue share 89%Confirmed
IFA-KV-001KV preemption not reproduced with this configurationNot reproduced
RecoveryQueue findings cleared after load stoppedConfirmed
Pod restartKubernetes rediscovery and telemetry recoveryConfirmed

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.

internal/k8s/watcher.go

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-Instruct

After — model identity moved to an annotation

metadata:
  annotations:
    inference.io/model: meta-llama/Llama-3.1-8B-Instruct
  labels:
    inference.io/runtime: vllm

Not 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.

internal/runtime/dcgm/testdata

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.