nsight-python 0.9.6__py3-none-any.whl → 1.0.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.
nsight/__init__.py CHANGED
@@ -5,8 +5,12 @@ from importlib.metadata import version
5
5
 
6
6
  from nsight import analyze
7
7
  from nsight.annotation import annotate
8
- from nsight.utils import col_panel, row_panel
8
+ from nsight.collection.ncu import try_init_injection
9
+ from nsight.utils import VerbosityLevel, col_panel, row_panel
10
+
11
+ # Load injection library and symbols for NCU attach (if available)
12
+ try_init_injection()
9
13
 
10
14
  __version__ = version("nsight-python")
11
15
 
12
- __all__ = ["analyze", "annotate"]
16
+ __all__ = ["analyze", "annotate", "VerbosityLevel"]
nsight/analyze.py CHANGED
@@ -7,7 +7,7 @@ import os
7
7
  import tempfile
8
8
  import warnings
9
9
  from collections.abc import Callable, Iterable, Sequence
10
- from typing import Any, Literal, overload
10
+ from typing import Any, Literal, Mapping, overload
11
11
 
12
12
  import matplotlib
13
13
  import matplotlib.figure
@@ -16,6 +16,7 @@ from numpy.typing import NDArray
16
16
 
17
17
  import nsight.collection as collection
18
18
  import nsight.visualization as visualization
19
+ from nsight.utils import VerbosityLevel
19
20
 
20
21
 
21
22
  # Overload 1: When used without parentheses: @kernel
