Diff Coverage

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

Source File Diff Coverage (%) Missing Lines
hyper_parallel/core/utils/clip_grad.py 65.5% 56-59,114,117,138,140,147,160,170,173,176,203,224-226,241-242,245-246,250-251,254-257,264,266-267,277,280,295-296,302,325-327,330,332-333,337,339-340,344,346-347,351,354,359,362,366-367,388,394,400,416-422,426-432,437-443,447-448,543,578-579,583-584,590-592,632,649,655-656,670,738,760-762,773-774,801,806,816-817,821-822,900,919,925
hyper_parallel/platform/torch/clip_grad.py 0.0% 16
hyper_parallel/platform/torch/platform.py 0.0% 1862
hyper_parallel/core/utils/clip_grad.py
52
53
54
55
56
57
58
59
60
61
62
        _device_has_foreach_support,
        _group_tensors_by_device_and_dtype,
        _has_foreach_support,
    )
except ImportError:
    _device_has_foreach_support = None  # type: ignore[assignment]
    _group_tensors_by_device_and_dtype = None  # type: ignore[assignment]
    _has_foreach_support = None  # type: ignore[assignment]

__all__: list[str] = ["clip_grad_norm_"]

110
111
112
113
114
115
116
117
118
119
120
121
    manually divide by the group size.
    """
    lower = op_str.lower()
    if lower == "avg" and not _REDUCE_OP_AVG_SUPPORTED:
        return dist.ReduceOp.SUM, True
    op = _STR_TO_REDUCE_OP.get(lower)
    if op is None:
        raise ValueError(
            f"Unsupported Partial reduce_op: {op_str!r}. "
            f"Supported: {sorted(set(list(_STR_TO_REDUCE_OP) + ['avg']))}"
        )
    return op, False
134
135
136
137
138
139
140
141
142
143
144
    * single ``torch.Tensor`` -> ``[tensor]``
    * iterable of tensors    -> ``list(iterable)``
    """
    if isinstance(parameters, torch.nn.Module):
        return list(parameters.parameters())
    if isinstance(parameters, torch.Tensor):
        return [parameters]
    return list(parameters)


def _param_device(param: torch.Tensor) -> torch.device:
143
144
145
146
147
148
149
150
151

def _param_device(param: torch.Tensor) -> torch.device:
    """Return the local device of *param* (unwrap DTensor if needed)."""
    if _is_dtensor(param):
        return param._local_tensor.device  # pylint: disable=protected-access
    return param.device


def _get_grad_obj(param: torch.nn.Parameter) -> Optional[torch.Tensor]:
156
157
158
159
160
161
162
163
164
    falling back to ``param.grad``.
    """
    grad = getattr(param, "main_grad", None)
    if grad is not None:
        return grad
    return param.grad


def _get_local_grad(param: torch.nn.Parameter) -> Optional[torch.Tensor]:
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180

    Supports ``main_grad`` for fp32 mixed-precision training.
    """
    if not param.requires_grad:
        return None
    grad = _get_grad_obj(param)
    if grad is None:
        return None
    if _is_dtensor(grad):
        return grad._local_tensor  # pylint: disable=protected-access
    return grad


