llm-cost-latency-benchmark · git:20261005.2bc0998 · 2026-10-05 · sha256 20cf9ae73527c84c

llm-cost-latency-benchmark git:20261005.2bc0998A

Immutable. This exact content never changes and is served at /api/v1/blob/20cf9ae73527c84c.

---
name: llm-cost-latency-benchmark
description: "Benchmark AI workflow cost and latency with TTFT/TPOT/ITL/e2e percentiles, cache-aware token billing, goodput under SLOs, and a quality floor. Use before claiming a model, prompt, or infra change is cheaper or faster."
category: development
risk: safe
source: self
source_type: self
date_added: "2026-09-11"
tags: ["ai-workflows", "evaluation", "latency", "ttft", "tpot", "cost", "tokens", "goodput", "prompt-caching", "slo"]
tools: ["claude", "cursor", "gemini", "codex"]
---

# LLM Cost & Latency Benchmark AI Skill Guide (Claude)

## Overview & Engine Architecture

Cost and latency are **not one number**. A claim is a tuple: *workload × clock boundary × cache state × concurrency/arrival process × token mix × pricing snapshot × quality floor*. Mixing those variables produces vendor-slide fiction. A response that is faster but fails acceptance is **not** an optimization win—it is a different product.

Claude operates as a Principal LLM Performance Economist, specializing in **client-observed TTFT vs server TTFT**, **TPOT vs ITL**, **cache-cold vs cache-hot regimes**, **billable token categories** (uncached input, cache read, cache write, output including reasoning), **workflow wall-clock** (retrieval + tools + retries + queue), **percentile reporting**, and **SLO-qualified goodput**.

```
┌─────────────────────────────────────────────────────────────┐
│              Cost × Latency Benchmark Stack                 │
│                                                             │
│  Workload contract                                          │
│  ├── Case population (prod-shaped) + quality floor          │
│  ├── Input/output length distributions (or fixed I/O)       │
│  ├── Concurrency + arrival process (closed vs open loop)    │
│  └── Cache policy: cold | warm | production-mix             │
│                                                             │
│  Timing planes (never conflate)                             │
│  ├── Client TTFT: send → first nonempty streamed token      │
│  ├── Prefill / queue / network (diagnostic breakdown)       │
│  ├── TPOT / ITL after first token                           │
│  └── Workflow e2e: user intent → final accepted artifact    │
│                                                             │
│  Cost planes                                                │
│  ├── Provider usage: input, cached_read, cache_write, out   │
│  ├── Non-token: tools, search, containers, retries, fails   │
│  └── Pricing manifest: URL + retrieved_at + model tier      │
│                                                             │
│  Decision                                                   │
│  ├── Percentiles + sample size + goodput@SLO                │
│  └── Compare only configs that meet the quality floor       │
└─────────────────────────────────────────────────────────────┘
```

---

## When to use / when not to

**Use when**

- Choosing models, regions, streaming settings, prompt-cache layouts, or concurrency limits.
- Proving a “cheaper/faster” PR for prompts, packers, rerankers, or serving stacks.
- Setting SLOs (interactive chat, voice, batch) and capacity planning.
- Debugging why p50 looks fine but p99 destroys UX (queueing, cold start, long prefill).

**Do not use when**

- Quality is undefined—run `@prompt-regression-gate` / `@ai-evaluation-dataset` first and set a **quality floor**.
- Someone wants a single TPS headline without workload/cache/concurrency disclosure.
- Extrapolating 10 synthetic short prompts to production guarantees.
- Hard-coding remembered $/MTok as “current” without a dated pricing source.

---

## Deep metric dictionary (formulas you must name)

Let request `i` be sent at client time `s_i`, first **nonempty usable** streamed token arrive at `f_i`, last token at `e_i`. Let `O_i` = provider-reported **output/completion tokens** (includes reasoning/formatting tokens when billed that way). Let `I_i` = total input/prompt tokens.

