Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3935380
[None][test] Gate the gpt_oss modeling_v2 target under disaggregated …
WeiHaocheng Oct 10, 2026
b4b6f27
[None][doc] Design spec for model-owned disaggregated KV transfer pla…
WeiHaocheng Oct 10, 2026
eb47a8b
[None][doc] Implementation plan for model-owned disaggregated transfe…
WeiHaocheng Oct 10, 2026
9c4d45f
[None][refactor] Add the TransferPlanner seam for disaggregated KV tr…
WeiHaocheng Oct 10, 2026
6ba0a92
[None][refactor] Carry planner_id and planner_blob on RankInfo
WeiHaocheng Oct 10, 2026
9153ea9
[None][refactor] Move transfer planning out of PeerRegistrar into Def…
WeiHaocheng Oct 10, 2026
c8f15d8
[None][refactor] Sender builds KV write metadata from the planner's T…
WeiHaocheng Oct 10, 2026
bec115d
[None][refactor] Receiver dispatches through the planner; transfer.py…
WeiHaocheng Oct 10, 2026
81bdecf
[None][feat] Let a model class supply its disaggregated TransferPlanner
WeiHaocheng Oct 10, 2026
15b9efe
[None][fix] Keep the default planner's statics static, wrap every pee…
WeiHaocheng Oct 10, 2026
265197f
[None][chore] Apply the repo formatters to the transfer-planner change
WeiHaocheng Oct 10, 2026
aad9ca5
[None][doc] Plan for the DeepSeek-R1 dep4 target-owned transfer planner
WeiHaocheng Oct 10, 2026
6f4057a
[None][feat] DeepSeek-R1 dep4 target plans its own disaggregated KV t…
WeiHaocheng Oct 10, 2026
0dab135
[None][test] Gate the DeepSeek-R1 dep4 target under disaggregated ser…
WeiHaocheng Oct 10, 2026
341d260
[None][chore] Log which TransferPlanner a transfer worker runs
WeiHaocheng Oct 10, 2026
159018c
[None][doc] Record the two-node DeepSeek-R1 dep4 planner gate results
WeiHaocheng Oct 10, 2026
d3a22f6
[None][refactor] Bind disaggregated KV planners to the targets they c…
WeiHaocheng Oct 11, 2026
da9b201
[None][doc] Record the two-node re-gate of the planner rebinding
WeiHaocheng Oct 11, 2026
037c586
[None][refactor] Hand TransferPlanner the request descriptions whole
WeiHaocheng Oct 11, 2026
b7958a4
[None][refactor] Identify transfer-planner deployments by architectur…
WeiHaocheng Oct 12, 2026
d2df040
[None][refactor] Let each transfer planner judge its deployments with…
WeiHaocheng Oct 13, 2026
e0e8792
[None][feat] Select the KV transfer planner by name through TRTLLM_KV…
WeiHaocheng Oct 13, 2026
9a47167
[None][doc] Document TRTLLM_KV_TRANSFER_PLANNER and bring the planner…
WeiHaocheng Oct 13, 2026
2ce870a
[None][test] Build the context-only CSA2 transfer test on the planner…
WeiHaocheng Oct 13, 2026
3472a11
[None][feat] Split the transfer planner into a cache describer and a …
WeiHaocheng Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/source/features/disagg-serving.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@ TRT-LLM uses some environment variables to control the behavior of disaggregated

* `TRTLLM_NIXL_KVCACHE_BACKEND`: Selects the transport NIXL itself uses. Valid values are `UCX` (default) and `LIBFABRIC`; an unsupported value logs a warning and falls back to `UCX`. `LIBFABRIC` additionally requires a NIXL build carrying the libfabric plugin — see the [disaggregated serving examples](source:examples/disaggregated/README.md).

* `TRTLLM_KV_TRANSFER_PLANNER`: Names a model-specific KV transfer planner by its `planner_id`, for example `trtllm.deepseek_mla_latent` for DeepSeek-V3 architectures under attention DP with no pipeline or context parallelism. Unset (the default), every model uses the generic transfer plan. The named planner checks at start-up that it supports the model architecture and parallel topology and fails with the reason otherwise. Context and generation servers must set the same value; the transceiver refuses to connect workers whose planners differ. Requires `transceiver_runtime: PYTHON`.

* `TRTLLM_GPU_KEEPALIVE`: If set to `1`, a generation worker that is waiting at the benchmark fill gate (`TLLM_BENCHMARK_REQ_QUEUES_SIZE`) keeps a resident warp on every SM in ~100 ms chunks instead of idling through the wait, so GPU-activity metrics do not read idle while the context tier fills it. The work is drained when the gate opens and never overlaps a forward pass. The default value is `0`.

There are some other useful environment variables that may help when encountering failures or performance issues.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# DeepSeek-R1 dep4 target-owned TransferPlanner — Phase 2 plan

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just for curiosity. Is it on purpose to put the superpowers plans in the PR?


> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. Steps use `- [ ]`.

**Goal:** The first modeling_v2 target that plans its own disaggregated KV transfer: the
`r1_0528_nvfp4__sm_103__dep4` target ships a `TransferPlanner` in its own directory, overrides
`build_transfer_planner`, and a ctx dep4 -> gen dep4 deployment across two 4-GPU nodes runs on it.

**Design (agreed in chat, 2026-10-10):**
- Under ADP dep4 every ctx rank pairs with exactly one gen rank (same dp_rank; flat rank == dp_rank
since pp=cp=1), MLA is one K buffer per layer (`kv_factor=1`), FP8 KV, one PAGED layer group over
one physical pool. The target planner therefore needs no pool matching, no policy, no mapper:
`targets` = `[peer_dp_rank]`, `plan` = ordinal pairing then `base + block*slot_stride`, merging
runs of blocks that are consecutive on both sides into one fragment (the default planner emits
one fragment per block).
- `check_peer` is the target declaring the only topology it supports: peer `dp_size == 4`,
`pp_size == cp_size == 1`, `enable_attention_dp`, one PAGED group with one pool, equal
`slot_bytes`, `tokens_per_block` and layer count. Anything else -> `PeerIncompatibleError`.
- `planner_id = "deepseek_r1_0528_nvfp4_sm103_dep4"`; `receiver_payload_bytes` = 0; `describe` = b"".
- Correctness proof is a CPU parity test against `DefaultTransferPlanner` on a synthesized R1
page table: split the target planner's merged runs back to slot granularity and the fragment
multiset must equal the default planner's.
- GPU gate: two nodes (ctx on A, gen on B), NIXL over UCX tcp/eth0, `TRTLLM_MODELING_V2=require`,
GSM8K against the `deepseek-ai/DeepSeek-R1-0528` NVFP4/FP8 reference (94.24). The in-repo
accuracy test is the standard localhost 8-GPU form for CI; the two-node launcher is a dev script
outside the repo because the harness only knows localhost.

**Spec:** `docs/superpowers/specs/2026-10-10-disagg-transfer-planner-design.md` (Phase 2 section)
and the agreed design above.

## Global Constraints
- NVIDIA header on new files; `git commit -s`; no AI attribution.
- Target isolation (`test_targets_do_not_share_files`): the planner file imports the catalog or
framework packages absolutely, never another target.
- Zero change to `native/` and to the default planner.

## Review Focus
1. Non-owning / empty pairing: a chunk whose ordinals the peer does not hold must yield
`TransferPlan.empty()` (pinned in the parity test with an all-holes dst table).
2. Merged runs must stop at any discontinuity on *either* side (pinned: src consecutive, dst not).
3. `dst_start_token` masking must match the default planner (pinned in parity).
4. A peer with a second pool (e.g. MTP layer in its own pool) must be refused, not silently
half-transferred (pinned in `test_check_peer_refuses_*`).
5. The target planner must never be selected for the gen side when the ctx side runs the default
planner: framework `planner_id` gate (already pinned in Phase 1).

---

### Task 1: planner + parity test
Files: Create `tensorrt_llm/_torch/_experimental/modeling_v2/models/deepseek_v3/r1_0528_nvfp4__sm_103__dep4/transfer.py`;
Modify `.../modeling.py` (override `build_transfer_planner`);
Test: `tests/unittest/_torch/modeling_v2/models/test_modeling_v2_deepseek_transfer_planner.py`.
Steps: write the parity + refusal tests, see them fail on import, implement, see them pass, commit.

### Task 2: in-repo accuracy gate (8 GPUs, localhost form)
Files: `tests/integration/defs/accuracy/test_modeling_v2_deepseek_v3.py` += `test_gsm8k_disagg`
(ctx dep4 + gen dep4, NIXL PYTHON, require mode via extra_env, DuckLLM args patched with
`kv_cache_quant_algo=FP8` so the R1 reference entry matches). Not runnable on 4-GPU nodes; it is
the CI form of what Task 3 runs by hand.

### Task 3: two-node dev launcher + GSM8K
Files (outside repo, `/scratch/.../users/fredw/mv2-disagg/twonode/`): `alloc2.sbatch`
(2 nodes x 4 GPUs, keepalive on both), `serve_ctx.sh`, `serve_gen.sh`, `disagg_server.sh`,
`client_gsm8k.py` (copy of the harness client closure around `run_accuracy_test`).
Verify: both workers log `planner_id=deepseek_r1_0528_nvfp4_sm103_dep4`... (grep "Resolved" +
transceiver logs), GSM8K within tolerance of 94.24, no `PeerIncompatibleError`.

