Skip to content

[None][perf] Add boundary-aware incremental router tokenization - #17462

Draft
lishicheng1996-nv wants to merge 5 commits into
NVIDIA:mainfrom
lishicheng1996:perf/incremental-tokenize-boundary-rollback
Draft

[None][perf] Add boundary-aware incremental router tokenization#17462
lishicheng1996-nv wants to merge 5 commits into
NVIDIA:mainfrom
lishicheng1996:perf/incremental-tokenize-boundary-rollback

Conversation

@lishicheng1996-nv

@lishicheng1996-nv lishicheng1996-nv commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

What

  • Add opt-in, boundary-aware incremental tokenization for KV-cache-aware routing.
  • Find the rendered-text longest common prefix, map it to cached tokenizer offsets, roll back one token, and retokenize only the changed suffix.
  • Fall back to canonical full tokenization when offsets are unavailable or inconsistent.
  • Support configurable rollback and periodic full-tokenization verification.

Real AgentX case

Qwen3.5's chat template does not preserve the previous rendered prompt as a literal prefix across AgentX turns. Before generation, a prompt ends with a synthetic generation prefix:

...<|im_start|>assistant
<think>

When the next turn is rendered, the actual assistant response replaces that final <think>\n, followed by the next user turn and a fresh generation prefix:

...<|im_start|>assistant
<actual assistant response><|im_end|>
<|im_start|>user
<next user message><|im_end|>
<|im_start|>assistant
<think>

The character LCP therefore stops immediately before the old <think>\n. In 12 inspected same-key, non-literal-prefix transitions, the complete unmatched old tail was exactly the 8-character <think>\n; across all 1,087 consecutive transitions, the median changed old tail was also 8 characters.

This defeats the exact-prefix method from #15040. In the AgentX v41 trace:

Why rollback is required

Starting suffix tokenization exactly at the character LCP is not generally composable: a BPE token can merge characters from both sides of that boundary. The implementation maps the LCP to the cached offset boundary and rolls back one complete token before retokenizing.

On the full AgentX transition set, cutting without rollback matched canonical tokenization for 1,085/1,087 transitions. One-token rollback fixed both mismatches and matched 1,087/1,087.

Validation

Qwen3.5 AgentX v41 trace on an 18-CPU frontend allocation:

Method Exactness Mean time Speedup vs full
Full rendered-template + tokenization reference 185.107 ms 1.00x
#15040 exact-prefix cache 1,087/1,087 188.332 ms 0.98x
Boundary-aware, one-token rollback 1,087/1,087 11.300 ms 16.38x

Additional validation:

  • Sequential cache simulation: 1,174/1,174 exact, including 1,030 incremental requests and 144 initial/identical/full requests; zero mismatches.
  • Mean reused prefix: 88,935 tokens; mean retokenized suffix: 10,908 characters.
  • Regression coverage includes the exact AgentX <think>\n rewrite, a cross-boundary merge, first-token offsets starting after character zero, unavailable/invalid offsets, and periodic verification fallback.
  • tests/integration/test_lists/test-db/l0_cpu.yml already runs the entire unittest/disaggregated directory.

Configuration

Enable with TRTLLM_INCREMENTAL_TOKENIZE=1.

Optional safeguards:

  • TRTLLM_INCREMENTAL_TOKENIZE_ROLLBACK_TOKENS controls the number of cached tokens rolled back; the minimum is one.
  • TRTLLM_INCREMENTAL_TOKENIZE_VERIFY_EVERY periodically compares incremental output with canonical full tokenization and falls back on mismatch.

Dev Engineer Review

  • Adds opt-in incremental tokenization for router and OpenAI chat prompts.
  • Adds shared OffsetTokenizer and IncrementalTokenizationCache APIs.
  • Reuses stable rendered prefixes and tokenizes only changed suffixes.
  • Supports rollback, bounded LRU storage, periodic verification, and canonical fallback.
  • Covers chat-template boundary rewrites.
  • No configuration or test-list files changed.
  • Review should confirm environment validation, logging, error handling, and API consistency with CODING_GUIDELINES.md.

