char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
192067b72
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 11from collections import defaultdict 12 13logger = logging .getLogger ("ggml-hexagon-trace" ) 14 15op_pattern = re .compile ( 16r"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+(?P<strides>[\d:x\s\->!]+?)\s+:\s+(?:(?P<params>.*?)\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]+)\])?" 17) 18 19trace_pattern = re .compile ( 20r"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+)" 21) 22 23device_pattern = re .compile (r"\b(HTP\d+(?::\d+)?)\s+(?:profile-op|trace-evt)\b" ) 24 25 26def extract_device (line ): 27m = device_pattern .search (line ) 28if m : 29return m .group (1 ) 30return "HTP0" 31 32 33def device_matches (record_device ,target_device ): 34targets = [t .strip ()for t in target_device .split (',' )] 35for target in targets : 36if record_device == target : 37return True 38if record_device .startswith (target + ":" ): 39return True 40return False 41 42 43def get_split_output_path (base_path ,device_name ): 44safe_device = device_name .replace (':' ,'_' ) 45root ,ext = os .path .splitext (base_path ) 46return f" { root } - { safe_device } { ext } " 47 48 49def normalize_event_name (evt_type ,info = 0 ): 50if evt_type == "HVX_COMP" : 51return "V-COMP" 52if evt_type == "HMX_COMP" : 53return "M-COMP" 54name = evt_type 55if name .startswith ("HVX_" )or name .startswith ("HMX_" ): 56name = name [4 :] 57return name .replace ("_" ,"-" ) 58 59 60class CycleUnwrapper : 61def __init__ (self ,initial_val = None ): 62if initial_val is not None : 63self .last_raw = initial_val & 0xFFFFFFFF 64self .high_part = initial_val & 0xFFFFFFFF00000000 65else : 66self .last_raw = None 67self .high_part = 0 68 69def unwrap (self ,raw ): 70if self .last_raw is None : 71self .last_raw = raw 72return raw 73diff = raw - self .last_raw 74if diff < - 0x80000000 : 75self .high_part += 0x100000000 76elif diff > 0x80000000 : 77self .high_part -= 0x100000000 78self .last_raw = raw 79return raw + self .high_part 80 81 82class DeviceTimeMapper : 83def __init__ (self ,dev ,ops ): 84self .dev = dev 85self .batches = [] 86for op in ops : 87if op .get ('device' )== dev and op .get ('name' )== 'OPBATCH' and op .get ('unwrapped_cycles_start' )is not None : 88cycles = op .get ('cycles' ,0 ) 89usec = op .get ('usec' ,0 ) 90start_cyc = op ['unwrapped_cycles_start' ] 91freq = (cycles / usec )if usec > 0 and cycles > 0 else 1000.0 92if freq <= 0 : 93freq = 1000.0 94self .batches .append ({ 95'start_cycles' :start_cyc , 96'cycles' :cycles , 97'end_cycles' :start_cyc + cycles , 98'usec' :usec , 99'dur_ns' :usec * 1000 , 100'freq_mhz' :freq , 101 }) 102 103self .batches .sort (key = lambda b :b ['start_cycles' ]) 104 105for i ,b in enumerate (self .batches ): 106if i == 0 : 107b ['start_time_ns' ]= 0 108else : 109prev = self .batches [i - 1 ] 110idle_cyc = max (0 ,b ['start_cycles' ]- prev ['end_cycles' ]) 111idle_ns = int (round ((idle_cyc / prev ['freq_mhz' ])* 1000 )) 112b ['start_time_ns' ]= prev ['start_time_ns' ]+ prev ['dur_ns' ]+ idle_ns 113 114self .batch_starts = [b ['start_cycles' ]for b in self .batches ] 115 116valid_starts = [op ['unwrapped_cycles_start' ]for op in ops if op .get ('device' )== dev and op .get ('unwrapped_cycles_start' )is not None ] 117self .min_cyc = min (valid_starts )if valid_starts else 0 118if self .batches : 119self .default_freq = self .batches [0 ]['freq_mhz' ] 120else : 121freqs = [op ['cycles' ]/ op ['usec' ]for op in ops if op .get ('device' )== dev and op .get ('usec' ,0 )> 0 and op .get ('cycles' ,0 )> 0 ] 122self .default_freq = statistics .mean (freqs )if freqs else 1000.0 123 124def get_batch (self ,cyc ): 125if not self .batches : 126return None 127idx = bisect .bisect_right (self .batch_starts ,cyc )- 1 128if idx >= 0 : 129return self .batches [idx ] 130return self .batches [0 ] 131 132def get_freq (self ,cyc = None ): 133if cyc is not None : 134b = self .get_batch (cyc ) 135if b is not None : 136return b ['freq_mhz' ] 137return self .default_freq 138 139def cycle_to_ns (self ,cyc ): 140if cyc is None : 141return 0 142b = self .get_batch (cyc ) 143if b is not None : 144return b ['start_time_ns' ]+ int (round (((cyc - b ['start_cycles' ])/ b ['freq_mhz' ])* 1000 )) 145return int (round (((cyc - self .min_cyc )/ self .default_freq )* 1000 )) 146 147def dur_cycles_to_ns (self ,cyc_start ,cyc_dur ): 148if cyc_dur is None : 149return 0 150freq = self .get_freq (cyc_start ) 151return int (round ((cyc_dur / freq )* 1000 )) 152 153 154def parse_log (file_path ,limit = None ,device_filter = None ,op_filter_re = None ): 155try : 156if file_path != "-" : 157f = open (file_path ,'r' ,encoding = 'utf-8' ,errors = 'ignore' ) 158else : 159f = os .fdopen (0 ,'r' ,encoding = 'utf-8' ,errors = 'ignore' ) 160except FileNotFoundError : 161logger .error (f"file ' { file_path } ' not found." ) 162sys .exit (1 ) 163 164all_ops :List [Dict [str ,Any ]]= [] 165all_traces :List [Dict [str ,Any ]]= [] 166current_op :Optional [Dict [str ,Any ]]= None 167ops_count_per_device = {} 168if device_filter is not None : 169for target in device_filter .split (',' ): 170ops_count_per_device [target .strip ()]= 0 171limit_reached = False 172unwrappers = {} 173last_batch_start = {} 174trace_unwrappers = {} 175line_idx = 0 176 177for line in f : 178line_idx += 1 179if "profile-op" not in line and "trace-evt" not in line : 180continue 181device = extract_device (line ) 182 183idx = line .find ("profile-op" ) 184if idx != - 1 and "|" in line [idx :]: 185parts = [p .strip ()for p in line [idx :].split ("|" )] 186prefix = parts [0 ] 187prefix_match = re .search (r"profile-op\s+(?P<op_name>[A-Z_0-9+]+)" ,prefix ) 188if not prefix_match : 189continue 190 191names = parts [1 ] 192if len (parts )== 7 : 193dims ,types ,strides ,params ,timings = parts [2 ],parts [3 ],parts [4 ],parts [5 ],parts [6 ] 194elif len (parts )== 6 : 195dims ,types ,strides ,params ,timings = parts [2 ],parts [3 ],parts [4 ],"" ,parts [5 ] 196else : 197continue 198 199timing_match = re .search ( 200r"(?: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]+)\])?" , 201timings 202 ) 203if not timing_match : 204continue 205 206op_match = timing_match 207op_name = prefix_match .group ("op_name" ) 208else : 209op_match = op_pattern .search (line ) 210if op_match : 211op_name = op_match .group ('op_name' ) 212names = "" 213dims = op_match .group ('dims' ).strip ()if op_match .group ('dims' )else '' 214types = op_match .group ('types' ).strip ()if op_match .group ('types' )else '' 215strides = op_match .group ('strides' ).strip ()if op_match .group ('strides' )else '' 216params = op_match .group ('params' ).strip ()if ('params' in op_match .groupdict ()and op_match .group ('params' ))else '' 217else : 218op_match = None 219 220if op_match : 221cycles_start_raw = op_match .group ('start' ) 222unwrapped_cycles_start = None 223if op_name == "OPBATCH" : 224if cycles_start_raw : 225unwrapped_cycles_start = int (cycles_start_raw ) 226unwrappers [device ]= CycleUnwrapper (unwrapped_cycles_start ) 227last_batch_start [device ]= unwrapped_cycles_start 228for k in list (trace_unwrappers .keys ()): 229if k [0 ]== device : 230del trace_unwrappers [k ] 231else : 232if cycles_start_raw : 233device_unwrapper = unwrappers .get (device ) 234if device_unwrapper is not None : 235unwrapped_cycles_start = device_unwrapper .unwrap (int (cycles_start_raw )) 236 237op_text = re .sub (r"^profile-op\s+" ,"" ,line [idx :]).strip ()if idx != - 1 else line .strip () 238 239evt_str = None 240if types .startswith ("evt-cnt " ): 241evt_str = types [8 :].strip () 242 243current_op = { 244'name' :op_name , 245'names' :names , 246'dims' :dims , 247'types' :types , 248'strides' :strides , 249'params' :params , 250'evt' :evt_str , 251'op_text' :op_text , 252'usec' :int (op_match .group ('usec' )), 253'cycles' :int (op_match .group ('cycles' )), 254'cycles_start' :int (cycles_start_raw )if cycles_start_raw else None , 255'unwrapped_cycles_start' :unwrapped_cycles_start , 256'trace_events' : [], 257'line_num' :line_idx , 258'device' :device 259 } 260all_ops .append (current_op ) 261 262# Check if matching early exit criteria 263matched = False 264matched_target = None 265if device_filter is not None : 266targets = [t .strip ()for t in device_filter .split (',' )] 267for target in targets : 268if device == target or device .startswith (target + ":" ): 269matched = True 270matched_target = target 271break 272else : 273matched = True 274matched_target = device 275 276if op_filter_re is not None and not op_filter_re .search (op_text ): 277matched = False 278 279if matched : 280if matched_target not in ops_count_per_device : 281ops_count_per_device [matched_target ]= 0 282ops_count_per_device [matched_target ]+= 1 283 284if limit is not None and len (ops_count_per_device )> 0 and all (count >= limit for count in ops_count_per_device .values ()): 285limit_reached = True 286 287if limit_reached and op_name == "OPBATCH" : 288break 289continue 290 291trace_match = trace_pattern .search (line ) 292if trace_match : 293thread = int (trace_match .group ('thread' )) 294raw_cyc = int (trace_match .group ('cycles' )) 295unwrapped_cyc = None 296th_key = (device ,thread ) 297if th_key not in trace_unwrappers : 298batch_start = last_batch_start .get (device ) 299trace_unwrappers [th_key ]= CycleUnwrapper (batch_start ) 300unwrapped_cyc = trace_unwrappers [th_key ].unwrap (raw_cyc ) 301all_traces .append ({ 302'thread' :thread , 303'event' :trace_match .group ('event' ), 304'info' :int (trace_match .group ('info' )), 305'cycles' :raw_cyc , 306'unwrapped_cycles' :unwrapped_cyc , 307'state' :trace_match .group ('state' ), 308'line_num' :line_idx , 309'device' :device 310 }) 311 312f .close () 313return all_ops ,all_traces 314 315# --- Simple protobuf encoder --- 316 317 318def write_varint (val ): 319if val < 0 : 320val = (1 << 64 )+ val 321res = bytearray () 322while True : 323towrite = val & 0x7f 324val >>= 7 325if val > 0 : 326res .append (towrite | 0x80 ) 327else : 328res .append (towrite ) 329break 330return bytes (res ) 331 332 333def pb_field (num ,wire ,data ): 334return write_varint ((num << 3 )| wire )+ data 335 336 337def pb_varint (num ,val ): 338return pb_field (num ,0 ,write_varint (val )) 339 340 341def pb_length_delimited (num ,data ): 342return pb_field (num ,2 ,write_varint (len (data ))+ data ) 343 344 345def pb_string (num ,text ): 346return pb_length_delimited (num ,text .encode ('utf-8' )) 347 348 349# Message Encoders 350def make_process_descriptor (pid ,name ): 351return pb_varint (1 ,pid )+ pb_string (6 ,name ) 352 353 354def make_thread_descriptor (pid ,tid ,name ,sort_index = None ): 355payload = pb_varint (1 ,pid )+ pb_varint (2 ,tid )+ pb_string (5 ,name ) 356if sort_index is not None : 357payload += pb_varint (3 ,sort_index ) 358return payload 359 360 361def make_track_descriptor (uuid ,name = None ,parent_uuid = None ,thread = None ,process = None ,sibling_merge_behavior = None ,child_ordering = None ,sibling_order_rank = None ): 362payload = pb_varint (1 ,uuid ) 363if name is not None : 364payload += pb_string (2 ,name ) 365if parent_uuid is not None : 366payload += pb_varint (5 ,parent_uuid ) 367if process is not None : 368payload += pb_length_delimited (3 ,process ) 369if thread is not None : 370payload += pb_length_delimited (4 ,thread ) 371if sibling_merge_behavior is not None : 372payload += pb_varint (15 ,sibling_merge_behavior ) 373if child_ordering is not None : 374payload += pb_varint (11 ,child_ordering ) 375if sibling_order_rank is not None : 376payload += pb_varint (12 ,sibling_order_rank ) 377return payload 378 379 380def make_debug_annotation (name ,string_val = None ,int_val = None ): 381payload = pb_string (10 ,name ) 382if string_val is not None : 383payload += pb_string (6 ,string_val ) 384elif int_val is not None : 385payload += pb_varint (4 ,int_val ) 386return payload 387 388 389def make_track_event (event_type ,track_uuid ,name = None ,category = None ,debug_annotations = None ): 390payload = pb_varint (9 ,event_type ) 391payload += pb_varint (11 ,track_uuid ) 392if name is not None : 393payload += pb_string (23 ,name ) 394if category is not None : 395payload += pb_string (22 ,category ) 396if debug_annotations is not None : 397for da in debug_annotations : 398payload += pb_length_delimited (4 ,da ) 399return payload 400 401 402def make_trace_packet (timestamp ,track_event = None ,track_descriptor = None ,seq_id = 1 ): 403payload = pb_varint (8 ,timestamp ) 404payload += pb_varint (10 ,seq_id ) 405if track_event is not None : 406payload += pb_length_delimited (11 ,track_event ) 407if track_descriptor is not None : 408payload += pb_length_delimited (60 ,track_descriptor ) 409return payload 410 411 412def write_trace_packet_to_file (f ,packet_bytes ): 413# Write as field 1 of top-level Trace message 414f .write (pb_length_delimited (1 ,packet_bytes )) 415 416# --- End Protobuf Encoder --- 417 418 419def generate_perfetto_trace (filtered_ops ,trace_events ,output_path ): 420if not filtered_ops : 421logger .warning ("No operators found after filtering." ) 422return 423 424# Assign start and end cycles to each operator 425for op in filtered_ops : 426op ['start_cycles' ]= op ['unwrapped_cycles_start' ] 427op ['end_cycles' ]= op ['start_cycles' ]+ op ['cycles' ]if op ['start_cycles' ]is not None else None 428 429# Get list of unique devices present in the operations 430unique_devices = sorted (list (set (op ['device' ]for op in filtered_ops ))) 431device_to_idx = {dev :idx for idx ,dev in enumerate (unique_devices )} 432time_mappers = {dev :DeviceTimeMapper (dev ,filtered_ops )for dev in unique_devices } 433 434# Process events 435completed_events = [] 436if trace_events : 437trace_events = sorted (trace_events ,key = lambda e :e ['unwrapped_cycles' ]) 438 439one_usec_cycles = {} 440for dev in unique_devices : 441one_usec_cycles [dev ]= max (time_mappers [dev ].get_freq (),1.0 ) 442 443active_starts = {} 444for e in trace_events : 445t = e ['thread' ] 446evt = e ['event' ] 447info = e ['info' ] 448state = e ['state' ] 449cyc = e ['unwrapped_cycles' ] 450dev = e ['device' ] 451 452key = (dev ,t ,evt ,info ) 453if state == 'start' : 454# Handle missing stop (start followed by another start) 455if key in active_starts : 456prev_e = active_starts [key ] 457completed_events .append ({ 458'thread' :t , 459'event' :evt , 460'info' :info , 461'start_cyc' :prev_e ['unwrapped_cycles' ], 462'end_cyc' :prev_e ['unwrapped_cycles' ]+ one_usec_cycles .get (dev ,1000.0 ), 463'line_num' :prev_e .get ('line_num' ), 464'missing_stop' :True , 465'device' :dev 466 }) 467active_starts [key ]= e 468elif state == 'stop' : 469if key in active_starts : 470prev_e = active_starts [key ] 471del active_starts [key ] 472completed_events .append ({ 473'thread' :t , 474'event' :evt , 475'info' :info , 476'start_cyc' :prev_e ['unwrapped_cycles' ], 477'end_cyc' :cyc , 478'line_num' :prev_e .get ('line_num' ), 479'device' :dev 480 }) 481else : 482# Handle missing start (stop without start) 483completed_events .append ({ 484'thread' :t , 485'event' :evt , 486'info' :info , 487'start_cyc' :cyc - one_usec_cycles .get (dev ,1000.0 ), 488'end_cyc' :cyc , 489'line_num' :e .get ('line_num' ), 490'missing_start' :True , 491'device' :dev 492 }) 493 494# Clear remaining unmatched starts 495for key ,prev_e in active_starts .items (): 496dev ,t ,evt ,info = key 497completed_events .append ({ 498'thread' :t , 499'event' :evt , 500'info' :info , 501'start_cyc' :prev_e ['unwrapped_cycles' ], 502'end_cyc' :prev_e ['unwrapped_cycles' ]+ one_usec_cycles .get (dev ,1000.0 ), 503'line_num' :prev_e .get ('line_num' ), 504'missing_stop' :True , 505'device' :dev 506 }) 507 508completed_events .sort (key = lambda e :e ['start_cyc' ]) 509 510# Convert event times to nanoseconds using per-device / per-batch time mapper 511for e in completed_events : 512dev = e ['device' ] 513tm = time_mappers [dev ] 514e ['ts_ns' ]= tm .cycle_to_ns (e ['start_cyc' ]) 515dur_ns = tm .dur_cycles_to_ns (e ['start_cyc' ],e ['end_cyc' ]- e ['start_cyc' ]) 516e ['dur_ns' ]= max (dur_ns ,100 ) 517 518# Allocate slots (sub-tracks) to prevent overlaps on same virtual track 519active_slots = defaultdict (list ) 520for e in completed_events : 521t = e ['thread' ] 522evt = e ['event' ] 523ts = e ['ts_ns' ] 524dur = e ['dur_ns' ] 525dev = e ['device' ] 526 527norm_evt = normalize_event_name (evt ,e ['info' ]) 528if norm_evt == "DMA" : 529track_key = (dev ,t ,"DMA" ) 530elif t == 10 : 531track_key = (dev ,t ,"HMX" ) 532else : 533track_key = (dev ,t ,"HVX" ) 534 535slots = active_slots [track_key ] 536allocated_slot = - 1 537for idx ,slot_end_ns in enumerate (slots ): 538if ts >= slot_end_ns : 539slots [idx ]= ts + dur 540allocated_slot = idx 541break 542if allocated_slot == - 1 : 543slots .append (ts + dur ) 544allocated_slot = len (slots )- 1 545e ['slot' ]= allocated_slot 546 547# Generate Track IDs and track definitions 548used_tracks = {} 549for e in completed_events : 550t = e ['thread' ] 551evt = e ['event' ] 552slot = e ['slot' ] 553dev = e ['device' ] 554 555norm_evt = normalize_event_name (evt ,e ['info' ]) 556if norm_evt == "DMA" : 557track_evt = "DMA" 558evt_id = 1 559elif t == 10 : 560track_evt = "HMX" 561evt_id = 3 562else : 563track_evt = "HVX" 564evt_id = 2 565 566t_sort = 1 if t == 10 else t + 2 567dev_idx = device_to_idx [dev ] 568 569# Unique UUID for each sub-track 570if t == 10 : 571uuid = dev_idx * 10000000 + 20 # HMX thread track UUID 572else : 573uuid = int (dev_idx * 10000000 + t_sort * 1000000 + evt_id * 1000 + slot ) 574e ['uuid' ]= uuid 575used_tracks [uuid ]= (dev ,t ,track_evt ,slot ) 576 577with open (output_path ,"wb" )as f : 578for dev in unique_devices : 579dev_idx = device_to_idx [dev ] 580pid = dev_idx + 1 581proc_uuid = dev_idx * 10000000 + 1 582 583# Define Process with EXPLICIT child sorting 584proc_name = dev 585proc_desc = make_process_descriptor (pid ,proc_name ) 586proc_packet = make_trace_packet (0 ,track_descriptor = make_track_descriptor (proc_uuid ,process = proc_desc ,child_ordering = 3 )) 587write_trace_packet_to_file (f ,proc_packet ) 588 589# Define Operators Track as a thread track 590op_track_uuid = dev_idx * 10000000 + 2 591op_tid = pid * 100 + 8 592op_thread_desc = make_thread_descriptor (pid ,op_tid ,"Ops" ,sort_index = 1 ) 593op_packet = make_trace_packet (0 ,track_descriptor = make_track_descriptor (op_track_uuid ,parent_uuid = proc_uuid ,thread = op_thread_desc )) 594write_trace_packet_to_file (f ,op_packet ) 595 596# Define HMX Thread Track at rank 2 597hmx_track_uuid = dev_idx * 10000000 + 20 598hmx_tid = pid * 100 + 9 599hmx_thread_desc = make_thread_descriptor (pid ,hmx_tid ,"HMX" ,sort_index = 2 ) 600hmx_packet = make_trace_packet (0 ,track_descriptor = make_track_descriptor (hmx_track_uuid ,parent_uuid = proc_uuid ,thread = hmx_thread_desc )) 601write_trace_packet_to_file (f ,hmx_packet ) 602 603# Define Thread Tracks (T0, T1, ..., T9) for this device 604dev_used_tracks = {uuid :val for uuid ,val in used_tracks .items ()if val [0 ]== dev } 605unique_threads = sorted (list (set (t for (_ ,t ,_ ,_ )in dev_used_tracks .values ()if t != 10 ))) 606for t in unique_threads : 607thread_uuid = dev_idx * 10000000 + 10 + t 608thread_name = f"T { t } " 609sort_index = 3 + t 610tid = pid * 100 + 10 + t 611thread_desc = make_thread_descriptor (pid ,tid ,thread_name ,sort_index = sort_index ) 612thread_packet = make_trace_packet (0 ,track_descriptor = make_track_descriptor ( 613thread_uuid , 614parent_uuid = proc_uuid , 615thread = thread_desc , 616sibling_order_rank = sort_index , 617child_ordering = 3 # Explicit child sorting for sub-tracks 618 )) 619write_trace_packet_to_file (f ,thread_packet ) 620 621# Define Track descriptors for sub-tracks parented to thread tracks 622for uuid in sorted (used_tracks .keys ()): 623dev ,t ,evt ,slot = used_tracks [uuid ] 624dev_idx = device_to_idx [dev ] 625if t == 10 : 626continue 627name = f"T { t } { evt } " 628rank = 0 if evt == "HVX" else 1 629parent_thread_uuid = dev_idx * 10000000 + 10 + t 630# Sibling merge behavior: 1 (SIBLING_MERGE_BEHAVIOR_BY_TRACK_NAME) 631track_desc = make_track_descriptor ( 632uuid = uuid , 633name = name , 634parent_uuid = parent_thread_uuid , 635sibling_merge_behavior = 1 , 636sibling_order_rank = rank 637 ) 638track_packet = make_trace_packet (0 ,track_descriptor = track_desc ) 639write_trace_packet_to_file (f ,track_packet ) 640 641# Emit Operators 642last_op_end_ns = defaultdict (int ) 643for op in filtered_ops : 644dev = op ['device' ] 645dev_idx = device_to_idx [dev ] 646tm = time_mappers [dev ] 647op_start_ns = tm .cycle_to_ns (op ['start_cycles' ]) 648op_dur_ns = tm .dur_cycles_to_ns (op ['start_cycles' ],op ['cycles' ]) 649if op ['name' ]!= "OPBATCH" : 650if op_start_ns < last_op_end_ns [dev ]: 651op_start_ns = last_op_end_ns [dev ] 652clamped_dur = max (op_dur_ns ,100 )# Clamp to 100ns (0.1us) 653last_op_end_ns [dev ]= op_start_ns + clamped_dur 654else : 655clamped_dur = max (op_dur_ns ,100 ) 656 657# Debug annotations for Ops 658debug_annots = [] 659if 'line_num' in op : 660debug_annots .append (make_debug_annotation ("line" ,int_val = op ['line_num' ])) 661if 'names' in op and op ['names' ]and op ['names' ]!= '----' : 662debug_annots .append (make_debug_annotation ("names" ,string_val = op ['names' ])) 663if 'strides' in op and op ['strides' ]and op ['strides' ]!= '----' : 664debug_annots .append (make_debug_annotation ("strides" ,string_val = op ['strides' ])) 665if 'params' in op and op ['params' ]and op ['params' ]!= '----' : 666debug_annots .append (make_debug_annotation ("params" ,string_val = op ['params' ])) 667if 'evt' in op and op ['evt' ]: 668debug_annots .append (make_debug_annotation ("evt" ,string_val = op ['evt' ])) 669 670op_track_uuid = dev_idx * 10000000 + 2 671 672# Slice Begin 673evt_begin = make_track_event (1 ,op_track_uuid ,name = f" { op [ 'name' ] } ( { op [ 'dims' ] } )" ,category = "operator" ,debug_annotations = debug_annots ) 674packet_begin = make_trace_packet (op_start_ns ,track_event = evt_begin ) 675write_trace_packet_to_file (f ,packet_begin ) 676 677# Slice End 678evt_end = make_track_event (2 ,op_track_uuid ) 679packet_end = make_trace_packet (op_start_ns + clamped_dur ,track_event = evt_end ) 680write_trace_packet_to_file (f ,packet_end ) 681 682# Emit Thread Trace Events 683for e in completed_events : 684norm_name = normalize_event_name (e ['event' ],e ['info' ]) 685if norm_name == "DMA" : 686name = f"DMA { e [ 'info' ] } " 687elif norm_name == "FENCE" : 688name = f"FENCE { e [ 'info' ] } " if e .get ('info' )is not None and e ['info' ]!= 0 else "FENCE" 689else : 690name = norm_name 691 692if e .get ('missing_start' )or e .get ('missing_stop' ): 693name += "!" 694 695debug_annots = [] 696if 'line_num' in e and e ['line_num' ]is not None : 697debug_annots .append (make_debug_annotation ("line" ,int_val = e ['line_num' ])) 698if norm_name == "FENCE" and e .get ('info' )is not None : 699debug_annots .append (make_debug_annotation ("seq" ,int_val = e ['info' ])) 700elif norm_name == "DMA" and e .get ('info' )is not None : 701debug_annots .append (make_debug_annotation ("channel" ,int_val = e ['info' ])) 702elif e .get ('info' )is not None and e ['info' ]!= 0 : 703debug_annots .append (make_debug_annotation ("info" ,int_val = e ['info' ])) 704 705if e .get ('missing_start' ): 706debug_annots .append (make_debug_annotation ("missing_start" ,string_val = "true" )) 707if e .get ('missing_stop' ): 708debug_annots .append (make_debug_annotation ("missing_stop" ,string_val = "true" )) 709 710# Slice Begin 711evt_begin = make_track_event (1 ,e ['uuid' ],name = name ,category = "trace" ,debug_annotations = debug_annots if debug_annots else None ) 712packet_begin = make_trace_packet (e ['ts_ns' ],track_event = evt_begin ) 713write_trace_packet_to_file (f ,packet_begin ) 714 715# Slice End 716evt_end = make_track_event (2 ,e ['uuid' ]) 717packet_end = make_trace_packet (e ['ts_ns' ]+ e ['dur_ns' ],track_event = evt_end ) 718write_trace_packet_to_file (f ,packet_end ) 719 720logger .info (f"Successfully generated Perfetto trace at { output_path } " ) 721 722 723def main (): 724parser = argparse .ArgumentParser (description = "Convert Hexagon Op profile logs to native Perfetto Protobuf traces." ) 725parser .add_argument ("logfile" ,help = "Path to hex-log profile file" ) 726parser .add_argument ("-o" ,"--output" ,default = "optrace.perfetto-trace" ,help = "Output trace file path (default: optrace.perfetto-trace)" ) 727parser .add_argument ("--filter" ,type = str ,help = "Regex filter matching against the original profile-op line" ) 728parser .add_argument ("--device" ,type = str ,help = "Device to filter by (e.g. HTP0, HTP0:0) or 'split' to generate separate files per device" ) 729 730group = parser .add_mutually_exclusive_group () 731group .add_argument ("--head" ,type = int ,help = "Limit to first N ops" ) 732group .add_argument ("--tail" ,type = int ,help = "Limit to last N ops" ) 733 734args = parser .parse_args () 735logging .basicConfig (level = logging .INFO ,format = '%(message)s' ) 736 737op_filter_re = None 738if args .filter : 739try : 740op_filter_re = re .compile (args .filter ) 741except re .error as e : 742logger .error (f"Invalid regex filter: { e } " ) 743sys .exit (1 ) 744 745limit = args .head if args .head is not None else None 746device_filter = args .device if (args .device and args .device != "split" )else None 747ops ,traces = parse_log (args .logfile ,limit = limit ,device_filter = device_filter ,op_filter_re = op_filter_re ) 748 749if args .device and args .device != "split" : 750ops = [op for op in ops if device_matches (op ['device' ],args .device )] 751traces = [t for t in traces if device_matches (t ['device' ],args .device )] 752 753if args .filter : 754try : 755filter_re = re .compile (args .filter ) 756except re .error as e : 757logger .error (f"Invalid regex filter: { e } " ) 758sys .exit (1 ) 759ops = [op for op in ops if filter_re .search (op ['op_text' ])] 760 761if args .head is not None or args .tail is not None : 762ops_by_dev = defaultdict (list ) 763for op in ops : 764ops_by_dev [op ['device' ]].append (op ) 765 766filtered_ops = [] 767for dev in sorted (ops_by_dev .keys ()): 768dev_ops = ops_by_dev [dev ] 769if args .head is not None : 770dev_ops = dev_ops [:args .head ] 771elif args .tail is not None : 772dev_ops = dev_ops [- args .tail :] 773filtered_ops .extend (dev_ops ) 774ops = filtered_ops 775 776if args .filter or args .head is not None or args .tail is not None : 777# Group valid ranges by device 778valid_ranges_by_dev = defaultdict (list ) 779for op in ops : 780start_cyc = op ['unwrapped_cycles_start' ] 781end_cyc = start_cyc + op ['cycles' ]if start_cyc is not None else None 782if start_cyc is not None and end_cyc is not None : 783valid_ranges_by_dev [op ['device' ]].append ((start_cyc ,end_cyc )) 784 785for dev in valid_ranges_by_dev : 786valid_ranges_by_dev [dev ].sort (key = lambda r :r [0 ]) 787 788range_starts_by_dev = {dev : [r [0 ]for r in ranges ]for dev ,ranges in valid_ranges_by_dev .items ()} 789 790filtered_traces = [] 791for e in traces : 792cyc = e ['unwrapped_cycles' ] 793if cyc is None : 794continue 795dev = e ['device' ] 796range_starts = range_starts_by_dev .get (dev ) 797if not range_starts : 798continue 799idx = bisect .bisect_right (range_starts ,cyc )- 1 800if idx >= 0 : 801start ,end = valid_ranges_by_dev [dev ][idx ] 802if start <= cyc <= end : 803filtered_traces .append (e ) 804traces = filtered_traces 805 806if args .device == "split" : 807unique_devices = sorted (list (set (op ['device' ]for op in ops ))) 808for dev in unique_devices : 809dev_ops = [op for op in ops if device_matches (op ['device' ],dev )] 810dev_traces = [t for t in traces if device_matches (t ['device' ],dev )] 811out_path = get_split_output_path (args .output ,dev ) 812generate_perfetto_trace (dev_ops ,dev_traces ,out_path ) 813else : 814generate_perfetto_trace (ops ,traces ,args .output ) 815 816 817if __name__ == "__main__" : 818main ()