| Metric | Formula / definition | Notes that break comparisons |
| :--- | :--- | :--- |
| **Client TTFT** | `f_i − s_i` | Includes network + queue + prefill. State client region. |
| **Server TTFT** | receipt → first token **sent** | Different clock; do not swap with client TTFT. |
| **E2E request latency** | `e_i − s_i` | Model-call only unless labeled **workflow e2e**. |
| **TPOT** | `(e_i − f_i) / (O_i − 1)` | Undefined if `O_i < 2`. Excludes first token by convention (NVIDIA/vLLM-style). |
| **ITL** | gaps `t_{j+1}−t_j` | Distribution over intervals; p95 of all gaps ≠ mean of per-request TPOT. |
| **Decode TPS (per user)** | `(O_i − 1) / (e_i − f_i)` = `1/TPOT` | Not full-request rate. |
| **Full-request TPS** | `O_i / (e_i − s_i)` | Includes TTFT; lower; different claim. |
| **Aggregate TPS** | `sum(O) / T_run` | Fleet/GPU scope must be named. |
| **Goodput** | compliant completions / second **or** fraction meeting SLO | Name SLO (e.g. TTFT p99≤X ∧ TPOT p99≤Y); count errors as noncompliant. |

**First-token identity (reasoning models):** declare whether `f_i` is (a) first SSE/data chunk, (b) first reasoning token, or (c) first **visible answer** token. Voice/chat UX usually cares about (c). Chunk ≠ token (common harness bug called out on r/LocalLLaMA tooling).

**OpenTelemetry alignment (when instrumenting):** prefer `gen_ai.client.operation.time_to_first_chunk` / `gen_ai.usage.input_tokens` / `output_tokens`, and cache detail attributes when available. Report **billed** units when providers distinguish billed vs consumed.

---

## Cost accounting (think in billable categories)

### Token classes (do not collapse)

Providers increasingly price **separately**:

1. **Uncached input** — ordinary prompt tokens  
2. **Cache read (`cached_tokens`)** — discounted reuse of a prefix  
3. **Cache write (`cache_write_tokens`)** — may be **>1×** uncached input on some model families (e.g. OpenAI docs: writes at 1.25×, reads at 0.1× on newer families—**verify live pricing**)  
4. **Output / completion** — includes **reasoning tokens** and often invisible formatting/tool-channel tokens; billed as output even when not shown to the user  

**Arithmetic sketch** (replace rates from a dated pricing pull):

```text
cost ≈
  u_in  * rate_uncached_in
+ c_read * rate_cached_in
+ c_write * rate_cache_write
+ out   * rate_out
+ Σ tool_calls * tool_rate
+ Σ non_token_fees
```

Rules:

- Prefer **provider `usage`** over local `len/4` guesses (tools, images, schemas break heuristics).  
- If usage is missing → mark **`unknown`**, never invent.  
- **Include failed and retried attempts** in cost totals (they still billed or partially billed).  
- Stream with `stream_options.include_usage` (or provider equivalent) so finals are recorded.  
- Reasoning: budget `max_output_tokens` for hidden tokens + visible answer; otherwise empty completions skew latency/cost.

### Pricing manifest (mandatory artifact)

```json
{
  "retrieved_at": "2026-10-05T18:00:00Z",
  "source_url": "https://…/pricing",
  "model": "…",
  "currency": "USD",
  "rates_per_1m": {
    "input_uncached": null,
    "input_cached_read": null,
    "input_cache_write": null,
    "output": null
  },
  "notes": "Paste exact rows; do not rely on memory."
}
```

### Unit economics to report

| KPI | Definition |
| :--- | :--- |
| **$/accepted task** | Total $ for all attempts / tasks that passed quality floor |
| **$/1k input tokens (effective)** | Blended after cache hit mix |
| **Cache hit rate** | `cached_read / input` (verify via usage fields, not hope) |
| **Retry tax** | $ and latency of retries / successes |
| **Cost at iso-quality** | Only among configs meeting the floor |

Cheapest raw tokens with 2× retries and quality fails is usually **more expensive per accepted task**.

---

## Workload contract (comparability gate)

Before trusting any A vs B claim, both sides must match—or differences must be labeled as the **intentional** experimental factor:

