From 01bda687068dfde6d56a7a25e5c4f2603700dadc Mon Sep 17 00:00:00 2001 From: anurag Date: Fri, 21 Aug 2026 11:58:38 -0600 Subject: [PATCH 1/5] add layer reporting Signed-off-by: anurag --- src/mldebug/client_debug.py | 39 ++++- src/mldebug/input_parser.py | 4 +- src/mldebug/interactive_prompt.py | 11 ++ src/mldebug/kernel_info.py | 252 ++++++++++++++++++++++++++++++ src/mldebug/layer_info.py | 89 ++++++++--- src/mldebug/layer_report.py | 118 ++++++++++++++ src/mldebug/mladf_report.py | 145 +++++++++++++---- src/mldebug/mldebug_cli.py | 23 ++- src/mldebug/work_dir.py | 41 +++++ 9 files changed, 658 insertions(+), 64 deletions(-) create mode 100644 src/mldebug/kernel_info.py create mode 100644 src/mldebug/layer_report.py diff --git a/src/mldebug/client_debug.py b/src/mldebug/client_debug.py index 7173621..d54b010 100644 --- a/src/mldebug/client_debug.py +++ b/src/mldebug/client_debug.py @@ -19,7 +19,9 @@ from mldebug.debug_state import DebugState from mldebug.debug_server import DebugServer from mldebug.interactive_controller import InteractiveController +from mldebug.kernel_info import format_kernel_info from mldebug.layer_info import LayerInfo +from mldebug.layer_report import format_layer_report from mldebug.memory_dumper import MemoryDumper from mldebug.utils import LOGGER, register_debug_server @@ -248,22 +250,47 @@ def print_current_state(self, layer_order=None): info_layer = self.state.get_layer_by_order(layer_order) if info_layer: print(f"{sep}\nInformation on layer: {layer_order}\n{sep}") - print(info_layer) + print(f"#Iterations: {info_layer.lcp.num_iter}") + print(info_layer.format_stamps()) + self.print_kernel_functions(info_layer) print(sep) else: print(f"Layer not found: {layer_order}. Note: TG Layers aren't supported.") return - self.design_info.print_info() if self.args.aie_only: return layer = self.state.get_current_layer() if layer: - stamp_names = ", ".join([f"Stamp {i}: {stamp.name}" for i, stamp in enumerate(layer.stamps)]) - LOGGER.log(f"Stopped at Start of Kernel(s): {stamp_names}") - LOGGER.log(f"Current Layer: {layer.layer_order}, Iteration: {self.state.cur_it}") - LOGGER.log(str(layer)) + LOGGER.log( + f"Current Layer: {layer.layer_order}, Current Iteration: {self.state.cur_it}, " + f"#Iterations: {layer.lcp.num_iter}" + ) + LOGGER.log(f"Stopped at Start of Kernel(s):\n{layer.format_stamps()}") + self.print_kernel_functions(layer) + + def print_kernel_functions(self, layer): + """ + Print the subfunction call tree of a TG layer's kernel; other layers have none. + """ + if not layer.lcp.is_tg: + return + kernels = self.design_info.work_dir.get_kernel_info(layer.stamps) + if kernels: + LOGGER.log(format_kernel_info(kernels)) + + def dump_layers(self, filename=None): + """ + Print a text report of every debuggable layer, or write it to filename. + """ + report = format_layer_report(self.design_info) + if not filename: + print(report) + return + with open(filename, "w", encoding="utf-8") as fd: + fd.write(report + "\n") + print(f"[INFO] Layer report written to {filename}") def read_lcp(self, col=None, row=None, ping=1): """ diff --git a/src/mldebug/input_parser.py b/src/mldebug/input_parser.py index 59e6eba..48af1f0 100644 --- a/src/mldebug/input_parser.py +++ b/src/mldebug/input_parser.py @@ -145,8 +145,8 @@ def get_flag(s, default=False): get_flag("disable_tg"), ) - # Support all stamps in standalone mode - if args.aie_only: + # Support all stamps in standalone mode and when reporting the design's layers + if args.aie_only or args.dump_layers: args.run_flags.multistamp=True diff --git a/src/mldebug/interactive_prompt.py b/src/mldebug/interactive_prompt.py index 4a891ba..9c9de42 100644 --- a/src/mldebug/interactive_prompt.py +++ b/src/mldebug/interactive_prompt.py @@ -111,6 +111,7 @@ def _build_shell_namespace(self): if not aie_only: help_text += """ LAYER BASED AIE CONTROL FUNCTIONS (-b or -v or -x2 flags required) + dump_layers(filename=None): Print or dump all debuggable layers of the design dump_buffers() : Dump AIE Buffers at current state step_it() : Step to next Iteration step_layer() : Step to next Layer @@ -126,6 +127,7 @@ def _build_shell_namespace(self): step_it = h.step_iter_manual cont = h.continue_execution dump_buffers = h.dump_memory + dump_layers = h.dump_layers dump_l3 = h.dump_l3_buffers_manual rmem = h.impl.dump_memory rreg = h.impl.read_register @@ -228,6 +230,8 @@ def run(self): INFORMATION COMMANDS h/help : Print this Message i/info : Print current state of Execution + layers : Print all debuggable layers of the design + Specify optional Filename AIE INSPECTION COMMANDS a/aie_status : Print Status for : AIE, Memory and Interface Tiles Specify optional Filename @@ -266,6 +270,13 @@ def run(self): h.print_current_state(layer_order=int(cmd[1])) else: h.print_current_state() + elif c in ["layers"]: + if nargs == 1: + h.dump_layers() + elif nargs == 2: + h.dump_layers(cmd[1]) + else: + print("Unrecognized Parameters. Use h/help") elif c in ["a", "aie_status"]: if nargs == 1: h.status_handle.get() diff --git a/src/mldebug/kernel_info.py b/src/mldebug/kernel_info.py new file mode 100644 index 0000000..277762c --- /dev/null +++ b/src/mldebug/kernel_info.py @@ -0,0 +1,252 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. + +""" +Rebuild a kernel's call tree from the linker map file. + +Some superkernels fuse many ops behind a single wrapper that the debugger can +only break on as a whole -- templated-graph (TG) kernels today. The caller +decides which layers are worth breaking down; this module just builds the tree +for whatever kernel entry PC it is handed, so the interactive 'i'/info() command +can show what a layer's kernel is actually made of. + +The map file's PM section lists every function with its address range, stack +frame and callees, which is enough to recover the tree without the compiler's +own .calltree file. Only the Synopsys bridge (Chess) layout is parsed today: + + 0x000009e0..0x00000b57 ( 376 items) : :: (Function, Global, .text) ... + + Called functions : + + +Peano/lld map files use a different layout and yield nothing; add a second +parser here when that flow is needed. +""" + +import re +import textwrap +from collections import namedtuple +from dataclasses import dataclass, field + +_MEM_SECTION = re.compile(r"^Memory map for memory '(\S+)':") +_ENTRY = re.compile(r"^\s+0x([0-9a-f]+)\.\.0x[0-9a-f]+\s+\(\s*(\d+) items\)\s*:\s*(.+?)\s*$") +_FUNCTION = re.compile( + r"^(?P\S+)\s+\(Function,\s*\w+,\s*\.text\S*\)" + r"(?:\s+\(stack frame size = (?P\d+)\))?$" +) +_CALLED = re.compile(r"^\s+Called functions\s*:\s*(\S+)\s*$") +_CALLED_MORE = re.compile(r"^\s{8,}(\S+)\s*$") +# Leftover mangling when c++filt cannot demangle: _Z[L], lowercased. +_MANGLED = re.compile(r"^_+z[a-z]*?(\d+)(.+)$") + +_PM_SECTION = "PM" +_REPEAT_MARK = " (*)" +_EMPTY = "-" +_TRUNC_SUFFIX = "..." +_LOCATION = "on {locations}" + + +@dataclass +class MapFunction: + """One PM function entry of a map file.""" + + symbol: str + start_addr: int + size: int + stack: int + callees: list = field(default_factory=list) + + +@dataclass +class KernelNode: + """One function of a kernel, plus its position in that kernel's call tree.""" + + func: MapFunction + name: str + branch: str + repeated: bool + aie_func: object = None + + +@dataclass +class KernelInfo: + """A kernel's call tree, and a label for where it runs.""" + + location: str + nodes: list = field(default_factory=list) + + +def parse_map_functions(map_path): + """Parse the PM section of a map file into {symbol: MapFunction}.""" + functions = {} + section = None + current = None + in_callees = False + with open(map_path, encoding="utf-8", errors="replace") as fd: + for line in fd: + line = line.rstrip("\n") + m_section = _MEM_SECTION.match(line) + if m_section: + section, current, in_callees = m_section.group(1), None, False + continue + m_called = _CALLED.match(line) + if m_called: + in_callees = current is not None + if in_callees: + current.callees.append(m_called.group(1)) + continue + m_entry = _ENTRY.match(line) + if m_entry: + in_callees = False + current = _make_function(m_entry) if section == _PM_SECTION else None + if current: + functions[current.symbol] = current + continue + if in_callees: + m_more = _CALLED_MORE.match(line) + if m_more: + current.callees.append(m_more.group(1)) + else: + in_callees = False + return functions + + +def _make_function(m_entry): + """Build a MapFunction from an entry match, or None when the entry is not a function.""" + m_func = _FUNCTION.match(m_entry.group(3)) + if not m_func: + return None + return MapFunction( + symbol=m_func.group("sym").rsplit("::", 1)[-1], + start_addr=int(m_entry.group(1), 16), + size=int(m_entry.group(2)), + stack=int(m_func.group("stack") or 0), + ) + + +def _readable(name): + """Recover the identifier from a name c++filt left mangled, using its length prefix.""" + m_mangled = _MANGLED.match(name) + if not m_mangled: + return name + return m_mangled.group(2)[: int(m_mangled.group(1))] or name + + +def _build_tree(root_symbol, functions, demangle): + """Flatten the call tree under root_symbol into pre-order KernelNodes.""" + nodes = [] + expanded = set() + + def visit(symbol, branch, child_prefix): + func = functions[symbol] + repeated = symbol in expanded + nodes.append(KernelNode(func, _readable(demangle(symbol)), branch, repeated)) + if repeated: + return + expanded.add(symbol) + callees = [c for c in func.callees if c in functions] + for i, callee in enumerate(callees): + last = i == len(callees) - 1 + visit( + callee, + child_prefix + ("└── " if last else "├── "), + child_prefix + (" " if last else "│ "), + ) + + visit(root_symbol, "", "") + return nodes + + +def _attach_pcs(nodes, aie_functions): + """Link each node to its AIEFunction from the work dir's parsed ELF listing.""" + # Keyed on entry PC: names cannot identify a function, since the LST parser + # records locals under a debug label and template clones demangle alike. + by_pc = {func.start_pc: func for func in aie_functions} + for node in nodes: + node.aie_func = by_pc.get(node.func.start_addr) + + +def build_kernel_info(map_path, start_pc, aie_functions, demangle, location): + """Call tree of the kernel entered at start_pc, or None when the map has none there.""" + functions = parse_map_functions(map_path) + root = next((sym for sym, f in functions.items() if f.start_addr == start_pc), None) + if not root: + return None + nodes = _build_tree(root, functions, demangle) + _attach_pcs(nodes, aie_functions) + return KernelInfo(location, nodes) + + +_Column = namedtuple("_Column", "label width align value") + + +def _pc(node, attr): + """PC from the work dir function database, 0 when the function is not in it.""" + return getattr(node.aie_func, attr, 0) if node.aie_func else 0 + + +def _hex(value): + """Format a PC as hex, or '-' when it is unknown.""" + return f"0x{value:06x}" if value else _EMPTY + + +def _label(node): + """Tree prefix plus function name, marking calls whose subtree was already shown.""" + return node.branch + node.name + (_REPEAT_MARK if node.repeated else "") + + +# FUNCTION is wide enough for a 3-deep tree prefix plus a templated kernel name. +_COLUMNS = ( + _Column("FUNCTION", 56, "<", _label), + _Column("START_PC", 9, ">", lambda n: _hex(n.func.start_addr)), + _Column("END_PC", 9, ">", lambda n: _hex(_pc(n, "end_pc"))), + _Column("LOCK_REL", 9, ">", lambda n: _hex(_pc(n, "final_lock_release_pc"))), + _Column("SIZE", 6, ">", lambda n: n.func.size), + _Column("STACK", 6, ">", lambda n: n.func.stack), +) + +_HEADER = " ".join(f"{c.label:{c.align}{c.width}}" for c in _COLUMNS) + + +def _clip(value, width): + """Fit a cell value into width, marking truncated values with a trailing '...'.""" + text = _EMPTY if value is None or value == "" else str(value) + if len(text) <= width: + return text + return text[: width - len(_TRUNC_SUFFIX)] + _TRUNC_SUFFIX + + +def _row(node): + """Format one tree node as a table row.""" + cells = [f"{_clip(c.value(node), c.width):{c.align}{c.width}}" for c in _COLUMNS] + return " ".join(cells) + + +def _group_identical(kernels): + """Group trees that match down to every PC; a layer's stamps usually share one.""" + groups = {} + for kernel in kernels: + signature = tuple( + (n.func.symbol, n.func.start_addr, _pc(n, "end_pc"), _pc(n, "final_lock_release_pc")) + for n in kernel.nodes + ) + groups.setdefault(signature, []).append(kernel) + return list(groups.values()) + + +def format_kernel_info(kernels): + """Render kernel call trees as text, or '' when there are none.""" + groups = _group_identical(kernels) + lines = [] + for same in groups: + lines.append("") + # A single tree covers every stamp; only say who is who when they diverge. + if len(groups) > 1: + lines += textwrap.wrap( + _LOCATION.format(locations=", ".join(k.location for k in same)), + width=len(_HEADER), + subsequent_indent=" ", + ) + lines += [_HEADER, "-" * len(_HEADER)] + lines += [_row(node) for node in same[0].nodes] + return "\n".join(lines) diff --git a/src/mldebug/layer_info.py b/src/mldebug/layer_info.py index f4f0ec4..0b39b4f 100644 --- a/src/mldebug/layer_info.py +++ b/src/mldebug/layer_info.py @@ -272,6 +272,11 @@ def __init__( self.out_buffers = [] self.wts_buffers = [] self.layer_order = info["layer_order"] + self.layer_name = info.get("layer_name", "") + # mladf layer_ids this Layer covers. + self.mladf_ids = [] + if mladf_report: + self.mladf_ids = mladf_report.get_layer_ids_for_bilo(self.layer_order) self.lcp = Lcp() self.pm_work_dir = info.get("pm", None) self.is_unsupported = False @@ -293,7 +298,7 @@ def __init__( # TBD: it details only one batch replica, make it work with nBnS mode if mladf_report and device_batch_size == 1 and num_stamps > 1: # None or 0 means mladf could not answer; fall back to buffer_info. - true_n = mladf_report.get_running_stamp_count(self.layer_order, num_stamps) + true_n = mladf_report.get_running_stamp_count(self.layer_order, num_stamps, self.mladf_ids) if true_n and n_stamps and true_n != n_stamps: LOGGER.log( f"[WARNING] Layer {self.layer_order}: buffer_info no_of_stamps={n_stamps} " @@ -317,8 +322,8 @@ def __init__( # Fill missing TG metadata from mladf report if self.lcp.is_tg: for sid, stamp in enumerate(self.stamps): - stamp.name = mladf_report.get_skname_for_bilo(self.layer_order, sid) - stamp.elf_name = mladf_report.get_elfid_for_bilo(self.layer_order, sid) + stamp.name = mladf_report.get_skname_for_bilo(self.layer_order, sid, self.mladf_ids) + stamp.elf_name = mladf_report.get_elfid_for_bilo(self.layer_order, sid, self.mladf_ids) if ( not stamp.name or stamp.elf_name == -1 @@ -329,7 +334,7 @@ def __init__( ) self.is_unsupported = True return - self.lcp.num_iter = mladf_report._get_iters_for_bilo(self.layer_order) + self.lcp.num_iter = mladf_report._get_iters_for_bilo(self.layer_order, self.mladf_ids) self._initialize_l3_buffers(info, version) @@ -506,15 +511,21 @@ def _initialize_buffers(self, info, aie_iface, size_shift, version): if name in info: self.out_buffers.append(Buffer(info[name], name, size_shift, aie_iface, ofm=True)) + def format_stamps(self): + """ + One indented line per stamp: its ELF, kernel and the PCs the debugger breaks on. + """ + return "\n".join( + f" Stamp {i}: elf: {stamp.elf_name}, kernels: {stamp.name}" + f", start pc: {stamp.start_pc}, final lock release pc: {stamp.end_pc}" + for i, stamp in enumerate(self.stamps) + ) + def __str__(self): """ Pretty-print metainfo about this layer. Returns kernel and iteration info. """ - s = f"iters: {self.lcp.num_iter}" - for i, stamp in enumerate(self.stamps): - s += f"\n Stamp {i}: elf: {stamp.elf_name}, kernels: {stamp.name}" - s += f", start pc: {stamp.start_pc}, final lock release pc: {stamp.end_pc}" - return s + return "\n".join(filter(None, [f"iters: {self.lcp.num_iter}", self.format_stamps()])) class LayerInfo: @@ -661,16 +672,16 @@ def _create_info(self): imap[elf][1] = max(imap[elf][1], order) return info - def print_info(self): + def format_info(self): """ - Print an overview of the loaded/stamped layers in human readable format. + Which ELF each stamp runs and over which layer range, or '' when unknown. """ info = self._create_info() sep = "--------------------------------------------" m = "Design info (Excluding TG Layer IDs)\n" m += f"{sep}\nFlexml Layer Count: {len(self.layers)}\n{sep}" if not self.work_dir.stamps or not self.layers: - return + return "" for sid, imap in info.items(): m += f"\nStamp {sid}: " for eid, (min_layer, max_layer) in imap.items(): @@ -680,7 +691,15 @@ def print_info(self): m += f"{{{eid}: {min_layer}-{max_layer}}} " m += "\n" m += "--------------------------------------------" - LOGGER.log(m) + return m + + def print_info(self): + """ + Print an overview of the loaded/stamped layers in human readable format. + """ + info = self.format_info() + if info: + LOGGER.log(info) def initialize_l3_offsets(self, flexmlrt_hsi, external_buffer_id): """ @@ -902,19 +921,39 @@ def _init_layers(self, raw_info, aie_iface, num_stamps, num_batches=1): raw_layers = sorted(raw_layers.items(), key=lambda item: item[1]["layer_order"]) for entry in raw_layers: info = entry[1] - layer = Layer( - info, - size_shift, - version, - aie_iface, - num_stamps, - self.mladf_report, - num_batches=num_batches, - device_batch_size=self.layout[0], + self._warn_if_scheduled_in_chunks(info) + self.layers.append( + Layer( + info, + size_shift, + version, + aie_iface, + num_stamps, + self.mladf_report, + num_batches=num_batches, + device_batch_size=self.layout[0], + ) ) - self.layers.append(layer) self._reorder_layers_by_execution() + def _warn_if_scheduled_in_chunks(self, info): + """ + Warn when the compiler scheduled one buffer_info layer in several chunks. + """ + if not self.mladf_report: + return + bilo = info["layer_order"] + segments = self.mladf_report.get_layer_id_segments(bilo) + if len(segments) < 2: + return + span = self.mladf_report.format_layer_id_display([i for s in segments for i in s]) + LOGGER.log( + f"[WARNING] Layer {bilo} ({info.get('layer_name', '')}) is scheduled in " + f"{len(segments)} chunks ({span}) but buffer_info describes it as one layer. " + "Stepping it whole, positioned at its last chunk; its iteration count " + "covers every chunk and its ELF is taken from the first." + ) + def _reorder_layers_by_execution(self): """ Stable-sort layers into true execution order (mladf `layer_id`), which the @@ -928,7 +967,9 @@ def _reorder_layers_by_execution(self): prev_exec = None disagreement = False for layer in self.layers: - exec_order = self.mladf_report.get_exec_order_for_bilo(layer.layer_order) + # Last id, not first: a layer scheduled in several chunks spans a gap, + # and it is least wrong to place it where it ends. + exec_order = max(layer.mladf_ids) if layer.mladf_ids else None if exec_order is None: if layer.lcp.is_tg and not layer.is_unsupported: LOGGER.log( diff --git a/src/mldebug/layer_report.py b/src/mldebug/layer_report.py new file mode 100644 index 0000000..e2fbfd2 --- /dev/null +++ b/src/mldebug/layer_report.py @@ -0,0 +1,118 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. + +""" +Text report of the debuggable layers in a design. + +Renders LayerInfo.layers as one line per layer so a user can see what the +debugger will step through without running it. Kept out of layer_info.py so +the report can grow without touching metadata parsing. + +Columns live in _COLUMNS: add, remove or resize an entry there and the header, +separator and every row follow. +""" + +from collections import namedtuple + +_EMPTY = "-" +_UNKNOWN = "?" +_TRUNC_SUFFIX = "..." +_TRUNC_RESERVE = len(_TRUNC_SUFFIX) +_SEPARATOR_CHAR = "-" +_COLUMN_GAP = " " + +_MSG_TITLE = "Debuggable layers: {count} Overlay: {overlay}" +_MSG_SUBTITLE = ( + "Rows are in execution order; BE_ID is the layer id accepted by breakpoints." +) +_MSG_NO_LAYERS = "(no debuggable layers found)" +_MSG_UNCLAIMED = "{count} mladf layer(s) unmapped: data movement only, nothing to step." +_MSG_UNCLAIMED_COMPUTE = ( + "{count} mladf layer(s) unmapped but running a kernel: mapping gap, " + "these cannot be stepped or dumped." +) + + +def _clip(value, width): + """Fit a cell value into width, marking truncated values with a trailing '...'.""" + text = _EMPTY if value is None or value == "" else str(value) + return text if len(text) <= width else text[: width - _TRUNC_RESERVE] + _TRUNC_SUFFIX + + +def _stamp_attr(layer, attr): + """Attribute of the layer's first stamp, or '' when the layer has none.""" + return getattr(layer.stamps[0], attr) if layer.stamps else "" + + +def _mladf_id(design_info, layer): + """mladf layer_id display string for a layer.""" + report = design_info.mladf_report + if not report or not layer.mladf_ids: + return _EMPTY + span = report.format_layer_id_display(layer.mladf_ids) + return _UNKNOWN if span is None else span + + +_Column = namedtuple("_Column", "label width align value") + +# Width 60 on KERNEL fits most mladf kernel names, whose template arguments are +# what distinguish otherwise identical superkernels. +_COLUMNS = ( + _Column("BE_ID", 5, ">", lambda _di, layer: layer.layer_order), + _Column("BE_LAYER_NAME", 40, "<", lambda _di, layer: layer.layer_name), + _Column("KERNEL", 60, "<", lambda _di, layer: _stamp_attr(layer, "name")), + _Column("#ITERS", 6, ">", lambda _di, layer: layer.lcp.num_iter), + _Column("#STAMPS", 7, ">", lambda _di, layer: len(layer.stamps)), + _Column("MLADF_IDS", 13, ">", _mladf_id), +) + +_HEADER = _COLUMN_GAP.join(f"{c.label:{c.align}{c.width}}" for c in _COLUMNS) + + +def _unclaimed_mladf_counts(design_info): + """(compute, data-movement) counts of mladf layers no buffer_info layer maps to.""" + report = design_info.mladf_report + if not report: + return 0, 0 + compute, other = report.get_unclaimed_layers() + return len(compute), len(other) + + +def _row(design_info, layer): + """Format one layer as a table row.""" + cells = [] + for col in _COLUMNS: + text = _clip(col.value(design_info, layer), col.width) + cells.append(f"{text:{col.align}{col.width}}") + return _COLUMN_GAP.join(cells) + + +def format_layer_report(design_info): + """Render every debuggable layer as a text table, in execution order.""" + layers = design_info.layers + lines = [] + design = design_info.format_info() + if design: + lines += [design, ""] + lines += [ + _MSG_TITLE.format(count=len(layers), overlay=design_info.overlay.get_repr()), + _MSG_SUBTITLE, + "", + _HEADER, + _SEPARATOR_CHAR * len(_HEADER), + ] + if not layers: + lines.append(_MSG_NO_LAYERS) + return "\n".join(lines) + + for layer in layers: + lines.append(_row(design_info, layer)) + + unclaimed_compute, unclaimed_dm = _unclaimed_mladf_counts(design_info) + if unclaimed_dm: + lines.append("") + lines.append(_MSG_UNCLAIMED.format(count=unclaimed_dm)) + if unclaimed_compute: + lines.append("") + lines.append(_MSG_UNCLAIMED_COMPUTE.format(count=unclaimed_compute)) + return "\n".join(lines) diff --git a/src/mldebug/mladf_report.py b/src/mldebug/mladf_report.py index 49206d2..4e06a60 100644 --- a/src/mldebug/mladf_report.py +++ b/src/mldebug/mladf_report.py @@ -13,6 +13,23 @@ from mldebug.utils import LOGGER +def _compact_id_ranges(ids): + """Collapse sorted layer ids into comma-separated spans, e.g. ``59,157-209``.""" + if not ids: + return None + ids = sorted(set(ids)) + parts = [] + start = prev = ids[0] + for cur in ids[1:]: + if cur == prev + 1: + prev = cur + continue + parts.append(str(start) if start == prev else f"{start}-{prev}") + start = prev = cur + parts.append(str(start) if start == prev else f"{start}-{prev}") + return ",".join(parts) + + def load_json(path): """ utility @@ -43,21 +60,51 @@ def __init__(self, bi_file, m2_file, cps=4): self.bi_layers = bi_data.get("layers", {}) self.m2_layers = m2_data.get("layer_information", {}) self.bi_to_m2 = self._approach1_map(self.bi_layers, self.m2_layers) + self._warn_unmapped_compute() - def get_aiec_layers_by_bilo(self, bilo): + def get_unclaimed_layers(self): + """ + mladf layers no buffer_info layer maps to, split into (compute, other). """ - Return list of aiecompiler layers for a specific - layer_order in buffer_info + # An unclaimed compute layer is a mapping failure; data movement is expected. + claimed = {k for keys in self.bi_to_m2.values() for k in keys} + compute, other = [], [] + for key, layer in self.m2_layers.items(): + if key in claimed: + continue + (compute if layer.get("is_compute_layer") else other).append(key) + return compute, other + + def _warn_unmapped_compute(self): + """Complain when a kernel-running mladf layer maps to no buffer_info layer.""" + compute, _ = self.get_unclaimed_layers() + if not compute: + return + ids = [self.m2_layers[k].get("layer_id") for k in compute] + ids = sorted(i for i in ids if i is not None) + LOGGER.log( + f"[WARNING] {len(compute)} mladf compute layer(s) map to no buffer_info " + f"layer: {_compact_id_ranges(ids) or '?'}. Their kernels cannot be " + "stepped or dumped; layer positions may also be off." + ) + + def get_aiec_layers_by_bilo(self, bilo, ids=None): + """ + aiecompiler layers for a buffer_info layer_order, optionally narrowed to `ids`. """ aiec_layer_keys = self.bi_to_m2.get(bilo, []) - return [self.m2_layers[k] for k in aiec_layer_keys] + layers = [self.m2_layers[k] for k in aiec_layer_keys] + if ids is None: + return layers + ids = set(ids) + return [lyr for lyr in layers if lyr.get("layer_id") in ids] - def get_running_stamp_count(self, bilo, max_stamps): + def get_running_stamp_count(self, bilo, max_stamps, ids=None): """ Number of per-batch stamps running a kernel for a buffer_info layer, or None if the report does not describe all `max_stamps` stamp cores. """ - aiec_layers = self.get_aiec_layers_by_bilo(bilo) + aiec_layers = self.get_aiec_layers_by_bilo(bilo, ids) running = [] described = 0 for s in range(max_stamps): @@ -83,19 +130,39 @@ def get_running_stamp_count(self, bilo, max_stamps): ) return len(running) - def get_exec_order_for_bilo(self, bilo): + def get_layer_ids_for_bilo(self, bilo): + """Sorted unique mladf layer_id values mapped to a buffer_info layer.""" + return sorted( + { + self.m2_layers[k]["layer_id"] + for k in self.bi_to_m2.get(bilo, []) + if "layer_id" in self.m2_layers[k] + } + ) + + def get_layer_id_segments(self, bilo): + """ + The layer's mladf `layer_id`s as contiguous runs; >1 run means split scheduling. + """ + segments = [] + for lid in self.get_layer_ids_for_bilo(bilo): + if segments and lid == segments[-1][-1] + 1: + segments[-1].append(lid) + else: + segments.append([lid]) + return segments + + def format_layer_id_display(self, ids): """ - Smallest mladf `layer_id` (the real execution/PM-reload order) for a - buffer_info layer_order, or None if unmapped. + Format mladf layer_ids for reports: contiguous runs as ``lo-hi``, gaps kept. """ - ids = [lyr["layer_id"] for lyr in self.get_aiec_layers_by_bilo(bilo) if "layer_id" in lyr] - return min(ids) if ids else None + return _compact_id_ranges(ids) - def get_skname_for_bilo(self, bilo, sid=0): + def get_skname_for_bilo(self, bilo, sid=0, ids=None): """ return superkernel for buffer info layer """ - aiec_layers = self.get_aiec_layers_by_bilo(bilo) + aiec_layers = self.get_aiec_layers_by_bilo(bilo, ids) if aiec_layers: core = f"{sid * self.cps}_0" if aiec_layers[0]["core_information"].get(core): @@ -108,11 +175,11 @@ def get_skname_for_bilo(self, bilo, sid=0): print(f"[WARNING] MLADF Info for core {core} at Layer_{bilo} not found") return "" - def _get_iters_for_bilo(self, bilo): + def _get_iters_for_bilo(self, bilo, ids=None): """ find iters for a layer """ - aiec_layers = self.get_aiec_layers_by_bilo(bilo) + aiec_layers = self.get_aiec_layers_by_bilo(bilo, ids) if not aiec_layers: return 1 iters = 0 @@ -120,11 +187,11 @@ def _get_iters_for_bilo(self, bilo): iters += aiec_layer["core_information"]["0_0"]["kernel_repetition"] return iters - def get_elfid_for_bilo(self, bilo, sid): + def get_elfid_for_bilo(self, bilo, sid, ids=None): """ Find elf ID for buffer info layer order + stamp id """ - aiec_layers = self.get_aiec_layers_by_bilo(bilo) + aiec_layers = self.get_aiec_layers_by_bilo(bilo, ids) if not aiec_layers: return -1 @@ -208,29 +275,47 @@ def _extract_parent_graph(self, name): parent = re.sub(r"_layer_\d+$", "", stripped) return parent + def _kernel_instance(self, m2_layer): + """The layer's kernel_instance, from the first core that reports one.""" + for core in m2_layer.get("core_information", {}).values(): + kinst = core.get("kernel_instance", "") + if kinst: + return kinst + return "" + def _approach1_map(self, bi_layers, m2_layers): """ - Map each m2 layer to exactly one buffer_info layer via parent graph name. + Map each m2 layer to the buffer_info layer that owns it. """ bi_parents = {} - for _, bi_layer in bi_layers.items(): + bi_prefix = {} + for bi_layer in bi_layers.values(): bi_key = bi_layer["layer_order"] - parents = set() - for obj_name in bi_layer.get("layer_object_name", []): - parents.add(self._extract_parent_graph(obj_name)) - bi_parents[bi_key] = parents + objs = bi_layer.get("layer_object_name", []) + bi_parents[bi_key] = {self._extract_parent_graph(obj) for obj in objs} + if bi_layer.get("templated_graph") and len(objs) == 1: + bi_prefix[bi_key] = f"{objs[0]}." m2_parents = {} + m2_kinst = {} for m2_key, m2_layer in m2_layers.items(): - kernel_str = m2_layer.get("kernel_node_instances", "") - m2_parents[m2_key] = self._extract_m2_parent_graphs(kernel_str) + m2_parents[m2_key] = self._extract_m2_parent_graphs( + m2_layer.get("kernel_node_instances", "") + ) + m2_kinst[m2_key] = self._kernel_instance(m2_layer) bi_to_m2 = {} for bi_key, bi_pgraphs in bi_parents.items(): - bi_to_m2[bi_key] = [] - for m2_key, m2_pgraphs in m2_parents.items(): - overlap = m2_pgraphs & bi_pgraphs - if overlap: - bi_to_m2[bi_key].append(m2_key) + keys = [] + prefix = bi_prefix.get(bi_key) + # Exact "." prefix, the key MLProfilerEngine uses. The dot marks the + # boundary, so templated_graph_10 cannot swallow templated_graph_101_0. + if prefix: + keys = [k for k in m2_parents if m2_kinst[k].startswith(prefix)] + # Looser fallback: regex-derived parent names, so an accidental overlap is + # possible. Also covers TG layers whose m2 layers report no kernel_instance. + if not keys: + keys = [k for k, pgraphs in m2_parents.items() if pgraphs & bi_pgraphs] + bi_to_m2[bi_key] = keys return bi_to_m2 diff --git a/src/mldebug/mldebug_cli.py b/src/mldebug/mldebug_cli.py index c6afb8e..02c5479 100644 --- a/src/mldebug/mldebug_cli.py +++ b/src/mldebug/mldebug_cli.py @@ -75,7 +75,13 @@ def check_args(args): Side Effects: Prints user warnings or messages about argument handling. """ - if args.dump_aie_status: + if args.dump_layers: + # The report is built purely from build metadata, so avoid XRT entirely. + args.interactive = False + args.aie_only = False + args.backend = "test" + print("[INFO] Dumping layer report and exiting (no hardware access)") + elif args.dump_aie_status: args.interactive = False args.aie_only = True print("[INFO] Dumping advanced AIE status and exiting (non-interactive)") @@ -165,6 +171,9 @@ def launch_debug(args, output_dir): # Top debug handle _apply_unsupported_kernels_from_args(args) handle = ClientDebug(args, context_id, pid, output_dir) + if args.dump_layers: + handle.dump_layers(None if args.dump_layers == "-" else args.dump_layers) + return if args.dump_aie_status: handle.status_handle.get(args.dump_aie_status, advanced=True, guidance=False) print(f"[INFO] Advanced AIE status written to {args.dump_aie_status}") @@ -246,6 +255,16 @@ def app(): help="Write AIE status to a file and exit.\n", default=None, ) + p.add_argument( + "--dump-layers", + dest="dump_layers", + nargs="?", + const="-", + metavar="", + help="Write a text report of the design's layers and exit.\n" + "Prints to stdout when no file is given. Needs no hardware.\n", + default=None, + ) # Hidden Argument # 'AIE Device type' p.add_argument( @@ -415,7 +434,7 @@ def app(): check_registry_keys(args, args.device == AIE_DEV_NPU3) registry_checked = True debug(args, timestamp, subgraph_name, fsp, model_folder_name) - if args.dump_aie_status: + if args.dump_aie_status or args.dump_layers: break # End Debug diff --git a/src/mldebug/work_dir.py b/src/mldebug/work_dir.py index 8755d3c..6d60979 100644 --- a/src/mldebug/work_dir.py +++ b/src/mldebug/work_dir.py @@ -13,6 +13,7 @@ from pathlib import Path from mldebug.extra.calltree import AIECallTree +from mldebug.kernel_info import build_kernel_info from mldebug.utils import LOGGER, is_aarch64, is_windows @@ -660,6 +661,46 @@ def find_functions_by_pc(self, pc): funclist.append(f"{elf}:{func.name}") return funclist + def _find_elf(self, sid, elf_id): + """ + Full ELF directory name for a layer stamp's elf_id, or None when unknown. + """ + # LayerInfo indexes ELFs by the text after 'reloadable', or the core name for the base ELF. + return next( + (e for e in self.stamps[sid].aie_functions if e.split("reloadable")[-1] == str(elf_id)), + None, + ) + + def get_kernel_info(self, layer_stamps): + """ + Call tree of each of a layer's stamps, one KernelInfo per stamp found in a map file. + """ + cache = {} + + def demangle(symbol): + if symbol not in cache: + cache[symbol] = self._demangle(symbol) + return cache[symbol] + + kernels = [] + for sid, stamp in enumerate(layer_stamps[: self.stamps_per_batch]): + elf = self._find_elf(sid, stamp.elf_name) + if not elf or not stamp.start_pc: + continue + map_path = Path(self.aie_dir) / "aie" / elf / "Release" / f"{elf}.map" + if not map_path.is_file(): + continue + kernel = build_kernel_info( + map_path, + stamp.start_pc, + self.stamps[sid].aie_functions[elf], + demangle, + f"stamp {sid} (elf {elf})", + ) + if kernel: + kernels.append(kernel) + return kernels + def print_aie_functions(self, elf_id=None): """ Print all parsed AIE functions per ELF and/or stamp. If 'elf_id' is supplied, From 3b6c8fcbdd9432f7f9d20ea2647568aab504ec80 Mon Sep 17 00:00:00 2001 From: anurag Date: Fri, 21 Aug 2026 12:43:39 -0600 Subject: [PATCH 2/5] rename flag Signed-off-by: anurag --- src/mldebug/mldebug_cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mldebug/mldebug_cli.py b/src/mldebug/mldebug_cli.py index 02c5479..2c54fd5 100644 --- a/src/mldebug/mldebug_cli.py +++ b/src/mldebug/mldebug_cli.py @@ -256,8 +256,7 @@ def app(): default=None, ) p.add_argument( - "--dump-layers", - dest="dump_layers", + "--dump_layers", nargs="?", const="-", metavar="", From 081e6bd20902dc132d2d2152fb7ca51f72f1d41e Mon Sep 17 00:00:00 2001 From: anurag Date: Fri, 21 Aug 2026 14:33:09 -0600 Subject: [PATCH 3/5] improve readability Signed-off-by: anurag --- src/mldebug/layer_report.py | 93 +++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/src/mldebug/layer_report.py b/src/mldebug/layer_report.py index e2fbfd2..de98a4d 100644 --- a/src/mldebug/layer_report.py +++ b/src/mldebug/layer_report.py @@ -4,26 +4,51 @@ """ Text report of the debuggable layers in a design. -Renders LayerInfo.layers as one line per layer so a user can see what the +Renders LayerInfo.layers as two lines per layer so a user can see what the debugger will step through without running it. Kept out of layer_info.py so the report can grow without touching metadata parsing. -Columns live in _COLUMNS: add, remove or resize an entry there and the header, -separator and every row follow. -""" +Each layer is one record of two lines: every number on the first, then the two +names stacked in one text column: + + BE_ID #ITERS #STAMPS MLADF_IDS BE_LAYER_NAME + └ KERNEL -from collections import namedtuple +The numbers are fixed-width and left of the names, and neither name is capped, +so a name wider than the terminal costs readability of that name only -- every +number stays lined up and legible. +""" _EMPTY = "-" _UNKNOWN = "?" -_TRUNC_SUFFIX = "..." -_TRUNC_RESERVE = len(_TRUNC_SUFFIX) _SEPARATOR_CHAR = "-" -_COLUMN_GAP = " " + +# Numeric column widths, each sized for its header since those are wider than +# the values in practice. MLADF_IDS also has to fit a span like "60,79-131". +_ID_W = 4 +_ITERS_W = 7 +_STAMPS_W = 7 +_MLADF_W = 7 +_GAP = " " +# Hangs KERNEL under BE_LAYER_NAME so the second line reads as subordinate. +_KERNEL_MARK = "└ " + + +def _numbers(be_id, iters, stamps, mladf): + """The four numeric columns that open a layer's first line.""" + return f"{be_id:>{_ID_W}} {iters:>{_ITERS_W}} {stamps:>{_STAMPS_W}} {mladf:>{_MLADF_W}}{_GAP}" + + +_INDENT = " " * len(_numbers("", "", "", "")) + +_HEADER = ( + f"{_numbers('ID', '#ITERS', '#STAMPS', '#MLADF')}LAYER_NAME\n" + f"{_INDENT}{_KERNEL_MARK}KERNEL" +) _MSG_TITLE = "Debuggable layers: {count} Overlay: {overlay}" _MSG_SUBTITLE = ( - "Rows are in execution order; BE_ID is the layer id accepted by breakpoints." + "Two lines per layer, in execution order; ID is the layer id accepted by breakpoints." ) _MSG_NO_LAYERS = "(no debuggable layers found)" _MSG_UNCLAIMED = "{count} mladf layer(s) unmapped: data movement only, nothing to step." @@ -33,12 +58,6 @@ ) -def _clip(value, width): - """Fit a cell value into width, marking truncated values with a trailing '...'.""" - text = _EMPTY if value is None or value == "" else str(value) - return text if len(text) <= width else text[: width - _TRUNC_RESERVE] + _TRUNC_SUFFIX - - def _stamp_attr(layer, attr): """Attribute of the layer's first stamp, or '' when the layer has none.""" return getattr(layer.stamps[0], attr) if layer.stamps else "" @@ -53,22 +72,6 @@ def _mladf_id(design_info, layer): return _UNKNOWN if span is None else span -_Column = namedtuple("_Column", "label width align value") - -# Width 60 on KERNEL fits most mladf kernel names, whose template arguments are -# what distinguish otherwise identical superkernels. -_COLUMNS = ( - _Column("BE_ID", 5, ">", lambda _di, layer: layer.layer_order), - _Column("BE_LAYER_NAME", 40, "<", lambda _di, layer: layer.layer_name), - _Column("KERNEL", 60, "<", lambda _di, layer: _stamp_attr(layer, "name")), - _Column("#ITERS", 6, ">", lambda _di, layer: layer.lcp.num_iter), - _Column("#STAMPS", 7, ">", lambda _di, layer: len(layer.stamps)), - _Column("MLADF_IDS", 13, ">", _mladf_id), -) - -_HEADER = _COLUMN_GAP.join(f"{c.label:{c.align}{c.width}}" for c in _COLUMNS) - - def _unclaimed_mladf_counts(design_info): """(compute, data-movement) counts of mladf layers no buffer_info layer maps to.""" report = design_info.mladf_report @@ -78,18 +81,22 @@ def _unclaimed_mladf_counts(design_info): return len(compute), len(other) -def _row(design_info, layer): - """Format one layer as a table row.""" - cells = [] - for col in _COLUMNS: - text = _clip(col.value(design_info, layer), col.width) - cells.append(f"{text:{col.align}{col.width}}") - return _COLUMN_GAP.join(cells) +def _record(design_info, layer): + """The layer's two lines: its numbers and name, then the kernel that runs it.""" + numbers = _numbers( + layer.layer_order, layer.lcp.num_iter, len(layer.stamps), _mladf_id(design_info, layer) + ) + return [ + f"{numbers}{layer.layer_name or _EMPTY}", + f"{_INDENT}{_KERNEL_MARK}{_stamp_attr(layer, 'name') or _EMPTY}", + ] def format_layer_report(design_info): - """Render every debuggable layer as a text table, in execution order.""" + """Render every debuggable layer as a two-line record, in execution order.""" layers = design_info.layers + records = [line for layer in layers for line in _record(design_info, layer)] + lines = [] design = design_info.format_info() if design: @@ -99,14 +106,12 @@ def format_layer_report(design_info): _MSG_SUBTITLE, "", _HEADER, - _SEPARATOR_CHAR * len(_HEADER), + _SEPARATOR_CHAR * max(len(line) for line in records + _HEADER.split("\n")), ] - if not layers: + if not records: lines.append(_MSG_NO_LAYERS) return "\n".join(lines) - - for layer in layers: - lines.append(_row(design_info, layer)) + lines += records unclaimed_compute, unclaimed_dm = _unclaimed_mladf_counts(design_info) if unclaimed_dm: From 92d8eb62b0aac20318c88a676c37dce2058fbfbc Mon Sep 17 00:00:00 2001 From: anurag Date: Fri, 21 Aug 2026 16:06:31 -0600 Subject: [PATCH 4/5] remove non ascii characters Signed-off-by: anurag --- src/mldebug/client_debug.py | 4 ++-- src/mldebug/extra/aie_guidance.py | 2 +- src/mldebug/extra/calltree.py | 12 ++++++------ src/mldebug/{ => extra}/kernel_info.py | 4 ++-- src/mldebug/{ => extra}/layer_report.py | 11 +++++------ src/mldebug/layer_info.py | 5 +---- src/mldebug/work_dir.py | 4 ++-- 7 files changed, 19 insertions(+), 23 deletions(-) rename src/mldebug/{ => extra}/kernel_info.py (98%) rename src/mldebug/{ => extra}/layer_report.py (91%) diff --git a/src/mldebug/client_debug.py b/src/mldebug/client_debug.py index d54b010..7b0f522 100644 --- a/src/mldebug/client_debug.py +++ b/src/mldebug/client_debug.py @@ -18,10 +18,10 @@ from mldebug.batch_runner import BatchRunner from mldebug.debug_state import DebugState from mldebug.debug_server import DebugServer +from mldebug.extra.kernel_info import format_kernel_info +from mldebug.extra.layer_report import format_layer_report from mldebug.interactive_controller import InteractiveController -from mldebug.kernel_info import format_kernel_info from mldebug.layer_info import LayerInfo -from mldebug.layer_report import format_layer_report from mldebug.memory_dumper import MemoryDumper from mldebug.utils import LOGGER, register_debug_server diff --git a/src/mldebug/extra/aie_guidance.py b/src/mldebug/extra/aie_guidance.py index 8f04923..5dddf15 100644 --- a/src/mldebug/extra/aie_guidance.py +++ b/src/mldebug/extra/aie_guidance.py @@ -479,7 +479,7 @@ def print_results(self, show_passed: bool = False, show_guidance: bool = True) - # Show guidance and values if requested and failed if show_guidance and not result.passed: - print(f" | {'':10} | {'':35} | → {result.guidance}") + print(f" | {'':10} | {'':35} | -> {result.guidance}") if result.actual_value is not None: print( f" | {'':10} | {'':35} | Actual: {result.actual_value}, Expected: {result.expected_value}" diff --git a/src/mldebug/extra/calltree.py b/src/mldebug/extra/calltree.py index f1d2b02..d9951f4 100644 --- a/src/mldebug/extra/calltree.py +++ b/src/mldebug/extra/calltree.py @@ -249,8 +249,8 @@ def _visualize_tree(self, node, prefix="", is_last=True, is_root=True): connector = "" new_prefix = "" else: - connector = "└── " if is_last else "├── " - new_prefix = prefix + (" " if is_last else "│ ") + connector = "|-- " + new_prefix = prefix + (" " if is_last else "| ") tail_marker = " [tail-call]" if node.is_tail_call else "" func_display = f"{node.func_name} (0x{node.pc:x}){tail_marker}" @@ -335,8 +335,8 @@ def get_calltree(self, root_func=None, include_summary=False): for root_addr in root_addrs: root_name = self._addr_to_name.get(root_addr, f"<0x{root_addr:x}>") - output.append(f"\n┌─ Call tree for: {root_name}") - output.append("│") + output.append(f"\n+- Call tree for: {root_name}") + output.append("|") tree = self._build_call_tree(root_addr) output.append(self._visualize_tree(tree)) @@ -362,11 +362,11 @@ def get_call_relationships(self): lines.append(f"\n{func.name} (0x{addr:x}):") for call_pc, target in func.calls: target_name = self._addr_to_name.get(target, f"") - lines.append(f" ├─ calls {target_name} at PC 0x{call_pc:x}") + lines.append(f" |- calls {target_name} at PC 0x{call_pc:x}") if func.tail_jump_target and func.tail_jump_target in self._addr_to_name: target_name = self._addr_to_name[func.tail_jump_target] if not target_name.startswith("."): - lines.append(f" └─ tail-calls {target_name}") + lines.append(f" |- tail-calls {target_name}") return "\n".join(lines) diff --git a/src/mldebug/kernel_info.py b/src/mldebug/extra/kernel_info.py similarity index 98% rename from src/mldebug/kernel_info.py rename to src/mldebug/extra/kernel_info.py index 277762c..7cf69fd 100644 --- a/src/mldebug/kernel_info.py +++ b/src/mldebug/extra/kernel_info.py @@ -149,8 +149,8 @@ def visit(symbol, branch, child_prefix): last = i == len(callees) - 1 visit( callee, - child_prefix + ("└── " if last else "├── "), - child_prefix + (" " if last else "│ "), + child_prefix + "|-- ", + child_prefix + (" " if last else "| "), ) visit(root_symbol, "", "") diff --git a/src/mldebug/layer_report.py b/src/mldebug/extra/layer_report.py similarity index 91% rename from src/mldebug/layer_report.py rename to src/mldebug/extra/layer_report.py index de98a4d..3c25d72 100644 --- a/src/mldebug/layer_report.py +++ b/src/mldebug/extra/layer_report.py @@ -11,8 +11,8 @@ Each layer is one record of two lines: every number on the first, then the two names stacked in one text column: - BE_ID #ITERS #STAMPS MLADF_IDS BE_LAYER_NAME - └ KERNEL + ID #ITERS #STAMPS #MLADF LAYER_NAME + |-- KERNEL The numbers are fixed-width and left of the names, and neither name is capped, so a name wider than the terminal costs readability of that name only -- every @@ -23,15 +23,14 @@ _UNKNOWN = "?" _SEPARATOR_CHAR = "-" -# Numeric column widths, each sized for its header since those are wider than -# the values in practice. MLADF_IDS also has to fit a span like "60,79-131". +# Numeric column widths, each sized for its header. _ID_W = 4 _ITERS_W = 7 _STAMPS_W = 7 _MLADF_W = 7 _GAP = " " -# Hangs KERNEL under BE_LAYER_NAME so the second line reads as subordinate. -_KERNEL_MARK = "└ " +# Hangs KERNEL under LAYER_NAME so the second line reads as subordinate. +_KERNEL_MARK = "|-- " def _numbers(be_id, iters, stamps, mladf): diff --git a/src/mldebug/layer_info.py b/src/mldebug/layer_info.py index 0b39b4f..329b567 100644 --- a/src/mldebug/layer_info.py +++ b/src/mldebug/layer_info.py @@ -946,12 +946,9 @@ def _warn_if_scheduled_in_chunks(self, info): segments = self.mladf_report.get_layer_id_segments(bilo) if len(segments) < 2: return - span = self.mladf_report.format_layer_id_display([i for s in segments for i in s]) LOGGER.log( f"[WARNING] Layer {bilo} ({info.get('layer_name', '')}) is scheduled in " - f"{len(segments)} chunks ({span}) but buffer_info describes it as one layer. " - "Stepping it whole, positioned at its last chunk; its iteration count " - "covers every chunk and its ELF is taken from the first." + f"{len(segments)} chunks." ) def _reorder_layers_by_execution(self): diff --git a/src/mldebug/work_dir.py b/src/mldebug/work_dir.py index 6d60979..1297ce0 100644 --- a/src/mldebug/work_dir.py +++ b/src/mldebug/work_dir.py @@ -13,7 +13,7 @@ from pathlib import Path from mldebug.extra.calltree import AIECallTree -from mldebug.kernel_info import build_kernel_info +from mldebug.extra.kernel_info import build_kernel_info from mldebug.utils import LOGGER, is_aarch64, is_windows @@ -621,7 +621,7 @@ def _parse_lst_llvm(self, elf, stampid, arch_name, dump_lst): function_name = self.parse_function_sig_llvm(m_fc.group(2)) start_pc = int(m_fc.group(1), base=16) in_func = AIEFunction(function_name, start_pc, 0, 0, False) - # end pc — match insn lines only; "ret" in path text (e.g. pretrained) is not an insn + # end pc -- match insn lines only; "ret" in path text (e.g. pretrained) is not an insn elif self._is_llvm_insn_line(line) and re.search(r"\bret\b", line): # functions with multiple returns if not in_func: From e527c352a0aa9c2d693357e6ba7eba536d40dd3c Mon Sep 17 00:00:00 2001 From: anurag Date: Fri, 21 Aug 2026 16:22:21 -0600 Subject: [PATCH 5/5] make reports work on peano Signed-off-by: anurag --- src/mldebug/extra/kernel_info.py | 43 ++++++++++++++++++++++++++------ src/mldebug/work_dir.py | 38 +++++++++++++++++----------- 2 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/mldebug/extra/kernel_info.py b/src/mldebug/extra/kernel_info.py index 7cf69fd..a7827e1 100644 --- a/src/mldebug/extra/kernel_info.py +++ b/src/mldebug/extra/kernel_info.py @@ -2,7 +2,7 @@ # Copyright (C) 2024-2026 Advanced Micro Devices, Inc. All rights reserved. """ -Rebuild a kernel's call tree from the linker map file. +Rebuild a kernel's call tree from the compiler's build artifacts. Some superkernels fuse many ops behind a single wrapper that the debugger can only break on as a whole -- templated-graph (TG) kernels today. The caller @@ -10,17 +10,20 @@ for whatever kernel entry PC it is handed, so the interactive 'i'/info() command can show what a layer's kernel is actually made of. -The map file's PM section lists every function with its address range, stack -frame and callees, which is enough to recover the tree without the compiler's -own .calltree file. Only the Synopsys bridge (Chess) layout is parsed today: +Where the call edges come from depends on the compiler: + +Chess. The bridge map file's PM section lists every function with its address +range, stack frame and callees, which is all we need: 0x000009e0..0x00000b57 ( 376 items) : :: (Function, Global, .text) ... Called functions : -Peano/lld map files use a different layout and yield nothing; add a second -parser here when that flow is needed. +Peano. The lld map file is a plain symbol table with no call graph in it at +all, so the edges come from the disassembly instead -- the `jl` targets that +AIECallTree already recovers. Sizes and stack frames are map-only, so they read +as unknown for Peano designs. """ import re @@ -28,6 +31,8 @@ from collections import namedtuple from dataclasses import dataclass, field +from mldebug.extra.calltree import AIECallTree + _MEM_SECTION = re.compile(r"^Memory map for memory '(\S+)':") _ENTRY = re.compile(r"^\s+0x([0-9a-f]+)\.\.0x[0-9a-f]+\s+\(\s*(\d+) items\)\s*:\s*(.+?)\s*$") _FUNCTION = re.compile( @@ -132,7 +137,7 @@ def _readable(name): return m_mangled.group(2)[: int(m_mangled.group(1))] or name -def _build_tree(root_symbol, functions, demangle): +def _build_tree(root_symbol, functions, name_of): """Flatten the call tree under root_symbol into pre-order KernelNodes.""" nodes = [] expanded = set() @@ -140,7 +145,7 @@ def _build_tree(root_symbol, functions, demangle): def visit(symbol, branch, child_prefix): func = functions[symbol] repeated = symbol in expanded - nodes.append(KernelNode(func, _readable(demangle(symbol)), branch, repeated)) + nodes.append(KernelNode(func, _readable(name_of(symbol)), branch, repeated)) if repeated: return expanded.add(symbol) @@ -177,6 +182,28 @@ def build_kernel_info(map_path, start_pc, aie_functions, demangle, location): return KernelInfo(location, nodes) +def _functions_from_lst(lst): + """MapFunction view of Peano disassembly, keyed by entry PC; sizes are map-only.""" + functions = {} + for addr, func in AIECallTree.from_string(lst).functions.items(): + callees = list(dict.fromkeys(target for _, target in func.calls)) + functions[addr] = MapFunction(addr, addr, None, None, callees) + return functions + + +def build_kernel_info_from_lst(lst, start_pc, aie_functions, location): + """Same as build_kernel_info, for Peano designs whose map file has no call graph.""" + functions = _functions_from_lst(lst) + if start_pc not in functions: + return None + # The LST parser's own names stop at the first non-word character, so take + # them from the work dir's function database instead. + names = {func.start_pc: func.name for func in aie_functions} + nodes = _build_tree(start_pc, functions, lambda pc: names.get(pc) or f"<0x{pc:x}>") + _attach_pcs(nodes, aie_functions) + return KernelInfo(location, nodes) + + _Column = namedtuple("_Column", "label width align value") diff --git a/src/mldebug/work_dir.py b/src/mldebug/work_dir.py index 1297ce0..4900fb4 100644 --- a/src/mldebug/work_dir.py +++ b/src/mldebug/work_dir.py @@ -13,7 +13,7 @@ from pathlib import Path from mldebug.extra.calltree import AIECallTree -from mldebug.extra.kernel_info import build_kernel_info +from mldebug.extra.kernel_info import build_kernel_info, build_kernel_info_from_lst from mldebug.utils import LOGGER, is_aarch64, is_windows @@ -671,9 +671,29 @@ def _find_elf(self, sid, elf_id): None, ) + def _stamp_kernel_info(self, sid, stamp, demangle): + """ + Call tree of one layer stamp's kernel, or None when its ELF cannot supply one. + """ + elf = self._find_elf(sid, stamp.elf_name) + if not elf or not stamp.start_pc: + return None + flist = self.stamps[sid].aie_functions[elf] + location = f"stamp {sid} (elf {elf})" + # Peano's lld map has no call graph, so its edges come from the disassembly. + if self.peano: + lst = dict(self.stamps[sid].lst_map).get(elf) + if not lst: + return None + return build_kernel_info_from_lst(lst, stamp.start_pc, flist, location) + map_path = Path(self.aie_dir) / "aie" / elf / "Release" / f"{elf}.map" + if not map_path.is_file(): + return None + return build_kernel_info(map_path, stamp.start_pc, flist, demangle, location) + def get_kernel_info(self, layer_stamps): """ - Call tree of each of a layer's stamps, one KernelInfo per stamp found in a map file. + Call tree of each of a layer's stamps, one KernelInfo per stamp we can resolve. """ cache = {} @@ -684,19 +704,7 @@ def demangle(symbol): kernels = [] for sid, stamp in enumerate(layer_stamps[: self.stamps_per_batch]): - elf = self._find_elf(sid, stamp.elf_name) - if not elf or not stamp.start_pc: - continue - map_path = Path(self.aie_dir) / "aie" / elf / "Release" / f"{elf}.map" - if not map_path.is_file(): - continue - kernel = build_kernel_info( - map_path, - stamp.start_pc, - self.stamps[sid].aie_functions[elf], - demangle, - f"stamp {sid} (elf {elf})", - ) + kernel = self._stamp_kernel_info(sid, stamp, demangle) if kernel: kernels.append(kernel) return kernels