Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_multicore / diagnostics / dump / api.py: 90%

311 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 05:03 +0800

1# Copyright 2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15"""Schedule-driven Tensor dump for Torch MegaKernel invocations.""" 

16 

17from __future__ import annotations 

18 

19from collections.abc import Callable, Collection, Sequence 

20from dataclasses import dataclass 

21import json 

22from pathlib import Path 

23from typing import Any 

24 

25import torch 

26 

27from hyper_multicore.diagnostics.common.session import ( 

28 _activate_session, 

29 _deactivate_session, 

30) 

31from hyper_multicore.diagnostics.common.call import DiagnosticCall, LaunchArguments, LaunchContext 

32from hyper_multicore.diagnostics.common.schedule import DiagnosticAction 

33from hyper_multicore.diagnostics.dump.layout import _SAFE_FILE_NAME, _stage_names 

34from hyper_multicore.diagnostics.dump.l0 import ( 

35 _GraphTensor, _TensorSnapshot, _describe_graph_tensors, _snapshot_tensor, _write_l0_stage_files, 

36) 

37from hyper_multicore.diagnostics.dump.l1 import ( 

38 _L1Snapshot, 

39 _PreparedL1Dump, 

40 _decode_l1_dump, 

41 _prepare_l1_dump, 

42) 

43 

44 

45__all__ = [ 

46 "TensorDumpConfig", 

47 "TorchMegaKernelTensorDumper", 

48 "tensor_dump", 

49] 

50 

51 

52_VALID_DIRECTIONS = frozenset(("forward", "backward")) 

53_VALID_ROLES = frozenset(("external_input", "stage_output", "final_output")) 

54_VALID_IO = frozenset(("input", "output")) 

55_VALID_CORE_TYPES = frozenset(("AIC", "AIV")) 

56 

57 

58def _normalize_string_filter( 

59 value: str | Collection[str] | None, 

60 *, 

61 name: str, 

62 allowed: frozenset[str] | None = None, 

63) -> tuple[str, ...] | None: 

64 """Normalize a scalar or collection of named selection values.""" 

65 if value is None: 

66 return None 

67 values = (value,) if isinstance(value, str) else tuple(value) 

68 if not values: 

69 raise ValueError(f"{name} must not be empty") 

70 for item in values: 

71 if not isinstance(item, str) or not item: 

72 raise TypeError(f"{name} entries must be non-empty strings, got {item!r}") 

73 if allowed is not None and item not in allowed: 

74 raise ValueError(f"unsupported {name} entry {item!r}; expected one of {sorted(allowed)}") 

75 return values 

76 

77 

78def _normalize_non_negative_int_filter( 

79 value: int | Collection[int] | None, 

80 *, 

81 name: str, 

82) -> tuple[int, ...] | None: 

83 """Normalize integer selections while rejecting booleans and negative IDs.""" 

84 if value is None: 

85 return None 

86 if isinstance(value, bool): 

87 raise ValueError(f"{name} entries must be non-negative integers, got {value!r}") 

88 values = (value,) if isinstance(value, int) else tuple(value) 

89 if not values: 

90 raise ValueError(f"{name} must not be empty") 

91 for item in values: 

92 if not isinstance(item, int) or isinstance(item, bool) or item < 0: 

93 raise ValueError(f"{name} entries must be non-negative integers, got {item!r}") 

94 return values 

95 

96 

97def _normalize_ranks(value: int | Collection[int] | None) -> tuple[int, ...] | None: 

98 return _normalize_non_negative_int_filter(value, name="ranks") 

99 

100 

101@dataclass(frozen=True) 

102class TensorDumpConfig: 

