Diff Coverage

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

Source File Diff Coverage (%) Missing Lines
hyper_parallel/compile/pass_config.py 100%  
hyper_parallel/compile/passes/cpu_offload.py 92.4% 77-81,133,178-179,186-187,195-196,216-217,308,316,324,328,371,418,420,424,431,472,490,524,549,591,641,774,857,881-882
hyper_parallel/compile/passes/inductor_pass.py 80.0% 183
hyper_parallel/compile/passes/pipeline.py 100%  
hyper_parallel/compile/passes/cpu_offload.py
73
74
75
76
77
78
79
80
81
82
83
84
85

try:  # Register the upstream ``ao::`` custom ops on import.
    import torch._functorch._activation_offloading.offload_ops  # noqa: F401

    _AO_OFFLOAD = torch.ops.ao.offload.default
    _AO_RELOAD = torch.ops.ao.reload.default
    _AO_WAIT = torch.ops.ao.wait_tensor.default
    _AO_OPS = frozenset((_AO_OFFLOAD, _AO_RELOAD, _AO_WAIT))
    _AO_AVAILABLE = True
except (ImportError, AttributeError):
    _AO_OFFLOAD = _AO_RELOAD = _AO_WAIT = None
    _AO_OPS = frozenset()
    _AO_AVAILABLE = False
129
130
131
132
133
134
135
136
137

def _in_attention_scope(node: fx.Node, roots: frozenset) -> bool:
    """Whether a node was produced inside one of the attention roots."""
    if not roots:
        return False
    for fqn in _stack_fqns(node):
        if fqn.rsplit(".", 1)[-1] == "self_attn":
            return True
        if any(fqn == root or fqn.startswith(f"{root}.") for root in roots):
174
175
176
177
178
179
180
181
182
183
    try:
        if isinstance(value, torch.SymInt):
            value = value.node.hint
        return int(value)
    except Exception:  # pylint: disable=broad-exception-caught
        return None


def _tensor_bytes(value: torch.Tensor) -> Optional[int]:
    """Return a hinted byte count, or ``None`` for unsupported values."""
182
183
184
185
186
187
188
189
190
191
def _tensor_bytes(value: torch.Tensor) -> Optional[int]:
    """Return a hinted byte count, or ``None`` for unsupported values."""
    try:
        nbytes = value.nelement() * value.element_size()
    except Exception:  # pylint: disable=broad-exception-caught
        return None
    return _hint_to_int(nbytes)


def _is_contiguous(value: torch.Tensor) -> bool:
191
192
193
194
195
196
197
198
199
200
def _is_contiguous(value: torch.Tensor) -> bool:
    """Check contiguity without tripping over symbolic fake tensors."""
    try:
        return bool(value.is_contiguous())
    except Exception:  # pylint: disable=broad-exception-caught
        return False


def _storage_key(value: torch.Tensor) -> Optional[int]:
    """Return a stable identity for the tensor's storage, or ``None`` if unknown.
212
213
214
215
216
217
218
219
220
    callers skip the candidate.
    """
    try:
        storage = value.untyped_storage()
    except Exception:  # pylint: disable=broad-exception-caught
        return None
    cdata = getattr(storage, "_cdata", None)
    return int(cdata) if cdata else None

304
305
306
307
308
309
310
311
312
    unresolved = False
    position = 0
    for argument in schema.arguments:
        if argument.kwarg_only:
            index = None
        else:
            index = position
            position += 1
        alias_info = argument.alias_info
312
313
314
315
316
317
318
319
320
        alias_info = argument.alias_info
        if alias_info is None or not alias_info.is_write:
            continue
        if argument.name in node.kwargs:
            value = node.kwargs[argument.name]
        elif index is not None and index < len(node.args):
            value = node.args[index]
        else:
            unresolved = True
320
321
322
323
324
325
326
327
328
329
330
331
332
            unresolved = True
            continue
        written = _iter_nodes(value)
        if not written:
            continue
        resolved = True
        written_keys = _storage_keys(written)
        if not written_keys:
            unresolved = True
        keys.extend(written_keys)
    if unresolved or not resolved:
        return _tensor_argument_keys(node)
    return keys
