Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed / encoder_parallel / transport.py: 96%
83 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""Transport CPU image shards without staging complete images on a device."""
16# pylint: disable=forbidden-backend-import
18from __future__ import annotations
20from typing import Any, Mapping
22import torch
23import torch.distributed as dist
25from hyper_parallel.data.parallel import TPBatchBroadcaster
26from hyper_parallel.distributed.encoder_parallel.contracts import EncoderLocalInputs
27from hyper_parallel.distributed.encoder_parallel.planning import DecoderSequenceLayout, build_segments
28from hyper_parallel.distributed.encoder_parallel.runtime import EncoderParallelPlan, EncoderParallelRuntime
31_METADATA_FIELD = "_encoder_transport_metadata"
32_FORWARD_FIELDS = ("encoder_parallel_plan", "encoder_parallel_inputs")
33_CHUNK_BYTES = 4 * 1024 * 1024
36class EncoderParallelBatchTransport:
37 """Plan from small metadata and send only each destination's CPU patches.
39 Every decoder CP coordinate already has a complete CPU batch on TP0. TP
40 peers may belong to different encoder DP buckets or CP shards, so their
41 payloads are transported individually with bounded device staging.
42 """
44 def __init__(self, *, runtime: EncoderParallelRuntime, chunk_bytes: int = _CHUNK_BYTES) -> None:
45 """Set the runtime and maximum device staging size in bytes."""
46 if isinstance(chunk_bytes, bool) or not isinstance(chunk_bytes, int) or chunk_bytes < 1:
47 raise ValueError("encoder transport chunk_bytes must be a positive integer")
48 self.runtime = runtime
49 self.chunk_bytes = chunk_bytes
51 def _prepare_source(self, batch: Mapping[str, Any]) -> dict[str, Any]:
52 """Validate CPU images and keep tensor payloads out of TP broadcasting."""
53 bridge = self.runtime.bridge
54 prepared = {name: value for name, value in batch.items() if name not in bridge.input_fields}
55 error = None
56 items = ()
57 dtype = torch.float32
58 try:
59 if set(batch).intersection((_METADATA_FIELD, *_FORWARD_FIELDS)):
60 raise ValueError("encoder transport fields cannot be supplied by the DataLoader")
61 for name in bridge.input_fields:
62 value = batch.get(name)
63 if torch.is_tensor(value) and value.device.type != "cpu":
64 raise ValueError(f"encoder source field {name} must remain on the CPU")
65 items = bridge.parse_images(batch)
66 pixels = batch.get(bridge.payload_field)
67 if pixels is not None:
68 dtype = pixels.dtype
69 except (ValueError, TypeError, KeyError) as exc:
70 error = str(exc)
71 prepared[_METADATA_FIELD] = (items, dtype, error)
72 return prepared
74 def _check_source_errors(self, error: str | None, device: Any) -> None:
75 """Make all FSDP participants fail before plan or payload collectives."""
76 invalid = torch.tensor(int(error is not None), device=device)
77 if dist.is_initialized() and self.runtime.global_mesh.size() > 1:
78 dist.all_reduce(invalid, op=dist.ReduceOp.MAX, group=self.runtime.global_mesh.get_group())
79 if bool(invalid.item()):
80 raise ValueError(f"invalid encoder CPU input: {error or 'another source rank rejected its batch'}")
82 def __call__(self, batch: Mapping[str, Any] | None,
83 broadcaster: TPBatchBroadcaster) -> tuple[dict[str, Any], dict[str, Any]]:
84 """Broadcast ordinary inputs, agree on a plan, and materialize local patches."""
85 source = self._prepare_source(batch) if batch is not None else None
86 device_batch = broadcaster.broadcast(source, broadcast_all_fields=True)
87 items, dtype, error = device_batch.pop(_METADATA_FIELD)
88 self._check_source_errors(error, broadcaster.device)
89 layout = DecoderSequenceLayout.build(
90 mesh_context=self.runtime.mesh_context, cp_local_length=int(device_batch["input_ids"].shape[1]),
91 )
92 plan = self.runtime.finalize_plan(items, layout)
93 inputs = {"encoder_parallel_plan": plan}
94 if plan.active:
95 inputs["encoder_parallel_inputs"] = EncoderLocalInputs(
96 self._transport_patches(batch, plan, dtype, broadcaster),
97 )
98 return device_batch, inputs
100 def _transport_patches(self, batch: Mapping[str, Any] | None, plan: EncoderParallelPlan,
101 dtype: torch.dtype, broadcaster: TPBatchBroadcaster) -> torch.Tensor:
102 """Pack on the CPU and reuse a chunk-sized buffer across TP destinations."""
103 context = broadcaster.parallel_context
104 local_count = sum(segment.local_padded_length for segment in plan.local_segments)
105 local = torch.empty((local_count, *self.runtime.bridge.empty_payload_shape),
106 dtype=dtype, device=broadcaster.device)
107 chunk_elements = max(1, self.chunk_bytes // local.element_size())
108 target_segments = []
109 for target in range(context.tp_world_size):
110 parent_rank = context.cp_rank * context.tp_world_size + target
111 bucket, cp_rank = divmod(parent_rank, self.runtime.config.encoder_cp_size)
112 target_segments.append(build_segments(plan.buckets[bucket], cp_rank, self.runtime.config.encoder_cp_size))
113 patch_elements = local[0].numel()
114 max_elements = max((sum(segment.local_padded_length for segment in segments) * patch_elements
115 for segments in target_segments[1:]), default=0)
116 staging = torch.empty(min(chunk_elements, max_elements), dtype=dtype, device=broadcaster.device)
117 for target, segments in enumerate(target_segments):
118 packed = self.runtime.bridge.pack_payload(batch, segments) if context.tp_rank == 0 else None
119 if target == 0:
120 if context.tp_rank == 0:
121 local.copy_(packed)
122 continue
123 count = sum(segment.local_padded_length for segment in segments)
124 elements = count * patch_elements
125 for start in range(0, elements, chunk_elements):
126 length = min(chunk_elements, elements - start)
127 buffer = (local.view(-1)[start:start + length] if context.tp_rank == target
128 else staging[:length])
129 if context.tp_rank == 0:
130 buffer.copy_(packed.view(-1)[start:start + length])
131 dist.broadcast(buffer, group=context.tp_group, group_src=0)
132 return local