Skip to content

Commit e375d65

Browse files
committed
feat: support data parallel (DP) for DeepSeek-V3.2 Python model executor.
1 parent bfe18ec commit e375d65

6 files changed

Lines changed: 72 additions & 41 deletions

File tree

xllm/models/llm/py_causal_lm.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,6 @@ PyCausalLM::PyCausalLM(const ModelContext& context)
8787
dp_size_ = (dp_group != nullptr) ? dp_group->world_size() : 1;
8888
dp_rank_ = (dp_group != nullptr) ? dp_group->rank() : 0;
8989
ep_size_ = parallel_args.ep_size();
90-
CHECK(ep_size_ == 1 || ep_size_ == parallel_args.world_size())
91-
<< "Python models support only ep_size=1 or ep_size=world_size.";
9290

9391
CHECK(parallel_args.moe_tp_group_ != nullptr);
9492
ProcessGroup* moe_tp_group = parallel_args.moe_tp_group_;

xllm/python/distributed/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
all_gather,
2121
all_gather_variable,
2222
all_reduce_,
23+
barrier,
2324
cp_rank,
2425
cp_world_size,
2526
init_process_group,
@@ -36,4 +37,5 @@
3637
"all_reduce_",
3738
"all_gather",
3839
"all_gather_variable",
40+
"barrier",
3941
]

