Diff Coverage

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

Source File Diff Coverage (%) Missing Lines
hyper_parallel/core/dtensor/layout.py 100%  
hyper_parallel/core/fully_shard/api.py 50.0% 567
hyper_parallel/platform/platform.py 66.7% 1205,1220
hyper_parallel/platform/torch/dry_run.py 45.7% 102,104,108,131-132,160,182,202,207-208,214-215,219-226,231-234,238-239,244-245,264-269,273-286,290-291,295-299,303-307,311-318,320-323,327-337,344-346,349-354,359-362,369-370,372,374-385,390,392,394-398,405,410-412,415-417,419,422,424-434,436,438-444,446,448-449,451,453-454,456-457,459-464,466,468,470-472,474,476-485,487,489-491,493,495-496,503-504,506,513-516,521-525,531-536,538,540-544,549-561,563,570,575,585,593,595,597-600,602,610-616,638-646,651,653-659,661-664,666-668,675-676,690-694,699-701,706-715,719,723,727,740,742,773,832,870-873,910,979,983,1030-1032,1036,1057-1059,1093-1103,1105-1106,1148,1200
hyper_parallel/platform/torch/dtensor.py 38.9% 37-38,43,69,79,122,132-133,137-138,143-145,160-161,172-174,320-321,329-330
hyper_parallel/platform/torch/fully_shard/param_group.py 80.0% 510
hyper_parallel/platform/torch/memory_profiler.py 80.5% 61,70-73,78-82,91-93,98,103-104,113-115,130,136,138,167-168,177,199-201,204,245,278,297,361-365,369,392,412,475,492-493,504,540,543,557-558,565-566
hyper_parallel/platform/torch/platform.py 43.8% 742,746-747,1356,1361-1362,1372-1374
hyper_parallel/trainer/base.py 0.0% 176-181,183-184,187-189,191,193-195,239-243,245-252,254,744,1147,2053,2056,2061-2062,2064,2067-2070
hyper_parallel/trainer/callbacks/base.py 66.0% 621,626,630,640,652,660,665,675,678-679,682-683,688,692-693,707,711,713,716,719,740-745,752-754,759,762-763,771-772
hyper_parallel/trainer/config.py 100%  
hyper_parallel/core/fully_shard/api.py
563
564
565
566
567
568
569
570
571
            device_type == "cpu"
            and platform.platform_type == PlatformType.PYTORCH
            and str(platform.get_backend()) == "fake"
    ):
        return platform.get_dry_run_device("cpu", 0)
    if device_type not in ("npu", "cuda"):
        raise AssertionError(
            f"hyper_parallel.fully_shard support device in [torch.npu, torch.cuda], "
            f"but got '{device_type}'"
hyper_parallel/platform/platform.py
1201
1202
1203
1204
1205
1206
1207
1208
1209
        Raises:
            NotImplementedError: When the selected backend has no fake process
                group implementation.
        """
        raise NotImplementedError(
            "Dry-run process groups are not supported by this platform"
        )

    @staticmethod
1216
1217
1218
1219
1220
1221
1222
1223
1224

        Raises:
            NotImplementedError: When the platform has no fake device support.
        """
        raise NotImplementedError(
            "Dry-run fake devices are not supported by this platform"
        )

    @staticmethod
hyper_parallel/platform/torch/dry_run.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
        rank = int(os.environ["RANK"])
        world_size = int(os.environ["WORLD_SIZE"])
        local_rank = int(os.environ["LOCAL_RANK"])
        if world_size < 1:
            raise ValueError(f"WORLD_SIZE must be >= 1, got {world_size}")
        if rank < 0 or rank >= world_size:
            raise ValueError(
                f"RANK must be in [0, {world_size}), got {rank}"
            )
        if local_rank < 0:
            raise ValueError(f"LOCAL_RANK must be >= 0, got {local_rank}")
        return cls(rank=rank, world_size=world_size, local_rank=local_rank)


def _report_limitations(metadata: Optional[Dict[str, Any]] = None) -> list[str]:
127
128
129
130
131
132
133
134
135
136
def _category_name(category: Any) -> str:
    """Return a stable JSON key for a MemTracker category."""
    if isinstance(category, str):
        return category
    value = getattr(category, "value", None)
    return str(value if value is not None else category)


def _normalize_snapshot(snapshot: Dict[Any, Dict[Any, int]]) -> Dict[str, Dict[str, int]]:
    """Convert device/category objects in a MemTracker snapshot to JSON keys."""
156
157
158
159
160
161
162
163
164
    """Aggregate category bytes for devices matching ``device_type``."""
    breakdown: Dict[str, int] = {}
    for device, categories in snapshot.items():
        if device.split(":", maxsplit=1)[0] != device_type:
            continue
        for category, value in categories.items():
            if category == "Total":
                continue
            breakdown[category] = breakdown.get(category, 0) + int(value)
178
179
180
181
182
183
184
185
186
def _serialize_module(module_stats: Any, device_type: str) -> Dict[str, Any]:
    """Serialize one MemTracker module-stat object without private enum types."""
    snapshots = {}
    for state, snapshot_list in getattr(module_stats, "snapshots", {}).items():
        snapshots[_category_name(state)] = [
            _normalize_snapshot(snapshot)
            for snapshot in snapshot_list
        ]
    return {
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
    """Capture and intern operator allocation stacks for one trace mode."""

    def __init__(self) -> None:
        """Initialize an empty cache keyed by complete filtered frame tuples."""
        self._cache: Dict[tuple[tuple[str, int, str], ...], Dict[str, Any]] = {}

    @staticmethod
    def _is_internal_frame(filename: str, function_name: str) -> bool:
        """Return whether a frame was excluded by the previous stack collector."""
        normalized_path = filename.replace("\\", "/")
        if normalized_path == __file__ and function_name in (
                "__torch_dispatch__",
                "capture",
                "_record_storage_resize",
                "resize_",
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
                "capture",
                "_record_storage_resize",
                "resize_",
        ):
            return True
        return normalized_path.endswith(("/torch/_compile.py", "/torch/_dynamo/eval_frame.py"))

    def capture(self) -> Dict[str, Any]:
        """Return cached stack text and leaf location for the current caller."""
        frame = sys._getframe()  # pylint: disable=W0212
        frames = []
        while frame is not None and len(frames) < 64:
            code = frame.f_code
            frames.append((code.co_filename, frame.f_lineno, code.co_name))
            frame = frame.f_back
        frames.reverse()
        stack_key = tuple(
            frame_info
            for frame_info in frames
            if not self._is_internal_frame(frame_info[0], frame_info[2])
        )
        cached = self._cache.get(stack_key)
        if cached is not None:
            return cached
        python_stack = "|".join(
            f"File:{filename};Line:{line_num};Function:{function_name}"
            for filename, line_num, function_name in stack_key
        )
        leaf_frame = stack_key[-1] if stack_key else ("", 0, "")
        captured = {
            "python_stack": python_stack,
            "file_name": leaf_frame[0],
            "line_num": leaf_frame[1],
        }
        self._cache[stack_key] = captured
        return captured


def _create_indexed_mem_tracker(mem_tracker_type: Any) -> Any:
    """Create a MemTracker whose module peak lookup follows active FQNs."""
260
261
262
263
264
265
266
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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
            self._next_module_registration_order = 0

        def _remove_from_fqn(self, module_fqn: str, module: Any) -> None:
            """Remove a module from one FQN bucket and prune an empty bucket."""
            bucket = self._module_stats_by_fqn.get(module_fqn)
            if bucket is None:
                return
            bucket.pop(module, None)
            if not bucket:
                self._module_stats_by_fqn.pop(module_fqn, None)

        def _rebind_module_stats(self, module: Any) -> None:
            """Bind a module's latest stats FQN while preserving first-seen order."""
            module_stats = self.memory_tracking.get(module)
            if module_stats is None:
                return
            new_fqn = str(module_stats.mod_fqn)
            old_fqn = self._module_fqn_by_module.get(module)
            if old_fqn == new_fqn:
                return
            if old_fqn is not None:
                self._remove_from_fqn(old_fqn, module)
            if module not in self._module_registration_order:
                self._module_registration_order[module] = self._next_module_registration_order
                self._next_module_registration_order += 1
            self._module_stats_by_fqn.setdefault(new_fqn, {})[module] = module_stats
            self._module_fqn_by_module[module] = new_fqn

        def _pre_fw_hook(self, module: Any, inputs: Any) -> None:
            """Run the original hook, then index its final module FQN."""
            super()._pre_fw_hook(module, inputs)
            self._rebind_module_stats(module)

        def reset_mod_stats(self) -> None:
            """Clear module statistics and all indexes derived from them."""
            super().reset_mod_stats()
            self._module_stats_by_fqn.clear()
            self._module_fqn_by_module.clear()
            self._module_registration_order.clear()
            self._next_module_registration_order = 0

        def _update_peak_stats(self, peak_state: Any) -> None:
            """Update active module peaks in registration order and global peak."""
            active_modules = {}
            for module_fqn in self._mod_tracker.parents:
                for module, module_stats in self._module_stats_by_fqn.get(str(module_fqn), {}).items():
                    active_modules[module] = module_stats
            ordered_modules = sorted(
                active_modules.items(),
                key=lambda item: self._module_registration_order[item[0]],
            )
            current_snapshot = self._curr_mem_snap
            for _, module_stats in ordered_modules:
                if peak_state not in module_stats.snapshots:
                    continue
                for device, device_snapshot in current_snapshot.items():
                    if module_stats.local_peak.get(device, 0) < device_snapshot["Total"]:
                        module_stats.local_peak[device] = device_snapshot["Total"]
                        module_stats.snapshots[peak_state][-1][device] = copy.deepcopy(device_snapshot)

            for device, device_snapshot in current_snapshot.items():
                if self._peak_mem.get(device, 0) < device_snapshot["Total"]:
                    self._peak_mem[device] = device_snapshot["Total"]
                    self._peak_mem_snap[device] = copy.deepcopy(device_snapshot)

        def get_device_memory_totals(self, device_name: str) -> tuple[int, int]:
            """Read current and peak totals for one device without copying snapshots."""
            current_total = 0
            for device, device_snapshot in self._curr_mem_snap.items():
                if str(device) == device_name:
                    current_total = int(device_snapshot.get("Total", 0))
                    break
            recorded_peak_total = 0
            for device, device_snapshot in self._peak_mem_snap.items():
                if str(device) == device_name:
                    recorded_peak_total = int(device_snapshot.get("Total", 0))
                    break
            return current_total, max(current_total, recorded_peak_total)

    return _IndexedMemTracker()

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


def _operator_phase(tracker: Any) -> str:
    """Resolve the current training phase from MemTracker state."""
    if getattr(tracker, "_in_opt", False):
        return "optimizer"
    from hyper_parallel.core.activation_checkpoint.recompute_state import (  # pylint: disable=C0415
        is_recomputing,
    )
    if is_recomputing():
        return "recompute"
    module_tracker = getattr(tracker, "_mod_tracker", None)
    if getattr(module_tracker, "is_bw", False):
        return "backward"
    return "forward"


def _active_module_fqn(tracker: Any) -> str:
    """Return the deepest active module as optional operator context."""
    module_tracker = getattr(tracker, "_mod_tracker", None)
    parents = getattr(module_tracker, "parents", ())
    candidates = [str(parent) for parent in parents if str(parent) not in ("", "Global")]
    return max(candidates, key=lambda fqn: (fqn.count("."), len(fqn)), default="")


def _create_operator_trace_mode(tracker: Any, device_type: str) -> Any:
    """Create a lazy TorchDispatchMode that records logical memory blocks."""
365
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
396
397
398
399
400
401
402
def _create_operator_trace_mode(tracker: Any, device_type: str) -> Any:
    """Create a lazy TorchDispatchMode that records logical memory blocks."""
    import torch  # pylint: disable=C0415
    from torch.distributed._tools.mem_tracker import get_untyped_storages  # pylint: disable=C0415
    from torch.utils._python_dispatch import TorchDispatchMode  # pylint: disable=C0415
    from torch.utils._pytree import tree_flatten  # pylint: disable=C0415

    def _storage_map(values: Any) -> Dict[int, Dict[str, Any]]:
        """Return storage identifiers, logical devices, and byte sizes."""
        flat_values, _ = tree_flatten(values)
        storages = {}
        for value in flat_values:
            if not isinstance(value, torch.Tensor):
                continue
            try:
                tensor_storages = get_untyped_storages(value)
            except (RuntimeError, TypeError):
                continue
            for storage in tensor_storages:
                storage_key = int(getattr(storage, "_cdata", id(storage)))
                storages[storage_key] = {
                    "device": str(value.device),
                    "size": int(storage.size()),
                    "storage": storage,
                }
        return storages

    def _lookup_tracked_storage(storage: Any) -> Optional[Dict[str, Any]]:
        """Read MemTracker metadata for one storage without scanning all entries."""
        entry = tracker._WINFO.get(storage)
        if entry is None:
            return None
        winfo, storage_ref = entry
        return {
            "device": str(winfo.device),
            "size": int(winfo.mem_consumed),
            "type": _category_name(winfo.reftype),
            "storage_ref": storage_ref,
401
402
403
404
405
406
407
408
409
410
411
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
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
            "type": _category_name(winfo.reftype),
            "storage_ref": storage_ref,
        }

    def _tracked_output_info(
            storage_keys: set[int],
            output_storages: Dict[int, Dict[str, Any]],
    ) -> Dict[int, Dict[str, Any]]:
        """Read MemTracker metadata only for selected operator outputs."""
        tracked = {}
        for storage_key in storage_keys:
            tracked_storage = _lookup_tracked_storage(
                output_storages[storage_key]["storage"]
            )
            if tracked_storage is not None:
                tracked[storage_key] = tracked_storage
        return tracked

    class _OperatorTraceMode(TorchDispatchMode):
        """Track producer, consumer, and lifetime data for fake storages."""

        def __init__(self) -> None:
            """Initialize ordered logical task and storage state."""
            super().__init__()
            self.memory_blocks: list[Dict[str, Any]] = []
            self._active_blocks: Dict[int, Dict[str, Any]] = {}
            self._active_storage_refs: Dict[int, Any] = {}
            self._active_storage_sizes: Dict[int, int] = {}
            self._released_storage_keys: set[int] = set()
            self._next_task_index = 0
            self._next_lifecycle_event_index = 0
            self._next_virtual_address = _VIRTUAL_ADDRESS_BASE
            self._previous_storage_resize = None
            self._stack_cache = _PythonStackCache()

        def __enter__(self) -> Any:
            """Enter dispatch mode and observe MemTracker's storage resize hook."""
            super().__enter__()
            try:
                self._install_storage_resize_hook()
            except Exception:
                super().__exit__(None, None, None)
                raise
            return self

        def __exit__(self, *args: Any) -> Any:
            """Restore MemTracker's storage resize hook before leaving dispatch mode."""
            self._restore_storage_resize_hook()
            return super().__exit__(*args)

        def _install_storage_resize_hook(self) -> None:
            """Wrap the active storage resize method to record capacity lifetimes."""
            previous_resize = torch.UntypedStorage.resize_
            self._previous_storage_resize = previous_resize

            @functools.wraps(previous_resize)
            def resize_(storage: Any, size: int) -> Any:
                """Resize storage through MemTracker, then record its new lifetime."""
                old_size = int(storage.size())
                result = previous_resize(storage, size)
                new_size = int(storage.size())
                if old_size != new_size:
                    self._record_storage_resize(storage, old_size, new_size)
                return result

            torch.UntypedStorage.resize_ = resize_  # type: ignore[method-assign, assignment]

        def _restore_storage_resize_hook(self) -> None:
            """Restore the resize method that was active when this mode entered."""
            if self._previous_storage_resize is not None:
                torch.UntypedStorage.resize_ = self._previous_storage_resize  # type: ignore[method-assign, assignment]
            self._previous_storage_resize = None

        def _close_blocks(self, storage_keys: set[int], end_index: int) -> None:
            """Close active blocks and discard their lifetime bookkeeping."""
            for storage_key in sorted(storage_keys):
                block = self._active_blocks.pop(storage_key, None)
                if block is None:
                    continue
                self._active_storage_refs.pop(storage_key, None)
                self._active_storage_sizes.pop(storage_key, None)
                self._released_storage_keys.discard(storage_key)
                block["end_time_stamp"] = end_index
                block["_end_lifecycle_event_index"] = self._next_lifecycle_event_index
                self._next_lifecycle_event_index += 1

        def _close_released_blocks(self, end_index: int) -> None:
            """Close blocks whose storage weak refs disappeared."""
            released_keys = self._released_storage_keys
            self._released_storage_keys = set()
            self._close_blocks(released_keys, end_index)

        def _allocate_virtual_address(self, size: int) -> str:
            """Return a deterministic aligned address in a reserved fake range."""
            address = self._next_virtual_address
            aligned_size = max(
                _VIRTUAL_ADDRESS_ALIGNMENT,
                (
                    (size + _VIRTUAL_ADDRESS_ALIGNMENT - 1)
                    // _VIRTUAL_ADDRESS_ALIGNMENT
499
500
501
502
503
504
505
506
507
508
509
510
                    (size + _VIRTUAL_ADDRESS_ALIGNMENT - 1)
                    // _VIRTUAL_ADDRESS_ALIGNMENT
                ) * _VIRTUAL_ADDRESS_ALIGNMENT,
            )
            self._next_virtual_address += aligned_size
            return f"0x{address:x}"

        def _assign_plot_coordinates(self) -> None:
            """Assign profiler-style fractional coordinates to lifecycle events.

            Logical task indices intentionally remain unchanged. Release and
            allocation events sharing one task are ordered by their observation
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
            Logical task indices intentionally remain unchanged. Release and
            allocation events sharing one task are ordered by their observation
            sequence and spread inside that task's ``[task, task + 1)`` range.
            """
            events_by_task: Dict[int, list[tuple[int, Dict[str, Any], str]]] = {}
            for block in self.memory_blocks:
                start_task = int(block["start_time_stamp"])
                events_by_task.setdefault(start_task, []).append((
                    int(block.pop("_start_lifecycle_event_index")),
                    block,
                    "start_plot_time_stamp",
                ))
                end_event_index = block.pop("_end_lifecycle_event_index", None)
                if end_event_index is None:
                    continue
                end_task = int(block["end_time_stamp"])
                events_by_task.setdefault(end_task, []).append((
                    int(end_event_index),
                    block,
                    "end_plot_time_stamp",
                ))
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
                    block,
                    "end_plot_time_stamp",
                ))

            for task_index, task_events in events_by_task.items():
                ordered_events = sorted(task_events, key=lambda event: event[0])
                denominator = Decimal(len(ordered_events) + 1)
                for event_offset, (_, block, field) in enumerate(ordered_events, start=1):
                    coordinate = Decimal(task_index) + Decimal(event_offset) / denominator
                    block[field] = format(coordinate, ".12f")

        def finalize(self) -> None:
            """Close released blocks and mark remaining storages persistent."""
            self._close_released_blocks(self._next_task_index)
            for storage_key, block in self._active_blocks.items():
                storage_ref = self._active_storage_refs.get(storage_key)
                storage = storage_ref() if storage_ref is not None else None
                tracked_storage = (
                    _lookup_tracked_storage(storage)
                    if storage is not None
                    else None
                )
                if tracked_storage is None or tracked_storage["size"] <= 0:
                    block["end_time_stamp"] = self._next_task_index
                    block["_end_lifecycle_event_index"] = self._next_lifecycle_event_index
                    self._next_lifecycle_event_index += 1
                    continue
                block["type"] = tracked_storage["type"]
                block["end_time_stamp"] = _PERSISTENT_END_INDEX
                block["is_persistent"] = 1
            self._active_blocks.clear()
            self._active_storage_refs.clear()
            self._active_storage_sizes.clear()
            self._released_storage_keys.clear()
            self._assign_plot_coordinates()

        def _classify_output_keys(
                self,
                input_storages: Dict[int, Dict[str, Any]],
                output_storages: Dict[int, Dict[str, Any]],
                active_input_keys: set[int],
566
567
568
569
570
571
572
573
574
575
576
577
578
579
                output_storages: Dict[int, Dict[str, Any]],
                active_input_keys: set[int],
        ) -> tuple[set[int], set[int]]:
            """Return resized inputs and newly allocated target-device outputs."""
            resized_output_keys = {
                storage_key
                for storage_key in active_input_keys & set(output_storages)
                if output_storages[storage_key]["size"] != self._active_storage_sizes[storage_key]
            }
            new_output_keys = {
                storage_key
                for storage_key, storage in output_storages.items()
                if (
                    storage_key not in input_storages
581
582
583
584
585
586
587
588
589
                    and storage["device"].split(":", maxsplit=1)[0] == device_type
                    and storage["size"] > 0
                )
            }
            new_output_keys.update(
                storage_key
                for storage_key in resized_output_keys
                if (
                    output_storages[storage_key]["device"].split(":", maxsplit=1)[0] == device_type
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
                    output_storages[storage_key]["device"].split(":", maxsplit=1)[0] == device_type
                    and output_storages[storage_key]["size"] > 0
                )
            )
            return resized_output_keys, new_output_keys

        def _record_input_users(self, active_input_keys: set[int], task_index: int) -> None:
            """Append the task index to every active input storage's user list."""
            for storage_key in active_input_keys:
                user_tasks = self._active_blocks[storage_key]["user_tasks"]
                if not user_tasks or user_tasks[-1] != task_index:
                    user_tasks.append(task_index)

        def _track_new_outputs(
                self,
                output_keys: set[int],
                output_storages: Dict[int, Dict[str, Any]],
                tracked_storages: Dict[int, Dict[str, Any]],
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
                tracked_storages: Dict[int, Dict[str, Any]],
                context: Dict[str, Any],
        ) -> None:
            """Create logical memory blocks and weak lifetime refs for outputs."""
            for storage_key in sorted(output_keys):
                output_storage = output_storages[storage_key]
                tracked_storage = tracked_storages.get(storage_key, {})
                device = output_storage["device"]
                size = int(tracked_storage.get("size", output_storage["size"]))
                current_total, peak_total = context["device_totals"].get(device, (0, 0))
                block = {
                    "start_time_stamp": context["task_index"],
                    "end_time_stamp": _PERSISTENT_END_INDEX,
                    "_start_lifecycle_event_index": self._next_lifecycle_event_index,
                    "device_addr": self._allocate_virtual_address(size),
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
                    "python_stack": context["stack"]["python_stack"],
                    "is_persistent": 0,
                    "is_small": int(size < _SMALL_ALLOCATION_BYTES),
                }
                self._next_lifecycle_event_index += 1
                self.memory_blocks.append(block)
                self._active_blocks[storage_key] = block
                self._active_storage_sizes[storage_key] = output_storage["size"]
                storage_ref = tracked_storage.get("storage_ref")
                storage = storage_ref() if storage_ref is not None else None
                if storage is None:
                    storage = output_storage["storage"]
                self._active_storage_refs[storage_key] = weakref.ref(
                    storage,
                    lambda _, key=storage_key: self._released_storage_keys.add(key),
                )

        def _record_storage_resize(self, storage: Any, old_size: int, new_size: int) -> None:
            """Record one tracked storage capacity transition as a block lifetime."""
            storage_key = int(getattr(storage, "_cdata", id(storage)))
            tracked_storage = _lookup_tracked_storage(storage)
            if tracked_storage is None:
                return
            device = tracked_storage["device"]
            if device.split(":", maxsplit=1)[0] != device_type:
                return

            if old_size > 0 or storage_key in self._active_blocks:
                self._close_blocks({storage_key}, self._next_task_index)
            if new_size <= 0:
                return

            task_index = self._next_task_index
            self._next_task_index += 1
            output_storages = {
                storage_key: {
                    "device": device,
                    "size": new_size,
                    "storage": storage,
671
672
673
674
675
676
677
678
679
680
                    "size": new_size,
                    "storage": storage,
                },
            }
            tracked_storages = {storage_key: tracked_storage}
            self._track_new_outputs(
                {storage_key},
                output_storages,
                tracked_storages,
                {
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
                    "module_fqn": _active_module_fqn(tracker),
                },
            )

        def __torch_dispatch__(self, func, types, args=(), kwargs=None):
            del types
            self._close_released_blocks(self._next_task_index)
            input_storages = _storage_map((args, kwargs))
            active_input_keys = {
                storage_key
                for storage_key in input_storages
                if storage_key in self._active_blocks
            }
            result = func(*args, **(kwargs or {}))
            output_storages = _storage_map(result)
            resized_output_keys, new_output_keys = self._classify_output_keys(
                input_storages,
                output_storages,
                active_input_keys,
            )
            self._close_released_blocks(self._next_task_index)
            if not active_input_keys and not new_output_keys and not resized_output_keys:
                return result
            task_index = self._next_task_index
            self._next_task_index += 1
            self._record_input_users(active_input_keys, task_index)
            self._close_blocks(resized_output_keys, task_index)
            if not new_output_keys:
                return result
            tracked_storages = _tracked_output_info(
                new_output_keys,
                output_storages,
            )
            output_devices = {
                output_storages[storage_key]["device"]
                for storage_key in new_output_keys
            }
            device_totals = {
                device: tracker.get_device_memory_totals(device)
                for device in output_devices
            }
            self._track_new_outputs(
                new_output_keys,
                output_storages,
                tracked_storages,
                {
736
737
738
739
740
741
742
743
744
745
746
                    "operator_name": str(func),
                    "module_fqn": _active_module_fqn(tracker),
                },
            )
            return result

    return _OperatorTraceMode()


