Files
TheRock/rocm-libraries/projects/hipblaslt/tensilelite/scripts/analyze_timing.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

724 lines
26 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Analyze timing instrumentation output from tensilelite-client.
This script parses the TIMING: lines from stderr and generates a summary report
of where time is being spent during test execution.
Usage:
# Run tests with timing instrumentation and capture stderr:
pytest -s ... --global-parameters "TimingInstrumentation=True" 2>&1 | tee test_output.log
# Analyze the output:
python scripts/analyze_timing.py test_output.log
The timing output format is:
TIMING:<category>:<duration_ms>
TIMING_CONTEXT:M=...,N=...,K=...,batch=...,typeA=...,typeD=...
Timing categories form a hierarchy: parents contain their children's wall-clock
time, so only top-level phases should be summed to get the total. The
TIMING_HIERARCHY dict (below) is the single source of truth for the nesting
structure. Display grouping (e.g. grouping C++ phases into "C++ Client
Setup", "Data Preparation", etc.) is a rendering concern handled by
DISPLAY_GROUPS / CPP_PHASE_GROUPS and applied at print time only.
"""
import argparse
from collections import defaultdict
from dataclasses import dataclass
from typing import List, Dict, Optional, Tuple
# --- Hierarchy definition (single source of truth) ---
#
# Each key is a timing category; its value is a dict of child categories.
# An empty dict {} means the category has no known sub-phases.
# The tree structure mirrors the docstring hierarchy diagram above.
TIMING_HIERARCHY = {
"python_benchmark_problems": {
"python_cache_check": {},
"python_solution_generation": {
"python_solgen_fork_permutations": {},
"python_solgen_forked_solutions": {},
"python_solgen_custom_kernels": {},
},
"python_kernel_compilation": {
"python_kernel_bench_setup": {},
"python_kernel_setup": {},
"python_kernel_codegen": {},
"python_kernel_validate": {},
"python_kernel_write_assemble": {},
"python_kernel_write_helpers": {},
"python_kernel_build_co": {},
"python_kernel_build_src_co": {
"python_kernel_build_src_co.setup": {},
"python_kernel_build_src_co.cache_check": {},
"python_kernel_build_src_co.cache_hit": {},
"python_kernel_build_src_co.compile": {},
"python_kernel_build_src_co.unbundle": {},
"python_kernel_build_src_co.move": {},
"python_kernel_build_src_co.cache_populate": {},
},
"python_kernel_bench_postprocess": {
"python_benchpost_naming": {},
"python_benchpost_lib_construction": {},
"python_benchpost_library_write": {},
"python_benchpost_client_config": {},
},
},
"python_write_cache": {},
"python_solution_indexing": {},
"python_write_solutions": {
"python_wsol_prepare": {
"python_wsol_prepare_cache": {},
"python_wsol_prepare_nocache": {},
},
"python_wsol_header": {},
"python_wsol_dump": {},
},
"python_client_execution": {
"hip_initialization": {},
"library_loading": {},
"code_object_loading": {},
"lazy_loading_init": {},
"data_init_setup": {},
"solution_iterator_setup": {},
"listener_setup": {},
"reporter_setup": {},
"pre_problem": {
"cpu_data_init": {},
"cpu_reference_gemm": {
"solve_cpu_fast": {},
"solve_cpu_slow": {},
},
},
"validate_warmups": {
"validate_gpu_sync": {},
"validate_gpu_readback": {},
"validate_element_comparison": {},
"validate_mismatch_printing": {},
},
"gpu_input_preparation": {},
"gpu_input_reset": {},
"rotating_buffer_preparation": {},
"solution_selection": {},
"pre_solution": {},
"kernel_solving": {},
"warmup_runs": {},
"benchmark_runs": {
"gpu_kernel_execution": {},
},
"post_solution": {
"post_solution_perf_calc": {},
"post_solution_reporting": {},
"post_solution_validation": {},
"post_solution_log": {},
"post_solution_result_file": {},
"post_solution_lib_update": {},
"post_solution_perf_reset": {},
"post_solution_sol_advance": {},
},
"post_problem": {},
"finalize_report": {},
"timing_overhead": {},
"flush_timing_buffer": {},
},
},
"python_library_logic": {
"python_logic_parse_solutions": {},
"python_logic_analyze": {},
"python_logic_write": {},
},
"python_client_writer": {},
}
# Display grouping for C++ phases (rendering concern only).
# Under python_client_execution, children are grouped into these display
# categories instead of being shown individually.
CPP_PHASE_GROUPS = {
"C++ Client Setup": [
"hip_initialization",
"library_loading",
"code_object_loading",
"lazy_loading_init",
"data_init_setup",
"solution_iterator_setup",
"listener_setup",
"reporter_setup",
],
"Data Preparation": [
"pre_problem",
"gpu_input_preparation",
"gpu_input_reset",
"rotating_buffer_preparation",
],
"Reference Computation": [
"validate_warmups",
],
"Kernel Execution": [
"solution_selection",
"pre_solution",
"kernel_solving",
"warmup_runs",
"benchmark_runs",
"post_solution",
"post_problem",
"finalize_report",
],
"Timing Overhead": [
"timing_overhead",
"flush_timing_buffer",
],
}
# Nodes whose children should be grouped for display
DISPLAY_GROUPS = {
"python_client_execution": CPP_PHASE_GROUPS,
}
# --- Derived constants ---
def _walk_hierarchy(d):
"""Yield all category names from a nested hierarchy dict."""
for k, v in d.items():
yield k
yield from _walk_hierarchy(v)
ALL_KNOWN_CATEGORIES = set(_walk_hierarchy(TIMING_HIERARCHY))
# Named constants for commonly-referenced hierarchy points (used in tests)
SOLUTION_GENERATION_PARENT = "python_solution_generation"
BENCH_POSTPROCESS_PARENT = "python_kernel_bench_postprocess"
SOLUTION_GENERATION_PHASES = list(
TIMING_HIERARCHY["python_benchmark_problems"]["python_solution_generation"].keys()
)
BENCH_POSTPROCESS_PHASES = list(
TIMING_HIERARCHY["python_benchmark_problems"]["python_kernel_compilation"]
["python_kernel_bench_postprocess"].keys()
)
TABLE_WIDTH = 120
BAR_CHART_WIDTH = 50
def as_percentage(ms: float, total: float) -> float:
"""Return ms as a percentage of total, or 0 if total is non-positive."""
return (ms / total * 100) if total > 0 else 0
@dataclass
class TimingRecord:
category: str
duration_ms: float
context: Optional[Dict[str, str]] = None
@dataclass
class ProblemTiming:
context: Dict[str, str]
cpu_data_init_ms: float = 0.0
cpu_reference_gemm_ms: float = 0.0
gpu_kernel_execution_ms: float = 0.0
@dataclass
class PhaseNode:
"""A node in the timing hierarchy tree."""
name: str
count: Optional[int]
total_ms: float
mean_ms: Optional[float]
children: List['PhaseNode']
# Categories that represent instrumentation overhead measurements, not actual work.
OVERHEAD_CATEGORIES = {'timing_overhead'}
def _count_descendant_invocations(children_dict: Dict, timings: Dict[str, List[float]]) -> int:
"""Count total timer invocations across all descendants in the hierarchy."""
count = 0
for child_name, grandchildren in children_dict.items():
count += len(timings.get(child_name, []))
count += _count_descendant_invocations(grandchildren, timings)
return count
def _compute_per_call_overhead(timings: Dict[str, List[float]]) -> float:
"""Compute per-call instrumentation overhead from the C++ timing_overhead record.
The C++ client emits a timing_overhead record containing the total estimated
overhead across all ScopedTimer invocations (calibrated at startup). Dividing
by total invocations gives the average overhead each timer call adds to its
parent's measurement.
Note: Python-side timing_context overhead is not tracked or adjusted here.
Python has only ~a dozen calls per run, so the overhead is negligible.
"""
total_overhead_ms = sum(timings.get('timing_overhead', []))
if total_overhead_ms <= 0:
return 0.0
total_invocations = sum(
len(vals) for cat, vals in timings.items()
if cat not in OVERHEAD_CATEGORIES
)
if total_invocations == 0:
return 0.0
return total_overhead_ms / total_invocations
def _build_node(name: str, children_dict: Dict, timings: Dict[str, List[float]],
per_call_overhead_ms: float = 0) -> Optional['PhaseNode']:
"""Build a PhaseNode recursively from the hierarchy dict.
Args:
name: The timing category name.
children_dict: Dict of {child_name: grandchildren_dict} from TIMING_HIERARCHY.
timings: Raw timing data from analyze_timing_file.
per_call_overhead_ms: Per-invocation overhead to subtract from parent totals
based on descendant invocation count.
"""
if name not in timings:
return None
values = timings[name]
count = len(values)
raw_total_ms = sum(values)
# Subtract estimated overhead from descendant timer invocations.
# Each descendant invocation adds ~per_call_overhead_ms to this node's
# raw measurement via context-manager / RAII bookkeeping.
desc_invocations = _count_descendant_invocations(children_dict, timings)
total_ms = max(0, raw_total_ms - desc_invocations * per_call_overhead_ms)
mean_ms = total_ms / count if count > 0 else 0
children = []
if children_dict:
child_sum = 0.0
for child_name, grandchildren in children_dict.items():
child_node = _build_node(child_name, grandchildren, timings, per_call_overhead_ms)
if child_node:
children.append(child_node)
child_sum += child_node.total_ms
untracked = total_ms - child_sum
if untracked > 0.01:
children.append(PhaseNode("(untracked)", None, untracked, None, []))
children.sort(key=lambda n: n.total_ms, reverse=True)
return PhaseNode(name, count, total_ms, mean_ms, children)
def _group_children_for_display(
children: List[PhaseNode], parent_total: float,
groups: Dict[str, List[str]]
) -> List[PhaseNode]:
"""Group PhaseNode children into display categories for rendering."""
grouped = []
grouped_sum = 0.0
cat_to_node = {c.name: c for c in children if c.name != "(untracked)"}
for group_name, group_cats in groups.items():
g_children = [cat_to_node[c] for c in group_cats if c in cat_to_node]
if g_children:
g_total = sum(c.total_ms for c in g_children)
grouped_sum += g_total
g_children.sort(key=lambda n: n.total_ms, reverse=True)
grouped.append(PhaseNode(group_name, None, g_total, None, g_children))
untracked = parent_total - grouped_sum
if untracked > 0.01:
grouped.append(PhaseNode("(untracked C++ overhead)", None, untracked, None, []))
grouped.sort(key=lambda n: n.total_ms, reverse=True)
return grouped
def _get_display_children(node: PhaseNode) -> List[PhaseNode]:
"""Return display-grouped children for a node, if applicable."""
if node.name in DISPLAY_GROUPS:
return _group_children_for_display(node.children, node.total_ms, DISPLAY_GROUPS[node.name])
return node.children
def build_hierarchy(timings: Dict[str, List[float]]) -> List[PhaseNode]:
"""Build the phase hierarchy from raw timings, sorted by total time descending.
Automatically adjusts parent totals by subtracting estimated per-call
instrumentation overhead based on descendant invocation counts.
"""
per_call_overhead_ms = _compute_per_call_overhead(timings)
top_nodes = []
for phase, children_dict in TIMING_HIERARCHY.items():
node = _build_node(phase, children_dict, timings, per_call_overhead_ms)
if node:
top_nodes.append(node)
top_nodes.sort(key=lambda n: n.total_ms, reverse=True)
return top_nodes
def parse_timing_line(line: str) -> Optional[TimingRecord]:
"""Parse a TIMING: line and return a TimingRecord."""
start_sentinel = 'TIMING:'
if not line.startswith(start_sentinel):
return None
parts = line.split(':', 2)
if len(parts) != 3:
return None
try:
return TimingRecord(category=parts[1], duration_ms=float(parts[2]))
except ValueError:
return None
def parse_context_line(line: str) -> Optional[Dict[str, str]]:
"""Parse a TIMING_CONTEXT: line and return context dict."""
start_sentinel = 'TIMING_CONTEXT:'
if not line.startswith(start_sentinel):
return None
payload = line[len(start_sentinel):]
context = {}
for part in payload.split(','):
if '=' in part:
key, value = part.split('=', 1)
context[key] = value
return context
def analyze_timing_file(filepath: str) -> Tuple[Dict[str, List[float]], List[ProblemTiming]]:
"""Analyze a file containing timing output and return categorized timings."""
timings = defaultdict(list)
problem_timings = []
current_problem = None
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
context = parse_context_line(line)
if context:
if current_problem:
problem_timings.append(current_problem)
current_problem = ProblemTiming(context=context)
continue
record = parse_timing_line(line)
if record:
timings[record.category].append(record.duration_ms)
if current_problem:
if record.category == 'cpu_data_init':
current_problem.cpu_data_init_ms = record.duration_ms
elif record.category == 'cpu_reference_gemm':
current_problem.cpu_reference_gemm_ms = record.duration_ms
elif record.category == 'gpu_kernel_execution':
current_problem.gpu_kernel_execution_ms = record.duration_ms
if current_problem:
problem_timings.append(current_problem)
return timings, problem_timings
# ---------------------------------------------------------------------------
# Hierarchical visual bar chart
# ---------------------------------------------------------------------------
def print_visual_breakdown(nodes: List[PhaseNode], wall_clock_ms: float):
"""Print a hierarchical bar chart reflecting the timing hierarchy."""
if wall_clock_ms <= 0:
return
print("TIME BREAKDOWN (visual, % of wall clock)")
print("-" * TABLE_WIDTH)
label_width = 42
def bar_line(indent: int, name: str, ms: float):
pct = ms / wall_clock_ms * 100
prefix = " " * indent
label = f" {prefix}{name}"
bar_len = int(pct / 100 * BAR_CHART_WIDTH)
bar = "#" * bar_len
print(f"{label:<{label_width}} {pct:>6.1f}% {bar}")
def render_visual(node: PhaseNode, depth: int):
bar_line(depth, node.name, node.total_ms)
for child in _get_display_children(node):
render_visual(child, depth + 1)
for node in nodes:
render_visual(node, 0)
print()
# ---------------------------------------------------------------------------
# Hierarchy validation
# ---------------------------------------------------------------------------
def print_hierarchy_validation(totals: Dict[str, float]):
"""Check and print hierarchy invariant violations.
Recursively walks TIMING_HIERARCHY and checks that for every parent-child
relationship, the sum of child durations does not exceed the parent.
"""
warnings = []
def _check(parent_name: str, children_dict: Dict):
parent_ms = totals.get(parent_name, 0)
if parent_ms <= 0 or not children_dict:
return
child_sum = sum(totals.get(c, 0) for c in children_dict)
if child_sum > parent_ms:
excess = child_sum - parent_ms
warnings.append(
f"Child phases of {parent_name} ({child_sum:,.2f} ms) exceed "
f"parent ({parent_ms:,.2f} ms) "
f"by {excess:,.2f} ms ({excess / parent_ms * 100:.1f}%)"
)
for child_name, grandchildren in children_dict.items():
if grandchildren:
_check(child_name, grandchildren)
for parent_name, children_dict in TIMING_HIERARCHY.items():
_check(parent_name, children_dict)
if warnings:
print("HIERARCHY VALIDATION WARNINGS")
print("-" * TABLE_WIDTH)
for w in warnings:
print(f" WARNING: {w}")
else:
print("HIERARCHY VALIDATION: OK (all child phases fit within their parents)")
# ---------------------------------------------------------------------------
# Summary output
# ---------------------------------------------------------------------------
def print_summary(timings: Dict[str, List[float]], problem_timings: List[ProblemTiming]):
"""Print a summary of timing data."""
print("=" * TABLE_WIDTH)
print("TIMING ANALYSIS SUMMARY")
print("=" * TABLE_WIDTH)
print()
if not timings:
print("No timing data found!")
print()
print('Make sure you ran with --global-parameters "TimingInstrumentation=True"')
print("and captured stderr output (2>&1).")
return
total_time_by_category = {cat: sum(vals) for cat, vals in timings.items()}
wall_clock_ms = sum(total_time_by_category.get(c, 0) for c in TIMING_HIERARCHY)
if wall_clock_ms == 0:
wall_clock_ms = sum(total_time_by_category.values())
wall_clock_s = wall_clock_ms / 1000.0
# -- Hierarchical table --------------------------------------------------
COL_CAT = 44
COL_CNT = 8
COL_TOT = 14
COL_MEAN = 14
COL_WALL = 11
COL_PAR = 11
def fmt_row(indent, name, count, total_ms, mean_ms, pct_parent, pct_wall):
prefix = " " * indent
cat = f" {prefix}{name}"
c = f"{count:>{COL_CNT},}" if count is not None else " " * COL_CNT
t = f"{total_ms:>{COL_TOT},.2f}" if total_ms is not None else " " * COL_TOT
m = f"{mean_ms:>{COL_MEAN},.2f}" if mean_ms is not None else " " * COL_MEAN
par = f"{pct_parent:>{COL_WALL - 1}.1f}%" if pct_parent is not None else " " * COL_WALL
wall = f"{pct_wall:>{COL_PAR - 1}.1f}%" if pct_wall is not None else " " * COL_PAR
print(f"{cat:<{COL_CAT}} {c} {t} {m} {par} {wall}")
print("TIMING BY PHASE")
print("-" * TABLE_WIDTH)
hdr_cat = "Category"
print(
f" {hdr_cat:<{COL_CAT - 2}}"
f" {'Count':>{COL_CNT}}"
f" {'Total (ms)':>{COL_TOT}}"
f" {'Mean (ms)':>{COL_MEAN}}"
f" {'% of Parent':>{COL_WALL}}"
f" {'% of Wall':>{COL_PAR}}"
)
print("-" * TABLE_WIDTH)
SEPARATORS = {
0: lambda: None, # no separator before top-level (handled specially)
1: lambda: print(" " + "-" * (TABLE_WIDTH - 4)),
2: lambda: print(" " + "- " * ((TABLE_WIDTH - 6) // 2)),
3: lambda: print(" " + ". " * ((TABLE_WIDTH - 8) // 2)),
}
def sep_top():
print(" " + "=" * (TABLE_WIDTH - 2))
def render_node(node: PhaseNode, depth: int, parent_ms: Optional[float],
wall_clock_ms: float, is_first: bool):
"""Recursively render a PhaseNode at a given depth."""
# Separators between siblings
if depth == 0:
pass # handled by caller
elif depth == 1:
SEPARATORS[1]()
elif depth == 2:
SEPARATORS[2]()
elif depth == 3 and is_first:
SEPARATORS[3]()
# depth >= 4 or depth == 3 non-first: no separator
pct_parent = as_percentage(node.total_ms, parent_ms) if parent_ms else None
pct_wall = as_percentage(node.total_ms, wall_clock_ms)
fmt_row(depth, node.name, node.count, node.total_ms, node.mean_ms,
pct_parent, pct_wall)
for i, child in enumerate(_get_display_children(node)):
render_node(child, depth + 1, node.total_ms, wall_clock_ms, is_first=(i == 0))
nodes = build_hierarchy(timings)
for top_idx, node in enumerate(nodes):
if top_idx > 0:
sep_top()
pct_wall = as_percentage(node.total_ms, wall_clock_ms)
fmt_row(0, node.name, node.count, node.total_ms, node.mean_ms, None, pct_wall)
for i, child in enumerate(node.children):
render_node(child, 1, node.total_ms, wall_clock_ms, is_first=(i == 0))
# Unknown categories
unknown_cats = set(timings.keys()) - ALL_KNOWN_CATEGORIES
if unknown_cats:
print()
print(" Unknown Categories")
print(f" {'-' * (TABLE_WIDTH - 2)}")
for cat in sorted(unknown_cats):
values = timings[cat]
count = len(values)
total_ms = sum(values)
mean_ms = total_ms / count if count > 0 else 0
fmt_row(0, cat, count, total_ms, mean_ms, as_percentage(total_ms, wall_clock_ms), None)
print()
print("-" * TABLE_WIDTH)
print(f" ESTIMATED WALL CLOCK TIME: {wall_clock_s:.2f}s")
print()
# -- Hierarchy validation -------------------------------------------------
print_hierarchy_validation(total_time_by_category)
print()
# -- Visual breakdown -----------------------------------------------------
print_visual_breakdown(nodes, wall_clock_ms)
# -- Key insights ---------------------------------------------------------
python_codegen_time = (
total_time_by_category.get("python_solution_generation", 0)
+ total_time_by_category.get("python_kernel_compilation", 0)
)
python_io_time = (
total_time_by_category.get("python_write_cache", 0)
+ total_time_by_category.get("python_write_solutions", 0)
+ total_time_by_category.get("python_cache_check", 0)
)
client_setup_time = sum(
total_time_by_category.get(c, 0)
for c in CPP_PHASE_GROUPS.get("C++ Client Setup", [])
)
data_prep_time = sum(
total_time_by_category.get(c, 0)
for c in CPP_PHASE_GROUPS.get("Data Preparation", [])
)
ref_time = sum(
total_time_by_category.get(c, 0)
for c in CPP_PHASE_GROUPS.get("Reference Computation", [])
)
kernel_time = sum(
total_time_by_category.get(c, 0)
for c in CPP_PHASE_GROUPS.get("Kernel Execution", [])
)
print("KEY INSIGHTS (% of wall clock)")
print("-" * TABLE_WIDTH)
print(f" Python phases (code gen, compilation): {python_codegen_time / 1000:.2f}s ({as_percentage(python_codegen_time, wall_clock_ms):.1f}%)")
print(f" Python I/O overhead (cache, YAML): {python_io_time / 1000:.2f}s ({as_percentage(python_io_time, wall_clock_ms):.1f}%)")
print(f" C++ client setup (library loading): {client_setup_time / 1000:.2f}s ({as_percentage(client_setup_time, wall_clock_ms):.1f}%)")
print(f" Data preparation (CPU init, GPU prep): {data_prep_time / 1000:.2f}s ({as_percentage(data_prep_time, wall_clock_ms):.1f}%)")
print(f" Reference computation (CPU GEMM): {ref_time / 1000:.2f}s ({as_percentage(ref_time, wall_clock_ms):.1f}%)")
print(f" Kernel execution (warmup, benchmark): {kernel_time / 1000:.2f}s ({as_percentage(kernel_time, wall_clock_ms):.1f}%)")
print()
cpu_ref_time = total_time_by_category.get('cpu_reference_gemm', 0)
gpu_exec_time = total_time_by_category.get('gpu_kernel_execution', 0)
if cpu_ref_time > 0 and gpu_exec_time > 0:
ratio = cpu_ref_time / gpu_exec_time
print(f" CPU reference / GPU execution ratio: {ratio:.1f}x")
if ratio > 10:
print(" >>> CPU reference > 10x GPU execution time!")
print()
# -- Top 10 slowest problems ----------------------------------------------
if problem_timings:
print("TOP 10 SLOWEST PROBLEMS (by CPU reference time)")
print("-" * TABLE_WIDTH)
sorted_problems = sorted(
problem_timings,
key=lambda p: p.cpu_reference_gemm_ms,
reverse=True,
)[:10]
print(
f" {'M':>8} {'N':>8} {'K':>8} {'Batch':>8}"
f" {'TypeA':>10} {'TypeD':>10}"
f" {'CPU Ref (ms)':>12} {'GPU (ms)':>10}"
)
print(f" {'-' * (TABLE_WIDTH - 2)}")
for p in sorted_problems:
ctx = p.context
print(
f" {ctx.get('M', '?'):>8} {ctx.get('N', '?'):>8}"
f" {ctx.get('K', '?'):>8} {ctx.get('batch', '?'):>8}"
f" {ctx.get('typeA', '?'):>10} {ctx.get('typeD', '?'):>10}"
f" {p.cpu_reference_gemm_ms:>12.2f}"
f" {p.gpu_kernel_execution_ms:>10.2f}"
)
print()
def main():
parser = argparse.ArgumentParser(
description='Analyze timing instrumentation output from tensilelite-client',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
parser.add_argument('logfile', help='Log file containing timing output')
parser.add_argument('--csv', help='Output CSV file for detailed data')
args = parser.parse_args()
timings, problem_timings = analyze_timing_file(args.logfile)
print_summary(timings, problem_timings)
if args.csv:
with open(args.csv, 'w') as f:
f.write("M,N,K,batch,typeA,typeD,cpu_data_init_ms,cpu_reference_gemm_ms,gpu_kernel_execution_ms\n")
for p in problem_timings:
ctx = p.context
f.write(
f"{ctx.get('M', '')},{ctx.get('N', '')},{ctx.get('K', '')},"
f"{ctx.get('batch', '')},{ctx.get('typeA', '')},{ctx.get('typeD', '')},"
f"{p.cpu_data_init_ms},{p.cpu_reference_gemm_ms},{p.gpu_kernel_execution_ms}\n"
)
print(f"Detailed data written to: {args.csv}")
if __name__ == '__main__':
main()