char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
67672dc5b
char/aliceai-t5-moe
1#!/usr/bin/env python3 2 3import sys 4import os 5import re 6import argparse 7import statistics 8import logging 9import bisect 10from typing import Any ,Dict ,List ,Optional ,Iterable 11 12from collections import defaultdict 13 14# Mapping of cli-friendly names to (internal_data_key, Display Header, numeric_sort_key) 15COL_MAP = { 16"tot-usec" : ("tot_usec" ,"Tot usec" ,"_sort_tot_usec" ), 17"op" : ("op" ,"Op" ,"op" ), 18"dims" : ("dims" ,"Dims" ,"dims" ), 19"dtypes" : ("dtypes" ,"DTypes" ,"dtypes" ), 20"count" : ("count" ,"Count" ,"_sort_count" ), 21"max-usec" : ("max_usec" ,"Max usec" ,"_sort_max_usec" ), 22"avg-usec" : ("avg_usec" ,"Avg usec" ,"_sort_avg_usec" ), 23"max-cycles" : ("max_cycles" ,"Max Cycles" ,"_sort_max_cycles" ), 24"avg-cycles" : ("avg_cycles" ,"Avg Cycles" ,"_sort_avg_cycles" ), 25"max-pmu" : ("max_pmu" ,"Max PMU" ,"_sort_max_pmu" ), 26"avg-pmu" : ("avg_pmu" ,"Avg PMU" ,"_sort_avg_pmu" ), 27} 28 29op_pattern = re .compile ( 30r"profile-op\s+(?P<op_name>[A-Z_0-9+]+):\s+.*?\s+:\s+(?P<dims>[\d:x\s\->!]+)\s+:\s+(?P<types>[a-z\d_\s\->x]+)\s+:\s+.*?\s+:\s+(?:op-)?usec\s+(?P<usec>\d+)\s+(?:op-)?cycles\s+(?P<cycles>\d+)(?:\s+start\s+(?P<start>\d+))?(?:\s+mhz\s+(?P<mhz>[\d.]+))?(?:\s+pmu\s+\[(?P<pmu>[\d,\s]+)\])?(?:\s+evt\s+\[(?P<evt>[\d,\s]+)\])?" 31) 32 33trace_pattern = re .compile ( 34r"trace-evt\s+(?P<event>[A-Z_0-9\-]+):\s+thread\s+(?P<thread>\d+)\s+info\s+(?P<info>\d+)\s+(?P<state>start|stop)\s+(?P<cycles>\d+)" 35) 36 37device_pattern = re .compile (r"\b(HTP\d+(?::\d+)?)\s+(?:profile-op|trace-evt)\b" ) 38 39 40def extract_device (line ): 41m = device_pattern .search (line ) 42if m : 43return m .group (1 ) 44return "HTP0" 45 46 47def device_matches (record_device ,target_device ): 48targets = [t .strip ()for t in target_device .split (',' )] 49for target in targets : 50if record_device == target : 51return True 52if record_device .startswith (target + ":" ): 53return True 54return False 55 56 57logger = logging .getLogger ("ggml-hexagon-profile" ) 58 59 60def normalize_event_name (evt_type ): 61if evt_type == "HVX_COMP" : 62return "V-COMP" 63if evt_type == "HMX_COMP" : 64return "M-COMP" 65 66# Strip HVX_ or HMX_ prefixes 67name = evt_type 68if name .startswith ("HVX_" )or name .startswith ("HMX_" ): 69name = name [4 :] 70return name .replace ("_" ,"-" ) 71 72 73class CycleUnwrapper : 74def __init__ (self ,initial_val = None ): 75if initial_val is not None : 76self .last_raw = initial_val & 0xFFFFFFFF 77self .high_part = initial_val & 0xFFFFFFFF00000000 78else : 79self .last_raw = None 80self .high_part = 0 81 82def unwrap (self ,raw ): 83if self .last_raw is None : 84self .last_raw = raw 85return raw 86diff = raw - self .last_raw 87if diff < - 0x80000000 : 88self .high_part += 0x100000000 89elif diff > 0x80000000 : 90self .high_part -= 0x100000000 91self .last_raw = raw 92return raw + self .high_part 93 94 95def parse_log (file_path ,pmu_index = None ,limit = None ,device_filter = None ,op_filter_re = None ): 96try : 97if file_path != "-" : 98f = open (file_path ,'r' ,encoding = 'utf-8' ,errors = 'ignore' ) 99else : 100f = os .fdopen (0 ,'r' ,encoding = 'utf-8' ,errors = 'ignore' ) 101except FileNotFoundError : 102logger .error (f"file ' { file_path } ' not found." ) 103sys .exit (1 ) 104 105all_ops :List [Dict [str ,Any ]]= [] 106all_traces :List [Dict [str ,Any ]]= [] 107current_op :Optional [Dict [str ,Any ]]= None 108ops_count_per_device = {} 109if device_filter is not None : 110for target in device_filter .split (',' ): 111ops_count_per_device [target .strip ()]= 0 112limit_reached = False 113 114timestamp_pattern = re .compile (r"(?P<min>\d+)\.(?P<sec>\d+)\.(?P<ms>\d+)\.(?P<us>\d+)\s+[A-Z]\s+" ) 115unwrappers = {} 116last_batch_start = {} 117trace_unwrappers = {} 118 119for line in f : 120if "profile-op" not in line and "trace-evt" not in line : 121continue 122 123ts_match = timestamp_pattern .search (line ) 124abs_usec = 0 125if ts_match : 126abs_usec = ( 127 (int (ts_match .group ('min' ))* 60 + int (ts_match .group ('sec' )))* 1000000 128+ int (ts_match .group ('ms' ))* 1000 129+ int (ts_match .group ('us' )) 130 ) 131 132device = extract_device (line ) 133 134idx = line .find ("profile-op" ) 135if idx != - 1 and "|" in line [idx :]: 136parts = [p .strip ()for p in line [idx :].split ("|" )] 137prefix = parts [0 ] 138prefix_match = re .search (r"profile-op\s+(?P<op_name>[A-Z_0-9+]+)" ,prefix ) 139if not prefix_match : 140continue 141 142names = parts [1 ] 143if len (parts )== 7 : 144dims ,types ,timings = parts [2 ],parts [3 ],parts [6 ] 145elif len (parts )== 6 : 146dims ,types ,timings = parts [2 ],parts [3 ],parts [5 ] 147else : 148continue 149 150timing_match = re .search ( 151r"(?:op-)?usec\s+(?P<usec>\d+)\s+(?:op-)?cycles\s+(?P<cycles>\d+)(?:\s+start\s+(?P<start>\d+))?(?:\s+mhz\s+(?P<mhz>[\d.]+))?(?:\s+pmu\s+\[(?P<pmu>[\d,\s]+)\])?(?:\s+evt\s+\[(?P<evt>[\d,\s]+)\])?" , 152timings 153 ) 154if not timing_match : 155continue 156 157op_match = timing_match 158op_name = prefix_match .group ("op_name" ) 159else : 160op_match = op_pattern .search (line ) 161if op_match : 162op_name = op_match .group ('op_name' ) 163names = "" 164dims = op_match .group ('dims' ).strip () 165types = op_match .group ('types' ).strip () 166else : 167op_match = None 168 169if op_match : 170pmu_raw = op_match .group ('pmu' )if 'pmu' in op_match .groupdict ()else None 171pmu_val = None 172if pmu_raw and pmu_index is not None : 173try : 174pmu_list = [int (x .strip ())for x in pmu_raw .split (',' )] 175if len (pmu_list )> pmu_index : 176pmu_val = pmu_list [pmu_index ] 177except (ValueError ,IndexError ): 178pmu_val = None 179 180evt_val = None 181if types .startswith ("evt-cnt " ): 182try : 183evt_val = [int (x .strip ())for x in types [8 :].split (',' )] 184except ValueError : 185evt_val = None 186 187cycles_start_raw = op_match .group ('start' ) 188unwrapped_cycles_start = None 189if op_name == "OPBATCH" : 190if cycles_start_raw : 191unwrapped_cycles_start = int (cycles_start_raw ) 192unwrappers [device ]= CycleUnwrapper (unwrapped_cycles_start ) 193last_batch_start [device ]= unwrapped_cycles_start 194for k in list (trace_unwrappers .keys ()): 195if k [0 ]== device : 196del trace_unwrappers [k ] 197else : 198if cycles_start_raw : 199device_unwrapper = unwrappers .get (device ) 200if device_unwrapper is not None : 201unwrapped_cycles_start = device_unwrapper .unwrap (int (cycles_start_raw )) 202 203op_text = re .sub (r"^profile-op\s+" ,"" ,line [idx :]).strip ()if idx != - 1 else line .strip () 204 205current_op = { 206'name' :op_name , 207'names' :names , 208'dims' :dims , 209'types' :types , 210'op_text' :op_text , 211'usec' :int (op_match .group ('usec' )), 212'cycles' :int (op_match .group ('cycles' )), 213'cycles_start' :int (cycles_start_raw )if cycles_start_raw else None , 214'unwrapped_cycles_start' :unwrapped_cycles_start , 215'pmu_val' :pmu_val , 216'evt_val' :evt_val , 217'abs_usec' :abs_usec , 218'trace_events' : [], 219'device' :device 220 } 221all_ops .append (current_op ) 222 223# Check if matching early exit criteria 224matched = False 225matched_target = None 226if device_filter is not None : 227targets = [t .strip ()for t in device_filter .split (',' )] 228for target in targets : 229if device == target or device .startswith (target + ":" ): 230matched = True 231matched_target = target 232break 233else : 234matched = True 235matched_target = device 236 237if op_filter_re is not None and not op_filter_re .search (op_text ): 238matched = False 239 240if matched : 241if matched_target not in ops_count_per_device : 242ops_count_per_device [matched_target ]= 0 243ops_count_per_device [matched_target ]+= 1 244 245if limit is not None and len (ops_count_per_device )> 0 and all (count >= limit for count in ops_count_per_device .values ()): 246limit_reached = True 247 248if limit_reached and op_name == "OPBATCH" : 249break 250continue 251 252trace_match = trace_pattern .search (line ) 253if trace_match : 254thread = int (trace_match .group ('thread' )) 255raw_cyc = int (trace_match .group ('cycles' )) 256unwrapped_cyc = None 257th_key = (device ,thread ) 258if th_key not in trace_unwrappers : 259batch_start = last_batch_start .get (device ) 260trace_unwrappers [th_key ]= CycleUnwrapper (batch_start ) 261unwrapped_cyc = trace_unwrappers [th_key ].unwrap (raw_cyc ) 262all_traces .append ({ 263'thread' :thread , 264'event' :trace_match .group ('event' ), 265'info' :int (trace_match .group ('info' )), 266'cycles' :raw_cyc , 267'unwrapped_cycles' :unwrapped_cyc , 268'state' :trace_match .group ('state' ), 269'device' :device 270 }) 271 272f .close () 273 274# Assign start/end cycles to all ops 275for op in all_ops : 276op ['start_cycles' ]= op ['unwrapped_cycles_start' ] 277op ['end_cycles' ]= op ['start_cycles' ]+ op ['cycles' ]if op ['start_cycles' ]is not None else None 278 279# Group ops by device 280valid_ops_by_dev = defaultdict (list ) 281for op in all_ops : 282if op ['start_cycles' ]is not None and op ['end_cycles' ]is not None : 283valid_ops_by_dev [op ['device' ]].append (op ) 284 285# Group trace events by device 286traces_by_dev = defaultdict (list ) 287for e in all_traces : 288if e ['unwrapped_cycles' ]is not None : 289traces_by_dev [e ['device' ]].append (e ) 290 291for device ,dev_ops in valid_ops_by_dev .items (): 292opbatch_ops = [op for op in dev_ops if op ['name' ]== "OPBATCH" ] 293other_ops = [op for op in dev_ops if op ['name' ]!= "OPBATCH" ] 294 295opbatch_ops .sort (key = lambda op :op ['start_cycles' ]) 296other_ops .sort (key = lambda op :op ['start_cycles' ]) 297 298opbatch_starts = [op ['start_cycles' ]for op in opbatch_ops ] 299other_starts = [op ['start_cycles' ]for op in other_ops ] 300 301dev_traces = traces_by_dev .get (device , []) 302for e in dev_traces : 303cyc = e ['unwrapped_cycles' ] 304 305# Map to OPBATCH 306idx = bisect .bisect_right (opbatch_starts ,cyc )- 1 307if idx >= 0 : 308op = opbatch_ops [idx ] 309if op ['start_cycles' ]<= cyc <= op ['end_cycles' ]: 310op ['trace_events' ].append (e ) 311 312# Map to other ops 313idx = bisect .bisect_right (other_starts ,cyc )- 1 314if idx >= 0 : 315op = other_ops [idx ] 316if op ['start_cycles' ]<= cyc <= op ['end_cycles' ]: 317op ['trace_events' ].append (e ) 318 319return all_ops 320 321 322def print_bubbles_timeline (op ): 323op_name = op ['name' ] 324dims = op ['dims' ] 325types = op ['types' ] 326usec = op ['usec' ] 327cycles = op ['cycles' ] 328events = op ['trace_events' ] 329logger .info ("=" * 100 ) 330logger .info (f" { op_name } ( { dims } : { types } ) - { usec } usec { cycles } cycles" ) 331logger .info ("=" * 100 ) 332 333if not events : 334logger .info (" No trace events recorded." ) 335return 336 337# Identify start and end cycles for this operator 338op_start = op ['start_cycles' ] 339op_end = op ['end_cycles' ] 340if op_start is None or op_end is None : 341logger .info (" Cannot analyze bubbles: missing start/end cycle counts." ) 342return 343 344batch_duration = op_end - op_start 345if batch_duration <= 0 : 346logger .info (" Cannot analyze bubbles: batch duration is 0." ) 347return 348 349# Group events by (thread, track_type) 350tracks = defaultdict (list ) 351for e in events : 352t = e ['thread' ] 353is_dma = (normalize_event_name (e ['event' ])== 'DMA' ) 354track_type = 'dma' if is_dma else 'compute' 355tracks [(t ,track_type )].append (e ) 356 357active_threads = sorted (list (set (t for (t ,track_type )in tracks .keys ()))) 358if not active_threads : 359logger .info (" No active threads in trace." ) 360return 361 362bubble_threshold = 10000 # 10k cycles 363 364thread_stats = {} 365for t in active_threads : 366thread_stats [t ]= { 367'compute_idle_cycles' :batch_duration , 368'compute_idle_pct' :100.0 , 369'compute_bubbles' : [], 370 371'dma_idle_cycles' :batch_duration , 372'dma_idle_pct' :100.0 , 373'dma_bubbles' : [] 374 } 375 376total_compute_idle_pct = 0.0 377total_dma_idle_pct = 0.0 378 379for t in active_threads : 380for track_type in ['compute' ,'dma' ]: 381key = (t ,track_type ) 382track_events = tracks .get (key , []) 383 384if not track_events : 385gaps = [(op_start ,op_end )] 386idle_cycles = batch_duration 387else : 388track_events = sorted (track_events ,key = lambda e :e .get ('unwrapped_cycles' )or e ['cycles' ]) 389 390active_intervals = [] 391active_count = 0 392curr_start = None 393 394for e in track_events : 395cyc = e .get ('unwrapped_cycles' )or e ['cycles' ] 396cyc = max (op_start ,min (op_end ,cyc )) 397state = e ['state' ] 398 399if state == 'start' : 400if active_count == 0 : 401curr_start = cyc 402active_count += 1 403elif state == 'stop' : 404if active_count > 0 : 405active_count -= 1 406if active_count == 0 : 407active_intervals .append ((curr_start ,cyc )) 408else : 409active_intervals .append ((op_start ,cyc )) 410 411if active_count > 0 and curr_start is not None : 412active_intervals .append ((curr_start ,op_end )) 413 414# Merge intervals 415active_intervals .sort (key = lambda x :x [0 ]) 416merged_intervals = [] 417for start ,end in active_intervals : 418if not merged_intervals : 419merged_intervals .append ([start ,end ]) 420else : 421last_start ,last_end = merged_intervals [- 1 ] 422if start <= last_end : 423merged_intervals [- 1 ][1 ]= max (last_end ,end ) 424else : 425merged_intervals .append ([start ,end ]) 426 427# Calculate gaps 428gaps = [] 429curr_time = op_start 430for start ,end in merged_intervals : 431if start > curr_time : 432gaps .append ((curr_time ,start )) 433curr_time = max (curr_time ,end ) 434if curr_time < op_end : 435gaps .append ((curr_time ,op_end )) 436 437idle_cycles = sum (end - start for start ,end in gaps ) 438 439idle_pct = (idle_cycles / batch_duration )* 100.0 440 441bubbles = [] 442for start ,end in gaps : 443dur = end - start 444if dur >= bubble_threshold : 445bubbles .append ((start ,end ,dur )) 446 447if track_type == 'compute' : 448thread_stats [t ]['compute_idle_cycles' ]= idle_cycles 449thread_stats [t ]['compute_idle_pct' ]= idle_pct 450thread_stats [t ]['compute_bubbles' ]= bubbles 451total_compute_idle_pct += idle_pct 452else : 453thread_stats [t ]['dma_idle_cycles' ]= idle_cycles 454thread_stats [t ]['dma_idle_pct' ]= idle_pct 455thread_stats [t ]['dma_bubbles' ]= bubbles 456total_dma_idle_pct += idle_pct 457 458avg_compute_idle = total_compute_idle_pct / len (active_threads ) 459avg_dma_idle = total_dma_idle_pct / len (active_threads ) 460 461logger .info (" Combined Idle Statistics:" ) 462logger .info (f" Active Threads : { ', ' . join ( str ( t ) for t in active_threads ) } " ) 463logger .info (f" Avg Thread Compute IDLE : { avg_compute_idle :.1f } %" ) 464logger .info (f" Avg Thread DMA IDLE : { avg_dma_idle :.1f } %" ) 465logger .info ("-" * 100 ) 466 467logger .info (" Per-Thread Idle Analysis:" ) 468for t in active_threads : 469stats = thread_stats [t ] 470thread_name = f"Thread { t :<2 } (HVX)" if t != 10 else "Thread 10 (HMX)" 471logger .info (f" { thread_name } -> Compute Idle: { stats [ 'compute_idle_pct' ]:.1f } % | DMA Idle: { stats [ 'dma_idle_pct' ]:.1f } %" ) 472 473all_bubbles = [] 474for t in active_threads : 475stats = thread_stats [t ] 476assert isinstance (stats ['dma_bubbles' ],Iterable ) 477assert isinstance (stats ['compute_bubbles' ],Iterable ) 478for start ,end ,dur in stats ['compute_bubbles' ]: 479pct = (dur / batch_duration )* 100.0 480all_bubbles .append ((dur ,f"Thread { t } Compute: bubble of { dur } cycles ( { pct :.1f } %) at { start - op_start } to { end - op_start } " )) 481for start ,end ,dur in stats ['dma_bubbles' ]: 482pct = (dur / batch_duration )* 100.0 483all_bubbles .append ((dur ,f"Thread { t } DMA : bubble of { dur } cycles ( { pct :.1f } %) at { start - op_start } to { end - op_start } " )) 484 485if all_bubbles : 486logger .info ("-" * 100 ) 487logger .info (f" Significant Bubbles (>= { bubble_threshold } cycles):" ) 488all_bubbles .sort (key = lambda x :x [0 ],reverse = True ) 489for dur ,desc in all_bubbles [:15 ]: 490logger .info (f" { desc } " ) 491else : 492logger .info ("-" * 100 ) 493logger .info (f" No significant bubbles detected (all idle gaps < { bubble_threshold } cycles)." ) 494 495 496def print_ascii_summary (op_name ,dims ,types ,usec ,cycles ,events ): 497logger .info ("=" * 100 ) 498logger .info (f" { op_name } ( { dims } : { types } ) - { usec } usec { cycles } cycles" ) 499logger .info ("=" * 100 ) 500 501events = sorted (events ,key = lambda e :e ['cycles' ]) 502if not events : 503logger .info (" No trace events recorded." ) 504return 505 506active_starts = {} 507thread_totals = defaultdict (lambda :defaultdict (int )) 508 509for e in events : 510t = e ['thread' ] 511evt = e ['event' ] 512info = e ['info' ] 513cyc = e ['cycles' ] 514state = e ['state' ] 515 516key = (t ,evt ,info ) 517if state == 'start' : 518active_starts [key ]= cyc 519elif state == 'stop' : 520if key in active_starts : 521start_cyc = active_starts [key ] 522del active_starts [key ] 523 524if cyc >= start_cyc : 525dur = cyc - start_cyc 526else : 527dur = (cyc + 0x100000000 )- start_cyc 528 529norm_evt = normalize_event_name (evt ) 530thread_totals [t ][norm_evt ]+= dur 531 532for t in sorted (thread_totals .keys ()): 533thread_name = f"Thread { t } (HVX)" if t != 10 else "Thread 10 (HMX)" 534sorted_evts = sorted (thread_totals [t ].items (),key = lambda item :item [0 ]) 535 536evt_strs = [] 537for evt ,dur in sorted_evts : 538pct = (dur / cycles * 100 )if cycles > 0 else 0 539evt_strs .append (f" { evt } { dur } ( { pct :.1f } %)" ) 540 541logger .info (f" { thread_name :<16 } : " + " | " .join (evt_strs )) 542 543 544def generate_report (ops ,top_n ,width_overrides ,sort_col ,pmu_name = None ): 545if not ops : 546logger .info ("No valid records found." ) 547return 548 549grouped = defaultdict (list ) 550for op in ops : 551key = (op ['name' ],op ['dims' ],op ['types' ]) 552grouped [key ].append (op ) 553 554group_stats = [] 555for (name ,dims ,types ),group_ops in grouped .items (): 556usecs = [o ['usec' ]for o in group_ops ] 557cycles = [o ['cycles' ]for o in group_ops ] 558pmu_vals = [o ['pmu_val' ]for o in group_ops if o ['pmu_val' ]is not None ] 559 560avg_usec_val = statistics .mean (usecs ) 561count_val = len (group_ops ) 562tot_usec_val = avg_usec_val * count_val 563 564group_stats .append ({ 565'op' :name , 566'dims' :dims , 567'dtypes' :types , 568'count' :str (count_val ), 569'max_usec' :str (max (usecs )), 570'avg_usec' :f" { avg_usec_val :.2f } " , 571'tot_usec' :f" { tot_usec_val :.2f } " , 572'max_cycles' :str (max (cycles )), 573'avg_cycles' :f" { statistics . mean ( cycles ):.2f } " , 574'max_pmu' :str (max (pmu_vals ))if pmu_vals else "0" , 575'avg_pmu' :f" { statistics . mean ( pmu_vals ):.2f } " if pmu_vals else "0.00" , 576# Numeric values for accurate sorting 577'_sort_count' :count_val , 578'_sort_max_usec' :max (usecs ), 579'_sort_avg_usec' :avg_usec_val , 580'_sort_tot_usec' :tot_usec_val , 581'_sort_max_cycles' :max (cycles ), 582'_sort_avg_cycles' :statistics .mean (cycles ), 583'_sort_max_pmu' :max (pmu_vals )if pmu_vals else 0 , 584'_sort_avg_pmu' :statistics .mean (pmu_vals )if pmu_vals else 0 585 }) 586 587# Sorting logic 588actual_sort_key = COL_MAP [sort_col ][2 ] 589is_numeric = actual_sort_key .startswith ("_" )or actual_sort_key == "count" 590sorted_groups = sorted (group_stats ,key = lambda x :x [actual_sort_key ],reverse = is_numeric )[:top_n ] 591 592# Define initial column order 593active_cols = ["op" ,"dims" ,"dtypes" ] 594if pmu_name : 595active_cols += ["max-pmu" ,"avg-pmu" ] 596active_cols += ["tot-usec" ,"avg-usec" ,"avg-cycles" ,"max-usec" ,"max-cycles" ,"count" ] 597 598final_headers ,final_keys ,final_widths = [], [], [] 599 600for col_name in active_cols : 601data_key ,header_text ,_ = COL_MAP [col_name ] 602if "pmu" in col_name and pmu_name : 603header_text = header_text .replace ("PMU" ,pmu_name ) 604 605natural_width = max ([len (str (row [data_key ]))for row in sorted_groups ]+ [len (header_text )]) 606target_width = width_overrides .get (col_name ,natural_width ) 607 608if target_width == 0 : 609continue 610 611final_headers .append (header_text ) 612final_keys .append (data_key ) 613final_widths .append (target_width ) 614 615# Print Report 616logger .info (f"\n# Profile Report (Top { top_n } Ops sorted by { sort_col } )\n" ) 617header_line = "| " + " | " .join (f" { h :<{ final_widths [ i ]} } " for i ,h in enumerate (final_headers ))+ " |" 618sep_line = "| " + " | " .join ("-" * final_widths [i ]for i in range (len (final_headers )))+ " |" 619logger .info (header_line ) 620logger .info (sep_line ) 621 622for group in sorted_groups : 623row_vals = [] 624for i ,key in enumerate (final_keys ): 625val = str (group [key ]) 626if len (val )> final_widths [i ]: 627val = val [:final_widths [i ]- 3 ]+ "..." 628row_vals .append (f" { val :<{ final_widths [ i ]} } " ) 629logger .info ("| " + " | " .join (row_vals )+ " |" ) 630 631 632def main (): 633parser = argparse .ArgumentParser (description = "Post-process Op profile info." ) 634parser .add_argument ("logfile" ) 635parser .add_argument ("-n" ,"--top" ,type = int ,default = 100 ) 636parser .add_argument ("--sort" ,type = str ,default = "tot-usec" ,choices = list (COL_MAP .keys ())) 637parser .add_argument ("--pmu-index" ,type = int ) 638parser .add_argument ("--pmu-name" ,type = str ) 639parser .add_argument ("--width" ,action = 'append' ,default = ['dims:40' ],help = "Override column width, e.g. --width dims:50" ) 640parser .add_argument ("--timeline" ,type = str ,nargs = '?' ,const = 'summary' ,choices = ["summary" ,"bubbles" ], 641help = "Output ASCII art event summary or thread idle bubble analysis (default: summary)" ) 642parser .add_argument ("--filter" ,type = str ,help = "Regex filter matching against the original profile-op line" ) 643parser .add_argument ("--device" ,type = str ,help = "Device to filter by (e.g. HTP0, HTP0:0) or 'split' to generate separate reports per device" ) 644 645group = parser .add_mutually_exclusive_group () 646group .add_argument ("--head" ,type = int ,help = "Limit to first N ops" ) 647group .add_argument ("--tail" ,type = int ,help = "Limit to last N ops" ) 648 649args = parser .parse_args () 650 651logging .basicConfig (level = logging .INFO ,format = '%(message)s' ) 652 653if "pmu" in args .sort and args .pmu_index is None : 654logger .error (f"Cannot sort by ' { args . sort } ' without --pmu-index." ) 655sys .exit (1 ) 656 657overrides = {} 658if args .width : 659for w in args .width : 660try : 661name ,val = w .split (':' ) 662overrides [name .lower ()]= int (val ) 663except ValueError : 664logger .warning (f"Invalid width format ' { w } '" ) 665 666final_pmu_name = (args .pmu_name or f"# { args . pmu_index } " )if args .pmu_index is not None else None 667 668op_filter_re = None 669if args .filter : 670try : 671op_filter_re = re .compile (args .filter ) 672except re .error as e : 673logger .error (f"Invalid regex filter: { e } " ) 674sys .exit (1 ) 675 676limit = args .head if args .head is not None else None 677device_filter = args .device if (args .device and args .device != "split" )else None 678ops = parse_log (args .logfile ,pmu_index = args .pmu_index ,limit = limit ,device_filter = device_filter ,op_filter_re = op_filter_re ) 679 680if args .device and args .device != "split" : 681ops = [op for op in ops if device_matches (op ['device' ],args .device )] 682 683if args .device == "split" : 684unique_devices = sorted (list (set (op ['device' ]for op in ops ))) 685for dev in unique_devices : 686dev_ops = [op for op in ops if device_matches (op ['device' ],dev )] 687 688if args .filter : 689try : 690filter_re = re .compile (args .filter ) 691except re .error as e : 692logger .error (f"Invalid regex filter: { e } " ) 693sys .exit (1 ) 694dev_ops = [op for op in dev_ops if filter_re .search (op ['op_text' ])] 695 696if args .head is not None : 697dev_ops = dev_ops [:args .head ] 698elif args .tail is not None : 699dev_ops = dev_ops [- args .tail :] 700 701logger .info ("\n=========================================" ) 702logger .info (f" Device: { dev } " ) 703logger .info ("=========================================" ) 704 705if args .timeline : 706for op in dev_ops : 707if args .timeline == "summary" : 708print_ascii_summary (op ['name' ],op ['dims' ],op ['types' ],op ['usec' ],op ['cycles' ],op ['trace_events' ]) 709elif args .timeline == "bubbles" : 710print_bubbles_timeline (op ) 711else : 712generate_report (dev_ops ,args .top ,overrides ,args .sort ,pmu_name = final_pmu_name ) 713else : 714if args .filter : 715try : 716filter_re = re .compile (args .filter ) 717except re .error as e : 718logger .error (f"Invalid regex filter: { e } " ) 719sys .exit (1 ) 720ops = [op for op in ops if filter_re .search (op ['op_text' ])] 721 722if args .head is not None or args .tail is not None : 723ops_by_dev = defaultdict (list ) 724for op in ops : 725ops_by_dev [op ['device' ]].append (op ) 726 727filtered_ops = [] 728for dev in sorted (ops_by_dev .keys ()): 729dev_ops = ops_by_dev [dev ] 730if args .head is not None : 731dev_ops = dev_ops [:args .head ] 732elif args .tail is not None : 733dev_ops = dev_ops [- args .tail :] 734filtered_ops .extend (dev_ops ) 735ops = filtered_ops 736 737if args .timeline : 738for op in ops : 739if args .timeline == "summary" : 740print_ascii_summary (op ['name' ],op ['dims' ],op ['types' ],op ['usec' ],op ['cycles' ],op ['trace_events' ]) 741elif args .timeline == "bubbles" : 742print_bubbles_timeline (op ) 743else : 744generate_report (ops ,args .top ,overrides ,args .sort ,pmu_name = final_pmu_name ) 745 746 747if __name__ == "__main__" : 748main ()