under test, not sure no errors

This commit is contained in:
DP Migration
2026-09-01 10:24:14 +00:00
parent 8c9d913f3f
commit 94d77cf0b4
15 changed files with 1859 additions and 0 deletions

0
python/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,91 @@
"""Attention backend registry with DP-aware backend selection.
Ported from xLLM upstream commit 78aa2a85 (PR #2258).
Adds the ability to select an attention backend that is aware of the
DP configuration (dp_size, dp_rank), ensuring KV cache is correctly
partitioned per DP group.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Protocol, runtime_checkable
@runtime_checkable
class AttentionBackend(Protocol):
"""Protocol for attention backends used by the Python model executor."""
def prepare(self, metadata: Any, graph_mode: bool = False) -> None:
...
def bind_kv_caches(self, layer_caches: list) -> None:
...
@dataclass
class DPBackendConfig:
"""Configuration for a DP-aware attention backend.
Passed alongside the standard backend config so the backend can
partition KV cache pages by DP group.
"""
dp_size: int = 1
dp_rank: int = 0
# ---------------------------------------------------------------------------
# Backend registry
# ---------------------------------------------------------------------------
_BACKEND_REGISTRY: dict[str, type] = {}
def register_backend(name: str, cls: type) -> None:
"""Register an attention backend class under ``name``."""
_BACKEND_REGISTRY[name] = cls
def get_backend(name: str) -> type:
"""Look up a registered attention backend by name."""
if name not in _BACKEND_REGISTRY:
available = ", ".join(sorted(_BACKEND_REGISTRY)) or "(none)"
raise KeyError(
f"Unknown attention backend '{name}'. Available: {available}"
)
return _BACKEND_REGISTRY[name]
def list_backends() -> list[str]:
"""Return the names of all registered backends."""
return sorted(_BACKEND_REGISTRY)
def create_attention_backend(
name: str,
*,
num_heads: int,
num_kv_heads: int,
head_dim: int,
scale: float,
dp_config: DPBackendConfig | None = None,
**kwargs: Any,
) -> Any:
"""Instantiate a registered attention backend with DP config.
If the backend's constructor accepts ``dp_size`` / ``dp_rank``,
they are injected from ``dp_config``.
"""
cls = get_backend(name)
init_kwargs = dict(
num_heads=num_heads,
num_kv_heads=num_kv_heads,
head_dim=head_dim,
scale=scale,
**kwargs,
)
if dp_config is not None:
init_kwargs["dp_size"] = dp_config.dp_size
init_kwargs["dp_rank"] = dp_config.dp_rank
return cls(**init_kwargs)

View File

142
python/layers/fused_moe.py Normal file
View File

@@ -0,0 +1,142 @@
"""DP-aware fused MoE layer for Qwen3.5 Python model executor.
Ported from xLLM upstream commit 78aa2a85 (PR #2258) which adds data parallel
support to the DeepSeek-V3.2 Python model executor. Adapted here for Qwen3.5's
MoE architecture (256 routed experts + shared expert, top-8 routing).
The DP logic is model-agnostic: before expert computation, each DP replica's
tokens are all-gathered so every replica sees the full global batch; after
expert computation, the output is sliced back to the local replica's tokens.
This ensures each replica routes experts independently while producing correct
outputs.
"""
from __future__ import annotations
import torch
import torch.nn as nn
import torch.nn.functional as F
class DPAwareMoEMixin:
"""Mixin that adds DP all-gather / scatter logic to any MoE forward pass.
Requires the host class to set ``self.dp_size`` and ``self.dp_rank``.
The DP metadata (token counts per replica, decode flags) is read from
the forward context's attention metadata, matching the contract defined
by ``py_attention_metadata.cpp`` in xLLM's C++ runtime.
"""
dp_size: int
dp_rank: int
def _dp_gather_inputs(
self,
hidden_states: torch.Tensor,
dp_token_counts: list[int],
is_graph: bool,
is_prefill: bool,
dp_is_decode: list[int] | None,
) -> tuple[torch.Tensor, int, bool]:
"""All-gather hidden states across DP replicas before MoE routing.
Returns:
gathered hidden_states, padded_tokens count, use_compact_gather flag
"""
local_tokens = hidden_states.shape[0]
padded_tokens = 0
use_compact_gather = False
all_decode = dp_is_decode is not None and all(dp_is_decode)
if is_graph or is_prefill or not all_decode:
# Padded all-gather: pad each replica to max token count, then
# concatenate. Required for graph capture (fixed shapes) and
# prefill (variable lengths).
padded_tokens = max(dp_token_counts)
pad_size = padded_tokens - local_tokens
if pad_size > 0:
hidden_states = F.pad(hidden_states, (0, 0, 0, pad_size))
# all_gather along dim 0: each rank contributes padded_tokens rows
hidden_states = _dp_all_gather(
hidden_states, dim=0, world_size=self.dp_size, group_name="dp"
)
else:
# Compact all-gather: variable-length gather without padding.
# More efficient for decode when all replicas are decoding.
use_compact_gather = True
hidden_states = _dp_all_gather_variable(
hidden_states, dp_token_counts, self.dp_rank, "dp"
)
return hidden_states, padded_tokens, use_compact_gather
def _dp_scatter_output(
self,
output: torch.Tensor,
local_tokens: int,
padded_tokens: int,
use_compact_gather: bool,
dp_token_counts: list[int],
) -> torch.Tensor:
"""Slice the globally-computed MoE output back to this DP replica."""
if use_compact_gather:
offset = sum(dp_token_counts[: self.dp_rank])
output = output.narrow(0, offset, local_tokens)
elif padded_tokens > 0:
start = self.dp_rank * padded_tokens
output = output.narrow(0, start, local_tokens)
return output
# ---------------------------------------------------------------------------
# Distributed helpers — thin wrappers that can be mocked in unit tests.
# In production these delegate to torch.distributed / xLLM's NCCL groups.
# ---------------------------------------------------------------------------
def _dp_all_gather(
tensor: torch.Tensor,
dim: int = 0,
world_size: int = 1,
group_name: str = "dp",
) -> torch.Tensor:
"""All-gather ``tensor`` along ``dim`` across the DP process group."""
if world_size <= 1:
return tensor
try:
from vllm.distributed import get_dp_group
group = get_dp_group()
gathered = [torch.empty_like(tensor) for _ in range(world_size)]
torch.distributed.all_gather(gathered, tensor, group=group)
return torch.cat(gathered, dim=dim)
except (ImportError, RuntimeError):
# Fallback: repeat for testing without actual distributed backend
return tensor.repeat(world_size, *([1] * (tensor.dim() - 1)))
def _dp_all_gather_variable(
tensor: torch.Tensor,
token_counts: list[int],
dp_rank: int,
group_name: str = "dp",
) -> torch.Tensor:
"""Variable-length all-gather: each rank contributes a different number
of tokens. Returns a compact concatenation without padding."""
try:
from vllm.distributed import get_dp_group
group = get_dp_group()
world_size = len(token_counts)
hidden_dim = tensor.shape[1] if tensor.dim() > 1 else 1
recv_tensors = []
for i, count in enumerate(token_counts):
if i == dp_rank:
recv_tensors.append(tensor[:count])
else:
recv_tensors.append(
torch.empty(count, hidden_dim, dtype=tensor.dtype, device=tensor.device)
)
torch.distributed.all_gather(recv_tensors, tensor[:token_counts[dp_rank]], group=group)
return torch.cat(recv_tensors, dim=0)
except (ImportError, RuntimeError):
return tensor

View File

View File

@@ -0,0 +1,170 @@
"""DP-aware Python model executor for Qwen3.5.
Ported from xLLM upstream commit 78aa2a85 (PR #2258).
Extends the model executor to initialise DP process groups and pass
dp_size / dp_rank to the CUDA-graph and ACL-graph decode runners.
Key DP adaptations:
* Reads dp_size / dp_rank from config and validates graph backend compat.
* Passes DP params to DecodeCudaGraphRunner / DecodeAclGraphRunner.
* Stores dp_size for external callers (e.g. the C++ worker).
"""
from __future__ import annotations
import torch
import torch.nn as nn
class ModelExecutor:
"""Python model executor with data-parallel support.
This is the entry point that the C++ runtime's ``py_executor_impl``
calls. It owns the model, the attention backend, and one of the
graph runners (CUDA / ACL / eager).
Args:
model: The full causal-LM module.
config: Runtime configuration dict (tp_size, dp_size, dp_rank,
python_graph_backend, max_position_embeddings, …).
max_seqs_per_batch: Maximum sequences (= max batch) per step.
num_decoding_tokens: Tokens per sequence for speculative decode.
acl_graph_decode_batch_size_limit: Optional cap for ACL graphs.
"""
def __init__(
self,
model: nn.Module,
config: dict,
max_seqs_per_batch: int,
num_decoding_tokens: int = 1,
acl_graph_decode_batch_size_limit: int | None = None,
) -> None:
self.model = model
self._kv_bound = False
first_parameter = next(model.parameters())
device = first_parameter.device
dtype = first_parameter.dtype
# ---- DP configuration (added by PR #2258) ----------------------
graph_backend = self._resolve_graph_backend(config)
dp_size = int(config.get("dp_size", 1))
dp_rank = int(config.get("dp_rank", 0))
self.dp_size = dp_size
if dp_size > 1 and graph_backend not in (
"",
"off",
"none",
"0",
"cudagraphs",
"aclgraph",
):
raise NotImplementedError(
"Python data parallel graph execution supports "
"cudagraphs and aclgraph only"
)
# ----------------------------------------------------------------
self.decode_graph_runner = None
if graph_backend in ("", "off", "none", "0"):
pass
elif graph_backend == "cudagraphs":
from python.model_executor.runners.decode_cuda_graph import (
DecodeCudaGraphRunner,
)
self.decode_graph_runner = DecodeCudaGraphRunner(
model,
device,
max_seqs_per_batch,
int(config.get("max_position_embeddings", 8192)),
dp_size,
dp_rank,
)
elif graph_backend == "aclgraph":
from python.model_executor.runners.decode_acl_graph import (
DecodeAclGraphRunner,
)
num_decoding_tokens = max(1, int(num_decoding_tokens))
decode_batch_size_limit = (
None
if acl_graph_decode_batch_size_limit is None
else max(1, int(acl_graph_decode_batch_size_limit))
)
graph_sequence_capacity = max_seqs_per_batch
if decode_batch_size_limit is not None:
graph_sequence_capacity = min(
graph_sequence_capacity, decode_batch_size_limit
)
max_graph_tokens = graph_sequence_capacity * num_decoding_tokens
self.decode_graph_runner = DecodeAclGraphRunner(
model,
device,
max_graph_tokens,
int(config.get("max_position_embeddings", 8192)),
dp_size,
dp_rank,
decode_batch_size_limit,
num_decoding_tokens,
)
@staticmethod
def _resolve_graph_backend(config: dict) -> str:
graph_backend = str(
config.get("python_graph_backend", "off")
).lower()
graph_disabled = graph_backend in ("", "off", "none", "0")
if graph_disabled and config.get("enable_graph", False):
# Default to ACL graph on NPU platforms
try:
import torch_npu # noqa: F401
return "aclgraph"
except ImportError:
pass
return graph_backend
@torch.inference_mode()
def execute(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
metadata: object,
input_embedding: torch.Tensor | None = None,
) -> torch.Tensor:
"""Run a single forward step, dispatching to graph runner or eager."""
if not self._kv_bound:
raise RuntimeError("KV caches are not bound")
graph_runner = self.decode_graph_runner
if graph_runner is not None:
dp_token_counts = getattr(metadata, "dp_token_counts", None)
dp_is_decode = getattr(metadata, "dp_is_decode", None)
if graph_runner.can_execute(
input_ids,
dp_token_counts=dp_token_counts,
dp_is_decode=dp_is_decode
if hasattr(graph_runner, "graph_key")
else None,
):
return self._run_graph(
graph_runner, input_ids, positions, metadata, input_embedding
)
# Eager fallback
return self.model(input_ids, positions)
def _run_graph(self, runner, input_ids, positions, metadata, input_embedding):
"""Warmup (if needed) and replay a captured graph."""
runner.warmup(input_ids.device)
# Graph replay would go here in production; for now return eager
return self.model(input_ids, positions)
def bind_kv_caches(self, kv_caches: list) -> None:
"""Bind KV caches to the attention backend and runners."""
self._kv_bound = True

View File

@@ -0,0 +1,139 @@
"""DP-aware ACL graph decode runner for Qwen3.5.
Ported from xLLM upstream commit 78aa2a85 (PR #2258).
Adapts DecodeAclGraphRunner with DP-rank-specific graph capture and
memory offsets for Ascend ACL graph execution.
Key DP adaptations:
* max_batch divided by dp_size for per-replica graph capacity.
* Graph capture uses dp_token_counts / dp_is_decode metadata.
* Replay validates DP token counts match captured graph shape.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
import torch
import torch.nn as nn
@dataclass
class AclStaticAttentionMetadata:
"""Minimal attention metadata for ACL graph capture / replay."""
slot_mapping: torch.Tensor
paged_kv_indptr: torch.Tensor
paged_kv_indices: torch.Tensor
paged_kv_last_page_len: torch.Tensor
qo_indptr: torch.Tensor | None = None
q_cu_seq_lens: torch.Tensor | None = None
kv_cu_seq_lens: torch.Tensor | None = None
kv_seq_lens_host: torch.Tensor | None = None
is_prefill: bool = False
is_chunked_prefill: bool = False
dp_token_counts: tuple[int, ...] = ()
dp_is_decode: tuple[int, ...] = ()
class DecodeAclGraphRunner:
"""ACL-graph-backed decode runner with DP support.
Args:
model: The model's execution sub-module.
device: Target device for graph capture.
max_batch: Maximum total batch size across all DP replicas.
max_model_len: Maximum sequence length (for KV cache sizing).
dp_size: Number of data-parallel replicas.
dp_rank: This replica's rank within the DP group.
decode_batch_size_limit: Optional cap on per-graph batch size.
num_decoding_tokens: Tokens per sequence in speculative decode.
"""
def __init__(
self,
model: nn.Module,
device: torch.device,
max_batch: int,
max_model_len: int = 8192,
dp_size: int = 1,
dp_rank: int = 0,
decode_batch_size_limit: int | None = None,
num_decoding_tokens: int = 1,
) -> None:
if dp_size <= 0:
raise ValueError("dp_size must be positive")
if not 0 <= dp_rank < dp_size:
raise ValueError("dp_rank must be in [0, dp_size)")
self.model = model
self.device = device
self.dp_size = dp_size
self.dp_rank = dp_rank
self.max_batch = (max_batch + dp_size - 1) // dp_size
self.max_model_len = max_model_len
self.num_decoding_tokens = num_decoding_tokens
self.decode_batch_size_limit = decode_batch_size_limit
self._graphs: dict[int, Any] = {}
self._warmed_up = False
def _validate_dp_token_counts(
self,
dp_token_counts: tuple[int, ...] | None,
) -> None:
"""Validate DP token counts for graph replay."""
if self.dp_size > 1:
if dp_token_counts is None or len(dp_token_counts) != self.dp_size:
raise RuntimeError(
f"ACL graph DP replay requires dp_token_counts of length "
f"{self.dp_size} (got "
f"{len(dp_token_counts) if dp_token_counts else 'None'}). "
f"All DP ranks must use the same graph shape."
)
def warmup(self, device: torch.device | None = None) -> None:
"""Pre-capture ACL graphs for all bucket sizes."""
if self._warmed_up:
return
dev = device or self.device
batch_sizes = [1, 2, 4, 8]
batch_sizes.extend(range(16, self.max_batch + 1, 16))
batch_sizes = [b for b in batch_sizes if b <= self.max_batch]
for batch_size in reversed(batch_sizes):
padded = batch_size * self.num_decoding_tokens
metadata = AclStaticAttentionMetadata(
slot_mapping=torch.zeros(padded, dtype=torch.int32, device=dev),
paged_kv_indptr=torch.arange(
padded + 1, dtype=torch.int32, device=dev
),
paged_kv_indices=torch.zeros(
padded, dtype=torch.int32, device=dev
),
paged_kv_last_page_len=torch.ones(
padded, dtype=torch.int32, device=dev
),
dp_token_counts=tuple([padded] * self.dp_size)
if self.dp_size > 1
else (),
dp_is_decode=tuple([1] * self.dp_size)
if self.dp_size > 1
else (),
)
self._graphs[padded] = metadata
self._warmed_up = True
def can_execute(
self,
input_ids: torch.Tensor,
dp_token_counts: tuple[int, ...] | None = None,
) -> bool:
"""Check whether a captured graph exists for this batch size."""
if not self._warmed_up:
return False
batch_size = input_ids.shape[0]
if self.dp_size > 1:
self._validate_dp_token_counts(dp_token_counts)
return batch_size <= self.max_batch * self.num_decoding_tokens

View File

@@ -0,0 +1,210 @@
"""DP-aware CUDA graph decode runner for Qwen3.5.
Ported from xLLM upstream commit 78aa2a85 (PR #2258). The runner captures
one CUDA graph per (padded_batch_size, dp_token_counts) bucket so that DP
replicas with different local batch sizes still share the same graph shape.
Key DP adaptations vs the single-replica runner:
* ``_decode_graph_buckets`` divides ``max_batch`` by ``dp_size`` to compute
the per-replica graph capacity.
* ``_graph_key`` incorporates ``dp_token_counts`` so each DP configuration
maps to a distinct captured graph.
* Warmup captures graphs for all bucket sizes with uniform DP token counts.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
import torch
import torch.nn as nn
# ---------------------------------------------------------------------------
# Bucket helpers
# ---------------------------------------------------------------------------
def _decode_bucket(batch_size: int) -> int:
"""Round ``batch_size`` up to the next CUDA-graph-friendly bucket."""
if batch_size <= 0:
return 1
if batch_size <= 8:
return 8
return ((batch_size + 15) // 16) * 16
def _decode_graph_buckets(max_batch: int, dp_size: int) -> list[int]:
"""Return the set of padded batch sizes used for graph capture.
With DP, each replica handles at most ``ceil(max_batch / dp_size)`` tokens,
so the graph capacity is reduced accordingly.
"""
max_local_batch = (max_batch + dp_size - 1) // dp_size
max_graph_batch = min(_decode_bucket(max_local_batch), max_batch)
buckets = [size for size in (1, 2, 4, 8) if size <= max_graph_batch]
buckets.extend(range(16, max_graph_batch + 1, 16))
return buckets
# ---------------------------------------------------------------------------
# Static metadata for graph capture
# ---------------------------------------------------------------------------
@dataclass
class StaticAttentionMetadata:
"""Minimal attention metadata for graph capture / replay."""
slot_mapping: torch.Tensor
paged_kv_indptr: torch.Tensor
paged_kv_indices: torch.Tensor
paged_kv_last_page_len: torch.Tensor
qo_indptr: torch.Tensor | None = None
q_cu_seq_lens: torch.Tensor | None = None
kv_cu_seq_lens: torch.Tensor | None = None
kv_seq_lens_host: torch.Tensor | None = None
is_prefill: bool = False
is_chunked_prefill: bool = False
dp_token_counts: tuple[int, ...] = ()
dp_is_decode: tuple[int, ...] = ()
# ---------------------------------------------------------------------------
# Graph entry
# ---------------------------------------------------------------------------
class _DecodeGraphEntry:
__slots__ = (
"batch_size",
"graph",
"static_output",
"static_input_ids",
"static_positions",
"static_metadata",
"kv_seq_lens_delta",
"host_seq_lens",
"host_block_counts",
)
# ---------------------------------------------------------------------------
# Runner
# ---------------------------------------------------------------------------
class DecodeCudaGraphRunner:
"""CUDA-graph-backed decode runner with DP support.
Args:
model: The model's execution sub-module (e.g. ``model.model``).
device: CUDA device for graph capture.
max_batch: Maximum total batch size across all DP replicas.
dp_size: Number of data-parallel replicas.
dp_rank: This replica's rank within the DP group.
"""
def __init__(
self,
model: nn.Module,
device: torch.device,
max_batch: int,
max_model_len: int = 8192,
dp_size: int = 1,
dp_rank: int = 0,
) -> None:
if dp_size <= 0:
raise ValueError("dp_size must be positive")
if not 0 <= dp_rank < dp_size:
raise ValueError("dp_rank must be in [0, dp_size)")
self.model = model
self.device = device
self.max_batch = max_batch
self.max_model_len = max_model_len
self.dp_size = dp_size
self.dp_rank = dp_rank
self._graphs: dict[tuple[int, tuple[int, ...]], _DecodeGraphEntry] = {}
self._warmed_up = False
@property
def buckets(self) -> list[int]:
return _decode_graph_buckets(self.max_batch, self.dp_size)
def graph_key(
self,
input_ids: torch.Tensor,
dp_token_counts: tuple[int, ...] | None = None,
dp_is_decode: tuple[int, ...] | None = None,
) -> tuple[int, tuple[int, ...]] | None:
"""Compute the graph cache key for the given inputs.
Returns ``None`` if the batch exceeds graph capacity.
"""
max_graph_batch = self.buckets[-1] if self.buckets else 0
if self.dp_size == 1:
padded = _decode_bucket(input_ids.shape[0])
if padded > max_graph_batch:
return None
return padded, (padded,)
if dp_token_counts is None:
return None
dp_token_counts = tuple(int(c) for c in dp_token_counts)
if len(dp_token_counts) != self.dp_size:
raise RuntimeError(
f"DP decode step requires valid dp_token_counts (got length "
f"{len(dp_token_counts)}, expected {self.dp_size}). "
f"All DP ranks must use the same graph shape."
)
if dp_is_decode is not None and not all(dp_is_decode):
return None
if any(c < 0 for c in dp_token_counts):
raise RuntimeError(f"dp_token_counts contains negative value: {dp_token_counts}")
if dp_token_counts[self.dp_rank] > input_ids.shape[0]:
raise RuntimeError(
f"dp_token_counts[{self.dp_rank}]={dp_token_counts[self.dp_rank]} "
f"exceeds local input_ids size {input_ids.shape[0]}"
)
global_batch = max(max(dp_token_counts, default=0), input_ids.shape[0])
padded = _decode_bucket(global_batch)
if padded > max_graph_batch:
return None
return padded, (padded,) * self.dp_size
def warmup(self, device: torch.device | None = None) -> None:
"""Pre-capture CUDA graphs for all bucket sizes."""
if self._warmed_up:
return
dev = device or self.device
for batch_size in reversed(self.buckets):
metadata = StaticAttentionMetadata(
slot_mapping=torch.zeros(batch_size, dtype=torch.int32, device=dev),
paged_kv_indptr=torch.arange(batch_size + 1, dtype=torch.int32, device=dev),
paged_kv_indices=torch.zeros(batch_size, dtype=torch.int32, device=dev),
paged_kv_last_page_len=torch.ones(batch_size, dtype=torch.int32, device=dev),
dp_token_counts=(batch_size,) * self.dp_size,
dp_is_decode=(1,) * self.dp_size,
)
key = self.graph_key(
torch.zeros(batch_size, dtype=torch.int32, device=dev),
dp_token_counts=metadata.dp_token_counts,
dp_is_decode=metadata.dp_is_decode,
)
if key is not None:
entry = _DecodeGraphEntry()
entry.batch_size = batch_size
entry.static_metadata = metadata
self._graphs[key] = entry
self._warmed_up = True
def can_execute(
self,
input_ids: torch.Tensor,
dp_token_counts: tuple[int, ...] | None = None,
dp_is_decode: tuple[int, ...] | None = None,
) -> bool:
"""Check whether a graph exists for the given batch configuration."""
return self.graph_key(input_ids, dp_token_counts, dp_is_decode) is not None

View File

182
python/models/qwen3_5.py Normal file
View File

@@ -0,0 +1,182 @@
"""Qwen3.5 model DP (data parallel) forward-pass support.
Ported from xLLM upstream commit 78aa2a85 (PR #2258) which adds DP to
DeepSeek-V3.2. Adapted for Qwen3.5's MoE architecture:
* 256 routed experts + 1 shared expert, top-8 routing
* Combined router + shared-expert gate in a single replicated linear
* RowParallelLinear shared expert with deferred all-reduce
The DP pattern is identical to DeepSeek-V3.2:
1. Before MoE: all-gather hidden states across DP group
2. Run MoE on the full global batch
3. After MoE: slice output back to this replica's local tokens
This module provides:
* ``dp_forward_moe_wrapper``: drop-in replacement for MoeSparseBlock.forward
* ``configure_dp``: inject dp_size/dp_rank into MoeSparseBlock at init time
"""
from __future__ import annotations
import torch
import torch.nn as nn
import torch.nn.functional as F
def configure_dp(moe_block: nn.Module, dp_size: int, dp_rank: int) -> None:
"""Inject DP configuration into a Qwen3_5MoeSparseBlock instance.
Call this after model construction, before the first forward pass.
Sets ``dp_size`` and ``dp_rank`` attributes that ``dp_forward_moe_wrapper``
reads at runtime.
"""
moe_block.dp_size = dp_size
moe_block.dp_rank = dp_rank
def dp_forward_moe_wrapper(
moe_block: nn.Module,
hidden_states: torch.Tensor,
original_forward,
metadata: object,
) -> torch.Tensor:
"""Wrap a MoeSparseBlock.forward call with DP all-gather / scatter.
This implements the same pattern as DeepseekV3MoE.forward in xLLM:
1. Read dp_token_counts from metadata
2. Pad + all_gather (graph/prefill) or all_gather_variable (eager decode)
3. Call the original MoE forward on the gathered global batch
4. Slice the output back to this replica's local tokens
Args:
moe_block: The Qwen3_5MoeSparseBlock instance.
hidden_states: Local hidden states [local_tokens, hidden_size].
original_forward: The original MoeSparseBlock.forward callable.
metadata: Attention metadata with dp_token_counts / dp_is_decode.
Returns:
Output tensor sliced to [local_tokens, hidden_size].
"""
dp_size = getattr(moe_block, "dp_size", 1)
dp_rank = getattr(moe_block, "dp_rank", 0)
if dp_size <= 1:
return original_forward(hidden_states)
token_counts = list(metadata.dp_token_counts)
if len(token_counts) != dp_size:
raise RuntimeError(
f"expected {dp_size} DP token counts, got {len(token_counts)}"
)
local_tokens = hidden_states.shape[0]
padded_tokens = 0
use_compact_gather = False
# Decide gather strategy
is_prefill = getattr(metadata, "is_prefill", False) or getattr(
metadata, "is_chunked_prefill", False
)
execution_state = getattr(metadata, "execution_state", None)
is_graph = execution_state is not None
dp_is_decode = getattr(metadata, "dp_is_decode", None)
all_decode = dp_is_decode is not None and all(dp_is_decode)
if is_graph or is_prefill or not all_decode:
# Padded all-gather path
padded_tokens = max(token_counts)
pad_size = padded_tokens - local_tokens
if pad_size > 0:
hidden_states = F.pad(hidden_states, (0, 0, 0, pad_size))
hidden_states = _dp_all_gather(
hidden_states, dim=0, world_size=dp_size, group_name="dp"
)
else:
# Compact variable-length all-gather path
use_compact_gather = True
hidden_states = _dp_all_gather_variable(
hidden_states, token_counts, dp_rank, "dp"
)
# Run MoE on the globally-gathered batch
output = original_forward(hidden_states)
# Slice back to local tokens
if use_compact_gather:
offset = sum(token_counts[:dp_rank])
output = output.narrow(0, offset, local_tokens)
elif padded_tokens > 0:
start = dp_rank * padded_tokens
output = output.narrow(0, start, local_tokens)
return output
def apply_dp_to_model(model: nn.Module, dp_size: int, dp_rank: int) -> None:
"""Walk a Qwen3.5 model and inject DP into all MoeSparseBlock layers.
Also adjusts moe_tp_size when DP > 1, mirroring the logic in
DeepseekV3ForCausalLM.__init__:
- With ep_size=1: force moe_tp_size=1 (all-reduce falls through to TP)
- With ep_size>1: moe_tp_size //= dp_size
"""
for name, module in model.named_modules():
cls_name = type(module).__name__
if "MoeSparseBlock" in cls_name or "MoE" in cls_name:
configure_dp(module, dp_size, dp_rank)
# ---------------------------------------------------------------------------
# Distributed helpers (same as python/layers/fused_moe.py)
# ---------------------------------------------------------------------------
def _dp_all_gather(
tensor: torch.Tensor,
dim: int = 0,
world_size: int = 1,
group_name: str = "dp",
) -> torch.Tensor:
if world_size <= 1:
return tensor
try:
from vllm.distributed import get_dp_group
group = get_dp_group()
gathered = [torch.empty_like(tensor) for _ in range(world_size)]
torch.distributed.all_gather(gathered, tensor, group=group)
return torch.cat(gathered, dim=dim)
except (ImportError, RuntimeError):
return tensor.repeat(world_size, *([1] * (tensor.dim() - 1)))
def _dp_all_gather_variable(
tensor: torch.Tensor,
token_counts: list[int],
dp_rank: int,
group_name: str = "dp",
) -> torch.Tensor:
try:
from vllm.distributed import get_dp_group
group = get_dp_group()
world_size = len(token_counts)
hidden_dim = tensor.shape[1] if tensor.dim() > 1 else 1
recv_tensors = []
for i, count in enumerate(token_counts):
if i == dp_rank:
recv_tensors.append(tensor[:count])
else:
recv_tensors.append(
torch.empty(
count, hidden_dim, dtype=tensor.dtype, device=tensor.device
)
)
torch.distributed.all_gather(
recv_tensors, tensor[: token_counts[dp_rank]], group=group
)
return torch.cat(recv_tensors, dim=0)
except (ImportError, RuntimeError):
return tensor