QA Engineer Review

  • Modified tests/unittest/disaggregated/test_router.py.
  • Added coverage for boundary rollback, zero-token cuts, suffix-only tokenization, unavailable or inconsistent offsets, periodic verification fallback, and canonical equivalence.
  • Added CTX frontend coverage for shared incremental-tokenizer behavior.
  • No corresponding test-db/ or qa/ coverage was identified.
  • Verdict: needs follow-up.

Signed-off-by: Shicheng Li <shicli@nvidia.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 89f8891c-b0ab-4b77-bce0-e1fb6990205a

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc143d and b2b49b3.

📒 Files selected for processing (2)
  • tensorrt_llm/serve/chat_tokenization.py
  • tensorrt_llm/serve/openai_server.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tensorrt_llm/serve/openai_server.py
  • tensorrt_llm/serve/chat_tokenization.py

Walkthrough

IncrementalTokenizationCache provides configurable, offset-aware prefix reuse for chat prompts and router encoding. OpenAI serving and BlockHashMixin use the shared cache. Tests cover rollback, fallback, canonical equivalence, and verification.

Changes

Incremental tokenization

Layer / File(s) Summary
Tokenization cache and offset contract
tensorrt_llm/serve/chat_tokenization.py
Adds OffsetTokenizer and IncrementalTokenizationCache. The cache supports environment settings, bounded storage, rollback, offset validation, fallback, and periodic verification.
Chat prompt integration
tensorrt_llm/serve/openai_server.py
Initializes the cache, resolves conversation IDs, and applies incremental tokenization to eligible text prompts through the input-processing executor.
Router prefix-cache integration
tensorrt_llm/serve/router_utils.py
Replaces the local prefix cache with the shared incremental tokenization cache.
Boundary and fallback regression coverage
tests/unittest/disaggregated/test_router.py
Tests suffix-only tokenization, boundary rollback, zero-token cuts, invalid offsets, canonical fallback, and periodic verification.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChatRequest
  participant OpenAIServer
  participant InputProcessingExecutor
  participant IncrementalTokenizationCache
  participant OffsetTokenizer
  ChatRequest->>OpenAIServer: provide conversation and messages
  OpenAIServer->>OpenAIServer: resolve conversation ID and render prompt
  OpenAIServer->>InputProcessingExecutor: submit eligible text prompt
  InputProcessingExecutor->>IncrementalTokenizationCache: encode rendered prompt
  IncrementalTokenizationCache->>OffsetTokenizer: encode changed suffix with offsets
  OffsetTokenizer-->>IncrementalTokenizationCache: return token IDs and offsets
  IncrementalTokenizationCache-->>OpenAIServer: return prompt token IDs
Loading