103 """Configuration for MegaKernel Tensor value collection. 

104 

105 Args: 

106 output_dir: Root directory for per-rank dump windows. 

107 level: Dump level. Supported values are ``"L0"`` and ``"L1"``. 

108 kernel_names: Optional exact MegaKernel names whose invocations should 

109 be retained. 

110 tensor_names: Optional exact logical Tensor names selected from the 

111 MegaKernel compute graph. 

112 roles: Optional L0 Tensor roles to retain. Supported roles are 

113 ``external_input``, ``stage_output`` and ``final_output``. 

114 directions: Forward and/or backward invocations to retain. 

115 ranks: Optional non-negative distributed ranks to retain. 

116 stages: Optional exact L1 stage names. 

117 task_ids: Optional stable global RuntimeConfig task IDs. 

118 stage_task_indices: Optional task indices local to a stage. 

119 owner_ids: Optional business diagnostic owner IDs. 

120 core_types: Optional L1 Device worker types, ``AIC`` and/or ``AIV``. 

121 core_ids: Optional core-local worker IDs. 

122 io: L1 operator ports to capture: ``input``, ``output``, or both. 

123 device_memory_budget_mb: Maximum Device memory used by one L1 launch. 

124 max_bytes_per_tensor: Optional per-record byte cap. A capped Tensor is 

125 exported as a flattened prefix and marked as truncated. 

126 """ 

127 

128 output_dir: str | Path 

129 level: str = "L0" 

130 tensor_names: str | Collection[str] | None = None 

131 roles: str | Collection[str] | None = None 

132 directions: str | Collection[str] | None = None 

133 ranks: int | Collection[int] | None = None 

134 kernel_names: str | Collection[str] | None = None 

135 stages: str | Collection[str] | None = None 

136 task_ids: int | Collection[int] | None = None 

137 stage_task_indices: int | Collection[int] | None = None 

138 owner_ids: int | Collection[int] | None = None 

139 core_types: str | Collection[str] | None = None 

140 core_ids: int | Collection[int] | None = None 

141 io: str | Collection[str] | None = None 

142 device_memory_budget_mb: int = 512 

143 max_bytes_per_tensor: int | None = None 

144 

145 def __post_init__(self) -> None: 

146 """Validate and normalize user-facing collection filters.""" 

147 if not isinstance(self.output_dir, (str, Path)): 

148 raise TypeError(f"output_dir must be str or Path, got {type(self.output_dir).__name__}") 

149 level = self.level.upper() if isinstance(self.level, str) else self.level 

150 if level not in ("L0", "L1"): 

151 raise ValueError(f"unsupported Tensor dump level {self.level!r}; expected 'L0' or 'L1'") 

152 output_dir = Path(self.output_dir).expanduser() 

153 kernel_names = _normalize_string_filter(self.kernel_names, name="kernel_names") 

154 tensor_names = _normalize_string_filter(self.tensor_names, name="tensor_names") 

155 roles = _normalize_string_filter(self.roles, name="roles", allowed=_VALID_ROLES) 

156 directions = _normalize_string_filter( 

157 self.directions, 

158 name="directions", 

159 allowed=_VALID_DIRECTIONS, 

160 ) 

161 stages = _normalize_string_filter(self.stages, name="stages") 

162 task_ids = _normalize_non_negative_int_filter(self.task_ids, name="task_ids") 

163 stage_task_indices = _normalize_non_negative_int_filter( 

164 self.stage_task_indices, 

165 name="stage_task_indices", 

166 ) 

167 owner_ids = _normalize_non_negative_int_filter(self.owner_ids, name="owner_ids") 

168 core_types = _normalize_string_filter( 

169 self.core_types, 

170 name="core_types", 

171 allowed=_VALID_CORE_TYPES, 

172 ) 

173 core_ids = _normalize_non_negative_int_filter(self.core_ids, name="core_ids") 

174 io = _normalize_string_filter(self.io, name="io", allowed=_VALID_IO) 

175 if not isinstance(self.device_memory_budget_mb, int) or isinstance( 

176 self.device_memory_budget_mb, 

177 bool, 

178 ): 

179 raise TypeError("device_memory_budget_mb must be a positive integer") 

180 if self.device_memory_budget_mb <= 0: 

181 raise ValueError("device_memory_budget_mb must be positive") 

182 if self.max_bytes_per_tensor is not None: 

183 if not isinstance(self.max_bytes_per_tensor, int) or isinstance( 

184 self.max_bytes_per_tensor, 

185 bool, 

186 ): 

187 raise TypeError("max_bytes_per_tensor must be a positive integer or None") 

188 if self.max_bytes_per_tensor <= 0: 

189 raise ValueError("max_bytes_per_tensor must be positive") 