def _get_param_mesh_info(
    param: torch.nn.Parameter,
199
200
201
202
203
204
205
206
207
    grad = _get_grad_obj(param)
    # Prefer grad's spec (most accurate); fall back to param's.
    spec_source = grad if _is_dtensor(grad) else param
    if not _is_dtensor(spec_source):
        return None, (), ()

    shard_dims = tuple(
        i for i, p in enumerate(spec_source.placements)
        if p.is_shard()
220
221
222
223
224
225
226
227
228
229
230
    device: torch.device,
    total: torch.Tensor,
) -> None:
    """Accumulate sum-of-p-th-powers for *dev_grads* into *total*."""
    for g in dev_grads:
        n = torch.linalg.vector_norm(g, norm_type)
        total.add_(n.to(device=device) ** norm_type)


def _foreach_p_norms(
    grads: List[torch.Tensor],
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
    precision as ``vector_norm(dtype=float32)``.  Non-float32 tensors
    and backends that raise ``RuntimeError`` fall back to per-tensor
    ``vector_norm``.
    """
    total = torch.tensor(0.0, device=device, dtype=torch.float32)
    grouped = _group_tensors_by_device_and_dtype(
        [[g.detach() for g in grads]],
    )
    for (dev, _), ([dev_grads], _) in grouped.items():
        if (
            dev_grads[0].dtype == torch.float32
            and _has_foreach_support(dev_grads, dev)
        ):
            try:
                per_norms = torch._foreach_norm(  # pylint: disable=W0212
                    dev_grads, norm_type,
                )
            except RuntimeError:
                per_norms = None
            if per_norms is not None:
                total.add_(
                    torch.stack([
                        n.to(device=device) ** norm_type
                        for n in per_norms
                    ]).sum(),
260
261
262
263
264
265
266
267
268
269
270
271
                        for n in per_norms
                    ]).sum(),
                )
            else:
                _sum_p_norms(dev_grads, norm_type, device, total)
        else:
            _sum_p_norms(dev_grads, norm_type, device, total)
    return total


def _per_tensor_norms(
    grads: List[torch.Tensor],
273
274
275
276
277
278
279
280
281
282
283
    device: torch.device,
) -> List[torch.Tensor]:
    """Return per-tensor norms as a list of scalar tensors on *device*."""
    if not grads:
        return []

    if _group_tensors_by_device_and_dtype is None or not hasattr(torch, "_foreach_norm"):
        return [
            torch.linalg.vector_norm(g.detach(), norm_type).to(device=device)
            for g in grads
        ]
291
292
293
294
295
296
297
298
299
300
            try:
                per_norms = torch._foreach_norm(  # pylint: disable=W0212
                    dev_grads, norm_type,
                )
            except RuntimeError:
                per_norms = None
            if per_norms is not None:
                norms.extend(
                    [n.to(device=device) for n in per_norms],
                )
298
299
300
301
302
303
304
305
306
                norms.extend(
                    [n.to(device=device) for n in per_norms],
                )
                continue
        norms.extend([
            torch.linalg.vector_norm(g, norm_type).to(device=device)
            for g in dev_grads
        ])
    return norms
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
    * ``-inf``  -> +inf (neutral for MIN)
    * ``0``     -> 0    (neutral for SUM)
    * finite    -> 0    (neutral for SUM)
    """
    if not grads:
        if norm_type == -math.inf:
            return torch.tensor(
                float("inf"), device=device, dtype=torch.float32,
            )
        return torch.tensor(0.0, device=device, dtype=torch.float32)

    if norm_type == math.inf:
        norms = [
            torch.linalg.vector_norm(g.detach(), math.inf)
            for g in grads
        ]
        return torch.stack(norms).max().to(device)

    if norm_type == -math.inf:
        norms = [
            torch.linalg.vector_norm(g.detach(), -math.inf)
            for g in grads
        ]
        return torch.stack(norms).min().to(device)

    if norm_type == 0:
        norms = [
            torch.linalg.vector_norm(g.detach(), 0)
            for g in grads
        ]
        return torch.stack(norms).sum().to(device)

    # Finite p-norm: return sum of p-th powers.
    if (
        len(grads) > 1
        and _group_tensors_by_device_and_dtype is not None
        and hasattr(torch, "_foreach_norm")
    ):
        return _foreach_p_norms(grads, norm_type, device)

    # Scalar fallback when foreach utilities are unavailable.
    norms = [
        torch.linalg.vector_norm(g.detach(), norm_type)
        for g in grads
    ]
    norm_powers = [n.to(device=device) ** norm_type for n in norms]
    return torch.stack(norm_powers).sum()


# ---------------------------------------------------------------------------
# Total norm aggregation with collectives
384
385
386
387
388
389
390
391
    ``norm_grads`` (parallel to ``key_per_grad``) holds the tensor whose
    norm to take per parameter; only the finite p-norm path consumes it.
    """
    if norm_type == math.inf:
        return _total_norm_inf(
            grad_groups, norm_type, mesh_cache, device,
            dist.ReduceOp.MAX,
        )
390
391
392
393
394
395
396
397
            dist.ReduceOp.MAX,
        )

    if norm_type == -math.inf:
        return _total_norm_inf(
            grad_groups, norm_type, mesh_cache, device,
            dist.ReduceOp.MIN,
        )
396
397
398
399
400
401
402
403
404
            dist.ReduceOp.MIN,
        )

    if norm_type == 0:
        return _total_norm_sum(
            grad_groups, norm_type, mesh_cache, device,
        )

    # Finite p-norm: FSDP2-aligned sequence.
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
def _total_norm_inf(  # pylint: disable=R0913,R0917
    grad_groups, norm_type, mesh_cache, device, reduce_op,
):
    """Shared logic for inf / -inf norms."""
    group_norms: List[torch.Tensor] = []
    for (mesh_id, shard_dims), grads in grad_groups.items():
        local_norm = _compute_local_norm(grads, norm_type, device)
        if mesh_id is not None:
            mesh = mesh_cache[mesh_id]
            for dim in shard_dims:
                dist.all_reduce(
                    local_norm, op=reduce_op,
                    group=mesh.get_group(dim),
                )
        group_norms.append(local_norm)
    if not group_norms:
        if norm_type == -math.inf:
            return torch.tensor(float("inf"), device=device)
        return torch.tensor(0.0, device=device)
    stacked = torch.stack(group_norms)
    return stacked.max() if reduce_op == dist.ReduceOp.MAX else stacked.min()


def _total_norm_sum(grad_groups, norm_type, mesh_cache, device):
    """Shared logic for finite norms and L0 (all use SUM all-reduce)."""
    total = torch.tensor(0.0, device=device)
    for (mesh_id, shard_dims), grads in grad_groups.items():
        local_val = _compute_local_norm(grads, norm_type, device)
        if mesh_id is not None:
            mesh = mesh_cache[mesh_id]
            for dim in shard_dims:
                dist.all_reduce(
                    local_val, op=dist.ReduceOp.SUM,
                    group=mesh.get_group(dim),
                )
        total.add_(local_val)
    return total


def _reduction_signature(grad_groups, mesh_cache):
    """Bucket grad-group keys by the *process group(s)* they reduce over.
539
540
541
542
543
544
545
546
            local_p = torch.linalg.vector_norm(
                torch.stack(norms).to(torch.float32), norm_type,
            ) ** norm_type
        else:
            local_p = torch.tensor(0.0, device=device, dtype=torch.float32)

        for group in sig_groups[sig]:
            dist.all_reduce(local_p, op=dist.ReduceOp.SUM, group=group)
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
            )
            chunk_sizes.append(local_grad.numel())
            has_grad.append(True)
            active_indices.append(idx)
        elif param.requires_grad:
            local_p = (
                param._local_tensor  # pylint: disable=W0212
                if _is_dtensor(param) else param.data
            )
            numel = local_p.numel()
            chunks.append(
                torch.zeros(
                    numel, device=local_p.device,
                    dtype=torch.float32,
                ),
586
587
588
589
590
591
592
593
594
595
                    numel, device=local_p.device,
                    dtype=torch.float32,
                ),
            )
            chunk_sizes.append(numel)
            has_grad.append(False)
            active_indices.append(idx)

    return chunks, chunk_sizes, has_grad, active_indices

