ttnn-visualizer 0.41.0__py3-none-any.whl → 0.42.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.
@@ -4,6 +4,7 @@
4
4
  import csv
5
5
  import json
6
6
  import os
7
+ import subprocess
7
8
  import tempfile
8
9
  from io import StringIO
9
10
  from pathlib import Path
@@ -13,10 +14,50 @@ import pandas as pd
13
14
  from tt_perf_report import perf_report
14
15
 
15
16
  from ttnn_visualizer.exceptions import DataFormatError
17
+ from ttnn_visualizer.models import Instance, RemoteConnection
18
+ from ttnn_visualizer.exceptions import SSHException, AuthenticationException, NoValidConnectionsError
16
19
  from ttnn_visualizer.models import Instance
17
- from ttnn_visualizer.ssh_client import get_client
18
20
  from ttnn_visualizer.sftp_operations import read_remote_file
19
21
 
22
+
23
+ def handle_ssh_subprocess_error(e: subprocess.CalledProcessError, remote_connection: RemoteConnection):
24
+ """
25
+ Convert subprocess SSH errors to appropriate SSH exceptions.
26
+
27
+ :param e: The subprocess.CalledProcessError
28
+ :param remote_connection: The RemoteConnection object for context
29
+ :raises: SSHException, AuthenticationException, or NoValidConnectionsError
30
+ """
31
+ stderr = e.stderr.lower() if e.stderr else ""
32
+
33
+ # Check for authentication failures
34
+ if any(auth_err in stderr for auth_err in [
35
+ "permission denied",
36
+ "authentication failed",
37
+ "publickey",
38
+ "password",
39
+ "host key verification failed"
40
+ ]):
41
+ raise AuthenticationException(f"SSH authentication failed: {e.stderr}")
42
+
43
+ # Check for connection failures
44
+ elif any(conn_err in stderr for conn_err in [
45
+ "connection refused",
46
+ "network is unreachable",
47
+ "no route to host",
48
+ "name or service not known",
49
+ "connection timed out"
50
+ ]):
51
+ raise NoValidConnectionsError(f"SSH connection failed: {e.stderr}")
52
+
53
+ # Check for general SSH protocol errors
54
+ elif "ssh:" in stderr or "protocol" in stderr:
55
+ raise SSHException(f"SSH protocol error: {e.stderr}")
56
+
57
+ # Default to generic SSH exception
58
+ else:
59
+ raise SSHException(f"SSH command failed: {e.stderr}")
60
+
20
61
  class LocalCSVQueryRunner:
21
62
  def __init__(self, file_path: str, offset: int = 0):
22
63
  self.file_path = file_path
@@ -110,7 +151,38 @@ class RemoteCSVQueryRunner:
110
151
  self.remote_connection = remote_connection
111
152
  self.sep = sep
112
153
  self.offset = offset
113
- self.ssh_client = get_client(remote_connection)
154
+
155
+ def _execute_ssh_command(self, command: str) -> str:
156
+ """Execute an SSH command and return the output."""
157
+ ssh_cmd = ["ssh"]
158
+
159
+ # Handle non-standard SSH port
160
+ if self.remote_connection.port != 22:
161
+ ssh_cmd.extend(["-p", str(self.remote_connection.port)])
162
+
163
+ ssh_cmd.extend([
164
+ f"{self.remote_connection.username}@{self.remote_connection.host}",
165
+ command
166
+ ])
167
+
168
+ try:
169
+ result = subprocess.run(
170
+ ssh_cmd,
171
+ capture_output=True,
172
+ text=True,
173
+ check=True,
174
+ timeout=30
175
+ )
176
+ return result.stdout
177
+ except subprocess.CalledProcessError as e:
178
+ if e.returncode == 255: # SSH protocol errors
179
+ handle_ssh_subprocess_error(e, self.remote_connection)
180
+ # This line should never be reached as handle_ssh_subprocess_error raises an exception
181
+ raise RuntimeError(f"SSH command failed: {e.stderr}")
182
+ else:
183
+ raise RuntimeError(f"SSH command failed: {e.stderr}")
184
+ except subprocess.TimeoutExpired:
185
+ raise RuntimeError(f"SSH command timed out: {command}")
114
186
 
