A symbol scan over every tracked file found 123 top-level definitions in vllm/ with zero references, including references inside strings. All of them came from features this fork deleted earlier. - Delete dead attention helpers: MLADCPManager, split_prefill_chunks, the reshape helpers, compute_varlen_chunk_metadata, the LINEAR backend's unused BailingLinearAttentionBackend, and the sparse flash-attn entry points. - Delete the CPU and AMX op wrappers from _custom_ops.py: cpu_mla_decode, cpu_mla_extend, cpu_mla_get_scheduler_metadata, gather_mla_context_cache_cpu. - Delete dead quantization helpers: awq_pack, gptq_pack, gptq_quantize_weights, get_and_maybe_dequant_weights, marlin_quant_fp8_torch, the rand_marlin_weight_*_like pair, ocp_mx_weight_dtype_and_rows, get_fp8_block_weight_scale, input_to_float8. - Delete dead model helpers: resolve_visual_encoder_outputs, run_dp_sharded_vision_model, fast_topk, get_layer_index, is_pp_missing_parameter, skip_spec_layers, KimiK3MergedQKVGateLinear, selective_scan_fn, causal_conv1d_update_torch. - Delete dead utils: format_kib, format_mib, chunk_list, prev_power_of_2, split_host_port, join_host_port, make_zmq_path, make_async_with_semaphore, is_meta_module, is_non_overlapping_and_dense, supports_xpu_graph, create_kv_caches_with_random*, and the unused has_* capability probes. - Delete the unreachable deep-gemm mega-MoE warmup path, which imported vllm.models.deepseek_v4.nvidia.model and vllm.models.kimi_k3.nvidia.model, neither of which exists anymore. - Drop the imports and the two now-empty guards left behind. Verified statically: every changed file compiles, no unused import remains, and no deleted name is referenced in vllm, tests or csrc. The runtime gates still need the Python environment rebuilt. Co-authored-by: Hinata <hinata@idihore.id> Signed-off-by: MOVZX <movzx@yahoo.com>
1446 lines
47 KiB
Python
1446 lines
47 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""MLA DCP collective selection and direct symmetric-memory implementations."""
|
|
|
|
|
|
import functools
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Any, Protocol
|
|
|
|
import torch
|
|
import torch.distributed as dist
|
|
|
|
import vllm.envs as envs
|
|
from vllm.config import VllmConfig
|
|
from vllm.logger import init_logger
|
|
from vllm.model_executor.warmup.jit_warmup import (
|
|
WarmupIntRange,
|
|
kernel_launcher,
|
|
)
|
|
from vllm.model_executor.warmup.jit_warmup_triton_helper import (
|
|
LaunchSpec,
|
|
TritonWarmupTensor,
|
|
VllmTritonJitKernel,
|
|
triton_scalar_specialization_rep,
|
|
)
|
|
from vllm.triton_utils import tl, triton
|
|
from vllm.v1.attention.backend import max_decode_query_len
|
|
from vllm.v1.attention.ops.cp_common import (
|
|
DirectCPWorkspace,
|
|
direct_cp_enabled,
|
|
direct_cp_multicast_enabled,
|
|
)
|
|
from vllm.v1.worker.ubatching import dbo_current_ubatch_id
|
|
|
|
logger = init_logger(__name__)
|
|
|
|
if TYPE_CHECKING:
|
|
from torch.distributed import ProcessGroup
|
|
|
|
from vllm.distributed.parallel_state import GroupCoordinator
|
|
|
|
|
|
# LSE/output combine
|
|
|
|
|
|
def _validate_dcp_empty_shard_args(
|
|
seq_lens: torch.Tensor | None,
|
|
query_start_loc: torch.Tensor | None,
|
|
) -> bool:
|
|
"""Validate the empty-shard mask inputs.
|
|
|
|
Returns True when masking is requested, False when it is disabled. Raises on
|
|
an inconsistent pair. Performs no device work so it is safe to call from a
|
|
CUDA-graph-captured region.
|
|
"""
|
|
if seq_lens is None and query_start_loc is None:
|
|
return False
|
|
if seq_lens is None or query_start_loc is None:
|
|
raise ValueError("seq_lens and query_start_loc must be provided together")
|
|
if (
|
|
seq_lens.ndim != 1
|
|
or query_start_loc.ndim != 1
|
|
or query_start_loc.shape[0] != seq_lens.shape[0] + 1
|
|
):
|
|
raise ValueError("query_start_loc must contain one boundary per sequence")
|
|
return True
|
|
|
|
|
|
def mask_dcp_empty_shards_(
|
|
lse: torch.Tensor,
|
|
seq_lens: torch.Tensor | None,
|
|
query_start_loc: torch.Tensor | None,
|
|
) -> None:
|
|
if not _validate_dcp_empty_shard_args(seq_lens, query_start_loc):
|
|
return
|
|
assert seq_lens is not None and query_start_loc is not None
|
|
|
|
# A DCP rank can receive no local sequences during CUDA graph warmup even
|
|
# though the padded LSE buffer still has rows. In that case every row is an
|
|
# empty shard; avoid indexing the empty seq_lens tensor below.
|
|
if seq_lens.shape[0] == 0:
|
|
lse.fill_(float("-inf"))
|
|
return
|
|
|
|
row_indices = torch.arange(
|
|
lse.shape[0], device=lse.device, dtype=query_start_loc.dtype
|
|
)
|
|
sequence_indices = torch.searchsorted(
|
|
query_start_loc[1:], row_indices, right=True
|
|
).clamp_max(seq_lens.shape[0] - 1)
|
|
empty_rows = (row_indices >= query_start_loc[-1]) | (
|
|
seq_lens[sequence_indices] == 0
|
|
)
|
|
lse.masked_fill_(empty_rows[:, None], float("-inf"))
|
|
|
|
|
|
# AG + RS/AR implementation
|
|
|
|
|
|
class CorrectAttnCPOutKernel(VllmTritonJitKernel["CorrectAttnCPOutKernel.CompileKey"]):
|
|
@dataclass(frozen=True)
|
|
class CompileKey:
|
|
output_dtype: torch.dtype
|
|
lse_dtype: torch.dtype
|
|
outputs_stride_b: int
|
|
outputs_stride_h: int
|
|
outputs_stride_d: int
|
|
lses_stride_n: int
|
|
lses_stride_b: int
|
|
lses_stride_h: int
|
|
lse_idx: int
|
|
head_dim: int
|
|
n_rounded: int
|
|
is_base_e: bool
|
|
|
|
@staticmethod
|
|
@triton.jit
|
|
def kernel(
|
|
outputs_ptr,
|
|
new_output_ptr,
|
|
lses_ptr,
|
|
vlse_ptr,
|
|
outputs_stride_B,
|
|
outputs_stride_H,
|
|
outputs_stride_D,
|
|
lses_stride_N,
|
|
lses_stride_B,
|
|
lses_stride_H,
|
|
lse_idx,
|
|
HEAD_DIM: tl.constexpr,
|
|
N_ROUNDED: tl.constexpr,
|
|
IS_BASE_E: tl.constexpr,
|
|
):
|
|
"""Apply the all-gathered lses to correct each local rank's attention
|
|
output. we still need perform a cross-rank reduction to obtain the
|
|
final attention output.
|
|
|
|
Args:
|
|
outputs_ptr (triton.PointerType):
|
|
Pointer to input tensor of shape [ B, H, D ]
|
|
lses_ptr (triton.PointerType):
|
|
Pointer to input tensor of shape [ N, B, H ]
|
|
new_output_ptr (triton.PointerType):
|
|
Pointer to output tensor of shape [ B, H, D ]
|
|
vlse_ptr (triton.PointerType):
|
|
Pointer to output tensor of shape [ B, H ]
|
|
outputs_stride_B (int): Batch stride of ``outputs_ptr``
|
|
outputs_stride_H (int): Head stride of ``outputs_ptr``
|
|
outputs_stride_D (int): Head-dim stride of ``outputs_ptr``
|
|
lses_stride_N (int): Rank stride of ``lses_ptr``
|
|
lses_stride_B (int): Batch stride of ``lses_ptr``
|
|
lses_stride_H (int): Head stride of ``lses_ptr``
|
|
lse_idx (int): Index of this rank's lse within the all-gathered tensor
|
|
HEAD_DIM: Head dimension, as a constexpr
|
|
N_ROUNDED: Rank count rounded to a power of two, as a constexpr
|
|
IS_BASE_E: Whether the lses are natural-log based, as a constexpr
|
|
|
|
"""
|
|
batch_idx = tl.program_id(axis=0).to(tl.int64)
|
|
head_idx = tl.program_id(axis=1).to(tl.int64)
|
|
d_offsets = tl.arange(0, HEAD_DIM)
|
|
num_n_offsets = tl.arange(0, N_ROUNDED)
|
|
|
|
# shape = [N]
|
|
lse_offsets = (
|
|
num_n_offsets * lses_stride_N
|
|
+ batch_idx * lses_stride_B
|
|
+ head_idx * lses_stride_H
|
|
)
|
|
|
|
# calc final lse
|
|
lse = tl.load(lses_ptr + lse_offsets).to(tl.float32)
|
|
lse = tl.where((lse != lse) | (lse == float("inf")), -float("inf"), lse)
|
|
lse_max = tl.max(lse, axis=0)
|
|
lse_max = tl.where(lse_max == -float("inf"), 0, lse_max)
|
|
lse -= lse_max
|
|
if IS_BASE_E:
|
|
lse_exp = tl.exp(lse)
|
|
lse_acc = tl.sum(lse_exp, axis=0)
|
|
lse = tl.log(lse_acc)
|
|
else:
|
|
lse_exp = tl.exp2(lse)
|
|
lse_acc = tl.sum(lse_exp, axis=0)
|
|
lse = tl.log2(lse_acc)
|
|
lse += lse_max
|
|
|
|
lse_offsets = batch_idx * lses_stride_B + head_idx * lses_stride_H
|
|
tl.store(vlse_ptr + lse_offsets, lse)
|
|
|
|
# shape = [D]
|
|
output_offsets = (
|
|
batch_idx * outputs_stride_B
|
|
+ head_idx * outputs_stride_H
|
|
+ d_offsets * outputs_stride_D
|
|
)
|
|
|
|
# correct output
|
|
lse_offset = (
|
|
lse_idx * lses_stride_N
|
|
+ batch_idx * lses_stride_B
|
|
+ head_idx * lses_stride_H
|
|
)
|
|
lse_tmp = tl.load(lses_ptr + lse_offset).to(tl.float32)
|
|
lse_finally = lse_tmp - lse
|
|
lse_finally = tl.where(
|
|
(lse_finally != lse_finally) | (lse_finally == float("inf")),
|
|
-float("inf"),
|
|
lse_finally,
|
|
)
|
|
factor = tl.exp(lse_finally) if IS_BASE_E else tl.exp2(lse_finally)
|
|
output = tl.load(outputs_ptr + output_offsets)
|
|
output = output * factor
|
|
output = tl.where(factor == 0.0, 0.0, output)
|
|
|
|
tl.store(new_output_ptr + output_offsets, output)
|
|
|
|
def dispatch( # type: ignore[override]
|
|
self,
|
|
*,
|
|
output_dtype: torch.dtype,
|
|
lse_dtype: torch.dtype,
|
|
num_tokens: int,
|
|
num_heads: int,
|
|
head_dim: int,
|
|
n_rounded: int,
|
|
lse_idx: int,
|
|
is_base_e: bool,
|
|
) -> CompileKey:
|
|
# Warm the contiguous [B, H, D] output and [N, B, H] LSE layouts used
|
|
# by the production collective path. Runtime still forwards real
|
|
# strides so unsupported layouts remain correct, but may compile on
|
|
# first use under the JIT monitor.
|
|
outputs_stride_d = 1
|
|
outputs_stride_h = head_dim
|
|
outputs_stride_b = num_heads * head_dim
|
|
lses_stride_h = 1
|
|
lses_stride_b = num_heads
|
|
lses_stride_n = num_tokens * num_heads
|
|
return self.CompileKey(
|
|
output_dtype=output_dtype,
|
|
lse_dtype=lse_dtype,
|
|
outputs_stride_b=triton_scalar_specialization_rep(outputs_stride_b),
|
|
outputs_stride_h=triton_scalar_specialization_rep(outputs_stride_h),
|
|
outputs_stride_d=triton_scalar_specialization_rep(outputs_stride_d),
|
|
lses_stride_n=triton_scalar_specialization_rep(lses_stride_n),
|
|
lses_stride_b=triton_scalar_specialization_rep(lses_stride_b),
|
|
lses_stride_h=triton_scalar_specialization_rep(lses_stride_h),
|
|
lse_idx=triton_scalar_specialization_rep(lse_idx),
|
|
head_dim=head_dim,
|
|
n_rounded=n_rounded,
|
|
is_base_e=is_base_e,
|
|
)
|
|
|
|
def get_warmup_keys(
|
|
self,
|
|
vllm_config: Any,
|
|
*,
|
|
output_dtype: torch.dtype | None = None,
|
|
num_heads: int | None = None,
|
|
head_dim: int | None = None,
|
|
is_base_e: bool | tuple[bool, ...] = (False, True),
|
|
) -> list[CompileKey]:
|
|
from vllm.model_executor.layers.attention.mla_attention import (
|
|
get_mla_dims,
|
|
)
|
|
|
|
dcp_world_size = vllm_config.parallel_config.decode_context_parallel_size
|
|
max_tokens = min(
|
|
16,
|
|
vllm_config.scheduler_config.max_num_batched_tokens,
|
|
)
|
|
hf_config = vllm_config.model_config.hf_config
|
|
if num_heads is None:
|
|
num_heads = (
|
|
hf_config.num_attention_heads
|
|
// vllm_config.parallel_config.tensor_parallel_size
|
|
)
|
|
if head_dim is None:
|
|
head_dim = get_mla_dims(vllm_config.model_config).v_head_dim
|
|
if output_dtype is None:
|
|
output_dtype = vllm_config.model_config.dtype
|
|
if dcp_world_size <= 1 or max_tokens <= 0 or num_heads <= 0:
|
|
return []
|
|
return self._trace_dispatch(self.dispatch)(
|
|
output_dtype=output_dtype,
|
|
lse_dtype=torch.float32,
|
|
num_tokens=WarmupIntRange(1, max_tokens + 1),
|
|
num_heads=num_heads,
|
|
head_dim=head_dim,
|
|
n_rounded=dcp_world_size,
|
|
lse_idx=WarmupIntRange(0, dcp_world_size),
|
|
is_base_e=is_base_e,
|
|
)
|
|
|
|
def warmup_inputs(self, compile_key: CompileKey) -> dict[str, Any]:
|
|
output_ptr = TritonWarmupTensor(
|
|
compile_key.output_dtype,
|
|
shape=(1, 1, compile_key.head_dim),
|
|
strides=(
|
|
compile_key.outputs_stride_b,
|
|
compile_key.outputs_stride_h,
|
|
compile_key.outputs_stride_d,
|
|
),
|
|
)
|
|
lse_ptr = TritonWarmupTensor(
|
|
compile_key.lse_dtype,
|
|
shape=(compile_key.n_rounded, 1, 1),
|
|
strides=(
|
|
compile_key.lses_stride_n,
|
|
compile_key.lses_stride_b,
|
|
compile_key.lses_stride_h,
|
|
),
|
|
)
|
|
return dict(
|
|
outputs=output_ptr,
|
|
new_output=output_ptr,
|
|
lses=lse_ptr,
|
|
vlse=lse_ptr,
|
|
lse_idx=compile_key.lse_idx,
|
|
ctx=None,
|
|
is_base_e=compile_key.is_base_e,
|
|
)
|
|
|
|
@kernel_launcher
|
|
def __call__(
|
|
self,
|
|
outputs: torch.Tensor,
|
|
new_output: torch.Tensor,
|
|
lses: torch.Tensor,
|
|
vlse: torch.Tensor,
|
|
lse_idx: int,
|
|
ctx: Any,
|
|
*,
|
|
is_base_e: bool,
|
|
) -> LaunchSpec:
|
|
num_tokens, num_heads, head_dim = outputs.shape
|
|
n_rounded = lses.shape[0]
|
|
outputs_stride_b, outputs_stride_h, outputs_stride_d = outputs.stride()
|
|
lses_stride_n, lses_stride_b, lses_stride_h = lses.stride()
|
|
grid = (num_tokens, num_heads, 1)
|
|
return grid, dict(
|
|
outputs_stride_B=outputs_stride_b,
|
|
outputs_stride_H=outputs_stride_h,
|
|
outputs_stride_D=outputs_stride_d,
|
|
lses_stride_N=lses_stride_n,
|
|
lses_stride_B=lses_stride_b,
|
|
lses_stride_H=lses_stride_h,
|
|
HEAD_DIM=head_dim,
|
|
N_ROUNDED=n_rounded,
|
|
IS_BASE_E=is_base_e,
|
|
_runtime_launcher=None if self._warming else ctx.call_kernel,
|
|
# CPTritonContext caches the non-constexpr positional prefix; derive
|
|
# its length so adding/reordering a kernel arg cannot silently
|
|
# misalign the cached replay path.
|
|
_runtime_launcher_arg_count=len(self.kernel.arg_names)
|
|
- len(self.kernel.constexprs),
|
|
)
|
|
|
|
|
|
class CPTritonContext:
|
|
"""The CPTritonContext is used to avoid recompilation of the Triton JIT."""
|
|
|
|
def __init__(self):
|
|
self.inner_kernel = None
|
|
|
|
def call_kernel(self, kernel, grid, *regular_args, **const_args):
|
|
if self.inner_kernel is None:
|
|
self.inner_kernel = kernel[grid](*regular_args, **const_args)
|
|
else:
|
|
self.inner_kernel[grid](*regular_args)
|
|
|
|
|
|
def correct_attn_out(
|
|
out: torch.Tensor,
|
|
lses: torch.Tensor,
|
|
cp_rank: int,
|
|
ctx: CPTritonContext,
|
|
is_lse_base_on_e: bool = True,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
"""Correct the attention output using the all-gathered lses.
|
|
|
|
Args:
|
|
out: Tensor of shape [ B, H, D ]
|
|
lses: Tensor of shape [ N, B, H ]
|
|
cp_rank: Current rank in the context-parallel group
|
|
ctx: Triton context to avoid recompilation
|
|
is_lse_base_on_e: Whether the lses use base e rather than base 2
|
|
|
|
Returns:
|
|
Tuple of (out, lse) with corrected attention and final log-sum-exp.
|
|
|
|
"""
|
|
if ctx is None:
|
|
ctx = CPTritonContext()
|
|
|
|
# --- Normalize to 3D views ---
|
|
if out.ndim == 4 and out.shape[1] == 1:
|
|
out = out.squeeze(1)
|
|
assert out.ndim == 3, f"expected out [B,H,D] or [B,1,H,D], got {tuple(out.shape)}"
|
|
|
|
if lses.ndim == 4 and lses.shape[-1] == 1:
|
|
lses = lses.squeeze(-1)
|
|
if lses.ndim == 4 and lses.shape[1] == 1:
|
|
lses = lses.squeeze(1)
|
|
assert lses.ndim == 3, (
|
|
f"expected lses [N,B,H] (optionally with a 1-sized extra dim), "
|
|
f"got {tuple(lses.shape)}"
|
|
)
|
|
|
|
B, H, _ = out.shape
|
|
|
|
# Strides after we normalized shapes to 3-D views. The kernel computes
|
|
# offsets for `vlse_ptr` using lses_stride_B/H, so the output buffer must
|
|
# have the same B/H stride layout as a slice of `lses`.
|
|
_, l_sB, l_sH = lses.stride()
|
|
|
|
# Allocate LSE with the same B/H strides as `lses` so writes land correctly
|
|
# even when `lses` is a non-contiguous view (e.g., 4-D to 3-D squeeze).
|
|
lse = torch.empty_strided(
|
|
(B, H), (l_sB, l_sH), device=lses.device, dtype=lses.dtype
|
|
)
|
|
|
|
_CORRECT_ATTN_CP_OUT_KERNEL(
|
|
out,
|
|
out,
|
|
lses,
|
|
lse,
|
|
cp_rank,
|
|
ctx,
|
|
is_base_e=is_lse_base_on_e,
|
|
)
|
|
return out, lse
|
|
|
|
|
|
def _cp_lse_common(
|
|
cp_attn_out: torch.Tensor,
|
|
cp_attn_lse: torch.Tensor,
|
|
cp_group: GroupCoordinator,
|
|
ctx: CPTritonContext | None = None,
|
|
is_lse_base_on_e=True,
|
|
seq_lens: torch.Tensor | None = None,
|
|
query_start_loc: torch.Tensor | None = None,
|
|
):
|
|
"""cp_attn_out: [ B, H, D ]
|
|
cp_attn_lse: [ B, H ]
|
|
"""
|
|
if cp_group.world_size == 1:
|
|
return cp_attn_out
|
|
|
|
if ctx is None:
|
|
ctx = CPTritonContext()
|
|
|
|
cp_attn_lse = cp_attn_lse.contiguous()
|
|
mask_dcp_empty_shards_(cp_attn_lse, seq_lens, query_start_loc)
|
|
lses = cp_group.all_gather(cp_attn_lse, dim=0).reshape(
|
|
(cp_group.world_size,) + cp_attn_lse.shape
|
|
)
|
|
out, lse = correct_attn_out(
|
|
cp_attn_out,
|
|
lses,
|
|
cp_group.rank_in_group,
|
|
ctx,
|
|
is_lse_base_on_e=is_lse_base_on_e,
|
|
)
|
|
return out, lse
|
|
|
|
|
|
def cp_lse_ag_out_rs(
|
|
cp_attn_out: torch.Tensor,
|
|
cp_attn_lse: torch.Tensor,
|
|
cp_group: GroupCoordinator,
|
|
ctx: CPTritonContext | None = None,
|
|
return_lse: bool = False,
|
|
is_lse_base_on_e=True,
|
|
seq_lens: torch.Tensor | None = None,
|
|
query_start_loc: torch.Tensor | None = None,
|
|
):
|
|
"""cp_attn_out: [ B, H, D ]
|
|
cp_attn_lse: [ B, H ]
|
|
"""
|
|
out, lse = _cp_lse_common(
|
|
cp_attn_out,
|
|
cp_attn_lse,
|
|
cp_group,
|
|
ctx=ctx,
|
|
is_lse_base_on_e=is_lse_base_on_e,
|
|
seq_lens=seq_lens,
|
|
query_start_loc=query_start_loc,
|
|
)
|
|
out = cp_group.reduce_scatter(out, dim=1)
|
|
|
|
if return_lse:
|
|
cp_num_heads = lse.shape[1] // cp_group.world_size
|
|
cp_rank = cp_group.rank_in_group
|
|
lse = lse[:, cp_num_heads * cp_rank : cp_num_heads * (cp_rank + 1)]
|
|
return out, lse
|
|
return out
|
|
|
|
|
|
def cp_lse_ag_out_ar(
|
|
cp_attn_out: torch.Tensor,
|
|
cp_attn_lse: torch.Tensor,
|
|
cp_group: GroupCoordinator,
|
|
ctx: CPTritonContext | None = None,
|
|
return_lse: bool = False,
|
|
is_lse_base_on_e=True,
|
|
seq_lens: torch.Tensor | None = None,
|
|
query_start_loc: torch.Tensor | None = None,
|
|
):
|
|
"""cp_attn_out: [ B, H, D ]
|
|
cp_attn_lse: [ B, H ]
|
|
"""
|
|
out, lse = _cp_lse_common(
|
|
cp_attn_out,
|
|
cp_attn_lse,
|
|
cp_group,
|
|
ctx=ctx,
|
|
is_lse_base_on_e=is_lse_base_on_e,
|
|
seq_lens=seq_lens,
|
|
query_start_loc=query_start_loc,
|
|
)
|
|
out = cp_group.all_reduce(out)
|
|
|
|
if return_lse:
|
|
return out, lse
|
|
return out
|
|
|
|
|
|
# Standard A2A implementation
|
|
|
|
|
|
def _lse_weighted_combine(
|
|
outputs: torch.Tensor,
|
|
lses: torch.Tensor,
|
|
return_lse: bool = False,
|
|
is_lse_base_on_e: bool = True,
|
|
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
|
"""CPU reference implementation for LSE-weighted combination.
|
|
|
|
This is a pure PyTorch implementation used for testing and validation.
|
|
|
|
Args:
|
|
outputs: Partial attention outputs [N, B, H, D]
|
|
N = number of KV shards (ranks)
|
|
B = batch size (num_tokens)
|
|
H = number of heads per rank
|
|
D = head dimension
|
|
lses: Log-sum-exp values [N, B, H]
|
|
return_lse: If True, also return the global LSE
|
|
is_lse_base_on_e: If True, LSE is base e; if False, base 2
|
|
|
|
Returns:
|
|
Combined output [B, H, D], and optionally global LSE [B, H]
|
|
|
|
"""
|
|
N, B, H, D = outputs.shape
|
|
|
|
# Handle NaN and inf in LSEs
|
|
lses = torch.where(
|
|
torch.isnan(lses) | torch.isinf(lses),
|
|
torch.tensor(float("-inf"), device=lses.device, dtype=lses.dtype),
|
|
lses,
|
|
)
|
|
|
|
# Compute max LSE for numerical stability
|
|
lse_max, _ = lses.max(dim=0) # [B, H]
|
|
lse_max = torch.where(
|
|
lse_max == float("-inf"),
|
|
torch.zeros_like(lse_max),
|
|
lse_max,
|
|
)
|
|
|
|
# Compute weights: softmax over the N dimension
|
|
if is_lse_base_on_e:
|
|
weights = torch.exp(lses - lse_max.unsqueeze(0)) # [N, B, H]
|
|
else:
|
|
weights = torch.pow(2.0, lses - lse_max.unsqueeze(0)) # [N, B, H]
|
|
|
|
# Handle NaN weights
|
|
weights = torch.where(torch.isnan(weights), torch.zeros_like(weights), weights)
|
|
|
|
# Normalize weights
|
|
weight_sum = weights.sum(dim=0, keepdim=True) # [1, B, H]
|
|
weights = weights / weight_sum.clamp(min=1e-10) # [N, B, H]
|
|
|
|
# Weighted combination: sum over N dimension
|
|
weights = weights.unsqueeze(-1)
|
|
outputs = torch.where(weights == 0, torch.zeros_like(outputs), outputs)
|
|
result = (outputs * weights).sum(dim=0) # [B, H, D]
|
|
|
|
if return_lse:
|
|
if is_lse_base_on_e:
|
|
global_lse = torch.log(weight_sum.squeeze(0)) + lse_max # [B, H]
|
|
else:
|
|
global_lse = torch.log2(weight_sum.squeeze(0)) + lse_max # [B, H]
|
|
return result, global_lse
|
|
|
|
return result
|
|
|
|
|
|
def _dcp_a2a_lse_pack_dim(output_dtype: torch.dtype) -> int:
|
|
bits = torch.finfo(output_dtype).bits
|
|
if bits == 16:
|
|
return 2
|
|
if bits == 32:
|
|
return 1
|
|
raise ValueError(f"Cannot pack fp32 LSE into output dtype {output_dtype}.")
|
|
|
|
|
|
def _dcp_a2a_send_recv_buffers(
|
|
shape: tuple[int, ...],
|
|
device: torch.device,
|
|
dtype: torch.dtype,
|
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
# Don't use the shared WorkspaceManager here. A FULL cudagraph bakes in the
|
|
# buffer address at capture, but the workspace is growable and sized only to
|
|
# the largest *captured* batch (the cudagraph capture cap). Any eager a2a
|
|
# with a bigger batch regrows it, freeing that address and poisoning every
|
|
# captured graph -> illegal memory access on replay. This bites the very
|
|
# first request: the post-capture warmup runs an eager decode at
|
|
# max_num_seqs (> the cap), so the graphs are already dangling before the
|
|
# server is ready. torch.empty buffers instead live in the graph's private
|
|
# pool and stay valid for its lifetime (as _dcp_a2a_unpack_combine and the
|
|
# AG+RS combine path already rely on).
|
|
return (
|
|
torch.empty(shape, device=device, dtype=dtype),
|
|
torch.empty(shape, device=device, dtype=dtype),
|
|
)
|
|
|
|
|
|
@triton.jit
|
|
def _dcp_a2a_pack_send_kernel(
|
|
out_ptr,
|
|
lse_ptr,
|
|
send_ptr,
|
|
seq_lens_ptr,
|
|
query_start_loc_ptr,
|
|
num_seqs,
|
|
out_stride_B,
|
|
out_stride_H,
|
|
out_stride_D,
|
|
lse_stride_B,
|
|
lse_stride_H,
|
|
send_stride_N,
|
|
send_stride_B,
|
|
send_stride_H,
|
|
send_stride_D,
|
|
N: tl.constexpr,
|
|
HEAD_DIM: tl.constexpr,
|
|
H_PER_RANK: tl.constexpr,
|
|
LSE_PACK_DIM: tl.constexpr,
|
|
HAS_MASK: tl.constexpr,
|
|
NUM_SEQS_POW2: tl.constexpr,
|
|
):
|
|
batch_idx = tl.program_id(0).to(tl.int64)
|
|
local_head_idx = tl.program_id(1).to(tl.int64)
|
|
d_offsets = tl.arange(0, HEAD_DIM)
|
|
|
|
# Empty-shard masking, fused in to avoid a separate per-layer pass over the
|
|
# LSE. Rows that belong to a request with no local KV shard, and rows past
|
|
# the end of the batch (CUDA-graph padding), must not contribute to the
|
|
# combine, which the reduction achieves by giving them an LSE of -inf.
|
|
row_is_empty = False
|
|
if HAS_MASK:
|
|
# searchsorted(query_start_loc[1:], batch_idx, right=True), i.e. the
|
|
# number of request boundaries at or below this row.
|
|
seq_offsets = tl.arange(0, NUM_SEQS_POW2)
|
|
boundaries = tl.load(
|
|
query_start_loc_ptr + 1 + seq_offsets,
|
|
mask=seq_offsets < num_seqs,
|
|
other=2**31 - 1,
|
|
)
|
|
seq_idx = tl.sum((boundaries <= batch_idx).to(tl.int32), axis=0)
|
|
seq_idx = tl.minimum(seq_idx, num_seqs - 1)
|
|
num_rows = tl.load(query_start_loc_ptr + num_seqs)
|
|
seq_len = tl.load(seq_lens_ptr + seq_idx)
|
|
row_is_empty = (batch_idx >= num_rows) | (seq_len == 0)
|
|
|
|
for rank_idx in tl.static_range(N):
|
|
src_head_idx = rank_idx * H_PER_RANK + local_head_idx
|
|
send_base = (
|
|
rank_idx * send_stride_N
|
|
+ batch_idx * send_stride_B
|
|
+ local_head_idx * send_stride_H
|
|
)
|
|
|
|
out_offsets = (
|
|
batch_idx * out_stride_B
|
|
+ src_head_idx * out_stride_H
|
|
+ d_offsets * out_stride_D
|
|
)
|
|
tl.store(
|
|
send_ptr + send_base + d_offsets * send_stride_D,
|
|
tl.load(out_ptr + out_offsets),
|
|
)
|
|
|
|
lse_val = tl.load(
|
|
lse_ptr + batch_idx * lse_stride_B + src_head_idx * lse_stride_H
|
|
).to(tl.float32)
|
|
if HAS_MASK:
|
|
lse_val = tl.where(row_is_empty, -float("inf"), lse_val)
|
|
if LSE_PACK_DIM == 1:
|
|
tl.store(
|
|
send_ptr + send_base + HEAD_DIM * send_stride_D,
|
|
lse_val.to(send_ptr.dtype.element_ty),
|
|
)
|
|
else:
|
|
lse_bits = lse_val.to(tl.uint32, bitcast=True)
|
|
lo = (lse_bits & 0xFFFF).to(tl.uint16)
|
|
hi = ((lse_bits >> 16) & 0xFFFF).to(tl.uint16)
|
|
tl.store(
|
|
send_ptr + send_base + HEAD_DIM * send_stride_D,
|
|
lo.to(send_ptr.dtype.element_ty, bitcast=True),
|
|
)
|
|
tl.store(
|
|
send_ptr + send_base + (HEAD_DIM + 1) * send_stride_D,
|
|
hi.to(send_ptr.dtype.element_ty, bitcast=True),
|
|
)
|
|
|
|
|
|
@triton.jit
|
|
def _dcp_a2a_unpack_combine_kernel(
|
|
recv_ptr,
|
|
out_ptr,
|
|
out_lse_ptr,
|
|
recv_stride_N,
|
|
recv_stride_B,
|
|
recv_stride_H,
|
|
recv_stride_D,
|
|
out_stride_B,
|
|
out_stride_H,
|
|
out_stride_D,
|
|
out_lse_stride_B,
|
|
out_lse_stride_H,
|
|
N: tl.constexpr,
|
|
HEAD_DIM: tl.constexpr,
|
|
IS_BASE_E: tl.constexpr,
|
|
RETURN_LSE: tl.constexpr,
|
|
LSE_PACK_DIM: tl.constexpr,
|
|
):
|
|
batch_idx = tl.program_id(0).to(tl.int64)
|
|
head_idx = tl.program_id(1).to(tl.int64)
|
|
d_offsets = tl.arange(0, HEAD_DIM)
|
|
|
|
lse_max = -float("inf")
|
|
for rank_idx in tl.static_range(N):
|
|
recv_base = (
|
|
rank_idx * recv_stride_N
|
|
+ batch_idx * recv_stride_B
|
|
+ head_idx * recv_stride_H
|
|
)
|
|
if LSE_PACK_DIM == 1:
|
|
lse_val = tl.load(recv_ptr + recv_base + HEAD_DIM * recv_stride_D).to(
|
|
tl.float32
|
|
)
|
|
else:
|
|
lo_raw = tl.load(recv_ptr + recv_base + HEAD_DIM * recv_stride_D)
|
|
hi_raw = tl.load(recv_ptr + recv_base + (HEAD_DIM + 1) * recv_stride_D)
|
|
lo = lo_raw.to(tl.uint16, bitcast=True).to(tl.uint32)
|
|
hi = hi_raw.to(tl.uint16, bitcast=True).to(tl.uint32)
|
|
lse_val = (lo | (hi << 16)).to(tl.float32, bitcast=True)
|
|
lse_val = tl.where(
|
|
(lse_val != lse_val) | (lse_val == float("inf")),
|
|
-float("inf"),
|
|
lse_val,
|
|
)
|
|
lse_max = tl.maximum(lse_max, lse_val)
|
|
|
|
lse_max = tl.where(lse_max == -float("inf"), 0.0, lse_max)
|
|
|
|
lse_sum = 0.0
|
|
for rank_idx in tl.static_range(N):
|
|
recv_base = (
|
|
rank_idx * recv_stride_N
|
|
+ batch_idx * recv_stride_B
|
|
+ head_idx * recv_stride_H
|
|
)
|
|
if LSE_PACK_DIM == 1:
|
|
lse_val = tl.load(recv_ptr + recv_base + HEAD_DIM * recv_stride_D).to(
|
|
tl.float32
|
|
)
|
|
else:
|
|
lo_raw = tl.load(recv_ptr + recv_base + HEAD_DIM * recv_stride_D)
|
|
hi_raw = tl.load(recv_ptr + recv_base + (HEAD_DIM + 1) * recv_stride_D)
|
|
lo = lo_raw.to(tl.uint16, bitcast=True).to(tl.uint32)
|
|
hi = hi_raw.to(tl.uint16, bitcast=True).to(tl.uint32)
|
|
lse_val = (lo | (hi << 16)).to(tl.float32, bitcast=True)
|
|
lse_val = tl.where(
|
|
(lse_val != lse_val) | (lse_val == float("inf")),
|
|
-float("inf"),
|
|
lse_val,
|
|
)
|
|
if IS_BASE_E:
|
|
lse_sum += tl.exp(lse_val - lse_max)
|
|
else:
|
|
lse_sum += tl.exp2(lse_val - lse_max)
|
|
|
|
if IS_BASE_E: # noqa: SIM108
|
|
global_lse = tl.log(lse_sum) + lse_max
|
|
else:
|
|
global_lse = tl.log2(lse_sum) + lse_max
|
|
|
|
acc = tl.zeros([HEAD_DIM], dtype=tl.float32)
|
|
for rank_idx in tl.static_range(N):
|
|
recv_base = (
|
|
rank_idx * recv_stride_N
|
|
+ batch_idx * recv_stride_B
|
|
+ head_idx * recv_stride_H
|
|
)
|
|
if LSE_PACK_DIM == 1:
|
|
lse_val = tl.load(recv_ptr + recv_base + HEAD_DIM * recv_stride_D).to(
|
|
tl.float32
|
|
)
|
|
else:
|
|
lo_raw = tl.load(recv_ptr + recv_base + HEAD_DIM * recv_stride_D)
|
|
hi_raw = tl.load(recv_ptr + recv_base + (HEAD_DIM + 1) * recv_stride_D)
|
|
lo = lo_raw.to(tl.uint16, bitcast=True).to(tl.uint32)
|
|
hi = hi_raw.to(tl.uint16, bitcast=True).to(tl.uint32)
|
|
lse_val = (lo | (hi << 16)).to(tl.float32, bitcast=True)
|
|
lse_val = tl.where(
|
|
(lse_val != lse_val) | (lse_val == float("inf")),
|
|
-float("inf"),
|
|
lse_val,
|
|
)
|
|
if IS_BASE_E:
|
|
weight = tl.exp(lse_val - global_lse)
|
|
else:
|
|
weight = tl.exp2(lse_val - global_lse)
|
|
weight = tl.where(weight != weight, 0.0, weight)
|
|
partial = tl.load(recv_ptr + recv_base + d_offsets * recv_stride_D).to(
|
|
tl.float32
|
|
)
|
|
partial = tl.where(weight == 0.0, 0.0, partial)
|
|
acc += partial * weight
|
|
|
|
final_offsets = (
|
|
batch_idx * out_stride_B + head_idx * out_stride_H + d_offsets * out_stride_D
|
|
)
|
|
tl.store(out_ptr + final_offsets, acc)
|
|
|
|
if RETURN_LSE:
|
|
out_lse_offset = batch_idx * out_lse_stride_B + head_idx * out_lse_stride_H
|
|
tl.store(out_lse_ptr + out_lse_offset, global_lse)
|
|
|
|
|
|
def _dcp_a2a_pack_send(
|
|
cp_attn_out: torch.Tensor,
|
|
cp_attn_lse: torch.Tensor,
|
|
send_buffer: torch.Tensor,
|
|
world_size: int,
|
|
h_per_rank: int,
|
|
head_dim: int,
|
|
lse_pack_dim: int,
|
|
seq_lens: torch.Tensor | None = None,
|
|
query_start_loc: torch.Tensor | None = None,
|
|
) -> None:
|
|
# The empty-shard mask is fused into the pack kernel below rather than
|
|
# applied as a separate pass: the mask is a pure function of seq_lens and
|
|
# query_start_loc, which are identical for every MLA layer in a step, while
|
|
# the eager form costs eight launch-bound kernels per layer.
|
|
has_mask = _validate_dcp_empty_shard_args(seq_lens, query_start_loc)
|
|
num_seqs = seq_lens.shape[0] if has_mask and seq_lens is not None else 0
|
|
|
|
grid = (cp_attn_out.shape[0], h_per_rank, 1)
|
|
_dcp_a2a_pack_send_kernel[grid](
|
|
cp_attn_out,
|
|
cp_attn_lse,
|
|
send_buffer,
|
|
seq_lens,
|
|
query_start_loc,
|
|
num_seqs,
|
|
cp_attn_out.stride(0),
|
|
cp_attn_out.stride(1),
|
|
cp_attn_out.stride(2),
|
|
cp_attn_lse.stride(0),
|
|
cp_attn_lse.stride(1),
|
|
send_buffer.stride(0),
|
|
send_buffer.stride(1),
|
|
send_buffer.stride(2),
|
|
send_buffer.stride(3),
|
|
N=world_size,
|
|
HEAD_DIM=head_dim,
|
|
H_PER_RANK=h_per_rank,
|
|
LSE_PACK_DIM=lse_pack_dim,
|
|
HAS_MASK=has_mask,
|
|
# Only the arange bound is a constexpr: num_seqs itself is a runtime
|
|
# argument so the kernel specializes on at most log2(max_num_seqs)
|
|
# variants instead of one per distinct batch size.
|
|
NUM_SEQS_POW2=triton.next_power_of_2(max(num_seqs, 1)),
|
|
)
|
|
|
|
|
|
def _dcp_a2a_unpack_combine(
|
|
recv_buffer: torch.Tensor,
|
|
head_dim: int,
|
|
lse_pack_dim: int,
|
|
return_lse: bool,
|
|
is_lse_base_on_e: bool,
|
|
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
|
world_size, num_tokens, h_per_rank, _ = recv_buffer.shape
|
|
out = torch.empty(
|
|
(num_tokens, h_per_rank, head_dim),
|
|
device=recv_buffer.device,
|
|
dtype=recv_buffer.dtype,
|
|
)
|
|
out_lse = torch.empty(
|
|
(num_tokens, h_per_rank) if return_lse else (1, 1),
|
|
device=recv_buffer.device,
|
|
dtype=torch.float32 if return_lse else recv_buffer.dtype,
|
|
)
|
|
grid = (num_tokens, h_per_rank, 1)
|
|
_dcp_a2a_unpack_combine_kernel[grid](
|
|
recv_buffer,
|
|
out,
|
|
out_lse,
|
|
recv_buffer.stride(0),
|
|
recv_buffer.stride(1),
|
|
recv_buffer.stride(2),
|
|
recv_buffer.stride(3),
|
|
out.stride(0),
|
|
out.stride(1),
|
|
out.stride(2),
|
|
out_lse.stride(0),
|
|
out_lse.stride(1),
|
|
N=world_size,
|
|
HEAD_DIM=head_dim,
|
|
IS_BASE_E=is_lse_base_on_e,
|
|
RETURN_LSE=return_lse,
|
|
LSE_PACK_DIM=lse_pack_dim,
|
|
)
|
|
if return_lse:
|
|
return out, out_lse
|
|
return out
|
|
|
|
|
|
def dcp_a2a_lse_reduce(
|
|
cp_attn_out: torch.Tensor,
|
|
cp_attn_lse: torch.Tensor,
|
|
cp_group: GroupCoordinator,
|
|
ctx: CPTritonContext | None = None,
|
|
return_lse: bool = False,
|
|
is_lse_base_on_e: bool = True,
|
|
seq_lens: torch.Tensor | None = None,
|
|
query_start_loc: torch.Tensor | None = None,
|
|
) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
|
|
"""Combine partial attention outputs across DCP ranks using All-to-All.
|
|
|
|
The output and LSE are packed into a single output-dtype buffer, sent
|
|
with one All-to-All, then unpacked and combined with exact LSE weighting.
|
|
|
|
Args:
|
|
cp_attn_out: [B, H, D] where B=num_tokens, H=total_heads, D=head_dim
|
|
cp_attn_lse: [B, H] floating-point log-sum-exp values
|
|
cp_group: GroupCoordinator for DCP communication
|
|
ctx: CPTritonContext (unused, for signature compatibility)
|
|
return_lse: If True, also return the combined global LSE
|
|
is_lse_base_on_e: If True, LSE is base e; if False, base 2
|
|
seq_lens: Local KV lengths. Empty shards contribute zero weight.
|
|
query_start_loc: Cumulative query-token offsets for each request.
|
|
|
|
Returns:
|
|
Combined output [B, H/N, D] (head-scattered)
|
|
If return_lse=True, also returns global_lse [B, H/N]
|
|
|
|
"""
|
|
world_size = cp_group.world_size
|
|
|
|
if world_size == 1:
|
|
if return_lse:
|
|
return cp_attn_out, cp_attn_lse
|
|
return cp_attn_out
|
|
|
|
B, H, D = cp_attn_out.shape
|
|
if H % world_size != 0:
|
|
raise ValueError(f"H={H} must be divisible by DCP world size {world_size}.")
|
|
H_per_rank = H // world_size
|
|
lse_pack_dim = _dcp_a2a_lse_pack_dim(cp_attn_out.dtype)
|
|
|
|
send_buffer, recv_buffer = _dcp_a2a_send_recv_buffers(
|
|
(world_size, B, H_per_rank, D + lse_pack_dim),
|
|
device=cp_attn_out.device,
|
|
dtype=cp_attn_out.dtype,
|
|
)
|
|
|
|
_dcp_a2a_pack_send(
|
|
cp_attn_out,
|
|
cp_attn_lse,
|
|
send_buffer,
|
|
world_size,
|
|
H_per_rank,
|
|
D,
|
|
lse_pack_dim,
|
|
seq_lens=seq_lens,
|
|
query_start_loc=query_start_loc,
|
|
)
|
|
|
|
work = dist.all_to_all_single(
|
|
recv_buffer.view(-1),
|
|
send_buffer.view(-1),
|
|
group=cp_group.device_group,
|
|
async_op=True,
|
|
)
|
|
work.wait()
|
|
|
|
return _dcp_a2a_unpack_combine(
|
|
recv_buffer, D, lse_pack_dim, return_lse, is_lse_base_on_e
|
|
)
|
|
|
|
|
|
def get_dcp_workspace_max_num_tokens(vllm_config: VllmConfig) -> int:
|
|
scheduler_config = vllm_config.scheduler_config
|
|
# num_speculative_tokens also counts a diffusion canvas.
|
|
tokens_per_seq = max(
|
|
1 + vllm_config.num_speculative_tokens, max_decode_query_len(vllm_config)
|
|
)
|
|
return min(
|
|
scheduler_config.max_num_batched_tokens,
|
|
max(
|
|
scheduler_config.max_num_seqs * tokens_per_seq,
|
|
vllm_config.compilation_config.max_cudagraph_capture_size or 0,
|
|
),
|
|
)
|
|
|
|
|
|
def reserve_query_head_storage(
|
|
query: torch.Tensor, padded_num_heads: int
|
|
) -> torch.Tensor:
|
|
"""Reserve backing storage for fixed-head decode kernels."""
|
|
assert query.ndim == 3
|
|
assert query.shape[1] <= padded_num_heads
|
|
padded = query.new_empty((query.shape[0], padded_num_heads, query.shape[2]))
|
|
padded.resize_(query.shape)
|
|
padded.copy_(query)
|
|
return padded
|
|
|
|
|
|
# Symmetric-memory A2A implementation
|
|
|
|
|
|
_A2A_SUPPORTED_DTYPES = (torch.float16, torch.bfloat16)
|
|
|
|
|
|
class DirectDCPA2AWorkspace(DirectCPWorkspace):
|
|
"""Persistent symmetric buffers for direct DCP output exchange."""
|
|
|
|
def __init__(
|
|
self,
|
|
group: ProcessGroup,
|
|
device: torch.device,
|
|
max_num_tokens: int,
|
|
heads_per_rank: int,
|
|
head_dim: int,
|
|
dtype: torch.dtype = torch.bfloat16,
|
|
num_ubatches: int = 1,
|
|
) -> None:
|
|
if dtype not in _A2A_SUPPORTED_DTYPES:
|
|
raise ValueError(f"Direct DCP A2A does not support {dtype}")
|
|
if num_ubatches < 1:
|
|
raise ValueError(
|
|
f"Direct DCP A2A requires at least one ubatch slot, got {num_ubatches}"
|
|
)
|
|
super().__init__(group, device, num_ubatches)
|
|
self.max_num_tokens = max_num_tokens
|
|
self.heads_per_rank = heads_per_rank
|
|
self.head_dim = head_dim
|
|
|
|
output_shape = (
|
|
num_ubatches,
|
|
2,
|
|
self.world_size,
|
|
max_num_tokens,
|
|
heads_per_rank,
|
|
head_dim,
|
|
)
|
|
lse_shape = (
|
|
num_ubatches,
|
|
2,
|
|
self.world_size,
|
|
max_num_tokens,
|
|
heads_per_rank,
|
|
)
|
|
signal_shape = (num_ubatches, 2, self.world_size)
|
|
self.received_output, self.peer_output_ptrs = self._allocate(
|
|
output_shape, dtype
|
|
)
|
|
self.received_lse, self.peer_lse_ptrs = self._allocate(lse_shape, torch.float32)
|
|
self.received_signal, self.peer_signal_ptrs = self._allocate(
|
|
signal_shape, torch.int32
|
|
)
|
|
|
|
def lse_reduce(
|
|
self,
|
|
partial_output: torch.Tensor,
|
|
partial_lse: torch.Tensor,
|
|
is_lse_base_on_e: bool,
|
|
seq_lens: torch.Tensor | None = None,
|
|
query_start_loc: torch.Tensor | None = None,
|
|
) -> torch.Tensor:
|
|
ubatch = dbo_current_ubatch_id()
|
|
num_tokens = partial_output.shape[0]
|
|
output = partial_output.new_empty(
|
|
(num_tokens, self.heads_per_rank, self.head_dim)
|
|
)
|
|
torch.ops._C.direct_dcp_a2a_lse_reduce(
|
|
partial_output,
|
|
partial_lse,
|
|
seq_lens,
|
|
query_start_loc,
|
|
self.peer_output_ptrs[ubatch],
|
|
self.peer_lse_ptrs[ubatch],
|
|
self.peer_signal_ptrs[ubatch],
|
|
self.received_output[ubatch],
|
|
self.received_lse[ubatch],
|
|
self.received_signal[ubatch],
|
|
self.epoch[ubatch : ubatch + 1],
|
|
output,
|
|
self.world_size,
|
|
self.rank,
|
|
self.max_num_tokens,
|
|
is_lse_base_on_e,
|
|
)
|
|
return output
|
|
|
|
|
|
@functools.cache
|
|
def get_direct_dcp_a2a_workspace(
|
|
group: GroupCoordinator,
|
|
device: torch.device,
|
|
max_num_tokens: int,
|
|
heads_per_rank: int,
|
|
head_dim: int,
|
|
dtype: torch.dtype,
|
|
num_ubatches: int,
|
|
) -> DirectDCPA2AWorkspace | None:
|
|
if not direct_cp_enabled(
|
|
group, dtype, envs.VLLM_USE_DIRECT_DCP_A2A, _A2A_SUPPORTED_DTYPES
|
|
):
|
|
return None
|
|
return DirectDCPA2AWorkspace(
|
|
group.device_group,
|
|
device,
|
|
max_num_tokens,
|
|
heads_per_rank,
|
|
head_dim,
|
|
dtype,
|
|
num_ubatches,
|
|
)
|
|
|
|
|
|
# Q gather
|
|
|
|
# Symmetric-memory implementation
|
|
|
|
|
|
def _q_gather_layout_supported(
|
|
world_size: int,
|
|
heads_per_rank: int,
|
|
head_dim: int,
|
|
dtype: torch.dtype,
|
|
padded_num_heads: int | None,
|
|
) -> bool:
|
|
element_size = torch.empty((), dtype=dtype).element_size()
|
|
gathered_num_heads = world_size * heads_per_rank
|
|
storage_num_heads = (
|
|
gathered_num_heads if padded_num_heads is None else padded_num_heads
|
|
)
|
|
return (
|
|
heads_per_rank * head_dim * element_size % 16 == 0
|
|
and storage_num_heads * head_dim * element_size % 16 == 0
|
|
)
|
|
|
|
|
|
class DirectDCPQGatherWorkspace(DirectCPWorkspace):
|
|
"""Publish query shards directly into the consumer-final symmetric buffer.
|
|
|
|
The final buffer is reusable after the downstream DCP output combine. That
|
|
combine orders all ranks after attention has consumed the gathered query.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
group: ProcessGroup,
|
|
device: torch.device,
|
|
max_num_tokens: int,
|
|
heads_per_rank: int,
|
|
head_dim: int,
|
|
dtype: torch.dtype = torch.bfloat16,
|
|
num_ubatches: int = 1,
|
|
padded_num_heads: int | None = None,
|
|
) -> None:
|
|
if num_ubatches < 1:
|
|
raise ValueError(
|
|
"Direct DCP q-gather requires at least one ubatch slot, "
|
|
f"got {num_ubatches}"
|
|
)
|
|
if max_num_tokens < 1 or heads_per_rank < 1 or head_dim < 1:
|
|
raise ValueError(
|
|
"Direct DCP q-gather dimensions must be positive, got "
|
|
f"T={max_num_tokens}, H={heads_per_rank}, D={head_dim}"
|
|
)
|
|
gathered_num_heads = group.size() * heads_per_rank
|
|
if not _q_gather_layout_supported(
|
|
group.size(), heads_per_rank, head_dim, dtype, padded_num_heads
|
|
):
|
|
raise ValueError("Direct DCP q-gather requires 16-byte-aligned query rows.")
|
|
super().__init__(group, device, num_ubatches)
|
|
if self.world_size <= 1:
|
|
raise ValueError("Direct DCP q-gather requires at least two ranks")
|
|
self.max_num_tokens = max_num_tokens
|
|
self.heads_per_rank = heads_per_rank
|
|
self.gathered_num_heads = gathered_num_heads
|
|
self.padded_num_heads = (
|
|
self.gathered_num_heads if padded_num_heads is None else padded_num_heads
|
|
)
|
|
if self.padded_num_heads < self.gathered_num_heads:
|
|
raise ValueError(
|
|
"Direct DCP q-gather padded heads must cover gathered heads: "
|
|
f"{self.padded_num_heads} < {self.gathered_num_heads}"
|
|
)
|
|
self.head_dim = head_dim
|
|
|
|
query_shape = (
|
|
num_ubatches,
|
|
max_num_tokens,
|
|
self.padded_num_heads,
|
|
head_dim,
|
|
)
|
|
signal_shape = (num_ubatches, 2, self.world_size)
|
|
self.final_query, _ = self._allocate(query_shape, dtype)
|
|
self.received_signal, _ = self._allocate(signal_shape, torch.int32)
|
|
query_multicast_ptrs = self._multicast_ptrs(self.final_query)
|
|
signal_multicast_ptrs = self._multicast_ptrs(self.received_signal)
|
|
self.multicast_ptrs = list(
|
|
zip(query_multicast_ptrs, signal_multicast_ptrs, strict=True)
|
|
)
|
|
if not all(
|
|
query_ptr and signal_ptr for query_ptr, signal_ptr in self.multicast_ptrs
|
|
):
|
|
raise RuntimeError(
|
|
"Direct DCP q-gather requires NVLS symmetric-memory multicast."
|
|
)
|
|
self.completion = self.received_signal.new_zeros((num_ubatches, 1))
|
|
torch.accelerator.synchronize()
|
|
|
|
def gather(self, local_query: torch.Tensor) -> torch.Tensor:
|
|
ubatch = dbo_current_ubatch_id()
|
|
if not 0 <= ubatch < self.num_ubatches:
|
|
raise ValueError(
|
|
f"DCP q-gather ubatch {ubatch} exceeds {self.num_ubatches} slots"
|
|
)
|
|
if local_query.ndim == 3 and local_query.shape[1] != self.heads_per_rank:
|
|
raise ValueError(
|
|
f"DCP q-gather expected {self.heads_per_rank} local query heads, "
|
|
f"got {local_query.shape[1]}"
|
|
)
|
|
|
|
num_tokens = local_query.shape[0]
|
|
output = torch.as_strided(
|
|
self.final_query[ubatch],
|
|
size=(num_tokens, self.gathered_num_heads, self.head_dim),
|
|
stride=(
|
|
self.gathered_num_heads * self.head_dim,
|
|
self.head_dim,
|
|
1,
|
|
),
|
|
)
|
|
query_multicast_ptr, signal_multicast_ptr = self.multicast_ptrs[ubatch]
|
|
torch.ops._C.direct_dcp_q_gather(
|
|
local_query,
|
|
output,
|
|
self.received_signal[ubatch],
|
|
self.completion[ubatch],
|
|
self.epoch[ubatch : ubatch + 1],
|
|
self.world_size,
|
|
self.rank,
|
|
self.max_num_tokens,
|
|
self.padded_num_heads,
|
|
query_multicast_ptr,
|
|
signal_multicast_ptr,
|
|
)
|
|
return output
|
|
|
|
|
|
@functools.cache
|
|
def get_direct_dcp_q_gather_workspace(
|
|
group: GroupCoordinator,
|
|
device: torch.device,
|
|
max_num_tokens: int,
|
|
heads_per_rank: int,
|
|
head_dim: int,
|
|
dtype: torch.dtype,
|
|
num_ubatches: int,
|
|
padded_num_heads: int | None = None,
|
|
) -> DirectDCPQGatherWorkspace | None:
|
|
if not direct_cp_multicast_enabled(group, dtype, envs.VLLM_USE_DIRECT_DCP_Q_GATHER):
|
|
return None
|
|
if not _q_gather_layout_supported(
|
|
group.world_size, heads_per_rank, head_dim, dtype, padded_num_heads
|
|
):
|
|
return None
|
|
return DirectDCPQGatherWorkspace(
|
|
group.device_group,
|
|
device,
|
|
max_num_tokens,
|
|
heads_per_rank,
|
|
head_dim,
|
|
dtype,
|
|
num_ubatches,
|
|
padded_num_heads,
|
|
)
|
|
|
|
|
|
# KV gather
|
|
|
|
# Symmetric-memory implementation
|
|
|
|
|
|
_KV_GATHER_SUPPORTED_DTYPES = (
|
|
torch.float16,
|
|
torch.bfloat16,
|
|
torch.float8_e4m3fn,
|
|
)
|
|
|
|
|
|
def _kv_gather_layout_supported(token_dim: int, dtype: torch.dtype) -> bool:
|
|
return token_dim * torch.empty((), dtype=dtype).element_size() % 16 == 0
|
|
|
|
|
|
class DirectDCPKVGatherWorkspace(DirectCPWorkspace):
|
|
"""Persistent symmetric buffers for direct DCP KV gather."""
|
|
|
|
def __init__(
|
|
self,
|
|
group: ProcessGroup,
|
|
device: torch.device,
|
|
max_gathered_tokens: int,
|
|
token_dim: int,
|
|
dtype: torch.dtype = torch.bfloat16,
|
|
num_ubatches: int = 1,
|
|
) -> None:
|
|
if dtype not in _KV_GATHER_SUPPORTED_DTYPES:
|
|
raise ValueError(f"Direct DCP kv-gather does not support {dtype}")
|
|
if num_ubatches < 1:
|
|
raise ValueError(
|
|
"Direct DCP kv-gather requires at least one ubatch slot, "
|
|
f"got {num_ubatches}"
|
|
)
|
|
if max_gathered_tokens < 1 or token_dim < 1:
|
|
raise ValueError(
|
|
"Direct DCP kv-gather dimensions must be positive, got "
|
|
f"T={max_gathered_tokens}, D={token_dim}"
|
|
)
|
|
if not _kv_gather_layout_supported(token_dim, dtype):
|
|
raise ValueError("Direct DCP kv-gather requires 16-byte-aligned KV rows.")
|
|
super().__init__(group, device, num_ubatches)
|
|
if self.world_size <= 1:
|
|
raise ValueError("Direct DCP kv-gather requires at least two ranks")
|
|
if max_gathered_tokens % self.world_size != 0:
|
|
raise ValueError(
|
|
"Direct DCP kv-gather capacity must divide evenly across "
|
|
f"ranks: {max_gathered_tokens} % {self.world_size} != 0"
|
|
)
|
|
self.max_gathered_tokens = max_gathered_tokens
|
|
|
|
kv_shape = (num_ubatches, 2, max_gathered_tokens, token_dim)
|
|
signal_shape = (num_ubatches, 2, self.world_size)
|
|
self.received_kv, _ = self._allocate(kv_shape, dtype)
|
|
self.received_signal, _ = self._allocate(signal_shape, torch.int32)
|
|
kv_multicast_ptrs = self._multicast_ptrs(self.received_kv)
|
|
signal_multicast_ptrs = self._multicast_ptrs(self.received_signal)
|
|
self.multicast_ptrs = list(
|
|
zip(kv_multicast_ptrs, signal_multicast_ptrs, strict=True)
|
|
)
|
|
if not all(kv_ptr and signal_ptr for kv_ptr, signal_ptr in self.multicast_ptrs):
|
|
raise RuntimeError(
|
|
"Direct DCP kv-gather requires NVLS symmetric-memory multicast."
|
|
)
|
|
self.completion = self.received_signal.new_zeros((num_ubatches, 2))
|
|
torch.accelerator.synchronize()
|
|
|
|
def gather(self, gathered_kv: torch.Tensor, local_kv: torch.Tensor) -> None:
|
|
ubatch = dbo_current_ubatch_id()
|
|
if not 0 <= ubatch < self.num_ubatches:
|
|
raise ValueError(
|
|
f"DCP kv-gather ubatch {ubatch} exceeds {self.num_ubatches} slots"
|
|
)
|
|
kv_multicast_ptr, signal_multicast_ptr = self.multicast_ptrs[ubatch]
|
|
torch.ops._C.direct_dcp_kv_gather(
|
|
local_kv,
|
|
self.received_kv[ubatch],
|
|
self.received_signal[ubatch],
|
|
self.completion[ubatch],
|
|
self.epoch[ubatch : ubatch + 1],
|
|
gathered_kv,
|
|
self.world_size,
|
|
self.rank,
|
|
self.max_gathered_tokens,
|
|
kv_multicast_ptr,
|
|
signal_multicast_ptr,
|
|
)
|
|
|
|
|
|
@functools.cache
|
|
def get_direct_dcp_kv_gather_workspace(
|
|
group: GroupCoordinator,
|
|
device: torch.device,
|
|
max_gathered_tokens: int,
|
|
token_dim: int,
|
|
dtype: torch.dtype,
|
|
num_ubatches: int,
|
|
) -> DirectDCPKVGatherWorkspace | None:
|
|
if not direct_cp_multicast_enabled(
|
|
group,
|
|
dtype,
|
|
envs.VLLM_USE_DIRECT_DCP_KV_GATHER,
|
|
_KV_GATHER_SUPPORTED_DTYPES,
|
|
):
|
|
return None
|
|
if not _kv_gather_layout_supported(token_dim, dtype):
|
|
return None
|
|
return DirectDCPKVGatherWorkspace(
|
|
group.device_group,
|
|
device,
|
|
max_gathered_tokens,
|
|
token_dim,
|
|
dtype,
|
|
num_ubatches,
|
|
)
|
|
|
|
|
|
# MLA DCP backend selection
|
|
|
|
|
|
class DCPCombine(Protocol):
|
|
def __call__(
|
|
self,
|
|
partial_output: torch.Tensor,
|
|
partial_lse: torch.Tensor,
|
|
*,
|
|
seq_lens: torch.Tensor,
|
|
query_start_loc: torch.Tensor,
|
|
) -> torch.Tensor: ...
|
|
|
|
|
|
_CORRECT_ATTN_CP_OUT_KERNEL = CorrectAttnCPOutKernel()
|