1. **Model + snapshot/version** + tokenizer  
2. **Prompt/tool schema hashes**  
3. **Input/output length policy** (fixed I/O vs production distribution)  
4. **Streaming on/off** (TTFT/ITL meaningless without streaming)  
5. **Cache regime**: cold (force unique prefixes) vs warm (stable system prefix) vs measured mix  
6. **Concurrency + arrival**: closed-loop (send next after done) ≠ open-loop/Poisson ≠ “fire all at t=0”  
7. **Client vantage** (region/network)  
8. **Warm-up policy** (discard N cold starts; separate serverless cold-start study)  
9. **Quality floor** + same eval dataset hash  
10. **Pricing date** for cost claims  

If any required field is missing → verdict **cannot-normalize**, not “approximately faster.”

### Cache is a different experiment

Prefix/KV caching can dominate TTFT. Practitioners repeatedly show:

- Accidental cache hits from reused prompts make TTFT look unrealistically low.  
- Calculated prefill savings ≠ measured TTFT (network/queue floor remains).  
- Production agents with growing tool histories live in **prefill-bound** regimes; chat short-prompt TPS rankings mislead.

**Always report cold and warm separately**, with measured `cached_tokens` / hit rate.

### Concurrency and tails

- p50 TTFT at concurrency=1 ≈ network + single prefill—**almost useless** for capacity.  
- Test near expected **p95 concurrent** load.  
- Under load, TTFT is often **queue wait**; log queue separately when the server exposes it.  
- For interactive/voice, **p95/p99** matter more than mean; hedging (dual endpoint race) is an architectural response to tail risk, not a benchmark cheat—cost it explicitly (~2× input on hedged fraction).

**Sample size honesty (IETF-style guidance):** p99 needs large N (order ~10³ samples for rough relative accuracy); tiny benches must not claim p99 production SLOs.

---

## Workflow vs model-call latency

For product decisions, measure **workflow wall-clock**:

```text
user_submit → retrieve/tools/guards → model (TTFT…E2E) → validate/repair → accept
```

Attribute spans:

| Span | Typical owners |
| :--- | :--- |
| Retrieval / rerank | `@rag-retrieval-audit` |
| Tool round-trips | agent graph |
| Validation retries | `@llm-json-contract-check` |
| Model TTFT/TPOT | provider/serving |
| Human-perceived | UX (streaming indicators reset abandonment clocks) |

Optimizing only decode TPS while RAG prefill and tools dominate e2e is cargo-cult performance work.

---

## Procedure

### 1. Preflight

- Define population (prod log sample vs synthetic fixed I/O).  
- Set quality floor (pass rate / P0 gate).  
- Set SLOs (e.g. client TTFT p95 ≤ 800ms; TPOT p95 ≤ 40ms; or batch: maximize $/tok within overnight window).  
- Budget ($) and max duration.  
- Fetch **live** pricing → pricing manifest.  
- Choose cache regime(s) and concurrency sweep plan.

### 2. Instrument each attempt

Record per request (sanitized):

```json
{
  "case_id": "c-17",
  "attempt": 1,
  "ok_quality": true,
  "error_type": null,
  "s_client": 0.0,
  "f_first_usable_token": 0.42,
  "e_last_token": 1.88,
  "ttft_ms": 420,
  "tpot_ms": 28.5,
  "usage": {
    "input": 3500,
    "cached_read": 3200,
    "cache_write": 0,
    "output": 180,
    "reasoning_output_subset": 40
  },
  "cost_usd": null,
  "cache_regime": "warm",
  "concurrency_bin": 16,
  "model": "…",
  "pricing_retrieved_at": "…"
}
```

### 3. Analyze

- Percentiles p50/p95/p99 for TTFT, TPOT, workflow e2e—**by cache regime and concurrency**.  
- Cost totals and **$/accepted**.  
- Goodput @ declared SLO.  
- Error/retry rates.  
- Reject cross-regime averages that hide cold starts.

### 4. Decide

Only compare configs that clear the quality floor. Prefer:

1. Higher goodput at equal or lower $/accepted, or  
2. Lower latency percentiles **without** quality loss, or  
3. Explicit Pareto: “+12% cost for −40% p99 TTFT” with product sign-off.

Deliver raw measurements + config + pricing refs + arithmetic. Label extrapolation limits.

---

## Production example: percentile + cost rollup

```python
"""Cost/latency rollup with TTFT, TPOT, and dated pricing."""
from __future__ import annotations

import json
import math
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Sequence


def percentile(sorted_vals: Sequence[float], p: float) -> float:
    """Nearest-rank percentile; p in [0, 100]. Empty → nan."""
    if not sorted_vals:
        return float("nan")
    if p <= 0:
        return sorted_vals[0]
    if p >= 100:
        return sorted_vals[-1]
    k = max(1, int(math.ceil(p / 100.0 * len(sorted_vals))))
    return sorted_vals[k - 1]


@dataclass(frozen=True)
class Rates:
    """USD per 1M tokens. None means unknown—do not invent."""
    input_uncached: float | None
    input_cached_read: float | None
    input_cache_write: float | None
    output: float | None


def cost_usd(usage: dict[str, int], rates: Rates) -> float | None:
    need = {
        "input_uncached": rates.input_uncached,
        "input_cached_read": rates.input_cached_read,
        "input_cache_write": rates.input_cache_write,
        "output": rates.output,
    }
    if any(v is None for v in need.values()):
        return None
    inp = usage.get("input", 0)
    cread = usage.get("cached_read", 0)
    cwrite = usage.get("cache_write", 0)
    # Uncached portion = input - cached_read - cache_write (clamp at 0)
    u = max(0, inp - cread - cwrite)
    out = usage.get("output", 0)
    return (
        u * rates.input_uncached
        + cread * rates.input_cached_read
        + cwrite * rates.input_cache_write
        + out * rates.output
    ) / 1_000_000.0


def tpot_ms(ttft_ms: float, e2e_ms: float, output_tokens: int) -> float:
    if output_tokens < 2:
        return float("nan")
    return (e2e_ms - ttft_ms) / (output_tokens - 1)


def summarize(rows: list[dict[str, Any]], rates: Rates, slo_ttft_ms: float) -> dict[str, Any]:
    ttfts = sorted(r["ttft_ms"] for r in rows if r.get("ttft_ms") is not None)
    tpots = sorted(
        tpot_ms(r["ttft_ms"], r["e2e_ms"], r["usage"]["output"])
        for r in rows
        if r.get("ttft_ms") is not None and r.get("e2e_ms") is not None
    )
    tpots = [x for x in tpots if x == x]

    costs = []
    unknown_cost = 0
    for r in rows:
        c = cost_usd(r["usage"], rates)
        if c is None:
            unknown_cost += 1
        else:
            costs.append(c)

    accepted = [r for r in rows if r.get("ok_quality")]
    good = [r for r in accepted if r["ttft_ms"] <= slo_ttft_ms]

    return {
        "n": len(rows),
        "ttft_p50": percentile(ttfts, 50),
        "ttft_p95": percentile(ttfts, 95),
        "ttft_p99": percentile(ttfts, 99),
        "tpot_p50": percentile(tpots, 50),
        "tpot_p95": percentile(tpots, 95),
        "cost_total_usd": sum(costs) if costs else None,
        "cost_unknown_rows": unknown_cost,
        "cost_per_accepted_usd": (sum(costs) / len(accepted)) if costs and accepted else None,
        "goodput_fraction_ttft_slo": (len(good) / len(rows)) if rows else float("nan"),
        "quality_pass_rate": (len(accepted) / len(rows)) if rows else float("nan"),
    }


# Load JSONL attempts, attach pricing from manifest, print summarize().
```

### Minimal experiment matrix (start here)

| Cell | Purpose |
| :--- | :--- |
| Fixed 0.5k/0.5k I/O, c=1, cache-cold | Pure model responsiveness |
| Same, cache-warm | Prefix-cache benefit |
| Prod-length distribution, c≈p95 load | Capacity reality |
| Full workflow e2e | Product truth |
| + quality floor filter | Economic truth |

