[None][perf] prepare_inputs: avoid O(seq_len) get_tokens(0) marshalling on the host#16791
Open
hyukn wants to merge 1 commit into
Open
[None][perf] prepare_inputs: avoid O(seq_len) get_tokens(0) marshalling on the host#16791hyukn wants to merge 1 commit into
hyukn wants to merge 1 commit into
Conversation
…ng on the host `request.get_tokens(0)` marshals the whole C++ VecTokens (entire sequence) into a fresh Python list of boxed ints -- O(seq_len) host work that grows linearly with ISL. Three call sites in `_prepare_tp_inputs` / `cuda_graph_runner` pay this only to read a length or a small slice, and re-pay it every iteration. In chunked prefill the context loop re-marshals the full prompt for every chunk -> O(L^2/chunk) per request over the prefill. Normal decode is already immune (get_last_tokens(0), O(1)); the waste is in the prefill/context and MTP first-draft paths. Changes: - New nanobind binding `LlmRequest.get_tokens_range(beam, begin, end)` that copies only [begin, end) (O(chunk)) instead of the whole VecTokens. - context loop and first_draft loop: use get_tokens_range for the chunk and get_num_tokens(0) (O(1)) for lengths, instead of get_tokens(0) + slice. - cuda_graph_runner first-draft branch: len(get_tokens(0)) -> get_num_tokens(0). Output is bit-identical (get_tokens_range returns the same subrange). Measured on a DSv4-Pro disagg CTX worker (c3120, non-overlap, CTX nsys): _prepare_inputs p50 27.53 ms -> 15.10 ms (-45%), p90 43.29 -> 20.43 ms; 100% request success. Complements NVIDIA#16734 (which removes the overlap device-scalar sync, 268 -> 19.96 ms); together they target the two independent costs in _prepare_inputs. Signed-off-by: Yukun He <23156053+hyukn@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hyukn
force-pushed
the
yukunh/prepare-inputs-gettokens-oL
branch
from
July 23, 2026 14:16
460954a to
1916433
Compare
hyukn
marked this pull request as ready for review
July 23, 2026 14:16
Collaborator
Author
|
/bot run --disable-fail-fast |
Collaborator
|
PR_Github #61304 [ run ] triggered by Bot. Commit: |
Contributor
WalkthroughAdds a bounded ChangesToken Range Retrieval
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
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.
Summary
request.get_tokens(0)marshals the whole C++VecTokens(the entire sequence) into a fresh Python list of boxed ints — O(seq_len) host work that grows linearly with ISL. Three call sites in_prepare_tp_inputs/cuda_graph_runnerpay this only to read a length or a small slice, and re-pay it every iteration. In chunked prefill the context loop re-marshals the full prompt for every chunk → O(L²/chunk) per request over the prefill. Normal decode is already immune (it usesget_last_tokens(0), O(1)); the waste is in the prefill/context path and the MTP first-draft paths.Changes
LlmRequest.get_tokens_range(beam, begin, end)— returns only the[begin, end)window, so nanobind copies just(end-begin)tokens (O(chunk)) instead of the wholeVecTokens. Bounds are clamped._prepare_tp_inputs): useget_tokens_range(0, begin, end)for the current chunk andget_num_tokens(0)(O(1)) for the prompt length, instead ofget_tokens(0)+ slice +len(...).get_num_tokens(0)for the length,get_tokens_rangefor the lastoriginal_max_draft_len+1tokens.len(get_tokens(0))→get_num_tokens(0).Output is bit-identical —
get_tokens_rangereturns exactly the same subrange the oldget_tokens(0)[begin:end]produced.Perf (DSv4-Pro disagg CTX worker, c3120, non-overlap, matched A/B; CTX nsys, iters 400–500, steady N=80)
_prepare_inputsEach site drops from O(L) to O(1)/O(chunk); a synthetic microbench (bs128) shows each site flat across ISL after the fix (baseline get_tokens ~18 ms/iter at ISL 50K → ~0.05 ms). The binding was rebuilt and the new
get_tokens_rangeverified present on the C++LlmRequest; the full disagg run served at 100% success (functional parity).Relationship to #16734
Complementary — different files, different cost. #16734 removes the overlap-scheduler device-scalar
cudaStreamSynchronizein the DSV4 ctx sparse metadata (_prepare_inputs268 ms → 19.96 ms); this PR removes the O(seq_len)get_tokens(0)host marshalling that remains. Together they target the two independent costs in_prepare_inputs.🤖 Generated with Claude Code