nsight-python 1.0.0__py3-none-any.whl → 1.1.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,16 +1,40 @@
1
1
  # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ import os
4
5
  from importlib.metadata import version
5
6
 
6
- from nsight import analyze
7
+ from nsight import analyze, exceptions, experimental
7
8
  from nsight.annotation import annotate
8
- from nsight.collection.ncu import try_init_injection
9
+ from nsight.collection import ncu
10
+ from nsight.info_collector import CollectionScope, InfoCollector
11
+ from nsight.tools_manager import Tool, activate, deactivate, get_active_tool
9
12
  from nsight.utils import VerbosityLevel, col_panel, row_panel
10
13
 
11
- # Load injection library and symbols for NCU attach (if available)
12
- try_init_injection()
14
+ # NVIDIA Nsight Compute must attach before CUDA is initialized, so by default it
15
+ # is activated here -- importing nsight early is enough to satisfy that. Set
16
+ # NSPY_NCU_INIT_AT_IMPORT=0 to disable this and activate a tool yourself.
17
+ if os.environ.get("NSPY_NCU_INIT_AT_IMPORT", "1") != "0":
18
+ try:
19
+ activate(Tool.NCU)
20
+ except (exceptions.ProfilerException, exceptions.NCUNotAvailableError) as exc:
21
+ # Importing nsight must keep working when the NVIDIA Nsight Compute
22
+ # injection fails to load, so record the error rather than raising it
23
+ # here. NCUCollector.collect re-raises it late, when a decorated
24
+ # function is first called.
25
+ ncu.injection_load_error = exc
13
26
 
14
27
  __version__ = version("nsight-python")
15
28
 
16
- __all__ = ["analyze", "annotate", "VerbosityLevel"]
29
+ __all__ = [
30
+ "CollectionScope",
31
+ "InfoCollector",
32
+ "Tool",
33
+ "VerbosityLevel",
34
+ "activate",
35
+ "analyze",
36
+ "annotate",
37
+ "deactivate",
38
+ "experimental",
39
+ "get_active_tool",
40
+ ]
nsight/analyze.py CHANGED
@@ -16,6 +16,8 @@ from numpy.typing import NDArray
16
16
 
17
17
  import nsight.collection as collection
18
18
  import nsight.visualization as visualization
19
+ from nsight.info_collector import CollectionScope, InfoCollector
20
+ from nsight.tools_manager import Tool, activate, get_active_tool
19
21
  from nsight.utils import VerbosityLevel
20
22
 
21
23
 
@@ -54,6 +56,14 @@ def kernel(
54
56
  combine_kernel_metrics: Callable[[float, float], float] | None = None,
55
57
  output_prefix: str | None = None,
56
58
  output_csv: bool = False,
59
+ info_collectors: (
60
+ Sequence[
61
+ InfoCollector
62
+ | tuple[str, Callable[..., Any], str | CollectionScope]
63
+ | Callable[..., Any]
64
+ ]
65
+ | None
66
+ ) = None,
57
67
  ) -> Callable[[Callable[..., None]], Callable[..., collection.core.ProfileResults]]: ...
58
68
 
59
69
 
@@ -84,12 +94,20 @@ def kernel(
84
94
  combine_kernel_metrics: Callable[[float, float], float] | None = None,
85
95
  output_prefix: str | None = None,
86
96
  output_csv: bool = False,
97
+ info_collectors: (
98
+ Sequence[
99
+ InfoCollector
100
+ | tuple[str, Callable[..., Any], str | CollectionScope]
101
+ | Callable[..., Any]
102
+ ]
103
+ | None
104
+ ) = None,
87
105
  ) -> (
88
106
  Callable[..., collection.core.ProfileResults]
89
107
  | Callable[[Callable[..., None]], Callable[..., collection.core.ProfileResults]]
90
108
  ):
