Diff Coverage

Diff: origin/master...HEAD, staged and unstaged changes

Source File Diff Coverage (%) Missing Lines
hyper_parallel/components/checkpoint/dcp_checkpointer.py 100%  
hyper_parallel/core/distributed_checkpoint/api.py 93.3% 382
hyper_parallel/core/distributed_checkpoint/async_persist.py 100%  
hyper_parallel/core/distributed_checkpoint/broadcast.py 91.2% 53-54,67,78-81,286-288,418,468
hyper_parallel/core/distributed_checkpoint/checkpoint_io.py 60.0% 38,53-55
hyper_parallel/core/distributed_checkpoint/filesystem_storage.py 100%  
hyper_parallel/core/distributed_checkpoint/offline_transform.py 47.8% 76,82,207,265,286,317,332,341-342,348,450-451
hyper_parallel/core/distributed_checkpoint/planner.py 100%  
hyper_parallel/core/distributed_checkpoint/ragged.py 93.2% 49,51,126,150,188
hyper_parallel/core/distributed_checkpoint/standard_planner.py 87.5% 442,794
hyper_parallel/core/distributed_checkpoint/state_dict.py 33.3% 43,62
hyper_parallel/core/distributed_checkpoint/utils.py 90.1% 58-60,75,78,82,133,175,179,220-221,233-234,238,321-322,335,363,392,455
hyper_parallel/core/distributed_checkpoint/api.py
378
379
380
381
382
383
384
385
        use_gloo,
    )

    # After the async save is completed, the user callback will be executed.
    def async_callback() -> None:
        """Run the user callback once the save has been persisted, if one was given."""
        if callback is not None:
            callback()
hyper_parallel/core/distributed_checkpoint/broadcast.py
49
50
51
52
53
54
55
56
57
58
    Args:
        dests (list): Tensors written into.
        srcs (list): Tensors read from, pairing with ``dests`` by position.
    """
    if dests:
        torch._foreach_copy_(dests, srcs)  # pylint: disable=protected-access


def get_created_group(rank_list: Union[list[int], tuple[int, ...]]) -> Any:
    """
63
64
65
66
67
68
69
70
71

    Returns:
        Any: The cached group, or None when the cache holds none over these ranks.
    """
    return EXISTING_COMM_GROUPS.get(str(tuple(sorted(rank_list))))


def synchronize() -> None:
    """
74
75
76
77
78
79
80
81
82
83
84
85
    Nothing can be queued on a device stream when the process has no device, and there are
    two ways to have none: a CPU-only install carries no ``torch.npu`` at all, and a gloo run
    on an accelerator box never initializes the one it has.
    """
    device = getattr(torch, "npu", None)
    if device is None or not device.is_initialized():
        return
    device.current_stream().synchronize()


# How many broadcasts one rank keeps going at once. Starting the next without waiting on
# the last is what lets a read overlap the send before it, but each one in flight holds
282
283
284
285
286
287
288
289
290
291
292
    while len(in_flight) >= _MAX_BROADCASTS_IN_FLIGHT:
        _finish_broadcast(in_flight.popleft())
    handle = dist.broadcast(buffer, source.src_rank, groups[source.group_ranks], async_op=True)
    if handle is None:
        if after is not None:
            after()
        return
    in_flight.append((handle, after))


def _finish_broadcast(pending: tuple) -> None:
414
415
416
417
418
419
420
421
422
        """Send one batch, and arrange for it to be dealt out once it has landed."""
        buffers = self._batches.pop(key, [])
        self._pending_bytes.pop(key, None)
        if not buffers:
            return

        group_ranks, src_rank = key[:2]
        source = BroadcastSource(group_ranks=group_ranks, src_rank=src_rank)
        self.sent += 1
464
465
466
467
468
469
470
        Any: A tensor view over the shard, writable in place by a collective.
    """
    if isinstance(obj, DTensor):
        if obj.layout is not None and obj.layout.ragged_shard is not None:
            return get_ragged_box_tensor(obj, index).detach()
        return obj.to_local().detach()
    return obj.detach()
hyper_parallel/core/distributed_checkpoint/checkpoint_io.py
34
35
36
37
38
39
40
41
42
    """
    if ckpt_format == "safetensors":
        save_file(tensors=state_dict, filename=file_path)
    else:
        torch.save(obj=state_dict, f=file_path)


def load_checkpoint_file(file_path: str, ckpt_format: str = "safetensors") -> dict:
    """
