Files
enginex-ascend-910-vllm/vllm_ascend/worker/pcp_utils.py
Sun Ruoxi 7f8a1b1f7a init v0.23.0
Signed-off-by: Sun Ruoxi <sunruoxi@4paradigm.com>
2026-08-27 15:11:51 +08:00

2388 lines
111 KiB
Python

#
# Copyright (c) 2025 Huawei Technologies Co., Ltd. All Rights Reserved.
# Copyright 2023 The vLLM team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This file is a part of the vllm-ascend project.
# Adapted from vllm-project/vllm/vllm/worker/worker.py
#
import copy
import logging
from collections.abc import Callable
from dataclasses import dataclass
from itertools import accumulate
from typing import TYPE_CHECKING, Any
import numpy as np
import torch
import torch.nn.functional as F
from vllm.config import VllmConfig
from vllm.logger import logger
from vllm.utils import length_from_prompt_token_ids_or_embeds
from vllm.utils.math_utils import cdiv
from vllm.v1.utils import CpuGpuBuffer
from vllm_ascend.spec_decode.utils import correct_optimistic_seq_lens_cpu
from vllm_ascend.utils import is_pd_decode_recompute_scheduler_enabled
from vllm_ascend.worker.npu_input_batch import NPUInputBatch
from vllm_ascend.worker.utils import copy_snapshot_to_gpu
if TYPE_CHECKING:
from vllm.v1.core.sched.output import SchedulerOutput
@dataclass(frozen=True)
class PCPSpecDecodeMTPInputs:
"""Device-side PCP state needed by proposer MTP draft steps."""
seq_lens: torch.Tensor
seq_lens_cpu: torch.Tensor | None
slot_indices: torch.Tensor
slot_mapping: torch.Tensor
@dataclass(frozen=True)
class PCPSpecDecodeFirstPassInputs:
"""PCP-adjusted inputs for the first speculative draft pass."""
num_tokens: int
input_ids: torch.Tensor
target_positions: torch.Tensor
target_hidden_states: torch.Tensor
token_indices_to_sample: torch.Tensor
long_seq_args: tuple[torch.Tensor | None, torch.Tensor | None] | None
@dataclass(frozen=True)
class PCPAsyncSpecDecodeRebuildResult:
"""Status returned after trying to rebuild async spec decode CP inputs."""
rebuilt: bool
positions_ready_on_device: bool
class PCPManager:
"""
Manager for Prefill Context Parallelism (PCP) metadata and buffers.
This manager encapsulates all PCP-related buffers and logic so that the
ModelRunner can access them via `self.pcp_manager`.
"""
num_reqs: int = 0
num_decode_reqs: int = 0
num_prefill_reqs: int = 0
num_decode_tokens: int = 0
decode_req_mask: np.ndarray | None = None
def __init__(
self,
pcp_world_size: int,
pcp_rank: int,
dcp_world_size: int,
dcp_rank: int,
max_buffer_num_tokens: int,
max_num_reqs: int,
device: torch.device,
vllm_config: VllmConfig,
use_async_scheduling: bool,
pin_memory: bool = False,
use_sparse: bool = False,
) -> None:
self.pcp_world_size = pcp_world_size
self.pcp_world_rank = pcp_rank
self.dcp_world_size = dcp_world_size
self.dcp_world_rank = dcp_rank
self.speculative_config = vllm_config.speculative_config
self.decode_threshold = 1 + (self.speculative_config.num_speculative_tokens if self.speculative_config else 0)
self.pcp_spec_token_offsets = torch.arange(
max(self.decode_threshold - 1, 1),
dtype=torch.int64,
device=device,
)
self.pcp_req_offsets = torch.arange(
max_num_reqs,
dtype=torch.int64,
device=device,
)
self.pcp_rank_offsets = torch.arange(
pcp_world_size,
dtype=torch.int64,
device=device,
)
self.mtp_slot_pad: torch.Tensor | None = None
self.vllm_config = vllm_config
self.pd_decode_recompute_scheduler_enabled = is_pd_decode_recompute_scheduler_enabled(vllm_config)
self.max_num_tokens = self.vllm_config.scheduler_config.max_num_batched_tokens
self.max_num_reqs = self.vllm_config.scheduler_config.max_num_seqs
self.device = device
self.use_async_scheduling = use_async_scheduling
self.pcp_allgather_restore_idx = CpuGpuBuffer(
max_buffer_num_tokens,
dtype=torch.int64,
device=device,
pin_memory=pin_memory,
)
self.pcp_exit_fa_scatter_idx = CpuGpuBuffer(
max_buffer_num_tokens,
dtype=torch.int64,
device=device,
pin_memory=pin_memory,
)
self.sample_slot_mapping = torch.full(
(max_buffer_num_tokens,),
fill_value=-1,
dtype=torch.int32,
device=device,
)
self.pcp_padded_slot_mapping_list: list = [] # reinitialized in initialize_slot_mapping
self.pcp_tokens = np.zeros(self.max_num_reqs, dtype=np.int32)
self.total_num_sampled_tokens_pcp = 0
self.num_pcp_pads_cpu_tensor = torch.zeros((max_num_reqs,), device="cpu", dtype=torch.int64)
self.num_pcp_pads_cpu = self.num_pcp_pads_cpu_tensor.numpy()
self.pcp_unpad_mask_cpu_tensor = torch.ones(
(max_buffer_num_tokens,),
device="cpu",
dtype=torch.bool,
)
self.num_actual_tokens_pcp_padded = 0
self.pcp_unpad_mask_cpu = self.pcp_unpad_mask_cpu_tensor.numpy()
self.full_indices = list(
range(
self.max_num_tokens * self.pcp_world_size * self.dcp_world_size
+ self.pcp_world_size * self.dcp_world_size * self.max_num_reqs
)
)
self.use_sparse = use_sparse
if self.speculative_config and self.pcp_world_size * self.dcp_world_size > 1:
self.input_ids_pcp_full = CpuGpuBuffer(
self.max_num_tokens, dtype=torch.int32, device=device, pin_memory=pin_memory
)
self.query_start_loc_pcp_full = CpuGpuBuffer(
self.max_num_reqs + 1, dtype=torch.int32, device=device, pin_memory=pin_memory
)
self.positions_pcp_full = torch.zeros(
self.max_num_tokens, dtype=torch.int64, device="cpu", pin_memory=pin_memory
)
self.positions_pcp_full_np = self.positions_pcp_full.numpy()
self.query_lens_pcp_full = CpuGpuBuffer(
self.max_num_reqs, dtype=torch.int32, device=device, pin_memory=pin_memory
)
self.pcp_fa_query_idx = torch.zeros(
self.max_num_tokens + 2 * self.max_num_reqs, dtype=torch.int32, device=self.device
)
self.pcp_enter_fa_restore_idx = torch.zeros(
self.max_num_tokens + 2 * self.pcp_world_size * self.max_num_reqs, dtype=torch.int32, device=self.device
)
self.pcp_fa_padding_restore_idx = torch.zeros(
self.max_num_tokens * self.pcp_world_size + 2 * self.pcp_world_size * self.max_num_reqs,
dtype=torch.int32,
device=self.device,
)
self.pcp_use_hybrid_attn = self.vllm_config.model_config.hf_config.model_type in (
"qwen3_next",
"qwen3_5",
"qwen3_5_moe",
)
self.dcp_mtp_attn_mask = CpuGpuBuffer(
(max_num_reqs, self.decode_threshold, vllm_config.model_config.max_model_len),
dtype=torch.bool,
device=device,
pin_memory=pin_memory,
)
self.pcp_pads_logits_hybrid_attn = torch.ones(self.max_num_reqs, dtype=torch.int32) * (self.pcp_world_size - 1)
self.pcp_padded_tokens_fla = 0
self.pcp_padded_tokens_length = 0
self.num_scheduled_tokens_padded: np.ndarray | None = None
self.max_num_tokens_across_pcp = 0
self.total_pcp_padding_tokens_fla = 0
self.pcp_tokens_padded = None
self.total_num_scheduled_tokens = 0
self._local_num_scheduled_tokens: np.ndarray | None = None
self._local_total_num_scheduled_tokens: int | None = None
# Full pre-PCP token layout used to rebuild draft slot mapping
# after async scheduling corrects num_computed_tokens.
self.async_rebuild_req_indices_full = None
self.async_rebuild_cu_num_tokens_full = None
self.async_rebuild_num_tokens_full = 0
logger.debug(
"PCP initialized: pcp_world_size=%s, pcp_rank=%s, "
"dcp_world_size=%s, dcp_rank=%s, "
"use_sparse=%s, use_async_scheduling=%s, hybrid_attn=%s",
self.pcp_world_size,
self.pcp_world_rank,
self.dcp_world_size,
self.dcp_world_rank,
self.use_sparse,
self.use_async_scheduling,
self.pcp_use_hybrid_attn,
)
@staticmethod
def _build_fa_padding_restore_idx(
pcp_unpad_mask: np.ndarray,
decode_offset: int,
actual_qkv_len: int,
) -> np.ndarray | None:
target_len = pcp_unpad_mask.shape[0]
if actual_qkv_len > target_len:
raise ValueError(f"actual_qkv_len ({actual_qkv_len}) must not exceed FA padded length ({target_len}).")
if actual_qkv_len == target_len:
return None
if decode_offset > target_len or actual_qkv_len < decode_offset:
raise ValueError(
f"Invalid PCP restore layout: decode_offset={decode_offset}, "
f"actual_qkv_len={actual_qkv_len}, target_len={target_len}."
)
restore_idx = np.empty(target_len, dtype=np.int32)
restore_idx[:decode_offset] = np.arange(decode_offset, dtype=np.int32)
prefill_unpad_mask = pcp_unpad_mask[decode_offset:]
prefill_real_tokens = int(prefill_unpad_mask.sum())
expected_actual_qkv_len = decode_offset + prefill_real_tokens
if expected_actual_qkv_len != actual_qkv_len:
raise ValueError(f"PCP unpad mask expects {expected_actual_qkv_len} QKV rows, but got {actual_qkv_len}.")
prefill_restore_idx = restore_idx[decode_offset:]
prefill_restore_idx.fill(actual_qkv_len)
prefill_restore_idx[prefill_unpad_mask] = np.arange(
decode_offset,
actual_qkv_len,
dtype=np.int32,
)
return restore_idx
def _get_cumsum_and_arange(
self,
num_scheduled_tokens: np.ndarray,
arange_np: np.ndarray,
cumsum_dtype: np.dtype | None = None,
) -> tuple[np.ndarray, np.ndarray]:
"""Get the cumulative sum and batched arange of the given array.
# E.g., [2, 5, 3] -> ([2, 7, 10], [0, 1, 0, 1, 2, 3, 4, 0, 1, 2])
# Equivalent to but faster than:
# np.concatenate([np.arange(n) for n in num_scheduled_tokens])
"""
# Step 1. [2, 5, 3] -> [2, 7, 10]
cu_num_tokens = np.cumsum(num_scheduled_tokens, dtype=cumsum_dtype)
total_num_tokens = cu_num_tokens[-1]
# Step 2. [2, 7, 10] -> [0, 0, 2, 2, 2, 2, 2, 7, 7, 7]
cumsums_offsets = np.repeat(cu_num_tokens - num_scheduled_tokens, num_scheduled_tokens)
# Step 3. [0, 1, 0, 1, 2, 3, 4, 0, 1, 2]
arange = arange_np[:total_num_tokens] - cumsums_offsets
return cu_num_tokens, arange
def classify_decode_request_mask(
self,
num_scheduled_tokens: np.ndarray | torch.Tensor,
num_computed_tokens: np.ndarray | torch.Tensor,
num_prompt_tokens: np.ndarray | torch.Tensor,
decode_threshold: int,
) -> np.ndarray:
"""Return a per-request mask for true decode requests.
Matches vLLM ``reorder_batch_to_split_decodes_and_prefills``:
decode = has context, scheduled tokens <= threshold, and prompt finished.
"""
has_context = num_computed_tokens > 0
is_below_threshold = num_scheduled_tokens <= decode_threshold
done_prefilling = num_computed_tokens >= num_prompt_tokens
if self.pd_decode_recompute_scheduler_enabled:
# PD D + RecomputeScheduler: KV recv leaves num_computed at N-1.
done_prefilling = done_prefilling | (num_computed_tokens == num_prompt_tokens - 1)
return has_context & is_below_threshold & done_prefilling
def init_batch_info(
self,
num_scheduled_tokens: np.ndarray,
num_reqs: int,
num_computed_tokens: np.ndarray,
num_prompt_tokens: np.ndarray,
) -> None:
self.num_reqs = num_reqs
scheduled = num_scheduled_tokens[:num_reqs]
self.decode_req_mask = self.classify_decode_request_mask(
scheduled,
num_computed_tokens[:num_reqs],
num_prompt_tokens[:num_reqs],
self.decode_threshold,
)
self.num_decode_reqs = int(self.decode_req_mask.sum())
self.num_prefill_reqs = num_reqs - self.num_decode_reqs
self.num_decode_tokens = int(scheduled[: self.num_decode_reqs].sum())
self.num_scheduled_tokens_padded = num_scheduled_tokens # for graph compiling in hybrid_attn
self.query_lens_pcp_full.cpu[: self.num_reqs] = torch.from_numpy(num_scheduled_tokens)
self.query_lens_pcp_full.cpu[self.num_reqs :].fill_(0)
self.query_lens_pcp_full.copy_to_gpu()
def adjust_cu_num_scheduled_tokens_for_pcp(
self,
cu_num_scheduled_tokens: np.ndarray,
num_pcp_pads: np.ndarray,
) -> np.ndarray:
# Re-align cu_num_scheduled_tokens under PCP hybrid attention so the
# caller can build correct logits_indices for PCP. Prefill requests
# need to be padded up to a multiple of (pcp_world_size * 2) tokens,
# while decode requests are simply multiplied by pcp_world_size and
# offset by the per-req pcp pads.
if self.num_prefill_reqs <= 0:
return cu_num_scheduled_tokens
prefill_lens = self.pcp_tokens[self.num_decode_reqs : self.num_decode_reqs + self.num_prefill_reqs]
pads = copy.deepcopy(num_pcp_pads)
pads[self.num_decode_reqs :] = np.cumsum(pads[self.num_decode_reqs :])
base = int(cu_num_scheduled_tokens[self.num_decode_reqs - 1]) if self.num_decode_reqs > 0 else 0
prefill_cu = [base + s for s in accumulate(prefill_lens)]
cu_num_scheduled_tokens = cu_num_scheduled_tokens.copy()
cu_num_scheduled_tokens[self.num_decode_reqs :] = prefill_cu
cu_num_scheduled_tokens[self.num_decode_reqs :] = (
cu_num_scheduled_tokens[self.num_decode_reqs :] * self.pcp_world_size - pads[self.num_decode_reqs :]
)
return cu_num_scheduled_tokens
def cache_local_schedule_layout(
self,
num_scheduled_tokens: np.ndarray,
num_reqs: int,
total_num_scheduled_tokens: int,
) -> None:
# Copy to decouple from mutable batch arrays.
self._local_num_scheduled_tokens = num_scheduled_tokens[:num_reqs].copy()
self._local_total_num_scheduled_tokens = int(total_num_scheduled_tokens)
def get_local_schedule_layout(
self,
) -> tuple[np.ndarray | None, int | None]:
return self._local_num_scheduled_tokens, self._local_total_num_scheduled_tokens
def fill_prompt_embeds_for_pcp(
self,
req_embeds: torch.Tensor,
req_positions_np: np.ndarray,
dst_slice: torch.Tensor,
) -> None:
valid_mask_np = req_positions_np < req_embeds.shape[0]
if not valid_mask_np.any():
return
if valid_mask_np.all():
torch.index_select(
req_embeds,
0,
torch.from_numpy(req_positions_np.astype(np.int64)),
out=dst_slice,
)
return
src_positions = torch.from_numpy(req_positions_np[valid_mask_np].astype(np.int64))
dst_positions = torch.from_numpy(np.nonzero(valid_mask_np)[0].astype(np.int64))
dst_slice.index_copy_(0, dst_positions, req_embeds.index_select(0, src_positions))
def build_local_mm_schedule(
self,
req_ids: list[str],
requests: dict[str, Any],
positions_np: np.ndarray,
local_num_scheduled_tokens: np.ndarray,
encoder_cache: dict[str, torch.Tensor],
) -> tuple[dict[str, list[int]], set[str]]:
scheduled_encoder_inputs: dict[str, list[int]] = {}
needed_mm_hashes: set[str] = set()
req_start_idx = 0
for req_idx, req_id in enumerate(req_ids):
if req_idx >= local_num_scheduled_tokens.shape[0]:
break
num_sched = int(local_num_scheduled_tokens[req_idx])
if num_sched <= 0:
req_start_idx += num_sched
continue
req_positions = positions_np[req_start_idx : req_start_idx + num_sched]
req_state = requests[req_id]
mm_input_ids = list[int]()
for mm_input_id, mm_feature in enumerate(req_state.mm_features):
pos_info = mm_feature.mm_position
start_pos = pos_info.offset
end_pos = start_pos + pos_info.length
mm_hash = mm_feature.identifier
local_mask = (req_positions >= start_pos) & (req_positions < end_pos)
if not local_mask.any():
continue
local_indices = np.nonzero(local_mask)[0]
rel_positions = req_positions[local_indices] - start_pos
is_embed = pos_info.is_embed
if is_embed is not None:
is_embed_np = is_embed.cpu().numpy()
if not is_embed_np[rel_positions].any():
continue
needed_mm_hashes.add(mm_hash)
if mm_hash not in encoder_cache:
mm_input_ids.append(mm_input_id)
if mm_input_ids:
scheduled_encoder_inputs[req_id] = mm_input_ids
req_start_idx += num_sched
return scheduled_encoder_inputs, needed_mm_hashes
def gather_mm_embeddings_for_pcp(
self,
req_ids: list[str],
requests: dict[str, Any],
positions_np: np.ndarray,
local_num_scheduled_tokens: np.ndarray,
shift_computed_tokens: int,
encoder_cache: dict[str, torch.Tensor],
is_mm_embed: torch.Tensor,
model: Any,
is_multimodal_pruning_enabled: bool,
uses_mrope: bool,
warning_once: Callable[..., Any] | None = None,
) -> tuple[list[torch.Tensor], bool, bool]:
mm_embeds = list[torch.Tensor]()
req_start_idx = 0
should_sync_mrope_positions = False
should_sync_xdrope_positions = False
for req_idx, req_id in enumerate(req_ids):
num_sched = int(local_num_scheduled_tokens[req_idx])
req_positions = positions_np[req_start_idx : req_start_idx + num_sched]
if shift_computed_tokens:
req_positions = req_positions + shift_computed_tokens
req_state = requests[req_id]
req_taken_mask = np.zeros(num_sched, dtype=np.bool_)
mm_embeds_req: list[torch.Tensor] = []
req_mm_local_indices: list[np.ndarray] = []
for mm_feature in req_state.mm_features:
pos_info = mm_feature.mm_position
start_pos = pos_info.offset
end_pos = start_pos + pos_info.length
mm_hash = mm_feature.identifier
local_mask = (req_positions >= start_pos) & (req_positions < end_pos)
if not local_mask.any():
continue
local_indices = np.nonzero(local_mask)[0]
rel_positions = req_positions[local_indices] - start_pos
is_embed = pos_info.is_embed
if is_embed is not None:
is_embed_np = is_embed.cpu().numpy()
keep_mask = is_embed_np[rel_positions]
if not keep_mask.any():
continue
local_indices = local_indices[keep_mask]
rel_positions = rel_positions[keep_mask]
embed_index_map = np.cumsum(is_embed_np.astype(np.int64)) - 1
embed_indices = embed_index_map[rel_positions]
else:
embed_indices = rel_positions
# OR semantics for overlapping mm features: keep first writer.
keep_new = ~req_taken_mask[local_indices]
if not keep_new.any():
continue
local_indices = local_indices[keep_new]
embed_indices = embed_indices[keep_new]
req_taken_mask[local_indices] = True
encoder_output = encoder_cache.get(mm_hash)
assert encoder_output is not None, f"Encoder cache miss for {mm_hash}."
embed_index_tensor = torch.from_numpy(embed_indices.astype(np.int64)).to(
device=encoder_output.device,
non_blocking=True,
)
mm_embeds_item = torch.index_select(encoder_output, 0, embed_index_tensor)
mm_embeds_req.append(mm_embeds_item)
req_mm_local_indices.append(local_indices.astype(np.int64, copy=False))
is_mm_embed[req_start_idx + local_indices] = True
if is_multimodal_pruning_enabled and uses_mrope:
assert req_state.mrope_positions is not None
should_sync_mrope_positions = True
mm_embeds_req, new_mrope_positions, new_delta = model.recompute_mrope_positions(
input_ids=req_state.prompt_token_ids,
multimodal_embeddings=mm_embeds_req,
mrope_positions=req_state.mrope_positions,
num_computed_tokens=req_state.num_computed_tokens,
)
req_state.mrope_positions.copy_(new_mrope_positions)
req_state.mrope_position_delta = new_delta
# Keep multimodal embedding order aligned with is_mm_embed scanning order.
# Under PCP, request positions may be non-monotonic; concatenating by
# feature order can misalign embeddings with boolean mask traversal.
if len(mm_embeds_req) > 1:
total_local_idx = sum(x.size for x in req_mm_local_indices)
total_embed_rows = sum(x.shape[0] for x in mm_embeds_req)
if total_local_idx == total_embed_rows and total_local_idx > 0:
local_idx_cat = np.concatenate(req_mm_local_indices, axis=0)
embed_cat = torch.cat(mm_embeds_req, dim=0)
order = np.argsort(local_idx_cat, kind="stable")
order_t = torch.from_numpy(order.astype(np.int64)).to(
device=embed_cat.device,
non_blocking=True,
)
mm_embeds_req = [embed_cat.index_select(0, order_t)]
elif warning_once is not None:
warning_once(
"PCP MM reorder skipped due to size mismatch: local_idx=%d, embed_rows=%d",
total_local_idx,
total_embed_rows,
)
mm_embeds.extend(mm_embeds_req)
req_start_idx += num_sched
return mm_embeds, should_sync_mrope_positions, should_sync_xdrope_positions
def maybe_localize_scheduler_output_for_mm_preprocess(
self,
scheduler_output: "SchedulerOutput",
req_ids: list[str],
requests: dict[str, Any],
positions_np: np.ndarray,
local_num_scheduled_tokens: np.ndarray | None,
local_total_num_scheduled_tokens: int | None,
encoder_cache: dict[str, torch.Tensor],
) -> dict[str, Any] | None:
need_localize = (
local_total_num_scheduled_tokens is not None
and local_total_num_scheduled_tokens != scheduler_output.total_num_scheduled_tokens
)
if not need_localize and local_num_scheduled_tokens is not None:
for req_idx, req_id in enumerate(req_ids):
if req_idx >= local_num_scheduled_tokens.shape[0]:
break
global_sched = scheduler_output.num_scheduled_tokens.get(req_id)
if global_sched is None or int(global_sched) != int(local_num_scheduled_tokens[req_idx]):
need_localize = True
break
if not need_localize:
return None
restore_state: dict[str, Any] = {
"total_num_scheduled_tokens": scheduler_output.total_num_scheduled_tokens,
"num_scheduled_tokens": scheduler_output.num_scheduled_tokens,
"scheduled_encoder_inputs": scheduler_output.scheduled_encoder_inputs,
"free_encoder_mm_hashes": scheduler_output.free_encoder_mm_hashes,
}
if local_total_num_scheduled_tokens is not None:
scheduler_output.total_num_scheduled_tokens = local_total_num_scheduled_tokens
if local_num_scheduled_tokens is None:
return restore_state
num_sched_by_req = dict(scheduler_output.num_scheduled_tokens)
for req_idx, req_id in enumerate(req_ids):
if req_idx >= local_num_scheduled_tokens.shape[0]:
break
num_sched_by_req[req_id] = int(local_num_scheduled_tokens[req_idx])
scheduler_output.num_scheduled_tokens = num_sched_by_req
(
scheduler_output.scheduled_encoder_inputs,
local_needed_mm_hashes,
) = self.build_local_mm_schedule(
req_ids=req_ids,
requests=requests,
positions_np=positions_np,
local_num_scheduled_tokens=local_num_scheduled_tokens,
encoder_cache=encoder_cache,
)
# Under PCP, global free list can be earlier than local consumption.
# Keep MM hashes for all active requests.
active_mm_hashes = {
mm_feature.identifier for req_state in requests.values() for mm_feature in req_state.mm_features
}
keep_hashes = active_mm_hashes | local_needed_mm_hashes
scheduler_output.free_encoder_mm_hashes = [
mm_hash for mm_hash in scheduler_output.free_encoder_mm_hashes if mm_hash not in keep_hashes
]
return restore_state
def restore_scheduler_output_after_mm_preprocess(
self,
scheduler_output: "SchedulerOutput",
restore_state: dict[str, Any] | None,
) -> None:
if restore_state is None:
return
scheduler_output.total_num_scheduled_tokens = restore_state["total_num_scheduled_tokens"]
scheduler_output.num_scheduled_tokens = restore_state["num_scheduled_tokens"]
scheduler_output.scheduled_encoder_inputs = restore_state["scheduled_encoder_inputs"]
scheduler_output.free_encoder_mm_hashes = restore_state["free_encoder_mm_hashes"]
def initialize_slot_mapping(self) -> None:
"""
Hyrbid-attention models, such as qwen3_next, have plural kv_cache_groups, which may lead to
problems like overwriting last group's pcp_padded_slot_mapping, since they share the same
address. Therefore we need as many pcp_padded_slot_mappings as kv_cache_groups.
"""
pcp_padded_slot_mapping = torch.full(
(self.sample_slot_mapping.shape[0],),
fill_value=-1,
dtype=torch.int32,
device=self.sample_slot_mapping.device,
)
self.pcp_padded_slot_mapping_list.append(pcp_padded_slot_mapping)
def update_tokens_for_pcp(
self,
num_scheduled_tokens: np.ndarray,
arange_np: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
"""
Update token counts and positions for Prefill Context Parallelism (PCP).
When using Prefill Context Parallelism, each request's prefill sequence is
split across multiple PCP ranks. The splitting strategy used here is the
"DualChunkSwap" style: each request's (padded) sequence is split into
2 * pcp_world_size chunks and ranks are assigned chunks in an interleaved
head/tail pattern to balance load.
This function:
- Computes how many tokens each request should be processed by the current
PCP rank (pcp_tokens).
- Computes the flattened positions of those tokens within the local
padded buffer (pcp_positions).
- Updates runner state arrays used to restore original order and mask out
padded tokens after allgather:
- self.num_pcp_pads_cpu: number of pads added per request
- self.pcp_unpad_mask_cpu: boolean mask marking real tokens in the
padded allgather buffer
- self.pcp_allgather_restore_idx: index array used to restore original
ordering after per-rank allgather and interleaving.
Args:
num_scheduled_tokens: 1D numpy array of length num_reqs containing
the number of new tokens scheduled per request.
arange_np: 1D numpy array of length max_buffer_num_tokens used for
efficient batched arange operations.
Returns:
Tuple (pcp_tokens, pcp_positions):
- pcp_tokens: number of tokens per request that this PCP rank will
actually process (after splitting / replication).
For hybrid-attention model: number of unpadded tokens
per requests
- pcp_positions: flattened positions for those tokens on this rank,
used to build the positions buffer for the model.
Example:
>>> Assume tokens = [1, 5, 8], pcp_world_size = 2. After _update_tokens_for_pcp.
>>> pcp_rank = 0 get ([1, 4, 4], [0, 0, 1, 6, 7, 0, 1, 6, 7])
>>> pcp_rank = 1 get ([1, 4, 4], [0, 2, 3, 4, 5, 2, 3, 4, 5])
>>> Meanwhile, the following results are same for each pcp rank
>>> self.num_pcp_pads_cpu
[1, 3, 0]
>>> self.pcp_unpad_mask_cpu
[True, False, True, True, True, True, True, False, False,
False, True, True, True, True, True, True, True, True]
>>> self.pcp_allgather_restore_idx
[0, 9, 1, 2, 10, 11, 12, 13, 3, 4, 5, 6, 14, 15, 16, 17, 7, 8]
"""
# DualChunkSwap requires alignment to a multiple of (2 * pcp_world_size).
# We first pad each request's token count up to that multiple.
num_padded_scheduled_tokens = np.ceil(num_scheduled_tokens / (2 * self.pcp_world_size)).astype(np.int32) * (
2 * self.pcp_world_size
)
# PCP does not split decode requests. For decode requests, we instead
# duplicate the scheduled tokens across the pcp_world_size ranks.
num_padded_scheduled_tokens[: self.num_decode_reqs] = (
num_scheduled_tokens[: self.num_decode_reqs] * self.pcp_world_size
)
# Record how many pads were added per request (padded - original).
self.num_pcp_pads_cpu[: self.num_reqs] = num_padded_scheduled_tokens - num_scheduled_tokens
# cu_padded_tokens: cumulative sum of padded token counts,
# pcp_padded_arange: per-request arange flattened for padded tokens.
cu_padded_tokens, pcp_padded_arange = self._get_cumsum_and_arange(num_padded_scheduled_tokens, arange_np)
self.pcp_padded_tokens_length = pcp_padded_arange.shape[0]
# Build the mask that marks which positions in the padded allgather buffer
# correspond to real (unpadded) tokens.
self.pcp_unpad_mask_cpu[: self.pcp_padded_tokens_length] = pcp_padded_arange < np.repeat(
num_scheduled_tokens, num_padded_scheduled_tokens
)
unpad_mask_decode = self.pcp_unpad_mask_cpu[: self.num_decode_tokens * self.pcp_world_size]
unpad_mask_decode = unpad_mask_decode.reshape([-1, self.pcp_world_size])
unpad_mask_decode[:, 0] = True
unpad_mask_decode[:, 1:] = False
pcp_tokens = num_padded_scheduled_tokens // self.pcp_world_size
# Compute per-request "chunk sizes" for the head/tail splitting.
# For prefill requests, we further split the pcp_tokens into two chunks
# (head and tail). For decode requests, the chunk equals pcp_tokens.
pcp_chunk_sizes = (pcp_tokens // 2).clip(min=1)
pcp_chunk_sizes[: self.num_decode_reqs] = pcp_tokens[: self.num_decode_reqs]
# Build arange-style helpers for pcp tokens and chunk sizes:
# - pcp_arange gives indices repeated for each token in pcp_tokens
# - pcp_chunk_arange gives indices repeated for each position inside chunks
_, pcp_arange = self._get_cumsum_and_arange(pcp_tokens, arange_np)
_, pcp_chunk_arange = self._get_cumsum_and_arange(pcp_chunk_sizes, arange_np)
# Mask that marks whether a position belongs to the head chunk (True)
# or the tail chunk (False). For decode requests, tail chunk won't exist
# and is handled specially below.
pcp_head_chunk_mask = pcp_arange < np.repeat(pcp_chunk_sizes, pcp_tokens)
def get_current_rank_positions(positions_start_loc: int | np.ndarray, rank: int):
"""
Compute flattened positions for the given rank with a given start
offset for each request (positions_start_loc).
- For head chunks: start at positions_start_loc + rank * chunk_size.
- For tail chunks: start at positions_start_loc + (2*pcp_world_size- rank -
1) * chunk_size.
- For decode requests: no tail chunks; their positions are filled from the
contiguous (unpadded) `tokens` arange instead (handled after).
"""
positions = np.zeros(len(pcp_head_chunk_mask), dtype=np.int32)
head_start_loc = positions_start_loc + rank * pcp_chunk_sizes
tail_start_loc = positions_start_loc + (2 * self.pcp_world_size - rank - 1) * pcp_chunk_sizes
# Fill head positions using chunk arange offset by head_start_loc.
positions[pcp_head_chunk_mask] = pcp_chunk_arange + np.repeat(head_start_loc, pcp_chunk_sizes)
# Fill tail positions. Note decode requests do not have tail chunks,
# so the tail filling is only for prefill positions.
positions[~pcp_head_chunk_mask] = (
pcp_chunk_arange[self.num_decode_tokens :]
+ np.repeat(tail_start_loc, pcp_chunk_sizes)[self.num_decode_tokens :]
)
return positions
positions = get_current_rank_positions(0, self.pcp_world_rank)
padded_pos_start_loc = np.roll(cu_padded_tokens, 1)
padded_pos_start_loc[0] = 0
# Decode tokens are duplicated only after AG. But their positions are
# same without prefill context parallel.
if self.num_decode_reqs > 0:
positions[: self.num_decode_tokens] = self._get_cumsum_and_arange(
num_scheduled_tokens[: self.num_decode_reqs], arange_np
)[1]
# Build the restore index used after allgather.
all_positions_lst = [
get_current_rank_positions(padded_pos_start_loc, rank_i) for rank_i in range(self.pcp_world_size)
]
all_positions = np.concatenate(all_positions_lst)
self.pcp_allgather_restore_idx.np[: all_positions.shape[0]] = all_positions.argsort()
self.pcp_allgather_restore_idx.copy_to_gpu(all_positions.shape[0])
self.pcp_tokens[: self.num_reqs] = pcp_tokens[: self.num_reqs]
self.total_num_sampled_tokens_pcp = pcp_tokens[: self.num_reqs].sum()
if self.pcp_use_hybrid_attn:
max_scheduled_prefill_tokens = 0
self.pcp_padded_tokens_fla = 0
if self.num_decode_reqs > 0:
num_padded_scheduled_tokens[: self.num_decode_reqs] = (
num_padded_scheduled_tokens[: self.num_decode_reqs] // self.pcp_world_size
)
self.total_pcp_padding_tokens_fla = 0
# have prefills
if self.num_reqs - self.num_decode_reqs > 0:
prefill_tokens_tensor = torch.Tensor(num_scheduled_tokens[self.num_decode_reqs :])
# [num_prefill_reqs, pcp_world_size, 1] [[3,2]] [[2,2,2,1],[2,1,1,1]]
num_prefill_tokens_allranks = (
self._get_cp_local_seq_lens(prefill_tokens_tensor, self.pcp_world_size, 1, 1).long().numpy()
)
# [3] [2] | [2,2] [2,1] [2,1] [1,1]
num_prefill_scheduled_tokens_linear = num_prefill_tokens_allranks[:, self.pcp_world_rank, 0]
num_padded_scheduled_tokens[self.num_decode_reqs :] = num_prefill_scheduled_tokens_linear
# [[3,5]] | [[0,0,0,0,0],[0,0,0,0,0]]
num_prefill_tokens_start_loc = np.zeros(
(self.num_reqs - self.num_decode_reqs, self.pcp_world_size + 1), dtype=np.int64
)
# [[0,3,5]] | [[0,2,4,6,7],[0,2,3,4,5]]
num_prefill_tokens_start_loc[:, 1:] = np.cumsum(num_prefill_tokens_allranks[..., 0], axis=-1)
# [0] [3] | [0,0] [2,2] [4,3] [6,4] [7,5]
num_prefill_tokens_cu_ranks = num_prefill_tokens_start_loc[:, self.pcp_world_rank]
# [0,1,2] [0,1] | [0,1,0,1] [0,1,0] [0,1,0] [0,0]
# -> [0,1,2] [3,4] | [0,1,0,1] [2,3,2] [4,5,3] [6,4]
_, positions_linear = self._get_cumsum_and_arange(num_padded_scheduled_tokens, arange_np)
positions_linear[self.num_decode_tokens :] = positions_linear[self.num_decode_tokens :] + np.repeat(
num_prefill_tokens_cu_ranks, num_prefill_scheduled_tokens_linear
)
max_scheduled_prefill_tokens = num_prefill_tokens_allranks[:, 0, 0].sum()
num_prefill_tokens = num_scheduled_tokens[self.num_decode_reqs :].sum()
self.total_pcp_padding_tokens_fla = (
max_scheduled_prefill_tokens * self.pcp_world_size - num_prefill_tokens
)
self.pcp_padded_tokens_fla += max_scheduled_prefill_tokens - num_prefill_scheduled_tokens_linear.sum()
max_scheduled_tokens = max_scheduled_prefill_tokens + self.num_decode_tokens
enter_fa_prefill_restore_idx = None
if self.num_reqs - self.num_decode_reqs > 0:
# prefill reorder idx
# [[3,2]] [[2,2,2,1],[2,2,1,1],[1,1,1,1]]
num_prefill_tokens_allranks = num_prefill_tokens_allranks[..., 0]
# [0,1,2,0,1] [0,1,0,1,0,1,0,|0,1,0,1,0,0]
_, prefill_arange_allranks = self._get_cumsum_and_arange(
num_prefill_tokens_allranks.flatten(), arange_np
)
# [0,1] [0,1,2,3,0,1,2,3]
_, prefill_rank_offset = self._get_cumsum_and_arange(
np.ones(self.num_reqs - self.num_decode_reqs, dtype=np.int64) * self.pcp_world_size, arange_np
)
# [0,0,0,3,3] [0,M,2M,3M,0,M,2M,3M] -> [0,0,M,M,2M,2M,3M,0,0,M,M,2M,3M] + D
prefill_all_offset = (
np.repeat(prefill_rank_offset * max_scheduled_tokens, num_prefill_tokens_allranks.flatten())
+ self.num_decode_tokens
)
# [0,0,0,0,|2,2,2,1,|4,4,3,2] -> [0,0,0,0,0,0,0,|2,2,2,2,2,1,|4,4,3,2]
# [[0,0]] -> [0,0,0,0,0]
prefill_local_start_local = np.zeros_like(num_prefill_tokens_allranks)
prefill_local_start_local[1:, :] = np.cumsum(num_prefill_tokens_allranks, axis=0)[:-1, :]
prefill_local_offset = np.repeat(
prefill_local_start_local.flatten(), num_prefill_tokens_allranks.flatten()
)
prefill_all_offset = np.add(prefill_all_offset, prefill_local_offset)
# [0,1,2,3,4] [0,1,M,M+1,2M,2M+1,3M,0,1,M,M+1,2M,3M]
enter_fa_prefill_restore_idx = np.add(prefill_all_offset, prefill_arange_allranks)
else:
_, positions_linear = self._get_cumsum_and_arange(num_padded_scheduled_tokens, arange_np)
# decode reorder idx
enter_fa_decode_restore_idx = None
if self.num_decode_reqs > 0:
if self.pcp_use_hybrid_attn and self.speculative_config:
# hybrid attn model has different position assignment for decode tokens.
decode_reqs_offset = np.tile(np.arange(self.num_decode_tokens, dtype=np.int64), self.pcp_world_size)
decode_ranks_offset = (
np.repeat(np.arange(self.pcp_world_size, dtype=np.int64), self.num_decode_tokens)
* max_scheduled_tokens
)
else:
num_decode_pcp_size = np.ones(self.num_decode_reqs, dtype=np.int64) * self.pcp_world_size
decode_reqs_offset = np.repeat(np.arange(self.num_decode_reqs, dtype=np.int64), num_decode_pcp_size)
decode_ranks_offset = (
self._get_cumsum_and_arange(num_decode_pcp_size, arange_np)[1] * max_scheduled_tokens
)
enter_fa_decode_restore_idx = np.add(decode_reqs_offset, decode_ranks_offset)
if enter_fa_decode_restore_idx is not None and enter_fa_prefill_restore_idx is not None:
pcp_enter_fa_restore_idx = torch.from_numpy(
np.concatenate([enter_fa_decode_restore_idx, enter_fa_prefill_restore_idx])
)
elif enter_fa_decode_restore_idx is not None:
pcp_enter_fa_restore_idx = torch.from_numpy(enter_fa_decode_restore_idx)
elif enter_fa_prefill_restore_idx is not None:
pcp_enter_fa_restore_idx = torch.from_numpy(enter_fa_prefill_restore_idx)
self.pcp_enter_fa_restore_idx[: pcp_enter_fa_restore_idx.shape[0]].copy_(
pcp_enter_fa_restore_idx.long(), non_blocking=True
)
pcp_unpad_mask = self.pcp_unpad_mask_cpu[: self.pcp_padded_tokens_length]
pcp_fa_padding_restore_idx = self._build_fa_padding_restore_idx(
pcp_unpad_mask,
self.num_decode_tokens * self.pcp_world_size,
pcp_enter_fa_restore_idx.shape[0],
)
if pcp_fa_padding_restore_idx is not None:
self.pcp_fa_padding_restore_idx[: pcp_fa_padding_restore_idx.shape[0]].copy_(
torch.from_numpy(pcp_fa_padding_restore_idx),
non_blocking=True,
)
if self.num_reqs > self.num_decode_reqs:
all_positions_prefill = [
get_current_rank_positions(padded_pos_start_loc, rank_i)[self.num_decode_tokens :]
- self.num_decode_tokens * self.pcp_world_size
for rank_i in range(self.pcp_world_size)
]
all_positions_prefill_tensor = torch.from_numpy(np.concatenate(all_positions_prefill))
all_exit_fa_restore_idx = all_positions_prefill_tensor.float().argsort()
unpad_mask_prefill = self.pcp_unpad_mask_cpu[: self.pcp_padded_tokens_length][
self.num_decode_tokens * self.pcp_world_size :
]
# [0] | [0,7]
ori_tokens_start_loc = np.roll(np.cumsum(num_scheduled_tokens[self.num_decode_reqs :]), 1)
ori_tokens_start_loc[0] = 0
# [0,1,2] [3,4] | [0,1,7,8] [2,3,9] [4,5,10] [6,11]
exit_fa_scatter_indices = positions_linear[self.num_decode_tokens :] + np.repeat(
ori_tokens_start_loc, num_prefill_scheduled_tokens_linear
)
exit_fa_scatter_idx = torch.index_select(
all_exit_fa_restore_idx[unpad_mask_prefill], 0, torch.from_numpy(exit_fa_scatter_indices)
)
self.pcp_exit_fa_scatter_idx.gpu[: exit_fa_scatter_idx.shape[0]].copy_(
exit_fa_scatter_idx.long(), non_blocking=True
)
positions_prefill = all_positions_prefill[self.pcp_world_rank]
pcp_fa_query_idx_tensor = torch.from_numpy(positions_prefill)
self.pcp_fa_query_idx[: pcp_fa_query_idx_tensor.shape[0]].copy_(
pcp_fa_query_idx_tensor.long(), non_blocking=True
)
self.pcp_tokens[: self.num_reqs] = pcp_tokens[: self.num_reqs]
self.total_num_sampled_tokens_pcp = num_scheduled_tokens[: self.num_reqs].sum()
self.max_num_tokens_across_pcp = max_scheduled_tokens
self.pcp_tokens_padded = pcp_tokens[: self.num_reqs]
self.num_scheduled_tokens_padded = np.array(self.pcp_tokens_padded, dtype=np.int32)
self.total_num_scheduled_tokens = num_padded_scheduled_tokens[: self.num_reqs].sum()
return num_padded_scheduled_tokens, positions_linear
return pcp_tokens[: self.num_reqs], positions
def get_logits_indices(
self,
cu_num_tokens: np.ndarray,
num_reqs: int,
tokens_original: list[int] | None = None,
):
if not self.pcp_use_hybrid_attn or tokens_original is None:
logits_indices = (
torch.from_numpy(cu_num_tokens) * self.pcp_world_size
- self.num_pcp_pads_cpu_tensor[: self.num_reqs]
- 1
)
else:
tokens_original_tensor = torch.tensor(tokens_original, dtype=torch.int32)
assert self.decode_req_mask is not None
num_decode_reqs = int(self.decode_req_mask.sum())
decode_pads = self.pcp_pads_logits_hybrid_attn[:num_decode_reqs]
pad_len = tokens_original_tensor.shape[0] - num_decode_reqs
tokens_logits = tokens_original_tensor + F.pad(decode_pads, (0, pad_len), value=0)
logits_indices = torch.cumsum(tokens_logits, dim=0) - 1
return logits_indices
def get_padded_slot_mapping(
self,
num_tokens: int,
num_tokens_padded: int,
slot_mapping: torch.Tensor,
kv_cache_group_id: int,
):
# After pcp allgather and restore, there are padded tokens in kv,
# so we need pad slotmapping for alignment.
pcp_padded_slot_mapping = self.pcp_padded_slot_mapping_list[kv_cache_group_id]
if self.pcp_use_hybrid_attn:
assert self.num_scheduled_tokens_padded is not None
num_tokens = self.num_scheduled_tokens_padded.sum()
if not self.pcp_use_hybrid_attn or self.total_num_sampled_tokens_pcp != num_tokens_padded:
pcp_padded_slot_mapping = pcp_padded_slot_mapping[: num_tokens_padded * self.pcp_world_size]
else:
pcp_padded_slot_mapping = pcp_padded_slot_mapping[: num_tokens * self.pcp_world_size]
cp_unpad_mask = self.pcp_unpad_mask_cpu_tensor[: num_tokens * self.pcp_world_size]
pcp_padded_slot_mapping.fill_(-1)
pcp_padded_slot_mapping[: num_tokens * self.pcp_world_size][cp_unpad_mask] = slot_mapping
return pcp_padded_slot_mapping
def get_restore_hidden_states(
self,
hidden_states: torch.Tensor,
num_input_tokens: int | None = None,
) -> torch.Tensor:
"""Gather PCP hidden states and restore the original token order.
``num_input_tokens`` is explicit for spec decode, where draft graph
padding can differ from the main-model PCP scheduled-token length.
Main-model callers omit it and use the PCP metadata length.
"""
from vllm.distributed.parallel_state import get_pcp_group
if not self.pcp_use_hybrid_attn:
local_num_tokens = (
num_input_tokens
if num_input_tokens is not None
else self.num_actual_tokens_pcp_padded // self.pcp_world_size
)
hidden_states = get_pcp_group().all_gather(
hidden_states[:local_num_tokens],
0,
)
restore_idx = self.pcp_allgather_restore_idx.gpu[: hidden_states.shape[0]]
return torch.index_select(
hidden_states,
0,
restore_idx,
)
else:
if num_input_tokens is not None:
hidden_states = hidden_states[:num_input_tokens]
if hidden_states.shape[0] == self.total_num_scheduled_tokens and self.pcp_padded_tokens_fla > 0:
hidden_states = F.pad(
hidden_states, pad=(0, 0, 0, self.pcp_padded_tokens_fla), mode="constant", value=0
)
hidden_states = (
hidden_states[: self.max_num_tokens_across_pcp] if self.max_num_tokens_across_pcp > 0 else hidden_states
)
hidden_states = get_pcp_group().all_gather(hidden_states.contiguous(), dim=0)
restore_idx = self.pcp_enter_fa_restore_idx[: hidden_states.shape[0] - self.total_pcp_padding_tokens_fla]
return torch.index_select(hidden_states, 0, restore_idx)
def mask_spec_decode_restore_idx_for_graph(
self,
pcp_allgather_restore_idx: torch.Tensor,
) -> None:
"""Mask graph-only PCP restore slots used by padded draft batches."""
index = torch.arange(
pcp_allgather_restore_idx.shape[0],
dtype=torch.int64,
device=pcp_allgather_restore_idx.device,
)
mask = (index % (self.pcp_world_size * self.decode_threshold)) >= self.decode_threshold
pcp_allgather_restore_idx[mask] = 0
restore_len = pcp_allgather_restore_idx.shape[0]
self.pcp_allgather_restore_idx.gpu[:restore_len].copy_(pcp_allgather_restore_idx)
self.pcp_allgather_restore_idx.gpu[restore_len:].fill_(0)
def get_spec_decode_decode_hidden_states(
self,
target_hidden_states_d_padded: torch.Tensor,
num_decode_reqs: int,
num_decode_tokens: int | None = None,
) -> torch.Tensor:
"""Remove PCP decode padding from target hidden states for proposer input."""
if num_decode_tokens is None:
num_decode_tokens = self.num_decode_tokens
if num_decode_tokens == 0:
return target_hidden_states_d_padded
if self.pcp_use_hybrid_attn:
return target_hidden_states_d_padded[:num_decode_tokens]
query_start_loc = self.query_start_loc_pcp_full.gpu[: num_decode_reqs + 1]
decode_req_starts = query_start_loc[:num_decode_reqs].to(torch.int64)
decode_query_lens = (query_start_loc[1 : num_decode_reqs + 1] - query_start_loc[:num_decode_reqs]).to(
torch.int64
)
decode_padded_starts = decode_req_starts * self.pcp_world_size
decode_req_starts_per_token = torch.repeat_interleave(
decode_req_starts,
decode_query_lens,
output_size=num_decode_tokens,
)
decode_padded_starts_per_token = torch.repeat_interleave(
decode_padded_starts,
decode_query_lens,
output_size=num_decode_tokens,
)
decode_offsets = (
torch.arange(
num_decode_tokens,
dtype=torch.int64,
device=target_hidden_states_d_padded.device,
)
- decode_req_starts_per_token
)
decode_hidden_state_indices = decode_padded_starts_per_token + decode_offsets
return target_hidden_states_d_padded[decode_hidden_state_indices]
def prepare_spec_decode_first_pass_inputs(
self,
input_ids: torch.Tensor,
target_positions: torch.Tensor,
target_hidden_states: torch.Tensor,
token_indices_to_sample: torch.Tensor,
common_attn_metadata: Any,
long_seq_metadata: Any | None,
req_scheduled_tokens: dict[str, int] | None,
req_ids: list[str],
logits_indices: torch.Tensor,
num_tokens: int,
num_prefill_reqs: int,
num_decode_reqs: int,
uses_mrope: bool,
) -> PCPSpecDecodeFirstPassInputs:
"""Prepare CP-adjusted proposer inputs for the first draft pass."""
long_seq_args: tuple[torch.Tensor | None, torch.Tensor | None] | None = None
if self.pcp_world_size * self.dcp_world_size <= 1:
return PCPSpecDecodeFirstPassInputs(
num_tokens=num_tokens,
input_ids=input_ids,
target_positions=target_positions,
target_hidden_states=target_hidden_states,
token_indices_to_sample=token_indices_to_sample,
long_seq_args=long_seq_args,
)
assert long_seq_metadata is not None
common_attn_metadata.prefill_context_parallel_metadata = long_seq_metadata
ori_token_indices_to_sample = token_indices_to_sample.clone()
query_lens_d = self.query_lens_pcp_full.cpu[:num_decode_reqs]
long_seq_args = (query_lens_d, ori_token_indices_to_sample)
if self.pcp_world_size <= 1:
return PCPSpecDecodeFirstPassInputs(
num_tokens=num_tokens,
input_ids=input_ids,
target_positions=target_positions,
target_hidden_states=target_hidden_states,
token_indices_to_sample=token_indices_to_sample,
long_seq_args=long_seq_args,
)
num_tokens_d = self.num_decode_tokens
num_tokens_d_padded = num_tokens_d * self.pcp_world_size
input_ids_d = input_ids[:num_tokens_d]
input_ids_p = input_ids[num_tokens_d:num_tokens]
target_hidden_states_d_padded = target_hidden_states[:num_tokens_d_padded]
if num_tokens_d:
target_hidden_states_d = self.get_spec_decode_decode_hidden_states(
target_hidden_states_d_padded,
num_decode_reqs,
num_tokens_d,
)
else:
target_hidden_states_d = target_hidden_states_d_padded
target_hidden_states_p = target_hidden_states[num_tokens_d_padded:]
req_scheduled_tokens_p: dict[str, int] = {}
if num_prefill_reqs:
assert req_scheduled_tokens is not None
num_reqs = num_decode_reqs + num_prefill_reqs
for i, req_id in enumerate(req_ids[:num_reqs]):
if i >= num_decode_reqs:
req_scheduled_tokens_p[req_id] = req_scheduled_tokens[req_id]
(
num_tokens_p,
input_ids_p,
target_hidden_states_p,
max_query_len_p,
seq_lens_p,
cu_num_tokens_p,
) = self._split_spec_decode_pcp_prefill_input(
req_scheduled_tokens_p,
input_ids_p,
target_hidden_states_p,
)
num_tokens = num_tokens_d + num_tokens_p
if uses_mrope:
target_positions = target_positions[:, :num_tokens]
else:
target_positions = target_positions[:num_tokens]
input_ids = torch.cat([input_ids_d, input_ids_p], dim=0)
target_hidden_states = torch.cat([target_hidden_states_d, target_hidden_states_p], dim=0)
if num_decode_reqs:
token_indices_to_sample[:num_decode_reqs] = logits_indices[token_indices_to_sample[:num_decode_reqs]]
if num_prefill_reqs:
token_indices_to_sample[-num_prefill_reqs:] = logits_indices[-num_prefill_reqs:]
common_attn_metadata.num_actual_tokens = num_tokens
common_attn_metadata.max_query_len = max(self.decode_threshold, max_query_len_p)
common_attn_metadata.seq_lens[-num_prefill_reqs:] = seq_lens_p
if common_attn_metadata.seq_lens_cpu is not None:
common_attn_metadata.seq_lens_cpu[-num_prefill_reqs:] = seq_lens_p
if common_attn_metadata._seq_lens_cpu is not None:
common_attn_metadata._seq_lens_cpu[-num_prefill_reqs:] = seq_lens_p
query_start_loc_p = cu_num_tokens_p[1:] + common_attn_metadata.query_start_loc_cpu[num_decode_reqs].item()
common_attn_metadata.query_start_loc[-num_prefill_reqs:] = query_start_loc_p
common_attn_metadata.query_start_loc_cpu[-num_prefill_reqs:] = query_start_loc_p
return PCPSpecDecodeFirstPassInputs(
num_tokens=num_tokens,
input_ids=input_ids,
target_positions=target_positions,
target_hidden_states=target_hidden_states,
token_indices_to_sample=token_indices_to_sample,
long_seq_args=long_seq_args,
)
def _split_spec_decode_pcp_prefill_input(
self,
req_scheduled_tokens: dict[str, int],
input_ids: torch.Tensor,
target_hidden_states: torch.Tensor,
) -> tuple[int, torch.Tensor, torch.Tensor, int, torch.Tensor, torch.Tensor]:
"""
Split prefill input_ids and target_hidden_states in the PCP group.
The target hidden states already include PCP padding; this method
selects only the local PCP rank's prefill tokens and returns the
attention metadata fields affected by that split.
"""
if len(req_scheduled_tokens) == 0:
return (
0,
input_ids.new_zeros((0,)),
target_hidden_states.new_zeros((0, target_hidden_states.size(1))),
0,
torch.zeros((0,), dtype=torch.int32),
torch.tensor([0], dtype=torch.int32),
)
if self.pcp_use_hybrid_attn:
return self._split_spec_decode_pcp_prefill_input_hybrid(
req_scheduled_tokens,
input_ids,
target_hidden_states,
)
def _pcp_pad_and_split(num_tokens: int) -> tuple[list[int], int, int]:
num_pcp_padded_scheduled_tokens = cdiv(num_tokens, 2 * self.pcp_world_size) * 2 * self.pcp_world_size
pcp_pad = num_pcp_padded_scheduled_tokens - num_tokens
chunk_size = num_pcp_padded_scheduled_tokens // (2 * self.pcp_world_size)
req_position_cp: list[int] = []
req_position_cp.extend(
self.full_indices[self.pcp_world_rank * chunk_size : (self.pcp_world_rank + 1) * chunk_size]
)
req_position_cp.extend(
self.full_indices[
num_pcp_padded_scheduled_tokens
- (self.pcp_world_rank + 1) * chunk_size : num_pcp_padded_scheduled_tokens
- self.pcp_world_rank * chunk_size
]
)
return req_position_cp, num_pcp_padded_scheduled_tokens, pcp_pad
num_pcp_scheduled_tokens = []
ori_start_index = 0
pad_start_index = 0
pcp_split_input_ids_list = []
pcp_split_hidden_states_list = []
for ori_num_tokens in req_scheduled_tokens.values():
req_position_pcp, num_pcp_padded_scheduled_tokens, num_pcp_pad = _pcp_pad_and_split(ori_num_tokens)
actual_num_tokens = len(req_position_pcp)
num_pcp_scheduled_tokens.append(actual_num_tokens)
pad_input_ids = F.pad(
input_ids[ori_start_index : ori_start_index + ori_num_tokens],
(0, num_pcp_pad),
)
ori_start_index += ori_num_tokens
pcp_chunk_indices = [pad_start_index + pos for pos in req_position_pcp]
pcp_split_input_ids = pad_input_ids[req_position_pcp]
pcp_split_hidden_states = target_hidden_states[pcp_chunk_indices]
pcp_split_input_ids_list.append(pcp_split_input_ids)
pcp_split_hidden_states_list.append(pcp_split_hidden_states)
pad_start_index += num_pcp_padded_scheduled_tokens
num_tokens = sum(num_pcp_scheduled_tokens)
input_ids = torch.cat(pcp_split_input_ids_list)
target_hidden_states = torch.cat(pcp_split_hidden_states_list, dim=0)
max_query_len = max(num_pcp_scheduled_tokens)
seq_lens = torch.tensor(num_pcp_scheduled_tokens, dtype=torch.int32)
cu_num_tokens = torch.tensor(np.insert(np.cumsum(np.array(num_pcp_scheduled_tokens)), 0, 0))
return num_tokens, input_ids, target_hidden_states, max_query_len, seq_lens, cu_num_tokens
def _split_spec_decode_pcp_prefill_input_hybrid(
self,
req_scheduled_tokens: dict[str, int],
input_ids: torch.Tensor,
target_hidden_states: torch.Tensor,
) -> tuple[int, torch.Tensor, torch.Tensor, int, torch.Tensor, torch.Tensor]:
"""Linear-split prefill inputs for hybrid-attention PCP models."""
num_pcp_scheduled_tokens = []
global_offset = 0
pcp_split_input_ids_list = []
pcp_split_hidden_states_list = []
for ori_num_tokens in req_scheduled_tokens.values():
padded_tokens = cdiv(ori_num_tokens, 2 * self.pcp_world_size) * 2 * self.pcp_world_size
pcp_tokens = padded_tokens // self.pcp_world_size
num_pads = padded_tokens - ori_num_tokens
rank_start = self.pcp_world_rank * pcp_tokens
num_pcp_scheduled_tokens.append(pcp_tokens)
req_input_ids = input_ids[global_offset : global_offset + ori_num_tokens]
if num_pads > 0:
req_input_ids = F.pad(req_input_ids, (0, num_pads))
pcp_split_input_ids_list.append(req_input_ids[rank_start : rank_start + pcp_tokens])
req_hidden = target_hidden_states[global_offset : global_offset + ori_num_tokens]
if num_pads > 0:
req_hidden = F.pad(req_hidden, (0, 0, 0, num_pads))
pcp_split_hidden_states_list.append(req_hidden[rank_start : rank_start + pcp_tokens])
global_offset += ori_num_tokens
num_tokens = sum(num_pcp_scheduled_tokens)
input_ids = torch.cat(pcp_split_input_ids_list)
target_hidden_states = torch.cat(pcp_split_hidden_states_list, dim=0)
max_query_len = max(num_pcp_scheduled_tokens)
seq_lens = torch.tensor(num_pcp_scheduled_tokens, dtype=torch.int32)
cu_num_tokens = torch.tensor(np.insert(np.cumsum(np.array(num_pcp_scheduled_tokens)), 0, 0))
return num_tokens, input_ids, target_hidden_states, max_query_len, seq_lens, cu_num_tokens
def _get_spec_decode_mtp_slot_inputs(
self,
ori_token_indices_to_sample: torch.Tensor,
num_reqs: int,
num_speculative_tokens: int,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Build device-side CP slot indices for MTP draft requests."""
assert self.mtp_slot_pad is not None
query_start_loc = self.query_start_loc_pcp_full.gpu[: num_reqs + 1]
req_starts = query_start_loc[:num_reqs].to(torch.int64)
cu_num_tokens = query_start_loc[1 : num_reqs + 1].to(torch.int64)
query_lens = cu_num_tokens - req_starts
num_reject_tokens = cu_num_tokens - ori_token_indices_to_sample.to(torch.int64) - 1
num_accept_tokens = query_lens - num_reject_tokens
slot_idx_base = (
req_starts * self.pcp_world_size
+ self.pcp_req_offsets[:num_reqs] * (num_speculative_tokens - 1) * self.pcp_world_size
+ (num_accept_tokens - 1) * self.pcp_world_size
)
slot_indices = (slot_idx_base[:, None] + self.pcp_rank_offsets[: self.pcp_world_size]).reshape(-1)
return slot_indices, self.mtp_slot_pad
def prepare_spec_decode_mtp_drafting_inputs(
self,
common_attn_metadata: Any,
attn_metadata: Any,
ori_token_indices_to_sample: torch.Tensor | None,
batch_size: int,
num_decode_reqs: int,
is_prefill_batch: bool,
num_speculative_tokens: int,
) -> PCPSpecDecodeMTPInputs | None:
"""Prepare CP MTP metadata for decode and DCP-prefill batches."""
is_decode_only_batch = num_decode_reqs > 0 and not is_prefill_batch
is_dcp_prefill_batch = self.pcp_world_size == 1 and self.dcp_world_size > 1 and is_prefill_batch
if num_speculative_tokens <= 1 or not (is_decode_only_batch or is_dcp_prefill_batch):
return None
assert ori_token_indices_to_sample is not None
num_reqs = batch_size if is_dcp_prefill_batch else num_decode_reqs
slot_indices, slot_mapping = self._get_spec_decode_mtp_slot_inputs(
ori_token_indices_to_sample,
num_reqs,
num_speculative_tokens,
)
seq_lens = getattr(attn_metadata, "seq_lens", None)
seq_lens_cpu = getattr(attn_metadata, "seq_lens_cpu", None)
if seq_lens is None:
assert seq_lens_cpu is not None
seq_lens = seq_lens_cpu
seq_lens = seq_lens[:batch_size].clone()
if seq_lens_cpu is not None:
seq_lens_cpu = seq_lens_cpu[:batch_size].clone()
common_attn_metadata.block_table_tensor = common_attn_metadata.block_table_tensor[:batch_size]
return PCPSpecDecodeMTPInputs(
seq_lens=seq_lens,
seq_lens_cpu=seq_lens_cpu,
slot_indices=slot_indices,
slot_mapping=slot_mapping,
)
def rebuild_async_spec_decode_inputs(
self,
*,
use_async_spec_decode: bool,
valid_sampled_token_count_gpu: torch.Tensor | None,
prev_req_id_to_index: Any,
prev_positions_gpu: torch.Tensor | None,
with_prefill: bool,
enable_prompt_embeds: bool,
has_req_prompt_embeds: bool,
supports_mm_inputs: bool,
num_reqs: int,
total_num_scheduled_tokens: int,
req_indices: np.ndarray,
req_indices_gpu: torch.Tensor,
position_pcp: np.ndarray | None,
query_pos_gpu: torch.Tensor,
query_pos_np: np.ndarray,
positions: torch.Tensor,
positions_np: np.ndarray,
num_computed_tokens: torch.Tensor,
num_computed_tokens_cpu: np.ndarray,
prev_positions_np: np.ndarray,
prev_num_draft_tokens_np: np.ndarray,
valid_sampled_token_count_event: Any | None,
valid_sampled_token_count_cpu: torch.Tensor | None,
input_batch: NPUInputBatch,
input_ids: CpuGpuBuffer,
scheduler_output: "SchedulerOutput",
arange_np: np.ndarray,
cu_num_tokens: np.ndarray,
draft_token_ids: torch.Tensor | None,
num_spec_tokens: int,
prepare_input_ids: Callable[["SchedulerOutput", int, int, np.ndarray], None],
) -> PCPAsyncSpecDecodeRebuildResult:
"""Rebuild CP/spec inputs after async accepted-token correction."""
should_rebuild = (
self.pcp_world_size * self.dcp_world_size > 1
and use_async_spec_decode
and valid_sampled_token_count_gpu is not None
and bool(prev_req_id_to_index)
and self.num_decode_reqs > 0
)
if not should_rebuild:
return PCPAsyncSpecDecodeRebuildResult(
rebuilt=False,
positions_ready_on_device=False,
)
can_rebuild_on_device = (
prev_positions_gpu is not None
and not with_prefill
and not enable_prompt_embeds
and not has_req_prompt_embeds
and not supports_mm_inputs
)
if can_rebuild_on_device:
if self.pcp_world_size > 1:
assert position_pcp is not None
position_offsets_gpu = (
torch.from_numpy(position_pcp[:total_num_scheduled_tokens])
.pin_memory()
.to(
dtype=torch.int64,
device=self.device,
non_blocking=True,
)
)
else:
position_offsets_gpu = query_pos_gpu[:total_num_scheduled_tokens].to(torch.int64)
positions_gpu = num_computed_tokens[req_indices_gpu].to(torch.int64) + position_offsets_gpu
positions[:total_num_scheduled_tokens].copy_(positions_gpu)
num_tokens_full = self.async_rebuild_num_tokens_full
query_start_loc_full = self.query_start_loc_pcp_full.gpu[: num_reqs + 1]
query_lens_full = (query_start_loc_full[1:] - query_start_loc_full[:-1]).to(torch.int64)
req_indices_full_gpu = torch.repeat_interleave(
self.pcp_req_offsets[:num_reqs],
query_lens_full,
output_size=num_tokens_full,
)
token_offsets_full = torch.arange(
num_tokens_full,
dtype=torch.int64,
device=self.device,
)
positions_full_gpu = (
num_computed_tokens[req_indices_full_gpu].to(torch.int64)
+ token_offsets_full
- query_start_loc_full[req_indices_full_gpu].to(torch.int64)
)
if self.pcp_world_size > 1:
input_batch.block_table.compute_slot_mapping(
num_reqs,
query_start_loc_full,
positions_full_gpu,
)
extra_tokens = self.decode_threshold - 2
if extra_tokens > 0 and not with_prefill:
mtp_lens = query_lens_full + extra_tokens
num_tokens_mtp = num_tokens_full + num_reqs * extra_tokens
req_indices_mtp = torch.repeat_interleave(
self.pcp_req_offsets[:num_reqs],
mtp_lens,
output_size=num_tokens_mtp,
)
mtp_start_loc = torch.empty(
num_reqs + 1,
dtype=torch.int64,
device=self.device,
)
mtp_start_loc[0].fill_(0)
mtp_start_loc[1:] = torch.cumsum(mtp_lens, dim=0)
mtp_offsets = torch.arange(
num_tokens_mtp,
dtype=torch.int64,
device=self.device,
)
positions_mtp = (
num_computed_tokens[req_indices_mtp].to(torch.int64) + mtp_offsets - mtp_start_loc[req_indices_mtp]
)
input_batch.block_table.compute_slot_mapping_draft(
req_indices_mtp,
positions_mtp,
)
mtp_slot_ori = input_batch.block_table.block_tables[0].slot_mapping.gpu[:num_tokens_mtp]
num_tokens_mtp_pad = num_tokens_mtp * self.pcp_world_size
if self.mtp_slot_pad is None or self.mtp_slot_pad.numel() < num_tokens_mtp_pad:
self.mtp_slot_pad = torch.empty(
num_tokens_mtp_pad,
dtype=torch.int32,
device=self.device,
)
mtp_slot_pad = self.mtp_slot_pad[:num_tokens_mtp_pad]
mtp_slot_pad.fill_(-1)
mtp_slot_pad[:: self.pcp_world_size].copy_(mtp_slot_ori)
return PCPAsyncSpecDecodeRebuildResult(
rebuilt=True,
positions_ready_on_device=True,
)
base_num_computed_tokens_np = num_computed_tokens_cpu[:num_reqs].copy()
assert valid_sampled_token_count_event is not None
assert valid_sampled_token_count_cpu is not None
valid_sampled_token_count_event.synchronize()
correct_optimistic_seq_lens_cpu(
base_num_computed_tokens_np,
prev_positions_np,
prev_num_draft_tokens_np,
valid_sampled_token_count_cpu.numpy(),
num_reqs,
)
if self.pcp_world_size > 1:
assert position_pcp is not None
position_offsets = position_pcp
else:
position_offsets = query_pos_np
np.add(
base_num_computed_tokens_np[req_indices],
position_offsets[:total_num_scheduled_tokens],
out=positions_np,
)
token_indices = positions_np[:total_num_scheduled_tokens] + req_indices * input_batch.token_ids_cpu.shape[1]
torch.index_select(
input_batch.token_ids_cpu_tensor.flatten(),
0,
torch.from_numpy(token_indices),
out=input_ids.cpu[:total_num_scheduled_tokens],
)
input_ids.copy_to_gpu(total_num_scheduled_tokens)
prepare_input_ids(
scheduler_output,
num_reqs,
total_num_scheduled_tokens,
cu_num_tokens,
)
req_indices_full = self.async_rebuild_req_indices_full
cu_num_tokens_full = self.async_rebuild_cu_num_tokens_full
num_tokens_full = self.async_rebuild_num_tokens_full
assert req_indices_full is not None
assert cu_num_tokens_full is not None
token_counts = np.diff(np.concatenate(([0], cu_num_tokens_full)))
token_starts = np.repeat(cu_num_tokens_full - token_counts, token_counts)
query_pos = arange_np[:num_tokens_full] - token_starts
positions_full = np.empty(num_tokens_full, dtype=np.int64)
np.add(
base_num_computed_tokens_np[req_indices_full],
query_pos,
out=positions_full,
)
if self.pcp_world_size > 1:
pre_pcp_query_start_loc = torch.zeros(
num_reqs + 1,
dtype=torch.int32,
device=self.device,
)
pre_pcp_query_start_loc[1 : num_reqs + 1] = torch.from_numpy(cu_num_tokens_full).to(
dtype=torch.int32, device=self.device
)
input_batch.block_table.compute_slot_mapping(
num_reqs,
pre_pcp_query_start_loc,
torch.from_numpy(positions_full).to(self.device),
)
self.generate_pcp_mtp_input(
num_tokens_full,
scheduler_output.num_scheduled_tokens,
with_prefill,
input_batch,
arange_np,
req_indices_full,
positions_full,
cu_num_tokens_full,
draft_token_ids,
scheduler_output,
num_spec_tokens,
precomputed_positions_np=positions_full,
prev_positions=prev_positions_gpu,
)
return PCPAsyncSpecDecodeRebuildResult(
rebuilt=True,
positions_ready_on_device=False,
)
def generate_pcp_mtp_input(
self,
total_num_scheduled_tokens: int,
num_scheduled_tokens: dict[str, int],
with_prefill: bool = True,
input_batch=None,
arange_np=None,
req_indices=None,
positions_np=None,
cu_num_tokens=None,
draft_token_ids=None,
scheduler_output=None,
num_spec_tokens=None,
precomputed_positions_np=None,
prev_positions: torch.Tensor | None = None,
):
"""
While pcp > 1, model inputs (input_ids, position, etc.) are split across pcp group,
but mtp need to shift original input_ids before pcp splitting,
so we record original input_ids here.
"""
total_num_scheduled_tokens_pcp_full = total_num_scheduled_tokens
num_scheduled_tokens_pcp_full = np.empty(self.num_reqs, dtype=np.int32)
for i, req_id in enumerate(input_batch.req_ids):
num_scheduled_tokens_pcp_full[i] = num_scheduled_tokens[req_id]
req_indices_pcp_full = np.repeat(arange_np[: self.num_reqs], num_scheduled_tokens_pcp_full)
cu_num_tokens_pcp_full = np.cumsum(num_scheduled_tokens_pcp_full)
self.query_start_loc_pcp_full.np[0] = 0
self.query_start_loc_pcp_full.np[1 : self.num_reqs + 1] = cu_num_tokens_pcp_full
self.query_start_loc_pcp_full.np[self.num_reqs + 1 :].fill(-1)
cumsums_offsets_pcp_full = np.repeat(
cu_num_tokens_pcp_full - num_scheduled_tokens_pcp_full, num_scheduled_tokens_pcp_full
)
arange_pcp_full = arange_np[:total_num_scheduled_tokens_pcp_full] - cumsums_offsets_pcp_full
positions_pcp_full_np = self.positions_pcp_full_np[:total_num_scheduled_tokens_pcp_full]
if precomputed_positions_np is None:
np.add(
input_batch.num_computed_tokens_cpu[req_indices_pcp_full],
arange_pcp_full,
out=positions_pcp_full_np,
)
else:
np.copyto(
positions_pcp_full_np,
precomputed_positions_np[:total_num_scheduled_tokens_pcp_full],
)
token_indices_pcp_full = positions_pcp_full_np + req_indices_pcp_full * input_batch.token_ids_cpu.shape[1]
torch.index_select(
input_batch.token_ids_cpu_tensor.flatten(),
0,
torch.from_numpy(token_indices_pcp_full),
out=self.input_ids_pcp_full.cpu[:total_num_scheduled_tokens_pcp_full],
)
self.input_ids_pcp_full.copy_to_gpu(total_num_scheduled_tokens_pcp_full)
copy_snapshot_to_gpu(self.query_start_loc_pcp_full)
if self.use_async_scheduling:
self._update_input_ids_pcp_full_ids(
input_batch,
draft_token_ids,
scheduler_output,
total_num_scheduled_tokens,
cu_num_tokens_pcp_full,
num_spec_tokens,
prev_positions,
)
self.cu_num_tokens_pcp_full = cu_num_tokens_pcp_full
if self.use_async_scheduling and precomputed_positions_np is None:
# Save full pre-CP layout so async scheduling can rebuild
# speculative inputs with corrected num_computed_tokens.
self.async_rebuild_req_indices_full = req_indices.copy()
self.async_rebuild_cu_num_tokens_full = cu_num_tokens.copy()
self.async_rebuild_num_tokens_full = total_num_scheduled_tokens
# For mtpx, pre-allocate mtp slot_mapping here
needs_dcp_prefill_slots = self.pcp_world_size == 1 and self.dcp_world_size > 1 and with_prefill
if self.decode_threshold > 2 and (not with_prefill or needs_dcp_prefill_slots):
num_tokens_ori = sum(list(num_scheduled_tokens.values()))
num_tokens_mtp = num_tokens_ori + self.num_reqs * (self.decode_threshold - 2)
num_tokens_mtp_pad = num_tokens_mtp * self.pcp_world_size
req_indices_split = np.array_split(req_indices, cu_num_tokens)[: self.num_reqs]
positions_split = np.array_split(positions_np, cu_num_tokens)[: self.num_reqs]
for req_idx in range(self.num_reqs):
ori_req_indice = req_indices_split[req_idx]
ori_position = positions_split[req_idx]
req_indices_split[req_idx] = np.append(
ori_req_indice, np.repeat(ori_req_indice[-1], self.decode_threshold - 2)
)
positions_split[req_idx] = np.append(
ori_position, np.arange(ori_position[-1] + 1, ori_position[-1] + self.decode_threshold - 1)
)
req_indices_mtp = np.concatenate(req_indices_split)
positions_mtp = np.concatenate(positions_split)
input_batch.block_table.compute_slot_mapping_draft(req_indices_mtp, positions_mtp)
mtp_slot_ori = input_batch.block_table.block_tables[0].slot_mapping.cpu[:num_tokens_mtp]
unpad_mask = np.repeat(False, num_tokens_mtp_pad)
unpad_mask[:: self.pcp_world_size] = True
self.mtp_slot_pad = torch.full([num_tokens_mtp_pad], -1, dtype=torch.int32, pin_memory=True)
self.mtp_slot_pad[unpad_mask] = mtp_slot_ori
self.mtp_slot_pad = self.mtp_slot_pad.to(self.device, non_blocking=True)
def _update_input_ids_pcp_full_ids(
self,
input_batch,
draft_token_ids,
scheduler_output: "SchedulerOutput",
total_num_scheduled_tokens: int,
cu_num_tokens: np.ndarray,
num_spec_tokens: int,
prev_positions: torch.Tensor | None = None,
) -> None:
"""Prepare the input IDs for the current batch.
Carefully handles the `prev_sampled_token_ids` which can be cached
from the previous engine iteration, in which case those tokens on the
GPU need to be copied into the corresponding slots into input_ids."""
if input_batch.prev_sampled_token_ids is None or input_batch.prev_req_id_to_index is None:
return
if prev_positions is not None:
num_reqs = self.num_reqs
query_start_loc = self.query_start_loc_pcp_full.gpu[: num_reqs + 1]
query_lens = query_start_loc[1:] - query_start_loc[:-1]
is_decode_req = self.pcp_req_offsets[:num_reqs] < self.num_decode_reqs
draft_lens = torch.where(
is_decode_req,
torch.clamp(query_lens - 1, min=0),
torch.zeros_like(query_lens),
)
sample_indices = (query_start_loc[1:] - 1 - draft_lens).to(torch.int64)
prev_positions = prev_positions[:num_reqs].to(torch.int64)
common_mask = prev_positions >= 0
safe_prev_positions = prev_positions.clamp(min=0)
sampled_src = input_batch.prev_sampled_token_ids[safe_prev_positions, 0]
sampled_src = sampled_src.to(dtype=self.input_ids_pcp_full.gpu.dtype)
sampled_src = torch.where(
common_mask,
sampled_src,
self.input_ids_pcp_full.gpu[sample_indices],
)
self.input_ids_pcp_full.gpu.scatter_(
dim=0,
index=sample_indices,
src=sampled_src,
)
if draft_token_ids is None or not num_spec_tokens:
return
assert isinstance(draft_token_ids, torch.Tensor)
if num_spec_tokens > self.pcp_spec_token_offsets.numel():
spec_offsets = torch.arange(
num_spec_tokens,
dtype=torch.int64,
device=self.device,
)
else:
spec_offsets = self.pcp_spec_token_offsets[:num_spec_tokens]
spec_offsets = spec_offsets.unsqueeze(0)
draft_lens = torch.clamp(
draft_lens.to(torch.int64),
max=num_spec_tokens,
)
spec_mask = common_mask.unsqueeze(1) & (spec_offsets < draft_lens.unsqueeze(1))
sample_indices_2d = sample_indices.unsqueeze(1)
spec_dst = sample_indices_2d + 1 + spec_offsets
safe_dst = torch.where(
spec_mask,
spec_dst,
sample_indices_2d.expand(-1, num_spec_tokens),
)
spec_src_indices = safe_prev_positions.unsqueeze(1) * num_spec_tokens + spec_offsets
draft_token_ids = draft_token_ids.to(dtype=torch.int32)
spec_src = draft_token_ids.flatten()[spec_src_indices]
spec_src = torch.where(
spec_mask,
spec_src,
self.input_ids_pcp_full.gpu[safe_dst],
)
self.input_ids_pcp_full.gpu.scatter_(
dim=0,
index=safe_dst.reshape(-1),
src=spec_src.reshape(-1),
)
return
# Async scheduling case, where some decode requests from the previous
# iteration won't have entries in input_ids_cpu and need to be copied
# on the GPU from prev_sampled_token_ids.
prev_req_id_to_index = input_batch.prev_req_id_to_index
sample_flattened_indices: list[int] = []
spec_flattened_indices: list[int] = []
prev_common_req_indices: list[int] = []
prev_draft_token_indices: list[int] = []
total_num_spec_tokens = 0
scheduled_spec_tokens = scheduler_output.scheduled_spec_decode_tokens
for req_id, cur_index in input_batch.req_id_to_index.items():
if (prev_index := prev_req_id_to_index.get(req_id)) is not None:
prev_common_req_indices.append(prev_index)
# We need to compute the flattened input_ids index of the
# last token in each common request.
draft_len = len(scheduled_spec_tokens.get(req_id, ()))
total_num_spec_tokens += draft_len
flattened_index = cu_num_tokens[cur_index].item() - 1
# example: cu_num_tokens = [2, 5, 8], draft_tokens = [1, 2, 2]
# sample_flattened_indices = [0, 2, 5]
# spec_flattened_indices = [1, 3, 4, 6, 7]
sample_flattened_indices.append(flattened_index - draft_len)
spec_flattened_indices.extend(range(flattened_index - draft_len + 1, flattened_index + 1))
start = prev_index * num_spec_tokens
# prev_draft_token_indices is used to find which draft_tokens_id
# should be copied to input_ids
# example: prev draft_tokens_id [[1,2], [3,4], [5, 6]]
# flatten draft_tokens_id [1,2,3,4,5,6]
# draft_len of each request [1, 2, 1]
# then prev_draft_token_indices is [0, 2, 3, 4]
prev_draft_token_indices.extend(range(start, start + draft_len))
num_common_tokens = len(sample_flattened_indices)
if num_common_tokens == 0:
# No requests in common with the previous iteration
# So input_ids.cpu will have all the input ids.
return
# Upload the index tensors asynchronously so the scatter can be non-blocking.
sampled_tokens_index_tensor = torch.tensor(sample_flattened_indices, dtype=torch.int64, device=self.device)
prev_common_req_indices_tensor = torch.tensor(prev_common_req_indices, dtype=torch.int64, device=self.device)
self.input_ids_pcp_full.gpu.scatter_(
dim=0,
index=sampled_tokens_index_tensor,
src=input_batch.prev_sampled_token_ids[prev_common_req_indices_tensor, 0],
)
# Scatter the draft tokens after the sampled tokens are scattered.
if draft_token_ids is None or not spec_flattened_indices:
return
assert isinstance(draft_token_ids, torch.Tensor)
draft_tokens_index_tensor = torch.tensor(spec_flattened_indices, dtype=torch.int64, device=self.device)
prev_draft_token_indices_tensor = torch.tensor(prev_draft_token_indices, dtype=torch.int64, device=self.device)
# because input_ids dtype is torch.int32,
# so convert draft_token_ids to torch.int32 here.
draft_token_ids = draft_token_ids.to(dtype=torch.int32)
self.input_ids_pcp_full.gpu.scatter_(
dim=0,
index=draft_tokens_index_tensor,
src=draft_token_ids.flatten()[prev_draft_token_indices_tensor],
)
def _get_cp_local_seq_lens(
self,
seq_lens: torch.Tensor,
pcp_world_size: int = 1,
dcp_world_size: int = 1,
cp_kv_cache_interleave_size: int = 1,
) -> torch.Tensor:
"""While using pcp or dcp, kv_cache size stored on each rank may be different,
use this function to calculate split decode seq_lens of each (p/d)cp rank.
"""
num_requests = seq_lens.size(0)
total_world_size = pcp_world_size * dcp_world_size
seq_lens_tiled = seq_lens.unsqueeze(-1).repeat(1, total_world_size)
rank_offsets = (
torch.arange(
total_world_size,
dtype=seq_lens.dtype,
device=seq_lens.device,
)
.unsqueeze(0)
.repeat(num_requests, 1)
)
base = seq_lens_tiled // cp_kv_cache_interleave_size // total_world_size * cp_kv_cache_interleave_size
remainder = seq_lens_tiled - base * total_world_size
remainder = torch.clip(
remainder - rank_offsets * cp_kv_cache_interleave_size,
0,
cp_kv_cache_interleave_size,
)
dcp_local_seq_lens = (base + remainder).reshape([-1, pcp_world_size, dcp_world_size])
return dcp_local_seq_lens
@staticmethod
def _is_mla_kv_cache_spec(kv_cache_spec: Any) -> bool:
from vllm_ascend.core.kv_cache_interface import AscendMLAAttentionSpec
return isinstance(kv_cache_spec, AscendMLAAttentionSpec)
@staticmethod
def _is_sfa_dcp_metadata_builder(attn_metadata_builder: Any | None) -> bool:
if attn_metadata_builder is None:
return False
from vllm_ascend.attention.context_parallel.sfa_cp import AscendSFADCPMetadataBuilder
return isinstance(attn_metadata_builder, AscendSFADCPMetadataBuilder)
def update_spec_decode_drafting_cp_metadata(
self,
attn_metadata: Any,
kv_cache_spec: Any,
seq_lens: torch.Tensor,
draft_index: int,
seq_lens_cpu: torch.Tensor | None = None,
attn_metadata_builder: Any | None = None,
) -> None:
"""Update per-draft-step CP seq-len metadata after metadata build."""
is_mla = self._is_mla_kv_cache_spec(kv_cache_spec)
is_sfa_dcp = self._is_sfa_dcp_metadata_builder(attn_metadata_builder)
seq_lens_for_cp = seq_lens
if not is_mla and seq_lens_cpu is not None:
seq_lens_for_cp = seq_lens_cpu
num_computed_tokens_of_pcp_dcp = self._get_cp_local_seq_lens(
seq_lens_for_cp + draft_index + 1,
self.pcp_world_size,
self.dcp_world_size,
self.vllm_config.parallel_config.cp_kv_cache_interleave_size,
)
cp_seq_len = num_computed_tokens_of_pcp_dcp[:, self.pcp_world_rank, self.dcp_world_rank]
if is_sfa_dcp:
dcp_context = attn_metadata.dcp_context
assert dcp_context is not None
dcp_seq_lens = dcp_context.seq_lens
sfa_cp_seq_len = cp_seq_len.to(
device=dcp_seq_lens.device,
dtype=dcp_seq_lens.dtype,
non_blocking=True,
)
dcp_seq_lens[: sfa_cp_seq_len.shape[0]].copy_(sfa_cp_seq_len, non_blocking=True)
dcp_seq_lens[sfa_cp_seq_len.shape[0] :].fill_(0)
elif is_mla:
attn_metadata.decode.cp_seq_len = cp_seq_len
else:
attn_metadata.decode_meta.num_computed_tokens_of_pcp_dcp = num_computed_tokens_of_pcp_dcp.numpy()
def generate_pcp_metadata(
self,
total_num_scheduled_tokens: int,
query_lens: torch.Tensor,
input_batch: "NPUInputBatch",
num_scheduled_tokens: np.ndarray | None,
block_table_tensor: torch.Tensor,
num_reqs_padded: int,
num_reqs: int,
fixed_decode_seq_lens_cpu: np.ndarray | None = None,
):
from vllm_ascend.attention.utils import AscendPrefillContextParallelMetadata
if self.pcp_world_size > 1 and self.pcp_use_hybrid_attn:
assert self.num_scheduled_tokens_padded is not None
total_num_scheduled_tokens = self.num_scheduled_tokens_padded.sum()
num_actual_tokens_pcp_padded = total_num_scheduled_tokens * self.pcp_world_size
self.num_actual_tokens_pcp_padded = num_actual_tokens_pcp_padded
long_seq_metadata = None
ori_query_lens_cpu = self.query_lens_pcp_full.cpu[:num_reqs_padded]
if self.pcp_world_size * self.dcp_world_size > 1:
assert num_scheduled_tokens is not None
if fixed_decode_seq_lens_cpu is not None:
decode_context_lens = fixed_decode_seq_lens_cpu[: self.num_decode_reqs]
else:
decode_context_lens = (
input_batch.num_computed_tokens_cpu[: self.num_decode_reqs]
+ num_scheduled_tokens[: self.num_decode_reqs]
)
prefill_context_lens = input_batch.num_computed_tokens_cpu[self.num_decode_reqs : self.num_reqs]
context_lens = np.concatenate([decode_context_lens, prefill_context_lens])
num_computed_tokens_of_pcp_dcp = self._get_cp_local_seq_lens(
torch.tensor(context_lens),
self.pcp_world_size,
self.dcp_world_size,
self.vllm_config.parallel_config.cp_kv_cache_interleave_size,
)
if logger.isEnabledFor(logging.DEBUG):
logger.debug(
"[PCP][DFX] num_computed_tokens_of_pcp_dcp=%s",
num_computed_tokens_of_pcp_dcp.tolist(),
)
pcp_unpad_mask = self.pcp_unpad_mask_cpu[: self.pcp_padded_tokens_length]
long_seq_metadata = AscendPrefillContextParallelMetadata(
pcp_use_hybrid_attn=self.pcp_use_hybrid_attn,
num_actual_tokens_pcp_padded=num_actual_tokens_pcp_padded,
num_computed_tokens_of_pcp_dcp=num_computed_tokens_of_pcp_dcp.numpy(),
pcp_unpad_mask=torch.from_numpy(pcp_unpad_mask),
pcp_padded_tokens_fla=self.pcp_padded_tokens_fla,
query_lens_pcp_full_cpu=ori_query_lens_cpu,
max_query_len_pcp_full=ori_query_lens_cpu.max().item(),
)
if self.pcp_world_size > 1:
q_head_idx, q_tail_idx = [], []
kv_with_q_head_nomask_idx, kv_with_q_head_mask_idx = [], []
kv_with_q_tail_nomask_idx, kv_with_q_tail_mask_idx = [], []
kv_tail_proj_idx: list[int] = []
kv_with_q_head_attn_idx_in_tail, kv_with_q_tail_attn_idx_in_tail = [], []
split_with_q_head_nomask_idx_reqs = []
split_kv_with_q_tail_nomask_idx_reqs = []
chunk_seqlens = []
kv_with_q_head_nomask_seqlens, kv_with_q_tail_nomask_seqlens = [], []
head_actual_seq_lengths_kv, tail_actual_seq_lengths_kv = [], []
q_req_offset = 0
kv_req_offset = 0
q_head_chunk_id = self.pcp_world_rank
q_tail_chunk_id = self.pcp_world_size * 2 - 1 - self.pcp_world_rank
for i, seq_len in enumerate(query_lens):
if i < self.num_decode_reqs:
continue
chunk_len = seq_len // 2
chunk_seqlens.append(chunk_len)
q_head_idx.extend(list(range(q_req_offset, q_req_offset + chunk_len)))
kv_with_q_head_nomask_idx.extend(
list(range(kv_req_offset, kv_req_offset + chunk_len * q_head_chunk_id))
)
kv_with_q_head_mask_idx.extend(
list(
range(
kv_req_offset + chunk_len * q_head_chunk_id,
kv_req_offset + chunk_len * (q_head_chunk_id + 1),
)
)
)
kv_with_q_head_nomask_seqlens.append(chunk_len * q_head_chunk_id)
split_with_q_head_nomask_idx_reqs.append(
list(range(kv_req_offset, kv_req_offset + chunk_len * q_head_chunk_id))
)
q_tail_idx.extend(list(range(q_req_offset + chunk_len, q_req_offset + chunk_len * 2)))
kv_with_q_tail_nomask_idx.extend(
list(range(kv_req_offset, kv_req_offset + chunk_len * q_tail_chunk_id))
)
kv_with_q_tail_mask_idx.extend(
list(
range(
kv_req_offset + chunk_len * q_tail_chunk_id,
kv_req_offset + chunk_len * (q_tail_chunk_id + 1),
)
)
)
kv_with_q_tail_nomask_seqlens.append(chunk_len * q_tail_chunk_id)
split_kv_with_q_tail_nomask_idx_reqs.append(
list(range(kv_req_offset, kv_req_offset + chunk_len * q_tail_chunk_id))
)
tail_proj_offset = len(kv_tail_proj_idx)
tail_proj_len = chunk_len * (q_tail_chunk_id + 1)
kv_tail_proj_idx.extend(list(range(kv_req_offset, kv_req_offset + tail_proj_len)))
kv_with_q_head_attn_idx_in_tail.extend(
list(range(tail_proj_offset, tail_proj_offset + chunk_len * (q_head_chunk_id + 1)))
)
kv_with_q_tail_attn_idx_in_tail.extend(
list(range(tail_proj_offset, tail_proj_offset + tail_proj_len))
)
head_actual_seq_lengths_kv.append(len(kv_with_q_head_attn_idx_in_tail))
tail_actual_seq_lengths_kv.append(len(kv_with_q_tail_attn_idx_in_tail))
q_req_offset += seq_len
kv_req_offset += seq_len * self.pcp_world_size
q_head_idx_tensor = self._list_to_tensor(q_head_idx, self.device)
q_tail_idx_tensor = self._list_to_tensor(q_tail_idx, self.device)
self.q_head_idx_tensor = q_head_idx_tensor
self.q_tail_idx_tensor = q_tail_idx_tensor
q_full_idx = torch.cat([q_head_idx_tensor, q_tail_idx_tensor])
q_full_idx = q_full_idx.to(torch.float32).argsort().to(torch.int32)
self.q_full_idx = q_full_idx
self.kv_idx_names = {
"kv_with_q_head_nomask_idx_tensor": kv_with_q_head_nomask_idx,
"kv_with_q_head_mask_idx_tensor": kv_with_q_head_mask_idx,
"kv_with_q_tail_nomask_idx_tensor": kv_with_q_tail_nomask_idx,
"kv_with_q_tail_mask_idx_tensor": kv_with_q_tail_mask_idx,
"kv_tail_proj_idx_tensor": kv_tail_proj_idx,
"kv_with_q_head_attn_idx_in_tail_tensor": kv_with_q_head_attn_idx_in_tail,
"kv_with_q_tail_attn_idx_in_tail_tensor": kv_with_q_tail_attn_idx_in_tail,
}
for key, value in self.kv_idx_names.items():
tensor_npu = self._list_to_tensor(value, self.device)
self.kv_idx_names[key] = tensor_npu
attn_chunk_seqlens = torch.tensor(chunk_seqlens, dtype=torch.int32)
attn_mask_seqlens = torch.cumsum(torch.tensor(chunk_seqlens, dtype=torch.int32), dim=0).tolist()
head_attn_nomask_seqlens = torch.cumsum(
torch.tensor(kv_with_q_head_nomask_seqlens, dtype=torch.int32), dim=0
).tolist()
tail_attn_nomask_seqlens = torch.cumsum(
torch.tensor(kv_with_q_tail_nomask_seqlens, dtype=torch.int32), dim=0
).tolist()
self.extra_long_seq_kwargs = {
"attn_mask_seqlens": attn_mask_seqlens,
"head_attn_nomask_seqlens": head_attn_nomask_seqlens,
"tail_attn_nomask_seqlens": tail_attn_nomask_seqlens,
"head_actual_seq_lengths_kv": head_actual_seq_lengths_kv,
"tail_actual_seq_lengths_kv": tail_actual_seq_lengths_kv,
}
long_seq_metadata.pcp_allgather_restore_idx = self.pcp_allgather_restore_idx.gpu[
:num_actual_tokens_pcp_padded
]
if self.pcp_use_hybrid_attn:
long_seq_metadata.pcp_exit_fa_scatter_idx = self.pcp_exit_fa_scatter_idx.gpu[
: num_scheduled_tokens.sum() - self.num_decode_tokens
]
long_seq_metadata.pcp_fa_query_idx = self.pcp_fa_query_idx[
: num_actual_tokens_pcp_padded // self.pcp_world_size - self.num_decode_tokens
]
actual_qkv_len = int(pcp_unpad_mask.sum()) + self.num_decode_tokens * (self.pcp_world_size - 1)
long_seq_metadata.pcp_enter_fa_restore_idx = self.pcp_enter_fa_restore_idx[:actual_qkv_len]
if actual_qkv_len < num_actual_tokens_pcp_padded:
long_seq_metadata.pcp_fa_padding_restore_idx = self.pcp_fa_padding_restore_idx[
:num_actual_tokens_pcp_padded
]
else:
long_seq_metadata.pcp_fa_padding_restore_idx = None
if logger.isEnabledFor(logging.DEBUG):
logger.debug(
"[PCP][DFX] long_seq_metadata reorder idx: "
"pcp_allgather_restore_idx=%s, "
"pcp_exit_fa_scatter_idx=%s, "
"pcp_enter_fa_restore_idx=%s, "
"pcp_fa_padding_restore_idx=%s",
long_seq_metadata.pcp_allgather_restore_idx.detach().cpu().tolist(),
long_seq_metadata.pcp_exit_fa_scatter_idx.detach().cpu().tolist(),
long_seq_metadata.pcp_enter_fa_restore_idx.detach().cpu().tolist(),
long_seq_metadata.pcp_fa_padding_restore_idx.detach().cpu().tolist()
if long_seq_metadata.pcp_fa_padding_restore_idx is not None
else None,
)
long_seq_metadata.max_num_tokens_across_pcp = self.max_num_tokens_across_pcp
long_seq_metadata.total_num_scheduled_tokens = self.total_num_scheduled_tokens
long_seq_metadata.q_head_idx_tensor = self.q_head_idx_tensor
long_seq_metadata.q_tail_idx_tensor = self.q_tail_idx_tensor
long_seq_metadata.q_full_idx = self.q_full_idx
long_seq_metadata.kv_with_q_head_nomask_idx_tensor = self.kv_idx_names[
"kv_with_q_head_nomask_idx_tensor"
]
long_seq_metadata.kv_with_q_head_mask_idx_tensor = self.kv_idx_names["kv_with_q_head_mask_idx_tensor"]
long_seq_metadata.kv_with_q_tail_nomask_idx_tensor = self.kv_idx_names[
"kv_with_q_tail_nomask_idx_tensor"
]
long_seq_metadata.kv_with_q_tail_mask_idx_tensor = self.kv_idx_names["kv_with_q_tail_mask_idx_tensor"]
long_seq_metadata.kv_tail_proj_idx_tensor = self.kv_idx_names["kv_tail_proj_idx_tensor"]
long_seq_metadata.kv_with_q_head_attn_idx_in_tail_tensor = self.kv_idx_names[
"kv_with_q_head_attn_idx_in_tail_tensor"
]
long_seq_metadata.kv_with_q_tail_attn_idx_in_tail_tensor = self.kv_idx_names[
"kv_with_q_tail_attn_idx_in_tail_tensor"
]
long_seq_metadata.attn_mask_seqlens = self.extra_long_seq_kwargs["attn_mask_seqlens"]
long_seq_metadata.head_attn_nomask_seqlens = self.extra_long_seq_kwargs["head_attn_nomask_seqlens"]
long_seq_metadata.tail_attn_nomask_seqlens = self.extra_long_seq_kwargs["tail_attn_nomask_seqlens"]
long_seq_metadata.head_actual_seq_lengths_kv = self.extra_long_seq_kwargs["head_actual_seq_lengths_kv"]
long_seq_metadata.tail_actual_seq_lengths_kv = self.extra_long_seq_kwargs["tail_actual_seq_lengths_kv"]
long_seq_metadata.attn_chunk_seqlens = attn_chunk_seqlens
# Generate MTP attention masks for decode requests when cp_size > 1
# with speculative decoding.
if (
self.dcp_world_size * self.pcp_world_size > 1
and self.speculative_config
and num_scheduled_tokens is not None
):
# Generate the mask contents for the real decode requests.
if self.num_decode_reqs > 0:
decode_num_scheduled_tokens = num_scheduled_tokens[: self.num_decode_reqs]
if fixed_decode_seq_lens_cpu is not None:
decode_num_computed_tokens = (
fixed_decode_seq_lens_cpu[: self.num_decode_reqs] - decode_num_scheduled_tokens
).tolist()
else:
decode_num_computed_tokens = input_batch.num_computed_tokens_cpu[
: self.num_decode_reqs
].tolist()
dcp_mtp_attn_mask = self.generate_mtp_attention_mask_for_decode(
decode_num_computed_tokens, decode_num_scheduled_tokens
)
if dcp_mtp_attn_mask is not None:
self.dcp_mtp_attn_mask.np[: self.num_decode_reqs] = dcp_mtp_attn_mask
self.dcp_mtp_attn_mask.copy_to_gpu(self.num_decode_reqs)
# Always expose the (stable, pre-allocated) MTP mask buffer
# for cp>1 + speculative decode, even when num_decode_reqs == 0.
mask_n = self.num_decode_reqs if self.num_decode_reqs > 0 else num_reqs
long_seq_metadata.dcp_mtp_attn_mask = self.dcp_mtp_attn_mask.gpu[:mask_n]
else:
long_seq_metadata.dcp_mtp_attn_mask = None
self.long_seq_metadata = long_seq_metadata
return long_seq_metadata, block_table_tensor
def _list_to_tensor(self, lst, device, dtype=torch.int32):
tensor_npu = torch.zeros(len(lst), dtype=dtype, device=device)
tensor_npu.copy_(torch.tensor(lst, dtype=dtype), non_blocking=True)
return tensor_npu
def remap_mrope_positions_for_pcp(
self,
positions_np: np.ndarray,
num_scheduled_tokens: np.ndarray,
num_reqs: int,
input_batch: "NPUInputBatch",
requests: dict[str, Any],
mrope_positions: CpuGpuBuffer,
):
"""Remap mrope_positions after PCP split.
_calc_mrope_positions fills mrope_positions using the original
(pre-PCP-split) sequential token ordering from scheduler_output.
After PCP splits tokens across ranks, each rank only processes a
subset of tokens (head+tail chunks), so we must remap mrope_positions
to match the PCP-local token ordering.
positions_np already contains the correct absolute position for each
token on this PCP rank (computed by update_tokens_for_pcp). We use
these positions to gather the correct mrope_positions from
req.mrope_positions (for prompt tokens) or compute them on-the-fly
(for completion/decode tokens).
"""
mrope_pos_ptr = 0
for index, req_id in enumerate(input_batch.req_ids):
req = requests[req_id]
num_sched = int(num_scheduled_tokens[index])
local_positions = positions_np[mrope_pos_ptr : mrope_pos_ptr + num_sched]
if req.mrope_positions is not None and req.mrope_positions.shape[1] > 0:
num_prompt_tokens = length_from_prompt_token_ids_or_embeds(req.prompt_token_ids, req.prompt_embeds)
max_mrope_idx = req.mrope_positions.shape[1]
# Build the mrope_positions for this request's PCP-local
# tokens. For each token, gather from req.mrope_positions
# using its absolute position from positions_np.
mrope_dst = np.empty((3, num_sched), dtype=np.int64)
# Prompt tokens: positions within prompt range,
# gather from pre-computed req.mrope_positions.
prompt_mask = local_positions < min(num_prompt_tokens, max_mrope_idx)
if prompt_mask.any():
prompt_indices = local_positions[prompt_mask].astype(np.int64)
prompt_indices = np.clip(prompt_indices, 0, max_mrope_idx - 1)
mrope_dst[:, prompt_mask] = req.mrope_positions[:, torch.from_numpy(prompt_indices)].numpy()
# Completion/decode tokens: all 3 dims use the same
# position.
completion_mask = local_positions >= num_prompt_tokens
if completion_mask.any():
# For completion tokens, use mrope_position_delta to
# compute the correct position, same as
# get_next_input_positions_tensor.
if req.mrope_position_delta is not None:
comp_positions = local_positions[completion_mask] + req.mrope_position_delta
else:
comp_positions = local_positions[completion_mask]
mrope_dst[:, completion_mask] = comp_positions[np.newaxis, :]
# Padding tokens beyond req.mrope_positions shape:
# use the last valid mrope position.
padding_mask = (~prompt_mask) & (~completion_mask)
if padding_mask.any():
last_idx = max_mrope_idx - 1
mrope_dst[:, padding_mask] = req.mrope_positions[:, last_idx : last_idx + 1].numpy()
mrope_positions.cpu[:, mrope_pos_ptr : mrope_pos_ptr + num_sched] = torch.from_numpy(mrope_dst)
else:
# No mrope_positions available:
# all 3 dims equal the 1D position.
mrope_positions.cpu[:, mrope_pos_ptr : mrope_pos_ptr + num_sched] = torch.from_numpy(
local_positions[np.newaxis, :].astype(np.int64)
)
mrope_pos_ptr += num_sched
def generate_mtp_attention_mask_for_decode(
self,
decode_num_computed_tokens: list[int],
decode_num_scheduled_tokens: np.ndarray,
) -> list[torch.Tensor | None]:
"""
Generate MTP attention masks for decode requests in PCP mode.
This function handles the case where decode requests with MTP (speculative decoding)
need attention masks computed based on the local sequence after load balancing.
New MTP token allocation logic (using position % cp_size):
- History tokens are already split via DualChunkSwap
- MTP tokens are allocated based on (history_len + mtp_idx) % cp_size
- Each rank only computes mask for tokens assigned to itself
Example:
- pcp=1, dcp=2 (cp_size=2)
- history_len=5: [a,b,c,d,e] split via DualChunkSwap
- cp0: [a,b,c] (positions 0,1,2) -> 3 tokens
- cp1: [d,e] (positions 3,4) -> 2 tokens
- num_scheduled_tokens=4: [f,g,h,i] (positions 5,6,7,8)
- MTP allocation by position % cp_size:
- f: pos 5 % 2 = 1 -> rank1
- g: pos 6 % 2 = 0 -> rank0
- h: pos 7 % 2 = 1 -> rank1
- i: pos 8 % 2 = 0 -> rank0
- Final:
- rank0: [a,b,c,g,i] positions [0,1,2,6,8] -> mask shape 4x5
- rank1: [d,e,f,h] positions [3,4,5,7] -> mask shape 4x4
Args:
decode_num_computed_tokens: List of global history lengths for decode requests
decode_num_scheduled_tokens: Array of scheduled token counts for decode requests
"""
cp_rank = self.pcp_world_rank * self.dcp_world_size + self.dcp_world_rank
cp_size = self.pcp_world_size * self.dcp_world_size
assert cp_size > 1, "cp_size must be greater than 1"
q_lens = torch.tensor(decode_num_scheduled_tokens[: self.num_decode_reqs], dtype=torch.int32)
global_histories = torch.tensor(decode_num_computed_tokens, dtype=torch.int32)
total_lens = global_histories + q_lens
context_lens = total_lens - q_lens
max_indices = total_lens - 1
valid = max_indices >= cp_rank
if not valid.any():
return self.dcp_mtp_attn_mask.cpu[: self.num_decode_reqs]
k_lens = torch.div(max_indices - cp_rank, cp_size, rounding_mode="floor") + 1
k_lens = torch.where(valid, k_lens, torch.zeros_like(k_lens))
mtp_attn_mask = self.dcp_mtp_attn_mask.cpu[: self.num_decode_reqs]
mtp_attn_mask.zero_()
num_valid = valid.sum().item()
if num_valid == 0:
return mtp_attn_mask
max_q = int(q_lens[valid].max().item())
max_k = int(k_lens[valid].max().item())
# Generate indices up to max dimensions
q_indices = torch.arange(max_q, dtype=torch.int32)
k_indices = torch.arange(max_k, dtype=torch.int32)
valid_q = valid[:, None] & (q_indices[None, :] < q_lens[:, None])
valid_k = valid[:, None] & (k_indices[None, :] < k_lens[:, None])
k_upper = (context_lens[:, None] + q_indices - cp_rank) // cp_size
k_upper_expanded = k_upper[:, :, None] # [num_decode_reqs, max_q, 1]
k_idx_expanded = k_indices[None, None, :] # [1, 1, max_k]
full_mask = (k_idx_expanded > k_upper_expanded) & (k_upper_expanded >= 0)
valid_mask_3d = valid_q[:, :, None] & valid_k[:, None, :]
full_mask = full_mask & valid_mask_3d
mtp_attn_mask[: self.num_decode_reqs, :max_q, :max_k] = full_mask
return mtp_attn_mask