nsight-python 0.9.6__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,12 +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.utils import col_panel, row_panel
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
12
+ from nsight.utils import VerbosityLevel, col_panel, row_panel
13
+
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
9
26
 
10
27
  __version__ = version("nsight-python")
11
28
 
12
- __all__ = ["analyze", "annotate"]
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
@@ -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,9 @@ 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
21
+ from nsight.utils import VerbosityLevel
19
22
 
20
23
 
21
24
  # Overload 1: When used without parentheses: @kernel
@@ -32,9 +35,14 @@ def kernel(
32
35
  *,
33
36
  configs: Iterable[Any] | None = None,
34
37
  runs: int = 1,
35
- derive_metric: Callable[..., float | dict[str, float]] | None = None,
38
+ derive_metric: (
39
+ Callable[
40
+ ..., float | tuple[float, str] | Mapping[str, float | tuple[float, str]]
41
+ ]
42
+ | None
43
+ ) = None,
36
44
  normalize_against: str | None = None,
37
- output: Literal["quiet", "progress", "verbose"] = "progress",
45
+ verbosity: VerbosityLevel = VerbosityLevel.INFO,
38
46
  metrics: Sequence[str] = ["gpu__time_duration.sum"],
39
47
  ignore_kernel_list: Sequence[str] | None = None,
40
48
  clock_control: Literal["base", "none"] = "none",
@@ -44,9 +52,18 @@ def kernel(
44
52
  thermal_wait: int | None = None,
45
53
  thermal_cont: int | None = None,
46
54
  thermal_timeout: int | None = None,
55
+ thermal_device: int | None = None,
47
56
  combine_kernel_metrics: Callable[[float, float], float] | None = None,
48
57
  output_prefix: str | None = None,
49
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,
50
67
  ) -> Callable[[Callable[..., None]], Callable[..., collection.core.ProfileResults]]: ...
51
68
 
52
69
 
@@ -56,9 +73,14 @@ def kernel(
56
73
  *,
57
74
  configs: Iterable[Any] | None = None,
58
75
  runs: int = 1,
59
- derive_metric: Callable[..., float | dict[str, float]] | None = None,
76
+ derive_metric: (
77
+ Callable[
78
+ ..., float | tuple[float, str] | Mapping[str, float | tuple[float, str]]
79
+ ]
80
+ | None
81
+ ) = None,
60
82
  normalize_against: str | None = None,
61
- output: Literal["quiet", "progress", "verbose"] = "progress",
83
+ verbosity: VerbosityLevel = VerbosityLevel.INFO,
62
84
  metrics: Sequence[str] = ["gpu__time_duration.sum"],
63
85
  ignore_kernel_list: Sequence[str] | None = None,
64
86
  clock_control: Literal["base", "none"] = "none",
@@ -68,15 +90,24 @@ def kernel(
68
90
  thermal_wait: int | None = None,
69
91
  thermal_cont: int | None = None,
70
92
  thermal_timeout: int | None = None,
93
+ thermal_device: int | None = None,
71
94
  combine_kernel_metrics: Callable[[float, float], float] | None = None,
72
95
  output_prefix: str | None = None,
73
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,
74
105
  ) -> (
75
106
  Callable[..., collection.core.ProfileResults]
76
107
  | Callable[[Callable[..., None]], Callable[..., collection.core.ProfileResults]]
77
108
  ):
78
109
  """
79
- A decorator that collects profiling data using NVIDIA Nsight Compute.
110
+ A decorator that collects profiling data from GPU kernels.
80
111
 
81
112
  Can be used with or without parentheses:
82
113
  - ``@nsight.analyze.kernel`` (no parentheses)
@@ -93,6 +124,29 @@ def kernel(
93
124
  - ``**kwargs``: Original function keyword arguments
94
125
  - Returns ``ProfileResults`` object containing profiling data
95
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
+
135
+ .. note::
136
+ Configs are passed to the decorated function as positional arguments.
137
+ Both positional and keyword-only parameters are supported in the function
138
+ signature. If the function has ``*args`` or ``**kwargs``, they are tolerated
139
+ but will always be empty during profiling. Parameters with default values
140
+ may be omitted from configs — missing values will be filled from the defaults.
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.
96
150
 
97
151
  Parameters:
98
152
  configs: An iterable of configurations to run the function with. Each configuration can be either:
@@ -105,30 +159,39 @@ def kernel(
105
159
 
106
160
  configs = [1, 2, 3, 4]
107
161
 
162
+ If configs provide fewer values than the function has parameters,
163
+ the remaining parameters must have default values, which will be used
164
+ automatically.
165
+
108
166
  If the configs are not provided at decoration time, they must be provided when calling the decorated function.
109
167
  runs: Number of times each configuration should be executed.
110
168
  derive_metric:
111
169
  A function to transform the collected metrics.
112
170
  This can be used to compute derived metrics like TFLOPs that cannot
113
- be captured by ncu directly. The function takes the metric values and
171
+ be captured directly. The function takes the metric values and
114
172
  the arguments of the profile-decorated function and returns the new
115
173
  metrics. Return value can be either:
116
174
 
117
175
  - 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.
176
+ - 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")``.
177
+ - A dictionary: Keys will be used as metric names, values can be either:
178
+ - Scalar values (float/int): Metric values without units. ``np.nan`` will be added to the ``Unit`` column and a warning will be issued.
179
+ - 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
180
 
120
181
  The parameter order requirements for the custom function:
121
182
 
122
183
  - 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
184
  - Remaining arguments: Must exactly match the signature of the decorated function. In other words, the original function's parameters are passed in order.
124
185
 
186
+ **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.
187
+
125
188
  See the examples for concrete use cases.
126
189
  normalize_against:
127
190
  Annotation name to normalize metrics against (computes current / baseline).
128
191
  Combine with ``derive_metric`` to compute speedup (reciprocal of normalized value).
129
192
  metrics: The metrics to collect. By default, kernel runtimes in nanoseconds
130
193
  are collected. Default: ``["gpu__time_duration.sum"]``. To see the available
131
- metrics on your system, use the command: ``ncu --query-metrics``.
194
+ metrics on your system when using ncu, use the command: ``ncu --query-metrics``.
132
195
  ignore_kernel_list:
133
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.
134
197
  If some of these kernels should be ignored in the profile, their names can be provided in this parameter. Default: ``None``
@@ -138,13 +201,19 @@ def kernel(
138
201
  how to summarize the collected metrics into a single number. For example,
139
202
  if we profile runtime and want to sum the times of all kernels we can specify
140
203
  ``combine_kernel_metrics = lambda x, y: x + y``. The function should take
141
- 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``.
142
207
  clock_control: Control the behavior of the GPU clocks during profiling. Allowed values:
143
208
 
144
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"``.
145
210
  - ``"none"``: No GPC or memory frequencies are changed during profiling.
146
211
 
147
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.
148
217
  cache_control: Control the behavior of the GPU caches during profiling. Allowed values:
149
218
 
150
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.
@@ -152,6 +221,8 @@ def kernel(
152
221
 
153
222
  Default: ``"all"``
154
223
 
224
+ Ignored when the CUPTI backend is active.
225
+
155
226
  replay_mode: Mechanism used for replaying a kernel launch multiple times to collect selected metrics. Allowed values:
156
227
 
157
228
  - ``"kernel"``: Replay individual kernel launches during the execution of the application.
@@ -159,6 +230,8 @@ def kernel(
159
230
 
160
231
  Default: ``"kernel"``
161
232
 
233
+ Ignored when the CUPTI backend is active.
234
+
162
235
  thermal_mode: Controls GPU thermal management mode. Default: ``"auto"``
163
236
 
164
237
  Monitors GPU thermal headroom (T.Limit) and pauses profiling when GPU gets
@@ -193,11 +266,22 @@ def kernel(
193
266
  thermal_timeout: Maximum wait time in seconds for GPU to cool down.
194
267
  Default: 180 seconds
195
268
 
196
- output: Controls the verbosity level of the output.
269
+ thermal_device: CUDA device ordinal to monitor for thermal throttling.
270
+
271
+ If set, monitor that CUDA device. If ``None``, monitor the current
272
+ CUDA device context. Device numbering follows CUDA runtime ordinals and
273
+ honors ``CUDA_VISIBLE_DEVICES``.
274
+
275
+ Default: ``None``
197
276
 
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.
277
+ verbosity: Controls the verbosity level of the output. Selecting level N
278
+ enables output at all levels with value <= N. Default: ``VerbosityLevel.INFO``.
279
+
280
+ - ``VerbosityLevel.SILENT``: Suppresses all output.
281
+ - ``VerbosityLevel.INFO``: Shows a progress bar, profiling completion messages,
282
+ and report file paths.
283
+ - ``VerbosityLevel.DEBUG``: Adds configuration-specific logs, the full NCU CLI
284
+ command, and enables NCU verbose output.
201
285
 
202
286
  output_prefix: When specified, all intermediate profiler files are created with this prefix.
203
287
  For example, if `output_prefix="/home/user/run1_"`, the profiler will generate:
@@ -221,6 +305,7 @@ def kernel(
221
305
  - ``Annotation``: Name of the annotated region being profiled
222
306
  - ``Value``: Raw metric values collected by the profiler
223
307
  - ``Metric``: The metrics being collected (e.g., ``gpu__time_duration.sum``) and the metrics being derived
308
+ - ``Unit``: Unit of measurement for the metric value (e.g., ``ns`` for nanoseconds)
224
309
  - ``Kernel``: Name of the GPU kernel(s) launched
225
310
  - ``GPU``: GPU device name
226
311
  - ``Host``: Host machine name
@@ -239,18 +324,79 @@ def kernel(
239
324
  - ``NumRuns``: Number of runs used for aggregation
240
325
  - ``CI95_Lower``: Lower bound of the 95% confidence interval
241
326
  - ``CI95_Upper``: Upper bound of the 95% confidence interval
242
- - ``RelativeStdDevPct``: Standard deviation as a percentage of the mean
243
- - ``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).
244
329
  - ``Metric``: The metrics being collected and the metrics being derived
330
+ - ``Unit``: Unit of measurement for the metric value (e.g., ``ns`` for nanoseconds)
245
331
  - ``Kernel``: Name of the GPU kernel(s) launched
246
332
  - ``GPU``: GPU device name
247
333
  - ``Host``: Host machine name
248
334
  - ``ComputeClock``: GPU compute clock frequency
249
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``.
250
380
  """
251
381
  # Strip whitespace
252
382
  metrics = [m.strip() for m in metrics]
253
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
+
254
400
  # Validate thermal parameters if both are provided
255
401
  if thermal_wait is not None and thermal_cont is not None:
256
402
  if thermal_cont <= thermal_wait:
@@ -263,46 +409,111 @@ def kernel(
263
409
 
264
410
  def _create_profiler() -> collection.core.NsightProfiler:
265
411
  """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
412
  # Create the output paths needed for the ncu report, ncu logs and the CSVs
273
413
  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)
414
+ if prefix is None:
415
+ prefix = tempfile.mkdtemp(prefix="nspy_")
416
+ prefix = os.path.join(prefix, "") # Adds a trailing path separator
417
+ else:
418
+ os.makedirs(os.path.dirname(prefix) or ".", exist_ok=True)
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
+ )
282
481
 
283
482
  settings = collection.core.ProfileSettings(
284
483
  configs=configs,
285
484
  runs=runs,
286
- output_progress=output_progress,
287
- output_detailed=output_detailed,
485
+ verbosity=verbosity,
288
486
  derive_metric=derive_metric,
289
487
  normalize_against=normalize_against,
290
488
  thermal_mode=thermal_mode,
291
489
  thermal_wait=thermal_wait,
292
490
  thermal_cont=thermal_cont,
293
491
  thermal_timeout=thermal_timeout,
492
+ thermal_device=thermal_device,
294
493
  output_prefix=prefix,
295
494
  output_csv=output_csv,
495
+ info_collectors=normalized_collectors,
296
496
  )
297
- ncu = collection.ncu.NCUCollector(
298
- metrics=metrics,
299
- ignore_kernel_list=ignore_kernel_list,
300
- combine_kernel_metrics=combine_kernel_metrics,
301
- clock_control=clock_control,
302
- cache_control=cache_control,
303
- replay_mode=replay_mode,
304
- )
305
- 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)
306
517
 
307
518
  # Support both @kernel and @kernel() syntax
308
519
  if _func is None:
@@ -377,7 +588,7 @@ def plot(
377
588
  ylabel: str | None = None,
378
589
  annotate_points: bool = False,
379
590
  show_aggregate: str | None = None,
380
- plot_type: str = "line",
591
+ plot_type: Literal["line", "bar"] = "line",
381
592
  plot_width: int = 6,
382
593
  plot_height: int = 4,
383
594
  row_panels: Sequence[str] | None = None,
@@ -443,6 +654,11 @@ def plot(
443
654
  variant_fields: List of config fields to use as variant fields (lines).
444
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.
445
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
+
446
662
  show_avg = show_aggregate == "avg"
447
663
  show_geomean = show_aggregate == "geomean"
448
664
 
@@ -451,30 +667,29 @@ def plot(
451
667
  def wrapper(*args: Any, **kwargs: Any) -> Any:
452
668
  result = func(*args, **kwargs)
453
669
 
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
- )
670
+ # Legalize the user-provided metric for plotting
671
+ legalized_metric = _legalize_metric(result, metric)
672
+
673
+ visualization.visualize(
674
+ result.to_dataframe(),
675
+ metric=legalized_metric,
676
+ row_panels=row_panels,
677
+ col_panels=col_panels,
678
+ x_keys=x_keys,
679
+ print_data=print_data,
680
+ title=title,
681
+ filename=filename,
682
+ ylabel=ylabel or "",
683
+ annotate_points=annotate_points,
684
+ show_avg=show_avg,
685
+ show_geomean=show_geomean,
686
+ plot_type=plot_type,
687
+ plot_width=plot_width,
688
+ plot_height=plot_height,
689
+ variant_fields=variant_fields,
690
+ variant_annotations=variant_annotations,
691
+ plot_callback=plot_callback,
692
+ )
478
693
  return result
479
694
 
480
695
  return wrapper
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,6 +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.
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.
132
137
 
133
138
  """
134
139
 
@@ -140,9 +145,7 @@ class annotate(nvtx.annotate): # type: ignore[misc]
140
145
  if ignore_failures and not utils.CUDA_CORE_AVAILABLE:
141
146
  raise ImportError(CUDA_CORE_UNAVAILABLE_MSG)
142
147
 
143
- super().__init__(name, domain=utils.NVTX_DOMAIN)
144
-
145
- def __enter__(self) -> nvtx.annotate:
148
+ def __enter__(self) -> "annotate":
146
149
 
147
150
  # Check for nested annotations
148
151
  if _is_in_annotation():
@@ -160,14 +163,17 @@ class annotate(nvtx.annotate): # type: ignore[misc]
160
163
 
161
164
  add_active_annotation(self.name)
162
165
  _set_in_annotation(True)
163
- 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
164
170
 
165
171
  def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> bool:
166
172
  try:
167
173
  if exc_type and self.ignore_failures:
168
174
  utils.launch_dummy_kernel_module()
169
175
  finally:
170
- super().__exit__(exc_type, exc_value, traceback)
176
+ nvtx.end_range(self._range_id)
171
177
  _set_in_annotation(False)
172
178
 
173
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"]