refactor(tokenizer): use strongly typed enum instead of hardcoded sentinel string - #8022
Conversation
Replace hardcoded U+FFFD replacement character string checks with a
typed DecodeResult enum (Complete | Partial) in the Decoder trait.
This makes partial-decode state explicit in the type system, eliminating
brittle `ends_with("U+FFFD")` checks in DecodeStream::step() and
Sequence::append_token_id().
Addresses: #6996 (comment)
- Complete variant may still contain interior U+FFFD from mid-stream invalid byte sequences; only trailing status is tracked. - Soften Decoder trait doc to not mandate a specific mechanism (from_utf8_lossy), since HF uses ByteFallback internally.
f5c6230 to
2849022
Compare
WalkthroughThis pull request introduces a new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
fixes a pre-existing edge case where legitimate U+FFFD vocabulary tokens were incorrectly suppressed
Can you add a test case that would've failed and caught this before this fix?
…ate U+FFFD tokens
…eResult Main commit 2cabf44 (feat: Decoder clean-up for incomplete multi-byte sequences, #8022) changed Decoder::decode() to return Result<DecodeResult> instead of Result<String>. The three backend tokenizer impls were updated, but two spots were missed: - lib/llm/src/tokenizers.rs: the default convert_ids_to_tokens trait impl .collect()s an iterator whose items are now DecodeResult, not String. Map via String::from (existing `impl From<DecodeResult> for String`) before collecting. - lib/llm/src/http/service/openai.rs: the /v1/detokenize handler passes DecodeResult directly into DetokenizeResponse { prompt: String }. Extract the inner string via .into() before constructing the response. Verified with `cargo check -p dynamo-py3 --features kv-indexer --lib` (the crate maturin builds in the container).
Overview:
Address a previous comment: #6996 (comment)
Introduce a
DecodeResultenum (Complete|Partial) to theDecodertrait, replacing hardcoded U+FFFD string checks in the incremental detokenization path. This makes partial-decode state explicit in the type system and fixes a pre-existing edge case where legitimate U+FFFD vocabulary tokens were incorrectly suppressed.Details:
DecodeResultenum (lib/llm/src/tokenizers.rs)Complete(String)-- fully valid output (may contain interior U+FFFD from mid-stream invalid bytes, but no trailing incomplete sequences)Partial(String)-- output ends with incomplete trailing multi-byte bytes that may be completed by subsequent tokensstrum::EnumIsfor auto-generatedis_partial()/is_complete()methodsFromconversions:From<String> for DecodeResult(viafrom_decodedheuristic) andFrom<DecodeResult> for Stringfrom_decoded()constructor classifies based on trailing U+FFFD;as_str()for borrowingDecoder trait returns
Result<DecodeResult>instead ofResult<String>DecodeStream::step()usesis_partial()instead ofends_with("\u{FFFD}")Sequence::append_token_id()usesis_partial()instead ofends_with("\u{FFFD}")TikToken fix (
lib/llm/src/tokenizers/tiktoken.rs)String::from_utf8()first on raw bytes -- if valid UTF-8, returnsCompletedirectly with zero extra allocation (takes ownership of theVec<u8>)from_utf8_lossy+ trailing-FFFD heuristic (from_decoded) when bytes are genuinely invalid UTF-8EF BF BD(valid UTF-8 encoding of U+FFFD) was previously misclassified asPartialbyfrom_utf8_lossy+ends_with('\u{FFFD}'), causing the incremental decoder to suppress itfrom_utf8validates and takes ownership vsfrom_utf8_lossywhich always allocates a newStringtest_decode_legitimate_replacement_char_token_is_complete) with a U+FFFD vocab token assertingCompletestatusHuggingFace / FastTokenizer -- use
From<String> for DecodeResult(which delegates tofrom_decoded, a post-hoc trailing-FFFD heuristic on the output string, since raw bytes are inaccessible through the HF library API)Where should the reviewer start?
lib/llm/src/tokenizers.rs--DecodeResultenum definition,Fromimpls, and updatedDecodeStream::step()/Sequence::append_token_id()lib/llm/src/tokenizers/tiktoken.rs--String::from_utf8fast path with lossy fallback, and the new regression test for legitimate U+FFFD tokensRelated Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)