ttnn-visualizer 0.49.0__py3-none-any.whl → 0.64.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ttnn_visualizer/app.py +151 -49
- ttnn_visualizer/csv_queries.py +154 -45
- ttnn_visualizer/decorators.py +0 -9
- ttnn_visualizer/exceptions.py +0 -7
- ttnn_visualizer/models.py +20 -1
- ttnn_visualizer/queries.py +8 -0
- ttnn_visualizer/serializers.py +53 -9
- ttnn_visualizer/settings.py +24 -10
- ttnn_visualizer/ssh_client.py +1 -4
- ttnn_visualizer/static/assets/allPaths-DWjqav_8.js +1 -0
- ttnn_visualizer/static/assets/allPathsLoader-B0eRT9aL.js +2 -0
- ttnn_visualizer/static/assets/index-BE2R-cuu.css +1 -0
- ttnn_visualizer/static/assets/index-BZITDwoa.js +1 -0
- ttnn_visualizer/static/assets/{index-DVrPLQJ7.js → index-DDrUX09k.js} +274 -479
- ttnn_visualizer/static/assets/index-voJy5fZe.js +1 -0
- ttnn_visualizer/static/assets/splitPathsBySizeLoader-_GpmIkFm.js +1 -0
- ttnn_visualizer/static/index.html +2 -2
- ttnn_visualizer/tests/test_serializers.py +2 -0
- ttnn_visualizer/tests/test_utils.py +362 -0
- ttnn_visualizer/utils.py +142 -0
- ttnn_visualizer/views.py +181 -87
- {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/METADATA +58 -30
- ttnn_visualizer-0.64.0.dist-info/RECORD +44 -0
- {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/licenses/LICENSE +6 -0
- ttnn_visualizer/remote_sqlite_setup.py +0 -100
- ttnn_visualizer/static/assets/allPaths-G_CNx_x1.js +0 -1
- ttnn_visualizer/static/assets/allPathsLoader-s_Yfmxfp.js +0 -2
- ttnn_visualizer/static/assets/index-CnPrfHYh.js +0 -1
- ttnn_visualizer/static/assets/index-Cnc1EkDo.js +0 -1
- ttnn_visualizer/static/assets/index-UuXdrHif.css +0 -7
- ttnn_visualizer/static/assets/splitPathsBySizeLoader-ivxxaHxa.js +0 -1
- ttnn_visualizer-0.49.0.dist-info/RECORD +0 -44
- {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/WHEEL +0 -0
- {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/entry_points.txt +0 -0
- {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/licenses/LICENSE_understanding.txt +0 -0
- {ttnn_visualizer-0.49.0.dist-info → ttnn_visualizer-0.64.0.dist-info}/top_level.txt +0 -0
ttnn_visualizer/app.py
CHANGED
|
@@ -25,7 +25,7 @@ from ttnn_visualizer.exceptions import (
|
|
|
25
25
|
)
|
|
26
26
|
from ttnn_visualizer.instances import create_instance_from_local_paths
|
|
27
27
|
from ttnn_visualizer.settings import Config, DefaultConfig
|
|
28
|
-
from ttnn_visualizer.utils import
|
|
28
|
+
from ttnn_visualizer.utils import find_gunicorn_path, get_app_data_directory
|
|
29
29
|
from werkzeug.debug import DebuggedApplication
|
|
30
30
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
31
31
|
|
|
@@ -55,8 +55,9 @@ def create_app(settings_override=None):
|
|
|
55
55
|
static_folder=config.STATIC_ASSETS_DIR,
|
|
56
56
|
static_url_path=f"{config.BASE_PATH}static",
|
|
57
57
|
)
|
|
58
|
-
logging.basicConfig(level=app.config.get("LOG_LEVEL", "INFO"))
|
|
59
58
|
|
|
59
|
+
# logging.basicConfig(level=app.config.get("LOG_LEVEL", "DEBUG"))
|
|
60
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
60
61
|
app.config.from_object(config)
|
|
61
62
|
|
|
62
63
|
if settings_override:
|
|
@@ -80,6 +81,7 @@ def create_app(settings_override=None):
|
|
|
80
81
|
"SERVER_MODE": app.config["SERVER_MODE"],
|
|
81
82
|
"BASE_PATH": app.config["BASE_PATH"],
|
|
82
83
|
"TT_METAL_HOME": app.config["TT_METAL_HOME"],
|
|
84
|
+
"REPORT_DATA_DIRECTORY": str(app.config["REPORT_DATA_DIRECTORY"]),
|
|
83
85
|
}
|
|
84
86
|
js = f"window.TTNN_VISUALIZER_CONFIG = {json.dumps(js_config)};"
|
|
85
87
|
|
|
@@ -114,23 +116,19 @@ def extensions(app: flask.Flask):
|
|
|
114
116
|
:param app: Flask application instance
|
|
115
117
|
:return: None
|
|
116
118
|
"""
|
|
117
|
-
|
|
118
119
|
flask_static_digest.init_app(app)
|
|
119
120
|
if app.config["USE_WEBSOCKETS"]:
|
|
120
121
|
socketio.init_app(app)
|
|
122
|
+
|
|
123
|
+
Path(app.config["APP_DATA_DIRECTORY"]).mkdir(parents=True, exist_ok=True)
|
|
121
124
|
db.init_app(app)
|
|
122
125
|
|
|
123
126
|
if app.config["USE_WEBSOCKETS"]:
|
|
124
127
|
register_handlers(socketio)
|
|
125
128
|
|
|
126
|
-
# Create the tables within the application context
|
|
127
129
|
with app.app_context():
|
|
128
130
|
db.create_all()
|
|
129
131
|
|
|
130
|
-
# For automatically reflecting table data
|
|
131
|
-
# with app.app_context():
|
|
132
|
-
# db.reflect()
|
|
133
|
-
|
|
134
132
|
return None
|
|
135
133
|
|
|
136
134
|
|
|
@@ -197,63 +195,151 @@ def parse_args():
|
|
|
197
195
|
parser.add_argument(
|
|
198
196
|
"--tt-metal-home", help="Specify a TT-Metal home path", default=None
|
|
199
197
|
)
|
|
198
|
+
parser.add_argument(
|
|
199
|
+
"--host",
|
|
200
|
+
type=str,
|
|
201
|
+
help="Host to bind to (default: auto-detected based on environment)",
|
|
202
|
+
default=None,
|
|
203
|
+
)
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
"--port",
|
|
206
|
+
type=str,
|
|
207
|
+
help="Port to bind to (default: 8000)",
|
|
208
|
+
default=None,
|
|
209
|
+
)
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"--server",
|
|
212
|
+
action="store_true",
|
|
213
|
+
help="Bind to all network interfaces (0.0.0.0) and enable server mode. Useful for servers and VMs",
|
|
214
|
+
)
|
|
215
|
+
parser.add_argument(
|
|
216
|
+
"-d",
|
|
217
|
+
"--daemon",
|
|
218
|
+
action="store_true",
|
|
219
|
+
help="Run the server as a daemon process",
|
|
220
|
+
)
|
|
200
221
|
return parser.parse_args()
|
|
201
222
|
|
|
202
223
|
|
|
224
|
+
def display_mode_info_without_db(config):
|
|
225
|
+
"""Display mode information using only config, without initializing database."""
|
|
226
|
+
# Determine if we're in TT-Metal mode
|
|
227
|
+
tt_metal_home = config.TT_METAL_HOME
|
|
228
|
+
is_tt_metal_mode = tt_metal_home is not None
|
|
229
|
+
|
|
230
|
+
if is_tt_metal_mode:
|
|
231
|
+
print("🚀 TT-METAL MODE: Working directly with tt-metal generated directory")
|
|
232
|
+
print(f" TT_METAL_HOME: {tt_metal_home}")
|
|
233
|
+
|
|
234
|
+
profiler_base = Path(tt_metal_home) / "generated" / "ttnn" / "reports"
|
|
235
|
+
performance_base = Path(tt_metal_home) / "generated" / "profiler" / "reports"
|
|
236
|
+
|
|
237
|
+
print(f" Profiler reports: {profiler_base}")
|
|
238
|
+
print(f" Performance reports: {performance_base}")
|
|
239
|
+
|
|
240
|
+
# Validate setup
|
|
241
|
+
if not Path(tt_metal_home).exists():
|
|
242
|
+
print(
|
|
243
|
+
f" ⚠️ Warning: TT_METAL_HOME directory does not exist: {tt_metal_home}"
|
|
244
|
+
)
|
|
245
|
+
elif not (Path(tt_metal_home) / "generated").exists():
|
|
246
|
+
print(f" ⚠️ Warning: TT-Metal generated directory not found")
|
|
247
|
+
elif not profiler_base.exists():
|
|
248
|
+
print(
|
|
249
|
+
f" ⚠️ Warning: Profiler reports directory not found: {profiler_base}"
|
|
250
|
+
)
|
|
251
|
+
elif not performance_base.exists():
|
|
252
|
+
print(
|
|
253
|
+
f" ⚠️ Warning: Performance reports directory not found: {performance_base}"
|
|
254
|
+
)
|
|
255
|
+
else:
|
|
256
|
+
print(f" ✓ TT-Metal setup is valid")
|
|
257
|
+
else:
|
|
258
|
+
print(
|
|
259
|
+
"📁 UPLOAD/SYNC MODE: Using local data directory for uploaded/synced reports"
|
|
260
|
+
)
|
|
261
|
+
print(f" Local directory: {config.LOCAL_DATA_DIRECTORY}")
|
|
262
|
+
print(f" Remote directory: {config.REMOTE_DATA_DIRECTORY}")
|
|
263
|
+
|
|
264
|
+
|
|
203
265
|
def main():
|
|
204
266
|
|
|
205
267
|
run_command = sys.argv[0].split("/")
|
|
206
268
|
if run_command[-1] == "ttnn-visualizer":
|
|
207
269
|
os.environ.setdefault("FLASK_ENV", "production")
|
|
208
270
|
|
|
209
|
-
config = cast(DefaultConfig, Config())
|
|
210
271
|
args = parse_args()
|
|
211
|
-
instance_id = None
|
|
212
272
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
273
|
+
# Handle host/port CLI overrides
|
|
274
|
+
# Priority: CLI args > env vars > auto-detection (in settings.py)
|
|
275
|
+
# Note: We need to set env vars before creating Config, but also
|
|
276
|
+
# manually update the config object in case it was already instantiated
|
|
277
|
+
if args.host:
|
|
278
|
+
os.environ["HOST"] = args.host
|
|
279
|
+
print(f"🌐 Binding to host: {args.host} (from --host flag)")
|
|
280
|
+
elif args.server:
|
|
281
|
+
os.environ["HOST"] = "0.0.0.0"
|
|
282
|
+
os.environ["SERVER_MODE"] = "true"
|
|
283
|
+
print("🌐 Binding to all interfaces (0.0.0.0) via --server flag")
|
|
284
|
+
print("🖥️ Server mode enabled")
|
|
285
|
+
|
|
286
|
+
if args.port:
|
|
287
|
+
os.environ["PORT"] = args.port
|
|
288
|
+
print(f"🔌 Binding to port: {args.port}")
|
|
289
|
+
|
|
290
|
+
config = cast(DefaultConfig, Config())
|
|
291
|
+
|
|
292
|
+
# Apply CLI overrides directly to config object
|
|
293
|
+
# (Config is a singleton that may have been created before we set env vars)
|
|
294
|
+
if args.host:
|
|
295
|
+
config.HOST = args.host
|
|
296
|
+
elif args.server:
|
|
297
|
+
config.HOST = "0.0.0.0"
|
|
298
|
+
config.SERVER_MODE = True
|
|
225
299
|
|
|
226
|
-
|
|
300
|
+
if args.port:
|
|
301
|
+
config.PORT = args.port
|
|
227
302
|
|
|
303
|
+
# Recalculate GUNICORN_BIND with the updated values
|
|
304
|
+
config.GUNICORN_BIND = f"{config.HOST}:{config.PORT}"
|
|
305
|
+
|
|
306
|
+
instance_id = None
|
|
307
|
+
|
|
308
|
+
# Display mode information first (using config only, no DB needed)
|
|
228
309
|
if args.tt_metal_home:
|
|
310
|
+
os.environ["TT_METAL_HOME"] = args.tt_metal_home
|
|
229
311
|
config.TT_METAL_HOME = args.tt_metal_home
|
|
230
312
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
resolver = create_path_resolver(app)
|
|
235
|
-
mode_info = resolver.get_mode_info()
|
|
236
|
-
|
|
237
|
-
if mode_info["mode"] == "tt_metal":
|
|
238
|
-
print(
|
|
239
|
-
"🚀 TT-METAL MODE: Working directly with tt-metal generated directory"
|
|
313
|
+
if not os.getenv("APP_DATA_DIRECTORY"):
|
|
314
|
+
config.APP_DATA_DIRECTORY = get_app_data_directory(
|
|
315
|
+
args.tt_metal_home, config.APPLICATION_DIR
|
|
240
316
|
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
# Validate setup
|
|
246
|
-
is_valid, message = resolver.validate_tt_metal_setup()
|
|
247
|
-
if is_valid:
|
|
248
|
-
print(f" ✓ {message}")
|
|
249
|
-
else:
|
|
250
|
-
print(f" ⚠️ Warning: {message}")
|
|
251
|
-
else:
|
|
252
|
-
print(
|
|
253
|
-
"📁 UPLOAD/SYNC MODE: Using local data directory for uploaded/synced reports"
|
|
317
|
+
# Recalculate database path with new APP_DATA_DIRECTORY
|
|
318
|
+
_db_file_path = str(
|
|
319
|
+
Path(config.APP_DATA_DIRECTORY) / f"ttnn_{config.DB_VERSION}.db"
|
|
254
320
|
)
|
|
255
|
-
|
|
256
|
-
|
|
321
|
+
config.SQLALCHEMY_DATABASE_URI = f"sqlite:///{_db_file_path}"
|
|
322
|
+
|
|
323
|
+
display_mode_info_without_db(config)
|
|
324
|
+
|
|
325
|
+
# If profiler/performance paths are provided, create an instance
|
|
326
|
+
# This requires DB access, so we create the app temporarily
|
|
327
|
+
if args.profiler_path or args.performance_path:
|
|
328
|
+
app = create_app()
|
|
329
|
+
with app.app_context():
|
|
330
|
+
try:
|
|
331
|
+
session = create_instance_from_local_paths(
|
|
332
|
+
profiler_path=args.profiler_path,
|
|
333
|
+
performance_path=args.performance_path,
|
|
334
|
+
)
|
|
335
|
+
instance_id = session.instance_id
|
|
336
|
+
except InvalidReportPath:
|
|
337
|
+
sys.exit("Invalid report path")
|
|
338
|
+
except InvalidProfilerPath:
|
|
339
|
+
sys.exit("Invalid profiler path")
|
|
340
|
+
|
|
341
|
+
# Clean up this temporary app - workers will create their own
|
|
342
|
+
del app
|
|
257
343
|
|
|
258
344
|
# Check if DEBUG environment variable is set
|
|
259
345
|
debug_mode = os.environ.get("DEBUG", "false").lower() == "true"
|
|
@@ -262,8 +348,21 @@ def main():
|
|
|
262
348
|
for key, value in config.to_dict().items():
|
|
263
349
|
print(f"{key}={value}")
|
|
264
350
|
|
|
351
|
+
# Warn if there's a gunicorn config file in current directory
|
|
352
|
+
if Path("gunicorn.conf.py").exists():
|
|
353
|
+
logger.warning(
|
|
354
|
+
"Found gunicorn.conf.py in current directory - this may override environment settings"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
gunicorn_cmd, gunicorn_warning = find_gunicorn_path()
|
|
358
|
+
|
|
359
|
+
if gunicorn_warning:
|
|
360
|
+
print(gunicorn_warning)
|
|
361
|
+
|
|
265
362
|
gunicorn_args = [
|
|
266
|
-
|
|
363
|
+
gunicorn_cmd,
|
|
364
|
+
"-t",
|
|
365
|
+
config.GUNICORN_TIMEOUT,
|
|
267
366
|
"-k",
|
|
268
367
|
config.GUNICORN_WORKER_CLASS,
|
|
269
368
|
"-w",
|
|
@@ -276,7 +375,10 @@ def main():
|
|
|
276
375
|
if debug_mode:
|
|
277
376
|
gunicorn_args.insert(1, "--reload")
|
|
278
377
|
|
|
279
|
-
if
|
|
378
|
+
if args.daemon:
|
|
379
|
+
gunicorn_args.insert(1, "--daemon")
|
|
380
|
+
|
|
381
|
+
if config.LAUNCH_BROWSER_ON_START and not args.daemon:
|
|
280
382
|
flask_env = os.getenv("FLASK_ENV", "development")
|
|
281
383
|
port = config.PORT if flask_env == "production" else config.DEV_SERVER_PORT
|
|
282
384
|
host = config.HOST if flask_env == "production" else config.DEV_SERVER_HOST
|
ttnn_visualizer/csv_queries.py
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import csv
|
|
6
6
|
import json
|
|
7
|
+
import logging
|
|
7
8
|
import os
|
|
8
9
|
import tempfile
|
|
9
10
|
from io import StringIO
|
|
@@ -14,7 +15,9 @@ import pandas as pd
|
|
|
14
15
|
import zstd
|
|
15
16
|
from tt_perf_report import perf_report
|
|
16
17
|
from ttnn_visualizer.exceptions import DataFormatError
|
|
17
|
-
from ttnn_visualizer.models import Instance
|
|
18
|
+
from ttnn_visualizer.models import Instance
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
class LocalCSVQueryRunner:
|
|
@@ -100,7 +103,6 @@ class NPEQueries:
|
|
|
100
103
|
|
|
101
104
|
@staticmethod
|
|
102
105
|
def get_npe_manifest(instance: Instance):
|
|
103
|
-
|
|
104
106
|
file_path = Path(
|
|
105
107
|
instance.performance_path,
|
|
106
108
|
NPEQueries.NPE_FOLDER,
|
|
@@ -417,34 +419,71 @@ class OpsPerformanceReportQueries:
|
|
|
417
419
|
"raw_op_code",
|
|
418
420
|
]
|
|
419
421
|
|
|
422
|
+
STACKED_REPORT_COLUMNS = [
|
|
423
|
+
"percent",
|
|
424
|
+
"op_code",
|
|
425
|
+
"device_time_sum_us",
|
|
426
|
+
"ops_count",
|
|
427
|
+
"flops_min",
|
|
428
|
+
"flops_max",
|
|
429
|
+
"flops_mean",
|
|
430
|
+
"flops_std",
|
|
431
|
+
]
|
|
432
|
+
|
|
420
433
|
PASSTHROUGH_COLUMNS = {
|
|
421
434
|
"pm_ideal_ns": "PM IDEAL [ns]",
|
|
422
435
|
}
|
|
423
436
|
|
|
424
|
-
|
|
425
|
-
|
|
437
|
+
DEFAULT_START_SIGNPOST = None
|
|
438
|
+
DEFAULT_END_SIGNPOST = None
|
|
439
|
+
DEFAULT_IGNORE_SIGNPOSTS = True
|
|
426
440
|
DEFAULT_MIN_PERCENTAGE = 0.5
|
|
427
441
|
DEFAULT_ID_RANGE = None
|
|
428
442
|
DEFAULT_NO_ADVICE = False
|
|
429
443
|
DEFAULT_TRACING_MODE = False
|
|
444
|
+
DEFAULT_RAW_OP_CODES = True
|
|
445
|
+
DEFAULT_NO_HOST_OPS = False
|
|
446
|
+
DEFAULT_NO_STACKED_REPORT = False
|
|
447
|
+
DEFAULT_NO_STACK_BY_IN0 = True
|
|
430
448
|
|
|
431
449
|
@classmethod
|
|
432
|
-
def generate_report(cls, instance):
|
|
450
|
+
def generate_report(cls, instance, **kwargs):
|
|
433
451
|
raw_csv = OpsPerformanceQueries.get_raw_csv(instance)
|
|
434
452
|
csv_file = StringIO(raw_csv)
|
|
435
453
|
csv_output_file = tempfile.mktemp(suffix=".csv")
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
)
|
|
454
|
+
csv_stacked_output_file = tempfile.mktemp(suffix=".csv")
|
|
455
|
+
start_signpost = kwargs.get("start_signpost", cls.DEFAULT_START_SIGNPOST)
|
|
456
|
+
end_signpost = kwargs.get("end_signpost", cls.DEFAULT_END_SIGNPOST)
|
|
457
|
+
ignore_signposts = cls.DEFAULT_IGNORE_SIGNPOSTS
|
|
458
|
+
stack_by_in0 = kwargs.get("stack_by_in0", cls.DEFAULT_NO_STACK_BY_IN0)
|
|
459
|
+
no_host_ops = kwargs.get("hide_host_ops", cls.DEFAULT_NO_HOST_OPS)
|
|
460
|
+
|
|
461
|
+
if start_signpost or end_signpost:
|
|
462
|
+
ignore_signposts = False
|
|
463
|
+
|
|
464
|
+
# perf_report currently generates a PNG alongside the CSV using the same temp name - we'll just delete it afterwards
|
|
465
|
+
stacked_png_file = os.path.splitext(csv_output_file)[0] + ".png"
|
|
466
|
+
|
|
467
|
+
try:
|
|
468
|
+
perf_report.generate_perf_report(
|
|
469
|
+
[csv_file],
|
|
470
|
+
start_signpost,
|
|
471
|
+
end_signpost,
|
|
472
|
+
ignore_signposts,
|
|
473
|
+
cls.DEFAULT_MIN_PERCENTAGE,
|
|
474
|
+
cls.DEFAULT_ID_RANGE,
|
|
475
|
+
csv_output_file,
|
|
476
|
+
cls.DEFAULT_NO_ADVICE,
|
|
477
|
+
cls.DEFAULT_TRACING_MODE,
|
|
478
|
+
cls.DEFAULT_RAW_OP_CODES,
|
|
479
|
+
no_host_ops,
|
|
480
|
+
cls.DEFAULT_NO_STACKED_REPORT,
|
|
481
|
+
stack_by_in0,
|
|
482
|
+
csv_stacked_output_file,
|
|
483
|
+
)
|
|
484
|
+
except Exception as e:
|
|
485
|
+
logger.error(f"Error generating performance report: {e}")
|
|
486
|
+
raise DataFormatError(f"Error generating performance report: {e}") from e
|
|
448
487
|
|
|
449
488
|
ops_perf_results = []
|
|
450
489
|
ops_perf_results_reader = csv.DictReader(StringIO(raw_csv))
|
|
@@ -452,34 +491,104 @@ class OpsPerformanceReportQueries:
|
|
|
452
491
|
for row in ops_perf_results_reader:
|
|
453
492
|
ops_perf_results.append(row)
|
|
454
493
|
|
|
494
|
+
# Returns a list of unique signposts in the order they appear
|
|
495
|
+
# TODO: Signpost names are not unique but tt-perf-report treats them as such
|
|
496
|
+
captured_signposts = set()
|
|
497
|
+
signposts = []
|
|
498
|
+
for index, row in enumerate(ops_perf_results):
|
|
499
|
+
if row.get("OP TYPE") == "signpost":
|
|
500
|
+
op_code = row["OP CODE"]
|
|
501
|
+
op_id = index + 2 # Match IDs with row numbers in ops perf results csv
|
|
502
|
+
if not any(s["op_code"] == op_code for s in signposts):
|
|
503
|
+
captured_signposts.add(op_code)
|
|
504
|
+
signposts.append(
|
|
505
|
+
{
|
|
506
|
+
"id": op_id,
|
|
507
|
+
"op_code": op_code,
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
|
|
455
511
|
report = []
|
|
456
512
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
column: row[index]
|
|
464
|
-
for index, column in enumerate(cls.REPORT_COLUMNS)
|
|
465
|
-
if index < len(row)
|
|
466
|
-
}
|
|
467
|
-
if "advice" in processed_row and processed_row["advice"]:
|
|
468
|
-
processed_row["advice"] = processed_row["advice"].split(" • ")
|
|
469
|
-
else:
|
|
470
|
-
processed_row["advice"] = []
|
|
471
|
-
|
|
472
|
-
for key, value in cls.PASSTHROUGH_COLUMNS.items():
|
|
513
|
+
if os.path.exists(csv_output_file):
|
|
514
|
+
try:
|
|
515
|
+
with open(csv_output_file, newline="") as csvfile:
|
|
516
|
+
reader = csv.reader(csvfile, delimiter=",")
|
|
517
|
+
next(reader, None)
|
|
518
|
+
for row in reader:
|
|
473
519
|
op_id = int(row[0])
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
processed_row
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
520
|
+
# IDs in result column one correspond to row numbers in ops perf results csv
|
|
521
|
+
idx = op_id - 2
|
|
522
|
+
|
|
523
|
+
processed_row = {
|
|
524
|
+
column: row[index]
|
|
525
|
+
for index, column in enumerate(cls.REPORT_COLUMNS)
|
|
526
|
+
if index < len(row)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if "advice" in processed_row and processed_row["advice"]:
|
|
530
|
+
processed_row["advice"] = processed_row["advice"].split(
|
|
531
|
+
" • "
|
|
532
|
+
)
|
|
533
|
+
else:
|
|
534
|
+
processed_row["advice"] = []
|
|
535
|
+
|
|
536
|
+
for key, value in cls.PASSTHROUGH_COLUMNS.items():
|
|
537
|
+
if 0 <= idx < len(ops_perf_results):
|
|
538
|
+
|
|
539
|
+
processed_row[key] = ops_perf_results[idx][value]
|
|
540
|
+
else:
|
|
541
|
+
processed_row[key] = None
|
|
542
|
+
|
|
543
|
+
# Get the op type from the raw file for this row as it is not returned from tt-perf-report
|
|
544
|
+
if 0 <= idx < len(ops_perf_results):
|
|
545
|
+
processed_row["op_type"] = ops_perf_results[idx].get(
|
|
546
|
+
"OP TYPE"
|
|
547
|
+
)
|
|
548
|
+
else:
|
|
549
|
+
processed_row["op_type"] = None
|
|
550
|
+
|
|
551
|
+
report.append(processed_row)
|
|
552
|
+
except csv.Error as e:
|
|
553
|
+
raise DataFormatError() from e
|
|
554
|
+
finally:
|
|
555
|
+
os.unlink(csv_output_file)
|
|
556
|
+
|
|
557
|
+
stacked_report = []
|
|
558
|
+
|
|
559
|
+
if os.path.exists(csv_stacked_output_file):
|
|
560
|
+
try:
|
|
561
|
+
with open(csv_stacked_output_file, newline="") as csvfile:
|
|
562
|
+
reader = csv.reader(csvfile, delimiter=",")
|
|
563
|
+
next(reader, None)
|
|
564
|
+
|
|
565
|
+
for row in reader:
|
|
566
|
+
processed_row = {
|
|
567
|
+
column: row[index]
|
|
568
|
+
for index, column in enumerate(cls.STACKED_REPORT_COLUMNS)
|
|
569
|
+
if index < len(row)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if "op_code" in processed_row and any(
|
|
573
|
+
processed_row["op_code"] in signpost["op_code"]
|
|
574
|
+
for signpost in signposts
|
|
575
|
+
):
|
|
576
|
+
processed_row["op_type"] = "signpost"
|
|
577
|
+
else:
|
|
578
|
+
processed_row["op_type"] = "unknown"
|
|
579
|
+
|
|
580
|
+
stacked_report.append(processed_row)
|
|
581
|
+
except csv.Error as e:
|
|
582
|
+
raise DataFormatError() from e
|
|
583
|
+
finally:
|
|
584
|
+
os.unlink(csv_stacked_output_file)
|
|
585
|
+
if os.path.exists(stacked_png_file):
|
|
586
|
+
os.unlink(stacked_png_file)
|
|
587
|
+
|
|
588
|
+
stacked_report.append(processed_row)
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
"report": report,
|
|
592
|
+
"stacked_report": stacked_report,
|
|
593
|
+
"signposts": signposts,
|
|
594
|
+
}
|
ttnn_visualizer/decorators.py
CHANGED
|
@@ -14,7 +14,6 @@ from ttnn_visualizer.exceptions import (
|
|
|
14
14
|
NoProjectsException,
|
|
15
15
|
NoValidConnectionsError,
|
|
16
16
|
RemoteConnectionException,
|
|
17
|
-
RemoteSqliteException,
|
|
18
17
|
SSHException,
|
|
19
18
|
)
|
|
20
19
|
from ttnn_visualizer.instances import get_or_create_instance
|
|
@@ -115,14 +114,6 @@ def remote_exception_handler(func):
|
|
|
115
114
|
message=user_message,
|
|
116
115
|
)
|
|
117
116
|
|
|
118
|
-
except RemoteSqliteException as err:
|
|
119
|
-
current_app.logger.error(f"Remote Sqlite exception: {str(err)}")
|
|
120
|
-
message = err.message
|
|
121
|
-
if "No such file" in str(err):
|
|
122
|
-
message = "Unable to open SQLite binary, check path"
|
|
123
|
-
raise RemoteConnectionException(
|
|
124
|
-
status=ConnectionTestStates.FAILED, message=message
|
|
125
|
-
)
|
|
126
117
|
except IOError as err:
|
|
127
118
|
message = f"Error opening remote folder: {str(err)}"
|
|
128
119
|
if "Name or service not known" in str(err):
|
ttnn_visualizer/exceptions.py
CHANGED
|
@@ -56,13 +56,6 @@ class NoProjectsException(RemoteConnectionException):
|
|
|
56
56
|
pass
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
class RemoteSqliteException(Exception):
|
|
60
|
-
def __init__(self, message, status):
|
|
61
|
-
super().__init__(message)
|
|
62
|
-
self.message = message
|
|
63
|
-
self.status = status
|
|
64
|
-
|
|
65
|
-
|
|
66
59
|
class DatabaseFileNotFoundException(Exception):
|
|
67
60
|
pass
|
|
68
61
|
|
ttnn_visualizer/models.py
CHANGED
|
@@ -154,6 +154,26 @@ class StackTrace(SerializeableDataclass):
|
|
|
154
154
|
stack_trace: str
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
@dataclasses.dataclass
|
|
158
|
+
class ErrorRecord(SerializeableDataclass):
|
|
159
|
+
operation_id: int
|
|
160
|
+
operation_name: str
|
|
161
|
+
error_type: str
|
|
162
|
+
error_message: str
|
|
163
|
+
stack_trace: str
|
|
164
|
+
timestamp: str
|
|
165
|
+
|
|
166
|
+
def to_nested_dict(self) -> dict:
|
|
167
|
+
"""
|
|
168
|
+
Returns a dictionary representation without operation_id and operation_name.
|
|
169
|
+
Use this when the error is nested under an operation to avoid redundancy.
|
|
170
|
+
"""
|
|
171
|
+
result = self.to_dict()
|
|
172
|
+
result.pop("operation_id", None)
|
|
173
|
+
result.pop("operation_name", None)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
157
177
|
# Non Data Models
|
|
158
178
|
|
|
159
179
|
|
|
@@ -169,7 +189,6 @@ class RemoteConnection(SerializeableModel):
|
|
|
169
189
|
port: int = Field(ge=1, le=65535)
|
|
170
190
|
profilerPath: str
|
|
171
191
|
performancePath: Optional[str] = None
|
|
172
|
-
sqliteBinaryPath: Optional[str] = None
|
|
173
192
|
|
|
174
193
|
|
|
175
194
|
class StatusMessage(SerializeableModel):
|
ttnn_visualizer/queries.py
CHANGED
|
@@ -12,6 +12,7 @@ from ttnn_visualizer.models import (
|
|
|
12
12
|
BufferPage,
|
|
13
13
|
Device,
|
|
14
14
|
DeviceOperation,
|
|
15
|
+
ErrorRecord,
|
|
15
16
|
InputTensor,
|
|
16
17
|
Instance,
|
|
17
18
|
Operation,
|
|
@@ -156,6 +157,13 @@ class DatabaseQueries:
|
|
|
156
157
|
operation_id, stack_trace = row
|
|
157
158
|
yield StackTrace(operation_id, stack_trace=stack_trace)
|
|
158
159
|
|
|
160
|
+
def query_error_records(
|
|
161
|
+
self, filters: Optional[Dict[str, Any]] = None
|
|
162
|
+
) -> Generator[ErrorRecord, None, None]:
|
|
163
|
+
rows = self._query_table("errors", filters)
|
|
164
|
+
for row in rows:
|
|
165
|
+
yield ErrorRecord(*row)
|
|
166
|
+
|
|
159
167
|
def query_tensor_comparisons(
|
|
160
168
|
self, local: bool = True, filters: Optional[Dict[str, Any]] = None
|
|
161
169
|
) -> Generator[TensorComparisonRecord, None, None]:
|