測定条件

数字を読むときに必要な前提をここに書きます。測り方に異論があるときも、まずこのページを見てください。

揃えているもの

項目補足
入力seedを固定して合成同じseedなら誰の手元でも同じ入力になる
温度0tasks/*.yamlrequest で変えられる
出力の上限全タスク一律 4096答えを切らないための安全弁で、難易度の一部ではない。思考を先に吐くモデルは思考分もここから使う。上限を答えの長さに絞ると、思考型モデルが本文を出す前に切れて、上限が正答率を決めてしまう
指示文タスクYAMLの1種類だけ指示文を工夫した場合との差は測っていない
採点決まった手順のみJSONとして読めるか / 項目が揃っているか / 値が正解と一致するか
文字列の比較タスクごとに指定同梱の2タスクは比べる前に空白を落とす(collapse_whitespace)。既定は落とさない
正解全行で同一行ごとの正答率を並べて比べられる
文面行ごとに描き直す。seedと行番号から決定論的に導く下の「行ごとに文面を変えている理由」を参照
投げる順番行の中では毎回同じ順序行の中で変えているのは同時本数だけ

行ごとに文面を変えている理由

同時1本、2本、4本…と測るとき、同じ正解を別の文面に描き直して投げます。正解(請求書なら金額・日付・明細、業務日報なら6件の決定とその置き場所)は全行で同じで、変わるのは表題・発行元・項目の並び順・埋め草の文章といった、正解に含まれない部分だけです。描き直しは seed と行番号から決めるので、同じ引数なら誰の手元でも同じものが出ます。

なぜ文面を変えるか。 全行で同じ文面を投げ直すと、推論サーバが前の行で読んだ内容を覚えていて、2行目以降は読み込みを省略します。入力2万字のタスクで、同じ4本同時が初見 59.69秒 / 2回目 0.40秒 に分かれました。150倍です。

なぜ正解は変えないか。 行ごとに正解まで変えると、行間の正答率の差に入力の個体差が混ざって読めなくなります。実測では、行ごとに別の48件を使ったときの正答率が83〜98%(幅15ポイント)にばらけ、同じ正解に揃えたときは88〜94%(幅6ポイント)に収まりました。

残るぶれ。 文面の表層が行ごとに違うので、表記の書き分けによる難易度のぶれは残ります。請求書では文面の型3種の構成比を全行で16件ずつに揃え、業務日報では答えの置き場所と紛らわしい行の本数(25本)を全行で固定していますが、ゼロにはなりません。

共通の指示文はキャッシュされます。 プロンプトの前半にあるタスクの指示は全行で同じなので、そこは読み込みが省略されます。実運用でも同じことが起きるので、これは許容しています。文面の1行目から先は行ごとに違うため、実測した先頭一致は請求書で6字、業務日報で107字(2万字のうち)でした。

測っている値

最初の文字が出るまでは、リクエストを送ってから、答えの本文の1文字目が届くまでの時間です。考えている内容を先に出すモデルでは、その分がここに含まれます。これは測り方の誤りではなく、利用者が実際に画面の前で待つ時間なので、そのまま測っています。

返り終わるまでは、リクエストを送ってから最後の文字を受け取るまでの時間です。

1秒あたりの生成量は、その同時本数での「全体で作った文字量 ÷ 全体にかかった時間」です。1件あたりの生成速度ではなく、機材全体が単位時間にこなせた量を表します。

正答率は、その行で投げた件数のうち採点を通った件数の割合です。1つでも値が違えばその件は不正解で、部分点はつけません。全行で同じ正解を使うので、行をまたいで比べられます。

時間超過と空応答は、正答率とは別に数えています。時間超過は --timeout で決めた秒数までに返り終わらなかった件、空応答は本文を1文字も返さなかった件です。どちらも不正解には入りますが、そのモデルの答えの質を測れていない状態なので、「このモデルではこの仕事が無理だ」という根拠には使えません。件数は結果のJSONに timeoutsempty_responses として残ります。

文字列を比べる前に、空白を落とすかどうかをタスクごとに選べますgrading.collapse_whitespace)。同梱の2タスクは落とす設定です。名刺印刷100枚名刺印刷 100枚 を区別すると、抜き出せているかどうかと、表記を揃えられるかどうかが混ざってしまうためです。実測では、品名の外れ29件のうち8件がこの形でした。帳票の文字列をそのまま転記する必要がある業務では、設定を外して測ってください。落としているのは空白だけで、カナと漢字、大文字と小文字、全角と半角は区別したままです。

p95 は補間せず、実際に測れた値をそのまま使います(48件なら遅い方から3件目)。補間すると、測っていない値が表に出てしまうためです。

正答率の95%区間は Wilson スコア区間で出しています。 を実測の正答率、n を件数、z を 1.96 として

中心 = (p̂ + z²/2n) / (1 + z²/n)幅 = z/(1 + z²/n) × √( p̂(1-p̂)/n + z²/4n² )

の中心 ± 幅です。閉じた式なので追加の依存も乱数も要りません。正規近似(p̂ ± z√(p̂(1-p̂)/n))と違って、正答率が0%や100%に寄っても区間が潰れず、範囲が0〜1をはみ出しません。

なぜ添えるか。 件数が少ないと正答率は粗くなります。48件なら1件が約2ポイント、12件だと 11/12 = 91.7% の次が 12/12 = 100% で、その間の値が存在しません。このとき「95%を割った」と書かれていても、件数が足りないだけかもしれません。区間の上端が基準を超えている行には表で を付けています。

読み方。 12件で全問正解しても区間は 76〜100% なので、「95%以上ある」とは言えません。48件で 46/48(96%)なら 86〜99% で、下端が95%を割るので、これも「95%以上を満たしている」と断言はできません。基準に対して白黒を付けたいなら、区間の幅が基準との差より狭くなるまで件数を増やす必要があります。判定そのものは実測の正答率(点推定)で決めていて、区間では変えていません。

揃えていないもの

以下は結果に影響しますが、このツールでは固定していません。結果どうしを比べるときは、これらが揃っているかを確認する必要があります。

同じ条件で測り直すと、1秒あたりの生成量は1割ほど動きます。小数第1位の違いに意味はありません。正答率は温度0なので、同じ入力に対しては測り直してもほぼ変わりません。

負荷のかけ方

同時にN本を保ち、1本終わったら次を投げる方式です。

実際の利用者は前の応答を待たずに次のリクエストを送るため、問い合わせが一気に集中する業務では、この測り方は待ち時間を実際より短く見積もります。到着の間隔を決めた測り方が必要になったときに差し替えられるよう、負荷をかける部分(src/local_llm_fit/run.py)は採点と分けてあります。

この測り方では分からないこと


使う

必要なものは uv と、OpenAI互換のAPIを出す推論環境です。LM Studio、Ollama、vLLM、SGLang、llama.cpp server、TGI などが該当します。動いている場所は手元でも社内サーバでも構いません。

git clone https://github.com/sys1yagi/local-llm-fit
cd local-llm-fit
uv sync

結果を持ち寄る

results/ に出たJSONをそのままプルリクエストで送ってもらえると、機材・モデル・量子化の組み合わせごとの実測が溜まり、一覧ページに並びます。1つの環境で測れることには限りがありますが、集まれば「この構成なら何本までさばける」という比較表になります。

推論サーバの種別は自動では分からないので、--server-label "LM Studio 0.3"のように書き添えてもらえると一覧の列に出ます。省いても測定はできます。

同じ理由で、結果を見比べるのに要るのに自動では取れない項目を、任意の引数で受け取ります。どれも省けます。省いた項目は unknown として記録され、測定は止まりません。

uv run fit --model <モデルID> \
  --server-label "LM Studio 0.3" \
  --quantization Q4_K_M \
  --server-concurrency 4

--quantization は量子化の表記、--server-concurrency は推論サーバ側に設定した同時処理数です。後者は待ち時間の変わり方をほぼ決めてしまうのに、APIからは読めません。量子化を返すサーバなら --quantization は省いても自動で入ります。電源につないでいるかどうかは macOS では自動で記録します。

送る前に、手元で形と数字を点検できます。

uv run fit check results/*.json

見るのは、必要なキーが揃っているか・タスクの version がいまの定義と同じか・数字の辻褄が合うか(正答数が件数を超えていないか等)の3つです。プルリクエストでも同じものが動きますが、警告を出すだけで落としません。測り直しを強いるより、事情を書き添えてもらう方が早いためです。