ttnn-visualizer 0.41.0__py3-none-any.whl → 0.43.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/__init__.py +0 -1
- ttnn_visualizer/app.py +15 -4
- ttnn_visualizer/csv_queries.py +150 -40
- ttnn_visualizer/decorators.py +42 -16
- ttnn_visualizer/exceptions.py +45 -1
- ttnn_visualizer/file_uploads.py +1 -0
- ttnn_visualizer/instances.py +42 -15
- ttnn_visualizer/models.py +12 -7
- ttnn_visualizer/queries.py +3 -109
- ttnn_visualizer/remote_sqlite_setup.py +104 -19
- ttnn_visualizer/requirements.txt +2 -3
- ttnn_visualizer/serializers.py +1 -0
- ttnn_visualizer/settings.py +9 -5
- ttnn_visualizer/sftp_operations.py +657 -220
- ttnn_visualizer/sockets.py +9 -3
- ttnn_visualizer/static/assets/{allPaths-4_pFqSAW.js → allPaths-BQN_j7ek.js} +1 -1
- ttnn_visualizer/static/assets/{allPathsLoader-CpLPTLlt.js → allPathsLoader-BvkkQ77q.js} +2 -2
- ttnn_visualizer/static/assets/index-B-fsa5Ru.js +1 -0
- ttnn_visualizer/static/assets/{index-DFVwehlj.js → index-Bng0kcmi.js} +214 -214
- ttnn_visualizer/static/assets/{index-C1rJBrMl.css → index-C-t6jBt9.css} +1 -1
- ttnn_visualizer/static/assets/index-DLOviMB1.js +1 -0
- ttnn_visualizer/static/assets/{splitPathsBySizeLoader-D-RvsTqO.js → splitPathsBySizeLoader-Cl0NRdfL.js} +1 -1
- ttnn_visualizer/static/index.html +2 -2
- ttnn_visualizer/tests/__init__.py +0 -1
- ttnn_visualizer/tests/test_queries.py +0 -69
- ttnn_visualizer/tests/test_serializers.py +2 -2
- ttnn_visualizer/utils.py +7 -3
- ttnn_visualizer/views.py +315 -52
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/LICENSE +0 -1
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/METADATA +6 -3
- ttnn_visualizer-0.43.0.dist-info/RECORD +45 -0
- ttnn_visualizer/ssh_client.py +0 -85
- ttnn_visualizer/static/assets/index-BKzgFDAn.js +0 -1
- ttnn_visualizer/static/assets/index-BvSuWPlB.js +0 -1
- ttnn_visualizer-0.41.0.dist-info/RECORD +0 -46
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/LICENSE_understanding.txt +0 -0
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/WHEEL +0 -0
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/entry_points.txt +0 -0
- {ttnn_visualizer-0.41.0.dist-info → ttnn_visualizer-0.43.0.dist-info}/top_level.txt +0 -0
ttnn_visualizer/models.py
CHANGED
@@ -119,6 +119,7 @@ class Tensor(SerializeableDataclass):
|
|
119
119
|
def __post_init__(self):
|
120
120
|
self.memory_config = parse_memory_config(self.memory_config)
|
121
121
|
|
122
|
+
|
122
123
|
@dataclasses.dataclass
|
123
124
|
class InputTensor(SerializeableDataclass):
|
124
125
|
operation_id: int
|
@@ -246,19 +247,21 @@ class InstanceTable(db.Model):
|
|
246
247
|
"remote_performance_folder": self.remote_performance_folder,
|
247
248
|
"profiler_path": self.profiler_path,
|
248
249
|
"performance_path": self.performance_path,
|
249
|
-
"npe_path": self.npe_path
|
250
|
+
"npe_path": self.npe_path,
|
250
251
|
}
|
251
252
|
|
252
253
|
def to_pydantic(self) -> Instance:
|
253
254
|
return Instance(
|
254
255
|
instance_id=str(self.instance_id),
|
255
|
-
profiler_path=
|
256
|
-
|
257
|
-
str(self.performance_path) if self.performance_path is not None else None
|
256
|
+
profiler_path=(
|
257
|
+
str(self.profiler_path) if self.profiler_path is not None else None
|
258
258
|
),
|
259
|
-
|
260
|
-
str(self.
|
259
|
+
performance_path=(
|
260
|
+
str(self.performance_path)
|
261
|
+
if self.performance_path is not None
|
262
|
+
else None
|
261
263
|
),
|
264
|
+
npe_path=(str(self.npe_path) if self.npe_path is not None else None),
|
262
265
|
active_report=(
|
263
266
|
(ActiveReports(**self.active_report) if self.active_report else None)
|
264
267
|
if isinstance(self.active_report, dict)
|
@@ -270,7 +273,9 @@ class InstanceTable(db.Model):
|
|
270
273
|
else None
|
271
274
|
),
|
272
275
|
remote_profiler_folder=(
|
273
|
-
RemoteReportFolder.model_validate(
|
276
|
+
RemoteReportFolder.model_validate(
|
277
|
+
self.remote_profiler_folder, strict=False
|
278
|
+
)
|
274
279
|
if self.remote_profiler_folder is not None
|
275
280
|
else None
|
276
281
|
),
|
ttnn_visualizer/queries.py
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
#
|
3
3
|
# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
|
4
4
|
|
5
|
-
import json
|
6
5
|
from typing import Generator, Dict, Any, Union
|
7
6
|
|
8
7
|
from ttnn_visualizer.exceptions import (
|
@@ -23,11 +22,9 @@ from ttnn_visualizer.models import (
|
|
23
22
|
ProducersConsumers,
|
24
23
|
TensorComparisonRecord,
|
25
24
|
)
|
26
|
-
from ttnn_visualizer.ssh_client import get_client
|
27
25
|
import sqlite3
|
28
26
|
from typing import List, Optional
|
29
27
|
from pathlib import Path
|
30
|
-
import paramiko
|
31
28
|
|
32
29
|
|
33
30
|
class LocalQueryRunner:
|
@@ -63,111 +60,10 @@ class LocalQueryRunner:
|
|
63
60
|
self.connection.close()
|
64
61
|
|
65
62
|
|
66
|
-
class RemoteQueryRunner:
|
67
|
-
column_delimiter = "|||"
|
68
|
-
|
69
|
-
def __init__(self, instance: Instance):
|
70
|
-
self.instance = instance
|
71
|
-
self._validate_instance()
|
72
|
-
self.ssh_client = self._get_ssh_client(self.instance.remote_connection)
|
73
|
-
self.sqlite_binary = self.instance.remote_connection.sqliteBinaryPath
|
74
|
-
self.remote_db_path = str(
|
75
|
-
Path(self.instance.remote_profiler_folder.remotePath, "db.sqlite")
|
76
|
-
)
|
77
|
-
|
78
|
-
def _validate_instance(self):
|
79
|
-
"""
|
80
|
-
Validate that the instance has all required remote connection attributes.
|
81
|
-
"""
|
82
|
-
if (
|
83
|
-
not self.instance.remote_connection
|
84
|
-
or not self.instance.remote_connection.sqliteBinaryPath
|
85
|
-
or not self.instance.remote_profiler_folder
|
86
|
-
or not self.instance.remote_profiler_folder.remotePath
|
87
|
-
):
|
88
|
-
raise ValueError(
|
89
|
-
"Remote connections require remote path and sqliteBinaryPath"
|
90
|
-
)
|
91
|
-
|
92
|
-
def _get_ssh_client(self, remote_connection) -> paramiko.SSHClient:
|
93
|
-
"""
|
94
|
-
Retrieve the SSH client for the given remote connection.
|
95
|
-
"""
|
96
|
-
return get_client(remote_connection=remote_connection)
|
97
|
-
|
98
|
-
def _format_query(self, query: str, params: Optional[List] = None) -> str:
|
99
|
-
"""
|
100
|
-
Format the query by replacing placeholders with properly quoted parameters.
|
101
|
-
"""
|
102
|
-
if not params:
|
103
|
-
return query
|
104
|
-
|
105
|
-
formatted_params = [
|
106
|
-
f"'{param}'" if isinstance(param, str) else str(param) for param in params
|
107
|
-
]
|
108
|
-
return query.replace("?", "{}").format(*formatted_params)
|
109
|
-
|
110
|
-
def _build_command(self, formatted_query: str) -> str:
|
111
|
-
"""
|
112
|
-
Build the remote SQLite command.
|
113
|
-
"""
|
114
|
-
return f'{self.sqlite_binary} {self.remote_db_path} "{formatted_query}" -json'
|
115
|
-
|
116
|
-
def _execute_ssh_command(self, command: str) -> tuple:
|
117
|
-
"""
|
118
|
-
Execute the SSH command and return the standard output and error.
|
119
|
-
"""
|
120
|
-
stdin, stdout, stderr = self.ssh_client.exec_command(command)
|
121
|
-
output = stdout.read().decode("utf-8").strip()
|
122
|
-
error_output = stderr.read().decode("utf-8").strip()
|
123
|
-
return output, error_output
|
124
|
-
|
125
|
-
def _parse_output(self, output: str, command: str) -> List:
|
126
|
-
"""
|
127
|
-
Parse the output from the SQLite command. Attempt JSON parsing first,
|
128
|
-
then fall back to line-based parsing.
|
129
|
-
"""
|
130
|
-
if not output.strip():
|
131
|
-
return []
|
132
|
-
|
133
|
-
try:
|
134
|
-
rows = json.loads(output)
|
135
|
-
return [tuple(row.values()) for row in rows]
|
136
|
-
except json.JSONDecodeError:
|
137
|
-
print(
|
138
|
-
f"Output is not valid JSON, attempting manual parsing.\nCommand: {command}"
|
139
|
-
)
|
140
|
-
return [tuple(line.split("|")) for line in output.splitlines()]
|
141
|
-
|
142
|
-
def execute_query(self, query: str, params: Optional[List] = None) -> List:
|
143
|
-
"""
|
144
|
-
Execute a remote SQLite query using the instance's SSH client.
|
145
|
-
"""
|
146
|
-
self._validate_instance()
|
147
|
-
formatted_query = self._format_query(query, params)
|
148
|
-
command = self._build_command(formatted_query)
|
149
|
-
output, error_output = self._execute_ssh_command(command)
|
150
|
-
|
151
|
-
if error_output:
|
152
|
-
raise RuntimeError(
|
153
|
-
f"Error executing query remotely: {error_output}\nCommand: {command}"
|
154
|
-
)
|
155
|
-
|
156
|
-
return self._parse_output(output, command)
|
157
|
-
|
158
|
-
def close(self):
|
159
|
-
"""
|
160
|
-
Close the SSH connection.
|
161
|
-
"""
|
162
|
-
if self.ssh_client:
|
163
|
-
self.ssh_client.close()
|
164
|
-
|
165
|
-
|
166
63
|
class DatabaseQueries:
|
167
64
|
|
168
65
|
instance: Optional[Instance] = None
|
169
|
-
|
170
|
-
query_runner: LocalQueryRunner | RemoteQueryRunner
|
66
|
+
query_runner: LocalQueryRunner
|
171
67
|
|
172
68
|
def __init__(self, instance: Optional[Instance] = None, connection=None):
|
173
69
|
self.instance = instance
|
@@ -181,7 +77,7 @@ class DatabaseQueries:
|
|
181
77
|
)
|
182
78
|
remote_connection = instance.remote_connection if instance else None
|
183
79
|
if remote_connection and remote_connection.useRemoteQuerying:
|
184
|
-
|
80
|
+
raise NotImplementedError("Remote querying is not implemented yet")
|
185
81
|
else:
|
186
82
|
self.query_runner = LocalQueryRunner(instance=instance)
|
187
83
|
|
@@ -382,7 +278,5 @@ class DatabaseQueries:
|
|
382
278
|
return self
|
383
279
|
|
384
280
|
def __exit__(self, exc_type, exc_value, traceback):
|
385
|
-
if isinstance(self.query_runner,
|
386
|
-
self.query_runner.close()
|
387
|
-
elif isinstance(self.query_runner, LocalQueryRunner):
|
281
|
+
if isinstance(self.query_runner, LocalQueryRunner):
|
388
282
|
self.query_runner.close()
|
@@ -3,29 +3,116 @@
|
|
3
3
|
# SPDX-FileCopyrightText: © 2025 Tenstorrent AI ULC
|
4
4
|
|
5
5
|
import re
|
6
|
+
import subprocess
|
6
7
|
|
7
8
|
from ttnn_visualizer.decorators import remote_exception_handler
|
8
9
|
from ttnn_visualizer.enums import ConnectionTestStates
|
9
|
-
from ttnn_visualizer.exceptions import
|
10
|
+
from ttnn_visualizer.exceptions import (
|
11
|
+
RemoteSqliteException,
|
12
|
+
SSHException,
|
13
|
+
AuthenticationException,
|
14
|
+
NoValidConnectionsError,
|
15
|
+
)
|
10
16
|
from ttnn_visualizer.models import RemoteConnection
|
11
|
-
|
17
|
+
|
18
|
+
|
19
|
+
def handle_ssh_subprocess_error(
|
20
|
+
e: subprocess.CalledProcessError, remote_connection: RemoteConnection
|
21
|
+
):
|
22
|
+
"""
|
23
|
+
Convert subprocess SSH errors to appropriate SSH exceptions.
|
24
|
+
|
25
|
+
:param e: The subprocess.CalledProcessError
|
26
|
+
:param remote_connection: The RemoteConnection object for context
|
27
|
+
:raises: SSHException, AuthenticationException, or NoValidConnectionsError
|
28
|
+
"""
|
29
|
+
stderr = e.stderr.lower() if e.stderr else ""
|
30
|
+
|
31
|
+
# Check for authentication failures
|
32
|
+
if any(
|
33
|
+
auth_err in stderr
|
34
|
+
for auth_err in [
|
35
|
+
"permission denied",
|
36
|
+
"authentication failed",
|
37
|
+
"publickey",
|
38
|
+
"password",
|
39
|
+
"host key verification failed",
|
40
|
+
]
|
41
|
+
):
|
42
|
+
raise AuthenticationException(f"SSH authentication failed: {e.stderr}")
|
43
|
+
|
44
|
+
# Check for connection failures
|
45
|
+
elif any(
|
46
|
+
conn_err in stderr
|
47
|
+
for conn_err in [
|
48
|
+
"connection refused",
|
49
|
+
"network is unreachable",
|
50
|
+
"no route to host",
|
51
|
+
"name or service not known",
|
52
|
+
"connection timed out",
|
53
|
+
]
|
54
|
+
):
|
55
|
+
raise NoValidConnectionsError(f"SSH connection failed: {e.stderr}")
|
56
|
+
|
57
|
+
# Check for general SSH protocol errors
|
58
|
+
elif "ssh:" in stderr or "protocol" in stderr:
|
59
|
+
raise SSHException(f"SSH protocol error: {e.stderr}")
|
60
|
+
|
61
|
+
# Default to generic SSH exception
|
62
|
+
else:
|
63
|
+
raise SSHException(f"SSH command failed: {e.stderr}")
|
64
|
+
|
12
65
|
|
13
66
|
MINIMUM_SQLITE_VERSION = "3.38.0"
|
14
67
|
|
15
68
|
|
69
|
+
def _execute_ssh_command(remote_connection: RemoteConnection, command: str) -> str:
|
70
|
+
"""Execute an SSH command and return the output."""
|
71
|
+
ssh_cmd = ["ssh", "-o", "PasswordAuthentication=no"]
|
72
|
+
|
73
|
+
# Handle non-standard SSH port
|
74
|
+
if remote_connection.port != 22:
|
75
|
+
ssh_cmd.extend(["-p", str(remote_connection.port)])
|
76
|
+
|
77
|
+
ssh_cmd.extend([f"{remote_connection.username}@{remote_connection.host}", command])
|
78
|
+
|
79
|
+
try:
|
80
|
+
result = subprocess.run(
|
81
|
+
ssh_cmd, capture_output=True, text=True, check=True, timeout=30
|
82
|
+
)
|
83
|
+
return result.stdout
|
84
|
+
except subprocess.CalledProcessError as e:
|
85
|
+
if e.returncode == 255: # SSH protocol errors
|
86
|
+
handle_ssh_subprocess_error(e, remote_connection)
|
87
|
+
# This line should never be reached as handle_ssh_subprocess_error raises an exception
|
88
|
+
raise RemoteSqliteException(
|
89
|
+
message=f"SSH command failed: {e.stderr}",
|
90
|
+
status=ConnectionTestStates.FAILED,
|
91
|
+
)
|
92
|
+
else:
|
93
|
+
raise RemoteSqliteException(
|
94
|
+
message=f"SSH command failed: {e.stderr}",
|
95
|
+
status=ConnectionTestStates.FAILED,
|
96
|
+
)
|
97
|
+
except subprocess.TimeoutExpired:
|
98
|
+
raise RemoteSqliteException(
|
99
|
+
message=f"SSH command timed out: {command}",
|
100
|
+
status=ConnectionTestStates.FAILED,
|
101
|
+
)
|
102
|
+
|
103
|
+
|
16
104
|
def find_sqlite_binary(connection):
|
17
105
|
"""Check if SQLite is installed on the remote machine and return its path."""
|
18
|
-
ssh_client = get_client(connection)
|
19
106
|
try:
|
20
|
-
|
21
|
-
binary_path =
|
22
|
-
error = stderr.read().decode().strip()
|
107
|
+
output = _execute_ssh_command(connection, "which sqlite3")
|
108
|
+
binary_path = output.strip()
|
23
109
|
if binary_path:
|
24
110
|
print(f"SQLite binary found at: {binary_path}")
|
25
111
|
return binary_path
|
26
|
-
elif error:
|
27
|
-
print(f"Error checking SQLite binary: {error}")
|
28
112
|
return None
|
113
|
+
except RemoteSqliteException:
|
114
|
+
# Re-raise RemoteSqliteException as-is
|
115
|
+
raise
|
29
116
|
except Exception as e:
|
30
117
|
raise RemoteSqliteException(
|
31
118
|
message=f"Error finding SQLite binary: {str(e)}",
|
@@ -33,17 +120,13 @@ def find_sqlite_binary(connection):
|
|
33
120
|
)
|
34
121
|
|
35
122
|
|
36
|
-
def is_sqlite_executable(
|
123
|
+
def is_sqlite_executable(remote_connection: RemoteConnection, binary_path):
|
37
124
|
"""Check if the SQLite binary is executable by trying to run it."""
|
38
125
|
try:
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
if error:
|
44
|
-
raise Exception(f"Error while trying to run SQLite binary: {error}")
|
45
|
-
|
46
|
-
version = get_sqlite_version(output)
|
126
|
+
output = _execute_ssh_command(remote_connection, f"{binary_path} --version")
|
127
|
+
version_output = output.strip()
|
128
|
+
|
129
|
+
version = get_sqlite_version(version_output)
|
47
130
|
if not is_version_at_least(version, MINIMUM_SQLITE_VERSION):
|
48
131
|
raise Exception(
|
49
132
|
f"SQLite version {version} is below the required minimum of {MINIMUM_SQLITE_VERSION}."
|
@@ -52,6 +135,9 @@ def is_sqlite_executable(ssh_client, binary_path):
|
|
52
135
|
print(f"SQLite binary at {binary_path} is executable. Version: {version}")
|
53
136
|
return True
|
54
137
|
|
138
|
+
except RemoteSqliteException:
|
139
|
+
# Re-raise RemoteSqliteException as-is
|
140
|
+
raise
|
55
141
|
except Exception as e:
|
56
142
|
raise Exception(f"Error checking SQLite executability: {str(e)}")
|
57
143
|
|
@@ -76,8 +162,7 @@ def is_version_at_least(version, minimum_version):
|
|
76
162
|
@remote_exception_handler
|
77
163
|
def check_sqlite_path(remote_connection: RemoteConnection):
|
78
164
|
try:
|
79
|
-
|
80
|
-
is_sqlite_executable(client, remote_connection.sqliteBinaryPath)
|
165
|
+
is_sqlite_executable(remote_connection, remote_connection.sqliteBinaryPath)
|
81
166
|
except Exception as e:
|
82
167
|
raise RemoteSqliteException(message=str(e), status=ConnectionTestStates.FAILED)
|
83
168
|
|
ttnn_visualizer/requirements.txt
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
Flask==3.1.1
|
2
2
|
gunicorn~=22.0.0
|
3
3
|
uvicorn==0.30.1
|
4
|
-
paramiko~=3.4.0
|
5
4
|
flask_cors==4.0.1
|
6
5
|
pydantic==2.7.3
|
7
6
|
pydantic_core==2.18.4
|
@@ -16,9 +15,9 @@ wheel
|
|
16
15
|
build
|
17
16
|
PyYAML==6.0.2
|
18
17
|
python-dotenv==1.0.1
|
19
|
-
tt-perf-report==1.0.
|
18
|
+
tt-perf-report==1.0.7
|
20
19
|
zstd==1.5.7.0
|
21
20
|
|
22
21
|
# Dev dependencies
|
23
22
|
mypy
|
24
|
-
|
23
|
+
black==25.1.0
|
ttnn_visualizer/serializers.py
CHANGED
ttnn_visualizer/settings.py
CHANGED
@@ -10,6 +10,7 @@ from ttnn_visualizer.utils import str_to_bool
|
|
10
10
|
|
11
11
|
load_dotenv()
|
12
12
|
|
13
|
+
|
13
14
|
class DefaultConfig(object):
|
14
15
|
# General Settings
|
15
16
|
SECRET_KEY = os.getenv("SECRET_KEY", "90909")
|
@@ -19,17 +20,20 @@ class DefaultConfig(object):
|
|
19
20
|
SERVER_MODE = str_to_bool(os.getenv("SERVER_MODE", "false"))
|
20
21
|
MALWARE_SCANNER = os.getenv("MALWARE_SCANNER")
|
21
22
|
ALLOWED_ORIGINS = [
|
22
|
-
o
|
23
|
+
o
|
24
|
+
for o in os.getenv(
|
25
|
+
"ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:8000"
|
26
|
+
).split(",")
|
23
27
|
if o
|
24
28
|
]
|
25
29
|
BASE_PATH = os.getenv("BASE_PATH", "/")
|
26
|
-
MAX_CONTENT_LENGTH = (
|
27
|
-
None if not (v := os.getenv("MAX_CONTENT_LENGTH")) else int(v)
|
28
|
-
)
|
30
|
+
MAX_CONTENT_LENGTH = None if not (v := os.getenv("MAX_CONTENT_LENGTH")) else int(v)
|
29
31
|
|
30
32
|
# Path Settings
|
31
33
|
DB_VERSION = "0.29.0" # App version when DB schema last changed
|
32
|
-
REPORT_DATA_DIRECTORY = os.getenv(
|
34
|
+
REPORT_DATA_DIRECTORY = os.getenv(
|
35
|
+
"REPORT_DATA_DIRECTORY", Path(__file__).parent.absolute().joinpath("data")
|
36
|
+
)
|
33
37
|
LOCAL_DATA_DIRECTORY = Path(REPORT_DATA_DIRECTORY).joinpath("local")
|
34
38
|
REMOTE_DATA_DIRECTORY = Path(REPORT_DATA_DIRECTORY).joinpath("remote")
|
35
39
|
PROFILER_DIRECTORY_NAME = "profiler-reports"
|