## Results (2026-10-10)

- Task 1: planner + parity test committed (9a9b56e485); 33 tests green including the modeling_v2
claims and target-contract suites.
- Task 2: in-repo 8-GPU gate committed (048bd64c25); collected, not runnable on 4-GPU nodes.
- Task 3: two nodes (ctx dep4 on nvl72d202-T12, gen dep4 on nvl72d202-T16, NIXL over UCX tcp/eth0,
static worker URLs, `TRTLLM_MODELING_V2=require`). Both workers log
`KV transfer planned by DeepseekR1Dep4TransferPlanner`, no default-planner fallback, no
`PeerIncompatibleError`. GSM8K flexible-extract **95.22** against the 94.24 reference, PASSED;
1319 requests in 6.5 min. Launcher findings (loopback-only HTTP discovery, `--server_role`
crash in static mode, `UCX_TLS=^ib` requirement) are recorded in the dev scripts and memory,
not in the repo. Logs: `/scratch/fsw/portfolios/coreai/projects/coreai_comparch_trtllm/users/fredw/mv2-disagg/twonode/logs/run_20261010T182505Z`.

## Redesign of the binding (after review)

The target-overrides-`build_transfer_planner` binding was rejected in review: one planner
must be able to serve several targets, including pairs such as ctx dep4 -> gen dep8 and
targets from different cores. A `COVERS` matrix of target-id pairs, then of
architecture-plus-topology pairs, followed and was dropped in turn: it encoded a topology
nothing consumed and made coverage an enumeration. Implemented instead:

- Planner at `disaggregation/planners/deepseek_mla_latent.py` as
`DeepseekMlaLatentTransferPlanner`, `planner_id = "trtllm.deepseek_mla_latent"`, with
`supports_deployment(architecture, mapping)`: `DeepseekV3ForCausalLM`, attention DP,
pp = cp = 1, any width. `check_peer` keeps the layout checks (one pool, layer count,
`tokens_per_block`, `slot_bytes`) because the arithmetic depends on them; `describe()` is
the base class's empty blob.
- `disaggregation/planners/__init__.py`: `PLANNER_ENV_VAR = "TRTLLM_KV_TRANSFER_PLANNER"`,
`PLANNERS` (planner_id -> class), `bind_transfer_planner`. The user names the planner; the
framework never picks one.
- `AutoModelForCausalLM.from_config` binds after construction for every model; the target's
`modeling.py` has zero transfer-related lines.
- Tests: `tests/unittest/_torch/disaggregation/test_planner_binding.py` (supports_deployment,
PLANNERS keys, binding direction, unset/empty variable, named planner installed, unknown name,
unsupported deployment) and `test_deepseek_mla_latent_planner.py` (parity, merge, targets,
geometry refusal).

## Interface hardening (after review)

Reviewing DeepSeek-V4.1 (PR #19727, merged 2026-10-11) against this branch showed
that `plan()` took a copy of `Chunk`'s field list as positional arguments. main
added `Chunk.excluded_pool_views` (remote-tail / bounded-replay SWA views the
generation side rebuilds locally) and filters on it inside the sender's pool
loop, which this branch moved into `DefaultTransferPlanner.plan`. With the old
signature that field could not reach the planner, and a textual rebase would
have merged cleanly while dropping the filter.

`plan(self_ri, peer_ri, chunk, req_info, prompt_len)` and
`receiver_payload_bytes(self_ri, peer_ri, req_info)` now receive the two request
descriptions whole. `RecvReqInfo` moved from `native/transfer.py` to
`base/backend.py` next to `Chunk`, since it is the wire protocol type the planner
contract depends on. The sender's `KVSendTask` is deliberately not passed: it
carries transfer progress, counters and timers.

Done at the rebase onto main: `DefaultTransferPlanner.plan` skips the
`chunk.excluded_pool_views` main added (the DeepSeek planner returns an empty plan
when its sole pool is excluded), and the `bounded_replay_on_generation` peer check
main added to `PeerRegistrar` lives in `DefaultTransferPlanner.check_peer`; both
sit on the planner, not on `transfer.py`. The disaggregated gates pass
`modeling_v2: require` through each server's config, since main made the switch
an `LLM` argument.

Verified: 292 unit tests green (planner, chunked transfer, ownership regressions,
unsubmitted session, peer, modeling_v2 transfers/claims/routing/target
contract/op bind).
Loading
Loading