Suggested reviewers: pcastongay, tabrizian

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the boundary-aware incremental router tokenization change.
Description check ✅ Passed The description explains the change, motivation, configuration, validation results, and regression coverage, but omits the template checklist.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tensorrt_llm/serve/router_utils.py`:
- Around line 320-322: Define a structural tokenizer Protocol near
_encode_with_offsets with encode() and offset-aware __call__() signatures
matching the operations used by the function, then annotate the tokenizer
parameter with that Protocol. Preserve the existing return type and encoding
behavior.
- Around line 225-228: Update the logger calls in
tensorrt_llm/serve/router_utils.py at lines 225-228 and 305-309 to preformat
their messages as single f-strings, embedding
self._incremental_tokenize_rollback in the first and key in the second; do not
pass printf-style format arguments separately.
- Around line 279-281: Update the cut-point calculation in the incremental
tokenization flow so `cut_char` is explicitly set to zero when `cut_token == 0`;
otherwise continue using `previous_offsets[cut_token][0]`. Ensure suffix
encoding includes the entire rendered string when no cached token is reusable.

In `@tests/unittest/disaggregated/test_router.py`:
- Around line 2387-2403: Expand coverage around
KvCacheAwareRouter._encode_with_prefix_cache beyond
test_prefix_cache_rolls_back_boundary_token: add cases for unavailable offsets,
invalid offsets, periodic verification, and an initial offset greater than zero,
asserting correct token results and tokenizer calls. Add
unittest/disaggregated/test_router.py to the appropriate CI test list under
tests/integration/test_lists/test-db/ or qa/, and run pytest tests/unittest/.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: bc3aed6d-5ce2-4ba5-886c-940782266563

📥 Commits

Reviewing files that changed from the base of the PR and between d0b5862 and fd137d9.

📒 Files selected for processing (2)
  • tensorrt_llm/serve/router_utils.py
  • tests/unittest/disaggregated/test_router.py

Comment thread tensorrt_llm/serve/router_utils.py
Comment thread tensorrt_llm/serve/router_utils.py Outdated
Comment thread tensorrt_llm/serve/router_utils.py Outdated
Comment thread tests/unittest/disaggregated/test_router.py Outdated
Signed-off-by: Shicheng Li <shicli@nvidia.com>
Signed-off-by: Shicheng Li <shicli@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unittest/disaggregated/test_router.py (1)

2456-2482: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cover a non-default rollback value.

Line 2463 enables incremental tokenization but does not set TRTLLM_INCREMENTAL_TOKENIZE_ROLLBACK. This test validates only the default one-token rollback. If configuration parsing or application fails, the current suite can pass.

Add a case with a non-default rollback such as 2. Assert canonical token IDs and the earlier suffix passed to the tokenizer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unittest/disaggregated/test_router.py` around lines 2456 - 2482, Extend
test_prefix_cache_rolls_back_boundary_token to configure a non-default
TRTLLM_INCREMENTAL_TOKENIZE_ROLLBACK value such as 2, then assert
tokenizer.encode(current) remains the canonical result and tokenizer.calls
contains the suffix beginning at the expected two-token rollback boundary. Keep
the existing prefix-cache setup and verify the earlier suffix passed to
_encode_with_prefix_cache, ensuring the configured rollback is actually applied
rather than only relying on the default.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/unittest/disaggregated/test_router.py`:
- Around line 2456-2482: Extend test_prefix_cache_rolls_back_boundary_token to
configure a non-default TRTLLM_INCREMENTAL_TOKENIZE_ROLLBACK value such as 2,
then assert tokenizer.encode(current) remains the canonical result and
tokenizer.calls contains the suffix beginning at the expected two-token rollback
boundary. Keep the existing prefix-cache setup and verify the earlier suffix
passed to _encode_with_prefix_cache, ensuring the configured rollback is
actually applied rather than only relying on the default.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: da4f1e47-10fa-4117-bd36-6a1b46706f86

📥 Commits

Reviewing files that changed from the base of the PR and between 06353c5 and 7250d59.

📒 Files selected for processing (2)
  • tensorrt_llm/serve/router_utils.py
  • tests/unittest/disaggregated/test_router.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tensorrt_llm/serve/router_utils.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tensorrt_llm/serve/chat_tokenization.py (1)

72-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use Python 3.10 annotation syntax in new code.

  • tensorrt_llm/serve/chat_tokenization.py#L72-L75: replace Optional[list[tuple[int, int]]] with list[tuple[int, int]] | None.
  • tensorrt_llm/serve/chat_tokenization.py#L177-L177: replace Optional[...] in the return type with ... | None.
  • tensorrt_llm/serve/openai_server.py#L1464-L1467: replace Union[str, List[int]] with str | list[int].

As per coding guidelines, use Python 3.10+ and prefer built-in generics and |. Retrieved learnings confirm this repository supports Python 3.10+ syntax.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tensorrt_llm/serve/chat_tokenization.py` around lines 72 - 75, Update the
type annotations in tensorrt_llm/serve/chat_tokenization.py lines 72-75 and 177
to use Python 3.10 union syntax, replacing Optional[...] with the equivalent |
None form; update tensorrt_llm/serve/openai_server.py lines 1464-1467 to replace
Union[str, List[int]] with str | list[int].