49
50
51
52
53
54
55

    Returns:
        dict: What the file holds, keyed by physical name.
    """
    if ckpt_format == "safetensors":
        return load_file(filename=file_path)
    return torch.load(f=file_path)
hyper_parallel/core/distributed_checkpoint/offline_transform.py
72
73
74
75
76
77
78
79
80
        return
    for key, value in state_dict.items():
        if isinstance(value, (torch.Tensor, bytes, str)):
            continue
        logger.warning(
            "Unsupported offline checkpoint value type for key %r: %s "
            "(expected torch.Tensor, bytes, or str per project rules).",
            key,
            type(value).__name__,
78
79
80
81
82
83
84
85
            "(expected torch.Tensor, bytes, or str per project rules).",
            key,
            type(value).__name__,
        )
        raise TypeError(
            f"Offline checkpoint expects torch.Tensor, bytes, or str per key; "
            f"got {type(value).__name__} for key {key!r}."
        )
203
204
205
206
207
208
209
210
211

    Shard filenames follow Hugging Face conventions (``model.safetensors`` or
    ``model-00001-of-00002.safetensors``).
    """
    get_storage_size = get_tensor_storage_size

    filename_pattern = _HF_SAFE_WEIGHTS_FILENAME_PATTERN

    max_cap: int | str = max_shard_size
