Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_multicore / diagnostics / profiler / cycle_trace.py: 81%

372 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"""Plan and decode TaskDesc-based MegaKernel per-core cycle records.""" 

16 

17from __future__ import annotations 

18 

19__all__ = [] 

20 

21from collections.abc import Mapping 

22from dataclasses import dataclass 

23from enum import Enum 

24import struct 

25from typing import Any 

26 

27from hyper_multicore.scheduler.config import ( 

28 EVENT_INVALID_ID, 

29 INVALID_PROFILE_DESC_ID, 

30 INVALID_PROFILE_OWNER_ID, 

31 MIN_CUBE_WORKER_CAPACITY, 

32 RuntimeConfigC, 

33 TaskDescC, 

34 TaskType, 

35) 

36from hyper_multicore.diagnostics.common.graph import _scheduled_task_ids 

37from hyper_multicore.diagnostics.common.runtime import DiagnosticMode, PreparedDiagnosticRuntime 

38from hyper_multicore.scheduler.runtime import runtime_cube_worker_capacity 

39 

40 

41# Minimum AIC/AIV profile slots. 

42CUBE_SLOT_COUNT = MIN_CUBE_WORKER_CAPACITY 

43VECTOR_SLOT_COUNT = 2 * CUBE_SLOT_COUNT 

44CORE_SLOT_COUNT = CUBE_SLOT_COUNT + VECTOR_SLOT_COUNT 

45RECORD_CAPACITY_ALIGNMENT = 16 

46CORE_HEADER = struct.Struct("<QIIIIII32x") 

47PROFILE_RECORD = struct.Struct("<QQIIII") 

48# Keep each slot representable by the Device ABI; allocate only the graph's actual requirement. 

