Diff Coverage

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

Source File Diff Coverage (%) Missing Lines
hyper_parallel/compile/compiler.py 100%  
hyper_parallel/compile/pass_config.py 80.0% 228
hyper_parallel/compile/passes/bucketing/bucketing_pass.py 85.2% 271-294
hyper_parallel/compile/passes/bucketing/planner.py 97.3% 109,297,336,338,356,381,384,387,406,463
hyper_parallel/compile/passes/overlap/overlap_grad_comm.py 73.3% 234-237
hyper_parallel/compile/passes/overlap/overlap_param_gather.py 100%  
hyper_parallel/compile/passes/overlap/utils.py 94.6% 79,113
hyper_parallel/compile/passes/parallel/fsdp_pass.py 92.2% 97,117,128,131,152
hyper_parallel/compile/passes/pipeline.py 60.0% 92,94
hyper_parallel/compile/pass_config.py
224
225
226
227
228
229
230
231
232
            value = getattr(self, name)
            if not isinstance(value, bool):
                raise ValueError(f"{name} must be a bool")
        if self.fsdp_bucket_modules is not None and not isinstance(self.fsdp_bucket_modules, (list, tuple)):
            raise ValueError("fsdp_bucket_modules must be None or a list/tuple of module groups")
        value = self.max_in_flight_gb
        if (
            isinstance(value, bool)
            or not isinstance(value, (int, float))
hyper_parallel/compile/passes/bucketing/bucketing_pass.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294

        Returns:
            Graph with fused AG/RS, or unchanged graph when disabled/unsupported.
        """
        if not pass_config.fsdp_bucketing or not pass_config.fsdp_enabled or not dist.is_initialized():
            return graph_module
        if dist.get_world_size() == 1:
            return graph_module
        if pass_config.dp_mode != "fsdp" or pass_config.pp_enabled or (pass_config.pp_degree or 1) != 1:
            _LOG.info("Skipped FSDP bucketing: requires dp_mode=fsdp and PP disabled")
            return graph_module
        modules = pass_config.fsdp_bucket_modules
        if modules is not None:
            modules = build_module_bucket_plans(kwargs["model"], module_bucket_plans=modules)
        names = [kwargs.get("fsdp_group_name", "fsdp")]
        if kwargs.get("expert_param_names"):
            names.append(kwargs.get("expert_fsdp_group_name", "fsdp_expert"))
        groups = {name: _resolve_process_group(name) for name in names}
        ranks = {name: tuple(dist.get_process_group_ranks(group)) for name, group in groups.items()}
        for apply in (apply_ag_buckets, apply_rs_buckets):
            plan = plan_fsdp_buckets(graph_module, group_ranks=ranks, module_bucket_plans=modules)
            for name, group in groups.items():
                report = {"group_ranks": {name: ranks[name]}, "group_digests": {name: plan.group_digest(name)}}
                reports = [None] * len(ranks[name])
                dist.all_gather_object(reports, report, group=group)
                validate_group_plans(dict(zip(ranks[name], reports)), name)
            apply(graph_module, plan)
        return graph_module
hyper_parallel/compile/passes/bucketing/planner.py
105
106
107
108
109
110
111
112
113
    Raises:
        ValueError: Types, module paths or group ownership are invalid.
    """
    if not isinstance(model, nn.Module):
        raise ValueError("model must be a torch.nn.Module")
    if (not isinstance(block_module_types, tuple)
            or any(not isinstance(kind, type) or not issubclass(kind, nn.Module) for kind in block_module_types)):
        raise ValueError("block_module_types must be a tuple of torch.nn.Module subclasses")
    explicit = normalize_module_bucket_plans(module_bucket_plans)
293
294
295
296
297
298
299
300
301
    Raises:
        ValueError: Reports are missing ranks or disagree on membership/order/layout.
    """
    if not plans_by_rank:
        raise ValueError("No rank reports supplied")
    reports = {rank: plan.to_dict() if isinstance(plan, BucketPlan) else plan
               for rank, plan in plans_by_rank.items()}
    first = next(iter(reports.values()))
    expected = first["group_ranks"].get(group)
332
333
334
335
336
337
338
339
340
341
342
    if not isinstance(source, fx.Node) or not isinstance(group, str) or group not in groups:
        return "unknown_process_group"
    degree = node.args[-2]
    if not _static_int(degree) or degree <= 1 or degree != len(groups[group]):
        return "group_size_mismatch"
    if node.meta.get("comm_group", group) != group:
        return "group_metadata_mismatch"
    name, index = node.meta.get("param_name"), node.meta.get("state_idx")
    if not isinstance(name, str) or not name or not _static_int(index) or index < 0:
        return "missing_parameter_identity"
    return None
352
353
354
355
356
357
358
359
360
    dtype, device = spec.get("dtype"), spec.get("device")
    if not isinstance(dtype, torch.dtype) or dtype not in (torch.float16, torch.bfloat16, torch.float32, torch.float64):
        return "unsupported_dtype"
    if not isinstance(device, str):
        return "unknown_device"
    try:
        torch.device(device)
    except (RuntimeError, ValueError):
        return "unknown_device"
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
    if reason:
        return None, reason
    waits = list(node.users)
    if len(waits) != 1 or not is_op(waits[0], c10d.wait_tensor):
        return None, "unsupported_wait_users"
    consumers = _consumers(waits[0])
    if not consumers:
        return None, "no_consumer"
    if kind == "fsdp_reduce_scatter":
        if node.args[1] not in ("sum", "avg"):
            return None, "unsupported_reduce_op"
        if any(user.op != "output" for user in consumers):
            return None, "non_output_rs_consumer"
    if module_fqns is None:
        return None, "outside_module_plan"
402
403
404
405
406
407
408
409
410

def _membership_reason(members: list[Candidate], candidate: Candidate, policy: BucketPolicy) -> Optional[str]:
    """Return why compatibility, uniqueness or capacity rejects the candidate."""
    if members[-1].key != candidate.key:
        return "incompatible_key"
    combined = [*members, candidate]
    param_names = {m.param_name for m in combined}
    state_indices = {m.state_idx for m in combined}
    if len(param_names) != len(combined) or len(state_indices) != len(combined):
459
460
461
462
463
464
465
466
467
        if is_collective(node) and node not in owned:
            return "intervening_collective"
        if node.op not in ("placeholder", "get_attr", "output") and (
                bool(node.meta.get("autograd_backward", False)) != candidate.backward):
            return "phase_boundary"
    return None


def _split_reason(members: list[Candidate], candidate: Candidate, nodes: list, order: dict,
hyper_parallel/compile/passes/overlap/overlap_grad_comm.py
230
231
232
233
234
235
236
237
238
239
240
241
    graph.lint()
    if normalized or advanced or sunk:
        graph_module.recompile()
    if _LOG.isEnabledFor(logging.DEBUG):
        after = {node: index for index, node in enumerate(graph.nodes)}
        for match in matches:
            launch, wait = match.launch, match.realization[0]
            _LOG.debug("FSDP gradient overlap completed: bucket=%s launch=%s index_before=%s index_after=%s "
                       "wait=%s wait_before=%s wait_after=%s",
                       launch.meta.get("fsdp_bucket_id"), launch.name, before.get(launch), after[launch],
                       wait.name, before.get(wait), after[wait])
    graph_module.meta["fsdp_grad_overlap"] = {
hyper_parallel/compile/passes/overlap/utils.py
75
76
77
78
79
80
81
82
83
        launch: Reduce-scatter launch whose input preparation is inspected.
    """
    inputs = launch.all_input_nodes
    if len(inputs) != 1:
        return []
    source = inputs[0]
    if set(source.users) != {launch} or source.meta.get("autograd_backward") is False:
        return []
    owner = source.meta.get("fsdp_reduce_scatter_prepare")
109
110
111
112
113
114
115
116
117
        if node.meta.get("fsdp_bucket_prepare") != bucket_id:
            continue
        schema = getattr(node.target, "_schema", None)
        if schema is None or schema.name not in ("aten::reshape", "aten::cat", "aten::contiguous"):
            return []
        if node not in preparation:
            preparation.add(node)
            stack.extend(node.all_input_nodes)
    if any(set(node.users) - preparation - {launch} for node in preparation):
hyper_parallel/compile/passes/parallel/fsdp_pass.py
 93
 94
 95
 96
 97
 98
 99
100
101
    node = wait
    while node.op == "call_function" and str(node.target).startswith("_c10d_functional."):
        node.meta.update(param_name=param_name, state_idx=state_idx)
        if not node.args or not isinstance(node.args[0], fx.Node):
            break
        node = node.args[0]


def _annotate_rs_spec(node: fx.Node, degree: int) -> None:
113
114
115
116
117
118
119
120
121
        return
    shape = spec["shape"]
    static = shape and all(isinstance(dim, int) and not isinstance(dim, bool) and dim > 0 for dim in shape)
    if not static or shape[0] % degree:
        return
    node.meta["fsdp_bucket_spec"] = {
        "input_shape": tuple(shape), "output_shape": (shape[0] // degree, *shape[1:]),
        "dtype": spec["dtype"], "device": spec["device"],
    }
124
125
126
127
128
129
130
131
132
133
134
135
def _annotate_ag_spec(node: fx.Node, degree: int, params: dict) -> None:
    """Attach shard/full shapes of one all-gather node from its parameter."""
    param = params.get(node.meta.get("param_name"))
    if param is None:
        return
    shape = tuple(param.shape)
    if not shape or any(not isinstance(dim, int) or isinstance(dim, bool) or dim <= 0 for dim in shape):
        return
    node.meta["fsdp_bucket_spec"] = {
        "input_shape": shape, "output_shape": (shape[0] * degree, *shape[1:]),
        "dtype": param.dtype, "device": str(param.device),
    }
148
149
150
151
152
153
154
155
156
        if kind not in ("fsdp_all_gather", "fsdp_all_gather_backward", "fsdp_reduce_scatter"):
            continue
        degree = node.meta.get("fsdp_degree")
        if (not isinstance(degree, int) or isinstance(degree, bool)) or degree <= 1:
            continue
        if kind == "fsdp_reduce_scatter":
            _annotate_rs_spec(node, degree)
        else:
            _annotate_ag_spec(node, degree, params)
hyper_parallel/compile/passes/pipeline.py
88
89
90
91
92
93
94
95
96
97
98
        # 2. Execution layer: Parallel dimension partitioning
        if getattr(self.config, "fsdp_enabled", False):
            self.passes.append(FSDPPass(parallel_plan=self.parallel_plan))
        if self.config.fsdp_bucketing:
            self.passes.append(FsdpBucketingPass())
        if getattr(self.config, "pp_enabled", False):
            self.passes.append(PpPass(parallel_plan=self.parallel_plan))

        # 3. Communication-compute overlap optimization
        if getattr(self.config, "enable_overlap", False):
            self.passes.append(AutoOverlapPass())