Repository navigation
Description
Proposal to improve performance
DetokenizedGenerationResultBase._handle_response checks string stop
sequences after it detokenizes a response. On the streaming path it runs
stop_reason in beam_output.text (and then find) over the whole accumulated
text, for every stop string, on every response. With the default
stream_interval=1 that is one full-text search per generated token, so a
streamed request with stop strings spends time quadratic in its output
length. The search runs on the proxy's asyncio thread (the trtllm-serve
event loop when num_postprocess_workers=0, the default) or in a postprocess
worker, in series with the response handling of every other request.
Incremental detokenization only appends to text, and earlier responses
already searched the text before _last_text_len, so the first match must end
in the newly appended part. Starting the search len(stop) - 1 characters
before that point returns the same index as a full search. The full search
would stay where the whole output is decoded again from its tokens
(non-streaming, beam search, a trimmed stop word) and when a post-processor
hook is configured, because the hook can rewrite the last chunk after it was
searched.
CPU benchmark
Intel Xeon Platinum 8352Y, CPython 3.12.3, transformers 5.5.4, tokenizers
0.22.2, one pinned core. The benchmark drives the repository's
DetokenizedGenerationResultBase._handle_response with the Qwen3-0.6B
tokenizer (HF incremental detokenization), one response per generated token,
on English text from docs/source. The stop strings do not occur, so every
response is searched, as happens until a stop string appears. One warm-up
stream, then five runs per row; medians. Stop strings: ["</answer>"] and
["Question:", "</s>", "<|im_end|>"].
| Output tokens (chars) | stop |
Whole stream, main |
With the change | Per response, last 1,000 responses |
|---|---|---|---|---|
| 8,192 (28K) | none | 91.8 ms | 90.4 ms | 11.4 -> 11.4 us |
| 8,192 (28K) | 1 string | 109.4 ms | 100.1 ms | 15.3 -> 12.2 us |
| 8,192 (28K) | 3 strings | 151.5 ms | 105.7 ms | 25.6 -> 13.0 us |
| 16,384 (58K) | none | 202.9 ms | 204.8 ms | 12.9 -> 13.1 us |
| 16,384 (58K) | 1 string | 461.4 ms | 218.8 ms | 53.0 -> 13.9 us |
| 16,384 (58K) | 3 strings | 796.0 ms | 231.4 ms | 98.1 -> 15.0 us |
| 32,768 (125K) | none | 465.2 ms | 451.6 ms | 16.7 -> 16.6 us |
| 32,768 (125K) | 1 string | 1767.0 ms | 475.6 ms | 102.2 -> 17.6 us |
| 32,768 (125K) | 3 strings | 3356.5 ms | 513.8 ms | 214.4 -> 18.6 us |
At 32K output tokens the change saves 1.3 s (one string) to 2.8 s (three
strings) of CPU per streamed request. Near 16K tokens each response
carries an extra 39-83 us; across 64 concurrent streams that is 2.5-5.3 ms of
serial work per decode step on the thread that also serves HTTP. Requests
without stop strings are unaffected.
26,000 randomized streams (both incremental detokenization backends, chunk
sizes 1-6, n > 1, beam search, non-streaming, token-level stop-word trims,
post-processor hooks, empty stop strings, multibyte byte-fallback tokens)
produce identical text deltas, final text, finish reasons and stop reasons on
main and with the change.
Current environment
-
OS: Linux x86_64
-
Python: 3.12.3
-
CPU: Intel Xeon Platinum 8352Y
-
GPU: not used
-
Base: current
mainat70fe2b2a6 -
I searched open and closed issues and pull requests, including recent
stop-string, streaming, detokenization and LLM API result-path work.
AI assistance (Cursor) was used to investigate and prepare this report.
Activity
[NVIDIA#22020][perf] Search only new streamed text for stop strings