nsight-python 0.9.5__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
@@ -1,12 +1,16 @@
1
1
  # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ from importlib.metadata import version
5
+
4
6
  from nsight import analyze
5
7
  from nsight.annotation import annotate
6
- 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
7
10
 
8
- # Versioning Scheme: major.minor.build
9
- __version__ = "0.9.5"
11
+ # Load injection library and symbols for NCU attach (if available)
12
+ try_init_injection()
10
13
 
14
+ __version__ = version("nsight-python")
11
15
 
12
- __all__ = ["analyze", "annotate"]
16
+ __all__ = ["analyze", "annotate", "VerbosityLevel"]
nsight/analyze.py CHANGED
@@ -7,14 +7,16 @@ 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
14
14
  import numpy as np
15
+ from numpy.typing import NDArray
15
16
 
16
17
  import nsight.collection as collection
17
18
  import nsight.visualization as visualization
19
+ from nsight.utils import VerbosityLevel
18
20
 
19
21
 
20
22
  # Overload 1: When used without parentheses: @kernel
@@ -31,15 +33,24 @@ def kernel(
31
33
  *,
32
34
  configs: Iterable[Any] | None = None,
33
35
  runs: int = 1,
34
- derive_metric: Callable[..., float] | None = None,
36
+ derive_metric: (
37
+ Callable[
38
+ ..., float | tuple[float, str] | Mapping[str, float | tuple[float, str]]
39
+ ]
40
+ | None
41
+ ) = None,
35
42
  normalize_against: str | None = None,
36
- output: Literal["quiet", "progress", "verbose"] = "progress",
43
+ verbosity: VerbosityLevel = VerbosityLevel.INFO,
37
44
  metrics: Sequence[str] = ["gpu__time_duration.sum"],
38
45
  ignore_kernel_list: Sequence[str] | None = None,
39
46
  clock_control: Literal["base", "none"] = "none",
40
47
  cache_control: Literal["all", "none"] = "all",
41
48
  replay_mode: Literal["kernel", "range"] = "kernel",
42
- thermal_control: bool = True,
49
+ thermal_mode: Literal["auto", "manual", "off"] = "auto",
50
+ thermal_wait: int | None = None,
51
+ thermal_cont: int | None = None,
52
+ thermal_timeout: int | None = None,
53
+ thermal_device: int | None = None,
43
54
  combine_kernel_metrics: Callable[[float, float], float] | None = None,
44
55
  output_prefix: str | None = None,
45
56
  output_csv: bool = False,
@@ -52,15 +63,24 @@ def kernel(
52
63
  *,
53
64
  configs: Iterable[Any] | None = None,
54
65
  runs: int = 1,
55
- derive_metric: Callable[..., float] | None = None,
66
+ derive_metric: (
67
+ Callable[
68
+ ..., float | tuple[float, str] | Mapping[str, float | tuple[float, str]]
69
+ ]
70
+ | None
71
+ ) = None,
56
72
  normalize_against: str | None = None,
57
- output: Literal["quiet", "progress", "verbose"] = "progress",
73
+ verbosity: VerbosityLevel = VerbosityLevel.INFO,
58
74
  metrics: Sequence[str] = ["gpu__time_duration.sum"],
59
75
  ignore_kernel_list: Sequence[str] | None = None,
60
76
  clock_control: Literal["base", "none"] = "none",
61
77
  cache_control: Literal["all", "none"] = "all",
62
78
  replay_mode: Literal["kernel", "range"] = "kernel",
63
- thermal_control: bool = True,
79
+ thermal_mode: Literal["auto", "manual", "off"] = "auto",
80
+ thermal_wait: int | None = None,
81
+ thermal_cont: int | None = None,
82
+ thermal_timeout: int | None = None,
83
+ thermal_device: int | None = None,
64
84
  combine_kernel_metrics: Callable[[float, float], float] | None = None,
65
85
  output_prefix: str | None = None,
66
86
  output_csv: bool = False,
@@ -86,6 +106,13 @@ def kernel(
86
106
  - ``**kwargs``: Original function keyword arguments
87
107
  - Returns ``ProfileResults`` object containing profiling data
88
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
+
89
116
 
90
117
  Parameters:
91
118
  configs: An iterable of configurations to run the function with. Each configuration can be either:
@@ -98,6 +125,10 @@ def kernel(
98
125
 
99
126
  configs = [1, 2, 3, 4]
100
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
+
101
132
  If the configs are not provided at decoration time, they must be provided when calling the decorated function.
102
133
  runs: Number of times each configuration should be executed.
103
134
  derive_metric:
@@ -105,15 +136,25 @@ def kernel(
105
136
  This can be used to compute derived metrics like TFLOPs that cannot
106
137
  be captured by ncu directly. The function takes the metric values and
107
138
  the arguments of the profile-decorated function and returns the new
108
- metric. The parameter order requirements for the custom function:
139
+ metrics. Return value can be either:
140
+
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>"``.
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")}``.
146
+
147
+ The parameter order requirements for the custom function:
109
148
 
110
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.
111
150
  - Remaining arguments: Must exactly match the signature of the decorated function. In other words, the original function's parameters are passed in order.
112
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
+
113
154
  See the examples for concrete use cases.
114
155
  normalize_against:
115
- Annotation name to normalize metrics against.
116
- This is useful to compute relative metrics like speedup.
156
+ Annotation name to normalize metrics against (computes current / baseline).
157
+ Combine with ``derive_metric`` to compute speedup (reciprocal of normalized value).
117
158
  metrics: The metrics to collect. By default, kernel runtimes in nanoseconds
118
159
  are collected. Default: ``["gpu__time_duration.sum"]``. To see the available
119
160
  metrics on your system, use the command: ``ncu --query-metrics``.
@@ -147,12 +188,56 @@ def kernel(
147
188
 
148
189
  Default: ``"kernel"``
149
190
 
150
- thermal_control : Toggles whether to enable thermal control. Default: ``True``
151
- output: Controls the verbosity level of the output.
191
+ thermal_mode: Controls GPU thermal management mode. Default: ``"auto"``
192
+
193
+ Monitors GPU thermal headroom (T.Limit) and pauses profiling when GPU gets
194
+ too hot, resuming after cooling. Prevents thermal throttling from affecting
195
+ profiling accuracy.
196
+
197
+ **Modes:**
198
+
199
+ - ``"auto"``: Adaptive mode - automatically adjusts thermal_cont threshold based on GPU heating behavior.
200
+ - ``"manual"``: Fixed mode - uses specified thermal_wait and thermal_cont thresholds without adaptation.
201
+ - ``"off"``: Thermal control disabled - no monitoring or pausing.
202
+
203
+ thermal_wait: Thermal headroom threshold (T.Limit in °C) that triggers cooling pause.
204
+
205
+ When GPU thermal headroom drops to this value, profiling pauses and waits
206
+ for the GPU to cool down. Lower values allow the GPU to run hotter before
207
+ pausing (more aggressive, faster profiling, higher throttling risk).
208
+
209
+ If ``None``, uses default value (10°C). Applied in both "auto" and "manual" modes.
210
+
211
+ thermal_cont: Thermal headroom threshold (T.Limit in °C) to resume profiling after cooling.
212
+
213
+ After a cooling pause, profiling resumes once GPU thermal headroom reaches
214
+ this value. Higher values mean the GPU cools more before resuming (safer,
215
+ fewer cooling cycles, but slower overall profiling time).
216
+
217
+ If ``None``, uses default value (20°C). In "auto" mode, this value adapts based on
218
+ workload characteristics. In "manual" mode, it remains fixed.
152
219
 
153
- - ``"quiet"``: Suppresses all output.
154
- - ``"progress"``: Shows a progress bar along with details about profiling and data extraction progress.
155
- - ``"verbose"``: Displays the progress bar, configuration-specific logs, and profiler logs.
220
+ **Requirement:** Must be greater than thermal_wait.
221
+
222
+ thermal_timeout: Maximum wait time in seconds for GPU to cool down.
223
+ Default: 180 seconds
224
+
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``
232
+
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.
156
241
 
157
242
  output_prefix: When specified, all intermediate profiler files are created with this prefix.
158
243
  For example, if `output_prefix="/home/user/run1_"`, the profiler will generate:
@@ -175,8 +260,8 @@ def kernel(
175
260
 
176
261
  - ``Annotation``: Name of the annotated region being profiled
177
262
  - ``Value``: Raw metric values collected by the profiler
178
- - ``Metric``: The metrics being collected (e.g., ``gpu__time_duration.sum``)
179
- - ``Transformed``: Name of the function used to transform the metrics (specified via ``derive_metric``), or ``False`` if no transformation was applied. For lambda functions, this shows ``"<lambda>"``
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)
180
265
  - ``Kernel``: Name of the GPU kernel(s) launched
181
266
  - ``GPU``: GPU device name
182
267
  - ``Host``: Host machine name
@@ -197,8 +282,8 @@ def kernel(
197
282
  - ``CI95_Upper``: Upper bound of the 95% confidence interval
198
283
  - ``RelativeStdDevPct``: Standard deviation as a percentage of the mean
199
284
  - ``StableMeasurement``: Boolean indicating if the measurement is stable (low variance). The measurement is stable if ``RelativeStdDevPct`` < 2 % .
200
- - ``Metric``: The metrics being collected
201
- - ``Transformed``: Name of the function used to transform the metrics (specified via ``derive_metric``), or ``False`` if no transformation was applied. For lambda functions, this shows ``"<lambda>"``
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)
202
287
  - ``Kernel``: Name of the GPU kernel(s) launched
203
288
  - ``GPU``: GPU device name
204
289
  - ``Host``: Host machine name
@@ -208,33 +293,37 @@ def kernel(
208
293
  # Strip whitespace
209
294
  metrics = [m.strip() for m in metrics]
210
295
 
296
+ # Validate thermal parameters if both are provided
297
+ if thermal_wait is not None and thermal_cont is not None:
298
+ if thermal_cont <= thermal_wait:
299
+ raise ValueError(
300
+ f"Invalid thermal control parameters: thermal_cont ({thermal_cont}°C) "
301
+ f"must be greater than thermal_wait ({thermal_wait}°C). "
302
+ f"thermal_cont is the target thermal headroom after cooling, "
303
+ f"thermal_wait is the threshold that triggers cooling."
304
+ )
305
+
211
306
  def _create_profiler() -> collection.core.NsightProfiler:
212
307
  """Helper to create the profiler with the given settings."""
213
- if output not in ("quiet", "progress", "verbose"):
214
- raise ValueError("output must be 'quiet', 'progress' or 'verbose'")
215
-
216
- output_progress = output == "progress" or output == "verbose"
217
- output_detailed = output == "verbose"
218
-
219
308
  # Create the output paths needed for the ncu report, ncu logs and the CSVs
220
309
  prefix = output_prefix
221
- if "NSPY_NCU_PROFILE" not in os.environ:
222
- if prefix is None:
223
- prefix = tempfile.mkdtemp(prefix="nspy_")
224
- prefix = os.path.join(
225
- prefix, ""
226
- ) # Adds a trailing forward/backward slash
227
- else:
228
- 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)
229
315
 
230
316
  settings = collection.core.ProfileSettings(
231
317
  configs=configs,
232
318
  runs=runs,
233
- output_progress=output_progress,
234
- output_detailed=output_detailed,
319
+ verbosity=verbosity,
235
320
  derive_metric=derive_metric,
236
321
  normalize_against=normalize_against,
237
- thermal_control=thermal_control,
322
+ thermal_mode=thermal_mode,
323
+ thermal_wait=thermal_wait,
324
+ thermal_cont=thermal_cont,
325
+ thermal_timeout=thermal_timeout,
326
+ thermal_device=thermal_device,
238
327
  output_prefix=prefix,
239
328
  output_csv=output_csv,
240
329
  )
@@ -259,29 +348,63 @@ def kernel(
259
348
  return profiler(_func) # type: ignore[return-value]
260
349
 
261
350
 
262
- def _validate_metric(result: collection.core.ProfileResults) -> None:
351
+ def _legalize_metric(result: collection.core.ProfileResults, metric: str | None) -> str:
263
352
  """
264
- Check if ProfileResults contains only a single metric.
353
+ Legalize the metric parameter for plotting.
354
+
355
+ This function ensures that the provided metric parameter is valid for plotting.
356
+ If the metric is None, it verifies there is exactly one metric available and
357
+ returns that metric's name. If a metric is specified, it validates that the
358
+ metric exists in the profile results and returns itself.
265
359
 
266
360
  Args:
267
- result: ProfileResults object
361
+ result: ProfileResults object containing profiling data.
362
+ metric: The name of the metric to plot, or None to use the single metric
363
+ if only one exists.
364
+
365
+ Returns:
366
+ The validated metric name for plotting.
268
367
 
269
368
  Raises:
270
- ValueError: If multiple metrics are detected
369
+ ValueError: Raised in the following cases:
370
+ 1. When `metric` is None and multiple metrics are found.
371
+ 2. When `metric` is None and no metrics are found.
372
+ 3. When `metric` is specified but not present in the results.
271
373
  """
272
374
  df = result.to_dataframe()
273
375
 
274
- # Check for multiple metrics in "Metric" column
275
- unique_metrics = df["Metric"].unique()
276
- if len(unique_metrics) > 1:
277
- raise ValueError(
278
- f"Cannot visualize {len(unique_metrics)} > 1 metrics with the "
279
- "@nsight.analyze.plot decorator."
280
- )
376
+ # Extract unique metric names from the DataFrame
377
+ unique_metrics: NDArray[Any] = df["Metric"].unique()
378
+ n_metrics = len(unique_metrics)
379
+
380
+ if metric is None:
381
+ # Case: No metric specified - ensure there is exactly one metric
382
+ if n_metrics > 1:
383
+ raise ValueError(
384
+ f"Cannot visualize multiple metrics ({n_metrics} > 1) with the "
385
+ "@nsight.analyze.plot decorator. Please specify a metric to plot. "
386
+ f"The metrics found are: {unique_metrics.tolist()}"
387
+ )
388
+ elif n_metrics == 0:
389
+ raise ValueError(
390
+ "No metrics found in the profile results. Please ensure that the "
391
+ "profiler is configured correctly."
392
+ )
393
+ else: # n_metrics == 1
394
+ return str(unique_metrics[0])
395
+ else:
396
+ # Case: Metric specified - ensure it exists in the results
397
+ if metric not in unique_metrics:
398
+ raise ValueError(
399
+ f"Metric '{metric}' not found in the profile results. Please check "
400
+ f"the metric name and try again. The metrics found are: {unique_metrics.tolist()}."
401
+ )
402
+ return metric
281
403
 
282
404
 
283
405
  def plot(
284
406
  filename: str = "plot.png",
407
+ metric: str | None = None,
285
408
  *,
286
409
  title: str = "Nsight Analyze Kernel Plot Results",
287
410
  ylabel: str | None = None,
@@ -320,6 +443,10 @@ def plot(
320
443
  def my_func(...):
321
444
 
322
445
  Args:
446
+ metric: The specific metric to plot (e.g., ``gpu__time_duration.sum``).
447
+ If None (default), the function will plot the single metric found in
448
+ the ``ProfileResults`` object containing profiling data, if only one exists,
449
+ otherwise it will raise an error if multiple metrics are present. Default: ``None``
323
450
  filename: Filename to save the plot. Default: ``'plot'``
324
451
  title: Title for the plot. Default: ``'Nsight Analyze Kernel Plot Results'``
325
452
  ylabel: Label for the y-axis in the generated plot.
@@ -357,29 +484,29 @@ def plot(
357
484
  def wrapper(*args: Any, **kwargs: Any) -> Any:
358
485
  result = func(*args, **kwargs)
359
486
 
360
- if "NSPY_NCU_PROFILE" not in os.environ:
361
- # Check for multiple metrics or complex data structures
362
- _validate_metric(result)
363
-
364
- visualization.visualize(
365
- result.to_dataframe(),
366
- row_panels=row_panels,
367
- col_panels=col_panels,
368
- x_keys=x_keys,
369
- print_data=print_data,
370
- title=title,
371
- filename=filename,
372
- ylabel=ylabel or "",
373
- annotate_points=annotate_points,
374
- show_avg=show_avg,
375
- show_geomean=show_geomean,
376
- plot_type=plot_type,
377
- plot_width=plot_width,
378
- plot_height=plot_height,
379
- variant_fields=variant_fields,
380
- variant_annotations=variant_annotations,
381
- plot_callback=plot_callback,
382
- )
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
+ )
383
510
  return result
384
511
 
385
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