Skip to content

[Perf] Host-path costs: prefix-key caching, O(1) free-block count, per-run KV bookkeeping, one argmax per batch - #151

Closed
0z5a wants to merge 1 commit into
InferMatrixLab:mainfrom
0z5a:perf/host-path-admission
Closed

0z5a wants to merge 1 commit into
InferMatrixLab:mainfrom
0z5a:perf/host-path-admission

Conversation

@0z5a

@0z5a 0z5a commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Four host-side fixes found by profiling multi-turn session workloads (cProfile inside the benchmark; in one cell the GPU was 13% busy while the Python process was at 100%):

  • Scheduler._plan_admission rehashed every waiting prompt (one chained SHA-256 per block) on every admission attempt, and num_free_blocks rescanned every prefix entry on each capacity check. Prompt keys are now computed once per request, and the manager keeps the count of cache-only blocks (refs == 1) current at every reference-count change, so num_free_blocks is O(1).
  • Growth reservations are computed once per admission tick instead of once per candidate.
  • Rows of a host KV batch are grouped once into runs of consecutive positions per (allocation, depth); per-layer write/attend bookkeeping advances one written record per run (add_run, identical to sequential add) and checks the strongest prefix requirement per run. A 2048-token prefill chunk costs one call per layer instead of 2048. The per-layer written contract is unchanged. In the session profile this bookkeeping was 31M calls, about 20 s of 146 s.
  • An all-greedy CODA batch samples with one argmax launch (Sampler.sample_batch) instead of one per row.

Measurements

RTX 5090, Ouro-1.4B, 32 agent sessions, 6 turns, prefix cache on, one run per arm (prefix-key and free-block changes vs main):

tok/s TTFT p50 Python calls
per-row prefill +2.6% 8.4 → 7.1 s −19%
packed prefill +4.7% 2.57 → 1.80 s —

The other two changes were not measured on their own.

Platforms. GH200: torch 2.14.0+cu130, Triton 3.8.0, NIXL 1.4.1. RTX 5090: torch 2.13.0+cu130, Triton 3.7.1. Models: Ouro-1.4B (24 layers, 4 loops, fixed depth) and Huginn-0125 (2 prelude + 4 recurrent + 2 coda layers, 32 loops). Burst workloads issue C requests at once with random-token prompts; session workloads replay multi-turn agent traces with the prefix cache on.

Tests

  • CPU suite: 831 passed; ruff passes. Randomized tests check the free-block counter against the old scan after each allocate/free/grow/publish, and add_run against sequential add; a sampler test covers the batched argmax.

Notes

The GPU numbers come from a development branch that contains all changes of this series; most cells are single runs, and on the shared GH200 host two runs of main at 32 requests differed by up to 16%, so smaller differences are ties. This branch alone was checked with the CPU suite and ruff.

Series. Draft PRs from one study of serving looped models. They are independent except #152, which is stacked on #147; several touch the same files, so later ones may need a rebase after earlier ones merge.

…r-run KV bookkeeping, one argmax per batch

Four host-side fixes found by profiling multi-turn session workloads (cProfile
inside the benchmark; the GPU was 13% busy and Python 100% in one cell):

- Scheduler._plan_admission rehashed every waiting prompt (one chained SHA-256
  per block) on every admission attempt, and num_free_blocks rescanned every
  prefix entry on each capacity check. Prompt keys are now computed once per
  request, and the manager keeps the count of cache-only blocks (refs == 1)
  current at every reference-count change, so num_free_blocks is O(1). A
  randomized test checks the counter against the old scan.
- Growth reservations are computed once per admission tick instead of once per
  admission candidate.
- Rows of a host KV batch are grouped once into runs of consecutive positions
  per (allocation, depth); per-layer write/attend bookkeeping advances one
  written record per run (add_run, identical to sequential add) and checks
  the strongest prefix requirement per run. A 2048-token prefill chunk costs
  one call per layer instead of 2048. The per-layer written contract is
  unchanged; a randomized test checks add_run against add. In the session
  profile this bookkeeping was 31M calls, about 20 s of 146 s.
- An all-greedy CODA batch samples with one argmax launch
  (Sampler.sample_batch) instead of one per row.

RTX 5090, Ouro-1.4B, 32 agent sessions, 6 turns, prefix cache on, one run per
arm (prefix-key and free-block changes vs main):
  per-row prefill: +2.6% tok/s, TTFT p50 8.4 -> 7.1 s, Python calls -19%
  packed prefill:  +4.7% tok/s, TTFT p50 2.57 -> 1.80 s
The other two changes were not measured on their own.

Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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