115
187
  def execute_query(
116
188
  self,
@@ -128,12 +200,7 @@ class RemoteCSVQueryRunner:
128
200
  """
129
201
  # Fetch header row, accounting for the offset
130
202
  header_cmd = f"head -n {self.offset + 1} {self.file_path} | tail -n 1"
131
- stdin, stdout, stderr = self.ssh_client.exec_command(header_cmd)
132
- raw_header = stdout.read().decode("utf-8").strip()
133
- error = stderr.read().decode("utf-8").strip()
134
-
135
- if error:
136
- raise RuntimeError(f"Error fetching header row: {error}")
203
+ raw_header = self._execute_ssh_command(header_cmd).strip()
137
204
 
138
205
  # Sanitize headers
139
206
  headers = [
@@ -160,12 +227,7 @@ class RemoteCSVQueryRunner:
160
227
  limit_clause = f"| head -n {limit}" if limit else ""
161
228
  awk_cmd = f"awk -F'{self.sep}' 'NR > {self.offset + 1} {f'&& {awk_filter}' if awk_filter else ''} {{print}}' {self.file_path} {limit_clause}"
162
229
 
163
- stdin, stdout, stderr = self.ssh_client.exec_command(awk_cmd)
164
- output = stdout.read().decode("utf-8").strip()
165
- error = stderr.read().decode("utf-8").strip()
166
-
167
- if error:
168
- raise RuntimeError(f"Error executing AWK command: {error}")
230
+ output = self._execute_ssh_command(awk_cmd).strip()
169
231
 
170
232
  # Split rows into lists of strings
171
233
  rows = [
@@ -205,12 +267,7 @@ class RemoteCSVQueryRunner:
205
267
  if total_lines
206
268
  else f"cat {self.file_path}"
207
269
  )
208
- stdin, stdout, stderr = self.ssh_client.exec_command(cmd)
209
- output = stdout.read().decode("utf-8").strip()
210
- error = stderr.read().decode("utf-8").strip()
211
-
212
- if error:
213
- raise RuntimeError(f"Error fetching raw rows: {error}")
270
+ output = self._execute_ssh_command(cmd).strip()
214
271
 
215
272
  return output.splitlines()[self.offset:]
216
273
 
@@ -220,12 +277,7 @@ class RemoteCSVQueryRunner:
220
277
  :return: Dictionary of headers.
221
278
  """
222
279
  header_cmd = f"head -n {self.offset + 1} {self.file_path} | tail -n 1"
223
- stdin, stdout, stderr = self.ssh_client.exec_command(header_cmd)
224
- header = stdout.read().decode("utf-8").strip()
225
- error = stderr.read().decode("utf-8").strip()
226
-
227
- if error:
228
- raise RuntimeError(f"Error reading CSV header: {error}")
280
+ header = self._execute_ssh_command(header_cmd).strip()
229
281
 
230
282
  # Trim spaces in header names
231
283
  column_names = [name.strip() for name in header.split(self.sep)]
@@ -254,10 +306,9 @@ class RemoteCSVQueryRunner:
254
306
 
255
307
  def __exit__(self, exc_type, exc_val, exc_tb):
256
308
  """
257
- Clean up the SSH connection when exiting context.
309
+ Clean up resources when exiting context.
258
310
  """
259
- if self.ssh_client:
260
- self.ssh_client.close()
311
+ pass
261
312
 
262
313
 
263
314
  class NPEQueries:
@@ -267,7 +318,6 @@ class NPEQueries:
267
318
  @staticmethod
268
319
  def get_npe_manifest(instance: Instance):
269
320
 
270
-
271
321
  if (
272
322
  not instance.remote_connection
273
323
  or instance.remote_connection
@@ -285,6 +335,31 @@ class NPEQueries:
285
335
  f"{profiler_folder.remotePath}/{NPEQueries.NPE_FOLDER}/{NPEQueries.MANIFEST_FILE}",
286
336
  )
287
337
 
338
+ @staticmethod
339
+ def get_npe_timeline(instance: Instance, filename: str):
340
+ if not filename:
341
+ raise ValueError("filename parameter is required and cannot be None or empty")
342
+
343
+ if (
344
+ not instance.remote_connection
345
+ or not instance.remote_connection.useRemoteQuerying
346
+ ):
347
+ if not instance.performance_path:
348
+ raise ValueError("instance.performance_path is None")
349
+
350
+ file_path = Path(
351
+ instance.performance_path, NPEQueries.NPE_FOLDER, filename
352
+ )
353
+ with open(file_path, "r") as f:
354
+ return json.load(f)
355
+ else:
356
+ profiler_folder = instance.remote_profile_folder
357
+ return read_remote_file(
358
+ instance.remote_connection,
359
+ f"{profiler_folder.remotePath}/{NPEQueries.NPE_FOLDER}/{filename}",
360
+ )
361
+
362
+
288
363
 
289
364
  class DeviceLogProfilerQueries:
290
365
  DEVICE_LOG_FILE = "profile_log_device.csv"
@@ -611,6 +686,7 @@ class OpsPerformanceReportQueries:
611
686
  "inner_dim_block_size",
612
687
  "output_subblock_h",
613
688
  "output_subblock_w",
689
+ "global_call_count",
614
690
  "advice",
615
691
  "raw_op_code"
616
692
  ]