@@ -32,9 +33,14 @@ def kernel(
32
33
  *,
33
34
  configs: Iterable[Any] | None = None,
34
35
  runs: int = 1,
35
- derive_metric: Callable[..., float | dict[str, float]] | None = None,
36
+ derive_metric: (
37
+ Callable[
38
+ ..., float | tuple[float, str] | Mapping[str, float | tuple[float, str]]
39
+ ]
40
+ | None
41
+ ) = None,
36
42
  normalize_against: str | None = None,
37
- output: Literal["quiet", "progress", "verbose"] = "progress",
43
+ verbosity: VerbosityLevel = VerbosityLevel.INFO,
38
44
  metrics: Sequence[str] = ["gpu__time_duration.sum"],
39
45
  ignore_kernel_list: Sequence[str] | None = None,
40
46
  clock_control: Literal["base", "none"] = "none",
@@ -44,6 +50,7 @@ def kernel(
44
50
  thermal_wait: int | None = None,
45
51
  thermal_cont: int | None = None,
46
52
  thermal_timeout: int | None = None,
53
+ thermal_device: int | None = None,
47
54
  combine_kernel_metrics: Callable[[float, float], float] | None = None,
48
55
  output_prefix: str | None = None,
49
56
  output_csv: bool = False,
@@ -56,9 +63,14 @@ def kernel(
56
63
  *,
57
64
  configs: Iterable[Any] | None = None,
58
65
  runs: int = 1,
59
- derive_metric: Callable[..., float | dict[str, float]] | None = None,
66
+ derive_metric: (
67
+ Callable[
68
+ ..., float | tuple[float, str] | Mapping[str, float | tuple[float, str]]
69
+ ]
70
+ | None
71
+ ) = None,
60
72
  normalize_against: str | None = None,
61
- output: Literal["quiet", "progress", "verbose"] = "progress",
73
+ verbosity: VerbosityLevel = VerbosityLevel.INFO,
62
74
  metrics: Sequence[str] = ["gpu__time_duration.sum"],
63
75
  ignore_kernel_list: Sequence[str] | None = None,
64
76
  clock_control: Literal["base", "none"] = "none",
@@ -68,6 +80,7 @@ def kernel(
68
80
  thermal_wait: int | None = None,
69
81
  thermal_cont: int | None = None,
70
82
  thermal_timeout: int | None = None,
83
+ thermal_device: int | None = None,
71
84
  combine_kernel_metrics: Callable[[float, float], float] | None = None,
72
85
  output_prefix: str | None = None,
73
86
  output_csv: bool = False,
@@ -93,6 +106,13 @@ def kernel(
93
106
  - ``**kwargs``: Original function keyword arguments
94
107
  - Returns ``ProfileResults`` object containing profiling data
95
108
 
109
+ .. note::
110
+ Configs are passed to the decorated function as positional arguments.
111
+ Both positional and keyword-only parameters are supported in the function
112
+ signature. If the function has ``*args`` or ``**kwargs``, they are tolerated
113
+ but will always be empty during profiling. Parameters with default values
114
+ may be omitted from configs — missing values will be filled from the defaults.
115
+
96
116
 
97
117
  Parameters:
98
118
  configs: An iterable of configurations to run the function with. Each configuration can be either:
@@ -105,6 +125,10 @@ def kernel(
105
125
 
106
126
  configs = [1, 2, 3, 4]
107
127
 
128
+ If configs provide fewer values than the function has parameters,
129
+ the remaining parameters must have default values, which will be used
130
+ automatically.
131
+
108
132
  If the configs are not provided at decoration time, they must be provided when calling the decorated function.
109
133
  runs: Number of times each configuration should be executed.
110
134
  derive_metric:
@@ -115,13 +139,18 @@ def kernel(
115
139
  metrics. Return value can be either:
116
140
 
117
141
  - A single value (float/int): Will be added as a new metric with the function name as the metric name. For lambda functions, the metric name will be ``"<lambda>"``.
118
- - A dictionary: Keys will be used as metric names, values as metric values.
142
+ - A tuple (value, unit): A single metric value with its unit specified. The value will be added with the function name as the metric name, and the unit will be stored in the ``Unit`` column of the dataframe. Example: ``(tflops_value, "TFLOPS")``.
143
+ - A dictionary: Keys will be used as metric names, values can be either:
144
+ - Scalar values (float/int): Metric values without units. ``np.nan`` will be added to the ``Unit`` column and a warning will be issued.
145
+ - Tuples (value, unit): Metric values with units specified. The unit will be stored in the ``Unit`` column of the dataframe. Example: ``{"TFLOPS": (tflops_value, "TFLOPS"), "ArithIntensity": (intensity_value, "FLOP/byte")}``.
119
146
 
120
147
  The parameter order requirements for the custom function:
121
148
 
122
149
  - First several arguments: Must exactly match the order of metrics declared in the @kernel decorator. These arguments will receive the actual measured values of those metrics.
123
150
  - Remaining arguments: Must exactly match the signature of the decorated function. In other words, the original function's parameters are passed in order.
124
151
 
152
+ **Note on units**: When units are not provided (returning scalar values instead of tuples), ``np.nan`` will be added to the ``Unit`` column in the dataframe and a warning will be issued. To avoid warnings and ensure proper unit tracking, return tuples ``(value, unit)`` for all derived metrics.
153
+
125
154
  See the examples for concrete use cases.
126
155
  normalize_against:
127
156
  Annotation name to normalize metrics against (computes current / baseline).
@@ -193,11 +222,22 @@ def kernel(
193
222
  thermal_timeout: Maximum wait time in seconds for GPU to cool down.
194
223
  Default: 180 seconds
195
224
 
196
- output: Controls the verbosity level of the output.
225
+ thermal_device: CUDA device ordinal to monitor for thermal throttling.
226
+
227
+ If set, monitor that CUDA device. If ``None``, monitor the current
228
+ CUDA device context. Device numbering follows CUDA runtime ordinals and
229
+ honors ``CUDA_VISIBLE_DEVICES``.
230
+
231
+ Default: ``None``
197
232
 
198
- - ``"quiet"``: Suppresses all output.
199
- - ``"progress"``: Shows a progress bar along with details about profiling and data extraction progress.
200
- - ``"verbose"``: Displays the progress bar, configuration-specific logs, and profiler logs.
233
+ verbosity: Controls the verbosity level of the output. Selecting level N
234
+ enables output at all levels with value <= N. Default: ``VerbosityLevel.INFO``.
235
+
236
+ - ``VerbosityLevel.SILENT``: Suppresses all output.
237
+ - ``VerbosityLevel.INFO``: Shows a progress bar, profiling completion messages,
238
+ and report file paths.
239
+ - ``VerbosityLevel.DEBUG``: Adds configuration-specific logs, the full NCU CLI
240
+ command, and enables NCU verbose output.
201
241
 
202
242
  output_prefix: When specified, all intermediate profiler files are created with this prefix.
203
243
  For example, if `output_prefix="/home/user/run1_"`, the profiler will generate:
@@ -221,6 +261,7 @@ def kernel(
221
261
  - ``Annotation``: Name of the annotated region being profiled
222
262
  - ``Value``: Raw metric values collected by the profiler
223
263
  - ``Metric``: The metrics being collected (e.g., ``gpu__time_duration.sum``) and the metrics being derived
264
+ - ``Unit``: Unit of measurement for the metric value (e.g., ``ns`` for nanoseconds)
224
265
  - ``Kernel``: Name of the GPU kernel(s) launched
225
266
  - ``GPU``: GPU device name
226
267
  - ``Host``: Host machine name
@@ -242,6 +283,7 @@ def kernel(
242
283
  - ``RelativeStdDevPct``: Standard deviation as a percentage of the mean
243
284
  - ``StableMeasurement``: Boolean indicating if the measurement is stable (low variance). The measurement is stable if ``RelativeStdDevPct`` < 2 % .
244
285
  - ``Metric``: The metrics being collected and the metrics being derived
286
+ - ``Unit``: Unit of measurement for the metric value (e.g., ``ns`` for nanoseconds)
245
287
  - ``Kernel``: Name of the GPU kernel(s) launched
246
288
  - ``GPU``: GPU device name
247
289
  - ``Host``: Host machine name
@@ -263,34 +305,25 @@ def kernel(
263
305
 
264
306
  def _create_profiler() -> collection.core.NsightProfiler:
265
307
  """Helper to create the profiler with the given settings."""
266
- if output not in ("quiet", "progress", "verbose"):
267
- raise ValueError("output must be 'quiet', 'progress' or 'verbose'")
268
-
269
- output_progress = output == "progress" or output == "verbose"
270
- output_detailed = output == "verbose"
271
-
272
308
  # Create the output paths needed for the ncu report, ncu logs and the CSVs
273
309
  prefix = output_prefix
274
- if "NSPY_NCU_PROFILE" not in os.environ:
275
- if prefix is None:
276
- prefix = tempfile.mkdtemp(prefix="nspy_")
277
- prefix = os.path.join(
278
- prefix, ""
279
- ) # Adds a trailing forward/backward slash
280
- else:
281
- os.makedirs(os.path.dirname(prefix) or ".", exist_ok=True)
310
+ if prefix is None:
311
+ prefix = tempfile.mkdtemp(prefix="nspy_")
312
+ prefix = os.path.join(prefix, "") # Adds a trailing forward/backward slash
313
+ else:
314
+ os.makedirs(os.path.dirname(prefix) or ".", exist_ok=True)
282
315
 
283
316
  settings = collection.core.ProfileSettings(
284
317
  configs=configs,
285
318
  runs=runs,
286
- output_progress=output_progress,
287
- output_detailed=output_detailed,
319
+ verbosity=verbosity,
288
320
  derive_metric=derive_metric,
289
321
  normalize_against=normalize_against,
290
322
  thermal_mode=thermal_mode,
291
323
  thermal_wait=thermal_wait,
292
324
  thermal_cont=thermal_cont,
293
325
  thermal_timeout=thermal_timeout,
326
+ thermal_device=thermal_device,
294
327
  output_prefix=prefix,
295
328
  output_csv=output_csv,
296
329
  )
@@ -451,30 +484,29 @@ def plot(
451
484
  def wrapper(*args: Any, **kwargs: Any) -> Any:
452
485
  result = func(*args, **kwargs)
453
486
 
454
- if "NSPY_NCU_PROFILE" not in os.environ:
455
- # Legalize the user-provided metric for plotting
456
- legalized_metric = _legalize_metric(result, metric)
457
-
458
- visualization.visualize(
459
- result.to_dataframe(),
460
- metric=legalized_metric,
461
- row_panels=row_panels,
462
- col_panels=col_panels,
463
- x_keys=x_keys,
464
- print_data=print_data,
465
- title=title,
466
- filename=filename,
467
- ylabel=ylabel or "",
468
- annotate_points=annotate_points,
469
- show_avg=show_avg,
470
- show_geomean=show_geomean,
471
- plot_type=plot_type,
472
- plot_width=plot_width,
473
- plot_height=plot_height,
474
- variant_fields=variant_fields,
475
- variant_annotations=variant_annotations,
476
- plot_callback=plot_callback,
477
- )
487
+ # Legalize the user-provided metric for plotting
488
+ legalized_metric = _legalize_metric(result, metric)
489
+
490
+ visualization.visualize(
491
+ result.to_dataframe(),
492
+ metric=legalized_metric,
493
+ row_panels=row_panels,
494
+ col_panels=col_panels,
495
+ x_keys=x_keys,
496
+ print_data=print_data,
497
+ title=title,
498
+ filename=filename,
499
+ ylabel=ylabel or "",
500
+ annotate_points=annotate_points,
501
+ show_avg=show_avg,
502
+ show_geomean=show_geomean,
503
+ plot_type=plot_type,
504
+ plot_width=plot_width,
505
+ plot_height=plot_height,
506
+ variant_fields=variant_fields,
507
+ variant_annotations=variant_annotations,
508
+ plot_callback=plot_callback,
509
+ )
478
510
  return result
479
511
 
480
512
  return wrapper
nsight/annotation.py CHANGED
@@ -129,6 +129,10 @@ class annotate(nvtx.annotate): # type: ignore[misc]
129
129
  Nested annotations are currently not supported. However, since each
130
130
  annotation is expected to contain a single kernel launch by default,
131
131
  nested annotations should not be necessary in typical usage scenarios.
132
+ Entering and exiting the annotation context pushes and pops an NVTX
133
+ range, so calling ``nvtx.pop_range()`` inside the annotation context
134
+ without a corresponding push would pop the annotation context's own
135
+ range instead, resulting in incorrect profiling results.
132
136
 
133
137
  """
134
138
 
nsight/collection/core.py CHANGED
@@ -11,12 +11,99 @@ import threading
11
11
  import time
12
12
  import warnings
13
13
  from collections.abc import Callable, Collection, Iterable, Sequence
14
- from typing import Any, List, Literal
14
+ from typing import Any, List, Literal, Mapping
15
15
 
16
16
  import numpy as np
17
17
  import pandas as pd
18
18
 
19
19
  from nsight import annotation, exceptions, thermovision, transformation, utils
20
+ from nsight.utils import VerbosityLevel
21
+
22
+
23
+ def _get_regular_params(
24
+ sig: inspect.Signature,
25
+ ) -> list[inspect.Parameter]:
26
+ """Return the list of regular (non-variadic) parameters from a signature."""
27
+ return [
28
+ p
29
+ for p in sig.parameters.values()
30
+ if p.kind
31
+ not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
32
+ ]
33
+
34
+
35
+ def _count_params(
36
+ sig: inspect.Signature,
37
+ ) -> tuple[int, int]:
38
+ """Count (required, total) regular parameters in a signature.
39
+
40
+ Returns:
41
+ A (required_count, total_count) tuple. required_count excludes
42
+ parameters with defaults, total_count includes them.
43
+ """
44
+ params = _get_regular_params(sig)
45
+ required = sum(1 for p in params if p.default is inspect.Parameter.empty)
46
+ return required, len(params)
47
+
48
+
49
+ def _pad_config_with_defaults(
50
+ sig: inspect.Signature, config: Sequence[Any]
51
+ ) -> tuple[Any, ...]:
52
+ """Pad a config tuple with default values for any missing trailing parameters.
53
+
54
+ If the config provides fewer values than the function has parameters, the
55
+ remaining parameters must have defaults, which are appended to the config.
56
+ This ensures the rest of the pipeline always sees full-length configs.
57
+
58
+ Args:
59
+ sig: The function's inspect.Signature.
60
+ config: The (possibly short) config tuple.
61
+
62
+ Returns:
63
+ A full-length config tuple with defaults filled in.
64
+ """
65
+ params = _get_regular_params(sig)
66
+ if len(config) == len(params):
67
+ return tuple(config)
68
+ padded = list(config)
69
+ for param in params[len(config) :]:
70
+ padded.append(param.default)
71
+ return tuple(padded)
72
+
73
+
74
+ def _bind_config_to_signature(
75
+ sig: inspect.Signature, config: Sequence[Any]
76
+ ) -> tuple[list[Any], dict[str, Any]]:
77
+ """Split a config tuple into positional args and keyword-only kwargs.
78
+
79
+ Maps config values to function parameters in declaration order, separating
80
+ them into positional arguments (for POSITIONAL_OR_KEYWORD params) and
81
+ keyword arguments (for KEYWORD_ONLY params). VAR_POSITIONAL and VAR_KEYWORD
82
+ parameters are skipped. Parameters with defaults that are not covered by
83
+ the config are left for the function to fill in.
84
+
85
+ Args:
86
+ sig: The function's inspect.Signature.
87
+ config: The config tuple to bind.
88
+
89
+ Returns:
90
+ A (positional_args, keyword_args) tuple ready for func(*pos, **kw).
91
+ """
92
+ positional: list[Any] = []
93
+ keyword: dict[str, Any] = {}
94
+ config_iter = iter(config)
95
+ for param in sig.parameters.values():
96
+ if param.kind in (
97
+ inspect.Parameter.VAR_POSITIONAL,
98
+ inspect.Parameter.VAR_KEYWORD,
99
+ ):
100
+ continue
101
+ val = next(config_iter)
102
+ if param.kind == inspect.Parameter.KEYWORD_ONLY:
103
+ keyword[param.name] = val
104
+ else:
105
+ positional.append(val)
106
+ return positional, keyword
20
107
 
21
108
 
22
109
  def _sanitize_configs(
@@ -60,6 +147,10 @@ def _sanitize_configs(
60
147
  - For functions with no parameters, an empty config [()] is created automatically.
61
148
  - The function combines `args` and `kwargs` into a single list if `args` are provided.
62
149
  - The function assumes that `kwargs` keys are in the expected order when combining.
150
+ - Config values are mapped to parameters in declaration order. Both
151
+ regular and keyword-only parameters are supported. Parameters with
152
+ defaults may be omitted from configs. ``*args`` and ``**kwargs``
153
+ are tolerated but ignored.
63
154
  """
64
155
  if len(args) > 0:
65
156
  # We do not expect any configs in this case
@@ -76,8 +167,8 @@ def _sanitize_configs(
76
167
  if decorator_configs is None:
77
168
  # Check if function takes no arguments
78
169
  sig = inspect.signature(func)
79
- expected_arg_count = len(sig.parameters)
80
- if expected_arg_count == 0:
170
+ required_count, total_count = _count_params(sig)
171
+ if required_count == 0:
81
172
  # For functions with no arguments, create a single empty config
82
173
  # This allows calling the function without requiring explicit configs
83
174
  configs = [()]
@@ -105,8 +196,8 @@ def _sanitize_configs(
105
196
 
106
197
  # If function takes exactly one argument, allow scalar configs
107
198
  sig = inspect.signature(func)
108
- expected_arg_count = len(sig.parameters)
109
- if expected_arg_count == 1:
199
+ required_count, total_count = _count_params(sig)
200
+ if total_count == 1:
110
201
  normalized_configs: list[Sequence[Any]] = []
111
202
  for config in configs:
112
203
  if utils.is_scalar(config):
@@ -122,11 +213,21 @@ def _sanitize_configs(
122
213
  )
123
214
  first_config_arg_count = config_lengths[0]
124
215
 
125
- # Validate that the number of args matches the number of function parameters
126
- if first_config_arg_count != expected_arg_count:
127
- raise exceptions.ProfilerException(
128
- f"Configs have {first_config_arg_count} arguments, but function expects {expected_arg_count}"
129
- )
216
+ # Validate that the number of args is between required and total parameters
217
+ if not (required_count <= first_config_arg_count <= total_count):
218
+ if required_count == total_count:
219
+ raise exceptions.ProfilerException(
220
+ f"Configs have {first_config_arg_count} arguments, but function expects {total_count}"
221
+ )
222
+ else:
223
+ raise exceptions.ProfilerException(
224
+ f"Configs have {first_config_arg_count} arguments, but function expects "
225
+ f"between {required_count} and {total_count}"
226
+ )
227
+
228
+ # Pad configs with default values for any missing trailing parameters
229
+ if first_config_arg_count < total_count:
230
+ configs = [_pad_config_with_defaults(sig, config) for config in configs]
130
231
 
131
232
  return configs # type: ignore[return-value]
132
233
 
@@ -135,15 +236,15 @@ def run_profile_session(
135
236
  func: Callable[..., None],
136
237
  configs: List[Sequence[Any]],
137
238
  runs: int,
138
- output_progress: bool,
139
- output_detailed: bool,
239
+ verbosity: VerbosityLevel,
140
240
  thermal_mode: Literal["auto", "manual", "off"],
141
241
  thermal_wait: int | None = None,
142
242
  thermal_cont: int | None = None,
143
243
  thermal_timeout: int | None = None,
244
+ thermal_device: int | None = None,
144
245
  ) -> None:
145
246
 
146
- if output_progress:
247
+ if verbosity >= VerbosityLevel.INFO:
147
248
  print("")
148
249
  print("")
149
250
 
@@ -155,7 +256,8 @@ def run_profile_session(
155
256
  thermal_wait=thermal_wait,
156
257
  thermal_cont=thermal_cont,
157
258
  thermal_timeout=thermal_timeout,
158
- verbose=output_detailed,
259
+ thermal_device=thermal_device,
260
+ verbose=verbosity >= VerbosityLevel.DEBUG,
159
261
  )
160
262
  thermovision_initialized = thermal_controller.init()
161
263
  if not thermovision_initialized:
@@ -170,21 +272,22 @@ def run_profile_session(
170
272
  bar_length = 100
171
273
  progress_time: float = 0
172
274
 
173
- # overwrite flag: we do not overwrite when output mode is detailed
174
- overwrite_output = not output_detailed
275
+ # overwrite flag: we do not overwrite when output mode is debug
276
+ overwrite_output = verbosity < VerbosityLevel.DEBUG
175
277
  show_return_type_warning = False
176
278
  config_lengths: list[int] = list()
177
279
 
178
280
  for c in configs:
179
- expected_arg_count = len(inspect.signature(func).parameters)
281
+ sig = inspect.signature(func)
282
+ required_count, total_count = _count_params(sig)
180
283
 
181
284
  # Handle scalar values
182
- if expected_arg_count == 1:
285
+ if total_count == 1:
183
286
  if utils.is_scalar(c):
184
287
  c = (c,)
185
288
 
186
289
  # Check if func supports the input configs
187
- if expected_arg_count != len(c):
290
+ if not (required_count <= len(c) <= total_count):
188
291
  raise exceptions.ProfilerException(
189
292
  f"Function '{func.__name__}' does not support the input configuration"
190
293
  )
@@ -199,7 +302,7 @@ def run_profile_session(
199
302
 
200
303
  curr_config += 1
201
304
 
202
- if output_progress:
305
+ if verbosity >= VerbosityLevel.INFO:
203
306
  utils.print_config(total_configs, curr_config, c, overwrite_output)
204
307
 
205
308
  for i in range(runs):
@@ -211,8 +314,10 @@ def run_profile_session(
211
314
  # Clear active annotations before each run
212
315
  annotation.clear_active_annotations()
213
316
 
214
- # Run the function with the config
215
- result = func(*c) # type: ignore[func-returns-value]
317
+ # Run the function with the config, splitting into positional
318
+ # and keyword-only args based on the function signature
319
+ pos_args, kw_args = _bind_config_to_signature(sig, c)
320
+ result = func(*pos_args, **kw_args) # type: ignore[func-returns-value]
216
321
  if result is not None:
217
322
  show_return_type_warning = True
218
323
 
@@ -225,7 +330,7 @@ def run_profile_session(
225
330
 
226
331
  # Update time estimates every half second
227
332
  if time.time() - progress_time > 0.5:
228
- if output_progress:
333
+ if verbosity >= VerbosityLevel.INFO:
229
334
  utils.print_progress_bar(
230
335
  total_runs,
231
336
  curr_run,
@@ -236,7 +341,7 @@ def run_profile_session(
236
341
  progress_time = time.time()
237
342
 
238
343
  # Update progress bar at end so it shows 100%
239
- if output_progress:
344
+ if verbosity >= VerbosityLevel.INFO:
240
345
  utils.print_progress_bar(
241
346
  total_runs,
242
347
  curr_run,
@@ -274,17 +379,18 @@ class ProfileSettings:
274
379
  runs: int
275
380
  """Number of times each configuration should be executed."""
276
381
 
277
- output_progress: bool
278
- """
279
- Will display a progress bar on stdout during profiling
280
- """
281
-
282
- output_detailed: bool
382
+ verbosity: VerbosityLevel
283
383
  """
284
- Will display a progress bar, detailed output for each config along with the profiler logs
384
+ Controls verbosity of output. SILENT suppresses all output, INFO shows progress bar,
385
+ DEBUG shows progress bar plus per-config logs and profiler output.
285
386
  """
286
387
 
287
- derive_metric: Callable[..., float | dict[str, float]] | None
388
+ derive_metric: (
389
+ Callable[
390
+ ..., float | tuple[float, str] | Mapping[str, float | tuple[float, str]]
391
+ ]
392
+ | None
393
+ )
288
394
  """
289
395
  A function to transform the collected metrics.
290
396
  This can be used to compute derived metrics like TFLOPs that cannot
@@ -350,6 +456,16 @@ class ProfileSettings:
350
456
  If None, uses default value (180 seconds).
351
457
  """
352
458
 
459
+ thermal_device: int | None
460
+ """
461
+ CUDA device ordinal to monitor for thermal throttling.
462
+
463
+ If set, monitor that CUDA device. If None, monitor the current CUDA device
464
+ context.
465
+ Device numbering follows CUDA runtime ordinals and honors
466
+ ``CUDA_VISIBLE_DEVICES``.
467
+ """
468
+
353
469
  output_prefix: str | None
354
470
  """
355
471
  All intermediate profiler files are created with this prefix
@@ -399,6 +515,7 @@ class ProfileResults:
399
515
  - ``RelativeStdDevPct``: Standard deviation as a percentage of the mean
400
516
  - ``StableMeasurement``: Boolean indicating if the measurement is stable (low variance). The measurement is stable if ``RelativeStdDevPct`` < 2 % .
401
517
  - ``Metric``: The metrics being collected and the metrics being derived
518
+ - ``Unit``: Unit of measurement for the metric value (e.g., ``ns`` for nanoseconds)
402
519
  - ``Kernel``: Name of the GPU kernel(s) launched
403
520
  - ``GPU``: GPU device name
404
521
  - ``Host``: Host machine name
@@ -487,7 +604,7 @@ class NsightProfiler:
487
604
  raw_df,
488
605
  func,
489
606
  self.settings.normalize_against,
490
- self.settings.output_progress,
607
+ self.settings.verbosity,
491
608
  )
492
609
 
493
610
  # Save to CSV if enabled
@@ -508,7 +625,7 @@ class NsightProfiler:
508
625
  index=False,
509
626
  )
510
627
 
511
- if self.settings.output_progress:
628
+ if self.settings.verbosity >= VerbosityLevel.INFO:
512
629
  print(
513
630
  f"[NSIGHT-PYTHON] Refer to {raw_csv_path} for the raw profiling data"
514
631
  )