628
629
630
631
632
633
634
635
636
    for idx, info in enumerate(param_infos):
        mesh, partial_info = info[2], info[3]
        if partial_info:
            if mesh is None:
                raise RuntimeError(
                    "clip_grad_norm_: parameter has Partial placements "
                    "but no DeviceMesh. This is a DTensor invariant "
                    "violation."
                )
645
646
647
648
649
650
651
652
653
            _build_coalesce_buffer(param_infos, indices)
        )

        if not chunks:
            continue  # all params frozen, no collective needed

        # Sanity check: same mesh → same device.  Fail fast on
        # misconfigured inputs rather than silent NCCL errors.
        buf_device = chunks[0].device
651
652
653
654
655
656
657
658
659
660
        # Sanity check: same mesh → same device.  Fail fast on
        # misconfigured inputs rather than silent NCCL errors.
        buf_device = chunks[0].device
        for chunk in chunks[1:]:
            if chunk.device != buf_device:
                raise RuntimeError(
                    f"clip_grad_norm_: parameters in the same Partial "
                    f"coalesce group are on different devices "
                    f"({buf_device} vs {chunk.device}). All parameters "
                    f"sharing the same DeviceMesh must reside on the "
666
667
668
669
670
671
672
673
674
        for pdim, reduce_op, needs_avg in partial_info:
            group = mesh.get_group(pdim)
            dist.all_reduce(buf, op=reduce_op, group=group)
            if needs_avg:
                buf /= dist.get_world_size(group=group)

        # Extract views for params with actual gradients.
        offset = 0
        for i, idx in enumerate(active_indices):
734
735
736
737
738
739
740
741
            (param, local_grad, mesh, partial_info, key),
        )

    if device is None:
        device = torch.device("cpu")

    # --- Phase 2: coalesced Partial reduction (O(N) → O(G)) ---
    reduced = _coalesce_partial_reduce(param_infos, mesh_cache)