xllm/python/distributed/collectives.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,24 @@ def _(
383383
return x.new_empty(shape)
384384

385385

386+
def barrier(device: torch.device | str, group_name: str = "dp") -> None:
387+
"""Block until all ranks in the group have reached this point.
388+
389+
Drains the local device queue first (synchronize), then coordinates
390+
across ranks via dist.barrier. Unlike an all_reduce approach, no
391+
data-plane traffic is injected into the work queue — this matters
392+
before NPUGraph capture which expects a clean queue.
393+
"""
394+
device_obj = torch.device(device) if isinstance(device, str) else device
395+
group = _groups.get((group_name, str(device_obj)))
396+
if group is None or group.size() <= 1:
397+
return
398+
device_module = getattr(torch, device_obj.type, None)
399+
if device_module is not None and hasattr(device_module, "synchronize"):
400+
device_module.synchronize(device_obj)
401+
dist.barrier(group=group)
402+
403+
386404
__all__ = [
387405
"init_process_group",
388406
"init_tp_group",
@@ -392,4 +410,5 @@ def _(
392410
"all_reduce_",
393411
"all_gather",
394412
"all_gather_variable",
413+
"barrier",
395414
]

xllm/python/layers/fused_moe.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,18 @@ def __init__(
103103
)
104104

105105
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
106-
local_hidden_states = hidden_states
107-
token_counts: list[int] | None = None
106+
local_tokens: int = 0
107+
padded_tokens: int = 0
108108
if self.dp_size > 1:
109109
token_counts = list(get_forward_context().metadata.dp_token_counts)
110110
if len(token_counts) != self.dp_size:
111111
raise RuntimeError(f"expected {self.dp_size} DP token counts, got {token_counts}")
112-
hidden_states = distributed.all_gather_variable(
113-
hidden_states,
114-
token_counts,
115-
self.dp_rank,
116-
"dp",
117-
)
112+
padded_tokens = max(token_counts)
113+
local_tokens = hidden_states.shape[0]
114+
pad_size = padded_tokens - local_tokens
115+
if pad_size > 0:
116+
hidden_states = torch.nn.functional.pad(hidden_states, (0, 0, 0, pad_size))
117+
hidden_states = distributed.all_gather(hidden_states, dim=0, world_size=self.dp_size, group_name="dp")
118118

119119
router_logits = self.gate(hidden_states)
120120
topk_weights, topk_ids = kernels.moe_fused_topk(
@@ -150,15 +150,7 @@ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
150150
distributed.all_reduce_(output, "moe_tp")
151151
if self.ep_size > 1:
152152
distributed.all_reduce_(output, "moe_ep")
153-
if token_counts is not None:
154-
local_tokens = token_counts[self.dp_rank]
155-
if local_tokens == 0:
156-
return torch.zeros_like(local_hidden_states)
157-
start = sum(token_counts[: self.dp_rank])
158-
local_output = output.narrow(0, start, local_tokens)
159-
if local_tokens == local_hidden_states.shape[0]:
160-
return local_output
161-
padding_shape = list(local_hidden_states.shape)
162-
padding_shape[0] -= local_tokens
163-
return torch.cat([local_output, local_hidden_states.new_zeros(padding_shape)], dim=0)
153+
if padded_tokens > 0:
154+
start = self.dp_rank * padded_tokens
155+
output = output.narrow(0, start, local_tokens)
164156
return output

xllm/python/model_executor/runners/decode_acl_graph.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
import torch
3636
import torch.nn as nn
3737

38-
from xllm.python import kernels
38+
from xllm.python import distributed, kernels
3939
from xllm.python.attention.backend import AttentionBackend, AttentionMetadata
4040
from xllm.python.attention.expanded_decode_metadata import (
4141
ExpandedDecodeMetadata,
@@ -138,14 +138,20 @@ def can_execute(
138138
if input_ids.dim() != 1:
139139
return False
140140
batch_size = input_ids.numel()
141-
bucket_size = _decode_bucket(batch_size)
142141
is_expanded_spec_verify = resolve_expanded_decode_metadata(metadata) is not None
143-
return (
142+
if not (
144143
((not metadata.is_prefill and not metadata.is_chunked_prefill) or is_expanded_spec_verify)
145144
and self._has_compatible_decode_metadata(input_ids, metadata)
146145
and (input_embedding is None or input_embedding.shape[0] == batch_size)
147-
and bucket_size <= self.max_batch
148-
)
146+
):
147+
return False
148+
if self.dp_size > 1:
149+
dp_token_counts = getattr(metadata, "dp_token_counts", None)
150+
if dp_token_counts is None or len(dp_token_counts) != self.dp_size:
151+
return False
152+
global_batch = max(max(int(c) for c in dp_token_counts), batch_size)
153+
return _decode_bucket(global_batch) <= self.max_batch
154+
return _decode_bucket(batch_size) <= self.max_batch
149155

150156
def _decode_metadata(
151157
self, metadata: AttentionMetadata
@@ -385,7 +391,14 @@ def execute(
385391
input_embedding: torch.Tensor | None = None,
386392
) -> torch.Tensor:
387393
batch_size = input_ids.shape[0]
388-
padded_batch_size = _decode_bucket(batch_size)
394+
395+
if self.dp_size > 1:
396+
dp_token_counts = tuple(int(c) for c in metadata.dp_token_counts)
397+
global_batch = max(max(dp_token_counts, default=0), batch_size)
398+
padded_batch_size = _decode_bucket(global_batch)
399+
else:
400+
padded_batch_size = _decode_bucket(batch_size)
401+
389402
if padded_batch_size > self.max_batch:
390403
raise ValueError("decode batch exceeds ACL graph capacity")
391404

@@ -418,6 +431,9 @@ def execute(
418431
self.attention_backend.prepare(entry.static_metadata, graph_mode=True)
419432

420433
if first_capture:
434+
self._stream.wait_stream(torch.npu.current_stream())
435+
if self.dp_size > 1:
436+
distributed.barrier(self.device, "dp")
421437
self._capture(entry)
422438

423439
self._stream.wait_stream(torch.npu.current_stream())
@@ -524,6 +540,7 @@ def _allocate_entry(
524540
paged_kv_last_page_len_host=torch.ones(padded_batch_size, dtype=torch.int32, device="cpu"),
525541
kv_seq_lens_host_values=[1] * padded_batch_size,
526542
block_table=static_block_table,
543+
dp_token_counts=tuple([padded_batch_size] * self.dp_size) if self.dp_size > 1 else (),
527544
)
528545
is_expanded = resolve_expanded_decode_metadata(metadata) is not None
529546
entry.kv_seq_lens_delta = torch.empty(padded_batch_size, dtype=torch.int32, device=device)
@@ -663,7 +680,7 @@ def _capture(self, entry: _DecodeGraphEntry) -> None:
663680
self.layer_caches,
664681
execution_state=entry.execution_state,
665682
)
666-
with forward_context(context):
683+
with forward_context(context), torch.npu.stream(self._stream):
667684
for _ in range(_CAPTURE_WARMUP_STEPS):
668685
self._forward_static(entry)
669686
torch.npu.synchronize()

xllm/python/models/deepseek_v32.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -829,16 +829,16 @@ def process_weights_after_loading(self) -> None:
829829
self.shared_experts.process_weights_after_loading()
830830

831831
def forward(self, hidden: torch.Tensor) -> torch.Tensor:
832-
local_hidden = hidden
833-
token_counts: list[int] | None = None
832+
local_tokens: int = 0
833+
padded_tokens: int = 0
834834
if self.dp_size > 1:
835835
token_counts = list(get_forward_context().metadata.dp_token_counts)
836-
hidden = distributed.all_gather_variable(
837-
hidden,
838-
token_counts,
839-
self.dp_rank,
840-
"dp",
841-
)
836+
padded_tokens = max(token_counts)
837+
local_tokens = hidden.shape[0]
838+
pad_size = padded_tokens - local_tokens
839+
if pad_size > 0:
840+
hidden = torch.nn.functional.pad(hidden, (0, 0, 0, pad_size))
841+
hidden = distributed.all_gather(hidden, dim=0, world_size=self.dp_size, group_name="dp")
842842

843843
logits = self.gate(hidden)
844844
routed = kernels.grouped_moe(
@@ -867,11 +867,8 @@ def forward(self, hidden: torch.Tensor) -> torch.Tensor:
867867
elif self.cfg.tp_size > 1 and self.ep_size == 1:
868868
distributed.all_reduce_(final)
869869

870-
if token_counts is not None:
871-
local_tokens = token_counts[self.dp_rank]
872-
if local_tokens == 0:
873-
return torch.zeros_like(local_hidden)
874-
start = sum(token_counts[: self.dp_rank])
870+
if padded_tokens > 0:
871+
start = self.dp_rank * padded_tokens
875872
final = final.narrow(0, start, local_tokens)
876873

877874
return final
@@ -967,6 +964,12 @@ def __init__(self, config: dict, build_model: bool = True) -> None:
967964
self.cfg.moe_tp_size = int(config.get("moe_tp_size", 1))
968965
self.cfg.moe_tp_rank = int(config.get("moe_tp_rank", 0))
969966
self.cfg.world_size = int(config.get("world_size", self.cfg.tp_size))
967+
# C++ computes moe_tp_size = world_size / ep_size, which conflates DP
968+
# replicas with TP shards. Correct to exclude DP ranks: each DP replica
969+
# holds the full expert set independently — no MoE TP reduce needed
970+
# across DP boundaries.
971+
if self.cfg.dp_size > 1 and self.cfg.moe_tp_size > 1:
972+
self.cfg.moe_tp_size = max(1, self.cfg.moe_tp_size // self.cfg.dp_size)
970973
if hasattr(self.cfg, "validate"):
971974
self.cfg.validate()
972975
dtype = self.resolve_dtype(config.get("dtype") or config.get("torch_dtype"))

0 commit comments

Comments
 (0)