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 +31 -3
- nsight/analyze.py +281 -66
- nsight/annotation.py +12 -6
- nsight/collection/__init__.py +2 -1
- nsight/collection/core.py +437 -49
- nsight/collection/cupti.py +449 -0
- nsight/collection/ncu.py +398 -122
- nsight/exceptions.py +2 -0
- nsight/experimental.py +86 -0
- nsight/extraction.py +335 -72
- nsight/info_collector.py +364 -0
- nsight/thermovision.py +124 -36
- nsight/tools_manager.py +98 -0
- nsight/transformation.py +75 -25
- nsight/utils.py +47 -22
- nsight/visualization.py +4 -1
- {nsight_python-0.9.6.dist-info → nsight_python-1.1.0.dist-info}/METADATA +7 -5
- nsight_python-1.1.0.dist-info/RECORD +20 -0
- {nsight_python-0.9.6.dist-info → nsight_python-1.1.0.dist-info}/WHEEL +1 -1
- nsight_python-0.9.6.dist-info/RECORD +0 -16
- {nsight_python-0.9.6.dist-info → nsight_python-1.1.0.dist-info}/licenses/LICENSE +0 -0
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.
|
|
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__ = [
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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:
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
nsight/collection/__init__.py
CHANGED