@@ -9,13 +9,10 @@ from ttnn_visualizer.enums import ConnectionTestStates
9
9
 
10
10
  from functools import wraps
11
11
  from flask import abort, request, session
12
- from paramiko.ssh_exception import (
12
+ from ttnn_visualizer.exceptions import (
13
13
  AuthenticationException,
14
14
  NoValidConnectionsError,
15
15
  SSHException,
16
- )
17
-
18
- from ttnn_visualizer.exceptions import (
19
16
  RemoteConnectionException,
20
17
  NoProjectsException,
21
18
  RemoteSqliteException,
@@ -37,6 +34,12 @@ def with_instance(func):
37
34
  abort(404)
38
35
 
39
36
  instance_query_data = get_or_create_instance(instance_id=instance_id)
37
+
38
+ # Handle case where get_or_create_instance returns None due to database error
39
+ if instance_query_data is None:
40
+ current_app.logger.error(f"Failed to get or create instance with ID: {instance_id}")
41
+ abort(500)
42
+
40
43
  instance = instance_query_data.to_pydantic()
41
44
 
42
45
  kwargs["instance"] = instance
@@ -43,5 +43,21 @@ class DataFormatError(Exception):
43
43
  class InvalidReportPath(Exception):
44
44
  pass
45
45
 
46
+
46
47
  class InvalidProfilerPath(Exception):
47
48
  pass
49
+
50
+
51
+ class SSHException(Exception):
52
+ """Base SSH exception for subprocess SSH operations"""
53
+ pass
54
+
55
+
56
+ class AuthenticationException(SSHException):
57
+ """Raised when SSH authentication fails"""
58
+ pass
59
+
60
+
61
+ class NoValidConnectionsError(SSHException):
62
+ """Raised when SSH connection cannot be established"""
63
+ pass
@@ -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
- ssh_client = None
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
- self.query_runner = RemoteQueryRunner(instance=instance)
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, RemoteQueryRunner):
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,109 @@
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 RemoteSqliteException
10
+ from ttnn_visualizer.exceptions import RemoteSqliteException, SSHException, AuthenticationException, NoValidConnectionsError
10
11
  from ttnn_visualizer.models import RemoteConnection
11
- from ttnn_visualizer.ssh_client import get_client
12
+
13
+
14
+ def handle_ssh_subprocess_error(e: subprocess.CalledProcessError, remote_connection: RemoteConnection):
15
+ """
16
+ Convert subprocess SSH errors to appropriate SSH exceptions.
17
+
18
+ :param e: The subprocess.CalledProcessError
19
+ :param remote_connection: The RemoteConnection object for context
20
+ :raises: SSHException, AuthenticationException, or NoValidConnectionsError
21
+ """
22
+ stderr = e.stderr.lower() if e.stderr else ""
23
+
24
+ # Check for authentication failures
25
+ if any(auth_err in stderr for auth_err in [
26
+ "permission denied",
27
+ "authentication failed",
28
+ "publickey",
29
+ "password",
30
+ "host key verification failed"
31
+ ]):
32
+ raise AuthenticationException(f"SSH authentication failed: {e.stderr}")
33
+
34
+ # Check for connection failures
35
+ elif any(conn_err in stderr for conn_err in [
36
+ "connection refused",
37
+ "network is unreachable",
38
+ "no route to host",
39
+ "name or service not known",
40
+ "connection timed out"
41
+ ]):
42
+ raise NoValidConnectionsError(f"SSH connection failed: {e.stderr}")
43
+
44
+ # Check for general SSH protocol errors
45
+ elif "ssh:" in stderr or "protocol" in stderr:
46
+ raise SSHException(f"SSH protocol error: {e.stderr}")
47
+
48
+ # Default to generic SSH exception
49
+ else:
50
+ raise SSHException(f"SSH command failed: {e.stderr}")
12
51
 
13
52
  MINIMUM_SQLITE_VERSION = "3.38.0"
14
53
 
15
54
 
55
+ def _execute_ssh_command(remote_connection: RemoteConnection, command: str) -> str:
56
+ """Execute an SSH command and return the output."""
57
+ ssh_cmd = ["ssh"]
58
+
59
+ # Handle non-standard SSH port
60
+ if remote_connection.port != 22:
61
+ ssh_cmd.extend(["-p", str(remote_connection.port)])
62
+
63
+ ssh_cmd.extend([
64
+ f"{remote_connection.username}@{remote_connection.host}",
65
+ command
66
+ ])
67
+
68
+ try:
69
+ result = subprocess.run(
70
+ ssh_cmd,
71
+ capture_output=True,
72
+ text=True,
73
+ check=True,
74
+ timeout=30
75
+ )
76
+ return result.stdout
77
+ except subprocess.CalledProcessError as e:
78
+ if e.returncode == 255: # SSH protocol errors
79
+ handle_ssh_subprocess_error(e, remote_connection)
80
+ # This line should never be reached as handle_ssh_subprocess_error raises an exception
81
+ raise RemoteSqliteException(
82
+ message=f"SSH command failed: {e.stderr}",
83
+ status=ConnectionTestStates.FAILED,
84
+ )
85
+ else:
86
+ raise RemoteSqliteException(
87
+ message=f"SSH command failed: {e.stderr}",
88
+ status=ConnectionTestStates.FAILED,
89
+ )
90
+ except subprocess.TimeoutExpired:
91
+ raise RemoteSqliteException(
92
+ message=f"SSH command timed out: {command}",
93
+ status=ConnectionTestStates.FAILED,
94
+ )
95
+
96
+
16
97
  def find_sqlite_binary(connection):
17
98
  """Check if SQLite is installed on the remote machine and return its path."""
18
- ssh_client = get_client(connection)
19
99
  try:
20
- stdin, stdout, stderr = ssh_client.exec_command("which sqlite3")
21
- binary_path = stdout.read().decode().strip()
22
- error = stderr.read().decode().strip()
100
+ output = _execute_ssh_command(connection, "which sqlite3")
101
+ binary_path = output.strip()
23
102
  if binary_path:
24
103
  print(f"SQLite binary found at: {binary_path}")
25
104
  return binary_path
26
- elif error:
27
- print(f"Error checking SQLite binary: {error}")
28
105
  return None
106
+ except RemoteSqliteException:
107
+ # Re-raise RemoteSqliteException as-is
108
+ raise
29
109
  except Exception as e:
30
110
  raise RemoteSqliteException(
31
111
  message=f"Error finding SQLite binary: {str(e)}",
@@ -33,17 +113,13 @@ def find_sqlite_binary(connection):
33
113
  )
34
114
 
35
115
 
36
- def is_sqlite_executable(ssh_client, binary_path):
116
+ def is_sqlite_executable(remote_connection: RemoteConnection, binary_path):
37
117
  """Check if the SQLite binary is executable by trying to run it."""
38
118
  try:
39
- stdin, stdout, stderr = ssh_client.exec_command(f"{binary_path} --version")
40
- output = stdout.read().decode().strip()
41
- error = stderr.read().decode().strip()
42
- stdout.channel.recv_exit_status()
43
- if error:
44
- raise Exception(f"Error while trying to run SQLite binary: {error}")
45
-
46
- version = get_sqlite_version(output)
119
+ output = _execute_ssh_command(remote_connection, f"{binary_path} --version")
120
+ version_output = output.strip()
121
+
122
+ version = get_sqlite_version(version_output)
47
123
  if not is_version_at_least(version, MINIMUM_SQLITE_VERSION):
48
124
  raise Exception(
49
125
  f"SQLite version {version} is below the required minimum of {MINIMUM_SQLITE_VERSION}."
@@ -52,6 +128,9 @@ def is_sqlite_executable(ssh_client, binary_path):
52
128
  print(f"SQLite binary at {binary_path} is executable. Version: {version}")
53
129
  return True
54
130
 
131
+ except RemoteSqliteException:
132
+ # Re-raise RemoteSqliteException as-is
133
+ raise
55
134
  except Exception as e:
56
135
  raise Exception(f"Error checking SQLite executability: {str(e)}")
57
136
 
@@ -76,8 +155,7 @@ def is_version_at_least(version, minimum_version):
76
155
  @remote_exception_handler
77
156
  def check_sqlite_path(remote_connection: RemoteConnection):
78
157
  try:
79
- client = get_client(remote_connection)
80
- is_sqlite_executable(client, remote_connection.sqliteBinaryPath)
158
+ is_sqlite_executable(remote_connection, remote_connection.sqliteBinaryPath)
81
159
  except Exception as e:
82
160
  raise RemoteSqliteException(message=str(e), status=ConnectionTestStates.FAILED)
83
161
 
@@ -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,8 @@ wheel
16
15
  build
17
16
  PyYAML==6.0.2
18
17
  python-dotenv==1.0.1
19
- tt-perf-report==1.0.6
18
+ tt-perf-report==1.0.7
20
19
  zstd==1.5.7.0
21
20
 
22
21
  # Dev dependencies
23
22
  mypy
24
- types-paramiko