[None][perf] prepare_inputs: avoid O(seq_len) get_tokens(0) marshalling on the host#16791
[None][perf] prepare_inputs: avoid O(seq_len) get_tokens(0) marshalling on the host#16791hyukn 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>
460954a to
1916433
Compare
|
/bot run --disable-fail-fast |
|
PR_Github #61304 [ run ] triggered by Bot. Commit: |
WalkthroughAdds a bounded ChangesToken Range Retrieval
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
/bot run --disable-fail-fast |
|
PR_Github #61437 [ run ] triggered by Bot. Commit: |
|
PR_Github #61304 [ run ] completed with state |
|
PR_Github #61437 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #61560 [ run ] triggered by Bot. Commit: |
|
PR_Github #61560 [ run ] completed with state
|
|
/bot run --disable-fail-fast |
|
PR_Github #61604 [ run ] triggered by Bot. Commit: |
|
PR_Github #61604 [ run ] completed with state |
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 —
_prepare_inputshost time (DSv4-Pro disagg, c3120, matched A/B, nsys,nvtx_range("_prepare_inputs"))CTX / prefill worker (non-overlap, host-bound; primary target; iters 400–500, N=80):
_prepare_inputsGEN / decode worker (overlap on; only the MTP first-draft path hits
get_tokens(0), so only the large-L tail moves; iters 6000–6050, N=50):_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). On CTX the whole_prepare_inputsshifts down (it sits on the exposed critical path). On GEN the median iter barely moves (normal decode is already O(1) viaget_last_tokens) while the tail collapses (−55% p90) — the large-accumulated-length first-draft iters. GPU-timeline analysis on the same GEN capture shows the reduction is exposed, not hidden: GEN GPU bubble 25.7% → 20.7% and per-iteration cycle −6.3% with GPU compute unchanged (host win on the local critical path); pooled E2E SSE/throughput stay flat because GEN decode is not this workload's binding constraint. The binding was rebuilt and the newget_tokens_rangeverified present on the C++LlmRequest; the full disagg run served at 100% success (functional parity).🤖 Generated with Claude Code