From b3321623e9ea8372cf1767f4f34d6f3b0b89cac8 Mon Sep 17 00:00:00 2001 From: Adeem Jassani Date: Fri, 11 Sep 2026 14:52:00 -0400 Subject: [PATCH 1/2] TraceIndex: promote roofline stats to typed catalog columns Filter bound and Pct Roofline without json_extract, and backfill existing SQLite catalogs from raw_row_json. Co-authored-by: Cursor --- TraceLens/TraceIndex/sqlite_store.py | 161 +++++++++++++++++++++++++-- docs/how-to/trace-index.md | 17 +++ docs/reference/trace-index-schema.md | 7 ++ tests/test_trace_index.py | 124 ++++++++++++++++++++- 4 files changed, 299 insertions(+), 10 deletions(-) diff --git a/TraceLens/TraceIndex/sqlite_store.py b/TraceLens/TraceIndex/sqlite_store.py index b3e716bc6..a02e0e0c4 100644 --- a/TraceLens/TraceIndex/sqlite_store.py +++ b/TraceLens/TraceIndex/sqlite_store.py @@ -27,6 +27,42 @@ utc_now, ) +# Promoted from unified_perf_summary.csv into typed columns so catalogs can +# filter without json_extract on raw_row_json. +UNIFIED_ROOFLINE_COLUMNS = ( + ("gpu_kernel_pct", "REAL"), + ("pct_roofline_max", "REAL"), + ("pct_roofline_mean", "REAL"), + ("pct_roofline_median", "REAL"), + ("pct_roofline_min", "REAL"), + ("pct_roofline_std", "REAL"), + ("roofline_bound", "TEXT"), + ("roofline_time_us", "REAL"), +) + +_ROOFLINE_TIME_KEYS = ( + "Roofline Time (\u00b5s)_first", + "Roofline Time (us)_first", + "Roofline Time (\u00b5s)", + "Roofline Time (us)", +) + + +def extract_unified_roofline(row: Dict[str, Any]) -> Dict[str, Any]: + """Pull roofline / GPU-share fields from a CSV row or parsed raw_row_json.""" + return { + "gpu_kernel_pct": as_float( + first_value(row, ["Percentage (%)", "gpu_kernel_pct", "percent"]) + ), + "pct_roofline_max": as_float(first_value(row, ["Pct Roofline_max"])), + "pct_roofline_mean": as_float(first_value(row, ["Pct Roofline_mean"])), + "pct_roofline_median": as_float(first_value(row, ["Pct Roofline_median"])), + "pct_roofline_min": as_float(first_value(row, ["Pct Roofline_min"])), + "pct_roofline_std": as_float(first_value(row, ["Pct Roofline_std"])), + "roofline_bound": as_text(first_value(row, ["Roofline Bound"])), + "roofline_time_us": as_float(first_value(row, _ROOFLINE_TIME_KEYS)), + } + def is_read_only_sql(sql: str) -> bool: stripped = sql.strip().lower() @@ -124,6 +160,13 @@ def init_schema(self) -> None: has_perf_model INTEGER, overlap_pct REAL, gpu_kernel_pct REAL, + pct_roofline_max REAL, + pct_roofline_mean REAL, + pct_roofline_median REAL, + pct_roofline_min REAL, + pct_roofline_std REAL, + roofline_bound TEXT, + roofline_time_us REAL, perf_params_json TEXT, kernel_details_json TEXT, raw_row_json TEXT @@ -216,8 +259,102 @@ def init_schema(self) -> None: tokenize='unicode61' ); """) + added = self._ensure_unified_roofline_columns() + if added: + self._backfill_roofline_from_raw() self.conn.commit() + def _ensure_unified_roofline_columns(self) -> List[str]: + existing = { + row["name"] + for row in self.conn.execute("PRAGMA table_info(unified_perf_rows)") + } + added: List[str] = [] + alter_sql = { + "gpu_kernel_pct": ( + "ALTER TABLE unified_perf_rows ADD COLUMN gpu_kernel_pct REAL" + ), + "pct_roofline_max": ( + "ALTER TABLE unified_perf_rows ADD COLUMN pct_roofline_max REAL" + ), + "pct_roofline_mean": ( + "ALTER TABLE unified_perf_rows ADD COLUMN pct_roofline_mean REAL" + ), + "pct_roofline_median": ( + "ALTER TABLE unified_perf_rows ADD COLUMN pct_roofline_median REAL" + ), + "pct_roofline_min": ( + "ALTER TABLE unified_perf_rows ADD COLUMN pct_roofline_min REAL" + ), + "pct_roofline_std": ( + "ALTER TABLE unified_perf_rows ADD COLUMN pct_roofline_std REAL" + ), + "roofline_bound": ( + "ALTER TABLE unified_perf_rows ADD COLUMN roofline_bound TEXT" + ), + "roofline_time_us": ( + "ALTER TABLE unified_perf_rows ADD COLUMN roofline_time_us REAL" + ), + } + for name, _decl in UNIFIED_ROOFLINE_COLUMNS: + if name in existing: + continue + self.conn.execute(alter_sql[name]) + added.append(name) + self.conn.execute( + "CREATE INDEX IF NOT EXISTS idx_trace_index_unified_roofline_bound " + "ON unified_perf_rows(roofline_bound)" + ) + self.conn.execute( + "CREATE INDEX IF NOT EXISTS idx_trace_index_unified_pct_roofline_mean " + "ON unified_perf_rows(pct_roofline_mean)" + ) + return added + + def _backfill_roofline_from_raw(self) -> None: + rows = self.conn.execute( + "SELECT id, raw_row_json FROM unified_perf_rows WHERE raw_row_json IS NOT NULL" + ).fetchall() + updates = [] + for row in rows: + try: + payload = json.loads(row["raw_row_json"]) + except (TypeError, json.JSONDecodeError): + continue + if not isinstance(payload, dict): + continue + fields = extract_unified_roofline(payload) + updates.append( + ( + fields["gpu_kernel_pct"], + fields["pct_roofline_max"], + fields["pct_roofline_mean"], + fields["pct_roofline_median"], + fields["pct_roofline_min"], + fields["pct_roofline_std"], + fields["roofline_bound"], + fields["roofline_time_us"], + row["id"], + ) + ) + if not updates: + return + self.conn.executemany( + """ + UPDATE unified_perf_rows SET + gpu_kernel_pct = COALESCE(gpu_kernel_pct, ?), + pct_roofline_max = COALESCE(pct_roofline_max, ?), + pct_roofline_mean = COALESCE(pct_roofline_mean, ?), + pct_roofline_median = COALESCE(pct_roofline_median, ?), + pct_roofline_min = COALESCE(pct_roofline_min, ?), + pct_roofline_std = COALESCE(pct_roofline_std, ?), + roofline_bound = COALESCE(roofline_bound, ?), + roofline_time_us = COALESCE(roofline_time_us, ?) + WHERE id = ? + """, + updates, + ) + def upsert_trace(self, trace: TraceRecord) -> int: now = utc_now() by_path = self.conn.execute( @@ -427,6 +564,7 @@ def _import_unified_rows( kernel_details = parse_repr( first_value(row, ["kernel_details_summary", "trunc_kernel_details"]) ) + roofline = extract_unified_roofline(row) cursor = self.conn.execute( """ INSERT INTO unified_perf_rows( @@ -435,10 +573,13 @@ def _import_unified_rows( kernel_time_std_us, kernel_time_min_us, kernel_time_max_us, op_duration_us, tflops_mean, tflops_median, tbs_mean, tbs_median, gflops, data_moved_mb, flops_per_byte, compute_spec, - has_perf_model, overlap_pct, gpu_kernel_pct, perf_params_json, - kernel_details_json, raw_row_json + has_perf_model, overlap_pct, gpu_kernel_pct, + pct_roofline_max, pct_roofline_mean, pct_roofline_median, + pct_roofline_min, pct_roofline_std, roofline_bound, + roofline_time_us, perf_params_json, kernel_details_json, + raw_row_json ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( trace_id, @@ -516,12 +657,14 @@ def _import_unified_rows( as_text(first_value(row, ["Compute Spec", "compute_spec"])), as_bool_int(first_value(row, ["has_perf_model", "Has Perf Model"])), as_float(first_value(row, ["overlap_pct", "Overlap (%)"])), - as_float( - first_value( - row, - ["Percentage (%)", "gpu_kernel_pct", "percent"], - ) - ), + roofline["gpu_kernel_pct"], + roofline["pct_roofline_max"], + roofline["pct_roofline_mean"], + roofline["pct_roofline_median"], + roofline["pct_roofline_min"], + roofline["pct_roofline_std"], + roofline["roofline_bound"], + roofline["roofline_time_us"], to_json(params), to_json(kernel_details), to_json(dict(row)), diff --git a/docs/how-to/trace-index.md b/docs/how-to/trace-index.md index 37124d6d5..12a91e4c3 100644 --- a/docs/how-to/trace-index.md +++ b/docs/how-to/trace-index.md @@ -162,6 +162,8 @@ Query GEMM / SDPA / convolution shapes from the satellite tables, or with `json_extract` on the parsed JSON columns. For example `SELECT "M", "N", "K" FROM gemm_perf` or `SELECT json_extract(perf_params_json, '$.M') FROM unified_perf_rows`. +Roofline stats are typed columns (`pct_roofline_mean`, `roofline_bound`, +`roofline_time_us`), not JSON. ## Example queries @@ -206,6 +208,21 @@ Channels equal groups (true depthwise) at 3×3 and 5×5 with widths 1536 / 3072 depthwise-conv workload, that's the file to open — found without reopening any trace. +### Which GEMMs are compute-bound with low roofline utilization? + +```sql +SELECT t.name, u.name AS op, u.pct_roofline_mean, u.roofline_bound +FROM unified_perf_rows u +JOIN traces t ON t.id = u.trace_id +WHERE u.op_category = 'GEMM' + AND u.roofline_bound = 'COMPUTE_BOUND' + AND u.pct_roofline_mean < 40 +ORDER BY u.pct_roofline_mean; +``` + +Roofline bound and utilization are typed columns on `unified_perf_rows`, so this +filter does not need `json_extract` on `raw_row_json`. + ### What are the longest attention sequences in the catalog? ```sql diff --git a/docs/reference/trace-index-schema.md b/docs/reference/trace-index-schema.md index 7d2ea3d81..7ada32eee 100644 --- a/docs/reference/trace-index-schema.md +++ b/docs/reference/trace-index-schema.md @@ -109,6 +109,13 @@ The following table lists the columns in `unified_perf_rows`. | `has_perf_model` | INTEGER | `1` when the row has a perf model, else `0`. | | `overlap_pct` | REAL | Overlap percentage from the report. | | `gpu_kernel_pct` | REAL | Operation share from the report's `Percentage (%)` column. | +| `pct_roofline_max` | REAL | Maximum `Pct Roofline` across instances of this op. | +| `pct_roofline_mean` | REAL | Mean `Pct Roofline` across instances of this op. | +| `pct_roofline_median` | REAL | Median `Pct Roofline` across instances of this op. | +| `pct_roofline_min` | REAL | Minimum `Pct Roofline` across instances of this op. | +| `pct_roofline_std` | REAL | Standard deviation of `Pct Roofline`. | +| `roofline_bound` | TEXT | `COMPUTE_BOUND` or `MEMORY_BOUND` from the report's `Roofline Bound` column. | +| `roofline_time_us` | REAL | Theoretical roofline time in microseconds (`Roofline Time (µs)_first`). | | `perf_params_json` | TEXT | Parsed `perf_params` as JSON. | | `kernel_details_json` | TEXT | Parsed `kernel_details_summary` as JSON. | | `raw_row_json` | TEXT | Full source CSV row as JSON. | diff --git a/tests/test_trace_index.py b/tests/test_trace_index.py index d38a2c87f..94e3df294 100644 --- a/tests/test_trace_index.py +++ b/tests/test_trace_index.py @@ -6,6 +6,7 @@ import csv import json +import sqlite3 from pathlib import Path import pytest @@ -103,6 +104,13 @@ def test_trace_index_append_from_report_and_search(tmp_path): "operation_count": "2", "Kernel Time (us)_sum": "123.5", "Percentage (%)": "80.0", + "Pct Roofline_max": "60.0", + "Pct Roofline_mean": "55.5", + "Pct Roofline_median": "54.0", + "Pct Roofline_min": "50.0", + "Pct Roofline_std": "2.5", + "Roofline Bound": "COMPUTE_BOUND", + "Roofline Time (\u00b5s)_first": "8.25", "TFLOPS/s_mean": "98.1", "perf_params": ( "{'M': 128, 'N': 64, 'K': 32, 'B': 1, 'bias': False, " @@ -165,7 +173,9 @@ def test_trace_index_append_from_report_and_search(tmp_path): rows = execute_read_query( db_path, - "SELECT name, op_category, kernel_time_sum_us, gpu_kernel_pct " + "SELECT name, op_category, kernel_time_sum_us, gpu_kernel_pct, " + "pct_roofline_mean, pct_roofline_max, pct_roofline_min, " + "roofline_bound, roofline_time_us " "FROM unified_perf_rows " "ORDER BY source_row", ) @@ -174,6 +184,11 @@ def test_trace_index_append_from_report_and_search(tmp_path): "op_category": "GEMM", "kernel_time_sum_us": 123.5, "gpu_kernel_pct": 80.0, + "pct_roofline_mean": 55.5, + "pct_roofline_max": 60.0, + "pct_roofline_min": 50.0, + "roofline_bound": "COMPUTE_BOUND", + "roofline_time_us": 8.25, } gemm = execute_read_query(db_path, 'SELECT "M", "N", "K", "B" FROM gemm_perf') @@ -245,6 +260,113 @@ def test_trace_index_append_from_report_and_search(tmp_path): assert search_rows[0]["trace_id"] == trace_id +def test_init_schema_backfills_roofline_columns_from_raw_json(tmp_path): + """Existing catalogs gain typed roofline columns via ALTER + JSON UPDATE.""" + db_path = tmp_path / "old.sqlite" + conn = sqlite3.connect(db_path) + conn.executescript(""" + CREATE TABLE traces ( + id INTEGER PRIMARY KEY, + tracelens_id TEXT UNIQUE, + root TEXT, + path TEXT NOT NULL UNIQUE, + rel_path TEXT, + name TEXT, + size_bytes INTEGER, + md5 TEXT, + format TEXT, + rank INTEGER, + top_dir TEXT, + parent_rel TEXT, + should_enrich INTEGER NOT NULL DEFAULT 1, + skip_reason TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + CREATE TABLE unified_perf_rows ( + id INTEGER PRIMARY KEY, + trace_id INTEGER NOT NULL, + source_row INTEGER NOT NULL, + name TEXT, + op_category TEXT, + operation_count INTEGER, + kernel_time_sum_us REAL, + kernel_time_mean_us REAL, + kernel_time_median_us REAL, + kernel_time_std_us REAL, + kernel_time_min_us REAL, + kernel_time_max_us REAL, + op_duration_us REAL, + tflops_mean REAL, + tflops_median REAL, + tbs_mean REAL, + tbs_median REAL, + gflops REAL, + data_moved_mb REAL, + flops_per_byte REAL, + compute_spec TEXT, + has_perf_model INTEGER, + overlap_pct REAL, + perf_params_json TEXT, + kernel_details_json TEXT, + raw_row_json TEXT + ); + CREATE INDEX idx_trace_index_unified_trace ON unified_perf_rows(trace_id); + CREATE INDEX idx_trace_index_unified_category ON unified_perf_rows(op_category); + CREATE INDEX idx_trace_index_unified_name ON unified_perf_rows(name); + """) + conn.execute( + "INSERT INTO traces(id, path, created_at, updated_at) VALUES (1, 't.json', 'x', 'x')" + ) + conn.execute( + "INSERT INTO unified_perf_rows(trace_id, source_row, name, raw_row_json) " + "VALUES (1, 0, 'aten::mm', ?)", + ( + json.dumps( + { + "Percentage (%)": "12.5", + "Pct Roofline_mean": "55.5", + "Pct Roofline_max": "60.0", + "Pct Roofline_median": "54.0", + "Pct Roofline_min": "50.0", + "Pct Roofline_std": "2.5", + "Roofline Bound": "MEMORY_BOUND", + "Roofline Time (\u00b5s)_first": "8.25", + } + ), + ), + ) + conn.commit() + conn.close() + + store = SQLiteTraceIndexStore(db_path) + try: + store.init_schema() + finally: + store.close() + + cols = table_column_names(db_path, "unified_perf_rows") + assert { + "gpu_kernel_pct", + "pct_roofline_mean", + "roofline_bound", + "roofline_time_us", + }.issubset(cols) + rows = execute_read_query( + db_path, + "SELECT gpu_kernel_pct, pct_roofline_mean, roofline_bound, roofline_time_us " + "FROM unified_perf_rows", + ) + assert rows == [ + { + "gpu_kernel_pct": 12.5, + "pct_roofline_mean": 55.5, + "roofline_bound": "MEMORY_BOUND", + "roofline_time_us": 8.25, + } + ] + + def test_import_handoff_uses_runner_tracelens_id_and_artifact_paths(tmp_path): db_path = tmp_path / "trace_index.sqlite" trace_path = write_stub_trace(tmp_path / "runner" / "trace.json") From e97595f4e8d3c014ebdd8f67c30d4bd669ef4987 Mon Sep 17 00:00:00 2001 From: Adeem Jassani Date: Fri, 11 Sep 2026 15:16:08 -0400 Subject: [PATCH 2/2] Retrigger CI after retargeting the PR to main. Co-authored-by: Cursor