Skip to content

perf(umbp): build LocalCopyEngine transfer plans incrementally - #765

Closed
TianDi101 wants to merge 7 commits into
mainfrom
perf/umbp-local-copy-plan-builder
Closed

TianDi101 wants to merge 7 commits into
mainfrom
perf/umbp-local-copy-plan-builder

Conversation

@TianDi101

@TianDi101 TianDi101 commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #758, which gives HbmCopyEngine an incremental TransferPlanBuilder. LocalCopyEngine plans the same per-page-fragment items when a ranged get lands in host memory, but it stays on the default BufferedPlanBuilder. That builder still holds every fragment of the batch, and the composite builder copies each one into it once more, so with #758 alone a host-destination restore gets 4–12% slower instead of faster. This gives LocalCopyEngine the same builder; the same restore is then 74–87% faster than before #758.

Stacked on #758. The first four commits are #758's. The change is 8f6599743; the two commits after it add and then remove a benchmark, so they cancel out. I'll rebase onto main once #758 lands, and it stays a draft until then.

Change

LocalCopyEngine::Plan()'s loop moves into LocalCopyPlanBuilder::Add(), as #758 did for HbmCopyEngine. Plan() is now the builder over a vector, so there is one copy of the merge logic. Grouping by (src, dst) buffer and coalescing are unchanged; the pair map grows with distinct endpoint pairs instead of reserving two buckets per item. +34 / −11 source lines ignoring whitespace, plus 68 lines of tests.

Results

Measured with a standalone microbenchmark (not in this PR). It builds items the way BuildLocalRangeTransfers does, runs them through the composite PoolClient assembles, and stops once the plan set exists; addresses are synthetic and nothing is copied. The shape is #758's: moonshotai/Kimi-K3, TP8, FP8 KV cache, 999,936 cached tokens. That is 24 MLA layers × 576 B per token in 1536-token blocks (651 keys × 24 per-layer ranges), one DRAM buffer, one registered region per layer. Each number is build items + plan, the median of 36 samples on one pinned core, with the three trees interleaved. The plans are identical in every tree.

Host destination (LocalCopyEngine):

Page MemoryDescs before #758 #758 this PR
64 KiB none 70.6 ms 74.0 ms 10.6 ms (−85%)
64 KiB mori-io 77.1 ms 86.6 ms 19.7 ms (−74%)
32 KiB none 148.4 ms 153.8 ms 19.6 ms (−87%)
32 KiB mori-io 159.7 ms 176.7 ms 35.9 ms (−78%)
  • The item vector disappears. Minor page faults go from 71,408 / 151,054 to 0: the 172 / 343 MiB vector of 720-byte TransferItems is never built. The merge itself also drops from 5.7 to 2.8 ms at 64 KiB.
  • Controls, unchanged by this PR: a GPU destination (11.5 → 11.4 ms) and a mixed DRAM+SSD batch (7.5 → 7.6 ms).

Test plan

🤖 Generated with Claude Code

BadrBasowid and others added 6 commits October 13, 2026 07:34
Signed-off-by: BadrBasowid <badr.basowid@gmail.com>
Signed-off-by: BadrBasowid <badr.basowid@gmail.com>
Signed-off-by: BadrBasowid <badr.basowid@gmail.com>
Signed-off-by: BadrBasowid <badr.basowid@gmail.com>
LocalCopyEngine plans a ranged get into host memory the way HbmCopyEngine
plans one into a GPU: one TransferItem per page fragment, grouped by
(src, dst) buffer and coalesced. Behind the default BufferedPlanBuilder it
still held every fragment of the batch, and the composite builder copied
each one into it once more, so host-destination restores got slower with
the incremental builder instead of faster.

Give it the builder HbmCopyEngine has: Plan()'s loop moves into Add(), and
Plan() is the builder over a vector, so there is one copy of the merge
logic. The pair map grows with distinct endpoint pairs instead of reserving
two buckets per item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cremental

bench_umbp_transfer_plan models the local half of PoolClient::BatchGetRanges
restoring a long cached prefix and stops once the plan set exists: items are
built the way BuildLocalRangeTransfers and BuildLocalFileRangeTransfers build
them, and the engine is the composite PoolClient assembles. Defaults are
moonshotai/Kimi-K3 at TP8 with an FP8 KV cache: 24 MLA layers x 576 B per
token, 1536-token blocks, a 999,936-token prefix.

--dst=gpu|cpu picks HbmCopyEngine or LocalCopyEngine, and --file-pct=N serves
N% of keys from SSD-tier files for GdsEngine (a verbatim stand-in where the
hipfile headers are absent); below 100 it is a mixed batch. The same source
builds with and without TransferEngine::CreatePlanBuilder, so a binary per
tree compares the buffered Plan() with the incremental builder. Each
iteration prints a CSV row with an order-insensitive plan-set checksum. Built
but not registered with ctest, like the other bench_* targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep this PR to the LocalCopyEngine builder and its tests. The bench that
produced the numbers in the description stays out of tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TianDi101

Copy link
Copy Markdown
Collaborator Author

migrated into PR: #774

@TianDi101 TianDi101 closed this Oct 15, 2026
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.

2 participants