Repository navigation
fix(server): count expanded media tokens in input_tokens - #2335
Merged
Merged
Conversation
The prompt-inspection routes encoded the rendered template text, where each image, clip or audio part is one placeholder; the soft-token expansion only happens on the worker, inside the per-family call that computes the media embeddings. On gemma-3-4b-it-4bit a two-clip body counted 52 while the same body's usage.prompt_tokens was 2,124, and an image body counted 16 against 275. Counting routes now share one counter. Text-only prompts are still encoded on the request thread. A prompt with resolved media goes to the worker as a count-only request (ServerGenerateOptions::count_prompt_only): the batch scheduler runs the same tokenization and media preparation a generation gets, answers Done with the expanded length, and releases the sequence before any prefill. Media bytes are the ones the render already fetched, and the preparation fills the vision feature cache a following generation reads. Workers that ignore the flag serve it as a one-token generation, which reports the same prompt_tokens. /v1/messages/count_tokens now renders and counts through the shared helpers instead of its own copy. Closes #1788
Review of the count-only path found two families whose media preparation leaves state outside the per-sequence bindings. Llama 3.2 Vision stashes cross-attention states in the model, shared by every in-flight decode, and never expands the prompt, so a count on it now answers from the unexpanded length without preparing the media. Phi4MM registers each prepared embedding by address until prefill consumes it, so the count path now drops that registration through LoadedModel::discard_prepared_vlm_embeddings instead of leaving it to outlive the array. The counter's documentation and docs/llama-server-compat.md now say a media count takes a worker slot like a generation, and narrow the no-second-read claim to images and audio. Refs #1788
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The counting routes encoded the rendered template text, where each image, clip or audio part is a single placeholder; the soft-token expansion only happens on the worker, inside the per-family call that builds the media embeddings. A client budgeting context with
input_tokensundercounted every media request by the size of its media.routes::prompt_inspection::count_prompt_tokens) now serves/chat/completions/input_tokens,/responses/input_tokensand/v1/messages/count_tokens(plus their unversioned aliases). The Anthropic handler renders and counts through the shared helpers instead of its own copy.ServerGenerateOptions::count_prompt_only). The batch scheduler runs the generation path's own tokenization and media preparation, answersDonewith the expanded length, and closes the sequence before any prefill, which releases the per-sequence state the binds attached. Media bytes are the ones the render fetched, and the preparation fills the vision feature cache a following generation reads. Workers that do not read the flag serve a one-token generation, which reports the sameprompt_tokens.Real checkpoint
mlxcel-server -m ~/models/mlx/gemma-3-4b-it-4bit --video-max-frames 4,MLXCEL_VIDEO_DIR_ALLOWLISTset to a directory with two 4 s ffmpeg lavfi clips (testsrc2,smptebars, 320x240), and a 224x224testsrcPNG as a data URI:Each "after" figure was identical on both paths of the route (
/v1/...and the unversioned alias). "Before" is the #1748 binary.Tests
routes/prompt_count_route_tests.rs: with a fake worker that expands each media item into 1,000 tokens,input_tokensequalsusagefor image bodies on all three routes and for the two-clip video body (that one is#[ignore], needs ffmpeg, and was run here with--ignored); a text-only count stays local and never reaches the worker.batch/scheduler/prompt_count_tests.rs: a count-only request answers its length from admission, queues nothing, runs no forward, leaves no live sequence, and a following request still prefills.--profile test-fast --features cuda,--test-threads=1) ofprompt_count,server::routes::prompt_inspection,server::routes::anthropic,server::batch::scheduler::request_memory,server::model_providerandphi4mmpass; clippy--lib --tests -D warnings, fmt and the contract checks are clean.Not covered
/v1/messagesstill fillsmessage_start.usage.input_tokensfrom a local encode of the placeholder text, so for image bodies it disagrees with this count and with non-streaming usage. That predates this change.Closes #1788