Diff Coverage

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

Source File Diff Coverage (%) Missing Lines
hyper_parallel/core/pipeline_parallel/mpipe/__init__.py 100%  
hyper_parallel/core/pipeline_parallel/mpipe/executor_base.py 89.5% 100,121,125-126,128,134,139,214,289-290,292,315,340-346
hyper_parallel/core/pipeline_parallel/mpipe/sampler.py 100%  
hyper_parallel/core/pipeline_parallel/mpipe/schedule.py 99.1% 190
hyper_parallel/core/pipeline_parallel/mpipe/step_types.py 100%  
hyper_parallel/core/pipeline_parallel/scheduler.py 68.1% 716,1087,1093,1112-1113,1116,1119-1121,1123-1124,1127-1130,1132-1136,1138-1140,1142-1144,1147-1155,1157-1158,1910,1914,1932,1966,1968,1972
hyper_parallel/platform/mindspore/pipeline_parallel/mpipe_transpose.py 0.0% 29
hyper_parallel/platform/torch/activation_checkpoint/activation_swap.py 50.0% 149
hyper_parallel/platform/torch/pipeline_parallel/mpipe_transpose.py 88.9% 64,80,96,106,111
hyper_parallel/platform/torch/pipeline_parallel/stage.py 100%  
hyper_parallel/trainer/base.py 6.2% 459,525-532,534-535,537-538,543,551-557,1919,1922,1927-1933,1935-1940,1953-1954,1956-1959,1963-1972,2043-2044,2046-2052
hyper_parallel/trainer/config.py 100%  
hyper_parallel/core/pipeline_parallel/mpipe/executor_base.py
 96
 97
 98
 99
100
101
102
103
        self._fwd_received.clear()
        # Snapshot the already-reduced grads from prior runs so GRAD_REDUCE
        # reduces only this run's added contribution.
        if self._owner_backward:
            self._grad_snapshot = self._snapshot_tower_grads()

    def _send_meta(self, tensor, dst) -> None:
        """Send a tensor's ``(shape, dtype)`` to ``dst``.
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

    def _output_arity(self):
        """Number of output tensors to communicate (cached after first use)."""
        if self._output_arity_for_comm is not None:
            return self._output_arity_for_comm
        # Fallback: derive the arity from the first retained micro.
        if self._outputs_for_stage0.get(0):
            self._output_arity_for_comm = len(self._outputs_for_stage0[0])
        elif self._outputs_for_bwd.get(0):
            self._output_arity_for_comm = len(self._outputs_for_bwd[0])
        else:
            raise ValueError("Cannot determine the arity, number of elements to be communicated")
        return self._output_arity_for_comm

    def _input_arity(self):
        """Number of input tensors to communicate (cached after first use)."""
130
131
132
133
134
135
136
137
138
139
140
141
142
143

    def _input_arity(self):
        """Number of input tensors to communicate (cached after first use)."""
        if self._input_arity_for_comm is not None:
            return self._input_arity_for_comm
        # Fallback: derive the arity from the first retained micro.
        if self._inputs_for_explicit_forward.get(0):
            self._input_arity_for_comm = len(self._inputs_for_explicit_forward[0])
        else:
            raise ValueError("Cannot determine the arity, number of elements to be communicated")
        return self._input_arity_for_comm

    def _global_rank(self, group_rank: int) -> int:
        """Global rank of group pipeline rank ``group_rank``."""
210
211
212
213
214
215
216
217
                self._output_arity_for_comm = len(out)
                self._save_grad_for_bwd(micro, out)
                self._transfer_out_for_fwd(ctx, micro, out)
            else:
                self._outputs_for_stage0[micro] = out
        # Stages built under meta-init only learn their real device here.
        if out and getattr(self._device, "type", None) in (None, "meta"):
            self._device = out[0].device
285
286
287
288
289
290
291
292
293
294
295
296
                are handed to the forward via ``_transfer_out_for_fwd`` and
                recorded via ``_save_grad_for_bwd``.
        """
        if not _RECV_BATCH:
            self._fwd_recv_one(step.micro_index, ctx)
            return
        if step.micro_index in self._fwd_received:
            return
        self._fwd_recv_all(ctx)

    def _recv_device(self):
        """Real (non-``meta``) device for receive buffers.
311
312
313
314
315
316
317
318
                             self._keep_grad.get(0)):
            if materialized:
                self._device = materialized[0].device
                return self._device
        return self._device

    def _recv_payload(self, micro):
        """Post the recv of ``micro``'s preprocess output; return ``(buffers, handles)``.
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
        return buffers, handles

    def _fwd_recv_one(self, micro: int, ctx: "PipelineContext") -> None:
        """Receive a single transposed micro-batch's output, waiting inline (fallback)."""
        buffers, handles = self._recv_payload(micro)
        for handle in handles:
            handle.wait()
        out = tuple(buffers)
        self._save_grad_for_bwd(micro, out)
        self._transfer_out_for_fwd(ctx, micro, out)
        self._fwd_received.add(micro)

    def _fwd_recv_all(self, ctx: "PipelineContext") -> None:
        """Post every outstanding transposed recv, then wait once so the transfers
        overlap each other and stage 0's own preprocess forward."""
hyper_parallel/core/pipeline_parallel/mpipe/schedule.py
186
187
188
189
190
191
192
193
194
            and self._has_trainable_preprocess
            and platform.platform_type == PlatformType.PYTORCH
        )
        if owner_backward and self._has_trainable_preprocess and not self._owner_backward:
            logger.warning(
                "[mpipe] pp_mpipe_owner_backward requested but unsupported on "
                "platform %s; falling back to the stage-0 backward.",
                platform.platform_type,
            )
hyper_parallel/core/pipeline_parallel/scheduler.py
712
713
714
715
716
717
718
719
720
            # An exception unwinds past the end-of-iteration drain, leaving
            # handles un-waited; wait them so the contract holds on that path too.
            self._drain_inflight_p2p()
            if self._swap_session is not None:
                self._swap_session.close()
                self._swap_session = None
        return losses

    def sync_shared_parameters_grad(self):
1083
1084
1085
1086
1087
1088
1089
1090
1091
        """
        # Only active under ``run_with_dataiterator``; the legacy ``run()``
        # path pre-stages every per-micro kwarg on every rank already.
        if self.data_iterator is None:
            return
        if step_type == MetaStepType.DATA_LOAD:
            if stage_index <= 0:
                device = self.stages[0].device
                micro_batch = next(self.data_iterator)
1089
1090
1091
1092
1093
1094
1095
1096
1097
            if stage_index <= 0:
                device = self.stages[0].device
                micro_batch = next(self.data_iterator)
                if isinstance(micro_batch, list):
                    micro_batch = micro_batch[micro_index]
                micro_batch = {
                    key: (value.to(device, non_blocking=True) if hasattr(value, "to") else value)
                    for key, value in micro_batch.items()
                }
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
                self.last_local_tokens += n_valid

                micro_batch["targets"] = targets
                for key in self.kwargs_batch_dim:
                    if key in kwarg_mbs[micro_index].keys():
                        kwarg_mbs[micro_index][key] = platform.cat(
                            [kwarg_mbs[micro_index][key], micro_batch[key]], dim=0)
                    else:
                        kwarg_mbs[micro_index][key] = micro_batch[key]
                arg_mbs[micro_index] = [input_ids]

        elif step_type == MetaStepType.DATA_SEND:
            if getattr(self, "_data_dst", False):
                dst_stage_idx = self._data_dst[stage_index][micro_index]
            else:
                dst_stage_idx = self.stages[0].dst_stage
            dst = self.stages[0]._global_rank(dst_stage_idx)  # pylint: disable=protected-access
            # One meta round-trip per DATA_SEND instead of K; the matching
            # DATA_RECV unpacks the list in the same order.
            metas = []
            tensors = []
            for key in self._DATA_KEYS:
                tensor = (arg_mbs[micro_index][0] if key == "input_ids"
                            else kwarg_mbs[micro_index][key])
                metas.append([tuple(tensor.shape), tensor.dtype])
                tensors.append(tensor)
            platform.send_object_list(metas, dst)
            handles = [platform.isend(t, dst) for t in tensors]
            self._wait_p2p(handles)

        elif step_type == MetaStepType.DATA_RECV:
            if getattr(self, "_data_src", False):
                src_stage_idx = self._data_src[stage_index][micro_index]
            else:
                src_stage_idx = self.stages[0].src_stage
            src = self.stages[0]._global_rank(src_stage_idx)  # pylint: disable=protected-access
            device = self.stages[0].device
            # One recv_object_list unpacks every key's (shape, dtype) at
            # once, matching the packed DATA_SEND above.
            metas: list = [None] * len(self._DATA_KEYS)
            platform.recv_object_list(metas, src)
            handles = []
            for key, meta in zip(self._DATA_KEYS, metas):
                shape, dtype = meta
                buffer = platform.empty(shape, dtype=dtype, device=device)
                handles.append(platform.irecv(buffer, src))
                if key == "input_ids":
                    arg_mbs[micro_index] = [buffer]
                else:
                    kwarg_mbs[micro_index][key] = buffer
            self._wait_p2p(handles)

    def _exec_pipeline_swap_step(self, cur_step, arg_mbs, kwarg_mbs):
        """Execute a pipeline activation-swap control step."""
        if self._swap_session is None:
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
            peer = _fwd_peer(step.stage_index)
            if peer is not None:
                new_schedule[rank].append(
                    MetaStep(step.micro_index, MetaStepType.DATA_SEND, step.stage_index))
                new_schedule[rank].append(
                    MetaStep(step.micro_index, MetaStepType.FWD_SEND, step.stage_index))
                new_schedule[peer].append(
                    MetaStep(step.micro_index, MetaStepType.DATA_RECV, step.stage_index + 1))
                new_schedule[peer].append(
                    MetaStep(step.micro_index, MetaStepType.FWD_RECV, step.stage_index + 1))
        elif step.type == MetaStepType.BWD:
            peer = _bwd_peer(step.stage_index)
            if peer is not None:
1928
1929
1930
1931
1932
1933
1934
1935
1936
    # the RECV triggered by an overlap's second sub-step lands one slot
    # later on the receiver — matching the fact that the sender can only
    # finish emitting the second sub-step after the first completes.
    expanded = _expand_overlap_slots(aligned, real_stage_num)
    return _insert_dataload_before_fwd(
        _column_scan_insert_comms(expanded, real_stage_num, _insert_comms_for_step))


class ScheduleGPipe(PipelineScheduleRuntime):
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
        """construct_exec_order of Gpipe."""
        for stage_index in range(self.real_stage_num):
            order_list = []
            for mb_index in range(self.micro_batch_num):
                order_list.append(MetaStep(mb_index, MetaStepType.DATA_LOAD, stage_index))
                if stage_index != 0:
                    order_list.append(MetaStep(mb_index, MetaStepType.DATA_RECV, stage_index))
                    order_list.append(MetaStep(mb_index, MetaStepType.FWD_RECV, stage_index))
                order_list.append(MetaStep(mb_index, MetaStepType.FWD, stage_index))
                if stage_index != self.real_stage_num - 1:
                    order_list.append(MetaStep(mb_index, MetaStepType.DATA_SEND, stage_index))
                    order_list.append(MetaStep(mb_index, MetaStepType.FWD_SEND, stage_index))
            for mb_index in range(self.micro_batch_num):
                if stage_index != self.real_stage_num - 1:
                    order_list.append(MetaStep(mb_index, MetaStepType.BWD_RECV, stage_index))
hyper_parallel/platform/mindspore/pipeline_parallel/mpipe_transpose.py
25
26
27
28
29
30
31
32
Note:
    Exercised by the MindSpore ST gate (msrun), not the torch/CPU coverage job
    (mindspore isn't importable there).
"""
from mindspore import ops

from hyper_parallel.core.pipeline_parallel.mpipe.executor_base import MPipeTransposeExecutorBase
from hyper_parallel.platform.mindspore.pipeline_parallel.backward import forward_and_gradfn
hyper_parallel/platform/torch/activation_checkpoint/activation_swap.py
145
146
147
148
149
150
151
152
153
                _raise_callable_already_wrapped(wrapped_callable)
            # A param-free module shared across siblings (one rotary embedding
            # per decoder layer) is never its own region; skip, do not flag.
            if next(submodule.parameters(recurse=True), None) is None:
                continue
            warnings.warn(
                f"Submodule '{getattr(submodule, '_swap_wrapped_module', submodule).__class__.__name__}' of "
                f"'{module.__class__.__name__}' is already wrapped. "
                "Wrapping overlapping module regions is not allowed."
hyper_parallel/platform/torch/pipeline_parallel/mpipe_transpose.py
60
61
62
63
64
65
66
67
68
            out = self._preprocess(*args, **kwargs)
        # The preprocess output may be a single tensor (text body input) or a
        # tuple (e.g. a VL visual payload: image_embeds + DeepStack levels).
        if isinstance(out, (tuple, list)):
            return tuple(t.detach() for t in out)
        return out.detach()

    def _connected_forward(self, args, kwargs):
        return self._preprocess(*args, **kwargs)
76
77
78
79
80
81
82
83
84
        # Backprop only the outputs that received a gradient; an absent
        # dL/dfeature is a zero contribution.
        pairs = [(out_i, g) for out_i, g in zip(out, grads) if g is not None]
        if not pairs:
            return
        outs, grad_tensors = zip(*pairs)
        torch.autograd.backward(outs, grad_tensors=grad_tensors)

    @staticmethod
 92
 93
 94
 95
 96
 97
 98
 99
100
        return tensor.detach().contiguous()

    def _zeros_like(self, tensor):
        # Contiguous zero grad for a feature tensor that received none.
        return torch.zeros_like(tensor).contiguous()

    def _owner_transpose_backward(self, retained_out, grads) -> None:
        # Backprop dL/dfeatures through the retained connected tower graph on this
        # rank's replica; skip non-grad-requiring outputs (else autograd raises).
102
103
104
105
106
107
108
109
110
111
112
113
114
115
        if pairs:
            outs, grad_tensors = zip(*pairs)
            torch.autograd.backward(outs, grad_tensors=grad_tensors)
        else:
            logger.debug("[mpipe] owner backward skipped: no retained output requires grad.")

    def _snapshot_tower_grads(self):
        # Clone each trainable tower grad's LOCAL shard (None where absent) so
        # _reduce_grads reduces only this run's contribution.
        return [None if p.grad is None else self._local(p.grad).detach().clone()
                for p in self._preprocess.parameters() if p.requires_grad]

    def _reduce_grads(self, group_info, snapshot) -> None:
        """SUM-reduce each trainable tower param's this-run contribution
hyper_parallel/trainer/base.py
455
456
457
458
459
460
461
462
463
            seed=sampler_seed,
            drop_last=True,
        )

        self._update_dataloader_sampler_for_mpipe()

        # StatefulDataLoader supports state_dict() / load_state_dict()
        # for checkpoint resume (torchdata API, used by  + ).
        num_workers = self.args.data.num_workers
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
        his owned data to load
        """
        # Only for MPipe's in-schedule dataload; every other PP path expects
        # the full step batch on each rank's dataloader.
        load_mode = getattr(self.args.data, "load", None)
        pp_size = int(getattr(self.parallel_dims, "pp", 1))
        pp_schedule = str(getattr(self.args.train.accelerator, "pp_schedule", "")).lower()
        if load_mode == "single" and pp_size > 1 and pp_schedule in ("mpipe", "mpipe_transpose"):
            micro_bs = self.args.train.micro_batch_size
            dp_size = self.parallel_dims.dp_size
            pp_micro_batch_num = int(getattr(self.args.train.accelerator, "pp_micro_batch_num", 1))
            overflow_mode = str(getattr(self.args.train.accelerator,
                                        "pp_mpipe_transpose_overflow", "full"))
            pp_rank = torch.distributed.get_rank(group=self.mesh["pp"].get_group())
            owned = mpipe_owned_micros(pp_size, pp_micro_batch_num, pp_rank,
                                       mode=overflow_mode)
            if not owned:
                raise ValueError(
                    f"data.load='single': PP rank {pp_rank} owns no micros "
                    f"(M={pp_micro_batch_num} < PP={pp_size}). "
                    "Configure pp_micro_batch_num >= pp."
                )
            self.sampler = PPRankOwnedSampler(
                base_sampler=self.sampler,
                owned_micros=owned,
                micro_batch_size=micro_bs,
                pp_micro_batch_num=pp_micro_batch_num,
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
                pp_micro_batch_num=pp_micro_batch_num,
            )

            # Fail on an undersized dataset here rather than mid-run.
            base_len = len(self.sampler.base)
            step_chunk = self.sampler.step_size
            available_steps = base_len // step_chunk if step_chunk else 0
            max_steps = int(getattr(self.args.train, "max_steps", 0))
            if max_steps and available_steps < max_steps:
                per_step = pp_micro_batch_num * micro_bs * dp_size
                raise ValueError(
                    f"data.load='single': dataset feeds only {available_steps} "
                    f"step(s)/epoch (base sampler has {base_len} samples, "
                    f"step chunk={step_chunk}) but max_steps={max_steps}. Each "
                    f"step consumes pp_micro_batch_num*micro_batch_size*dp_size "
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
        Gradient clipping uses the **global** cross-stage norm
        (:meth:`_pp_clip_grad_norm`) so every stage scales by the same
        coefficient — required so the tied embed / lm_head copies stay in sync.
        """
        if getattr(self.args.data, "load", None) == "single":
            # ``last_local_tokens`` carries only this rank's micros, so PP
            # all-reduce for the cross-stage global count.
            outputs = self.pp_schedule.run_with_dataiterator(
                data_iterator,
                getattr(self, "_pp_fsdp_composed", False),
                dp_group_info=getattr(self, "_dp_group_info", None),
            )
            self.state.global_step += 1
            nt = platform.full((1,), self.pp_schedule.last_local_tokens).to(self.device)
            platform.all_reduce(nt, self._pp_group_info)
            n_valid = max(int(nt.item()), 1)
            self._last_global_tokens = n_valid
        elif getattr(self.pp_schedule, "requires_all_rank_input", False):
            outputs, n_valid = self._pp_run_all_rank_input_step(data_iterator)
        else:
            batch, targets, stop = self._pp_load_first_stage_batch(data_iterator)
            targets, attention_mask, has_attn = self._pp_prepare_broadcast_inputs(batch, targets, stop)
            self.state.global_step += 1
            self._pp_validate_rank_average_targets(targets)
            n_valid = self._pp_count_valid_tokens(targets)
            outputs = self._pp_run_schedule(batch, targets, attention_mask, has_attn)
        self._pp_post_schedule_grad_reduce()
        self._pp_average_plain_dp_grads()
        self._pp_normalize_grads(n_valid)
        grad_norm_value = self._optimizer_step_after_backward(self._pp_clip_grad_norm)
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975

        MPipe Transpose runs the transposed preprocess on every rank, so the
        heavy (vision) inputs must be present everywhere.
        """
        micro_batches = next(data_iterator)
        batch = self._pp_concat_micro_batches(micro_batches)
        # End-of-epoch partial grad-accum group: end the epoch.
        if batch["input_ids"].shape[0] % self.pp_micro_batch_num != 0:
            raise StopIteration
        self.state.global_step += 1
        batch = {
            key: (value.to(self.device, non_blocking=True) if hasattr(value, "to") else value)
            for key, value in batch.items()
        }
        labels = batch["labels"]
        targets = torch.nn.functional.pad(labels, (0, 1), value=-100)[..., 1:].contiguous()
        self._pp_validate_rank_average_targets(targets)
        n_valid = self._pp_count_valid_tokens(targets)
        run_kwargs = {"targets": targets}
        for key in self.pp_schedule.kwargs_batch_dim:
            if key != "targets" and key in batch:
                run_kwargs[key] = batch[key]
        outputs = self.pp_schedule.run(batch["input_ids"], **run_kwargs)
        return outputs, n_valid

    def train(self):
        """Main training loop: epoch → step → micro-batch.
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
        Groups ``self._grad_accum`` consecutive batches into a list for
        gradient accumulation. The underlying ``StatefulDataLoader`` tracks
        iteration position, so checkpoint/resume skips consumed batches.
        """
        if getattr(self.args.data, "load", False) == "single":
            yield from self.train_dataloader
        else:
            batch_buffer = []
            for batch in self.train_dataloader:
                batch_buffer.append(batch)
                if len(batch_buffer) >= self._grad_accum:
                    yield batch_buffer
                    batch_buffer = []
            if batch_buffer:
                yield batch_buffer

    def _get_layers(self) -> list:
        """Return the repeating layers for FSDP/AC wrapping.