Fixed character encoding for arch-specific generate loops. - #595
Draft
fgheorghe wants to merge 1 commit into
Draft
Fixed character encoding for arch-specific generate loops.#595fgheorghe wants to merge 1 commit into
fgheorghe wants to merge 1 commit into
Conversation
Per-token tokenizer.decode() ran from_utf8_lossy over half a character, so emoji and byte-fallback CJK reached the client as replacement chars. Adds TokenTextStream (holds back the incomplete tail, flushes at end of stream) and routes every arch decode loop through it. qwen35's daemon path was already correct, which is why this stayed hidden.
Collaborator
|
Triage: still relevant, but the branch predates the generate/daemon split. Please rebase and re-home TokenTextStream onto current spec_emit, dense loops, and multi-slot decoding with focused UTF-8 boundary coverage. |
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.
Appears to affect: arch-specific generate loops.
Tested for Deepseek v4 flash 0713 - requires more manual testing to confirm nothing else is broken.
Supplied by GLM 5.2 and Claude. Feels like it could be done better though. Tests are dubious.
Problem
Any character whose UTF-8 spans more than one token — emoji, byte-fallback
CJK,
∑∫, accented Latin — reached the HTTP client as replacement chars:Tokenizer::decode()only reassembles byte fragments within a single call, sodecode(&[tok])per streamed token runsfrom_utf8_lossyover half acharacter. qwen35's ordinary daemon path already held the tail back correctly
with
decode_bytes+valid_up_to— because that's the mainline arch, the bugstayed invisible on every other one.
Broken — every request, all modes:
deepseek-v4-flash, gemma4, muse-glimmer, lfm2moe, minimax, cohere2moe, qwen2. Single-GPU, multi-GPU, expert-parallel and speculative decoding alike — no mode escaped it.
Broken — conditionally:
qwen3.5 / 3.6 only with serve.multi_slot = true. Off by default.
Fine:
qwen3.5 / 3.6 on ordinary serve (the default, so most traffic), the continuous-batch lane, and the pipeline-parallel path.
What actually corrupts: emoji always, maths symbols, accented Latin, and CJK on vocabs without whole-character tokens. Plain ASCII was never affected — which is why this survived so long.
Where it showed: visible content, the reasoning channel, and tool-call arguments — anywhere text crosses the wire. Not the KV cache or sampling; the model was fine, only the text handed back was mangled.
Change
Adds
TokenTextStream: buffers only the trailing incomplete codepoint, O(1)per token, ≤3 bytes held. Flushes at end of stream (generation can stop
mid-character); invalid bytes emit one replacement char and advance rather than
stalling the buffer.
Rewired: deepseek4 (AR/EP/multi-GPU/spec), gemma4 (AR + EAGLE), muse-glimmer,
lfm2moe, minimax (AR/EP), cohere2moe (AR + spec), qwen2, CLI multi-slot serve.
Channel-routed paths (cohere2moe sections, harmony router, ds4 DSML parser)
send the flushed tail down whichever channel was open at end of turn. Grammar
matchers keep per-token decode — they drive a state machine, not client output.