def build_memory_report(
        tracker: Any,
769
770
771
772
773
774
775
776
    peak = _normalize_snapshot(tracker.get_tracker_snapshot("peak"))
    current = _normalize_snapshot(tracker.get_tracker_snapshot("current"))
    peak_bytes = _snapshot_total(peak, device_type)
    if peak_bytes <= 0:
        raise ValueError(
            f"MemTracker observed no {device_type!r} tensor memory; "
            f"tracked devices are {sorted(peak)}"
        )
828
829
830
831
832
833
834
835
836
    Raises:
        ValueError: If ``report`` does not describe a successful run.
    """
    if report.get("status") != "ok":
        raise ValueError("CSV output requires a successful HyperDryRun report")
    rows = []
    for block in report.get("memory_blocks", []):
        user_tasks = list(block.get("user_tasks", []))
        row = {
866
867
868
869
870
871
872
873
874
875
876
877
            writer = csv.DictWriter(report_file, fieldnames=_CSV_FIELDS)
            writer.writeheader()
            writer.writerows(rows)
        os.replace(temporary_path, destination)
    except Exception:
        if os.path.exists(temporary_path):
            os.unlink(temporary_path)
        raise
    return str(destination)


class TorchDryRunRunner:
906
907
908
909
910
911
912
913
914

    def _get_runtime(self) -> DryRunRuntime:
        """Return the cached torchrun identity for this worker."""
        if self._runtime is None:
            self._runtime = DryRunRuntime.from_torchrun_env()
        return self._runtime

    def _prepare_args(self, torch_module: Any) -> Any:
        """Validate user input and return an isolated dry-run config copy."""
975
976
977
978
979
980
981
982
983
984
985
986
987
        if build_dry_run_batch_fn is None:
            micro_batch_size = int(base.args.train.micro_batch_size)
            sequence_length = int(base.args.data.max_seq_len)
            if micro_batch_size < 1:
                raise ValueError(
                    f"train.micro_batch_size must be >= 1, got {micro_batch_size}"
                )
            if sequence_length < 2:
                raise ValueError(
                    "data.max_seq_len must be >= 2 for causal-LM dry-run, "
                    f"got {sequence_length}"
                )
            shape = (micro_batch_size, sequence_length)
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
            target_device_type: str,
            batch_shapes: Dict[str, list[int]],
    ) -> Dict[str, Any]:
        """Build reproducibility metadata for a successful report."""
        dims = base.parallel_dims
        runtime = self._get_runtime()
        parallel = {
            name: int(getattr(dims, name))
            for name in ("dp_replicate", "dp_shard", "tp", "cp", "ep", "pp", "etp")
        }
        return {
            "model": base.args.model.name,
            "torch_version": torch_module.__version__,
            "rank": runtime.rank,
            "world_size": runtime.world_size,
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
    @staticmethod
    def _initialize_flat_buffers(base: Any) -> None:
        """Materialize enabled FSDP flat shards before tracker registration."""
        for hsdp_state in base._iter_hsdp_states():
            param_group = getattr(hsdp_state, "param_group", None)
            if param_group is not None and param_group.enable_zero_copy:
                param_group._init_flat_param_buffer()

    def _execute_fake_step(
            self,
            torch_module: Any,
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
            self._stage = "memory_tracking"
            tracker = _create_indexed_mem_tracker(mem_tracker_type)
            tracker.track_external(base.model, base.optimizer, batch)
            operator_trace = _create_operator_trace_mode(tracker, base.device.type)
            with tracker, operator_trace:
                if hasattr(base.model, "set_requires_gradient_sync"):
                    base.model.set_requires_gradient_sync(True)
                if hasattr(base.model, "set_is_last_backward"):
                    base.model.set_is_last_backward(True)
                global_tokens = token_count * base.parallel_dims.dp_size * base.parallel_dims.cp
                base.forward_backward_step(batch, token_count, global_tokens)
                base._run_post_fsdp_grad_reduce()
                base.optimizer.step()
                base.optimizer.zero_grad(set_to_none=True)
            operator_trace.finalize()

            self._stage = "report_generation"
            return build_memory_report(
                tracker=tracker,
                metadata=self._metadata(
                    torch_module,
                    base,
1144
1145
1146
1147
1148
1149
1150
1151
                f"rank_{runtime.rank}",
                f"rank_{runtime.rank}_memory.csv",
            )
            if self.args.train.backend != "torch":
                raise NotImplementedError(
                    "HyperDryRun currently supports train.backend='torch' only, "
                    f"got {self.args.train.backend!r}"
                )
1196
1197
1198
1199
1200
1201
1202
1203
1204
            # ordinary meta tensors. DeviceMesh owns real CPU rank metadata;
            # deepcopying that metadata inside FakeTensorMode would incorrectly
            # mix fake meta storage with its real CPU storage.
            if base.spec.parallelize_fn is None:
                raise ValueError(
                    f"Model spec {base.spec.name!r} must define parallelize_fn "
                    "for HyperDryRun"
                )
            base.model = base.spec.parallelize_fn(base.model, base.mesh, base.args)
hyper_parallel/platform/torch/dtensor.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
                (skips ``_build_layout``; see ``DTensor.from_local_with_layout``).
        """
        if isinstance(local_tensor, DTensorBase):
            # Copy from existing DTensorBase — use alias_placements to preserve multi-axis ordering
            if getattr(local_tensor, "_is_fake_wrapper", False):
                copy_placements = (
                    local_tensor.layout.alias_placements
                    if local_tensor.layout
                    else local_tensor.placements
                )
                return cls(
                    local_tensor._local_tensor,
                    local_tensor.device_mesh,
                    copy_placements,
                    local_tensor.layout,
65
66
67
68
69
70
71
72
73
        # path below.
        # pylint: disable=C0415
        from torch._subclasses.fake_tensor import FakeTensor
        if isinstance(local_tensor, FakeTensor):
            t = Tensor._make_wrapper_subclass(
                cls,
                local_tensor.size(),
                strides=local_tensor.stride(),
                storage_offset=local_tensor.storage_offset(),
75
76
77
78
79
80
81
82
83
                layout=local_tensor.layout,
                device=torch.device("meta"),
                requires_grad=local_tensor.requires_grad,
            )
            t._is_fake_wrapper = True
        else:
            t = Tensor._make_subclass(cls, local_tensor, local_tensor.requires_grad)
        t.__init_data__(local_tensor, device_mesh, placements, layout)
        return t
118
119
120
121
122
123
124
125
126
            # Wrapper subclasses must enter at TorchDispatch so autograd sees
            # their meta C++ device. Calling the op on unwrapped FakeTensors
            # here would make autograd inspect the outer wrapper's logical
            # CUDA/NPU device on a CPU-only build.
            return super().__torch_function__(func, types, args, kwargs)
        # pylint: disable=C0415
        from hyper_parallel.core.shard._op_dispatch import _OP_DISPATCHER
        out = _OP_DISPATCHER.dispatch(func, args, kwargs)
        return out
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
    @classmethod
    def __torch_dispatch__(cls, func, types, args=(), kwargs=None):
        """Dispatch fake-wrapper DTensors through HyperParallel layout rules."""
        # pylint: disable=C0415
        from hyper_parallel.core.shard._op_dispatch import _OP_DISPATCHER
        return _OP_DISPATCHER.dispatch(func, args, kwargs or {})

    def __tensor_flatten__(self):
        """Expose local storage so MemTracker can inspect wrapper DTensors."""
        context = (self._device_mesh, self._alias_placements(), self._layout)
        return ["_local_tensor"], context

    @classmethod
    def __tensor_unflatten__(cls, inner_tensors, context, outer_size, outer_stride):
        """Rebuild a traceable wrapper DTensor from its local tensor."""
        del outer_size, outer_stride
        device_mesh, placements, layout = context
        return cls(
            inner_tensors["_local_tensor"],
            device_mesh=device_mesh,
            placements=placements,
            layout=layout,
156
157
158
159
160
161
162
163
164
165

        Returns:
            Optional[Tensor]: The gradient tensor, or None if no gradient is set.
        """
        if getattr(self, "_is_fake_wrapper", False):
            return Tensor.grad.__get__(self, type(self))  # pylint: disable=C2801
        return self._local_tensor.grad

    @grad.setter
    def grad(self, value: Optional[Tensor]) -> None:
168
169
170
171
172
173
174
175
176
177
178

        Args:
            value (Optional[Tensor]): The gradient tensor to set, or None to clear.
        """
        if getattr(self, "_is_fake_wrapper", False):
            Tensor.grad.__set__(self, value)  # pylint: disable=C2801
            return
        self._local_tensor.grad = value

    @property
    def requires_grad(self) -> bool:
316
317
318
319
320
321
322
323
324
325
    def data(self, value: Tensor) -> None:
        """Set the underlying tensor data, extracting the local shard if a DTensor is given."""
        local_value = value.to_local() if isinstance(value, DTensorBase) else value
        if getattr(self, "_is_fake_wrapper", False):
            if self.dtype != local_value.dtype:
                raise ValueError(
                    "Fake-wrapper DTensor data replacement must preserve dtype, "
                    f"got {local_value.dtype} for {self.dtype}"
                )
            # Wrapper subclasses have no mutable storage of their own. Rebase
325
326
327
328
329
330
331
332
333
334
            # Wrapper subclasses have no mutable storage of their own. Rebase
            # the traceable inner FakeTensor, which is the storage exposed to
            # dispatch and MemTracker. FSDP legitimately rebases a full-shape
            # wrapper onto a local shard when it creates its flat buffer.
            self._local_tensor = local_value
            return
        # Tensor.data.__set__ on a Tensor subclass otherwise enters __torch_function__
        # and only rebinds _local_tensor through DTensor dispatch.
        with getattr(torch, "_C").DisableTorchFunctionSubclass():
            Tensor.data.__set__(self, local_value)
hyper_parallel/platform/torch/fully_shard/param_group.py
506
507
508
509
510
511
512
513
514
            return False
        param_data = self.hsdp_params[0]._sharded_param_data
        from torch._subclasses.fake_tensor import FakeTensor
        if isinstance(param_data, FakeTensor):
            return param_data.untyped_storage() is self._flat_param_buffer.untyped_storage()
        return param_data.data_ptr() == self._flat_param_buffer.data_ptr()

    def unshard(self, async_op: bool = False):
        """Trigger fused all-gather to reconstruct full parameters from shards.
hyper_parallel/platform/torch/memory_profiler.py
57
58
59
60
61
62
63
64
65
    )
    # PyTorch may expose a renamed PrivateUse1 backend by either spelling,
    # depending on when the profiler event was materialized.
    if device_name.lower() in ("privateuse1", "privateuseone"):
        return "npu"
    return device_name


def _tensor_identity(metadata: Any) -> Optional[tuple[str, int]]:
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
    """Return an allocation identity, falling back to its storage pointer."""
    allocation_id = getattr(metadata, "allocation_id", None)
    if allocation_id is not None:
        return "allocation", int(allocation_id)
    pointer = getattr(metadata, "storage_data_ptr", None)
    if pointer is None:
        pointer = getattr(metadata, "ptr", None)
    return None if pointer is None else ("pointer", int(pointer))


def _flatten_tensor_inputs(value: Any) -> Iterable[Any]:
    """Yield tensor metadata objects from nested profiler operator inputs."""
    if isinstance(value, (list, tuple)):
        for item in value:
            yield from _flatten_tensor_inputs(item)
    elif getattr(value, "storage_data_ptr", None) is not None:
        yield value


def _python_frames(event: Any) -> list[Any]:
    """Collect Python callsite frames from root to the event."""
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
    frames = []
    parent = event
    while parent is not None:
        if _event_type_name(parent) == "PyCall":
            callsite = getattr(parent.typed[1], "callsite", None)
            if callsite is not None:
                frames.append(callsite)
        parent = getattr(parent, "parent", None)
    frames.reverse()
    deduplicated = []
    for frame in frames:
        key = (
            str(getattr(frame, "file_name", "")),
            int(getattr(frame, "line_number", 0)),
            str(getattr(frame, "function_name", "")),
        )
        if not deduplicated or deduplicated[-1][0] != key:
            deduplicated.append((key, frame))
    return [frame for _, frame in deduplicated]


def _module_name(event: Any) -> str:
109
110
111
112
113
114
115
116
117
118
    """Return the nearest profiled module class name."""
    parent = event
    while parent is not None:
        if _event_type_name(parent) == "PyCall":
            module = getattr(parent.typed[1], "module", None)
            if module is not None:
                return str(getattr(module, "cls_name", ""))
        parent = getattr(parent, "parent", None)
    return ""

126
127
128
129
130
131
132
133
134
        if (
            _event_type_name(parent) == "PyCall"
            and getattr(parent.typed[1], "optimizer", None) is not None
        ):
            return "optimizer"
        parent = getattr(parent, "parent", None)
    scope = str(getattr(event.typed[1], "scope", "")).lower()
    if "backward" in scope or any(
        "backward" in name or "autograd" in name for name in names
132
133
134
135
136
137
138
139
140
141
142
    scope = str(getattr(event.typed[1], "scope", "")).lower()
    if "backward" in scope or any(
        "backward" in name or "autograd" in name for name in names
    ):
        return "backward"
    if any("recompute" in name or "checkpointfunction" in name for name in names):
        return "recompute"
    return "forward"


def _stack_context(event: Any) -> Dict[str, Any]:
163
164
165
166
167
168
169
170
171
172
        task_index = task_by_event.get(id(parent))
        if task_index is not None:
            return task_index
        parent = getattr(parent, "parent", None)
    position = bisect.bisect_right(starts, int(event.start_time_ns)) - 1
    return max(position, 0)


def _category_for_allocation(memory_profile: Any, allocation: Any) -> str:
    """Resolve the profiler memory category for one allocation."""
173
174
175
176
177
178
179
180
181
    try:
        from torch.profiler._memory_profiler import TensorKey  # pylint: disable=C0415

        key = TensorKey.from_allocation(allocation)
        category = (
            memory_profile._categories.get(key, 0)  # pylint: disable=protected-access
            if key is not None else None
        )
    except (AttributeError, KeyError, TypeError):
195
196
197
198
199
200
201
202
203
204
205
206
207
208
    task_by_event = {id(event): index for index, event in enumerate(tasks)}
    users = defaultdict(list)
    for task_index, event in enumerate(tasks):
        for value in getattr(event.typed[1], "inputs", ()):
            for metadata in _flatten_tensor_inputs(value):
                identity = _tensor_identity(metadata)
                if identity is not None and (
                    not users[identity] or users[identity][-1] != task_index
                ):
                    users[identity].append(task_index)
    return tasks, task_by_event, users


def _assign_plot_coordinates(blocks: list[Dict[str, Any]]) -> None:
241
242
243
244
245
246
247
248
249

    allocation_devices = defaultdict(int)
    for event in events:
        if _event_type_name(event) == "Allocation":
            allocation_devices[_device_type(event.typed[1].device)] += 1
    observed = ", ".join(
        f"{device}={count}"
        for device, count in sorted(allocation_devices.items())
    ) or "none"
274
275
276
277
278
279
280
281
282
) -> int:
    """Close the newest active block matching one release event."""
    candidates = active.get(identity, [])
    if not candidates:
        return lifecycle_index
    block = candidates.pop()
    block["end_time_stamp"] = task_index
    block["_end_event_index"] = lifecycle_index
    return lifecycle_index + 1
293
294
295
296
297
298
299
300
301
    """
    all_events = list(events)
    tasks, task_by_event, users = _task_metadata(all_events)
    if not tasks:
        raise RuntimeError("Torch profiler contains no CPU/ATen operator tasks")
    starts = [int(event.start_time_ns) for event in tasks]
    allocations = _target_allocation_events(all_events, device_type)
    active = defaultdict(list)
    blocks = []
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372


def build_profiler_report(profiler: Any, device_type: str) -> Dict[str, Any]:
    """Convert a completed Torch profiler cycle into a CSV-compatible report."""
    try:
        memory_profile = profiler._memory_profile()  # pylint: disable=protected-access
        events = memory_profile._op_tree.dfs()  # pylint: disable=protected-access
    except (AttributeError, RuntimeError, ValueError) as error:
        raise RuntimeError(
            "Torch profiler did not expose the allocation event tree required "
            "for memory CSV output"
        ) from error
    return {
        "status": "ok",
        "memory_blocks": build_memory_blocks(events, memory_profile, device_type),
    }
388
389
390
391
392
393
394
395
            for row in connection.execute(f"PRAGMA table_info({table})")
        }
        missing = columns - actual
        if missing:
            raise ValueError(
                f"Ascend profiler database table {table} is missing columns: "
                f"{sorted(missing)}"
            )
408
409
410
411
412
413
414
415
416
                 api.globalTid, api.rowid
        """
    ).fetchall()
    if not records:
        raise ValueError("Ascend profiler database contains no type=50001 CPU tasks")
    return [
        _CpuTask(
            start_ns=int(start_ns),
            end_ns=int(end_ns),
471
472
473
474
475
476
477
478
479
        tasks: list[_CpuTask], memory_records: list[tuple[Any, ...]],
) -> list[Dict[str, Any]]:
    """Map released Ascend allocations onto same-name containing CPU tasks."""
    if not memory_records:
        raise ValueError("Ascend profiler database contains no released OP_MEMORY rows")
    tasks_by_name = defaultdict(list)
    for task in tasks:
        tasks_by_name[task.name].append(task)
    starts = [task.start_ns for task in tasks]
488
489
490
491
492
493
494
495
496
497
            task for task in tasks_by_name.get(str(name), [])
            if task.start_ns <= allocation_ns <= task.end_ns
        ]
        if not candidates:
            unmatched.append((str(name), allocation_ns))
            continue
        producer = min(
            candidates,
            key=lambda task: (
                task.end_ns - task.start_ns,
500
501
502
503
504
505
506
507
508
            ),
        )
        end_task = bisect.bisect_left(starts, release_ns)
        if end_task < producer.task_id:
            raise ValueError(
                f"Ascend profiler lifecycle ends before it starts for {name!r}: "
                f"producer={producer.task_id}, release={end_task}"
            )
        size = int(size)
536
537
538
539
540
541
542
543
544
545
546
547
            "_release_time_ns": release_ns,
            "_rowid": int(rowid),
        })
    if unmatched:
        examples = ", ".join(
            f"{name}@{timestamp}" for name, timestamp in unmatched[:5]
        )
        raise ValueError(
            f"Failed to map {len(unmatched)}/{len(memory_records)} Ascend memory "
            f"allocations to containing same-name CPU tasks; examples: {examples}"
        )
    for block in mapped:
553
554
555
556
557
558
559
560
561
562
def build_ascend_profiler_report(database_path: str) -> Dict[str, Any]:
    """Convert an Ascend profiler database to released logical lifetimes."""
    try:
        connection = sqlite3.connect(database_path)
    except sqlite3.Error as error:
        raise ValueError(
            f"Unable to open Ascend profiler database {database_path}: {error}"
        ) from error
    try:
        _validate_ascend_database(connection)
561
562
563
564
565
566
567
568
569
570
    try:
        _validate_ascend_database(connection)
        tasks = _load_ascend_cpu_tasks(connection)
        memory_records = _load_ascend_memory_records(connection)
    except sqlite3.Error as error:
        raise ValueError(
            f"Failed to query Ascend profiler database {database_path}: {error}"
        ) from error
    finally:
        connection.close()
hyper_parallel/platform/torch/platform.py
738
739
740
741
742
743
744
745
746
747
748
749
750
751
        Returns:
            str: The operation name.
        """
        if isinstance(func, OpOverload):
            full_name = func.name() if callable(func.name) else func.name
            core_name = full_name.split("::")[-1].split(".")[0]
            return core_name
        if isinstance(func, OpOverloadPacket):
            full_name = func.name() if callable(func.name) else func.name
            return full_name.split("::")[-1]
        if hasattr(func, "__name__"):
            return func.__name__
        func_str = str(func)
        if "built-in function" in func_str:
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
    @staticmethod
    def init_dry_run_process_group() -> None:
        """Initialize PyTorch's fake process group from torchrun variables."""
        # pylint: disable=C0415
        from torch.testing._internal.distributed import fake_pg

        # Importing PyTorch's fake_pg module registers the version-matched
        # factory. In particular, newer releases require a private internal
        # constructor while 2.7/2.9 use the factory shipped with that release.
        del fake_pg
        dist.init_process_group(
            backend=dist.Backend.FAKE,
            store=dist.HashStore(),
            world_size=int(os.environ["WORLD_SIZE"]),
            rank=int(os.environ["RANK"]),
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378

    @staticmethod
    def get_dry_run_device(device_type: str, device_idx: int) -> torch.device:
        """Return a Torch device for fake execution without selecting it."""
        if device_type == "cpu":
            return torch.device("cpu")
        return torch.device(device_type, device_idx)

    @staticmethod
    def destroy_process_group(group: Optional[ProcessGroup] = None) -> None:
        """
hyper_parallel/trainer/base.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199

        Calls hyper's own ``init_process_group`` and ``init_device_mesh``.
        Mesh shape is derived from ``args.parallel`` (dp, tp, cp, pp, ep).
        """
        dry_run_cfg = getattr(self.args.train, "dry_run", None)
        dry_run_enabled = bool(dry_run_cfg and dry_run_cfg.enabled)
        if not dry_run_enabled:
            self._apply_pre_init_deterministic_env()
        if dry_run_enabled:
            platform.init_dry_run_process_group()
        else:
            backend = self.args.train.comm_backend
            init_process_group(backend=backend)

        local_rank = self.args.train.local_rank
        if dry_run_enabled:
            device_type = dry_run_cfg.simulation_device_type
            self.device = platform.get_dry_run_device(device_type, 0)
        else:
            device_type = platform.device_type()  # "npu" or "cuda"
            # Use platform.device(idx) — backend-agnostic.
            self.device = platform.device(local_rank)
            device_handle = platform.get_device_handle(device_type)
            device_handle.set_device(local_rank)

        # Build & validate parallel dims in one place (fail-fast).

        self.parallel_dims = ParallelDims.from_config(
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
        self._dp_group_info = GroupInfo(
            group_name="trainer_dp", group=dp_group, rank_size=dp_size,
        )

        if not dry_run_enabled:
            seed = self.args.train.seed
            platform.manual_seed(seed)
            random.seed(seed)
            np.random.seed(seed)
            # ``platform.manual_seed`` only covers CPU; seed the device RNG too.
            try:
                handle = platform.get_device_handle(device_type)
                if hasattr(handle, "manual_seed_all"):
                    handle.manual_seed_all(seed)
                elif hasattr(handle, "manual_seed"):
                    handle.manual_seed(seed)
            except Exception as exc:  # pylint: disable=W0718
                logger.warning("Device-side seed init skipped: %s", exc)

        if self._deterministic and not dry_run_enabled:
            warn_only = self.args.train.debug.deterministic_warn_only
            torch.use_deterministic_algorithms(True, warn_only=warn_only)
            torch.backends.cudnn.deterministic = True
            torch.backends.cudnn.benchmark = False
740
741
742
743
744
745
746
747
748
        ``deepstack_merger_list`` included — so a complete load leaves nothing
        random.
        """
        init_device = self.args.train.init_device
        weights_path = (
            None
            if getattr(getattr(self.args.train, "dry_run", None), "enabled", False)
            else self.args.model.weights_path
        )
1143
1144
1145
1146
1147
1148
1149
1150
1151

    def on_step_begin(self):
        """Dispatch on_step_begin to all callbacks."""
        self.logging_callback.on_step_begin(self.state)
        self.profiler_callback.on_step_begin(self.state)
        for cb in self.user_callbacks:
            cb.on_step_begin(self.state)

    def on_step_end(self, loss=None, grad_norm=None):
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074

        This is the meta-init path used after ``fully_shard`` has installed
        FSDP views.
        """
        dry_run_enabled = bool(
            getattr(getattr(self.args.train, "dry_run", None), "enabled", False)
        )
        device_type = self.device.type if dry_run_enabled else platform.device_type()
        # Step 1: meta → real storage, in-place (FSDP-views preserved).
        self.model.to_empty(device=device_type)
        self._materialize_replicate_params(device_type)
        # Step 2: init the local shard of every param (and zero every buffer).
        if dry_run_enabled:
            param_count = sum(1 for _ in self.model.parameters())
        else:
            param_count = self._init_local_shards()
        # Re-derive buffers wiped by ``to_empty`` (e.g. ``inv_freq``);
        # without this RoPE silently returns identity rotation.
        if not dry_run_enabled:
            for module in self.model.modules():
                if hasattr(module, "reset_inv_freq"):
                    module.reset_inv_freq()
        # Re-tie weights — ``to_empty`` gives every nn.Parameter fresh
        # storage so ``__init__``-time ties are broken. Must happen before
        # ``lazy_init`` re-wraps params as DTensor (non-leaf), which would
        # cause ``register_parameter`` to reject the assignment. Skipped under
hyper_parallel/trainer/callbacks/base.py
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634

    def _validate_config(self) -> None:
        """Validate the profiling boundary before training starts."""
        if self._device_type not in ("npu", "cuda"):
            raise ValueError(
                "train.profile requires an NPU or CUDA training device, got "
                f"{self._device_type!r} on rank {self._rank}"
            )
        if not isinstance(self.cfg.output_dir, str) or not self.cfg.output_dir.strip():
            raise ValueError("train.profile.output_dir must be a non-empty string")
        for field_name in ("wait_steps", "warmup_steps"):
            value = getattr(self.cfg, field_name)
            if not isinstance(value, int) or isinstance(value, bool) or value < 0:
                raise ValueError(
                    f"train.profile.{field_name} must be a non-negative integer, "
                    f"got {value!r}"
                )
        active_steps = self.cfg.active_steps
636
637
638
639
640
641
642
643
            not isinstance(active_steps, int)
            or isinstance(active_steps, bool)
            or active_steps <= 0
        ):
            raise ValueError(
                "train.profile.active_steps must be a positive integer, got "
                f"{active_steps!r}"
            )
648
649
650
651
652
653
654
655
656
        activities = [profiler_module.ProfilerActivity.CPU]
        if self._device_type == "npu":
            npu_activity = getattr(profiler_module.ProfilerActivity, "NPU", None)
            if npu_activity is None:
                raise RuntimeError(
                    f"NPU profiler activity is unavailable on rank {self._rank}"
                )
            activities.append(npu_activity)
            return activities
656
657
658
659
660
661
662
663
664
665
666
667
668
669
            return activities
        activity_name = "CUDA" if self._device_type == "cuda" else "PrivateUse1"
        device_activity = getattr(profiler_module.ProfilerActivity, activity_name, None)
        if device_activity is None:
            raise RuntimeError(
                f"{activity_name} profiler activity is unavailable on rank {self._rank}"
            )
        supported_activities = profiler_module.supported_activities()
        if device_activity not in supported_activities:
            raise RuntimeError(
                f"{activity_name} profiler activity is unsupported on rank {self._rank}; "
                f"supported activities: {supported_activities}"
            )
        activities.append(device_activity)
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697

    def _write_trace(self, profiler) -> None:
        """Convert the completed active cycle and atomically write its CSV."""
        if self._trace_written:
            return
        try:
            if self._device_type == "npu":
                self._native_trace_handler(profiler)
                databases = sorted(Path(self._trace_tempdir.name).rglob(
                    "ascend_pytorch_profiler_*.db"
                ))
                if len(databases) != 1:
                    raise RuntimeError(
                        "Ascend profiler analysis must produce exactly one database "
                        f"for rank {self._rank}, found {len(databases)} under "
                        f"{self._trace_tempdir.name}"
                    )
                report = build_ascend_profiler_report(str(databases[0]))
            else:
                report = build_profiler_report(profiler, self._device_type)
            write_memory_csv(report, self._csv_path)
        except (OSError, RuntimeError, ValueError) as error:
            raise RuntimeError(
                f"ProfilerCallback failed to write memory CSV for rank "
                f"{self._rank} on {self._device_type}: {error}"
            ) from error
        self._trace_written = True
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723

    def on_train_begin(self, state: "TrainerState", **kwargs) -> None:
        del state, kwargs
        if not self.enabled:
            return
        try:
            profiler_module = torch.profiler
            if self._device_type == "npu":
                from torch_npu import profiler as npu_profiler  # pylint: disable=C0415

                profiler_module = npu_profiler
                # The directory must outlive this callback and is closed in
                # ``on_train_end``, so a local ``with`` block is not applicable.
                self._trace_tempdir = tempfile.TemporaryDirectory(  # pylint: disable=consider-using-with
                    prefix=f"hyper_profiler_rank_{self._rank}_"
                )
                self._native_trace_handler = npu_profiler.tensorboard_trace_handler(
                    dir_name=self._trace_tempdir.name,
                    worker_name=f"rank_{self._rank}",
                    analyse_flag=True,
                    async_mode=False,
736
737
738
739
740
741
742
743
744
745
746
747
748
                profile_memory=True,
                with_stack=True,
            )
            self._profiler.start()
        except (ImportError, OSError, RuntimeError, ValueError) as error:
            self._profiler = None
            if self._trace_tempdir is not None:
                self._trace_tempdir.cleanup()
                self._trace_tempdir = None
            raise RuntimeError(
                f"ProfilerCallback failed to start on rank {self._rank} for "
                f"{self._device_type}: {error}"
            ) from error
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
            ) from error

    def on_step_end(self, state: "TrainerState", *, loss: Optional[float] = None,
                    grad_norm: Optional[float] = None, **kwargs) -> None:
        del state, loss, grad_norm, kwargs
        if self._profiler is not None and not self._trace_written:
            self._profiler.step()

    def on_train_end(self, state: "TrainerState", **kwargs) -> None:
        del state, kwargs
        if self._profiler is None:
            return
        try:
            self._profiler.stop()
        except (RuntimeError, ValueError) as error:
            raise RuntimeError(
                f"ProfilerCallback failed to stop on rank {self._rank} for "
                f"{self._device_type}: {error}"
            ) from error
        finally:
767
768
769
770
771
772
773
774
775
776
        finally:
            self._profiler = None
            self._native_trace_handler = None
            if self._trace_tempdir is not None:
                self._trace_tempdir.cleanup()
                self._trace_tempdir = None


class WandbCallback(Callback):
    """Weights & Biases logging callback — STUB (not verified).