190 if level == "L1" and all( 

191 value is None 

192 for value in (stages, task_ids, stage_task_indices, owner_ids, tensor_names) 

193 ): 

194 raise ValueError("L1 Tensor dump requires at least one stage, task, owner, or Tensor filter") 

195 

196 object.__setattr__(self, "output_dir", output_dir) 

197 object.__setattr__(self, "level", level) 

198 object.__setattr__(self, "kernel_names", kernel_names) 

199 object.__setattr__(self, "tensor_names", tensor_names) 

200 object.__setattr__(self, "roles", roles or tuple(sorted(_VALID_ROLES))) 

201 object.__setattr__(self, "directions", directions or tuple(sorted(_VALID_DIRECTIONS))) 

202 object.__setattr__(self, "ranks", _normalize_ranks(self.ranks)) 

203 object.__setattr__(self, "stages", stages) 

204 object.__setattr__(self, "task_ids", task_ids) 

205 object.__setattr__(self, "stage_task_indices", stage_task_indices) 

206 object.__setattr__(self, "owner_ids", owner_ids) 

207 object.__setattr__(self, "core_types", core_types) 

208 object.__setattr__(self, "core_ids", core_ids) 

209 object.__setattr__(self, "io", io or tuple(sorted(_VALID_IO))) 

210 

211 def selects_invocation(self, direction: str, rank: int, kernel_name: str | None = None) -> bool: 

212 """Return whether one invocation passes kernel, direction and rank filters. 

213 

214 Args: 

215 direction: Forward or backward invocation direction. 

216 rank: Job-global distributed rank used for selection and export. 

217 kernel_name: Stable diagnostic name of the invoked kernel. 

218 """ 

219 if self.kernel_names is not None and kernel_name not in self.kernel_names: 

220 return False 

221 if direction not in self.directions: 

222 return False 

223 return self.ranks is None or rank in self.ranks 

224 

225 def selects_tensor(self, tensor_name: str, role: str) -> bool: 

226 """Return whether one logical Tensor passes L0 name and role filters. 

227 

228 Args: 

229 tensor_name: Logical tensor name recorded by the graph. 

230 role: Graph-derived external input, stage output or final output role. 

231 """ 

232 if role not in self.roles: 

233 return False 

234 return self.tensor_names is None or tensor_name in self.tensor_names 

235 

236 

237@dataclass 

238class _DumpInvocation: 

239 """A completed invocation retained until its dump window is finalized.""" 

240 

241 invocation_id: int 

242 step: int 

243 kernel_name: str 

244 direction: str 

245 rank: int 

246 device: str 

247 level: str 

248 snapshots: list[_TensorSnapshot] 

249 l1_snapshots: list[_L1Snapshot] 

250 device_buffer_bytes: int = 0 

251 

252 

253class _TensorDumpCall(DiagnosticCall): 

254 """Capture handle surrounding one native MegaKernel launch.""" 

255 

256 def __init__( 

257 self, 

258 on_complete: Callable[[_DumpInvocation], None] | None = None, 

259 invocation: _DumpInvocation | None = None, 

260 graph_tensors: tuple[_GraphTensor, ...] = (), 

261 parameters: Sequence[Any] = (), 

262 prepared_l1: _PreparedL1Dump | None = None, 

263 *, 

264 arguments: LaunchArguments, 

265 ) -> None: 

266 """Store complete launch arguments and L0 or L1 capture state.""" 

267 super().__init__(arguments) 

268 self._on_complete = on_complete 

269 self._invocation = invocation 

270 self._graph_tensors = graph_tensors 

271 self._parameters = parameters 

272 self._prepared_l1 = prepared_l1 

273 self.finished = False 

274 

275 def complete(self) -> None: 

276 """Capture post-launch values after a successful native launch.""" 

277 if self.finished: 

278 return 

279 if self._on_complete is not None and self._invocation is not None: 

280 if self._prepared_l1 is not None: 

281 self._invocation.l1_snapshots.extend(_decode_l1_dump(self._prepared_l1)) 

282 else: 

283 for graph_tensor in self._graph_tensors: 

284 if graph_tensor.role != "external_input": 