91
109
  """
92
- A decorator that collects profiling data using NVIDIA Nsight Compute.
110
+ A decorator that collects profiling data from GPU kernels.
93
111
 
94
112
  Can be used with or without parentheses:
95
113
  - ``@nsight.analyze.kernel`` (no parentheses)
@@ -106,6 +124,14 @@ def kernel(
106
124
  - ``**kwargs``: Original function keyword arguments
107
125
  - Returns ``ProfileResults`` object containing profiling data
108
126
 
127
+ .. warning::
128
+ Decorating a function changes its return value. Any value returned by the
129
+ original function is ignored, and calling the decorated function returns a
130
+ ``ProfileResults`` object instead. If the original function returns a
131
+ non-``None`` value, Nsight Python emits a ``RuntimeWarning``. Keep any
132
+ computation whose result you need in an undecorated helper and use the
133
+ decorated function only as a profiling wrapper.
134
+
109
135
  .. note::
110
136
  Configs are passed to the decorated function as positional arguments.
111
137
  Both positional and keyword-only parameters are supported in the function
@@ -113,6 +139,14 @@ def kernel(
113
139
  but will always be empty during profiling. Parameters with default values
114
140
  may be omitted from configs — missing values will be filled from the defaults.
115
141
 
142
+ Profiling uses NVIDIA Nsight Compute by default. Activating it is automatic
143
+ -- it happens during ``import nsight`` -- and needs no attention from you.
144
+ For more control over when NVIDIA Nsight Compute is loaded, see the
145
+ :doc:`/tools` page.
146
+
147
+ ``@nsight.analyze.kernel`` supports collection of
148
+ ``"gpu__time_duration.sum"`` via the low-overhead, experimental CUPTI
149
+ backend. See the :doc:`/tools` page for how to enable it.
116
150
 
117
151
  Parameters:
118
152
  configs: An iterable of configurations to run the function with. Each configuration can be either:
@@ -134,7 +168,7 @@ def kernel(
134
168
  derive_metric:
135
169
  A function to transform the collected metrics.
136
170
  This can be used to compute derived metrics like TFLOPs that cannot
137
- be captured by ncu directly. The function takes the metric values and
171
+ be captured directly. The function takes the metric values and
138
172
  the arguments of the profile-decorated function and returns the new
139
173
  metrics. Return value can be either:
140
174
 
@@ -157,7 +191,7 @@ def kernel(
157
191
  Combine with ``derive_metric`` to compute speedup (reciprocal of normalized value).
158
192
  metrics: The metrics to collect. By default, kernel runtimes in nanoseconds
159
193
  are collected. Default: ``["gpu__time_duration.sum"]``. To see the available
160
- metrics on your system, use the command: ``ncu --query-metrics``.
194
+ metrics on your system when using ncu, use the command: ``ncu --query-metrics``.
161
195
  ignore_kernel_list:
162
196
  List of kernel names to ignore. If you call a library within an annotated range context, you might not have precise control over which and how many kernels are being launched.
163
197
  If some of these kernels should be ignored in the profile, their names can be provided in this parameter. Default: ``None``
@@ -167,13 +201,19 @@ def kernel(
167
201
  how to summarize the collected metrics into a single number. For example,
168
202
  if we profile runtime and want to sum the times of all kernels we can specify
169
203
  ``combine_kernel_metrics = lambda x, y: x + y``. The function should take
170
- two arguments and return a single value. Default: ``None``.
204
+ two arguments and return a single value. Each annotation must still launch
205
+ the same number of kernels across configurations and repeated runs;
206
+ different counts may cause an error or incorrect results. Default: ``None``.
171
207
  clock_control: Control the behavior of the GPU clocks during profiling. Allowed values:
172
208
 
173
209
  - ``"base"``: GPC and memory clocks are locked to their respective base frequency during profiling. This has no impact on thermal throttling. Note that actual clocks might still vary, depending on the level of driver support for this feature. As an alternative, use nvidia-smi to lock the clocks externally and set this option to ``"none"``.
174
210
  - ``"none"``: No GPC or memory frequencies are changed during profiling.
175
211
 
176
212
  Default: ``"none"``
213
+
214
+ Refer the `NVIDIA Nsight Compute documentation on Clock Control <https://docs.nvidia.com/nsight-compute/ProfilingGuide/index.html#clock-control>`_ for more details.
215
+
216
+ Ignored when the CUPTI backend is active.
177
217
  cache_control: Control the behavior of the GPU caches during profiling. Allowed values:
178
218
 
179
219
  - ``"all"``: All GPU caches are flushed before each kernel replay iteration during profiling. While metric values in the execution environment of the application might be slightly different without invalidating the caches, this mode offers the most reproducible metric results across the replay passes and also across multiple runs of the target application.
@@ -181,6 +221,8 @@ def kernel(
181
221
 
182
222
  Default: ``"all"``
183
223
 
224
+ Ignored when the CUPTI backend is active.
225
+
184
226
  replay_mode: Mechanism used for replaying a kernel launch multiple times to collect selected metrics. Allowed values:
185
227
 
186
228
  - ``"kernel"``: Replay individual kernel launches during the execution of the application.
@@ -188,6 +230,8 @@ def kernel(
188
230
 
189
231
  Default: ``"kernel"``
190
232
 
233
+ Ignored when the CUPTI backend is active.
234
+
191
235
  thermal_mode: Controls GPU thermal management mode. Default: ``"auto"``
192
236
 
193
237
  Monitors GPU thermal headroom (T.Limit) and pauses profiling when GPU gets
@@ -280,8 +324,8 @@ def kernel(
280
324
  - ``NumRuns``: Number of runs used for aggregation
281
325
  - ``CI95_Lower``: Lower bound of the 95% confidence interval
282
326
  - ``CI95_Upper``: Upper bound of the 95% confidence interval
283
- - ``RelativeStdDevPct``: Standard deviation as a percentage of the mean
284
- - ``StableMeasurement``: Boolean indicating if the measurement is stable (low variance). The measurement is stable if ``RelativeStdDevPct`` < 2 % .
327
+ - ``RelativeStdDevPct``: Standard deviation as a percentage of the absolute mean
328
+ - ``StableMeasurement``: Nullable Boolean: true if ``RelativeStdDevPct`` < 2%, false otherwise, and ``pd.NA`` when relative variability is undefined (for example, with one valid sample or a zero mean).
285
329
  - ``Metric``: The metrics being collected and the metrics being derived
286
330
  - ``Unit``: Unit of measurement for the metric value (e.g., ``ns`` for nanoseconds)
287
331
  - ``Kernel``: Name of the GPU kernel(s) launched
@@ -289,10 +333,70 @@ def kernel(
289
333
  - ``Host``: Host machine name
290
334
  - ``ComputeClock``: GPU compute clock frequency
291
335
  - ``MemoryClock``: GPU memory clock frequency
336
+ info_collectors: Optional list of custom information collectors.
337
+ Collectors should be functions decorated with
338
+ ``@nsight.experimental.collect(scope=...)``.
339
+ The function name will be used as the column name in the DataFrame.
340
+
341
+ **Decorated functions** (recommended)::
342
+
343
+ @nsight.experimental.collect(scope=nsight.CollectionScope.ONCE)
344
+ def driver_version():
345
+ from cuda.core import system
346
+ return ".".join(
347
+ str(part) for part in system.get_user_mode_driver_version()
348
+ )
349
+
350
+ @nsight.experimental.collect(scope=nsight.CollectionScope.CONFIG)
351
+ def matrix_elements(n, dtype):
352
+ return n * n
353
+
354
+ @nsight.experimental.collect(scope=nsight.CollectionScope.RUN)
355
+ def gpu_temp_c(n, dtype):
356
+ return get_gpu_temperature()
357
+
358
+ @nsight.experimental.collect(
359
+ scope=nsight.CollectionScope.ANNOTATION
360
+ )
361
+ def ann_timestamp(annotation_name, *config_args):
362
+ return time.time()
363
+
364
+ @nsight.analyze.kernel(
365
+ info_collectors=[
366
+ driver_version, matrix_elements, gpu_temp_c, ann_timestamp
367
+ ]
368
+ )
369
+ def my_benchmark(n, dtype):
370
+ ...
371
+
372
+ Collectors may also be supplied as ``(name, callback, scope)``
373
+ tuples, where ``scope`` is ``"once"``, ``"config"``, ``"run"``,
374
+ or ``"annotation"`` (or a :class:`nsight.CollectionScope`).
375
+
376
+ Column naming: ``once``-scope and non-numeric collectors produce a
377
+ single column named after the function. Numeric ``config``/``run``/
378
+ ``annotation``-scope collectors are aggregated across runs into four
379
+ columns ``<name>_Avg``, ``<name>_Std``, ``<name>_Min``, ``<name>_Max``.
292
380
  """
