Diff Coverage

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

Source File Diff Coverage (%) Missing Lines
hyper_parallel/core/utils/clip_grad.py 75.5% 56-59,114,117,147,224-226,241-242,245-246,250-251,254-257,264,266-267,277,280,295-296,302,326-327,330,339-340,344,346-347,351,354,359,362,366-367,394,400,420-422,428-430,437-443,447-448,578-579,583-584,590-592,632,649,655-656,670,806,816-817,821-822
hyper_parallel/platform/torch/clip_grad.py 100%  
hyper_parallel/platform/torch/platform.py 0.0% 1844
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
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]:
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
322
323
324
325
326
327
328
329
330
331
332
333
334
    * ``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)
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
            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
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.
416
417
418
419
420
421
422
423
424
425
426
    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)
424
425
426
427
428
429
430
431
432
433
                    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()

433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452


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.
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):
802
803
804
805
806
807
808
809
810
                    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
hyper_parallel/platform/torch/platform.py
1840
1841
1842
1843
1844
1845
1846
1847
1848
        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,