fix(pipeline): bound embed batches by UTF-8 byte count, not character count [pipeline, poller, docs, tests] - #245
Conversation
… count [pipeline, poller, docs, tests] `planEmbeddingBatches` の予算軸を UTF-16 code unit から UTF-8 バイト数へ移した。 #242 が置いた前提「1 token は入力の 1 文字以上に対応する」は byte_fallback で 破れる。語彙に無い文字は UTF-8 バイト列へ分解されるため、3 バイトの日本語 1 文字が 最大 3 token になる。実測は 2026-08-15 00:30 JST の cron で、文字数で高々 60000 と 課金した batch に 68736 token が返った(1 文字あたり 1.146 token)。 バイト数はその分解より下にある。byte_fallback が tokenizer の作れる最も細かい 分解であり、1 token は入力の 1 バイト以上を必ず消費するので、batch の合計 UTF-8 バイト数を天井(60000)以下に保てば合計 token 数も天井以下になる。#241 が 求めた「校正しない、構成上正しい」性質はそのままに、軸を 1 つ下げただけである。 変更点: - `MAX_EMBEDDING_BATCH_CHARS` -> `MAX_EMBEDDING_BATCH_BYTES`、課金は `utf8ByteLength(input)`。`utf8ByteLength` は code unit から数えるので入力の 複製を確保しない。孤立サロゲートは `TextEncoder` が置く U+FFFD と同じ 3 バイト として数える(truncate が code unit 境界で切るため実際に発生しうる) - `SPECIAL_TOKENS_PER_INPUT` (2) -> `TOKEN_OVERHEAD_PER_INPUT` (3)。sentinel 2 個 (`<s>` … `</s>`)に加え、SentencePiece の語境界マーカーが先頭 piece に併合され ない場合、入力バイトを消費しない token として 1 個現れる。極小 input が多数並ぶ batch ではこれが積み上がるため、input ごとに課金する(#242 の判断を維持) - #240 の file 予算との整合: 1 batch の最小 file 数が 7 から 2 へ下がる (8000 文字 = 最大 24000 バイト、予算 60000)。按分は 2/2 = 1、固定 2 と合わせて worst case はちょうど 3 で、`DIFF_SUBREQUESTS_PER_FILE = 3` は変更不要。ただし 余裕は無くなったので、その不変条件を `poller.test.ts` の実行可能な assertion に した - 回帰テスト: 16 file の日本語 commit(文字数予算なら 1 call、実測で天井超過)が 分割されること、`utf8ByteLength` が `TextEncoder` と全幅で一致すること 残る前提は「NFKC 正規化が入力をバイト数で膨らませない」ことのみで、互換分解を持つ 文字(アラビア語の合字、CJK 組文字)が入力の大半を占める場合にのみ問題になる。 退けた 2 つの前提が通常の diff テキストで破れたのとは性質が異なるため、 コードと要件記述の両方に前提として明記した。 #244
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
github-rag-mcp | 5753a41 | Aug 14 2026, 04:20 PM |
smileygames
left a comment
There was a problem hiding this comment.
AI self-review (auto mode, parent)
utf8ByteLength の正しさ
分岐を全て追った。< 0x80 → 1、< 0x800 → 2、well-formed な surrogate pair → 2 code unit で 4(i++ で low をスキップ)、孤立 surrogate → 3、その他 → 3。TextEncoder の意味論と一致する。孤立 surrogate を 3 にしているのは U+FFFD 置換が 3 バイトであることに合わせたもので、prepareDiffEmbeddingInput が code unit 境界で truncate する以上、実際に発生しうる入力への手当てになっている。
TOKEN_OVERHEAD_PER_INPUT を 2 から 3 へ上げた判断
支持する。しかもこれは、過去 3 回と同じ種類の穴を先回りして塞いでいる。
バイト境界が成立する条件は「全ての token が入力の 1 バイト以上を消費すること」。SentencePiece の語境界マーカー ▁ は、先頭に付与される場合に入力バイトを 1 つも消費しない token になりうる。入力ごとに 1 個。そして batch の入力数に上限が無い(#242 が per-input 課金を選んだのと同じ理由)以上、微小入力を並べれば未計上の token が積み上がって境界が破れるわ。
#237 は「推定が実測より楽観」、#242 は「文字より細かい token が存在する」で破れた。今回のこれは**「入力を消費しない token が存在する」**で、同じ族の 3 つめよ。実測を待たずに閉じているのは良い仕事。
#240 との整合(本 issue が要求した検証)
算術を再計算した。
1 入力の最大バイト数 = 8000 code unit × 3 = 24,000
1 batch の最小 file 数 = floor(60,000 / 24,000) = 2
batch あたり subrequest 2 を 2 file で按分 = 1/file
固定費 2/file(D1 FTS mirror + Store DO)
合計 = 3/file = DIFF_SUBREQUESTS_PER_FILE
一致する。ただし余裕が消えている。文字数軸のときは最小 7 file/batch で按分後 0.29、丸めで 3 に切り上げていた分の緩衝があったけれど、いまは端数なしでちょうど 3 よ。
6 repo での phase 最悪値も確認した。18×2 + 9×2 + 20 = 74、予算 75。残り 1。
この状態を放置せず実行可能な assertion に変えていることを評価する。batch 軸をこれ以上詰めたら、静かに invocation 予算を超えるのではなくテストが落ちるわ。rules/model/subtractive-structural-beauty.md が言う「将来の実行が保証されない手続きは、確実に実行される構造へ置き換える」の実施例ね。
境界の性質について、正確に書いておく
この PR で境界は無条件にはならない。仮定が 1 つ残り、性質が変わっただけよ。
tokens(input) <= utf8ByteLength(input) + TOKEN_OVERHEAD_PER_INPUT
右辺の 3 は「入力バイトを消費しない token は入力あたり 3 個以下」という仮定を含む(<s> / </s> / ▁)。それを超える種類が存在すれば破れる。
ただし過去 2 回との差は大きいわ。#237 は比率の推定(連続量の当て推量)、#242 は偽である補題。今回は離散的な列挙で、外れるとすれば「数え漏らした種類がある」という形になる。同じ轍ではないけれど、無条件と呼ぶのは正確ではないの。実装側が NFKC 正規化の残存仮定を明記しているのと同じ扱いで、こちらも記録しておく。
受け入れ条件
| criterion | 結果 |
|---|---|
| 1 batch の合計 UTF-8 バイト数が上限を超えない | pass — planner が utf8ByteLength + 3 で課金 |
1fb0f6b / 92eb94d が embed に成功 |
post-merge 観測 |
forward watermark が 2026-08-11T15:33:02Z を越える |
post-merge 観測 |
| 非 ASCII を含む実測相当の回帰テスト | pass — 16 file 日本語 commit、文字数予算では通り実コストでは超える二重固定 |
| #240 の file 予算が破綻していない | pass — 上記の算術、assertion 化済み |
345 tests pass、tsc --noEmit clean、CI 3 件 green、mergeStateStatus CLEAN。
申し送り(判断を求められた件)
MAX_EMBEDDING_INPUT_CHARS の per-input 軸にバイト上限が要るかどうか。本 PR では対応不要と判断する。
per-input 窓はモデル自身が持っていて、endpoint が拒否しているのは batch 合計だけ。軸が違うわ。ただし旧根拠(8000 文字 ≈ 8192 token 窓の内側)が偽と判明した以上、per-input 側が実際に何を保証しているかは未確認の状態になっているの。本 PR が導入した退行ではないので、観測が出てから扱う。
next step
auto mode のため human check なし。self-review pass をもって AI が直接 merge する。merge により Workers Builds が deploy し、次の DIFFS_CRON(毎時 30 分)が判定になる。4 周目にして初めて、境界の根拠が実測より楽観でない状態で観測に入る。
Closes #244
何を変えたか
planEmbeddingBatchesの予算軸を UTF-16 code unit(文字数)から UTF-8 バイト数へ移した。なぜ
#242 が置いた前提「1 token は入力の 1 文字以上に対応する」は byte_fallback で破れる。語彙に無い文字は UTF-8 バイト列へ分解されるため、3 バイトの日本語 1 文字が最大 3 token になる。
実測(2026-08-15 00:30 JST cron、#242 deploy 後):
文字数で高々 60000 と課金した 1 batch に 68736 token が返っている(1 文字あたり 1.146 token)。
byte_fallback は tokenizer が作れる最も細かい分解であり、それ以下には割れない。したがって 1 token は入力の 1 バイト以上を必ず消費する。batch の合計 UTF-8 バイト数を天井(60000)以下に保てば、合計 token 数も無条件に天井以下になる。#241 が求めた「校正しない、構成上正しい」という性質を保ったまま、軸を 1 つ下げただけである。
変更点
MAX_EMBEDDING_BATCH_CHARS->MAX_EMBEDDING_BATCH_BYTESutf8ByteLength(input)に変更。天井 60000 は据え置きutf8ByteLengthを追加TextEncoderが置く U+FFFD と同じ 3 バイトとして数える(truncate が code unit 境界で切るため実際に発生しうる)。TextEncoderとの一致をテストで固定SPECIAL_TOKENS_PER_INPUT(2) ->TOKEN_OVERHEAD_PER_INPUT(3)<s>…</s>)に加え、SentencePiece の語境界マーカーが先頭 piece に併合されない場合、入力バイトを消費しない token として 1 個現れる。極小 input が多数並ぶ batch ではこれが件数分積み上がるため、input ごとに課金する(#242 の判断を維持)MAX_EMBEDDING_INPUT_CHARSの comment#240 の file 予算との整合(issue の必須検証項目)
1 batch あたりの最小 file 数が 7 -> 2 に下がる(1 input = 8000 文字 = 最大 24000 バイト、予算 60000)。
DIFF_SUBREQUESTS_PER_FILEしたがって定数の変更は不要。ただし丸めの余裕(従来 2.29 -> 3)は無くなったため、この不変条件を
poller.test.tsの実行可能な assertion にした。batch 軸をこれ以上絞ると invocation 予算を静かに超過するのではなくテストが落ちる。6 repo 時の 1 phase の worst case: file 18 × 2 + batch 9 × 2 + overhead 20 = 74 <= 75(
floor(900/12))。テスト
utf8ByteLengthがTextEncoderと全幅(ASCII / 2 / 3 / 4 バイト / 孤立サロゲート)で一致npm test= 345 tests pass、tsc --noEmitclean。残る前提(明記した上で受け入れ)
「tokenizer の NFKC 正規化が入力をバイト数で膨らませない」ことだけが前提として残る。互換分解を持つ文字(アラビア語の合字、CJK 組文字)は例外だが、退けた 2 つの前提が通常の diff テキストで破れたのとは性質が異なり、こちらは入力の大半がその稀なブロックで占められている必要がある。隠さずコードと
docs/0-requirements.mdの両方に前提として書いた。コスト
日本語主体の入力では 1 文字 3 バイトのため、文字数軸に比べ約 3 倍保守的になり batch 数が増える。これは正しさの代金として受け入れる(issue の方針どおり)。