Diff Coverage

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

Source File Diff Coverage (%) Missing Lines
hyper_parallel/auto_parallel/dense_tuner/__init__.py 100%  
hyper_parallel/auto_parallel/dense_tuner/candidate_generator.py 90.7% 258,260,295,367-371,373,415,418,424,498,501-502,514,517-518,586,610,636
hyper_parallel/auto_parallel/dense_tuner/config.py 98.8% 423,470
hyper_parallel/auto_parallel/dense_tuner/estimator.py 97.2% 73,75,329,393,468
hyper_parallel/auto_parallel/dense_tuner/pipeline_balancer.py 89.0% 111,114-116,153,155,278,334,391-392,399,483,577-579,582,604-605,607
hyper_parallel/auto_parallel/dense_tuner/result.py 100%  
hyper_parallel/auto_parallel/dense_tuner/strategy.py 98.9% 156
hyper_parallel/auto_parallel/dense_tuner/tuner.py 91.3% 96,215-216,246,250,255-257,286-287
hyper_parallel/auto_parallel/dense_tuner/visualizer.py 82.7% 63-65,107,114,134,164-165,167,184,214-215,217,262,288-289,291,322-324,340-341,343,362-363,370,373,375-391,478-479,481
hyper_parallel/auto_parallel/dense_tuner/candidate_generator.py
254
255
256
257
258
259
260
261
262
263
264
        return False
    if search_space.pp_range and strategy.pp_degree not in search_space.pp_range:
        return False
    if search_space.cp_range and strategy.cp_degree not in search_space.cp_range:
        return False
    if search_space.micro_batch_num_range and strategy.micro_batch_num not in search_space.micro_batch_num_range:
        return False
    return True


def generate_candidates_sapp(
291
292
293
294
295
296
297
298
299
    _patch_evaluator_set_config(parallelize)
    parallelize.instance.enable_debug = False
    ccfg = parallelize.config.ccfg
    if "vp" not in ccfg.__dict__ or ccfg.__dict__.get("vp", 0) < 1:
        ccfg.vp = 1
    parallelize.bound_space()
    space = parallelize.generate_search_space(
        folder="", threads_num=None
    )
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
    )
    if not os.path.isdir(nd_output_dir):
        return collected

    for pattern in ["results.pdf", "debug_*.csv"]:
        for src in sorted(glob.glob(os.path.join(nd_output_dir, pattern))):
            dst = os.path.join(output_dir, os.path.basename(src))
            shutil.copy2(src, dst)
            collected.append(dst)

    return collected