---

## Technical troubleshooting matrix

| Signature | Deep cause | What to do |
| :--- | :--- | :--- |
| Amazing TTFT in bench, bad in prod | Accidental prefix cache / short prompts | Force unique prefixes; match prod lengths; report hit rate |
| p50 fine, p99 awful | Queueing, throttling, noisy neighbors | Correlate spikes with RPS vs wall clock; raise replicas or shed load; consider hedge |
| High TPS, unhappy users | Optimizing aggregate decode, ignoring TTFT SLO | Track goodput; plot latency vs offered load |
| “Cheaper model” costs more | More output/reasoning/retries | Optimize **$/accepted** and output tokens, not $/MTok sticker |
| Empty/fast failures | Reasoning burned `max_tokens` | Raise output budget; classify as errors not wins |
| Serverless look slow | Cold start (pull/CUDA/weights) | Separate cold-start study; warm pool for interactive |
| TTFT ≈ queue | Saturation | Capacity problem, not “slow model” |
| Cross-provider bakeoff chaos | Different clocks/tokenizers/cache | Comparability gate; cannot-normalize if mismatched |
| Client harness overhead | Sync client dominating | Async load gen; note NVIDIA warnings on client overhead |
| Cost unknown | Streaming without usage | Enable usage on stream; else mark unknown |

---

## Best practices

1. **Quality floor first**—then latency—then cost among survivors.  
2. Report **percentiles**, never mean-only for SLOs.  
3. Separate **cold/warm** cache and **model-only vs workflow** e2e.  
4. Pin and date **pricing**; re-pull when claiming savings.  
5. Include **retries, failures, tool fees** in cost.  
6. Name every TPS/TTFT definition in the report title.  
7. For p99 SLOs, collect enough samples or refuse the claim.  
8. Align product class: interactive (TTFT/ITL), RAG/agents (prefill + tools), batch (throughput/$).  
9. Prefer provider usage + OTel GenAI attributes for long-term dashboards.  
10. When changing prompts for cache locality, re-run `@prompt-regression-gate`—cache wins that break quality are not wins.

---

## Limitations

- Third-party latency leaderboards rarely match your prompt length, tools, and cache mix.  
- This skill does not replace provider SLAs or capacity postmortems.  
- Tokenization differs across models—iso-character prompts are not iso-token.  
- Stop and ask if quality floor, workload shape, or pricing source is missing.

---

## Related skills

- `@prompt-regression-gate` — quality floor / paired go-no-go  
- `@ai-evaluation-dataset` — representative case population  
- `@rag-retrieval-audit` — retrieval latency vs generation  
- `@llm-json-contract-check` — repair retries inflate cost/latency  
- `@llm-cost-latency-benchmark` — this skill  
- `@vllm` / provider skills — serving knobs (batching, prefix cache)

---

## Agent Operational Directive

> **MANDATORY**: Treat every cost/latency claim as a workload+clock+cache+concurrency+pricing+quality tuple. Measure client TTFT and TPOT/ITL with named formulas. Bill all token classes and failures honestly; date the pricing source. Never call a quality regression an optimization. Prefer goodput and $/accepted over vanity TPS. If comparability fields are missing, say **cannot-normalize**.

---

## Source anchors (research)

- Anyscale / vLLM metric definitions (TTFT, TPOT, ITL, goodput)  
- NVIDIA / Artificial Analysis / GPUSmith: clock boundaries, TPS ambiguity, cache disclosure  
- OpenAI: prompt caching billable categories; reasoning tokens billed as output; dated pricing page  
- OpenTelemetry GenAI: `time_to_first_chunk`, token usage (+ cache attributes), billed vs consumed  
- IETF draft LLM benchmarking methodology: percentiles, sample sizes, SLO capacity search  
- r/LLMDevs & r/LocalLLaMA: p99 tails, hedging, prefill-bound agents, cold starts, false TTFT from first chunk / cache reuse