367
368
369
370
371
372
373
374
    """Whether a storage group has no external, mutating, or output sharer."""
    if any(node.op != "call_function" for node in nodes):
        return False
    if any(_is_mutable(node) for node in nodes):
        return False
    if any(user.op == "output" for node in nodes for user in node.users):
        return False
    return True
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
    group owner is eligible, and ``unsafe`` groups (external or mutating
    aliases) are left on device.
    """
    if node.op != "call_function":
        return False
    if node.target in _AO_OPS:
        return False
    if is_view(node):
        return False
    if _is_collective_or_wait(node):
        return False
    value = node.meta.get("val")
    if not isinstance(value, torch.Tensor) or _is_dtensor(value):
        return False
    if value.dim() < 2:
427
428
429
430
431
432
433
434
435
        return False
    if value.dim() < 2:
        return False
    if not _is_contiguous(value):
        return False
    nbytes = _tensor_bytes(value)
    if nbytes is None or nbytes < _MIN_OFFLOAD_BYTES:
        return False
    # Offload one storage at a time. Only the group owner (the node that
468
469
470
471
472
473
474
475
476
    def collect(current: fx.Node) -> None:
        """Walk alias users and record backward readers."""
        for user in current.users:
            if user.op != "call_function":
                continue
            if _is_backward(user):
                backward_users.setdefault(current, []).append(user)
            elif _is_alias(user, current) and user not in visited:
                visited.add(user)
486
487
488
489
490
491
492
493
494
    def is_read_in_backward(current: fx.Node) -> bool:
        """Whether a view subtree has any backward reader (memoized)."""
        cached = memo.get(current)
        if cached is not None:
            return cached
        needed_here = bool(backward_users.get(current))
        for child in children.get(current, ()):
            if is_read_in_backward(child):
                needed_here = True
520
521
522
523
524
525
526
527
528

    @property
    def all_bwd_users(self) -> List[fx.Node]:
        """Every backward consumer, direct or reached through a view."""
        return self.direct_bwd_users + [user for _, user in self.view_bwd_redirects]


def _collect_offload_info(
    node: fx.Node, order: Dict[fx.Node, int]
545
546
547
548
549
550
551
552
553
                last_forward_use = user
                last_index = user_index

    if last_forward_use is node:
        last_use_arg: fx.Node = node
    elif isinstance(last_forward_use.meta.get("val"), torch.Tensor):
        last_use_arg = last_forward_use
    else:
        # The forward ``ao.wait_tensor`` frees the producer's storage, so it must
587
588
589
590
591
592
593
594
        if info is None:
            continue
        nbytes = _tensor_bytes(node.meta["val"])
        if nbytes is None:
            continue
        candidates.append((node, nbytes, info))
    return candidates

637
638
639
640
641
642
643
644
645
    offload.meta.update(src_meta)
    offload.meta["val"] = value.to(torch.device("cpu"))

    if info.last_forward_use is node:
        anchor = offload
    else:
        anchor = info.last_forward_use

    with graph.inserting_after(anchor):
770
771
772
773
774
775
776
777
778
    """Move one reload earlier by ``n_regions`` regions when possible."""
    root = _attention_root_of(reload_node, roots)
    positions = anchor_positions.get(root) if root is not None else None
    if not positions:
        return False
    reload_index = order[reload_node]
    # Latest occurrence of this root at or before the reload is its own
    # region when a module is reused across blocks; otherwise fall back to
    # the first occurrence (reload inserted before the region's first node).
853
854
855
856
857
858
859
860
861
                no ``self_attn`` module, matching the eager attention-swap
                contract.
        """
        if pass_config.activation_swap == "none":
            return graph_module
        if not _AO_AVAILABLE:
            raise RuntimeError(
                "activation_swap='attention' requires torch's "
                "torch._functorch._activation_offloading.offload_ops (ao:: "
877
878
879
880
881
882
883
884
885
886
        budget_bytes = pass_config.offload_budget_gb * (1024**3)
        selected, total_bytes = _select_within_budget(candidates, budget_bytes)

        if not selected:
            _LOG.info("CPU offload: no attention activations selected")
            return graph_module

        wait_offloads: List[Tuple[_OffloadInfo, fx.Node]] = []
        for info in selected:
            wait_offload = _insert_forward_offload(graph_module, info)
hyper_parallel/compile/passes/inductor_pass.py
179
180
181
182
183
184
185
186
    stream and an event registry outside the compiled region, and rewriting
    them as fused kernels would break the host/device synchronization.
    """
    if node.op != "call_function":
        return False
    if node.target in _side_effectful_functions:
        return True
    return str(node.target).startswith("ao.")