756
757
758
759
760
761
762
763
764
765
766
        param, local_grad, key = info[0], info[1], info[4]
        if local_grad is None:
            # Ensure the key exists so the Shard norm all-reduce is
            # entered even when this rank has no grads for the group.
            if key not in grad_groups:
                grad_groups[key] = []
            continue

        grad_obj = _get_grad_obj(param)
        if _is_dtensor(grad_obj):
            has_dtensor_grad = True
769
770
771
772
773
774
775
776
777
778
        if idx in reduced:
            grad_groups[key].append(reduced[idx])
            norm_grads.append(reduced[idx])
        else:
            grad_groups[key].append(local_grad)
            norm_grads.append(local_grad)

    return _GradGroups(
        grad_groups, all_grads, norm_grads, key_per_grad,
        mesh_cache, device, has_dtensor_grad,
797
798
799
800
801
802
803
804
805
806
807
808
809
810
            use_foreach = (
                foreach is None and _has_foreach_support(device_grads, device)
            ) or (foreach and _device_has_foreach_support(device))
            if use_foreach:
                torch._foreach_mul_(  # pylint: disable=W0212
                    device_grads,
                    clip_coef_clamped.to(device=device, dtype=dtype),
                )
            elif foreach:
                raise RuntimeError(
                    f"foreach=True was passed, but can't use the "
                    f"foreach API on {device.type} tensors"
                )
            else:
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
                for g in device_grads:
                    g.mul_(clip_coef_clamped_cast)
    else:
        # Fallback when _foreach_utils is unavailable.
        if foreach:
            raise RuntimeError(
                "foreach=True was passed, but "
                "torch.utils._foreach_utils is not available"
            )
        for grad in all_grads:
            grad.mul_(clip_coef_clamped.to(grad.device, grad.dtype))


# ---------------------------------------------------------------------------
# Public API
896
897
898
899
900
901
902
903
904

    if error_if_nonfinite and torch.logical_or(
        total_norm.isnan(), total_norm.isinf()
    ):
        raise RuntimeError(
            f"The total norm of order {norm_type} for gradients from "
            "`parameters` is non-finite, so it cannot be clipped. To "
            "disable this error and scale the gradients by the "
            "non-finite norm anyway, set "
915
916
917
918
919
920
921
922
923
    # Promote return dtype to match gradient dtypes (FSDP1 convention).
    # When this rank has no gradients, return in the default FP32 dtype
    # (same as FSDP1's behavior to avoid extra communication).
    if not all_grads:
        warnings.warn(
            "clip_grad_norm_ called on this rank with no gradients -- "
            "returning the local norm in the default dtype "
            f"{total_norm.dtype}",
            stacklevel=2,
921
922
923
924
925
926
927
928
929
            "returning the local norm in the default dtype "
            f"{total_norm.dtype}",
            stacklevel=2,
        )
        return total_norm

    total_norm_dtype = functools.reduce(
        torch.promote_types,
        [g.dtype for g in all_grads],
hyper_parallel/platform/torch/clip_grad.py
12
13
14
15
16
17
18
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Compatibility entry point for the relocated gradient clipping utility."""
from hyper_parallel.core.utils.clip_grad import clip_grad_norm_

__all__: list[str] = ["clip_grad_norm_"]
hyper_parallel/platform/torch/platform.py
1858
1859
1860
1861
1862
1863
1864
1865
1866
        parameters, max_norm, norm_type=2.0,
        error_if_nonfinite=False, foreach=None,
    ):
        # pylint: disable=C0415
        from hyper_parallel.core.utils.clip_grad import (
            clip_grad_norm_ as _clip_grad_norm,
        )
        return _clip_grad_norm(
            parameters, max_norm, norm_type,