Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

468 lines
18 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations
import json
from dataclasses import dataclass
from enum import Enum
from typing import Literal
class UnsupportedPDLayoutError(ValueError):
pass
class BufferKind(str, Enum):
TARGET_K = "target_k"
TARGET_V = "target_v"
DRAFT_K = "draft_k"
DRAFT_V = "draft_v"
MAMBA_STATE = "mamba_state"
@dataclass(frozen=True)
class ParallelLayout:
role: Literal["prefill", "decode"]
world_size: int
dp_size: int = 1
def __post_init__(self):
if self.world_size <= 0:
raise UnsupportedPDLayoutError("world_size must be positive")
if self.dp_size <= 0:
raise UnsupportedPDLayoutError("dp_size must be positive")
if self.world_size % self.dp_size != 0:
raise UnsupportedPDLayoutError(
f"world_size={self.world_size} must be divisible by dp_size={self.dp_size}"
)
@property
def tp_size_per_dp(self) -> int:
return self.world_size // self.dp_size
@dataclass(frozen=True)
class BufferLayout:
"""Logical layout for one cache/state buffer.
``tp_replica_group_size`` describes TP ranks that hold the same logical
shard. It is used by GQA/MQA-style KV caches when the prefill TP size is
larger than the number of distinct KV heads.
"""
buffer_index: int
buffer_kind: BufferKind
logical_axis: Literal["kv_head", "state_channel", "replicated"]
logical_size: int
page_size: int
bytes_per_logical_unit: int
item_stride_bytes: int
tp_replica_group_size: int = 1
def __post_init__(self):
if self.logical_size <= 0:
raise UnsupportedPDLayoutError("logical_size must be positive")
if self.page_size <= 0:
raise UnsupportedPDLayoutError("page_size must be positive")
if self.bytes_per_logical_unit <= 0:
raise UnsupportedPDLayoutError("bytes_per_logical_unit must be positive")
if self.item_stride_bytes <= 0:
raise UnsupportedPDLayoutError("item_stride_bytes must be positive")
if self.tp_replica_group_size <= 0:
raise UnsupportedPDLayoutError("tp_replica_group_size must be positive")
@dataclass(frozen=True)
class TransferFragment:
buffer_index: int
buffer_kind: BufferKind
src_rank: int
dst_rank: int
src_page_stride_bytes: int
dst_page_stride_bytes: int
src_byte_offset: int
dst_byte_offset: int
bytes_per_page: int
page_count: int | None = None
TRANSFER_PLAN_PROTOCOL_VERSION = 1
def encode_transfer_fragments(
fragments: tuple[TransferFragment, ...],
) -> tuple[bytes, bytes]:
payload = [
{
"buffer_index": fragment.buffer_index,
"buffer_kind": fragment.buffer_kind.value,
"src_rank": fragment.src_rank,
"dst_rank": fragment.dst_rank,
"src_page_stride_bytes": fragment.src_page_stride_bytes,
"dst_page_stride_bytes": fragment.dst_page_stride_bytes,
"src_byte_offset": fragment.src_byte_offset,
"dst_byte_offset": fragment.dst_byte_offset,
"bytes_per_page": fragment.bytes_per_page,
"page_count": fragment.page_count,
}
for fragment in fragments
]
return (
str(TRANSFER_PLAN_PROTOCOL_VERSION).encode("ascii"),
json.dumps(payload, separators=(",", ":")).encode("utf-8"),
)
def decode_transfer_fragments(
version_frame: bytes | None,
payload_frame: bytes | None,
) -> tuple[TransferFragment, ...]:
if not version_frame and not payload_frame:
return ()
if not version_frame or not payload_frame:
raise UnsupportedPDLayoutError("incomplete transfer plan frames")
try:
version = int(version_frame.decode("ascii"))
except ValueError as exc:
raise UnsupportedPDLayoutError(
"invalid transfer plan protocol version"
) from exc
if version != TRANSFER_PLAN_PROTOCOL_VERSION:
raise UnsupportedPDLayoutError(
f"unsupported transfer plan protocol version={version}"
)
raw_fragments = json.loads(payload_frame.decode("utf-8"))
return tuple(
TransferFragment(
buffer_index=int(fragment["buffer_index"]),
buffer_kind=BufferKind(fragment["buffer_kind"]),
src_rank=int(fragment["src_rank"]),
dst_rank=int(fragment["dst_rank"]),
src_page_stride_bytes=int(fragment["src_page_stride_bytes"]),
dst_page_stride_bytes=int(fragment["dst_page_stride_bytes"]),
src_byte_offset=int(fragment["src_byte_offset"]),
dst_byte_offset=int(fragment["dst_byte_offset"]),
bytes_per_page=int(fragment["bytes_per_page"]),
page_count=(
None if fragment["page_count"] is None else int(fragment["page_count"])
),
)
for fragment in raw_fragments
)
@dataclass(frozen=True)
class RankTransferPlan:
plan_kind: Literal["identity", "fragmented"]
target_dp_group: int
target_prefill_ranks: tuple[int, ...]
required_prefill_response_num: int
fragments_by_prefill_rank: dict[int, tuple[TransferFragment, ...]]
required_dst_info_num_by_prefill_rank: dict[int, int]
def required_dst_info_num_for_prefill_rank(self, prefill_rank: int) -> int:
return self.required_dst_info_num_by_prefill_rank[prefill_rank]
@dataclass(frozen=True)
class _Interval:
start: int
end: int
@property
def length(self) -> int:
return self.end - self.start
def intersect(self, other: "_Interval") -> "_Interval | None":
start = max(self.start, other.start)
end = min(self.end, other.end)
if start >= end:
return None
return _Interval(start, end)
class PDTransferPlanner:
def __init__(
self,
*,
prefill_layout: ParallelLayout,
decode_layout: ParallelLayout,
prefill_buffers: tuple[BufferLayout, ...],
decode_buffers: tuple[BufferLayout, ...],
):
self.prefill_layout = prefill_layout
self.decode_layout = decode_layout
self.prefill_buffers = prefill_buffers
self.decode_buffers = decode_buffers
self._validate_buffers()
self._validate_alignment()
self._required_dst_info_num_by_prefill_rank = self._calc_source_fanout()
def plan_for_decode_rank(self, decode_rank: int) -> RankTransferPlan:
decode_tp_size = self.decode_layout.tp_size_per_dp
if decode_rank < 0 or decode_rank >= self.decode_layout.world_size:
raise UnsupportedPDLayoutError(f"decode_rank={decode_rank} is out of range")
target_dp_group = decode_rank // decode_tp_size
decode_tp_rank = decode_rank % decode_tp_size
if self._can_use_identity_plan() and (
self.prefill_layout.tp_size_per_dp == decode_tp_size
):
prefill_rank = (
target_dp_group * self.prefill_layout.tp_size_per_dp + decode_tp_rank
)
return RankTransferPlan(
plan_kind="identity",
target_dp_group=target_dp_group,
target_prefill_ranks=(prefill_rank,),
required_prefill_response_num=1,
fragments_by_prefill_rank={},
required_dst_info_num_by_prefill_rank={
prefill_rank: self._required_dst_info_num_by_prefill_rank[
prefill_rank
]
},
)
fragments: dict[int, list[TransferFragment]] = {}
for prefill_buffer, decode_buffer in zip(
self.prefill_buffers, self.decode_buffers
):
if prefill_buffer.logical_axis == "replicated":
prefill_tp_rank = self._replicated_source_tp_rank(
self.prefill_layout.tp_size_per_dp,
decode_tp_size,
decode_tp_rank,
)
prefill_rank = (
target_dp_group * self.prefill_layout.tp_size_per_dp
+ prefill_tp_rank
)
fragment = TransferFragment(
buffer_index=prefill_buffer.buffer_index,
buffer_kind=prefill_buffer.buffer_kind,
src_rank=prefill_rank,
dst_rank=decode_rank,
src_page_stride_bytes=prefill_buffer.item_stride_bytes,
dst_page_stride_bytes=decode_buffer.item_stride_bytes,
src_byte_offset=0,
dst_byte_offset=0,
bytes_per_page=decode_buffer.item_stride_bytes,
)
fragments.setdefault(prefill_rank, []).append(fragment)
continue
decode_interval = self._rank_interval_for_buffer(
decode_buffer,
self.decode_layout,
decode_tp_rank,
)
if decode_interval is None:
continue
for prefill_tp_rank in range(self.prefill_layout.tp_size_per_dp):
prefill_rank = (
target_dp_group * self.prefill_layout.tp_size_per_dp
+ prefill_tp_rank
)
prefill_interval = self._rank_interval_for_buffer(
prefill_buffer,
self.prefill_layout,
prefill_tp_rank,
)
if prefill_interval is None:
continue
intersection = prefill_interval.intersect(decode_interval)
if intersection is None:
continue
fragment = TransferFragment(
buffer_index=prefill_buffer.buffer_index,
buffer_kind=prefill_buffer.buffer_kind,
src_rank=prefill_rank,
dst_rank=decode_rank,
src_page_stride_bytes=prefill_buffer.item_stride_bytes,
dst_page_stride_bytes=decode_buffer.item_stride_bytes,
src_byte_offset=(intersection.start - prefill_interval.start)
* prefill_buffer.bytes_per_logical_unit,
dst_byte_offset=(intersection.start - decode_interval.start)
* decode_buffer.bytes_per_logical_unit,
bytes_per_page=intersection.length
* prefill_buffer.bytes_per_logical_unit,
)
fragments.setdefault(prefill_rank, []).append(fragment)
fragments_by_rank = {
rank: tuple(rank_fragments)
for rank, rank_fragments in sorted(fragments.items())
}
target_prefill_ranks = tuple(fragments_by_rank)
return RankTransferPlan(
plan_kind="fragmented",
target_dp_group=target_dp_group,
target_prefill_ranks=target_prefill_ranks,
required_prefill_response_num=len(target_prefill_ranks),
fragments_by_prefill_rank=fragments_by_rank,
required_dst_info_num_by_prefill_rank={
rank: self._required_dst_info_num_by_prefill_rank[rank]
for rank in target_prefill_ranks
},
)
def _validate_buffers(self) -> None:
if len(self.prefill_buffers) != len(self.decode_buffers):
raise UnsupportedPDLayoutError("prefill/decode buffer counts differ")
for prefill_buffer, decode_buffer in zip(
self.prefill_buffers, self.decode_buffers
):
if prefill_buffer.buffer_index != decode_buffer.buffer_index:
raise UnsupportedPDLayoutError("prefill/decode buffer indexes differ")
if prefill_buffer.buffer_kind != decode_buffer.buffer_kind:
raise UnsupportedPDLayoutError("prefill/decode buffer kinds differ")
if prefill_buffer.logical_axis != decode_buffer.logical_axis:
raise UnsupportedPDLayoutError("prefill/decode logical axes differ")
if prefill_buffer.logical_size != decode_buffer.logical_size:
raise UnsupportedPDLayoutError("prefill/decode logical sizes differ")
if (
prefill_buffer.bytes_per_logical_unit
!= decode_buffer.bytes_per_logical_unit
):
raise UnsupportedPDLayoutError(
"prefill/decode logical unit sizes differ"
)
def _validate_alignment(self) -> None:
for layout, buffers in (
(self.prefill_layout, self.prefill_buffers),
(self.decode_layout, self.decode_buffers),
):
for buffer in buffers:
if buffer.logical_axis == "replicated":
continue
if layout.tp_size_per_dp % buffer.tp_replica_group_size != 0:
raise UnsupportedPDLayoutError(
"tp replica group must divide TP size for "
f"buffer_kind={buffer.buffer_kind.value}: "
f"tp_size_per_dp={layout.tp_size_per_dp}, "
f"tp_replica_group_size={buffer.tp_replica_group_size}"
)
effective_tp_size = (
layout.tp_size_per_dp // buffer.tp_replica_group_size
)
if buffer.logical_size % effective_tp_size != 0:
raise UnsupportedPDLayoutError(
"non-aligned TP heterogeneous mapping for "
f"buffer_kind={buffer.buffer_kind.value}: logical_size="
f"{buffer.logical_size}, effective_tp_size={effective_tp_size}"
)
item_units = buffer.item_stride_bytes // buffer.bytes_per_logical_unit
required_units = buffer.logical_size // effective_tp_size
if item_units < required_units:
raise UnsupportedPDLayoutError(
"buffer item is smaller than its logical shard for "
f"buffer_kind={buffer.buffer_kind.value}: item_units="
f"{item_units}, required_units={required_units}"
)
def _calc_source_fanout(self) -> dict[int, int]:
fanout = {rank: 0 for rank in range(self.prefill_layout.world_size)}
for decode_rank in range(self.decode_layout.world_size):
decode_tp_rank = decode_rank % self.decode_layout.tp_size_per_dp
target_dp_group = decode_rank // self.decode_layout.tp_size_per_dp
intersected_prefill_ranks = set()
for prefill_buffer, decode_buffer in zip(
self.prefill_buffers, self.decode_buffers
):
if prefill_buffer.logical_axis == "replicated":
prefill_tp_rank = self._replicated_source_tp_rank(
self.prefill_layout.tp_size_per_dp,
self.decode_layout.tp_size_per_dp,
decode_tp_rank,
)
prefill_rank = (
target_dp_group * self.prefill_layout.tp_size_per_dp
+ prefill_tp_rank
)
intersected_prefill_ranks.add(prefill_rank)
continue
decode_interval = self._rank_interval_for_buffer(
decode_buffer,
self.decode_layout,
decode_tp_rank,
)
if decode_interval is None:
continue
for prefill_tp_rank in range(self.prefill_layout.tp_size_per_dp):
prefill_interval = self._rank_interval_for_buffer(
prefill_buffer,
self.prefill_layout,
prefill_tp_rank,
)
if prefill_interval is None:
continue
if prefill_interval.intersect(decode_interval) is None:
continue
prefill_rank = (
target_dp_group * self.prefill_layout.tp_size_per_dp
+ prefill_tp_rank
)
intersected_prefill_ranks.add(prefill_rank)
for prefill_rank in intersected_prefill_ranks:
fanout[prefill_rank] += 1
return fanout
def _can_use_identity_plan(self) -> bool:
return all(
prefill_buffer.tp_replica_group_size == 1
and decode_buffer.tp_replica_group_size == 1
for prefill_buffer, decode_buffer in zip(
self.prefill_buffers, self.decode_buffers
)
)
@staticmethod
def _rank_interval(logical_size: int, tp_size: int, tp_rank: int) -> _Interval:
local_size = logical_size // tp_size
start = tp_rank * local_size
return _Interval(start, start + local_size)
@staticmethod
def _rank_interval_for_buffer(
buffer: BufferLayout, layout: ParallelLayout, tp_rank: int
) -> _Interval | None:
replica_group_size = buffer.tp_replica_group_size
if tp_rank % replica_group_size != 0:
return None
effective_tp_size = layout.tp_size_per_dp // replica_group_size
effective_tp_rank = tp_rank // replica_group_size
return PDTransferPlanner._rank_interval(
buffer.logical_size, effective_tp_size, effective_tp_rank
)
@staticmethod
def _replicated_source_tp_rank(
prefill_tp_size: int, decode_tp_size: int, decode_tp_rank: int
) -> int:
return (decode_tp_rank * prefill_tp_size) // decode_tp_size