261
262
263
264
265
266
267
268
    """
    if state_dict is None:
        raise ValueError("state_dict is None.")

    _validate_state_dict(state_dict)

    if os.path.isfile(save_directory):
        raise ValueError(f"The save_directory {save_directory} should be a directory, but a file.")
282
283
284
285
286
287
288
289
290
    filename_to_tensors = state_dict_split.filename_to_tensors.items()

    for shard_file, tensors in filename_to_tensors:
        shard = {tensor: state_dict[tensor] for tensor in tensors}
        save_checkpoint_file(shard, os.path.join(save_directory, shard_file), ckpt_format="safetensors")
    if index is None:
        path_to_weights = os.path.join(save_directory, weights_name)
        logger.info("Model weights saved in %s", path_to_weights)
    else:
313
314
315
316
317
318
319
320
321

    state_dict: dict[str, Any] = {}

    if os.path.isfile(safe_weights_file):
        state_dict = load_checkpoint_file(safe_weights_file, ckpt_format="safetensors")
        logger.info("Loaded safetensors checkpoint from %s", safe_weights_file)

    else:
        safe_index_file = os.path.join(resume_from_checkpoint, _SAFE_WEIGHTS_INDEX_NAME)
328
329
330
331
332
333
334
335
336

        total_size = 0
        for shard_file in shard_files:
            shard_path = os.path.join(resume_from_checkpoint, shard_file)
            shard_state_dict = load_checkpoint_file(shard_path, ckpt_format="safetensors")
            for key, value in shard_state_dict.items():
                if key in state_dict:
                    logger.warning(
                        "Duplicate key %r when merging Hugging Face shards; keeping first occurrence.",
337
338
339
340
341
342
343
344
345
346
                        key,
                    )
                    continue
                state_dict[key] = value
                if isinstance(value, torch.Tensor):
                    size = get_tensor_storage_size(value)
                    total_size += size
                    logger.debug("Loaded tensor %r, size %s bytes", key, size)
                else:
                    logger.debug("Loaded non-tensor entry %r", key)
344
345
346
347
348
349
350
351
352
                    logger.debug("Loaded tensor %r, size %s bytes", key, size)
                else:
                    logger.debug("Loaded non-tensor entry %r", key)
        logger.info("Merged Hugging Face shards, total tensor bytes (sum per key): %s", total_size)
    _validate_state_dict(state_dict)
    return state_dict


def _full_checkpoint_format_for_path(path: str) -> str:
446
447
448
449
450
451
452
453
454
455
            raise ValueError(
                "src_platform='torch' requires a single PyTorch checkpoint file path; "
                f"src_ckpt must be an existing file, got {src_ckpt!r}."
            )
        fmt = _full_checkpoint_format_for_path(str(src_ckpt))
        state_dict = load_checkpoint_file(str(src_ckpt), ckpt_format=fmt)
    else:
        raise ValueError(
            f"Unsupported src_platform={src_platform!r}; expected 'huggingface' or 'torch'."
        )
hyper_parallel/core/distributed_checkpoint/ragged.py
45
46
47
48
49
50
51
52
53
54
55
) -> tuple[tuple[tuple[int, ...], tuple[int, ...]], ...]:
    """Decompose a row-major flat interval into ordered N-D boxes."""
    total_numel = prod(shape)
    if not shape:
        raise ValueError("Ragged checkpoint geometry requires a non-scalar global shape")
    if flat_start < 0 or flat_end < flat_start or flat_end > total_numel:
        raise ValueError(
            "Invalid flat interval for Ragged checkpoint geometry, "
            f"got interval=({flat_start}, {flat_end}), shape={shape!r}"
        )
    if flat_start == flat_end:
122
123
124
125
126
127
128
129
130
def compute_ragged_boxes(tensor: DTensor) -> tuple[RaggedCheckpointBox, ...]:
    """Return ordered N-D boxes covering one RaggedShard local flat tensor."""
    layout = tensor.layout
    if layout.ragged_shard is None:
        raise ValueError("compute_ragged_boxes requires a RaggedShard DTensor")

    global_shape = tuple(tensor.shape)
    ragged_slice = _compute_ragged_slice(global_shape, layout)
    raw_boxes = _decompose_flat_interval(
146
147
148
149
150
151
152
153
154
        )
        local_flat_offset += box_numel

    if local_flat_offset != ragged_slice.local_numel:
        raise ValueError(
            "Ragged checkpoint boxes do not cover the local flat tensor, "
            f"covered={local_flat_offset}, expected={ragged_slice.local_numel}"
        )
    return tuple(boxes)
184
185
186
187
188
189
190
191
            local_flat = tensor.to_local().reshape((-1,))
            return local_flat[
                box.local_flat_start:box.local_flat_end
            ].reshape(box.sizes)
    raise ValueError(
        "Ragged checkpoint box was not found in the local DTensor, "
        f"fqn={index.fqn!r}, offset={index.offset!r}"
    )
hyper_parallel/core/distributed_checkpoint/standard_planner.py
438
439
440
441
442
443
444
445
446
        if item.type == WriteItemType.TENSOR:
            if isinstance(obj, DTensor):
                if obj.layout is not None and obj.layout.ragged_shard is not None:
                    return get_ragged_box_tensor(obj, item.index).detach().to("cpu")
                return obj.to_local().detach().to("cpu")
            if isinstance(obj, Tensor):
                return obj.detach().to("cpu")
            raise TypeError(f"Write item {fqn} expected tensor-like object, got {type(obj)}")
        if item.type == WriteItemType.BYTE_IO:
790
791
792
793
794
795
796
797
798
                and target.layout.ragged_shard is not None
        ):
            local_tensor = get_ragged_box_tensor(target, read_item.dest_index)
        elif isinstance(target, DTensor):
            local_tensor = target.to_local()
        else:
            local_tensor = target

        # Detached because the caller writes the loaded slice in place: an in-place write
hyper_parallel/core/distributed_checkpoint/state_dict.py
39
40
41
42
43
44
45
46
47

    Returns:
        dict: Optimizer state dict with FQN-based keys.
    """
    return _get_optim_state_dict(model, optimizer, options=options)


