File size: 9,822 Bytes
5c93746
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
"""
Utility functions and constants for communication operations.

This module provides utility functions and constants used across the communication module.
"""

import torch
import torch.distributed as dist
from typing import List, Tuple, Optional
import time
import logging

# Communication tags for different types of data
class CommunicationTags:
    """Constants for communication tags."""
    LATENT_HDR = 11001
    LATENT_PAY = 11002
    START_END_STEP = 11003
    PATCHED_X_SHAPE = 11004
    LATENT_ORIGIN_HDR = 11005
    LATENT_ORIGIN_PAY = 11006
    KV_CACHE_K = 11007
    KV_CACHE_V = 11008
    KV_CACHE_GLOBAL_END = 11009
    KV_CACHE_LOCAL_END = 11010
    BLOCK_INTERVALS = 11011
    PERFORMANCE_METRICS = 11012
    UPDATED_PROMPT_LENGTH = 11013
    UPDATED_PROMPT = 11014


def init_distributed():
    """
    Initialize distributed communication.
    
    This function initializes the distributed process group if not already initialized.
    """
    if not dist.is_initialized():
        backend = "nccl"
        dist.init_process_group(backend=backend)


def get_rank_info() -> Tuple[int, int]:
    """
    Get current rank and world size.
    
    Returns:
        Tuple of (rank, world_size)
    """
    if not dist.is_initialized():
        raise RuntimeError("Distributed not initialized")
    return dist.get_rank(), dist.get_world_size()


def get_next_rank(rank: int, world_size: int) -> int:
    """
    Get the next rank in the ring topology.
    
    Args:
        rank: Current rank
        world_size: Total number of ranks
        
    Returns:
        Next rank in the ring
    """
    return (rank + 1) % world_size


def get_prev_rank(rank: int, world_size: int) -> int:
    """
    Get the previous rank in the ring topology.
    
    Args:
        rank: Current rank
        world_size: Total number of ranks
        
    Returns:
        Previous rank in the ring
    """
    return (rank - 1) % world_size


def create_tensor_header(shape: Tuple[int, ...], dtype: torch.dtype, 
                        chunk_idx: int, device: torch.device) -> torch.Tensor:
    """
    Create a header tensor for communication.
    
    Args:
        shape: Shape of the tensor to be sent
        dtype: Data type of the tensor
        chunk_idx: Chunk index
        device: Device where the header will be created
        
    Returns:
        Header tensor containing metadata
    """
    header_data = [chunk_idx] + list(shape)
    return torch.tensor(header_data, dtype=torch.int64, device=device)


def parse_tensor_header(header: torch.Tensor) -> Tuple[int, Tuple[int, ...]]:
    """
    Parse a header tensor to extract metadata.
    
    Args:
        header: Header tensor
        
    Returns:
        Tuple of (chunk_idx, shape)
    """
    header_list = header.tolist()
    chunk_idx = int(header_list[0])
    shape = tuple(int(x) for x in header_list[1:])
    return chunk_idx, shape


def validate_tensor_for_communication(tensor: torch.Tensor, 
                                    expected_device: torch.device,
                                    expected_dtype: torch.dtype) -> None:
    """
    Validate tensor properties for communication.
    
    Args:
        tensor: Tensor to validate
        expected_device: Expected device
        expected_dtype: Expected data type
        
    Raises:
        ValueError: If tensor properties don't match expectations
    """
    if not isinstance(tensor, torch.Tensor):
        raise ValueError("Input must be a torch.Tensor")
    
    if tensor.device != expected_device:
        raise ValueError(f"Tensor device {tensor.device} doesn't match expected {expected_device}")
    
    if tensor.dtype != expected_dtype:
        raise ValueError(f"Tensor dtype {tensor.dtype} doesn't match expected {expected_dtype}")


