forked from ggml-org/llama.cpp
-
Notifications
You must be signed in to change notification settings - Fork 45
server: do not abort a completion when the model emits invalid UTF-8 #202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
danielhanchen
wants to merge
9
commits into
master
Choose a base branch
from
fix/server-invalid-utf8-abort
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
61726ba
server: do not abort a completion when the model emits invalid UTF-8
danielhanchen 25fcc22
server: flush a trailing incomplete UTF-8 sequence on the final parse
danielhanchen be6431c
server: match the JSON serialiser's UTF-8 replacement boundaries
danielhanchen 4470648
server: apply the lead-specific first-continuation bounds
danielhanchen 9b2955e
server: reject complete but illegal UTF-8 before copying it through
danielhanchen 6dc4a96
server: trim the UTF-8 sanitising comments
danielhanchen b27c0b4
tighten the comments added by this change
danielhanchen a5ac51d
server: unit test for the UTF-8 sanitising, against an independent WH…
danielhanchen ff1d590
server: pin the token pipeline case, streaming against non streaming
danielhanchen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a streamed completion ends with an incomplete UTF-8 sequence, this final flush cannot replace it because
server-context.cppdetects the incomplete suffix at lines 1744–1749 and never sends those bytes in a partial result; the streaming final result then supplies an emptycontentat lines 2002–2005. Consequentlygenerated_text_pendingis empty here, so the malformed suffix is silently omitted rather than emitted as U+FFFD, while the equivalent non-streaming request is sanitized. This occurs when EOS or the token limit follows a token ending in a UTF-8 lead/prefix, and the new test misses it because it callsupdate_chat_msg()directly instead of exercising the server token pipeline.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct on the mechanism, and it is now pinned by a test. Measured rather than reasoned, on both arms.
What the streaming path actually does.
validate_utf8()atserver-context.cpp:1745scans the last up to four bytes ofslot.generated_textfor a lead byte and returnslen - iif the sequence is cut off, soincompleteis true andprocess_token()skipssend_partial_response()for that token entirely.send_final_response()then setscontent = ""in stream mode. Sogenerated_text_pendingis indeed empty at the final flush and the tail is dropped.But it was dropped before this change too, so nothing is lost that was not lost already. End to end on
stories15M-q4_0.gguf,logit_biasforcing a lead byte so generation ends on one, 12 streaming configurations (n_predict1 and 4, three lead bytes0xC30xE20xF4,/completionand/v1/chat/completions):With four forced
0xC3tokens the stream carries exactly one SSE event, the final one with"content":"", on both arms: no partial is ever emitted for a run of lead bytes, becausevalidate_utf8()holds the last one back every time.So what this change creates is a divergence, not a loss: the non-streaming side is now substituted and the streaming side still is not, where before both were lossy. That is worth fixing, and the fix is in
process_token()andsend_final_response()rather than here: it means sending bytes that are currently deliberately withheld, which changes what every streaming client receives. Widening this PR to do that would put a streaming-content change behind a "do not abort" fix, so I have left it out and noted it as a follow up instead.On the test missing it. Fair, and fixed in
ff1d5901e.test-server-utf8 pipelinenow modelsprocess_token()'s hold-back rather than callingupdate_chat_msg()directly: it accumulates a slot text, applies the realvalidate_utf8()gate, sends only what the slot would send, then does the empty final flush, and compares that against the same text driven non-streaming. 13 cases.The hard assertion is that no decodable content may be lost in either mode: 0 before, 0 after, on both sides. The asymmetric rows are reported and deliberately not counted as failures, with the reason stated in the output. Note the
tail-*rows areagreebefore the change because both modes dropped the tail, andASYMMETRICafter because only one of them was fixed, which is exactly the point you are making.Also worth noting from the same table: a complete but ill formed sequence at the end (surrogate, overlong, above U+10FFFF, stray continuation byte) is not held back by
validate_utf8(), so both modes see it and both substitute identically. The divergence is confined to the incomplete-suffix case.