Sources: Coding guidelines, Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tensorrt_llm/serve/chat_tokenization.py`:
- Around line 146-151: Preformat all three affected logger messages as single
f-strings because tensorrt_llm.logger does not interpolate printf-style
arguments: update the mismatch log in tensorrt_llm/serve/chat_tokenization.py
lines 146-151, the hit-count log in tensorrt_llm/serve/chat_tokenization.py
lines 166-171, and the server log in tensorrt_llm/serve/openai_server.py lines
338-340. In the hit-count condition, require a nonzero hit count before applying
the periodic logging check so zero-hit requests do not flood the logs.
- Around line 106-114: Replace the binary-search prefix computation in the
surrounding tokenization flow with a single-pass comparison that advances
through matching characters in previous_text and rendered, avoiding repeated
growing slices. Preserve the resulting common_prefix_chars value and existing
suffix tokenization behavior.

---

Nitpick comments:
In `@tensorrt_llm/serve/chat_tokenization.py`:
- Around line 72-75: Update the type annotations in
tensorrt_llm/serve/chat_tokenization.py lines 72-75 and 177 to use Python 3.10
union syntax, replacing Optional[...] with the equivalent | None form; update
tensorrt_llm/serve/openai_server.py lines 1464-1467 to replace Union[str,
List[int]] with str | list[int].
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f078bb57-a1b2-49ef-908e-2773833adeba

📥 Commits

Reviewing files that changed from the base of the PR and between 7250d59 and 3cc143d.

📒 Files selected for processing (4)
  • tensorrt_llm/serve/chat_tokenization.py
  • tensorrt_llm/serve/openai_server.py
  • tensorrt_llm/serve/router_utils.py
  • tests/unittest/disaggregated/test_router.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tensorrt_llm/serve/router_utils.py
  • tests/unittest/disaggregated/test_router.py

Comment on lines +106 to +114
prefix_limit = min(len(previous_text), len(rendered))
low, high = 0, prefix_limit
while low < high:
middle = (low + high + 1) // 2
if previous_text[:middle] == rendered[:middle]:
low = middle
else:
high = middle - 1
common_prefix_chars = low

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Compute the common prefix in one pass.

Lines 106-114 allocate and compare increasingly large string slices. A long shared prompt performs Θ(n log n) copied-character work before suffix tokenization. This can reduce the cache speedup for long conversations.

Proposed fix
-            prefix_limit = min(len(previous_text), len(rendered))
-            low, high = 0, prefix_limit
-            while low < high:
-                middle = (low + high + 1) // 2
-                if previous_text[:middle] == rendered[:middle]:
-                    low = middle
-                else:
-                    high = middle - 1
-            common_prefix_chars = low
+            common_prefix_chars = 0
+            for previous_char, rendered_char in zip(previous_text, rendered):
+                if previous_char != rendered_char:
+                    break
+                common_prefix_chars += 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
prefix_limit = min(len(previous_text), len(rendered))
low, high = 0, prefix_limit
while low < high:
middle = (low + high + 1) // 2
if previous_text[:middle] == rendered[:middle]:
low = middle
else:
high = middle - 1
common_prefix_chars = low
common_prefix_chars = 0
for previous_char, rendered_char in zip(previous_text, rendered):
if previous_char != rendered_char:
break
common_prefix_chars += 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tensorrt_llm/serve/chat_tokenization.py` around lines 106 - 114, Replace the
binary-search prefix computation in the surrounding tokenization flow with a
single-pass comparison that advances through matching characters in
previous_text and rendered, avoiding repeated growing slices. Preserve the
resulting common_prefix_chars value and existing suffix tokenization behavior.

Comment thread tensorrt_llm/serve/chat_tokenization.py
@lishicheng1996-nv
lishicheng1996-nv marked this pull request as draft August 10, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant