AI News HubLIVE
站内改写3 分で読了

最初のトークンまでの時間(TTFT)の測定方法

この記事では、AIシステムにおける最初のトークンまでの時間(TTFT)の測定方法、従来のWeb APIパフォーマンス測定との根本的な違い、およびPython、Node.js、Apache JMeterを使用したLLMワークロードの計装方法について説明します。

ソースHacker News AI著者: qainsights

AIシステムにおいて、最初のトークンまでの時間(TTFT)は、ユーザーが感じる応答性を測る重要な指標です。従来のHTTP応答時間とは異なり、TTFTはプロンプトリクエストを送信してから応答ストリームの最初のトークンを受信するまでの経過時間を測定します。ストリーミングLLM APIでは、ユーザーは応答の終了時ではなく開始時に応答性を知覚するため、この違いは重要です。

従来のWeb APIパフォーマンス指標(応答時間、スループット、エラー率)は、LLMに適用すると完全に機能しなくなります。OpenAI、Anthropic、またはローカルでホストされたOllamaエンドポイントを呼び出すと、他のREST APIと同様に見えますが、その応答時間は誤解を招きます。LLMは応答全体を一度に計算するのではなく、トークンを1つずつ生成してストリーミングします。従来のパフォーマンスツールが記録するのは、最初のトークンではなく、最後のトークンが到着した時刻です。ユーザーが4秒間空白の画面を見つめ、その後テキストの壁が到着する体験は、たとえ総経過時間が5秒でSLA内であっても、ひどいものです。

完全なLLMパフォーマンス測定戦略では、TTFT(知覚応答性)、TTLT(エンドツーエンドレイテンシ)、トークンスループット(生成速度)、トークン間レイテンシ(ストリーミングの滑らかさ)、グッドプット(実際のシステム容量)、ジッター(同時負荷下での一貫性)など、複数の指標を追跡する必要があります。TTFTは主要な指標ですが、単独で存在するわけではありません。

LLM APIは、サーバー送信イベント(SSE)またはチャンクHTTP転送エンコーディングを介してトークンを配信します。サーバーは完全な応答をバッファリングするのではなく、生成されたトークン(またはトークンの小さなグループ)をそのままフラッシュします。例えば、OpenAI APIの生のSSEストリームは次のようになります:

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{"content":"The"}}]}

data: {"id":"chatcmpl-abc","object":"chat.completion.chunk","choices":[{"delta":{"content":" capital"}}]}

data: [DONE]

各data:行は個別のチャンクです。実際のコンテンツを含む最初のdata:行のタイムスタンプがTTFT測定ポイントです。

以下は、PythonのhttpxライブラリとAnthropic Messages APIを使用した正確なTTFT測定の例です:

import httpx
import time
import json

def measure_ttft(prompt: str, api_key: str) -> dict:
    url = "https://api.anthropic.com/v1/messages"
    headers = {
        "x-api-key": api_key,
        "anthropic-version": "2023-06-01",
        "content-type": "application/json",
    }
    payload = {
        "model": "claude-sonnet-4-20250514",
        "max_tokens": 512,
        "stream": True,
        "messages": [{"role": "user", "content": prompt}],
    }
    ttft = None
    ttlt = None
    token_count = 0
    request_start = time.perf_counter()
    with httpx.Client(timeout=60) as client:
        with client.stream("POST", url, headers=headers, json=payload) as response:
            for line in response.iter_lines():
                if not line.startswith("data:"):
                    continue
                raw = line[len("data:"):].strip()
                if raw == "[DONE]":
                    break
                try:
                    chunk = json.loads(raw)
                except json.JSONDecodeError:
                    continue
                event_type = chunk.get("type", "")
                if event_type == "content_block_delta":
                    now = time.perf_counter()
                    if ttft is None:
                        ttft = now - request_start
                    token_count += 1
                    ttlt = now - request_start
    return {
        "ttft_ms": round(ttft * 1000, 2) if ttft else None,
        "ttlt_ms": round(ttlt * 1000, 2) if ttlt else None,
        "token_count": token_count,
        "throughput_tokens_per_sec": round(token_count / ttlt, 2) if ttlt else None,
    }

重要なのは、time.perf_counter()を使用して高解像度のタイミングを取得することです。time.time()はサブ秒精度に適していません。

Node.jsでは、Anthropic SDKのストリーミングインターフェースを使用します:

import Anthropic from "@anthropic-ai/sdk";

interface LLMMetrics {
    ttft_ms: number | null;
    ttlt_ms: number | null;
    token_count: number;
    throughput_tokens_per_sec: number | null;
}

async function measureTTFT(prompt: string): Promise<LLMMetrics> {
    const client = new Anthropic();
    let ttft: number | null = null;
    let ttlt: number | null = null;
    let tokenCount = 0;
    const requestStart = performance.now();
    const stream = client.messages.stream({
        model: "claude-sonnet-4-20250514",
        max_tokens: 512,
        messages: [{ role: "user", content: prompt }],
    });
    for await (const chunk of stream) {
        if (chunk.type === "content_block_delta") {
            const now = performance.now();
            if (ttft === null) {
                ttft = now - requestStart;
            }
            tokenCount++;
            ttlt = now - requestStart;
        }
    }
    return {
        ttft_ms: ttft !== null ? Math.round(ttft * 100) / 100 : null,
        ttlt_ms: ttlt !== null ? Math.round(ttlt * 100) / 100 : null,
        token_count: tokenCount,
        throughput_tokens_per_sec: ttlt && tokenCount ? Math.round((tokenCount / (ttlt / 1000)) * 100) / 100 : null,
    };
}

performance.now()Date.now()の代わりに使用し、ミリ秒未満の分解能を得ます。

TTFTのベンチマーク:200ms未満が優秀、200-500msが良好、500-1秒が普通、1-3秒が不良、3秒超はインタラクティブ用途では許容できません。同時負荷下ではこれらの数値は大幅に増加します。したがって、平均ではなくp95 TTFT目標を設定することを推奨します。

一般的な落とし穴:HTTP応答時間をTTFTとして測定する、ストリーミングを有効にしない、低解像度のタイマーを使用する、単一ユーザーの同時実行のみテストする、プロンプト長を変数として無視する、モデルレイテンシとネットワークレイテンシを混同する。