285 self._invocation.snapshots.append( 

286 _snapshot_tensor(self._parameters[graph_tensor.param_position], graph_tensor, "after") 

287 ) 

288 self._on_complete(self._invocation) 

289 self.finished = True 

290 

291 def cancel(self) -> None: 

292 """Discard retained snapshots when the native launch fails.""" 

293 if self.finished: 

294 return 

295 if self._invocation is not None: 

296 self._invocation.snapshots.clear() 

297 self._invocation.l1_snapshots.clear() 

298 self._prepared_l1 = None 

299 self.finished = True 

300 

301 

302class TorchMegaKernelTensorDumper: 

303 """Standalone schedule-driven L0/L1 Tensor dumper for Torch MegaKernels.""" 

304 

305 def __init__( 

306 self, 

307 config: TensorDumpConfig, 

308 *, 

309 schedule: Callable[[int], DiagnosticAction] | None, 

310 on_dump_ready: Callable[[Any], None] | None, 

311 ) -> None: 

312 # pylint: disable=redefined-outer-name 

313 """Validate options and initialize dump window state.""" 

314 if not isinstance(config, TensorDumpConfig): 

315 raise TypeError(f"config must be TensorDumpConfig, got {type(config).__name__}") 

316 if schedule is not None and not callable(schedule): 

317 raise TypeError("schedule must be callable or None") 

318 if on_dump_ready is not None and not callable(on_dump_ready): 

319 raise TypeError("on_dump_ready must be callable or None") 

320 self.config = config 

321 self._schedule = schedule 

322 self._on_dump_ready = on_dump_ready 

323 self._step = 0 

324 self._action = self._action_for_step(0) 

325 self._entered = False 

326 self._next_invocation_id = 0 

327 self._window_index = 0 

328 self._invocations: list[_DumpInvocation] = [] 

329 self._last_manifest_paths: tuple[Path, ...] = () 

330 

331 @property 

332 def step_num(self) -> int: 

333 """Return the zero-based user step currently selected.""" 

334 return self._step 

335 

336 @property 

337 def current_action(self) -> DiagnosticAction: 

338 """Return the current shared diagnostic schedule action.""" 

339 return self._action 

340 

341 @property 

342 def last_manifest_paths(self) -> tuple[Path, ...]: 

343 """Return manifest paths from the most recently finalized window.""" 

344 return self._last_manifest_paths 

345 

346 def __enter__(self) -> TorchMegaKernelTensorDumper: 

347 """Activate this dumper as the process-local diagnostic session.""" 

348 _activate_session("tensor_dump", self) 

349 self._entered = True 

350 return self 

351 

352 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: 

353 """Finalize successful snapshots or discard them after an exception.""" 

354 try: 

355 if exc_type is None and self._invocations: 

356 self._finalize_window() 

357 elif exc_type is not None: 

358 self._invocations.clear() 

359 finally: 

360 _deactivate_session(self) 

361 self._entered = False 

362 

363 def step(self) -> None: 

364 """Finish the current user step and advance the dump schedule.""" 

365 if not self._entered: 

366 raise RuntimeError("dumper.step() must be called inside its context") 

367 next_action = self._action_for_step(self._step + 1) 

368 if self._action is DiagnosticAction.RECORD_AND_SAVE: 

369 self._finalize_window() 

370 elif self._action is DiagnosticAction.RECORD and next_action not in ( 

371 DiagnosticAction.RECORD, 

372 DiagnosticAction.RECORD_AND_SAVE, 

373 ): 

374 self._finalize_window() 

375 self._step += 1 

376 self._action = next_action 

377 

378 def _action_for_step(self, step: int) -> DiagnosticAction: 

379 if self._schedule is None: 

380 return DiagnosticAction.RECORD 

381 action = self._schedule(step) 

382 if not isinstance(action, DiagnosticAction): 

383 raise TypeError(f"MegaKernel schedule must return DiagnosticAction, got {type(action).__name__}") 

384 return action 

385 

386 def is_capture_enabled(self) -> bool: 

387 """Return whether this step collects Tensor values.""" 

388 return self._action in (DiagnosticAction.RECORD, DiagnosticAction.RECORD_AND_SAVE) 

389 

390 def prepare_call(self, context: LaunchContext) -> DiagnosticCall: 

