[Bug] Context overflow is checked only after tokenization in the worker: a streamed request gets 200 plus an in-stream error, and an oversized `max_tokens` is silently clamped
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
調査の方向性
Start with python/freetoken/server/generation.py and trace submit_generation, then inspect message/tokenizer.py, tokenizer/tokenize.py, and scheduler/scheduler.py. Reproduce the context boundary for a streamed request and for prompt + max_tokens exceeding the limit. Done means oversized requests receive HTTP 400 with context_length_exceeded before streaming, counted token IDs are reused by the worker, and the policy for omitted max_tokens is resolved.
索引モデルが issue の本文から書いたものです。
説明
Summary
The context-length check runs only in the scheduler, after the worker has rendered and tokenized the prompt. Two
behaviours follow:
- A streamed request whose prompt does not fit gets
HTTP 200, an assistant role chunk, and then an SSE
{"error": ...}event, not a 400context_length_exceeded. Theprerender_errordocstring describes the client
side of this: some agents show only "empty response" for an in-stream error. - A request whose prompt fits but whose
prompt + max_tokensdoes not has itsmax_tokenslowered silently (a log
line only). The client asked for N tokens, cannot know it got fewer, and receivesfinish_reason: "length"early.
The scheduler's own error text already treats prompt plus generation as the limit ("... maximum (prompt +
generation)").
We fixed both by tokenizing once in the frontend, before any response header, and passing those exact IDs to the
worker.
Other fixes of ours from the same list, checked against main 4d2e2e2: overlap-scheduler output budget (requests
stopped at 127 of 128 tokens) is fixed upstream by #546 (scheduler.py:353). Request-local seed is still
missing on main, but #693 already reports it (our notes go there as a comment). Our disconnect handling is posted as
comments on #644 and #567. Our in-engine abort changes apply only to our layer-major prefill and do not concern
upstream.
Setup
- FreeToken 0.1.3
cac247a8plus our fork;nvidia/Qwen3.6-35B-A3B-NVFP4; context 131072; OpenAI
/v1/chat/completions - The behaviour is in the HTTP/scheduler layer; GPU and model do not matter.
Problem in upstream today (main 4d2e2e2)
python/freetoken/server/openai_api.py:194-199: forstream: true,prerender_errorruns first, but
server/generation.py:358-387only renders the template (manager.render_prompt) and never counts tokens.openai_api.py:201-210:submit_generation(generation.py:297-315) sends the raw messages to the worker and
returns a uid.StreamingResponseis then returned, which commits200.openai_api.py:250-256: the stream yields the role chunk before any reply. If the worker then rejects the prompt,
generation.py:700-701raisesGenerationErrorinside the stream, andopenai_api.py:270-276sends it as an SSE
error event.python/freetoken/scheduler/scheduler.py:514-539: the only length check (max_output_len <= 0), which produces the
context_length_exceededreply.scheduler.py:540-544:if msg.sampling_params.max_tokens > max_output_len:it sets
max_tokens = max_output_lenand only logs a warning.- The prompt is rendered twice (frontend pre-check, then worker) and tokenized once, in the worker.
What we changed
freetoken/python/freetoken/server/generation.py(submit_generation): for text-only requests the frontend
tokenizer (state.frontend_tokenizer) encodes the conversation once, in a worker thread. Ifcount < 1or
count + max_tokens > max_seq_len, it raisesGenerationError(..., code="context_length_exceeded"). Every adapter
already turns that into a 400 before aStreamingResponseexists. Template errors become a 400 the same way.message/tokenizer.py:TokenizeMsg.prepared_input_ids.tokenizer/tokenize.pyuses those IDs as they are, so the
worker does not render or tokenize again, and the IDs it serves are exactly the ones that were counted.scheduler/scheduler.py:max_tokens > max_output_lennow returnsErrorReplyMsg(code="context_length_exceeded")
instead of clamping. The scheduler stays the authoritative check, for image requests and other paths.
Measured impact
Boundary gate on the 128K profile (results/context-gates/final-f1-c128-a1-evaluation.json):
- prompt 130,396 tokens with
max_tokens676 (sum exactly 131,072) -> 200,finish_reason: "stop"after 28 tokens; - the same request plus one token -> 400
context_length_exceeded.
Not measured: the extra frontend tokenization time per request (the worker no longer tokenizes, so the total work is
roughly unchanged), and the 256K/64K profiles separately (their gates use the same check).
Correctness evidence
- The worker serves exactly the IDs the frontend counted, so the admission count and
usage.prompt_tokenscannot
disagree. - Our release soak runs through this path (2-hour soak: 891 client requests, 0 errors;
docs/RESULTS.md§3).
Limitations / not measured
- Requests with images keep the old path (the frontend check is skipped and only the scheduler check applies).
- When the client omits
max_tokens, our check uses the default output budget (DEFAULT_MAX_OUTPUT_TOKENS, 32768,
unless configured). A long prompt withoutmax_tokensis then rejected where upstream would clamp. Upstream may
prefer to clamp only an implicit default and reject only an explicitmax_tokens. - Only
/v1/chat/completionswas exercised;/v1/completions,/v1/messagesand/v1/responsesshare
submit_generation, but we did not test them.
Proposal
A PR along the lines above: count tokens in the frontend before committing the stream, pass prepared_input_ids,
and reject (rather than clamp) an explicit max_tokens that does not fit. Open question: should an omitted
max_tokens keep today's clamp?
Our implementation, for reference (fork of 0.1.3, Apache-2.0; paths in the fork):
- 主要言語
- Python
- スター
- 14.2k
- フォーク
- 1.4k
- 平均マージ
- 21時間 53分
- マージ済み PR(30日)
- 100
環境構築
このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
FlashML-org/FreeToken のほかの issue
-
Desktop feature
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
FlashML-org/FreeToken#800 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
FlashML-org/FreeToken#767 ·
メンテナーはふだん 1 日以内に返信
-
test_gemv_path_matches_the_transcription fails on sm_120 (bf16 rounding tie exceeds the tolerance)対応中かも @void-mckenzie が 18 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 77/100
FlashML-org/FreeToken#649 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
FlashML-org/FreeToken#648 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
FlashML-org/FreeToken#646 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
FlashML-org/FreeToken の issue をすべて見る
似ている issue
-
Bug: POWERTOOLS_IDEMPOTENCY_DISABLED set to an empty or non-boolean value crashes every invocation対応中かも @lechakrawarthy が今日担当しました。 オープンbug good first issue help wanted idempotency
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
aws-powertools/powertools-lambda-python#8728 · コメント 2 件 ·
メンテナーはふだん 2 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
emeraldleaf/okl#320 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 80/100
mnemosyne-oss/mnemosyne#1309 ·
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
huggingface/diffusers#15163 ·
メンテナーはふだん 1 日以内に返信
-
integration: ring
難易度 2/5 1〜3時間 初心者へのやさしさ 85/100
home-assistant/core#189787 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信