Repository navigation
[Rust Frontend] Support --skip-tokenizer-init - #80
Closed
richard-for-bugen[bot] wants to merge 2 commits into
Closed
richard-for-bugen[bot] wants to merge 2 commits into
richard-for-bugen[bot] wants to merge 2 commits into
Conversation
richard-for-bugen
Bot
force-pushed
the
richard/parity-impl-skip-tokenizer-init
branch
from
October 30, 2026 07:26
4112478 to
972d35f
Compare
Author
|
Redesigned as you suggested, force-pushed over the old head 4112478 (now ec3f71c, rebased on the synced fork
CI command set green (2408 passed, 13 skipped); GB200 e2e |
richard-for-bugen
Bot
force-pushed
the
richard/parity-impl-skip-tokenizer-init
branch
from
October 30, 2026 07:40
972d35f to
ec3f71c
Compare
BugenZhao
approved these changes
Oct 30, 2026
| pub fn prepare(&self, mut request: TextRequest) -> Result<PreparedTextRequest> { | ||
| // Without a tokenizer the output stays token IDs only, which also | ||
| // rejects stop strings in `validate`. | ||
| if !self.backend.has_tokenizer() { |
Owner
There was a problem hiding this comment.
Suggested change
| if !self.backend.has_tokenizer() { | |
| if !self.has_tokenizer() { |
Serve token IDs only without loading a tokenizer, like Python's `skip_tokenizer_init`. Model file resolution reads no tokenizer-side files, and the text backend holds an `UnavailableTokenizer`: a null tokenizer whose text operations fail with `TokenizerError::Unavailable`, mapped to a 400, and whose vocabulary size is the model's, which bounds token IDs. The `Tokenizer` interface is unchanged. Only loading and one `TextBackend::has_tokenizer` flag know the tokenizer is absent: request preparation turns detokenization off, reusing the token-only output path and its stop-string rejection, and the chat backend rejects every chat request. Everything that needs a tokenizer (text prompts, chat requests, stop strings, bad_words, string-form completion logprobs, echoing a token-ID prompt as text, /tokenize, /detokenize) returns a 400, and so do structured outputs submitted to the engine, which skips its tokenizer too. `vllm-rs serve` forwards the flag to the managed engine. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: richard-for-bugen[bot] <342439978+richard-for-bugen[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: richard-for-bugen[bot] <342439978+richard-for-bugen[bot]@users.noreply.github.com>
richard-for-bugen
Bot
force-pushed
the
richard/parity-impl-skip-tokenizer-init
branch
from
October 30, 2026 08:00
47f09f8 to
46faac6
Compare
Author
|
Promoted as vllm-project#64697 (one squashed commit e3008f8 on |
Author
|
Merged upstream as vllm-project#64697; closing. |
richard-for-bugen
Bot
deleted the
richard/parity-impl-skip-tokenizer-init
branch
November 4, 2026 19:05
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
Python's
--skip-tokenizer-initserves token IDs only: no tokenizer is loaded, prompts must be token IDs, and responses carry token IDs with empty text. The Rust frontend declares the flagUnsupported, so a server started with it exits at startup. Undervllm servewithVLLM_USE_RUST_FRONTEND=1, the flag reaches Rust through--args-jsonand fails the same way, althoughtests/entrypoints/openai/completion/test_token_in_token_out.py(a model directory without tokenizer files) is exactly this use case.This PR implements the flag. Behavior with it:
/v1/completionswith a token-ID prompttextis"", token IDs returned on request/inference/v1/generate, gRPC generate/tokenize,/detokenizeTokenizer not available when skip_tokenizer_init=True)"this request needs a tokenizer, which is not loaded when `--skip-tokenizer-init` is set"echoof a token-ID prompt withoutreturn_token_idsAssertionError(500)stopstrings,bad_wordsupdate_from_tokenizerskipped)logprobsreturn_tokens_as_token_idsworksreturn_tokens_as_token_ids(request or server default) applies; choice-levelprompt_logprobs(keyed by token ID) and/inference/v1/generatelogprobs workNone;generation_configEOS ids become stop tokensconfig.jsonEOS id (Rust's existing fallback when the tokenizer has no EOS); the rest stay stop tokensDesign and layering
The tokenizer is a null object, not an
Option. Under the flag the text backend holds anUnavailableTokenizerwhose text operations fail with a dedicated error, so theTokenizerinterface and its consumers (prompt encoding,tokenize_bad_words,/tokenize,/detokenize, echo decoding) are unchanged and fail through their existing error paths. Only two places know the tokenizer is absent: loading, and oneTextBackend::has_tokenizerflag for the checks that have no tokenizer call to fail.vllm-tokenizer).TokenizerErrorbecomes an enum:Backend { message }for the existing failures, andUnavailable.UnavailableTokenizerreturnsUnavailablefromencode,encode_ordinaryanddecode, has no token strings, and reports the model vocabulary as its size, which bounds token-ID prompts like Python ([Bugfix][Core] Validate prompt token ids when skip_tokenizer_init is set vllm-project/vllm#62911). The rest of this crate's diff is the mechanical rename of the error macro (tokenizer_error!tobackend!).vllm-text,vllm-chat).TokenizerError::Unavailablemaps tovllm_text::Error::TokenizerUnavailable, a request validation error, so every rejection is a 400 with one message./tokenizeand/detokenizenow map tokenizer errors throughtext_submit_errorinstead of a blanket 500, so other tokenizer failures keep their 500 and onlyUnavailablebecomes a 400. Side effect: other request validation errors from/tokenize(for example prompt truncation) become 400 too, instead of 500.vllm-cmd).skip_tokenizer_initmoves from theUnsupportedmarkers toModelArgs, next to--tokenizer,--revisionand--hf-config-path, because like them it decides which model files are read. It flows intoModelFileSources, andModelArgs::python_argsforwards it to the managed engine undervllm-rs serve: the engine's structured-output manager would otherwise load a tokenizer, which fails when the files are absent. The render server sharesModelArgs.vllm-text,vllm-chat).ModelFileSources::skip_tokenizer_initreads no tokenizer-side file (tokenizer,tokenizer_config.json, chat template; an explicit--tokenizersource is not opened), so a cached snapshot only needsconfig.json.HfTextBackendbuilds theUnavailableTokenizerwhen no tokenizer source is resolved, andload_model_backendsbuilds aTokenizerlessChatBackendinstead ofHfChatBackend: rendering and output parsing both need a tokenizer, so it rejects every chat request.TextRequestProcessor::preparesetsdecode_options.detokenize = falsewhen there is no tokenizer, which reuses the token-only output path (TokenOnlyDecoder) and the stop-string rejection of [Perf][Rust Frontend] Skip detokenization for gRPC requests that read no text vllm-project/vllm#63789. Two more checks read it, because their consumer is not a tokenizer call in this process: structured outputs are rejected inTextLlmjust before engine submission, since the engine skips its tokenizer too (the render server, which sharesprepare, forwards them to an engine it does not configure); completionslogprobsare rejected up front unlessreturn_tokens_as_token_idsapplies, becauseid_to_tokenreturningNonewould key every candidate by the same empty string instead of failing.Validation
cargo fmt --check,cargo sort --workspace --check,cargo deny check bans,cargo clippy --workspace --all-targets --all-features --locked -D warnings: clean.cargo nextest run --workspace --all-features --locked: 2408 passed, 13 skipped.vllm-texttokenizerless_backend_prepares_token_id_prompts_only(token-ID prompt lowers with detokenization off; text prompt, stop strings,bad_wordsrejected; structured outputs survivepreparefor the render server; model-vocab bound) andskip_tokenizer_init_loads_a_model_without_tokenizer_files(resolution and backend load ignore a missing--tokenizersource and an unparsabletokenizer_config.json; EOS from the model config);vllm-chatskip_tokenizer_init_rejects_every_chat_request;vllm-serverskip_tokenizer_init_serves_token_id_completions_only(token-ID completion withreturn_token_idsreturnstext: ""and the token IDs; text prompt,echo,stop, string-formlogprobs, structured outputs,/tokenize,/detokenizeare 400);vllm-cmdskip_tokenizer_init_reaches_backends_and_managed_engine(CLI and--args-json, forwarded to the managed engine).tests/entrypoints/openai/completion/test_token_in_token_out.py(Qwen3-0.6B with the tokenizer files removed,--skip-tokenizer-init, dummy weights): with this branch'svllm-rs1 passed; withmain'svllm-rs1 error (the frontend rejectsskip_tokenizer_initin--args-jsonat startup); Python frontend 1 passed.Notes
bad_wordsrejections are deliberate divergences: Python accepts them and silently does nothing, which the Rust frontend's "reject what is not implemented" rule forbids.--skip-tokenizer-init(Python only tests that the chat server object starts without one), so this PR rejects every chat request uniformly. Keeping a tokenizer-optional Harmony chat backend is a possible follow-up.test_token_in_token_out.pycan join the Rust frontend CI step once this merges.Codex review
This revision replaces the
Option<DynTokenizer>design (old head 4112478) with the null tokenizer. Codex (gpt-6.1-sol, high) on the new design: changes needed, with the same two P2 findings as on the old design, both rejected for the same reasons, and one nit left open./tokenizewithout the HF tokenizer". Python's Harmony path does work without one, but only incidentally; nothing tests or documents chat under--skip-tokenizer-init. Supporting it means makingHfChatBackendtokenizer-optional, a follow-up (see Notes).skip_tokenizer_initis oneModelConfigfield shared by frontend and engine: Python's launcher andvllm-rs servepass it to both, and Python's frontend rejects structured outputs under it regardless of the engine.logprobswithreturn_tokens_as_token_ids, streaming) have no route test. Covering them needs a mock engine that emits logprobs; the condition itself is one boolean next to the rejection that is tested.It confirmed the rest of the layering (file loading in
ModelArgs→ModelFileSources→HfTextBackend, token-only output throughTokenOnlyDecoder, the logprobs guard protectingdecoded_logprobs_to_openai) and found no other correctness issue.