def generate_candidates(
    hardware: HardwareConfig,
411
412
413
414
415
416
417
418
419
420
421
422
                continue
            remaining = n // (tp * pp)
            for cp in cp_values:
                if remaining % cp != 0:
                    continue
                dp = remaining // cp
                if dp < 1:
                    continue
                if dp_values and dp not in dp_values:
                    continue

                mbn_values = _enumerate_mbn_values(gbs, dp, pp, search_space)
420
421
422
423
424
425
426
427
428
                    continue

                mbn_values = _enumerate_mbn_values(gbs, dp, pp, search_space)
                if not mbn_values and not search_space.micro_batch_num_range:
                    mbn_values = [1]

                for mbn in mbn_values:
                    strategy = ParallelStrategy(
                        dp_degree=dp,
494
495
496
497
498
499
500
501
502
503
504
505
506
            filtered.append(s)
            continue

        if s.tp_degree > kv_heads:
            s.mark_infeasible(
                f"tp_degree={s.tp_degree} > num_kv_heads={kv_heads}"
            )
            filtered.append(s)
            continue

        if s.pp_degree > 1 and model.num_layers % s.pp_degree != 0:
            s.mark_infeasible(
                f"num_layers={model.num_layers} not divisible by "
510
511
512
513
514
515
516
517
518
519
520
521
522
            continue

        total = s.dp_degree * s.tp_degree * s.pp_degree * s.cp_degree
        if total != n:
            s.mark_infeasible(
                f"dp*tp*pp*cp={total} != num_devices={n}"
            )
            filtered.append(s)
            continue

        if s.pp_degree > 1 and s.micro_batch_num < s.pp_degree:
            s.mark_infeasible(
                f"micro_batch_num={s.micro_batch_num} < pp_degree={s.pp_degree}"
582
583
584
585
586
587
588
589
590
        return strategies

    for s in strategies:
        if not s.is_feasible:
            continue
        if s.memory_cost.total > constraints.memory_limit_mb:
            s.mark_infeasible(
                f"memory={s.memory_cost.total:.0f}MB > "
                f"limit={constraints.memory_limit_mb:.0f}MB"
606
607
608
609
610
611
612
613
614
        Filtered list of strategies.
    """
    for s in strategies:
        if not s.is_feasible:
            continue
        if s.dp_degree < constraints.min_dp_degree:
            s.mark_infeasible(
                f"dp_degree={s.dp_degree} < min_dp_degree={constraints.min_dp_degree}"
            )
632
633
634
635
636
637
638
639
640
        return strategies

    for s in strategies:
        if not s.is_feasible:
            continue
        if s.performance_cost.total > constraints.max_step_time_ms:
            s.mark_infeasible(
                f"step_time={s.performance_cost.total:.1f}ms > "
                f"max_step_time={constraints.max_step_time_ms:.1f}ms"
hyper_parallel/auto_parallel/dense_tuner/config.py
419
420
421
422
423
424
425
426
427
            extra["dimensions"] = dimensions
        par = Parallelize("hyper_v2", yaml_path, machine, **extra)
        ccfg = par.config.ccfg
        if not hasattr(ccfg, "vp") or ccfg.vp == 0:
            ccfg.vp = 1
        par._yaml_path = yaml_path  # pylint: disable=protected-access
        return par

    def to_sapp_yaml(self) -> str:
466
467
468
469
470
471
472
473
474
        mem_gb = int(h.memory_per_device_mb / 1024)

        ac_mode = "full"
        if hasattr(self, "_recompute_mode"):
            ac_mode = self._recompute_mode

        doc = {
            "model": {
                "name": m.model_name,
hyper_parallel/auto_parallel/dense_tuner/estimator.py
69
70
71
72
73
74
75
76
77
78
79
    new_pp = strategy.pp_degree
    new_vpp = strategy.vpp_degree
    n_lay = ccfg.n_lay
    if getattr(ccfg, "emb_out_in_offset", False):
        n_lay += 2
    if getattr(ccfg, "is_mtp_in_offset", False):
        n_lay += getattr(ccfg, "n_mtp", 0)
    adapter = BalancingAdapter(
        layers=n_lay,
        offset=ccfg.offset,
        recompute=ccfg.full_rec,
325
326
327
328
329
330
331
332
        )

        _apply_strategy_to_ccfg(ccfg, strategy)
        device_type = hardware.to_sapp_machine().device
        sapp_score = sapp_estimate_performance(ccfg, device_type=device_type)

    pp = strategy.pp_degree
    mbn = strategy.micro_batch_num
389
390
391
392
393
394
395
396
397
    total_flops = n_lay * flops_per_layer / pp

    tflops = hardware.tflops_per_device
    if tflops <= 0:
        tflops = 300.0
    compute_ms = total_flops / (tflops * 1e9) * 1000

    comm_ms = 0.0
    if tp > 1:
464
465
466
467
468
469
470
471
472
    stages = []
    start = 0
    for i in range(pp):
        if offset and i < len(offset):
            num_layers = layers_per_stage + int(offset[i])
        else:
            num_layers = layers_per_stage
        end = start + num_layers
        stage_mem = strategy.memory_cost.total / pp
hyper_parallel/auto_parallel/dense_tuner/pipeline_balancer.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
        ValueError: If the JSON file is missing ``layers_description``.
    """
    # pylint: disable=import-outside-toplevel
    if profiling_json_path:
        from hyper_parallel.auto_parallel.sapp_ppb.utils.layer import (
            generate_layers_list,
        )
        folder = os.path.dirname(os.path.abspath(profiling_json_path))
        base = os.path.splitext(os.path.basename(profiling_json_path))[0]
        return generate_layers_list(folder, base)

    from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.estimate_v2 import (
        EvaluatorV2,
    )
149
150
151
152
153
154
155
156
157
158
159
    model_name = getattr(ccfg, "model_name", "dense-llm")

    layer_list = ppb_input.get("layers_description", [])
    if not layer_list:
        layer_list = ppb_input.get("layers_description_new", [])
    if not layer_list:
        return layers

    for layer_dict in layer_list:
        ltype_str = layer_dict.get("type", "BODY")
        ltype = {
274
275
276
277
278
279
280
281
282
    new_stage_info = []
    for i in range(min(n_stages, len(mem_act_flat))):
        n_lay = strategy.extra.get("layers_per_stage", strategy.extra.get("num_layers", 0) // n_stages)
        if isinstance(n_lay, list) and i < len(n_lay):
            nl = n_lay[i]
        else:
            nl = strategy.memory_cost.total / n_stages if n_stages > 0 else 0
        end = start + int(nl) if isinstance(nl, (int, float)) else start
        new_stage_info.append(StageInfo(
330
331
332
333
334
335
336
337
338
    stage_recompute = [0] * num_stages
    for layer_name in body_names:
        rec_map = variables.get(layer_name)
        if rec_map is None:
            continue
        for rec in Recompute.TYPE:
            counts = rec_map[rec]
            for i in range(num_interleave):
                for s in range(num_stages):
387
388
389
390
391
392
393
394
395
396
            number_of_micro_batch=num_micro_batch,
            max_memory=max_memory,
            layers=layers,
        )
    except Exception:
        pipeline = _solve_pipeline(
            model_name=model_name,
            num_stages=num_stages,
            num_micro_batch=num_micro_batch,
            max_memory=max_memory,
395
396
397
398
399
400
401
402
403
            num_micro_batch=num_micro_batch,
            max_memory=max_memory,
            layers=layers,
        )
        best_dist = pipeline.get_result()

    pipeline = _solve_pipeline(
        model_name=model_name,
        num_stages=num_stages,
479
480
481
482
483
484
485
486
487
        strategy, ccfg, profiling_json_path=profiling_json_path,
    )

    if not layers:
        raise RuntimeError(
            f"PPB produced no layers for strategy {strategy.key}"
        )

    max_memory = int(memory_limit_mb) if memory_limit_mb else int(strategy.memory_cost.total)
573
574
575
576
577
578
579
580
581
582
583
584
585
    _apply_strategy_to_ccfg(ccfg, strategy)

    try:
        layers = _build_layers_from_strategy(strategy, ccfg)
    except Exception as exc:
        logger.warning("PPB layer build failed for %s plot: %s", strategy.key, exc)
        return None

    if not layers:
        return None

    max_memory = int(memory_limit_mb) if memory_limit_mb else int(strategy.memory_cost.total)
    vpp = strategy.vpp_degree if strategy.vpp_degree > 0 else 1
600
601
602
603
604
605
606
607
        plot_path = os.path.join(output_dir, f"ppb_pipeline_{strategy.key}.svg")
        pipeline.simulate(show=False, file_name=plot_path)
        if os.path.exists(plot_path):
            return plot_path
    except Exception as exc:
        logger.warning("PPB pipeline plot failed for %s: %s", strategy.key, exc)

    return None
hyper_parallel/auto_parallel/dense_tuner/strategy.py
152
153
154
155
156
157
158
159
160
            f"cp{self.cp_degree}",
            f"mbn{self.micro_batch_num}",
        ]
        if self.vpp_degree > 1:
            parts.append(f"vpp{self.vpp_degree}")
        if self.ep_degree > 1:
            parts.append(f"ep{self.ep_degree}")
        if self.op_degree > 1:
            parts.append(f"op{self.op_degree}")
hyper_parallel/auto_parallel/dense_tuner/tuner.py
 92
 93
 94
 95
 96
 97
 98
 99
100

    def _init_sapp(self) -> None:
        """Lazily initialise SAPP-ND Parallelize instance and extract ccfg."""
        if self._parallelize is not None:
            return
        self._parallelize = self.config.create_sapp_parallelize()
        self._ccfg = self._parallelize.config.ccfg

    def tune(self) -> TunerResult:
211
212
213
214
215
216
217
218
219
220
                try:
                    nd_plot_files.extend(
                        generate_nd_memory_plots(s, self._ccfg, out)
                    )
                except Exception as exc:
                    logger.warning("ND memory plot failed: %s", exc)

        chart_files = self._visualizer.visualize(
            result.top_k_strategies,
            result.all_candidates,
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
            constraints: Constraint configuration.
        """
        gbs = constraints.global_batch_size
        if gbs <= 0:
            gbs = self._infer_gbs(strategies, hardware)

        for s in strategies:
            if not s.is_feasible:
                continue
            try:
                s.memory_cost = estimate_memory(s, model, gbs, ccfg=self._ccfg)
                s.performance_cost = estimate_performance(s, model, hardware, gbs, ccfg=self._ccfg)
                s.stage_info = compute_stage_info(s, model, gbs)
            except Exception as exc:
                s.mark_not_supported(f"estimation failed: {exc}")
                logger.warning("Estimation failed for %s: %s", s.key, exc)

    def _infer_gbs(
        self,
        strategies: List[ParallelStrategy],
282
283
284
285
286
287
288
289
290
291
        yaml_path = getattr(self._parallelize, "_yaml_path", None)
        if yaml_path and os.path.exists(yaml_path):
            try:
                os.unlink(yaml_path)
            except OSError:
                pass

    @staticmethod
    def _build_ppb_result(strategy: ParallelStrategy) -> Optional[PPBResult]:
        """Extract PPB result from a strategy's extra metadata.
hyper_parallel/auto_parallel/dense_tuner/visualizer.py
59
60
61
62
63
64
65
66
67
68
                # pylint: disable=import-outside-toplevel
                import matplotlib
                matplotlib.use("Agg")
                self._matplotlib_available = True
            except ImportError:
                self._matplotlib_available = False
                logger.warning(
                    "matplotlib not available; falling back to text charts"
                )
        return self._matplotlib_available
103
104
105
106
107
108
109
110
111
        for src in (nd_plot_files or []):
            if src and os.path.exists(src):
                dst = os.path.join(out, os.path.basename(src))
                if os.path.abspath(src) != os.path.abspath(dst):
                    shutil.copy2(src, dst)
                files.append(dst)

        for src in (ppb_plot_files or []):
            if src and os.path.exists(src):
110
111
112
113
114
115
116
117
        for src in (ppb_plot_files or []):
            if src and os.path.exists(src):
                dst = os.path.join(out, os.path.basename(src))
                if os.path.abspath(src) != os.path.abspath(dst):
                    shutil.copy2(src, dst)
                files.append(dst)

        return [f for f in files if f]
130
131
132
133
134
135
136
137
138
        Returns:
            Output file path, or None if no strategies.
        """
        if not strategies:
            return None

        labels = [s.key for s in strategies]
        step_times = [s.performance_cost.total for s in strategies]
        path = os.path.join(out, "step_time_comparison.txt")
160
161
162
163
164
165
166
167
168
169
170
171
                img_path = os.path.join(out, "step_time_comparison.png")
                fig.savefig(img_path, dpi=150)
                plt.close(fig)
                return img_path
            except Exception as exc:
                logger.warning("matplotlib chart failed: %s", exc)

        return path

    def _memory_chart(
        self,
        strategies: List[ParallelStrategy],
180
181
182
183
184
185
186
187
188
        Returns:
            Output file path, or None if no strategies.
        """
        if not strategies:
            return None

        labels = [s.key for s in strategies]
        memories = [s.memory_cost.total for s in strategies]
        path = os.path.join(out, "memory_comparison.txt")
210
211
212
213
214
215
216
217
218
219
220
221
                img_path = os.path.join(out, "memory_comparison.png")
                fig.savefig(img_path, dpi=150)
                plt.close(fig)
                return img_path
            except Exception as exc:
                logger.warning("matplotlib chart failed: %s", exc)

        return path

    def _stage_distribution_chart(
        self,
        strategies: List[ParallelStrategy],
258
259
260
261
262
263
264
265
266

                n_strats = len(pp_strategies)
                fig, axes = plt.subplots(1, n_strats, figsize=(6 * n_strats, 4))
                if n_strats == 1:
                    axes = [axes]
                for ax, s in zip(axes, pp_strategies):
                    stages = s.stage_info
                    stage_ids = [st.stage_id for st in stages]
                    mems = [st.memory_mb for st in stages]
284
285
286
287
288
289
290
291
292
293
294
295
                img_path = os.path.join(out, "stage_distribution.png")
                fig.savefig(img_path, dpi=150)
                plt.close(fig)
                return img_path
            except Exception as exc:
                logger.warning("matplotlib chart failed: %s", exc)

        return path

    def _feasibility_chart(
        self,
        all_candidates: List[ParallelStrategy],
318
319
320
321
322
323
324
325
326
327
328
                f.write(f"Feasible:         {feasible} ({100 * feasible / total:.1f}%)\n")
                f.write(f"Infeasible:       {infeasible} ({100 * infeasible / total:.1f}%)\n")
                f.write(f"Not supported:    {not_supported} ({100 * not_supported / total:.1f}%)\n")
            else:
                f.write("Feasible:         0\n")
                f.write("Infeasible:       0\n")
                f.write("Not supported:    0\n")

        if self._check_matplotlib() and total > 0:
            try:
                # pylint: disable=import-outside-toplevel
336
337
338
339
340
341
342
343
344
345
346
347
                img_path = os.path.join(out, "feasibility_distribution.png")
                fig.savefig(img_path, dpi=150)
                plt.close(fig)
                return img_path
            except Exception as exc:
                logger.warning("matplotlib chart failed: %s", exc)

        return path

    def _filter_reasons_chart(
        self,
        all_candidates: List[ParallelStrategy],
358
359
360
361
362
363
364
365
366
367
        """
        reasons: Dict[str, int] = {}
        for s in all_candidates:
            if s.filter_reason:
                category = re.split(r"[=<>]", s.filter_reason)[0].strip()
                reasons[category] = reasons.get(category, 0) + 1

        path = os.path.join(out, "filter_reasons.txt")
        with open(path, "w", encoding="utf-8") as f:
            f.write("Filter Reasons Statistics\n")
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
        with open(path, "w", encoding="utf-8") as f:
            f.write("Filter Reasons Statistics\n")
            f.write("=" * 40 + "\n")
            for reason, count in sorted(reasons.items(), key=lambda x: -x[1]):
                f.write(f"  {reason}: {count}\n")

        if self._check_matplotlib() and reasons:
            try:
                # pylint: disable=import-outside-toplevel
                import matplotlib.pyplot as plt
                fig, ax = plt.subplots(figsize=(8, max(4, len(reasons) * 0.5)))
                sorted_reasons = sorted(reasons.items(), key=lambda x: -x[1])
                labels = [r[0][:40] for r in sorted_reasons]
                counts = [r[1] for r in sorted_reasons]
                ax.barh(range(len(labels)), counts, align="center")
                ax.set_yticks(range(len(labels)))
                ax.set_yticklabels(labels)
                ax.set_xlabel("Count")
                ax.set_title("Filter Reasons Distribution")
                fig.tight_layout()
                img_path = os.path.join(out, "filter_reasons.png")
                fig.savefig(img_path, dpi=150)
                plt.close(fig)
                return img_path
            except Exception as exc:
                logger.warning("matplotlib chart failed: %s", exc)

        return path

    def _dimension_trend_chart(
474
475
476
477
478
479
480
481
                img_path = os.path.join(out, "dimension_trends.png")
                fig.savefig(img_path, dpi=150)
                plt.close(fig)
                return img_path
            except Exception as exc:
                logger.warning("matplotlib chart failed: %s", exc)

        return path