391 """Prepare only this dumper's selected invocation, with complete arguments. 

392 

393 Args: 

394 context: Selected invocation or operand context supplied by the diagnostic framework. 

395 """ 

396 runtime = context.runtime 

397 if not self.is_capture_enabled() or not self.config.selects_invocation( 

398 context.direction, runtime.rank, runtime.kernel_name, 

399 ): 

400 return DiagnosticCall(context.ordinary_arguments()) 

401 # Graph/owner work is lazy and never part of ordinary MegaKernel execution. 

402 _ = runtime.metadata 

403 _stage_names(runtime.graph.topological_sort()) 

404 return self._acquire_call( 

405 runtime.graph, context.parameters, direction=context.direction, rank=runtime.rank, 

406 kernel_name=runtime.kernel_name, device=runtime.normal_tensor.device, 

407 runtime=runtime, event_counters=context.fallback_event_counters, 

408 ) 

409 

410 def _acquire_call( 

411 self, 

412 graph: Any, 

413 parameters: Sequence[Any], 

414 *, 

415 direction: str, 

416 rank: int, 

417 kernel_name: str, 

418 device: Any, 

419 runtime: Any, 

420 event_counters: Any, 

421 ) -> DiagnosticCall: 

422 """Plan a selected invocation and retain independent input snapshots before launch.""" 

423 arguments = LaunchArguments( 

424 runtime.normal_tensor, event_counters, 

425 runtime.normal_tensor if event_counters is None else event_counters, event_counters, 

426 ) 

427 if self.config.level == "L1": 

428 prepared_l1 = _prepare_l1_dump( 

429 runtime, 

430 graph, 

431 parameters, 

432 self.config, 

433 rank=rank, 

434 device=device, 

435 ) 

436 invocation = _DumpInvocation( 

437 invocation_id=self._next_invocation_id, 

438 step=self._step, 

439 direction=direction, 

440 kernel_name=kernel_name, 

441 rank=rank, 

442 device=str(device), 

443 level=self.config.level, 

444 snapshots=[], 

445 l1_snapshots=[], 

446 device_buffer_bytes=prepared_l1.total_bytes, 

447 ) 

448 self._next_invocation_id += 1 

449 arguments.runtime_config = prepared_l1.runtime_config 

450 arguments.diagnostic_buffer = prepared_l1.buffer 

451 return _TensorDumpCall(self._invocations.append, invocation, prepared_l1=prepared_l1, arguments=arguments) 

452 

453 graph_tensors = tuple( 

454 tensor 

455 for tensor in _describe_graph_tensors(graph) 

456 if self.config.selects_tensor(tensor.name, tensor.role) 

457 ) 

458 if not graph_tensors: 

459 return DiagnosticCall(arguments) 

460 max_position = max(tensor.param_position for tensor in graph_tensors) 

461 if max_position >= len(parameters): 

462 raise ValueError( 

463 f"MegaKernel dump requires param_position {max_position}, but only {len(parameters)} parameters exist" 

464 ) 

465 invocation = _DumpInvocation( 

466 invocation_id=self._next_invocation_id, 

467 step=self._step, 

468 direction=direction, 

469 kernel_name=kernel_name, 

470 rank=rank, 

471 device=str(device), 

472 level=self.config.level, 

473 snapshots=[], 

474 l1_snapshots=[], 

475 ) 

476 self._next_invocation_id += 1 

477 for graph_tensor in graph_tensors: 

478 if graph_tensor.role == "external_input": 

479 invocation.snapshots.append( 

480 _snapshot_tensor(parameters[graph_tensor.param_position], graph_tensor, "before") 

481 ) 

482 return _TensorDumpCall(self._invocations.append, invocation, graph_tensors, parameters, arguments=arguments) 

483 

484 def _finalize_window(self) -> None: 

485 """Export completed invocations by rank and release the window's CPU snapshots.""" 

486 if not self._invocations: 

487 self._last_manifest_paths = () 

488 self._window_index += 1 

489 return 

490 manifest_paths = [] 

491 ranks = sorted({invocation.rank for invocation in self._invocations}) 

492 for rank in ranks: 

