[NPU] fix pp 2 hang on npu (#38249)
This commit is contained in:
@@ -1811,6 +1811,161 @@ class GroupCoordinator:
|
|||||||
tensor_dict[key] = value
|
tensor_dict[key] = value
|
||||||
return tensor_dict
|
return tensor_dict
|
||||||
|
|
||||||
|
def send_recv_tensor_dict(
|
||||||
|
self,
|
||||||
|
send_tensor_dict: Dict[str, Union[torch.Tensor, Any]],
|
||||||
|
send_dst: Optional[int] = None,
|
||||||
|
recv_src: Optional[int] = None,
|
||||||
|
send_all_gather_group: Optional["GroupCoordinator"] = None,
|
||||||
|
recv_all_gather_group: Optional["GroupCoordinator"] = None,
|
||||||
|
) -> Optional[Dict[str, Union[torch.Tensor, Any]]]:
|
||||||
|
"""Send tensor dict to *send_dst* and simultaneously recv from *recv_src*.
|
||||||
|
|
||||||
|
Uses ``batch_isend_irecv`` to submit all send/recv operations
|
||||||
|
atomically, avoiding deadlock on backends (e.g. NPU/HCCL) where
|
||||||
|
``isend`` may block until a matching ``recv`` is posted.
|
||||||
|
|
||||||
|
NOTE: ``send_dst`` / ``recv_src`` are local ranks within this group.
|
||||||
|
"""
|
||||||
|
if not torch.distributed.is_initialized() or self.world_size == 1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if send_dst is None:
|
||||||
|
send_dst = (self.rank_in_group + 1) % self.world_size
|
||||||
|
if recv_src is None:
|
||||||
|
recv_src = (self.rank_in_group - 1) % self.world_size
|
||||||
|
|
||||||
|
assert send_dst < self.world_size, f"Invalid send_dst rank ({send_dst})"
|
||||||
|
assert recv_src < self.world_size, f"Invalid recv_src rank ({recv_src})"
|
||||||
|
|
||||||
|
group = self.device_group
|
||||||
|
metadata_group = self.cpu_group
|
||||||
|
|
||||||
|
# ---- 1. Exchange metadata via batch_isend_irecv on CPU group ----
|
||||||
|
send_metadata_list, send_tensor_list = _split_tensor_dict(send_tensor_dict)
|
||||||
|
|
||||||
|
send_meta_bytes = pickle.dumps(send_metadata_list)
|
||||||
|
send_meta_size = torch.tensor([len(send_meta_bytes)], dtype=torch.long)
|
||||||
|
send_meta_data = torch.frombuffer(send_meta_bytes, dtype=torch.uint8)
|
||||||
|
|
||||||
|
recv_meta_size = torch.empty(1, dtype=torch.long)
|
||||||
|
|
||||||
|
meta_ops = [
|
||||||
|
torch.distributed.P2POp(
|
||||||
|
torch.distributed.isend,
|
||||||
|
send_meta_size,
|
||||||
|
self.ranks[send_dst],
|
||||||
|
group=metadata_group,
|
||||||
|
),
|
||||||
|
torch.distributed.P2POp(
|
||||||
|
torch.distributed.irecv,
|
||||||
|
recv_meta_size,
|
||||||
|
self.ranks[recv_src],
|
||||||
|
group=metadata_group,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
reqs = torch.distributed.batch_isend_irecv(meta_ops)
|
||||||
|
for req in reqs:
|
||||||
|
req.wait()
|
||||||
|
|
||||||
|
recv_meta_len = recv_meta_size.item()
|
||||||
|
recv_meta_data = torch.empty(recv_meta_len, dtype=torch.uint8)
|
||||||
|
|
||||||
|
meta_ops = [
|
||||||
|
torch.distributed.P2POp(
|
||||||
|
torch.distributed.isend,
|
||||||
|
send_meta_data,
|
||||||
|
self.ranks[send_dst],
|
||||||
|
group=metadata_group,
|
||||||
|
),
|
||||||
|
torch.distributed.P2POp(
|
||||||
|
torch.distributed.irecv,
|
||||||
|
recv_meta_data,
|
||||||
|
self.ranks[recv_src],
|
||||||
|
group=metadata_group,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
reqs = torch.distributed.batch_isend_irecv(meta_ops)
|
||||||
|
for req in reqs:
|
||||||
|
req.wait()
|
||||||
|
|
||||||
|
recv_metadata_list = pickle.loads(recv_meta_data.numpy())
|
||||||
|
|
||||||
|
# ---- 2. Prepare recv buffers and collect all tensor ops ----
|
||||||
|
recv_tensor_dict: Dict[str, Any] = {}
|
||||||
|
tensor_ops: List[torch.distributed.P2POp] = []
|
||||||
|
recv_tensor_info: List[
|
||||||
|
Tuple[str, torch.Tensor, bool, Optional[torch.Size]]
|
||||||
|
] = []
|
||||||
|
|
||||||
|
for key, value in recv_metadata_list:
|
||||||
|
if isinstance(value, TensorMetadata):
|
||||||
|
tensor = torch.empty(value.size, dtype=value.dtype, device=value.device)
|
||||||
|
if tensor.numel() == 0:
|
||||||
|
recv_tensor_dict[key] = tensor
|
||||||
|
continue
|
||||||
|
|
||||||
|
use_all_gather = (
|
||||||
|
recv_all_gather_group is not None
|
||||||
|
and tensor.numel() % recv_all_gather_group.world_size == 0
|
||||||
|
)
|
||||||
|
orig_shape = None
|
||||||
|
if use_all_gather:
|
||||||
|
orig_shape = tensor.shape
|
||||||
|
tensor = tensor.reshape(recv_all_gather_group.world_size, -1)[
|
||||||
|
recv_all_gather_group.rank_in_group
|
||||||
|
]
|
||||||
|
|
||||||
|
comm_group = metadata_group if tensor.is_cpu else group
|
||||||
|
tensor_ops.append(
|
||||||
|
torch.distributed.P2POp(
|
||||||
|
torch.distributed.irecv,
|
||||||
|
tensor,
|
||||||
|
self.ranks[recv_src],
|
||||||
|
group=comm_group,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
recv_tensor_info.append((key, tensor, use_all_gather, orig_shape))
|
||||||
|
else:
|
||||||
|
recv_tensor_dict[key] = value
|
||||||
|
|
||||||
|
# Add send ops
|
||||||
|
for tensor in send_tensor_list:
|
||||||
|
if tensor.numel() == 0:
|
||||||
|
continue
|
||||||
|
send_t = tensor
|
||||||
|
if (
|
||||||
|
send_all_gather_group is not None
|
||||||
|
and send_t.numel() % send_all_gather_group.world_size == 0
|
||||||
|
):
|
||||||
|
send_t = send_t.reshape(send_all_gather_group.world_size, -1)[
|
||||||
|
send_all_gather_group.rank_in_group
|
||||||
|
]
|
||||||
|
comm_group = metadata_group if send_t.is_cpu else group
|
||||||
|
tensor_ops.append(
|
||||||
|
torch.distributed.P2POp(
|
||||||
|
torch.distributed.isend,
|
||||||
|
send_t,
|
||||||
|
self.ranks[send_dst],
|
||||||
|
group=comm_group,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- 3. Batch exchange all tensors ----
|
||||||
|
if tensor_ops:
|
||||||
|
reqs = torch.distributed.batch_isend_irecv(tensor_ops)
|
||||||
|
for req in reqs:
|
||||||
|
req.wait()
|
||||||
|
|
||||||
|
# ---- 4. Post-process received tensors (all_gather if needed) ----
|
||||||
|
for key, tensor, use_all_gather, orig_shape in recv_tensor_info:
|
||||||
|
if use_all_gather:
|
||||||
|
tensor = recv_all_gather_group.all_gather(tensor, dim=0)
|
||||||
|
tensor = tensor.reshape(orig_shape)
|
||||||
|
recv_tensor_dict[key] = tensor
|
||||||
|
|
||||||
|
return recv_tensor_dict
|
||||||
|
|
||||||
def barrier(self):
|
def barrier(self):
|
||||||
"""Barrier synchronization among the group.
|
"""Barrier synchronization among the group.
|
||||||
NOTE: don't use `device_group` here! `barrier` in NCCL is
|
NOTE: don't use `device_group` here! `barrier` in NCCL is
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ from sglang.srt.sampling.sampling_observer_pp import (
|
|||||||
pop_auxiliary_output_from_pp_tensors,
|
pop_auxiliary_output_from_pp_tensors,
|
||||||
)
|
)
|
||||||
from sglang.srt.utils import DynamicGradMode, point_to_point_pyobj
|
from sglang.srt.utils import DynamicGradMode, point_to_point_pyobj
|
||||||
from sglang.srt.utils.common import is_xpu
|
from sglang.srt.utils.common import is_npu, is_xpu
|
||||||
|
|
||||||
|
_is_npu = is_npu()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -1025,6 +1026,20 @@ class SchedulerPPMixin:
|
|||||||
batch_result = None
|
batch_result = None
|
||||||
send_output_work = []
|
send_output_work = []
|
||||||
|
|
||||||
|
# On NPU (HCCL), isend/irecv may block until a matching peer op is
|
||||||
|
# posted, so the parity-based send-first/recv-first ordering used
|
||||||
|
# for NPU is replaced by batch_isend_irecv which submits all
|
||||||
|
# send/recv operations atomically.
|
||||||
|
if _is_npu and self.ps.pp_size == 2:
|
||||||
|
return self._pp2_only_send_recv_output_tensors_npu(
|
||||||
|
next_first_rank_mb_id,
|
||||||
|
next_mb_id,
|
||||||
|
mbs,
|
||||||
|
mb_metadata,
|
||||||
|
last_rank_comm_queue,
|
||||||
|
pp_outputs,
|
||||||
|
)
|
||||||
|
|
||||||
# On CUDA, isend is async: it enqueues to the stream and returns,
|
# On CUDA, isend is async: it enqueues to the stream and returns,
|
||||||
# so every rank can send first safely. On some backends isend is
|
# so every rank can send first safely. On some backends isend is
|
||||||
# effectively blocking and does not return until the peer posts a
|
# effectively blocking and does not return until the peer posts a
|
||||||
@@ -1075,6 +1090,118 @@ class SchedulerPPMixin:
|
|||||||
|
|
||||||
return next_pp_outputs, batch_result, d2h_event, send_output_work
|
return next_pp_outputs, batch_result, d2h_event, send_output_work
|
||||||
|
|
||||||
|
def _pp2_only_send_recv_output_tensors_npu(
|
||||||
|
self: Scheduler,
|
||||||
|
next_first_rank_mb_id: int,
|
||||||
|
next_mb_id: int,
|
||||||
|
mbs: List[ScheduleBatch],
|
||||||
|
mb_metadata: List[PPBatchMetadata],
|
||||||
|
last_rank_comm_queue: deque[Tuple[torch.Event, PPProxyTensors]],
|
||||||
|
pp_outputs: PPProxyTensors | None,
|
||||||
|
) -> Tuple[
|
||||||
|
Optional[PPProxyTensors],
|
||||||
|
Optional[GenerationBatchResult],
|
||||||
|
Optional[torch.Event],
|
||||||
|
List[P2PWork],
|
||||||
|
]:
|
||||||
|
"""NPU-specific output tensor send/recv using batch_isend_irecv.
|
||||||
|
|
||||||
|
Pairs the send of output tensors to the next stage with the recv
|
||||||
|
of output tensors from the previous stage in a single
|
||||||
|
``batch_isend_irecv`` call, avoiding the deadlock that can occur
|
||||||
|
on HCCL when separate isend/irecv calls block waiting for each
|
||||||
|
other.
|
||||||
|
"""
|
||||||
|
next_pp_outputs = None
|
||||||
|
d2h_event = None
|
||||||
|
batch_result = None
|
||||||
|
send_output_work = []
|
||||||
|
|
||||||
|
all_gather_group = (
|
||||||
|
self.attn_tp_group if self.require_attn_tp_allgather else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- Prepare send dict ----
|
||||||
|
# On NPU, always send something (full output or a lightweight skip
|
||||||
|
# marker) so the peer's recv in batch_isend_irecv always has a
|
||||||
|
# matching send. Without this, SGLANG_PP_SKIP_PURE_CHUNKED_OUTPUT_COMM
|
||||||
|
# would cause asymmetric skip decisions between adjacent ranks
|
||||||
|
# (send target and recv target are different micro-batches), leading
|
||||||
|
# to deadlock or forcing the user to disable the optimisation
|
||||||
|
# entirely (≈10 % throughput loss).
|
||||||
|
send_dict: Optional[Dict[str, torch.Tensor]] = None
|
||||||
|
if self.pp_group.is_last_rank:
|
||||||
|
target_send = mbs[next_first_rank_mb_id]
|
||||||
|
if target_send is not None:
|
||||||
|
q_event, pp_outputs_to_send = last_rank_comm_queue.popleft()
|
||||||
|
if not target_send.forward_mode.is_prebuilt():
|
||||||
|
if _pp_can_skip_output_comm(target_send):
|
||||||
|
send_dict = {"__msg_type__": "output", "__skip__": True}
|
||||||
|
else:
|
||||||
|
self.device_module.current_stream().wait_event(q_event)
|
||||||
|
send_dict = dict(pp_outputs_to_send.tensors)
|
||||||
|
send_dict["__msg_type__"] = "output"
|
||||||
|
elif pp_outputs:
|
||||||
|
if pp_outputs.tensors.get("__skip__"):
|
||||||
|
send_dict = {"__msg_type__": "output", "__skip__": True}
|
||||||
|
else:
|
||||||
|
send_dict = dict(pp_outputs.tensors)
|
||||||
|
send_dict["__msg_type__"] = "output"
|
||||||
|
|
||||||
|
# ---- Determine recv conditions ----
|
||||||
|
# NOTE: skip_recv is NOT computed here. The skip decision is made
|
||||||
|
# by the sender (last rank) and communicated via the __skip__
|
||||||
|
# marker. The receiver always participates in the
|
||||||
|
# batch_isend_irecv and inspects the marker after recv.
|
||||||
|
target_recv = mbs[next_mb_id]
|
||||||
|
should_recv = (
|
||||||
|
target_recv is not None and not target_recv.forward_mode.is_prebuilt()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_recv_dict(recv_dict):
|
||||||
|
nonlocal next_pp_outputs, batch_result, d2h_event
|
||||||
|
if recv_dict.get("__skip__"):
|
||||||
|
# _pp_make_skip_output_result returns next_pp_outputs=None
|
||||||
|
# (correct for the non-NPU path where the skip propagates
|
||||||
|
# via pp_outputs=None). On NPU we must propagate the skip
|
||||||
|
# marker through the ring, so override it here.
|
||||||
|
_, batch_result, d2h_event = self._pp_make_skip_output_result(
|
||||||
|
target_recv, mb_metadata[next_mb_id]
|
||||||
|
)
|
||||||
|
next_pp_outputs = PPProxyTensors(recv_dict)
|
||||||
|
else:
|
||||||
|
next_pp_outputs = PPProxyTensors(recv_dict)
|
||||||
|
with self.copy_stream_ctx:
|
||||||
|
self.copy_stream.wait_stream(self.schedule_stream)
|
||||||
|
batch_result = self._pp_prep_batch_result(
|
||||||
|
target_recv, mb_metadata[next_mb_id], next_pp_outputs
|
||||||
|
)
|
||||||
|
d2h_event = self.device_module.Event()
|
||||||
|
d2h_event.record(self.device_module.current_stream())
|
||||||
|
|
||||||
|
# ---- Execute communication ----
|
||||||
|
if send_dict is not None and should_recv:
|
||||||
|
# Paired send + recv via batch_isend_irecv
|
||||||
|
with torch.profiler.record_function("send_recv_res_dict"):
|
||||||
|
recv_dict = self.pp_group.send_recv_tensor_dict(
|
||||||
|
send_tensor_dict=send_dict,
|
||||||
|
send_all_gather_group=all_gather_group,
|
||||||
|
recv_all_gather_group=all_gather_group,
|
||||||
|
)
|
||||||
|
_handle_recv_dict(recv_dict)
|
||||||
|
elif send_dict is not None:
|
||||||
|
# Send only (recv not needed — target is None or prebuilt)
|
||||||
|
send_output_work = self._pp_send_dict_to_next_stage(
|
||||||
|
send_dict, async_send=True, msg_type="output"
|
||||||
|
)
|
||||||
|
elif should_recv:
|
||||||
|
# Recv only (no send needed)
|
||||||
|
with torch.profiler.record_function("recv_res_dict_from_prev_stage"):
|
||||||
|
recv_dict = self._pp_recv_dict_from_prev_stage()
|
||||||
|
_handle_recv_dict(recv_dict)
|
||||||
|
|
||||||
|
return next_pp_outputs, batch_result, d2h_event, send_output_work
|
||||||
|
|
||||||
def _pp_launch_batch(
|
def _pp_launch_batch(
|
||||||
self: Scheduler,
|
self: Scheduler,
|
||||||
mb_id: int,
|
mb_id: int,
|
||||||
|
|||||||
Reference in New Issue
Block a user