Lección 1 — Mesurer avant d'optimiser

TTFT, débit et percentiles : les bonnes mesures

6 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

TTFT, débit et percentiles : les bonnes mesures

Les cinq mesures

MesureDéfinitionCe qu'elle ditRessenti utilisateur
TTFT (time to first token)temps entre l'envoi et le premier fragment de réponsefile d'attente + lecture du prompt + début de génération« ça répond »
Latence totaletemps jusqu'au dernier fragmentTTFT + génération de tous les tokens de sortie« c'est fini »
Débit (throughput)requêtes (ou tokens) traitées par secondecapacité du système sous chargenombre de clients servis
Tokens entrée / en cache / sortie / réflexiondécompte renvoyé par l'API (usage_metadata)la base du coûtinvisible, mais c'est la facture
Qualitépart des réponses justes, sur un jeu de référencece qu'on risque en optimisant« c'est faux »

Un token est un morceau de mot (en français, environ 4 caractères en moyenne, ordre de grandeur) : c'est l'unité que les modèles lisent, produisent et que l'on paie.

sequenceDiagram
  participant C as "Client"
  participant API as "API Gemini"
  C->>API: "requête (1 030 tokens d'entrée)"
  Note over API: "file d'attente + lecture du prompt"
  API-->>C: "1er fragment"
  Note over C: "TTFT"
  API-->>C: "fragments suivants"
  API-->>C: "dernier fragment + usage_metadata"
  Note over C: "latence totale"

Instrumenter un appel

Tout le banc repose sur une seule fonction, ask, qui chronomètre et récupère les compteurs :

files/router.py (extrait)
async def call():
    c, parts, t0 = Call(model), [], time.perf_counter()
    async for chunk in await client.aio.models.generate_content_stream(
            model=model, contents=question, config=config):
        if not c.ttft and chunk.text:
            c.ttft = time.perf_counter() - t0          # premier fragment non vide
        parts.append(chunk.text or "")
        if chunk.usage_metadata:
            u = chunk.usage_metadata
            c.prompt_tokens = u.prompt_token_count or 0
            c.cached_tokens = u.cached_content_token_count or 0
            c.output_tokens = (u.candidates_token_count or 0) + (u.thoughts_token_count or 0)
    c.latency = time.perf_counter() - t0

Le coût découle des compteurs et d'une grille de prix (USD par million de tokens, relevée le 2026-10-15 sur la page officielle, à revérifier) :

files/router.py (extrait)
PRICES = {  # (entrée, entrée en cache, sortie)
    "gemini-3.5-flash-lite": (0.30, 0.03, 2.50),
    "gemini-3.8-flash": (0.75, 0.075, 3.75),
}

def cost_usd(model, prompt, cached, output):
    p_in, p_cached, p_out = PRICES.get(model, (0.0, 0.0, 0.0))
    return ((prompt - cached) * p_in + cached * p_cached + output * p_out) / 1e6

Une réponse a un TTFT de 0,7 s et une latence totale de 0,8 s. Où passe l'essentiel du temps ?

Pourquoi des percentiles et pas une moyenne

La moyenne est tirée par quelques valeurs extrêmes et cache la dispersion. Le p50 (médiane) est la valeur sous laquelle se trouvent 50 % des mesures ; le p95, celle sous laquelle se trouvent 95 % : 1 utilisateur sur 20 attend plus que le p95.

files/bench.py (extrait)
def pct(values, p):
    """Percentile p (inclusif). Avec 16 mesures, le p95 est presque le max : restez prudent."""
    if len(values) == 1:
        return values[0]
    return statistics.quantiles(values, n=100, method="inclusive")[p - 1]
Nombre de mesuresFiabilité du p95
16 (notre banc)indicatif : il dépend d'une ou deux mesures
100utilisable pour comparer deux réglages
1 000 et plusfiable, et permet le p99

Sur 16 requêtes, la latence p95 passe de 1,35 s à 1,76 s entre deux réglages. Que conclure ?

Exercice

À partir de ces 8 latences en secondes : 0.71, 0.78, 0.69, 0.79, 1.33, 0.86, 0.92, 3.10, calculez la moyenne, le p50 et le p95 avec statistics.quantiles(..., method="inclusive"). Laquelle des trois décrit le mieux « l'expérience typique » ? Laquelle signale le problème ?

Voir la solution
import statistics
v = [0.71, 0.78, 0.69, 0.79, 1.33, 0.86, 0.92, 3.10]
q = statistics.quantiles(v, n=100, method="inclusive")
print(round(statistics.mean(v), 2), round(q[49], 2), round(q[94], 2))
# 1.15 0.82 2.48
  • la moyenne (1,15 s) ne correspond à aucune requête réelle : elle est tirée par la valeur de 3,10 s ;
  • le p50 (0,82 s) décrit l'expérience typique ;
  • le p95 (2,48 s) révèle la longue traîne : il faut aller voir la requête à 3,10 s (escalade ? 429 suivi d'un réessai ?).

Résultat obtenu

Mesures réelles du 2026-10-15 (API Gemini, 16 questions, 4 en parallèle), extrait de python bench.py :

stratégie                        TTFT p50    p95  lat p50    p95  req/s  qualité    coût $ appels
big (gemini-3.8-flash)               1.57   2.02     1.81   2.57   1.97     100%   0.01560     16
small (gemini-3.5-flash-lite)        0.76   1.23     0.86   1.35   3.89      94%   0.00683     16
big (gemini-3.8-flash)           tokens entrée=16503 dont en cache=0 sortie+réflexion=860
small (gemini-3.5-flash-lite)    tokens entrée=16503 dont en cache=0 sortie+réflexion=753

Lecture : environ 1 030 tokens d'entrée par question pour une cinquantaine en sortie. C'est l'entrée qui domine la facture : 16 503 × 0,30 ≈5millieˋmesdedollard′entreˊepourflash−lite,contre753×2,50≈ 5 millièmes de dollar d'entrée pour flash-lite, contre 753 × 2,50 ≈ 2 millièmes en sortie (prix par million).

Je sais faire

  • Définir TTFT, latence totale, débit et percentiles, et dire ce que chacun révèle
  • Instrumenter un appel en streaming avec perf_counter et usage_metadata
  • Calculer le coût d'un appel, tokens en cache et de réflexion compris
  • Interpréter un p95 en fonction du nombre de mesures
Siguiente