def compute_balanced_split(total_blocks: int, rank_times: List[float], 
                          dit_times: List[float], 
                          current_block_nums: List[List[int]]) -> List[List[int]]:
    """
    Compute new block splits for all ranks to balance total rank times.
    
    This function is moved from the original file to provide better organization.
    
    Args:
        total_blocks: Total number of DiT blocks
        rank_times: List of total iteration times for each rank [t_rank0, t_rank1, ..., t_rankN] (DiT + VAE time)
        dit_times: List of pure DiT inference times for each rank [dit_rank0, dit_rank1, ..., dit_rankN] (DiT time only)
        current_block_nums: List of current block_num format for each rank [[rank0_blocks], [rank1_blocks], ...]
        
    Returns:
        List of new block_num format for each rank, matching the original format:
        - For world_size == 2: [[end_idx_rank0], [start_idx_rank1]]
        - For world_size > 2: [[end_idx_rank0], [start1, end1], [start2, end2], ..., [start_idx_last]]
        Note: Numbers are shared across ranks (rank0_end = rank1_start, rank1_end = rank2_start, etc.)
    """
    num_ranks = len(rank_times)
    if num_ranks == 0 or num_ranks != len(current_block_nums) or num_ranks != len(dit_times):
        return current_block_nums
    
    # Edge case: if we have more ranks than blocks, we can't guarantee 1 block per rank
    if num_ranks > total_blocks:
        # Fall back to original behavior for this edge case
        return current_block_nums

    # Step 1: Calculate total DiT time and per-block DiT time
    total_dit_time = sum(dit_times)
    dit_time_per_block = total_dit_time / total_blocks
    
    # Step 2: Calculate average rank time
    avg_rank_time = sum(rank_times) / num_ranks
    
    # Step 3: Extract current block counts from current_block_nums (all ranks use [start, end) now)
    current_block_counts = []
    for block_num in current_block_nums:
        # block_num: [start, end) exclusive end
        start_idx, end_idx = int(block_num[0]), int(block_num[1])
        current_block_counts.append(max(0, end_idx - start_idx))
    
    # Step 4: Calculate target block counts based on time differences
    target_blocks = []
    for i in range(num_ranks):
        time_diff = avg_rank_time - rank_times[i]  # positive = needs more time, negative = needs less time
        block_adjustment = time_diff / dit_time_per_block  # convert time difference to block count
        target_count = current_block_counts[i] + block_adjustment
        # Ensure each rank gets at least 1 block (minimum allocation)
        target_count = max(1, int(round(target_count)))
        target_blocks.append(target_count)
    
    # Step 5: Adjust to ensure total blocks sum to total_blocks while maintaining minimum 1 block per rank
    current_total = sum(target_blocks)
    if current_total != total_blocks:
        diff = total_blocks - current_total
        # When adding, give to ranks with smallest counts first; when removing, take from largest counts first
        if diff > 0:
            order = sorted(range(num_ranks), key=lambda i: (target_blocks[i], i))
        else:
            order = sorted(range(num_ranks), key=lambda i: (target_blocks[i], i), reverse=True)
        i = 0
        while diff != 0 and num_ranks > 0:
            idx = order[i % num_ranks]
            if diff > 0:
                target_blocks[idx] += 1
                diff -= 1
            else:
                # Only remove blocks if rank has more than 1 block (maintain minimum allocation)
                if target_blocks[idx] > 1:
                    target_blocks[idx] -= 1
                    diff += 1
            i += 1
    
    # Step 6: Convert target block counts to contiguous [start, end) intervals from 0 to total_blocks
    new_block_nums = []
    running_start = 0
    for i in range(num_ranks):
        block_count = int(target_blocks[i])
        start_idx = running_start
        end_idx = start_idx + block_count
        # Guard (should not trigger if sums are correct)
        if end_idx > total_blocks:
            end_idx = total_blocks
        new_block_nums.append([start_idx, end_idx])
        running_start = end_idx
    
    return new_block_nums


def setup_logging(rank: int, log_level: int = logging.INFO) -> logging.Logger:
    """
    Setup logging for the current rank.
    
    Args:
        rank: Current rank
        log_level: Logging level
        
    Returns:
        Configured logger
    """
    logger = logging.getLogger(f"rank_{rank}")
    logger.setLevel(log_level)
    # Prevent messages from propagating to the root logger (avoid double prints)
    logger.propagate = False
    
    if not logger.handlers:
        handler = logging.StreamHandler()
        formatter = logging.Formatter(
            f'[Rank {rank}] %(asctime)s - %(name)s - %(levelname)s - %(message)s'
        )
        handler.setFormatter(formatter)
        logger.addHandler(handler)
    
    return logger


class CommunicationTimer:
    """
    Timer for measuring communication performance.
    
    This class provides context manager functionality for timing communication operations.
    """
    
    def __init__(self, operation_name: str, logger: Optional[logging.Logger] = None):
        self.operation_name = operation_name
        self.logger = logger
        self.start_time = None
        self.end_time = None
    
    def __enter__(self):
        self.start_time = time.time()
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.end_time = time.time()
        duration = self.end_time - self.start_time
        
        if self.logger:
            self.logger.info(f"{self.operation_name} took {duration:.4f} seconds")
    
    @property
    def duration(self) -> float:
        """Get the duration of the timed operation."""
        if self.start_time is None or self.end_time is None:
            return 0.0
        return self.end_time - self.start_time