293
381
  # Strip whitespace
294
382
  metrics = [m.strip() for m in metrics]
295
383
 
384
+ # These are decorator parameters, so reject illegal values now rather than
385
+ # when the decorated function is first called. Whether the active tool
386
+ # supports a legal value is a separate check, made by the collector.
387
+ if clock_control not in ("base", "none"):
388
+ raise ValueError(
389
+ f"Invalid clock_control {clock_control!r}. Expected 'base' or 'none'."
390
+ )
391
+ if cache_control not in ("all", "none"):
392
+ raise ValueError(
393
+ f"Invalid cache_control {cache_control!r}. Expected 'all' or 'none'."
394
+ )
395
+ if replay_mode not in ("kernel", "range"):
396
+ raise ValueError(
397
+ f"Invalid replay_mode {replay_mode!r}. Expected 'kernel' or 'range'."
398
+ )
399
+
296
400
  # Validate thermal parameters if both are provided
297
401
  if thermal_wait is not None and thermal_cont is not None:
298
402
  if thermal_cont <= thermal_wait:
@@ -309,10 +413,72 @@ def kernel(
309
413
  prefix = output_prefix
310
414
  if prefix is None:
311
415
  prefix = tempfile.mkdtemp(prefix="nspy_")
312
- prefix = os.path.join(prefix, "") # Adds a trailing forward/backward slash
416
+ prefix = os.path.join(prefix, "") # Adds a trailing path separator
313
417
  else:
314
418
  os.makedirs(os.path.dirname(prefix) or ".", exist_ok=True)
315
419
 
420
+ # Normalize info_collectors: convert decorated functions to
421
+ # (name, callback, scope) tuples with a canonical string scope.
422
+ valid_scopes = {s.value for s in CollectionScope}
423
+
424
+ def _canonical_scope(scope: Any, label: str) -> str:
425
+ if isinstance(scope, CollectionScope):
426
+ return scope.value
427
+ if isinstance(scope, str) and scope in valid_scopes:
428
+ return scope
429
+ raise ValueError(
430
+ f"Invalid scope {scope!r} for collector {label}. "
431
+ f"Expected one of {sorted(valid_scopes)} or a CollectionScope."
432
+ )
433
+
434
+ normalized_collectors = None
435
+ if info_collectors:
436
+ normalized_collectors = []
437
+ for collector in info_collectors:
438
+ if isinstance(collector, InfoCollector):
439
+ # An InfoCollector instance (the exported public class).
440
+ normalized_collectors.append(
441
+ (
442
+ collector.name,
443
+ collector.callback,
444
+ _canonical_scope(collector.scope, repr(collector.name)),
445
+ )
446
+ )
447
+ elif isinstance(collector, tuple):
448
+ # Raw tuple format: (name, callback, scope)
449
+ if len(collector) != 3:
450
+ raise ValueError(
451
+ "Tuple-format info collector must be "
452
+ f"(name, callback, scope); got {collector!r}."
453
+ )
454
+ name, callback, scope = collector
455
+ if not isinstance(name, str) or not callable(callback):
456
+ raise TypeError(
457
+ "Tuple-format info collector must be "
458
+ f"(str name, callable callback, scope); got {collector!r}."
459
+ )
460
+ normalized_collectors.append(
461
+ (name, callback, _canonical_scope(scope, repr(name)))
462
+ )
463
+ elif callable(collector):
464
+ # Decorated function: extract metadata
465
+ if hasattr(collector, "_nsight_collector_scope"):
466
+ name = getattr(collector, "_nsight_collector_name")
467
+ scope = getattr(collector, "_nsight_collector_scope")
468
+ normalized_collectors.append(
469
+ (name, collector, _canonical_scope(scope, repr(name)))
470
+ )
471
+ else:
472
+ raise ValueError(
473
+ f"Collector function {collector.__name__} is missing "
474
+ "@nsight.experimental.collect(scope=...) decorator"
475
+ )
476
+ else:
477
+ raise TypeError(
478
+ f"Invalid collector type: {type(collector)}. "
479
+ "Expected tuple (name, callback, scope) or decorated function."
480
+ )
481
+
316
482
  settings = collection.core.ProfileSettings(
317
483
  configs=configs,
318
484
  runs=runs,
@@ -326,16 +492,28 @@ def kernel(
326
492
  thermal_device=thermal_device,
327
493
  output_prefix=prefix,
328
494
  output_csv=output_csv,
495
+ info_collectors=normalized_collectors,
329
496
  )
330
- ncu = collection.ncu.NCUCollector(
331
- metrics=metrics,
332
- ignore_kernel_list=ignore_kernel_list,
333
- combine_kernel_metrics=combine_kernel_metrics,
334
- clock_control=clock_control,
335
- cache_control=cache_control,
336
- replay_mode=replay_mode,
337
- )
338
- return collection.core.NsightProfiler(settings, ncu)
497
+
498
+ def get_collector() -> collection.core.NsightCollector:
499
+ if get_active_tool() == Tool.CUPTI:
500
+ # clock_control, cache_control and replay_mode have no
501
+ # meaning for CUPTI, so the collector does not take them.
502
+ return collection.cupti.CUPTICollector(
503
+ metrics=metrics,
504
+ ignore_kernel_list=ignore_kernel_list,
505
+ combine_kernel_metrics=combine_kernel_metrics,
506
+ )
507
+ return collection.ncu.NCUCollector(
508
+ metrics=metrics,
509
+ ignore_kernel_list=ignore_kernel_list,
510
+ combine_kernel_metrics=combine_kernel_metrics,
511
+ clock_control=clock_control,
512
+ cache_control=cache_control,
513
+ replay_mode=replay_mode,
514
+ )
515
+
516
+ return collection.core.NsightProfiler(settings, get_collector)
339
517
 
340
518
  # Support both @kernel and @kernel() syntax
341
519
  if _func is None:
@@ -410,7 +588,7 @@ def plot(
410
588
  ylabel: str | None = None,
411
589
  annotate_points: bool = False,
412
590
  show_aggregate: str | None = None,
413
- plot_type: str = "line",
591
+ plot_type: Literal["line", "bar"] = "line",
414
592
  plot_width: int = 6,
415
593
  plot_height: int = 4,
416
594
  row_panels: Sequence[str] | None = None,
@@ -476,6 +654,11 @@ def plot(
476
654
  variant_fields: List of config fields to use as variant fields (lines).
477
655
  variant_annotations: List of annotated range names for which to apply variant splitting. The provided strings must each match one of the names defined using nsight.annotate.
478
656
  """
657
+ # Reject illegal values now rather than when the decorated function is
658
+ # first called.
659
+ if plot_type not in ("line", "bar"):
660
+ raise ValueError(f"Invalid plot_type {plot_type!r}. Expected 'line' or 'bar'.")
661
+
479
662
  show_avg = show_aggregate == "avg"
480
663
  show_geomean = show_aggregate == "geomean"
481
664
 
nsight/annotation.py CHANGED
@@ -9,6 +9,7 @@ from typing import Any
9
9
  import nvtx
10
10
 
11
11
  import nsight.utils as utils
12
+ from nsight import info_collector
12
13
  from nsight.exceptions import CUDA_CORE_UNAVAILABLE_MSG
13
14
 
14
15
  # Thread-local storage for tracking annotations
@@ -88,7 +89,7 @@ def is_active_annotation(name: str) -> bool:
88
89
  return False
89
90
 
90
91
 
91
- class annotate(nvtx.annotate): # type: ignore[misc]
92
+ class annotate:
92
93
  """
93
94
  A decorator/context-manager hybrid for marking profiling regions.
94
95
  The encapsulated code will be profiled and associated with an NVTX
@@ -129,10 +130,10 @@ class annotate(nvtx.annotate): # type: ignore[misc]
129
130
  Nested annotations are currently not supported. However, since each
130
131
  annotation is expected to contain a single kernel launch by default,
131
132
  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.
133
+ Entering and exiting the annotation context starts and ends an NVTX
134
+ range that is visible across all threads in the profiled process, so
135
+ kernels launched from worker threads within the annotated region are
136
+ also captured.
136
137
 
137
138
  """
138
139
 
@@ -144,9 +145,7 @@ class annotate(nvtx.annotate): # type: ignore[misc]
144
145
  if ignore_failures and not utils.CUDA_CORE_AVAILABLE:
145
146
  raise ImportError(CUDA_CORE_UNAVAILABLE_MSG)
146
147
 
147
- super().__init__(name, domain=utils.NVTX_DOMAIN)
148
-
149
- def __enter__(self) -> nvtx.annotate:
148
+ def __enter__(self) -> "annotate":
150
149
 
151
150
  # Check for nested annotations
152
151
  if _is_in_annotation():
@@ -164,14 +163,17 @@ class annotate(nvtx.annotate): # type: ignore[misc]
164
163
 
165
164
  add_active_annotation(self.name)
166
165
  _set_in_annotation(True)
167
- return super().__enter__()
166
+ # Collect annotation-scope information
167
+ info_collector.collect_for_annotation(self.name)
168
+ self._range_id = nvtx.start_range(message=self.name, domain=utils.NVTX_DOMAIN)
169
+ return self
168
170
 
169
171
  def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> bool:
170
172
  try:
171
173
  if exc_type and self.ignore_failures:
172
174
  utils.launch_dummy_kernel_module()
173
175
  finally:
174
- super().__exit__(exc_type, exc_value, traceback)
176
+ nvtx.end_range(self._range_id)
175
177
  _set_in_annotation(False)
176
178
 
177
179
  if exc_type and not self.ignore_failures:
@@ -4,7 +4,8 @@
4
4
  import functools
5
5
 
6
6
  import nsight.collection.core as core
7
+ import nsight.collection.cupti as cupti
7
8
  import nsight.collection.ncu as ncu
8
9
  import nsight.utils as utils
9
10
 
10
- __all__ = ["ncu", "core"]
11
+ __all__ = ["ncu", "cupti", "core"]