fix(llama): grow the detokenize buffer when a token piece exceeds 32 bytes - #2363
Open
Anai-Guo wants to merge 1 commit into
Open
fix(llama): grow the detokenize buffer when a token piece exceeds 32 bytes#2363Anai-Guo wants to merge 1 commit into
Anai-Guo wants to merge 1 commit into
Conversation
…bytes LlamaModel.detokenize() hands llama_token_to_piece a fixed 32-byte buffer and never checks for the negative return that means "did not fit". llama.cpp returns -(required size) in that case (src/llama-vocab.cpp:3596-3597) without writing anything, so `n` goes negative, `assert n <= size` still passes, and `bytes(buffer[:n])` slices from the end of a 32-byte buffer - yielding b'' for any piece of 64 bytes or more. Tokens whose piece is longer than 32 bytes are therefore dropped silently: a run of 64 spaces tokenizes fine and detokenizes to nothing, so detokenize(tokenize(x)) != x. Long whitespace runs are the common case, but any oversized piece is affected, and detokenize() is on the generation path. tokenize() one method up already handles exactly this convention for llama_tokenize (n_tokens = abs(n_tokens), then retry with a bigger buffer); llama.cpp's own token_to_piece_for_cache does the same. Do it here too, keeping the grown buffer for the remaining tokens. Fixes abetlen#2362 Signed-off-by: Tai An <antai12232931@outlook.com>
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.
Fixes #2362.
The bug
LlamaModel.detokenize()givesllama_token_to_piecea fixed 32-byte buffer and ignores the "did not fit" signal:llama_token_to_piecereturns negative-(required size)when the buffer is too small, and writes nothing (vendor/llama.cpp/src/llama-vocab.cpp:3596-3597):So for a piece of 64 bytes,
n == -64.assert n <= sizestill passes (-64 <= 32), andbytes(buffer[:n])becomesbytes(buffer[:-64])on a 32-byte buffer — which isb''. The token is dropped, silently.That is exactly the reported symptom: a run of 64 spaces tokenizes fine, and the whitespace token detokenizes to
b'', sodetokenize(tokenize(x)) != x. Long whitespace runs are the common trigger, but any piece over 32 bytes is affected, anddetokenize()is on the generation path (llama.pycalls it ~20 times while streaming completions), so this can corrupt model output, not just round-trips.The fix
Retry with a big-enough buffer, keeping it for the remaining tokens. This is the same convention
tokenize()one method up already handles forllama_tokenize:and what llama.cpp itself does in
llama_vocab::impl::token_to_piece_for_cache(llama-vocab.cpp:3328-3334):Pieces of 32 bytes or fewer take exactly the same code path as before — one call, same buffer.
Verification
Building llama.cpp was not needed: both
detokenize()bodies (upstreammainand this branch) are sliced verbatim out ofllama_cpp/_internals.pyby AST line range and run against a Python transcription ofllama_token_to_piecefollowing the C contract quoted above, with realctypesbuffers so thebuffer[:n]slicing behaviour is the real one. The vocabulary is one token per whitespace-run length plus" X", mirroring the[15270, 1599]pair in the issue.The 1/2/4/8/16/32-OK, 40/64/128-FAIL boundary and the
b''for the oversized token match the issue report exactly. 32 is the last size that fits, because the C check islength < size.ruff(>=0.15.7, aslint.yamlinstalls it, run against the repopyproject.toml):checkpasses,format --checkreports the file already formatted.Note on tests
I did not add a regression test:
tests/test_llama.pyruns against./vendor/llama.cpp/models/ggml-vocab-llama-spm.gguf, and I could not confirm that vocabulary contains a token whose piece exceeds 32 bytes — a test that silently passes on both branches would be worse than none. Happy to add one if you can point me at a test vocab that has such a token (or if you would prefer allama_token_to_piece-level unit test along the lines of the harness above).🤖 Generated with Claude Code