49MAX_RECORDS_PER_CORE = ((0xFFFFFFFF - CORE_HEADER.size) // PROFILE_RECORD.size 

50 // RECORD_CAPACITY_ALIGNMENT * RECORD_CAPACITY_ALIGNMENT) 

51MAX_CORE_STRIDE_BYTES = CORE_HEADER.size + MAX_RECORDS_PER_CORE * PROFILE_RECORD.size 

52MAX_PROFILE_BUFFER_BYTES = CORE_SLOT_COUNT * MAX_CORE_STRIDE_BYTES 

53INVALID_OWNER_ID = 0xFFFFFFFF 

54TASK_TYPE_DESC_BASE = 0x20000 

55GRAPH_STAGE_DESC_BASE = 0x30000 

56# GetSystemCycle uses the fixed system counter, not the variable AI Core clock. 

57_SYSTEM_COUNTER_FREQUENCY_MHZ = { 

58 "ascend910b": 50.0, 

59 "ascend910c": 50.0, 

60 "ascend910_93": 50.0, 

61 "ascend950": 1000.0, 

62} 

63 

64DEFAULT_STAGE_NAMES = { 

65 0x10000: "WaitDependency", 

66 0x10006: "TerminateTask", 

67 0x10007: "TriggerEvent", 

68} 

69TASK_TYPE_NAMES = { 

70 0: "Terminate", 

71 10: "BeginTaskGraph", 

72 101: "AddCustom", 

73 102: "SwiGLU", 

74 103: "MatMul", 

75 104: "GroupedMatMul", 

76 105: "ShmemPutMemSignal", 

77 106: "SwiGLUGrad", 

78 107: "MhcPost", 

79 108: "MhcNormCast", 

80 109: "MhcProjection", 

81 110: "MhcMapping", 

82 111: "RmsNorm", 

83 112: "MhcInputMix", 

84 113: "RmsNormGrad", 

85 114: "MhcGradPrevA", 

86 115: "MhcGradMapping", 

87 116: "MhcGradPhiRms", 

88 117: "MhcGradPrevXAndPost", 

89 118: "MhcPostGrad", 

90 119: "MhcGradPrevAAndMapping", 

91 141: "ShmemGetMem", 

92} 

93CORE_TYPE_NAMES = { 

94 1: "AIC", 

95 2: "AIV", 

96} 

97 

98 

99class _TaskExecutionMode(Enum): 

100 """Describe how Device workers execute scheduled task descriptors. 

101 

102 ``EVENT_DRIVEN`` assigns a prebuilt task graph across AIC and AIV worker 

103 queues. Device events enforce graph dependencies, allowing independent 

104 Cube and Vector tasks to overlap. 

105 

106 ``AIV_PIPELINE`` makes every active AIV worker execute the same ordered 

107 stage sequence on its own data shard without scheduler events. 

108 """ 

109 

110 EVENT_DRIVEN = "event_driven" 

111 AIV_PIPELINE = "aiv_pipeline" 

112 

113 

114@dataclass(frozen=True) 

115class _ProfileMetadata: 

116 """Host-only display metadata selected by a concrete MegaKernel builder.""" 

117 

118 kernel_name: str 

119 owner_label: str 

120 stage_names: Mapping[int, str] 

121 task_stage_names: Mapping[int, str] 

122 execution_mode: _TaskExecutionMode 

123 vector_worker_stride: int = 2 

124 

125 

126@dataclass(frozen=True) 

127class _ProfileLayout: 

128 """Host-computed per-worker record requirements and bounded buffer layout.""" 

129 

130 aic_required_records: int 

131 aiv_required_records: int 

132 aic_record_capacity: int 

133 aiv_record_capacity: int 

134 cube_slot_count: int = CUBE_SLOT_COUNT 

135 

136 @property 

137 def buffer_size(self) -> int: 

138 """Return the ordinary Device buffer size for per-core cycle records.""" 

139 return _profile_buffer_bytes_for_capacities( 

140 self.aic_record_capacity, 

141 self.aiv_record_capacity, 

142 self.cube_slot_count, 

143 ) 

144 

145 

146@dataclass(frozen=True) 

147class _CycleTraceConfig: 

148 """Resolved hardware and display metadata used while decoding one buffer.""" 

149 

150 rank: int 

151 device_id: int | None 

152 cycle_frequency_mhz: float 

153 detailed_task_names: bool 

154 kernel_name: str 

155 owner_label: str 

156 stage_names: Mapping[int, str] 

157 soc_name: str 

158 task_stage_names: Mapping[int, str] | None = None 

159 cube_slot_count: int = CUBE_SLOT_COUNT 

160 

161 

162def _get_soc_name(device_id: int | None) -> str: 

163 """Return and validate the current Ascend SoC name through Torch NPU.""" 

164 # Metadata construction and CPU/meta modules must not import an optional backend. 

165 import torch_npu # pylint: disable=import-outside-toplevel 

166 

167 get_device_name = torch_npu.npu.get_device_name 

168 try: 

169 soc_name = get_device_name(device_id) 

170 except TypeError: 

171 # Older torch_npu releases only expose the current-device form. 

172 soc_name = get_device_name() 

173 if not isinstance(soc_name, str) or not soc_name.strip(): 

174 raise RuntimeError(f"Torch NPU returned an invalid Ascend SoC name: {soc_name!r}") 

175 return soc_name.strip() 

176 

177 

178def _resolve_cycle_frequency_mhz(soc_name: str) -> float: 

179 """Resolve the GetSystemCycle frequency for a backend-reported SoC name.""" 

180 normalized_name = "".join(character for character in soc_name.lower() if character.isalnum() or character == "_") 

181 if not normalized_name.startswith("ascend"): 

182 normalized_name = f"ascend{normalized_name}" 

183 for soc_prefix, frequency_mhz in _SYSTEM_COUNTER_FREQUENCY_MHZ.items(): 

184 if normalized_name.startswith(soc_prefix): 

185 return frequency_mhz 

186 supported_soc_names = ", ".join(sorted(_SYSTEM_COUNTER_FREQUENCY_MHZ)) 

187 raise RuntimeError( 

188 f"Unsupported Ascend SoC for MegaKernel profiling: {soc_name!r}; " 

189 f"supported SoC prefixes: {supported_soc_names}" 

190 ) 

191 

192 

193def _validate_runtime_config(runtime_config: RuntimeConfigC) -> None: 

194 if not isinstance(runtime_config, RuntimeConfigC): 

195 raise TypeError(f"runtime_config must be RuntimeConfigC, got {type(runtime_config).__name__}") 

196 

197 

198def _profile_buffer_bytes_for_capacities( 

199 aic_record_capacity: int, aiv_record_capacity: int, cube_slot_count: int = CUBE_SLOT_COUNT, 

200) -> int: 

201 aic_stride = CORE_HEADER.size + aic_record_capacity * PROFILE_RECORD.size 

202 aiv_stride = CORE_HEADER.size + aiv_record_capacity * PROFILE_RECORD.size 

203 return cube_slot_count * aic_stride + 2 * cube_slot_count * aiv_stride 

204 

205 

206def _round_up_record_capacity(required_records: int) -> int: 

207 """Round the full per-worker requirement without silently dropping records.""" 

208 aligned_capacity = ( 

209 (required_records + RECORD_CAPACITY_ALIGNMENT - 1) // RECORD_CAPACITY_ALIGNMENT * RECORD_CAPACITY_ALIGNMENT 

210 ) 

211 _validate_record_capacity(aligned_capacity, "per-worker") 

212 return aligned_capacity 

213 

214 

215def _records_for_task(task_desc: TaskDescC) -> int: 

216 """Mirror the wait, compute, and trigger records emitted by ExecuteTaskProfiled().""" 

217 record_count = 1 

218 if task_desc.dependent_event != EVENT_INVALID_ID: 

219 record_count += 1 

220 if task_desc.task_type != TaskType.TASK_SHMEM_PUT_MEM_SIGNAL: 

221 record_count += 1 

222 return record_count 

223 

224 

225def _max_worker_record_count( 

226 runtime_config: RuntimeConfigC, 

227 task_indices: Any, 

228 scheduled_task_count: int, 

229 active_worker_count: int, 

230) -> int: 

231 """Return the largest record count assigned to one active worker.""" 

232 if scheduled_task_count < 0 or scheduled_task_count > len(task_indices): 

233 raise ValueError(f"scheduled task count is outside RuntimeConfig capacity: {scheduled_task_count}") 

234 if scheduled_task_count == 0: 

235 return 0 

236 if active_worker_count <= 0: 

237 raise ValueError("RuntimeConfig.num_workers must identify at least one AIC/AIV worker") 

238 

239 worker_record_counts = [0] * active_worker_count 

240 for schedule_index in range(scheduled_task_count): 

241 task_id = int(task_indices[schedule_index]) 

242 if not 0 <= task_id < len(runtime_config.all_tasks): 

243 raise ValueError(f"scheduled task ID is outside RuntimeConfig capacity: {task_id}") 

244 worker_id = schedule_index % active_worker_count 

245 worker_record_counts[worker_id] += _records_for_task(runtime_config.all_tasks[task_id]) 

246 return max(worker_record_counts) 

247 

248 

249def _calculate_aiv_pipeline_profile_layout( 

250 runtime_config: RuntimeConfigC, 

251) -> _ProfileLayout: 

252 """Size AIV slots for workers that each execute the complete stage sequence.""" 

253 worker_capacity = int(runtime_config.num_workers) 

254 if worker_capacity <= 0: 

255 raise ValueError(f"AIV pipeline worker capacity must be positive, got {worker_capacity}") 

256 scheduled_task_count = int(runtime_config.task_index_num[1]) 

257 if not 0 <= scheduled_task_count <= len(runtime_config.vector_task_indices): 

258 raise ValueError(f"scheduled task count is outside RuntimeConfig capacity: {scheduled_task_count}") 

259 required_records = 0 

260 for schedule_index in range(scheduled_task_count): 

261 task_id = int(runtime_config.vector_task_indices[schedule_index]) 

262 if not 0 <= task_id < len(runtime_config.all_tasks): 

263 raise ValueError(f"scheduled task ID is outside RuntimeConfig capacity: {task_id}") 

264 task = runtime_config.all_tasks[task_id] 

265 if task.dependent_event != EVENT_INVALID_ID or task.trigger_event != EVENT_INVALID_ID: 

266 raise ValueError("AIV pipelines cannot contain scheduler events") 

267 required_records += 1 

268 return _ProfileLayout( 

269 aic_required_records=0, 

270 aiv_required_records=required_records, 

271 aic_record_capacity=0, 

272 aiv_record_capacity=_round_up_record_capacity(required_records), 

273 cube_slot_count=runtime_cube_worker_capacity(worker_capacity), 

274 ) 

275 

276 

277def _calculate_event_driven_profile_layout( 

278 runtime_config: RuntimeConfigC, vector_worker_stride: int = 2, 

279) -> _ProfileLayout: 

280 """Size AIC/AIV slots for event-driven task queues using the busiest worker.""" 

281 num_workers = int(runtime_config.num_workers) 

282 if num_workers < 0 or num_workers % 2 != 0: 

283 raise ValueError(f"RuntimeConfig.num_workers must be a nonnegative even value, got {num_workers}") 

284 active_worker_count = num_workers // 2 

285 aic_required_records = _max_worker_record_count( 

286 runtime_config, 

287 runtime_config.cube_task_indices, 

288 int(runtime_config.task_index_num[0]), 

289 active_worker_count, 

290 ) 

291 aiv_required_records = _max_worker_record_count( 

292 runtime_config, 

293 runtime_config.vector_task_indices, 

294 int(runtime_config.task_index_num[1]), 

295 num_workers // vector_worker_stride, 

296 ) 

297 return _ProfileLayout( 

298 aic_required_records=aic_required_records, 

299 aiv_required_records=aiv_required_records, 

300 aic_record_capacity=_round_up_record_capacity(aic_required_records), 

301 aiv_record_capacity=_round_up_record_capacity(aiv_required_records), 

302 cube_slot_count=runtime_cube_worker_capacity(num_workers), 

303 ) 

304 

305 

306def _calculate_profile_layout( 

307 runtime_config: RuntimeConfigC, metadata: _ProfileMetadata | None = None, 

308) -> _ProfileLayout: 

309 """Select the profile layout algorithm for the configured task execution mode.""" 

310 _validate_runtime_config(runtime_config) 

311 if metadata is None: 

312 metadata = _ProfileMetadata("MegaKernel", "Owner", {}, {}, _TaskExecutionMode.EVENT_DRIVEN) 

313 if metadata.execution_mode is _TaskExecutionMode.AIV_PIPELINE: 

314 return _calculate_aiv_pipeline_profile_layout(runtime_config) 

315 if metadata.execution_mode is _TaskExecutionMode.EVENT_DRIVEN: 

316 return _calculate_event_driven_profile_layout(runtime_config, metadata.vector_worker_stride) 

317 raise ValueError(f"unsupported task execution mode: {metadata.execution_mode!r}") 

318 

319 

320def _configure_profile_layout(runtime_config: RuntimeConfigC, metadata: _ProfileMetadata) -> _ProfileLayout: 

321 """Calculate and serialize the bounded AIC/AIV record capacities.""" 

322 layout = _calculate_profile_layout(runtime_config, metadata) 

323 runtime_config.aic_profile_record_capacity = layout.aic_record_capacity 

324 runtime_config.aiv_profile_record_capacity = layout.aiv_record_capacity 

325 return layout 

326 

327 

328def _resolve_stage_names(stage_names: Mapping[int, str] | None) -> dict[int, str]: 

329 """Merge validated kernel-specific names with the common runtime stages.""" 

330 resolved_stage_names = dict(DEFAULT_STAGE_NAMES) 

331 if stage_names is None: 

332 return resolved_stage_names 

333 for desc_id, stage_name in stage_names.items(): 

334 if isinstance(desc_id, bool) or not isinstance(desc_id, int): 

335 raise TypeError(f"stage_names keys must be integers, got {desc_id!r}") 

336 if not isinstance(stage_name, str) or not stage_name.strip(): 

337 raise ValueError(f"stage_names values must be non-empty strings, got {stage_name!r}") 

338 resolved_stage_names[desc_id] = stage_name 

339 return resolved_stage_names 

340 

341 

342def _configure_trace_metadata(runtime_config: RuntimeConfigC, metadata: Any) -> _ProfileMetadata: 

343 """Encode trace-specific descriptor IDs in an independent profiling configuration.""" 

344 stages = dict(DEFAULT_STAGE_NAMES) 

345 stages.update({GRAPH_STAGE_DESC_BASE + key: name for key, name in metadata.stage_names.items()}) 

346 task_names = dict(metadata.task_stage_names) 

347 for task_id in set(range(int(runtime_config.task_num))) | _scheduled_task_ids(runtime_config): 

348 task = runtime_config.all_tasks[task_id] 

349 stage_id = metadata.task_stage_ids.get(task_id) 

350 if stage_id is not None: 

351 task.profile_desc_id = GRAPH_STAGE_DESC_BASE + stage_id 

352 owner = metadata.task_owner_ids.get(task_id) 

353 task.profile_owner_id = INVALID_PROFILE_OWNER_ID if owner is None else owner 

354 elif task.task_type == TaskType.TASK_TERMINATE: 

355 task.profile_desc_id = 0x10006 

356 task.profile_owner_id = INVALID_PROFILE_OWNER_ID 

357 task_names[task_id] = "TerminateTask" 

358 else: 

359 task.profile_desc_id = INVALID_PROFILE_DESC_ID 

360 task.profile_owner_id = INVALID_PROFILE_OWNER_ID 

361 return _ProfileMetadata(metadata.kernel_name, metadata.owner_label, stages, task_names, 

362 _TaskExecutionMode.EVENT_DRIVEN) 

363 

364 

365class _PreparedProfileRuntime: 

366 """Own profiling-specific preparation and trace decoding for one runtime.""" 

367 

368 def __init__(self, runtime: PreparedDiagnosticRuntime) -> None: 

369 """Build only profiling-owned metadata, layout and Device configuration.""" 

370 runtime_config = runtime.copy_host_config() 

371 self._metadata = _configure_trace_metadata(runtime_config, runtime.metadata) 

372 self._metadata = _ProfileMetadata( 

373 self._metadata.kernel_name, self._metadata.owner_label, self._metadata.stage_names, 

374 self._metadata.task_stage_names, _TaskExecutionMode(runtime.spec.execution_mode), 

375 runtime.spec.vector_worker_stride, 

376 ) 

377 self._layout = _configure_profile_layout(runtime_config, self._metadata) 

378 self._rank = runtime.rank 

379 self._device_id = runtime.device_id 

380 self._host_config = runtime_config 

381 self._normal_tensor = runtime.normal_tensor 

382 self._device = getattr(runtime.normal_tensor, "device", None) 

383 runtime_config.diagnostic_mode = DiagnosticMode.PROFILE 

384 self._profile_tensor = runtime.tensor_from_config(runtime_config) 

385 

386 @property 

387 def normal_tensor(self) -> Any: 

388 """Return the original normal image without modifying it.""" 

389 return self._normal_tensor 

390 

391 @property 

392 def profile_tensor(self) -> Any: 

393 """Return the independent profiling configuration.""" 

394 return self._profile_tensor 

395 

396 @property 

397 def buffer_size(self) -> int: 

398 """Return the exact ordinary Device buffer size required while profiling.""" 

399 return self._layout.buffer_size 

400 

401 @property 

402 def rank(self) -> int: 

403 """Return the distributed rank that owns this runtime.""" 

404 return self._rank 

405 

406 @property 

407 def device(self) -> Any: 

408 """Return the device used to allocate runtime-owned tensors.""" 

409 return self._device 

410 

411 @property 

412 def device_id(self) -> int: 

413 """Return the local device index that executes this runtime.""" 

414 return self._device_id 

415 

416 @property 

417 def kernel_name(self) -> str: 

418 """Return the concrete kernel display name.""" 

419 return self._metadata.kernel_name 

420 

421 @property 

422 def host_config(self) -> RuntimeConfigC: 

423 """Return an immutable Host copy retaining the graph-sized runtime arrays.""" 

424 return self._host_config 

425 

426 @property 

427 def task_stage_names(self) -> Mapping[int, str]: 

428 """Return task ID to human-facing stage-name metadata.""" 

429 return self._metadata.task_stage_names 

430 

431 @property 

432 def owner_label(self) -> str: 

433 """Return the concrete kernel's task-owner label.""" 

434 return self._metadata.owner_label 

435 

436 def parse(self, buffer: Any, *, detailed_task_names: bool) -> dict[str, Any]: 

437 """Decode one completed invocation buffer. 

438 

439 Args: 

440 buffer: Host-visible bytes copied from the profiling buffer. 

441 detailed_task_names: Whether event names include owner and task details. 

442 """ 

443 soc_name = _get_soc_name(self._device_id) 

444 trace = _parse_cycle_buffer( 

445 buffer, 

446 _CycleTraceConfig( 

447 rank=self._rank, 

448 device_id=self._device_id, 

449 cycle_frequency_mhz=_resolve_cycle_frequency_mhz(soc_name), 

450 detailed_task_names=detailed_task_names, 

451 kernel_name=self._metadata.kernel_name, 

452 owner_label=self._metadata.owner_label, 

453 stage_names=self._metadata.stage_names, 

454 soc_name=soc_name, 

455 task_stage_names=self._metadata.task_stage_names, 

456 cube_slot_count=self._layout.cube_slot_count, 

457 ), 

458 aic_record_capacity=self._layout.aic_record_capacity, 

459 aiv_record_capacity=self._layout.aiv_record_capacity, 

460 ) 

461 workers = int(self._host_config.num_workers) 

462 pipeline = self._metadata.execution_mode is _TaskExecutionMode.AIV_PIPELINE 

463 trace["megaKernelCycleTrace"]["workerCapacities"] = { 

464 "cubeSlots": self._layout.cube_slot_count, 

465 "vectorSlots": 2 * self._layout.cube_slot_count, 

466 "activeCubeWorkers": 0 if pipeline else workers // 2, 

467 "activeVectorWorkers": workers // self._metadata.vector_worker_stride, 

468 "vectorWorkerStride": self._metadata.vector_worker_stride, 

469 } 

470 return trace 

471 

472 

473def _as_bytes(buffer: Any, cube_slot_count: int = CUBE_SLOT_COUNT) -> bytes: 

474 """Convert an array-like profile buffer to validated immutable bytes.""" 

475 if hasattr(buffer, "tobytes"): 

476 raw_buffer = buffer.tobytes() 

477 else: 

478 raw_buffer = bytes(buffer) 

479 minimum_buffer_bytes = 3 * cube_slot_count * CORE_HEADER.size 

480 if len(raw_buffer) < minimum_buffer_bytes: 

481 raise ValueError( 

482 f"MegaKernel profile buffer is too small: expected at least {minimum_buffer_bytes} bytes, " 

483 f"got {len(raw_buffer)}" 

484 ) 

485 return raw_buffer 

486 

487 

488def _validate_record_capacity(record_capacity: int, core_name: str) -> None: 

489 if record_capacity < 0: 

490 raise ValueError(f"{core_name} profile record capacity must be nonnegative: {record_capacity}") 

491 if record_capacity > MAX_RECORDS_PER_CORE: 

492 raise ValueError( 

493 f"{core_name} profile record capacity exceeds the {MAX_RECORDS_PER_CORE}-record limit: " 

494 f"{record_capacity}" 

495 ) 

496 if record_capacity % RECORD_CAPACITY_ALIGNMENT != 0: 

497 raise ValueError( 

498 f"{core_name} profile record capacity must be a multiple of {RECORD_CAPACITY_ALIGNMENT}: " 

499 f"{record_capacity}" 

500 ) 

501 

502 

503def _profile_layout_from_buffer( 

504 raw_buffer: bytes, cube_slot_count: int = CUBE_SLOT_COUNT, 

505) -> tuple[int, int, int]: 

506 """Read AIC/AIV capacities from the first initialized header of each slot region.""" 

507 aic_header = CORE_HEADER.unpack_from(raw_buffer, 0) 

508 aic_record_capacity = aic_header[5] 

509 _validate_record_capacity(aic_record_capacity, "AIC") 

510 aic_stride = CORE_HEADER.size + aic_record_capacity * PROFILE_RECORD.size 

511 aiv_region_offset = cube_slot_count * aic_stride 

512 if len(raw_buffer) < aiv_region_offset + CORE_HEADER.size: 

513 raise ValueError( 

514 f"MegaKernel profile buffer is too small for the AIV header: " 

515 f"expected at least {aiv_region_offset + CORE_HEADER.size} bytes, got {len(raw_buffer)}" 

516 ) 

517 aiv_header = CORE_HEADER.unpack_from(raw_buffer, aiv_region_offset) 

518 aiv_record_capacity = aiv_header[5] 

519 _validate_record_capacity(aiv_record_capacity, "AIV") 

520 required_buffer_bytes = _profile_buffer_bytes_for_capacities( 

521 aic_record_capacity, 

522 aiv_record_capacity, 

523 cube_slot_count, 

524 ) 

525 if len(raw_buffer) < required_buffer_bytes: 

526 raise ValueError( 

527 f"MegaKernel profile buffer is too small for its declared capacities: " 

528 f"expected at least {required_buffer_bytes} bytes, got {len(raw_buffer)}" 

529 ) 

530 return aic_record_capacity, aiv_record_capacity, required_buffer_bytes 

531 

532 

533def _profile_slot_layouts( 

534 aic_record_capacity: int, aiv_record_capacity: int, cube_slot_count: int = CUBE_SLOT_COUNT, 

535): 

536 aic_stride = CORE_HEADER.size + aic_record_capacity * PROFILE_RECORD.size 

537 aiv_stride = CORE_HEADER.size + aiv_record_capacity * PROFILE_RECORD.size 

538 for block_id in range(cube_slot_count): 

539 yield 1, block_id, block_id * aic_stride, aic_record_capacity 

540 aiv_region_offset = cube_slot_count * aic_stride 

541 for block_id in range(2 * cube_slot_count): 

542 yield 2, block_id, aiv_region_offset + block_id * aiv_stride, aiv_record_capacity 

543 

544 

545def _thread_id(core_type: int, block_id: int) -> int: 

546 return core_type * 1000 + block_id 

547 

548 

549def _event_name( 

550 desc_id: int, 

551 task_id: int, 

552 stage_task_index: int, 

553 owner_id: int, 

554 detailed_task_names: bool, 

555 owner_label: str, 

556 stage_names: Mapping[int, str], 

557 task_stage_names: Mapping[int, str], 

558) -> str: 

559 """Build a stage-only or detailed task name while retaining raw IDs in event args.""" 

560 if desc_id in stage_names: 

561 stage_name = stage_names[desc_id] 

562 elif desc_id >= TASK_TYPE_DESC_BASE: 

563 task_type = desc_id - TASK_TYPE_DESC_BASE 

564 stage_name = f"TaskType_{TASK_TYPE_NAMES.get(task_type, task_type)}" 

565 else: 

566 stage_name = f"DescId_{desc_id}" 

567 task_stage_name = task_stage_names.get(task_id) 

568 if desc_id in (0x10000, 0x10007) and task_stage_name is not None: 

569 stage_name = f"{task_stage_name}_{stage_name}" 

570 if not detailed_task_names: 

571 return stage_name 

572 task_name = f"{stage_name}_task{stage_task_index + 1}" 

573 if owner_id == INVALID_OWNER_ID: 

574 return task_name 

575 return f"{owner_label}{owner_id}_{task_name}" 

576 

577 

578def _resolve_buffer_layout( 

579 raw_buffer: bytes, 

580 aic_record_capacity: int | None, 

581 aiv_record_capacity: int | None, 

582 cube_slot_count: int = CUBE_SLOT_COUNT, 

583) -> tuple[int, int, int]: 

584 """Resolve and validate the two per-core capacities and total buffer size.""" 

585 if aic_record_capacity is None and aiv_record_capacity is None: 

586 return _profile_layout_from_buffer(raw_buffer, cube_slot_count) 

587 if aic_record_capacity is None or aiv_record_capacity is None: 

588 raise ValueError("AIC and AIV profile capacities must be provided together") 

589 _validate_record_capacity(aic_record_capacity, "AIC") 

590 _validate_record_capacity(aiv_record_capacity, "AIV") 

591 required_buffer_bytes = _profile_buffer_bytes_for_capacities( 

592 aic_record_capacity, 

593 aiv_record_capacity, 

594 cube_slot_count, 

595 ) 

596 if len(raw_buffer) < required_buffer_bytes: 

597 raise ValueError( 

598 "MegaKernel profile buffer is smaller than its Host-computed layout: " 

599 f"expected at least {required_buffer_bytes} bytes, got {len(raw_buffer)}" 

600 ) 

601 return aic_record_capacity, aiv_record_capacity, required_buffer_bytes 

602 

603 

604def _validate_profile_slot_header( 

605 slot: int, 

606 core_type: int, 

607 block_id: int, 

608 expected_core_type: int, 

609 expected_block_id: int, 

610 header_capacity: int, 

611 record_capacity: int, 

612 record_count: int, 

613 reserved: int, 

614) -> None: 

615 """Validate the identity, layout, and count fields of one Device slot.""" 

616 if core_type != expected_core_type or block_id != expected_block_id: 

617 raise ValueError( 

618 f"Profile slot identity mismatch: slot={slot}, expected=({expected_core_type}, {expected_block_id}), " 

619 f"got=({core_type}, {block_id})" 

620 ) 

621 if header_capacity != record_capacity: 

622 raise ValueError( 

623 f"Profile slot capacity mismatch: slot={slot}, expected={record_capacity}, got={header_capacity}" 

624 ) 

625 if reserved != 0: 

626 raise ValueError(f"Profile core header reserved field must be zero: slot={slot}, got={reserved}") 

627 if record_count > record_capacity: 

628 raise ValueError( 

629 f"Profile record count exceeds capacity: slot={slot}, count={record_count}, " 

630 f"capacity={record_capacity}" 

631 ) 

632 

633 

634def _decode_slot_records( 

635 raw_buffer: bytes, 

636 slot: int, 

637 slot_offset: int, 

638 record_count: int, 

639 core_type: int, 

640 block_id: int, 

641 entry_cycle: int, 

642) -> list[dict[str, int]]: 

643 """Decode all cycle records stored after one validated core header.""" 

644 records = [] 

645 record_offset = slot_offset + CORE_HEADER.size 

646 for record_index in range(record_count): 

647 record = PROFILE_RECORD.unpack_from(raw_buffer, record_offset + record_index * PROFILE_RECORD.size) 

648 if record[1] < record[0]: 

649 raise ValueError( 

650 f"Profile cycle interval is negative: slot={slot}, record={record_index}, " 

651 f"start={record[0]}, end={record[1]}" 

652 ) 

653 records.append( 

654 { 

655 "start_cycle": record[0], 

656 "end_cycle": record[1], 

657 "desc_id": record[2], 

658 "task_id": record[3], 

659 "stage_task_index": record[4], 

660 "owner_id": record[5], 

661 "core_type": core_type, 

662 "block_id": block_id, 

663 "entry_cycle": entry_cycle, 

664 } 

665 ) 

666 return records 

667 

668 

669def _decode_profile_slot( 

670 raw_buffer: bytes, 

671 slot: int, 

672 layout: tuple[int, int, int, int], 

673) -> tuple[list[dict[str, int]], int | None, int]: 

674 """Decode one initialized slot and return records, entry cycle, and drops.""" 

675 expected_core_type, expected_block_id, slot_offset, record_capacity = layout 

676 header = CORE_HEADER.unpack_from(raw_buffer, slot_offset) 

677 if not any(header): 

678 return [], None, 0 

679 entry_cycle, record_count, dropped_count, core_type, block_id, header_capacity, reserved = header 

680 _validate_profile_slot_header( 

681 slot, 

682 core_type, 

683 block_id, 

684 expected_core_type, 

685 expected_block_id, 

686 header_capacity, 

687 record_capacity, 

688 record_count, 

689 reserved, 

690 ) 

691 if record_count == 0: 

692 return [], None, dropped_count 

693 return ( 

694 _decode_slot_records( 

695 raw_buffer, 

696 slot, 

697 slot_offset, 

698 record_count, 

699 core_type, 

700 block_id, 

701 entry_cycle, 

702 ), 

703 entry_cycle or None, 

704 dropped_count, 

705 ) 

706 

707 

708def _decode_cycle_records( 

709 raw_buffer: bytes, 

710 aic_record_capacity: int, 

711 aiv_record_capacity: int, 

712 cube_slot_count: int = CUBE_SLOT_COUNT, 

713) -> tuple[list[dict[str, int]], list[int], int]: 

714 """Decode all initialized AIC/AIV slots and aggregate drop counters.""" 

715 raw_records = [] 

716 active_entry_cycles = [] 

717 dropped_records = 0 

718 for slot, layout in enumerate(_profile_slot_layouts(aic_record_capacity, aiv_record_capacity, cube_slot_count)): 

719 records, entry_cycle, dropped_count = _decode_profile_slot(raw_buffer, slot, layout) 

720 dropped_records += dropped_count 

721 if entry_cycle is not None: 

722 active_entry_cycles.append(entry_cycle) 

723 raw_records.extend(records) 

724 return raw_records, active_entry_cycles, dropped_records 

725 

726 

727def _thread_metadata_events( 

728 rank: int, 

729 device_id: int, 

730 kernel_name: str, 

731 raw_records: list[dict[str, int]], 

732) -> list[dict[str, Any]]: 

733 """Build process and thread metadata for all cores that emitted records.""" 

734 trace_events = [ 

735 { 

736 "name": "process_name", 

737 "ph": "M", 

738 "pid": rank, 

739 "tid": 0, 

740 "args": {"name": f"{kernel_name} rank {rank} device {device_id}"}, 

741 } 

742 ] 

743 used_threads = sorted({(record["core_type"], record["block_id"]) for record in raw_records}) 

744 for sort_index, (core_type, block_id) in enumerate(used_threads): 

745 thread_id = _thread_id(core_type, block_id) 

746 trace_events.extend( 

747 [ 

748 { 

749 "name": "thread_name", 

750 "ph": "M", 

751 "pid": rank, 

752 "tid": thread_id, 

753 "args": {"name": f"{CORE_TYPE_NAMES[core_type]}/{block_id}"}, 

754 }, 

755 { 

756 "name": "thread_sort_index", 

757 "ph": "M", 

758 "pid": rank, 

759 "tid": thread_id, 

760 "args": {"sort_index": sort_index}, 

761 }, 

762 ] 

763 ) 

764 return trace_events 

765 

766 

767def _duration_trace_event( 

768 record: Mapping[str, int], 

769 *, 

770 rank: int, 

771 device_id: int, 

772 anchor_cycle: int, 

773 cycle_frequency_mhz: float, 

774 detailed_task_names: bool, 

775 owner_label: str, 

776 stage_names: Mapping[int, str], 

777 task_stage_names: Mapping[int, str], 

778) -> dict[str, Any]: 

779 """Convert one raw cycle record into a Chrome Trace duration event.""" 

780 desc_id = record["desc_id"] 

781 task_id = record["task_id"] 

782 args = { 

783 "rank": rank, 

784 "device_id": device_id, 

785 "core_type": CORE_TYPE_NAMES[record["core_type"]], 

786 "block_id": record["block_id"], 

787 "task_id": task_id, 

788 "desc_id": desc_id, 

789 "stage_task_index": record["stage_task_index"], 

790 "task_number": record["stage_task_index"] + 1, 

791 "start_cycle": record["start_cycle"], 

792 "end_cycle": record["end_cycle"], 

793 "core_entry_cycle": record["entry_cycle"], 

794 } 

795 if task_id in task_stage_names: 

796 args["task_stage"] = task_stage_names[task_id] 

797 if desc_id >= TASK_TYPE_DESC_BASE: 

798 args["task_type"] = desc_id - TASK_TYPE_DESC_BASE 

799 if record["owner_id"] != INVALID_OWNER_ID: 

800 args["owner_id"] = record["owner_id"] 

801 return { 

802 "name": _event_name( 

803 desc_id, 

804 task_id, 

805 record["stage_task_index"], 

806 record["owner_id"], 

807 detailed_task_names, 

808 owner_label, 

809 stage_names, 

810 task_stage_names, 

811 ), 

812 "cat": "MegaKernelInternal", 

813 "ph": "X", 

814 "pid": rank, 

815 "tid": _thread_id(record["core_type"], record["block_id"]), 

816 "ts": (record["start_cycle"] - anchor_cycle) / cycle_frequency_mhz, 

817 "dur": (record["end_cycle"] - record["start_cycle"]) / cycle_frequency_mhz, 

818 "args": args, 

819 } 

820 

821 

822def _parse_cycle_buffer( 

823 buffer: Any, 

824 config: _CycleTraceConfig, 

825 *, 

826 aic_record_capacity: int | None = None, 

827 aiv_record_capacity: int | None = None, 

828) -> dict[str, Any]: 

829 """Decode a device cycle buffer with already resolved hardware and display metadata.""" 

830 if config.cycle_frequency_mhz <= 0: 

831 raise ValueError(f"cycle_frequency_mhz must be positive, got {config.cycle_frequency_mhz}") 

832 resolved_stage_names = _resolve_stage_names(config.stage_names) 

833 resolved_task_stage_names = dict(config.task_stage_names or {}) 

834 if config.cube_slot_count < CUBE_SLOT_COUNT: 

835 raise ValueError(f"profile Cube slot count must be at least {CUBE_SLOT_COUNT}, got {config.cube_slot_count}") 

836 raw_buffer = _as_bytes(buffer, config.cube_slot_count) 

837 aic_record_capacity, aiv_record_capacity, required_buffer_bytes = _resolve_buffer_layout( 

838 raw_buffer, 

839 aic_record_capacity, 

840 aiv_record_capacity, 

841 config.cube_slot_count, 

842 ) 

843 raw_records, active_entry_cycles, dropped_records = _decode_cycle_records( 

844 raw_buffer, 

845 aic_record_capacity, 

846 aiv_record_capacity, 

847 config.cube_slot_count, 

848 ) 

849 if not raw_records: 

850 raise ValueError( 

851 "MegaKernel cycle profile buffer contains no records; verify that the current profiler schedule is active " 

852 "and that the profiled operator completed" 

853 ) 

854 anchor_cycle = ( 

855 min(active_entry_cycles) if active_entry_cycles else min(record["start_cycle"] for record in raw_records) 

856 ) 

857 resolved_device_id = config.rank if config.device_id is None else config.device_id 

858 trace_events = _thread_metadata_events(config.rank, resolved_device_id, config.kernel_name, raw_records) 

859 for record in sorted( 

860 raw_records, 

861 key=lambda item: (item["start_cycle"], item["core_type"], item["block_id"]), 

862 ): 

863 trace_events.append(_duration_trace_event( 

864 record, 

865 rank=config.rank, 

866 device_id=resolved_device_id, 

867 anchor_cycle=anchor_cycle, 

868 cycle_frequency_mhz=config.cycle_frequency_mhz, 

869 detailed_task_names=config.detailed_task_names, 

870 owner_label=config.owner_label, 

871 stage_names=resolved_stage_names, 

872 task_stage_names=resolved_task_stage_names, 

873 )) 

874 return { 

875 "traceEvents": trace_events, 

876 "megaKernelCycleTrace": { 

877 "schemaVersion": 1, 

878 "rank": config.rank, 

879 "deviceId": resolved_device_id, 

880 "socName": config.soc_name, 

881 "cycleFrequencyMHz": config.cycle_frequency_mhz, 

882 "anchorCycle": anchor_cycle, 

883 "recordCapacityPerCore": { 

884 "AIC": aic_record_capacity, 

885 "AIV": aiv_record_capacity, 

886 }, 

887 "maxRecordCapacityPerCore": MAX_RECORDS_PER_CORE, 

888 "recordCount": len(raw_records), 

889 "droppedRecordCount": dropped_records, 

890 "profileBufferBytes": required_buffer_bytes, 

891 "detailedTaskNames": config.detailed_task_names, 

892 "kernelName": config.kernel_name, 

893 "ownerLabel": config.owner_label, 

894 "warnings": ([] if dropped_records == 0 else [f"Device dropped {dropped_records} cycle trace records"]), 

895 }, 

896 }