def set_optim_state_dict(
    model: Any,
58
59
60
61
62
        optim_state_dict: The optimizer state dict to load.
        options: Optional configuration (full_state_dict, cpu_offload,
            strict, broadcast_from_rank0, etc.).
    """
    _set_optim_state_dict(model, optimizer, optim_state_dict, options=options)
hyper_parallel/core/distributed_checkpoint/utils.py
54
55
56
57
58
59
60
61
62
63
64

    Returns:
        int: Element count times element size, which is what it occupies on disk.
    """
    if not isinstance(tensor, torch.Tensor):
        raise TypeError(f"get_tensor_storage_size expects torch.Tensor, got {type(tensor)!r}")
    return int(tensor.numel()) * int(tensor.element_size())


def str_to_dtype(dtype_str: str) -> torch.dtype:
    """
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
        torch.dtype: The dtype it names.
    """
    parts = dtype_str.split(".", 1)
    if len(parts) != 2:
        raise ValueError(f"Expected dtype string like 'torch.float32', got {dtype_str!r}.")
    prefix, name = parts
    if prefix != "torch":
        raise ValueError(f"Expected PyTorch dtype string with prefix 'torch', got {dtype_str!r}.")
    dtype = getattr(torch, name, None)
    if isinstance(dtype, torch.dtype):
        return dtype
    raise ValueError(f"{dtype_str!r} does not resolve to a torch.dtype.")


def dcp_timer_decorator(func: Callable) -> Callable:
    """
129
130
131
132
133
134
135
136
137
    Returns:
        Any: The narrowed tensor slice (tensor-like object).
    """
    if not offsets or not lengths:
        return tensor
    slices = tuple(
        slice(int(off), int(off) + int(ln))
        for off, ln in zip(offsets, lengths)
    )
171
172
173
174
175
176
177
178
179
180
181
182
    # Validate input formats
    def is_valid_axis_list(axis_list: Any) -> None:
        """Reject an area that is not a sequence of two-element ranges."""
        if not isinstance(axis_list, (tuple, list)):
            raise TypeError("Area must be a tuple of ranges")
        for axis_range in axis_list:
            if (not isinstance(axis_range, (tuple, list)) \
                or len(axis_range) != 2):
                raise TypeError("Each axis range must be a 2-element tuple")

    is_valid_axis_list(area_a)
    is_valid_axis_list(area_b)
216
217
218
219
220
221
222
223
224
225
    """
    if isinstance(obj, DTensor):
        layout = obj.layout
        if layout is None:
            shape = obj.shape if hasattr(obj, "shape") else obj.to_local().shape
            return [ChunkStorageMetadata(offsets=(0,) * len(shape), sizes=tuple(shape))]
        if layout.ragged_shard is not None:
            return [
                ChunkStorageMetadata(offsets=box.offsets, sizes=box.sizes)
                for box in compute_ragged_boxes(obj)
229
230
231
232
233
234
235
236
237
238
239
240
241
242
        tensor_map = getattr(layout, "tensor_map", None) or getattr(layout, "_tensor_map", None)
        rank_list = getattr(layout, "rank_list", None) or getattr(layout, "_rank_list", None)

        if mesh_shape is None or tensor_map is None or rank_list is None:
            shape = obj.shape if hasattr(obj, "shape") else obj.to_local().shape
            return [ChunkStorageMetadata(offsets=(0,) * len(shape), sizes=tuple(shape))]

        current_rank = dist.get_rank()
        if current_rank not in rank_list:
            return []

        inner_rank_id = rank_list.index(current_rank)
        full_shape = obj.shape
        slice_area = infer_slice_area_by_layout(
317
318
319
320
321
322
323
324
325
326
        holder_count = (len(occurrences) - 1) // 2
        if holder_count > 1:
            duplicates.append((item_size, occurrences))
            continue
        masks[occurrences[1]][occurrences[2]] = 1
        storage_sizes[occurrences[1]] += item_size

    # Largest first, so the shards with room to unbalance the plans are placed while every plan is
    # still a candidate. Python's sort is stable, including with reverse=True, so equally sized
    # duplicates keep their gather order and every rank still agrees on the owner. Sorting is
331
332
333
334
335
336
337
338
339
    for item_size, occurrences in duplicates:
        # Occurrences were appended in ascending plan order, so slot 1 is the lowest plan index
        # and the storage-size search breaks ties towards it.
        if save_to_minimum_rank:
            owner_slot = 1
        else:
            owner_slot = _least_loaded_owner_slot(occurrences, storage_sizes)
        owner_idx = occurrences[owner_slot]
        masks[owner_idx][occurrences[owner_slot + 1]] = 1
359
360
361
362
363
364
365
366
367
    best_size = storage_sizes[occurrences[1]]
    for slot in range(3, len(occurrences), 2):
        size = storage_sizes[occurrences[slot]]
        if size < best_size:
            best_slot, best_size = slot, size
    return best_slot


def traverse_state_dict(
388
389
390
391
392
393
394
395
396
        for entry in values:
            if isinstance(entry, (Mapping, list, tuple)) and not _is_terminal(entry):
                return False
            if isinstance(entry, Tensor):
                return False
        return True

    def _traverse_obj(path: tuple[Any, ...], value: Any) -> None:
        if isinstance(value, Mapping):
451
452
453
454
455
456
457
458
            cur_container = cur_container[prev_key]

    last_key = path[-1]
    if isinstance(last_key, int):
        extend_list(cur_container, last_key)

    cur_container[last_key] = value