feat: support heterogeneous qwen3.5 PD disaggregation. - #1995
Conversation
|
RFC: #2060\n\nThis RFC documents the architecture, supported scope, cache resharding strategy, synchronization guarantees, and validation plan for this implementation. |
| const std::vector<uint64_t>& dst_blocks, | ||
| const std::vector<uint64_t>& dst_linear_state_ids, | ||
| int64_t source_shard_count) { | ||
| if (source_shard_count != 2 || |
There was a problem hiding this comment.
已修改。2指的是当前只支持 Prefill TP2 到 Decode TP1。已经换成 kSupportedHeterogeneousSourceShardCount,并在 staging 注册和 pull/merge 校验里统一使用。
There was a problem hiding this comment.
仅支持 prefill TP 2 到 decode TP 1,只有这种情况嘛?其他 TP 组合是否支持。例如 qwen3.5 35b a3b 要用的话,是否可以其他 TP 组合方式
| LayerRegisteredCaches& staging_registered_caches, | ||
| int64_t source_shard_count, | ||
| bool source_is_sharded) { | ||
| CHECK_EQ(source_shard_count, 2) |
| const std::vector<uint64_t>& dst_blocks, | ||
| const std::vector<uint64_t>& src_linear_state_ids, | ||
| const std::vector<uint64_t>& dst_linear_state_ids) { | ||
| if (src_cluster_ids.size() != 2 || |
| return true; | ||
| } | ||
|
|
||
| bool LlmDataDistTransfer::merge_pre_pushed_sharded_caches( |
There was a problem hiding this comment.
为何 pull_and_merge_sharded_caches 内部持有锁 merge_pre_pushed_sharded_caches 无锁?
There was a problem hiding this comment.
已修改。原来锁放在 pull_and_merge_sharded_caches 里面,走 PUSH 路径时 merge_pre_pushed_sharded_caches 没有被同一把锁保护。我现在把锁提到了 pull_hetero_kv_blocks 外层,一次锁住 linear-state pull/merge、预推送 target KV merge 和 draft KV pull/merge,避免并发请求复用 staging rows 时互相覆盖。
| if (source_cache.role == KVCacheTensorRole::KEY || | ||
| source_cache.role == KVCacheTensorRole::VALUE) { | ||
| // max_tokens_per_batch=32768 with block_size=128. | ||
| shape[0] = std::min<int64_t>(shape[0], 256) * source_shard_count; |
There was a problem hiding this comment.
已修改。原来的 256 是按 max_tokens_per_batch=32768、block_size=128 直接写死的,现在会根据 KVCacheConfig::block_size()、SchedulerConfig::max_tokens_per_batch() 和 max_seqs_per_batch() 算每个 shard 需要多少 staging 行,同时给每条序列可能存在的尾部未满 block 留出空间。
| // max_tokens_per_batch=32768 with block_size=128. | ||
| shape[0] = std::min<int64_t>(shape[0], 256) * source_shard_count; | ||
| } else if (source_cache.role == KVCacheTensorRole::SSM) { | ||
| shape[0] = std::min<int64_t>(shape[0], 4) * source_shard_count; |
There was a problem hiding this comment.
已修改。原来的 4 是当前 Qwen3.5 GDN linear-state 的 checkpoint stride。现在直接用同层 SSM cache 和 CONV cache 的行数比例算出 checkpoint_stride,再用它计算 staging 容量和展开 SSM id。
| // The staging tensors are registered once and reused by all requests. | ||
| // Serialize request-level restore/merge so concurrent FirstGeneration RPCs | ||
| // cannot overwrite each other's staging rows. | ||
| std::lock_guard<std::mutex> hetero_pull_lock(hetero_pull_mutex_); |
There was a problem hiding this comment.
pull_hetero_kv_blocks 全局持 hetero_pull_mutex_,是否会导致任意两个并发 Decode 请求都串行(即使它们的 request_id、layer、shard 完全不重叠)
There was a problem hiding this comment.
会的。目前同一个 Decode worker 上,异构 cache 的“恢复/接管”阶段还是串行的,但请求进入 Decode queue 后的生成不会被这把锁串行。原因是 staging tensor 是 worker 级共享的,不同请求会复用同一组 staging rows;如果同时 pull/merge,数据可能互相覆盖。
| // Heterogeneous TP uses decode-side PULL+merge even when the configured | ||
| // transport mode is PUSH, so source topology and cache ids are always | ||
| // included in the first-generation metadata. | ||
| ADD_VECTOR_TO_PROTO(gen->mutable_cluster_ids(), |
There was a problem hiding this comment.
原来只在pull做的事情,为何改成push也会做了
There was a problem hiding this comment.
这里没有把普通 PUSH 改成 PULL,同构 PUSH 的行为也没变。异构 TP 下走的是混合路径:Prefill 逐层把 target KEY/VALUE PUSH 到 Decode staging;Qwen3.5 的 CONV/SSM linear state 和 Draft TP1 KV 仍由 Decode 在接管时同步 PULL。所以即使 transfer mode 是 PUSH,Decode 还是要拿到源 cluster/addrs、block_ids 和 linear_state_ids,才能把剩下的状态恢复完整。
当前代码仍保留原来的 transfer mode,这些额外元数据只在异构恢复路径使用,注释里也说明了这个限制。如果不带这些信息,target KV 虽然到了,Decode 还是没法恢复 linear state 和 draft KV。
|
冲突了,有空可以再rebase下 |
|
|
||
| const int64_t shard_count = static_cast<int64_t>(src_cluster_ids.size()); | ||
| Timer breakdown_total_timer; | ||
| const char* parallel_pull_env = std::getenv("XLLM_PD_PARALLEL_SHARD_PULL"); |
There was a problem hiding this comment.
仿照 disagg_pd_config 其他参数是不是更好?
There was a problem hiding this comment.
已修改。运行路径里不再直接读 XLLM_PD_PARALLEL_SHARD_PULL,改成正式的 disagg_pd_config 参数 enable_pd_parallel_shard_pull,默认开启。flag、JSON 配置解析和导出、支持参数列表、配置单测已补充。
| torch::tensor(signed_final_ids, | ||
| torch::TensorOptions().dtype(torch::kLong)) | ||
| .to(registered_cache.tensor.device(), /*non_blocking=*/false); | ||
| registered_cache.tensor.index_copy_(0, final_indices, merged); |
There was a problem hiding this comment.
大量重复代码在 pull_and_merge_sharded_caches / merge_pre_pushed_sharded_caches / push_layer_registered_caches_to_staging 之间
There was a problem hiding this comment.
已重构。shape 判断、ID 生成、staging shard view、shard merge、回写和 2 MiB 对齐等公共逻辑都提成了 helper,三个入口里只保留各自的流程。
| const std::vector<uint64_t>& src_linear_state_ids = {}, | ||
| const std::vector<uint64_t>& dst_linear_state_ids = {}) override; | ||
|
|
||
| bool pull_hetero_kv_blocks( |
There was a problem hiding this comment.
为何要新增 pull_hetero_kv_blocks ? 为何不能拓展 pull_kv_blocks
There was a problem hiding this comment.
现有 pull_kv_blocks 对应同构场景,语义是一对一 TP rank 映射和直接搬运;异构恢复需要接收多个源 TP shard,先写 staging,再按 tensor role 和分片维度重排合并,还要区分 Qwen3.5 linear state 和 Draft TP1 KV。
| CHECK_GT(local_v_width, 0); | ||
| const int64_t local_conv_width = stage_cache.tensor.size(shard_dim); | ||
| CHECK_EQ((local_conv_width - local_v_width) % 2, 0); | ||
| const int64_t local_qk_width = (local_conv_width - local_v_width) / 2; |
There was a problem hiding this comment.
llm_data_dist_transfer.cpp, transport 层只负责搬数据
但是现在 transport 现在要懂 qwen3.5 结构和细节,是否不太合理?
There was a problem hiding this comment.
已修改。tensor role、CONV Q/K/V 布局、SSM checkpoint stride、异构 staging 和 merge 这些模型/Speculative 相关逻辑,现在都放在 SpecKVCacheTransfer 里。LlmDataDistTransfer 只保留通用的 DataDist 注册、PUSH/PULL 搬运和 layer synchronization,不再理解 Qwen3.5 的 cache 结构。
8c586f5 to
bbed49b
Compare
bbed49b to
ae5f532
Compare
|
已rebase到最新 |
- transfer target, draft, convolution, and SSM state across heterogeneous TP layouts\n- propagate per-rank cache metadata and synchronize staged prefill pushes\n- parallelize Decode shard pulls with a safe serial fallback and request-level staging protection\n- add topology and push-route coverage plus request-level performance instrumentation
- serialize the complete heterogeneous cache restore transaction\n- keep model-specific resharding in SpecKVCacheTransfer\n- derive staging layout from runtime config and expose parallel pull via DisaggPDConfig
ae5f532 to
5279545
Compare

中文说明
概述
本 PR 为昇腾 NPU 上的 Qwen3.5 推测解码增加异构 Tensor Parallel 的 PD 分离能力,当前完整支持:
当 Prefill 与 Decode 的 TP 布局不一致时,Decode 无法直接复用原有一对一缓存传输。本 PR 引入多对一 TP 路由、异构 staging cache、按缓存角色合并以及完整的 Worker RPC 链路,使 Decode 能接管 Prefill 产生的 Target KV、CONV State、SSM State 和 MTP Draft KV,并从首 token 继续生成,无需重跑 Prefill。
Important
核心性能收益(16K+ 长输入,单轮 Discovery)
整体框架
方案采用控制面与数据面分离,并对不同缓存使用混合 PUSH/PULL:
Target KV 数据量大,Prefill 每完成一层便将该层 KEY/VALUE shard PUSH 到 Decode staging,使传输与后续计算重叠。CONV/SSM 是 Prefill 结束后的最终递推状态,Draft KV 数据量较小;两者由 Decode 同步 PULL,使 shard 到达、tensor merge、设备流同步和请求入队形成明确的完成顺序。
请求时序
多对一 TP 路由
一个 Decode TP rank 对应的 Prefill TP ranks 满足:
等价于
dst_tp_rank = src_tp_rank % dst_tp_size。TP2→TP1 时,D-rank0 同时接收 P-rank0 和 P-rank1 的 shard。link_cluster()、unlink_cluster()和实际恢复共用同一 owner 映射。缓存传输与合并
dim 2dim 1,支持 checkpoint ID 展开dim 2;Draft body TP1 时只拉一个完整副本CONV 每个 Prefill rank 的布局是
[Q,K,V]。实现将[Q0,K0,V0] + [Q1,K1,V1]重排为 Decode TP1 所需的[Q0,Q1,K0,K1,V0,V1]。正确性与并发安全
hetero_pull_mutex_防止并发 FirstGeneration 覆盖共享 staging。synchronize_default_stream()。enable_pd_parallel_shard_pull默认开启,并行拉取两个源 TP shard;需要排查问题或回退到串行路径时,可使用--enable_pd_parallel_shard_pull=false。主要代码改动
get_src_tp_ranks(),修改 Engine 建链、拆链和异构恢复路由。pull_hetero_kv_blocks调用链。register_hetero_staging_caches()创建并注册 Target/Draft staging。push_layer_registered_caches_to_staging()按层 PUSH Target KV。pull_and_merge_sharded_caches()并行 PULL 并合并 CONV/SSM 或 Draft KV。merge_pre_pushed_sharded_caches()合并已 PUSH 的 Target KV。SpecKVCacheTransfer::pull_hetero_kv_blocks()编排 CONV/SSM、Target KV 和 Draft KV 的恢复顺序。[PD-PERF]日志。本 PR 修改 33 个文件,代码量为
+1538/-93。使能方式
共同参数:
角色参数:
两侧必须使用相同模型、Draft Model、Block Size、MTP 深度和 etcd 地址,并配置兼容的 DataDist 端口。
16K+ 长输入性能验证(Discovery)
为验证 PD 分离在长 Prompt、高并发场景下的收益,完成了 5 种拓扑的单轮 Discovery 对比矩阵。
测试合同:
ignore_eos=true;并发 8/16/32。异构 P2/D1 相对混部 TP2 的直接对比如下:
主要结论:
完整 5 拓扑矩阵(点击展开)
Note
以上为单轮 Discovery 趋势数据,不是 3 轮 Formal 发布数据;并发 32 仅作为容量趋势。跨拓扑比较必须同时考虑实际 NPU 数。
功能验证
Warning
精度尚未验证。 当前证据覆盖异构缓存传输、非空输出和并发安全 smoke test;
pd_launch/test_accuracy.py尚未执行,因此不能声明模型精度通过。clang-formatpre-commit 检查通过。已知限制与后续工作
English Description
Summary
This PR adds heterogeneous tensor-parallel PD disaggregation for Qwen3.5 speculative decoding on Ascend NPU. The complete target is
Prefill DP1 × TP2 → Decode DP1 × TP1.It reconstructs target KV, causal-convolution state, SSM recurrent state, and MTP draft KV so Decode can continue from Prefill's first generated token without replaying Prefill.
Important
Key performance benefit (16K+ input, single-round Discovery)
Architecture
The implementation separates the control plane from the data plane and uses hybrid PUSH/PULL:
Target KV is pushed layer by layer to overlap transfer with Prefill computation. Decode synchronously pulls CONV/SSM and Draft KV so arrival, merge, device synchronization, and enqueueing have an explicit completion order.
A Decode rank owns source ranks satisfying
src_tp_rank = dst_tp_rank + n × dst_tp_size. For TP2→TP1, D-rank0 receives both Prefill shards.dim 2dim 1, with checkpoint expansiondim 2, or pull one TP1 replicaSafety
enable_pd_parallel_shard_pullis enabled by default and pulls the two source TP shards concurrently. Use--enable_pd_parallel_shard_pull=falseto fall back to serial PULL for debugging or compatibility.Configuration
This option is enabled by default. Set
--enable_pd_parallel_shard_pull=falseon Decode to switch heterogeneous source-shard recovery back to serial PULL.Main changes
This PR changes 33 files with
+1538/-93.16K+ Long-Context Performance (Discovery)
A single-round Discovery matrix was added for 51 prompts of 16,422–20,465 input tokens (18,591.824 on average), fixed 32/64-token outputs with
ignore_eos=true, and concurrency 8/16/32. MTP depth was 3 with Draft Body TP1. Each topology/shape was restarted independently and warmed up with 16/32/64 same-shape requests. All 30/30 combinations and 1530/1530 formal requests succeeded with exact output lengths.Across all six shapes, heterogeneous P2/D1 improves system Req/s and mean E2E over mixed TP2, with a stable 4.62–5.95 ms/token TPOT versus 19.63–185.24 ms/token for mixed TP2. This comparison is a system-level result: P2/D1 uses three NPUs while mixed TP2 uses two, so it does not claim higher per-NPU efficiency over mixed TP2.
The direct heterogeneous-value comparison is P2/D1 (3 NPUs) versus homogeneous P2/D2 (4 NPUs): P2/D1 retains 98.3%–98.7% of system Req/s with 25% fewer NPUs, while Req/s/NPU improves by 31.1%–31.6%. Mean E2E is only 1.3%–1.8% higher. P2×2/D2 additionally demonstrates scale-out headroom: versus single-Prefill P2/D2, system Req/s improves by 81.6%–93.6% and E2E falls by 45.5%–48.5%.
PD TTFT remains higher than mixed TP2 because long prompts queue on the centralized Prefill TP2 instance. The observed benefit is therefore stable Decode TPOT, system throughput, and E2E—not TTFT.
Note
These are single-round Discovery trends, not three-round Formal publishable results. Concurrency 32 is retained as a capacity trend, and cross-topology comparisons must account for NPU count. The full five-topology matrix is provided in the Chinese section above.
Validation and limitations
Warning
Model accuracy has not been validated. Current evidence covers transfer routing, non-empty outputs, and concurrency safety; formal accuracy remains pending.
Known limits: