Diff Coverage

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

Source File Diff Coverage (%) Missing Lines
hyper_parallel/trainer/base.py 0.0% 489,496-497
hyper_parallel/trainer/callbacks/environ_meter_callback.py 15.0% 47,87-89,93,102,167-174,176-177,181-187,191,271-272,274,289-290,312-316
hyper_parallel/trainer/callbacks/tqdm_callback.py 25.6% 41-48,57-58,66-71,80-84,93-94,157-160,230,236
hyper_parallel/trainer/text_trainer.py 0.0% 181,187,228,233-234
hyper_parallel/trainer/base.py
485
486
487
488
489
490
491
492
493
        """Run all registered callbacks at the start of a training step."""
        for callback in self._callbacks:
            callback.on_step_begin(self.state, **kwargs)

    def on_micro_step_begin(self, micro_batch: Dict[str, Any], **kwargs: Any) -> None:
        """Run all registered callbacks before one forward-backward micro step.

        Args:
            micro_batch: Prepared inputs for the current micro step.
492
493
494
495
496
497
498
499
500
501
        Args:
            micro_batch: Prepared inputs for the current micro step.
            **kwargs: Additional callback context.
        """
        for callback in self._callbacks:
            callback.on_micro_step_begin(self.state, micro_batch, **kwargs)

    def on_step_end(
        self,
        loss: Optional[float] = None,
hyper_parallel/trainer/callbacks/environ_meter_callback.py
43
44
45
46
47
48
49
50
51
            trainer: Trainer that owns the callback lifecycle.
        """
        super().__init__(trainer)
        self._step_start_time = 0.0
        self._local_step_tokens: Any = None
        self._local_step_samples = 0
        self._consumed_tokens = 0
        self._consumed_samples = 0
        self.trainer.step_train_metrics = {}
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97

    @classmethod
    def _batch_tokens(cls, batch: Mapping[str, Any]) -> Any:
        """Count text tokens without synchronizing a device scalar to the host."""
        token_count = batch.get("token_count")
        if token_count is not None:
            return token_count

        labels = batch.get("labels")
        if labels is not None and callable(getattr(labels, "sum", None)):
            return (labels != IGNORE_INDEX).sum()

        attention_mask = batch.get("attention_mask")
        attention_mask_shape = getattr(attention_mask, "shape", ())
        if (
 98
 99
100
101
102
103
104
105
106
            len(attention_mask_shape) <= 2
            and attention_mask is not None
            and callable(getattr(attention_mask, "sum", None))
        ):
            return attention_mask.sum()

        input_ids = batch.get("input_ids")
        input_numel = cls._tensor_numel(input_ids)
        if input_numel is not None:
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
        return float(reduced)

    def _accumulate_batches(self, value: Any) -> None:
        """Accumulate batch metrics without retaining input tensor references."""
        for batch in self._micro_batches(value):
            token_count = self._batch_tokens(batch)
            if callable(getattr(token_count, "detach", None)):
                token_count = token_count.detach()
            if self._local_step_tokens is None:
                if callable(getattr(token_count, "clone", None)):
                    token_count = token_count.clone()
                self._local_step_tokens = token_count
            else:
                self._local_step_tokens = self._local_step_tokens + token_count
            self._local_step_samples += self._batch_samples(batch)

    def _global_samples(self) -> int:
        """Reduce samples across DP+CP while removing CP replicas."""
        cp_size = int(getattr(self.trainer.mesh, "cp_size", 1))
        if cp_size < 1:
            raise ValueError(f"mesh.cp_size must be positive, but got {cp_size}")
        reduced_samples = self._reduce(self._local_step_samples, op="sum")
        global_samples = reduced_samples / cp_size
        if not global_samples.is_integer():
            raise ValueError(
                "Reduced sample count must be divisible by cp_size, "
                f"but got reduced_samples={reduced_samples} and cp_size={cp_size}"
            )
        return int(global_samples)

    def _current_lr(self) -> float:
        """Return the maximum learning rate across scheduler or optimizer groups."""
        schedulers = self.trainer.lr_scheduler
267
268
269
270
271
272
273
274
275
276
277
278
            micro_batches: Batches available before the step starts.
            **kwargs: Unused callback context.
        """
        del state, kwargs
        self._local_step_tokens = None
        self._local_step_samples = 0
        self._step_start_time = time.perf_counter()
        self._accumulate_batches(micro_batches)

    def on_micro_step_begin(
        self,
        state: TrainerState,
285
286
287
288
289
290
291
292
293
294
            state: Current training progress.
            micro_batch: Prepared inputs and lightweight metric metadata.
            **kwargs: Unused callback context.
        """
        del state, kwargs
        self._accumulate_batches(micro_batch)

    def on_step_end(
        self,
        state: TrainerState,
308
309
310
311
312
313
314
315
316
317
318
319
320
        """
        del state, kwargs
        step_time = max(time.perf_counter() - self._step_start_time, 0.0)
        global_step_time = self._reduce(step_time, op="max")
        local_step_tokens = 0 if self._local_step_tokens is None else self._local_step_tokens
        global_tokens = int(self._reduce(local_step_tokens, op="sum"))
        global_samples = self._global_samples()
        self._local_step_tokens = None
        self._local_step_samples = 0
        self._consumed_tokens += global_tokens
        self._consumed_samples += global_samples

        train_metrics = {
hyper_parallel/trainer/callbacks/tqdm_callback.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
        initial: int,
        stream: Any,
    ) -> None:
        """Initialize non-interactive progress state without emitting a line."""
        self._tqdm_type = tqdm_type
        self._initial = initial
        self._start_time = time.monotonic()
        self._postfix: dict[str, str] = {}
        self._pending_message: str | None = None
        self.total = total
        self.n = initial
        self.fp = stream

    def set_postfix(self, postfix: dict[str, str], refresh: bool = True) -> None:
        """Store the final metrics for the next completed-step snapshot.
53
54
55
56
57
58
59
60
61
        Args:
            postfix: Formatted metric names and values.
            refresh: Accepted for compatibility with the tqdm API.
        """
        del refresh
        self._postfix = dict(postfix)

    def update(self, amount: int) -> None:
        """Advance progress and write exactly one newline-terminated snapshot.
62
63
64
65
66
67
68
69
70
71
72
73
74
75

        Args:
            amount: Number of newly completed steps.
        """
        self.n += amount
        elapsed = max(time.monotonic() - self._start_time, 0.0)
        completed = self.n - self._initial
        rate = completed / elapsed if elapsed > 0 else None
        postfix = ", ".join(f"{name}={value}" for name, value in self._postfix.items())
        progress_line = self._tqdm_type.format_meter(
            n=self.n,
            total=self.total,
            elapsed=elapsed,
            prefix="Training",
76
77
78
79
80
81
82
83
84
85
86
87
            unit="step",
            rate=rate,
            postfix=postfix or None,
        )
        if self._pending_message:
            progress_line = f"{progress_line} | {self._pending_message}"
        self.fp.write(f"{progress_line}\n")
        self.fp.flush()
        self._pending_message = None

    def write(self, message: str, file: Any = None) -> None:
        """Attach one structured message to the next progress snapshot.
89
90
91
92
93
94
95
96
97
        Args:
            message: Structured log message for the current step.
            file: Accepted for compatibility with the tqdm API.
        """
        del file
        self._pending_message = message

    def close(self) -> None:
        """Close without writing a duplicate final progress line."""
153
154
155
156
157
158
159
160
161
162
163
164
            if not self._missing_dependency_warned:
                logger.warning("TqdmCallback: 'tqdm' is not installed; progress bar disabled")
                self._missing_dependency_warned = True
            return None
        stream = sys.stderr
        isatty = getattr(stream, "isatty", None)
        if not callable(isatty) or not isatty():
            return _LogProgressBar(
                tqdm_type=tqdm,
                total=total,
                initial=initial,
                stream=stream,
226
227
228
229
230
231
232
233
234
            loss_dict: Named loss values for the completed step.
            grad_norm: Gradient norm measured before the optimizer update.
            **kwargs: Additional callback arguments.
        """
        del loss, loss_dict, grad_norm, kwargs
        if self._progress_bar is None or state.global_step <= self._last_updated_step:
            return

        postfix = self._postfix()
232
233
234
235
236
237
238
239
240
            return

        postfix = self._postfix()
        if postfix:
            self._progress_bar.set_postfix(postfix, refresh=False)
        self._progress_bar.update(state.global_step - self._last_updated_step)
        self._last_updated_step = state.global_step

    def write(self, message: str) -> bool:
hyper_parallel/trainer/text_trainer.py
177
178
179
180
181
182
183
184
185
    def on_step_begin(self) -> None:
        """Dispatch the step-begin lifecycle hook."""
        self.base.on_step_begin()

    def on_micro_step_begin(self, micro_batch: dict[str, Any]) -> None:
        """Dispatch the micro-step-begin lifecycle hook.

        Args:
            micro_batch: Prepared inputs and lightweight metric metadata.
183
184
185
186
187
188
189
190
191

        Args:
            micro_batch: Prepared inputs and lightweight metric metadata.
        """
        self.base.on_micro_step_begin(micro_batch)

    def on_step_end(
            self,
            loss: Any = None,
224
225
226
227
228
229
230
231
232
233
234
235
236
237
        self.base.step_token_counts = {
            name: token_count * num_micro_steps
            for name, token_count in self.base.current_token_counts.items()
        }
        metric_inputs = {
            **model_inputs,
            **loss_inputs,
            "token_count": self.base.current_token_counts["foundation_tokens"],
        }
        self.on_micro_step_begin(metric_inputs)
        del metric_inputs
        loss, loss_dict = self.base.forward_backward_step(model_inputs, loss_inputs)

        return loss, loss_dict