493 rank_invocations = [invocation for invocation in self._invocations if invocation.rank == rank] 

494 manifest_paths.append(self._write_rank_window(rank, rank_invocations)) 

495 self._last_manifest_paths = tuple(manifest_paths) 

496 self._invocations.clear() 

497 self._window_index += 1 

498 if self._on_dump_ready is not None: 

499 self._on_dump_ready(self) 

500 

501 def _write_l0_invocation( 

502 self, 

503 invocation: _DumpInvocation, 

504 invocation_dir: Path, 

505 window_dir: Path, 

506 ) -> dict[str, Any]: 

507 """Write logical tensor snapshots and their stage-port metadata.""" 

508 tensor_metadata = [] 

509 for snapshot in invocation.snapshots: 

510 safe_name = _SAFE_FILE_NAME.sub("_", snapshot.graph_tensor.name) 

511 output_path, port_metadata = _write_l0_stage_files( 

512 snapshot, 

513 invocation_dir, 

514 window_dir, 

515 safe_name, 

516 ) 

517 tensor_metadata.append( 

518 { 

519 "name": snapshot.graph_tensor.name, 

520 "paramPosition": snapshot.graph_tensor.param_position, 

521 "role": snapshot.graph_tensor.role, 

522 "capturePhase": snapshot.phase, 

523 "producers": list(snapshot.graph_tensor.producers), 

524 "consumers": list(snapshot.graph_tensor.consumers), 

525 "ports": port_metadata, 

526 "declaredShape": list(snapshot.graph_tensor.declared_shape), 

527 "actualShape": list(snapshot.shape), 

528 "originalDtype": snapshot.original_dtype, 

529 "storedDtype": snapshot.stored_dtype, 

530 "file": output_path.relative_to(window_dir).as_posix(), 

531 } 

532 ) 

533 return { 

534 "invocationId": invocation.invocation_id, 

535 "step": invocation.step, 

536 "direction": invocation.direction, 

537 "kernelName": invocation.kernel_name, 

538 "device": invocation.device, 

539 "tensorCount": len(tensor_metadata), 

540 "tensors": tensor_metadata, 

541 } 

542 

543 def _write_l1_invocation( 

544 self, 

545 invocation: _DumpInvocation, 

546 invocation_dir: Path, 

547 window_dir: Path, 

548 ) -> dict[str, Any]: 

549 """Write task-local tensor snapshots with their native copy metadata.""" 

550 snapshots_by_task: dict[tuple[int, str, int], list[_L1Snapshot]] = {} 

551 for snapshot in invocation.l1_snapshots: 

552 record = snapshot.record 

553 snapshots_by_task.setdefault((record.task_id, record.core_type, record.core_id), []).append(snapshot) 

554 

555 task_metadata = [] 

556 for key in sorted(snapshots_by_task): 

557 task_id = key[0] 

558 snapshots = snapshots_by_task[key] 

559 first = snapshots[0].record 

560 safe_stage = _SAFE_FILE_NAME.sub("_", first.stage) 

561 task_dir = invocation_dir / "stages" / safe_stage / f"task_{task_id:06d}" 

562 if first.worker_id != 0xFFFFFFFF: 

563 task_dir = task_dir.with_name(f"{task_dir.name}_worker_{first.core_id:03d}") 

564 task_dir.mkdir(parents=True, exist_ok=True) 

565 tensor_metadata = [] 

566 for snapshot in snapshots: 

567 record = snapshot.record 

568 safe_name = _SAFE_FILE_NAME.sub("_", record.tensor_name) 

569 file_name = f"{record.io}_{record.index:02d}_{safe_name}.pt" 

570 output_path = task_dir / file_name 

571 torch.save(snapshot.data, output_path) 

572 tensor_metadata.append( 

573 { 

574 "name": record.tensor_name, 

575 "paramPosition": record.parameter_position, 

576 "io": record.io, 

577 "index": record.index, 

578 "capturePhase": record.phase, 

579 "sourceOffsetBytes": record.source_offset_bytes, 

580 "actualShape": list(record.actual_shape), 

581 "storedShape": list(snapshot.stored_shape), 

582 "originalDtype": snapshot.original_dtype, 

583 "storedDtype": snapshot.stored_dtype, 

584 "fullBytes": record.full_bytes, 

585 "storedBytes": record.payload_bytes, 

586 "truncated": record.truncated, 

587 "note": record.note, 

588 "file": output_path.relative_to(window_dir).as_posix(), 

589 } 

590 ) 

591 metadata_path = task_dir / "task.json" 

592 metadata = { 

593 "taskId": task_id, 

594 "stageTaskIndex": first.task_index, 

595 "stage": first.stage, 

596 "owner": {"label": first.owner_label, "id": first.owner_id}, 

597 "coreType": first.core_type, 

598 "coreId": first.core_id, 

599 "tensorCount": len(tensor_metadata), 

600 "tensors": tensor_metadata, 

601 } 

602 metadata_path.write_text( 

603 json.dumps(metadata, ensure_ascii=False, indent=2), 

604 encoding="utf-8", 

605 ) 

606 metadata["metadataFile"] = metadata_path.relative_to(window_dir).as_posix() 

607 task_metadata.append(metadata) 

608 return { 

609 "invocationId": invocation.invocation_id, 

610 "step": invocation.step, 

611 "direction": invocation.direction, 

612 "kernelName": invocation.kernel_name, 

613 "device": invocation.device, 

614 "deviceBufferBytes": invocation.device_buffer_bytes, 

615 "taskCount": len(task_metadata), 

616 "tasks": task_metadata, 

617 } 

618 

619 def _write_rank_window(self, rank: int, invocations: list[_DumpInvocation]) -> Path: 

620 """Export one rank's window without overwriting an existing directory.""" 

621 window_dir = self.config.output_dir / f"rank_{rank}" / f"window_{self._window_index:04d}" 

622 if window_dir.exists(): 

623 raise FileExistsError(f"Tensor dump window already exists: {window_dir}") 

624 window_dir.mkdir(parents=True) 

625 invocation_metadata = [] 

626 for invocation in invocations: 

627 invocation_dir = window_dir / f"invocation_{invocation.invocation_id:06d}_{invocation.direction}" 

628 if invocation.level == "L1": 

629 metadata = self._write_l1_invocation(invocation, invocation_dir, window_dir) 

630 else: 

631 metadata = self._write_l0_invocation(invocation, invocation_dir, window_dir) 

632 invocation_metadata.append(metadata) 

633 is_l1 = self.config.level == "L1" 

634 manifest = { 

635 "schemaVersion": 4, 

636 "level": self.config.level, 

637 "tensorSerialization": "torch.save", 

638 "tensorFileLayout": "stage_only", 

639 "windowIndex": self._window_index, 

640 "capturePhaseSemantics": "task_boundary" if is_l1 else "mega_kernel_boundary", 

641 "stageViewStorage": "per_task_files" if is_l1 else "primary_file_with_hard_links", 

642 "rank": rank, 

643 "invocationCount": len(invocation_metadata), 

644 "invocations": invocation_metadata, 

645 } 

646 manifest_path = window_dir / "manifest.json" 

647 manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8") 

648 return manifest_path 

649 

650 

651def tensor_dump( 

652 config: TensorDumpConfig, 

653 *, 

654 schedule: Callable[[int], DiagnosticAction] | None = None, 

655 on_dump_ready: Callable[[Any], None] | None = None, 

656) -> TorchMegaKernelTensorDumper: 

657 # pylint: disable=redefined-outer-name 

658 """Create the standalone tensor MegaKernel Tensor dumper. 

659 

660 Args: 

661 config: Output and logical Tensor selection configuration. 

662 schedule: Optional shared diagnostic step schedule. Warmup actions do 

663 not copy Tensor values because L0 collection needs no warmup. 

664 on_dump_ready: Optional callback invoked after a window is exported. 

665 

666 Returns: 

667 tensor MegaKernel Tensor dump context manager. 

668 

669 Note: 

670 Tensor dumping synchronizes Device-to-Host copies and changes execution 

671 timing. It is mutually exclusive with MegaKernel cycle profiling. 

672 """ 

673 return TorchMegaKernelTensorDumper( 

674 config, 

675 schedule=schedule, 

676 on_dump_ready=on_dump_ready, 

677 )