torch-prisma 0.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.
- torch_prisma/__init__.py +20 -0
- torch_prisma/complexity.py +303 -0
- torch_prisma/execution.py +19 -0
- torch_prisma/formulas.py +677 -0
- torch_prisma/latency.py +246 -0
- torch_prisma-0.1.0.dist-info/METADATA +72 -0
- torch_prisma-0.1.0.dist-info/RECORD +9 -0
- torch_prisma-0.1.0.dist-info/WHEEL +4 -0
- torch_prisma-0.1.0.dist-info/licenses/LICENSE +21 -0
torch_prisma/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Inference latency and model complexity measurement for PyTorch."""
|
|
2
|
+
|
|
3
|
+
from .complexity import (
|
|
4
|
+
ModelComplexity,
|
|
5
|
+
OperationCounts,
|
|
6
|
+
calculate_model_complexity,
|
|
7
|
+
count_operations,
|
|
8
|
+
count_parameters,
|
|
9
|
+
)
|
|
10
|
+
from .latency import LatencyStats, measure_module_latency
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"LatencyStats",
|
|
14
|
+
"ModelComplexity",
|
|
15
|
+
"OperationCounts",
|
|
16
|
+
"calculate_model_complexity",
|
|
17
|
+
"count_operations",
|
|
18
|
+
"count_parameters",
|
|
19
|
+
"measure_module_latency",
|
|
20
|
+
]
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Parameter counts, nominal FLOPs, dense MACs, and coverage diagnostics."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
from torch import nn
|
|
10
|
+
from torch.autograd.profiler_util import FunctionEvent
|
|
11
|
+
from torch.nn.attention import SDPBackend, sdpa_kernel
|
|
12
|
+
from torch.profiler import ProfilerActivity, profile, record_function
|
|
13
|
+
from torch.utils.flop_counter import FlopCounterMode, flop_registry
|
|
14
|
+
|
|
15
|
+
from .execution import call_arguments, tensor_leaves
|
|
16
|
+
from .formulas import (
|
|
17
|
+
FLOP_COMPOSITES,
|
|
18
|
+
FLOP_FREE_OPS,
|
|
19
|
+
MAC_COMPOSITES,
|
|
20
|
+
MAC_FORMULAS,
|
|
21
|
+
MAC_FREE_OPS,
|
|
22
|
+
arithmetic_formulas,
|
|
23
|
+
biased_matrix_vector_flops,
|
|
24
|
+
cudnn_convolution_flops,
|
|
25
|
+
dot_flops,
|
|
26
|
+
efficient_attention_flops,
|
|
27
|
+
is_native_pointwise,
|
|
28
|
+
matrix_extra_flops,
|
|
29
|
+
matrix_vector_flops,
|
|
30
|
+
slow_conv2d_flops,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_FORMULA_PREFIX = "torch_prisma.formula/"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def count_parameters(model: nn.Module) -> int:
|
|
37
|
+
"""Count elements in the model's unique registered parameters.
|
|
38
|
+
|
|
39
|
+
Counts use tensor dimensions, including for frozen or masked parameters.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
model: Module whose registered parameters are counted.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Total number of parameter elements.
|
|
46
|
+
"""
|
|
47
|
+
return sum(parameter.numel() for parameter in model.parameters())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class OperationCounts:
|
|
52
|
+
"""Dense MACs, nominal FLOPs, and coverage gaps for a forward."""
|
|
53
|
+
|
|
54
|
+
macs: int
|
|
55
|
+
flops: int
|
|
56
|
+
uncovered_ops: dict[str, tuple[str, ...]]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class ModelComplexity:
|
|
61
|
+
"""Model parameter count and operation counts for one forward.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
macs: Known dense matrix and convolution multiply-accumulates.
|
|
65
|
+
params: Number of elements in unique registered parameters.
|
|
66
|
+
flops: Known nominal floating-point operations.
|
|
67
|
+
uncovered_ops: Sorted names of operations with incomplete counts,
|
|
68
|
+
grouped under `macs` and `flops`.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
macs: int
|
|
72
|
+
params: int
|
|
73
|
+
flops: int
|
|
74
|
+
uncovered_ops: dict[str, tuple[str, ...]] = field(
|
|
75
|
+
default_factory=lambda: {"macs": (), "flops": ()}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def to_dict(self) -> dict[str, Any]:
|
|
79
|
+
"""Return counts and coverage as JSON-ready data."""
|
|
80
|
+
return {
|
|
81
|
+
"macs": self.macs,
|
|
82
|
+
"params": self.params,
|
|
83
|
+
"flops": self.flops,
|
|
84
|
+
"uncovered_ops": {key: list(ops) for key, ops in self.uncovered_ops.items()},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@contextmanager
|
|
89
|
+
def _counting_attention() -> Iterator[None]:
|
|
90
|
+
"""Expose attention matrix work and restore public backend switches on exit."""
|
|
91
|
+
fastpath = torch.backends.mha.get_fastpath_enabled()
|
|
92
|
+
try:
|
|
93
|
+
torch.backends.mha.set_fastpath_enabled(False)
|
|
94
|
+
with sdpa_kernel(SDPBackend.MATH):
|
|
95
|
+
yield
|
|
96
|
+
finally:
|
|
97
|
+
torch.backends.mha.set_fastpath_enabled(fastpath)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _find_coverage_gaps(
|
|
101
|
+
events: list[FunctionEvent], mac_free: frozenset[str]
|
|
102
|
+
) -> tuple[set[str], set[str]]:
|
|
103
|
+
"""Match each formula marker to one invocation and its backend children.
|
|
104
|
+
|
|
105
|
+
A C++ composite can invoke an operation below Python dispatch. Follow
|
|
106
|
+
profiler parents to distinguish hidden calls from separately counted calls.
|
|
107
|
+
"""
|
|
108
|
+
# Native pointwise semantics prove the absence of dense matrix/convolution
|
|
109
|
+
# MACs without implying a FLOP count. Inspect each observed packet once.
|
|
110
|
+
mac_free = mac_free | {
|
|
111
|
+
name
|
|
112
|
+
for name in {event.name for event in events}
|
|
113
|
+
if name not in mac_free and name not in MAC_FORMULAS and is_native_pointwise(name)
|
|
114
|
+
}
|
|
115
|
+
counted: dict[int, str] = {}
|
|
116
|
+
recorded: set[str] = set()
|
|
117
|
+
for event in events:
|
|
118
|
+
if not event.name.startswith(_FORMULA_PREFIX):
|
|
119
|
+
continue
|
|
120
|
+
name = event.name.removeprefix(_FORMULA_PREFIX)
|
|
121
|
+
recorded.add(name)
|
|
122
|
+
parent = event.cpu_parent
|
|
123
|
+
while parent is not None and parent.name != name:
|
|
124
|
+
parent = parent.cpu_parent
|
|
125
|
+
if (
|
|
126
|
+
parent is None
|
|
127
|
+
and not name.startswith("aten::")
|
|
128
|
+
and event.cpu_parent is not None
|
|
129
|
+
and event.cpu_parent.name == "PythonDispatchMode"
|
|
130
|
+
):
|
|
131
|
+
# Python custom operators can lack their own profiler event. The
|
|
132
|
+
# immediate dispatch scope still contains this call and its formula.
|
|
133
|
+
parent = event.cpu_parent
|
|
134
|
+
if parent is not None:
|
|
135
|
+
counted[parent.id] = name
|
|
136
|
+
mac_gaps = recorded - MAC_FORMULAS - mac_free
|
|
137
|
+
flop_gaps: set[str] = set()
|
|
138
|
+
mac_ignored = (mac_free | MAC_COMPOSITES) - mac_gaps
|
|
139
|
+
flop_ignored = FLOP_FREE_OPS | MAC_COMPOSITES | FLOP_COMPOSITES
|
|
140
|
+
for event in events:
|
|
141
|
+
if "::" not in event.name or event.name.startswith(_FORMULA_PREFIX):
|
|
142
|
+
continue
|
|
143
|
+
mac_covered = flop_covered = False
|
|
144
|
+
parent = event
|
|
145
|
+
while parent is not None:
|
|
146
|
+
if parent.id in counted:
|
|
147
|
+
flop_covered = True
|
|
148
|
+
mac_covered |= counted[parent.id] in MAC_FORMULAS
|
|
149
|
+
parent = parent.cpu_parent
|
|
150
|
+
if not mac_covered and event.name not in mac_ignored:
|
|
151
|
+
mac_gaps.add(event.name)
|
|
152
|
+
if not flop_covered and event.name not in flop_ignored:
|
|
153
|
+
flop_gaps.add(event.name)
|
|
154
|
+
return mac_gaps, flop_gaps
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def calculate_model_complexity(
|
|
158
|
+
target: Callable[..., Any],
|
|
159
|
+
input_args: Any,
|
|
160
|
+
input_kwargs: dict[str, Any] | None = None,
|
|
161
|
+
) -> ModelComplexity:
|
|
162
|
+
"""Count eager inference FLOPs, MACs and parameters for one forward.
|
|
163
|
+
|
|
164
|
+
Prepare the model mode and device placement before calling this function.
|
|
165
|
+
Changes made by the forward persist. Pass a separate model instance to
|
|
166
|
+
preserve the original model state.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
target: An eager module or a prepared callable.
|
|
170
|
+
input_args: One positional input or a tuple/list of positional inputs.
|
|
171
|
+
input_kwargs: Optional keyword arguments.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Counts for the supplied batch and executed path, with coverage gaps
|
|
175
|
+
listed in `uncovered_ops`. A plain callable has zero registered parameters.
|
|
176
|
+
"""
|
|
177
|
+
args, kwargs = call_arguments(input_args, input_kwargs)
|
|
178
|
+
params = count_parameters(target) if isinstance(target, nn.Module) else 0
|
|
179
|
+
operations = count_operations(lambda: target(*args, **kwargs))
|
|
180
|
+
return ModelComplexity(operations.macs, params, operations.flops, operations.uncovered_ops)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def count_operations(forward: Callable[[], Any]) -> OperationCounts:
|
|
184
|
+
"""Count the MACs and FLOPs of a prepared eager forward.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
forward: A zero-argument callable that runs the desired forward pass.
|
|
188
|
+
The caller prepares the model and inputs and manages changes
|
|
189
|
+
made by the forward.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
MACs, FLOPs, and coverage gaps. Counting uses math scaled dot-product
|
|
193
|
+
attention and temporarily disables the multihead attention fast path.
|
|
194
|
+
"""
|
|
195
|
+
with torch.inference_mode(False), torch.no_grad():
|
|
196
|
+
# Preserve external non-MAC formulas. The mappings and validation
|
|
197
|
+
# wrappers belong to this invocation, never the global registry.
|
|
198
|
+
arithmetic = arithmetic_formulas()
|
|
199
|
+
additions = {op: formula for op, formula in arithmetic.items() if op not in flop_registry}
|
|
200
|
+
counter = FlopCounterMode(
|
|
201
|
+
display=False,
|
|
202
|
+
custom_mapping={
|
|
203
|
+
**additions,
|
|
204
|
+
torch.ops.aten.dot: dot_flops,
|
|
205
|
+
torch.ops.aten.vdot: dot_flops,
|
|
206
|
+
torch.ops.aten.mv: matrix_vector_flops,
|
|
207
|
+
torch.ops.aten.addmv: biased_matrix_vector_flops,
|
|
208
|
+
torch.ops.aten.addmv_: biased_matrix_vector_flops,
|
|
209
|
+
torch.ops.aten.cudnn_convolution: cudnn_convolution_flops,
|
|
210
|
+
torch.ops.aten._slow_conv2d_forward: slow_conv2d_flops,
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
# Reuse the existing dense formulas for identical operand layouts.
|
|
214
|
+
# Assign functions already wrapped by the registry directly.
|
|
215
|
+
for alias, source in (
|
|
216
|
+
(torch.ops.aten.addmm_, torch.ops.aten.addmm),
|
|
217
|
+
(torch.ops.aten.baddbmm_, torch.ops.aten.baddbmm),
|
|
218
|
+
(torch.ops.aten.addbmm, torch.ops.aten.baddbmm),
|
|
219
|
+
(torch.ops.aten.addbmm_, torch.ops.aten.baddbmm),
|
|
220
|
+
(
|
|
221
|
+
torch.ops.aten._scaled_dot_product_flash_attention_for_cpu,
|
|
222
|
+
torch.ops.aten._scaled_dot_product_flash_attention,
|
|
223
|
+
),
|
|
224
|
+
):
|
|
225
|
+
counter.flop_registry[alias] = counter.flop_registry[source]
|
|
226
|
+
counter.flop_registry[torch.ops.aten._efficient_attention_forward] = (
|
|
227
|
+
efficient_attention_flops
|
|
228
|
+
)
|
|
229
|
+
macs = 0
|
|
230
|
+
uncovered_flops: set[str] = set()
|
|
231
|
+
|
|
232
|
+
def checked_formula(op: object, formula: Callable, added: bool) -> Callable:
|
|
233
|
+
"""Validate each invocation before aggregation and retain its dense MAC count."""
|
|
234
|
+
name = str(op).replace(".", "::", 1)
|
|
235
|
+
|
|
236
|
+
def record(*values: Any, out_val: Any = None, **options: Any) -> int:
|
|
237
|
+
"""Check the formula result and add separately defined non-MAC arithmetic."""
|
|
238
|
+
nonlocal macs
|
|
239
|
+
output_tensors = tensor_leaves(out_val)
|
|
240
|
+
if added and any(t.is_complex() for t in tensor_leaves((values, options))):
|
|
241
|
+
uncovered_flops.add(name)
|
|
242
|
+
return 0
|
|
243
|
+
if added and output_tensors and not output_tensors[0].is_floating_point():
|
|
244
|
+
if output_tensors[0].is_complex():
|
|
245
|
+
uncovered_flops.add(name)
|
|
246
|
+
with record_function(f"{_FORMULA_PREFIX}{name}"):
|
|
247
|
+
pass
|
|
248
|
+
return 0
|
|
249
|
+
value = formula(*values, out_val=out_val, **options)
|
|
250
|
+
if value is None and added:
|
|
251
|
+
uncovered_flops.add(name)
|
|
252
|
+
return 0
|
|
253
|
+
is_mac = name in MAC_FORMULAS
|
|
254
|
+
if type(value) is not int or value < 0 or (is_mac and value % 2):
|
|
255
|
+
expected = "nonnegative even" if is_mac else "nonnegative integer"
|
|
256
|
+
raise ValueError(f"{name}: expected a {expected} FLOP count")
|
|
257
|
+
if (
|
|
258
|
+
name in ("aten::_scaled_mm", "aten::convolution_backward")
|
|
259
|
+
or (name.startswith("aten::") and "attention" in name)
|
|
260
|
+
or name in ("flex_attention", "flex_attention_backward")
|
|
261
|
+
):
|
|
262
|
+
# Native fused formulas omit arithmetic outside their
|
|
263
|
+
# matrix products, such as softmax or bias gradients.
|
|
264
|
+
uncovered_flops.add(name)
|
|
265
|
+
if is_mac:
|
|
266
|
+
macs += value // 2
|
|
267
|
+
value += matrix_extra_flops(name, values, options, out_val)
|
|
268
|
+
if output_tensors and not output_tensors[0].is_floating_point():
|
|
269
|
+
if output_tensors[0].is_complex():
|
|
270
|
+
uncovered_flops.add(name)
|
|
271
|
+
# Count integer and complex MACs as logical products.
|
|
272
|
+
value = 0
|
|
273
|
+
with record_function(f"{_FORMULA_PREFIX}{name}"):
|
|
274
|
+
pass
|
|
275
|
+
return value
|
|
276
|
+
|
|
277
|
+
return record
|
|
278
|
+
|
|
279
|
+
counter.flop_registry = {
|
|
280
|
+
op: checked_formula(op, formula, op in additions)
|
|
281
|
+
for op, formula in counter.flop_registry.items()
|
|
282
|
+
}
|
|
283
|
+
with (
|
|
284
|
+
profile(activities=[ProfilerActivity.CPU]) as trace,
|
|
285
|
+
_counting_attention(),
|
|
286
|
+
counter,
|
|
287
|
+
):
|
|
288
|
+
forward()
|
|
289
|
+
mac_free = (
|
|
290
|
+
MAC_FREE_OPS
|
|
291
|
+
| FLOP_FREE_OPS
|
|
292
|
+
| {str(op).replace("aten.", "aten::", 1) for op in arithmetic}
|
|
293
|
+
)
|
|
294
|
+
uncovered_macs, flop_gaps = _find_coverage_gaps(trace.events(), mac_free)
|
|
295
|
+
uncovered_flops.update(flop_gaps)
|
|
296
|
+
return OperationCounts(
|
|
297
|
+
macs=macs,
|
|
298
|
+
flops=counter.get_total_flops(),
|
|
299
|
+
uncovered_ops={
|
|
300
|
+
"macs": tuple(sorted(uncovered_macs)),
|
|
301
|
+
"flops": tuple(sorted(uncovered_flops)),
|
|
302
|
+
},
|
|
303
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Argument and tensor helpers shared by the measurement APIs."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
from torch.utils._pytree import tree_leaves
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def call_arguments(
|
|
10
|
+
input_args: Any, input_kwargs: dict[str, Any] | None
|
|
11
|
+
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
|
12
|
+
"""Normalize positional and keyword arguments for a forward call."""
|
|
13
|
+
args = tuple(input_args) if isinstance(input_args, (tuple, list)) else (input_args,)
|
|
14
|
+
return args, {} if input_kwargs is None else input_kwargs
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def tensor_leaves(value: Any) -> list[torch.Tensor]:
|
|
18
|
+
"""Find the tensors in a nested value."""
|
|
19
|
+
return [leaf for leaf in tree_leaves(value) if isinstance(leaf, torch.Tensor)]
|
torch_prisma/formulas.py
ADDED
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
"""Shape formulas for nominal real arithmetic, independent of pruning."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from torch.utils.flop_counter import flop_registry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_native_pointwise(name: str) -> bool:
|
|
12
|
+
"""Identify ATen packets whose overloads all declare pointwise semantics.
|
|
13
|
+
|
|
14
|
+
Pointwise work is outside the dense matrix/convolution MAC convention.
|
|
15
|
+
These tags do not determine FLOPs. Require every overload to carry the tag
|
|
16
|
+
because profiler names identify packets rather than individual overloads.
|
|
17
|
+
"""
|
|
18
|
+
if not name.startswith("aten::"):
|
|
19
|
+
return False
|
|
20
|
+
packet = getattr(torch.ops.aten, name.removeprefix("aten::"), None)
|
|
21
|
+
if packet is None or not callable(getattr(packet, "overloads", None)):
|
|
22
|
+
return False
|
|
23
|
+
overloads = packet.overloads()
|
|
24
|
+
return bool(overloads) and all(
|
|
25
|
+
torch.Tag.pointwise in getattr(packet, overload).tags for overload in overloads
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def elementwise_flops(*args: Any, out_shape: tuple[int, ...], **kwargs: Any) -> int:
|
|
30
|
+
"""Count one arithmetic operation per output element, including broadcasting."""
|
|
31
|
+
return math.prod(out_shape)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def add_flops(*args: Any, out_shape: tuple[int, ...], alpha: float = 1, **kwargs: Any) -> int:
|
|
35
|
+
"""Count addition and an explicit nonunit scale of the second operand."""
|
|
36
|
+
return math.prod(out_shape) * (1 + (alpha != 1))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def add_product_flops(
|
|
40
|
+
*args: Any, out_shape: tuple[int, ...], value: float = 1, **kwargs: Any
|
|
41
|
+
) -> int:
|
|
42
|
+
"""Count addcmul/addcdiv arithmetic and an optional nonunit scalar scale."""
|
|
43
|
+
return math.prod(out_shape) * (2 + (value != 1))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def sum_flops(input: tuple[int, ...], *args: Any, out_shape: tuple[int, ...], **kwargs: Any) -> int:
|
|
47
|
+
"""Count one fewer addition than input elements in each reduction group."""
|
|
48
|
+
return max(0, math.prod(input) - math.prod(out_shape))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def mean_flops(
|
|
52
|
+
input: tuple[int, ...], *args: Any, out_shape: tuple[int, ...], **kwargs: Any
|
|
53
|
+
) -> int:
|
|
54
|
+
"""Count reduction additions and one division per output."""
|
|
55
|
+
return sum_flops(input, out_shape=out_shape) + math.prod(out_shape)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def softmax_flops(input: tuple[int, ...], dim: int, *args: Any, **kwargs: Any) -> int:
|
|
59
|
+
"""Count stable softmax arithmetic, excluding maximum comparisons."""
|
|
60
|
+
size = input[dim] if input else 1
|
|
61
|
+
total = math.prod(input)
|
|
62
|
+
return 4 * total - total // size if size else 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def log_softmax_flops(input: tuple[int, ...], *args: Any, **kwargs: Any) -> int:
|
|
66
|
+
"""Count stable log-softmax arithmetic with one unit per exp/log."""
|
|
67
|
+
return 4 * math.prod(input)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def normalization_flops(total: int, groups: int, weight: object, bias: object) -> int:
|
|
71
|
+
"""Count centered variance with reused centered values and optional affine work."""
|
|
72
|
+
return 5 * total + 2 * groups + total * ((weight is not None) + (bias is not None))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def layer_norm_flops(
|
|
76
|
+
input: tuple[int, ...],
|
|
77
|
+
normalized_shape: tuple[int, ...],
|
|
78
|
+
weight: object,
|
|
79
|
+
bias: object,
|
|
80
|
+
*args: Any,
|
|
81
|
+
**kwargs: Any,
|
|
82
|
+
) -> int | None:
|
|
83
|
+
"""Count mean, centered variance, epsilon, rsqrt and affine arithmetic."""
|
|
84
|
+
width = math.prod(normalized_shape)
|
|
85
|
+
return (
|
|
86
|
+
normalization_flops(math.prod(input), math.prod(input) // width, weight, bias)
|
|
87
|
+
if width
|
|
88
|
+
else None
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def group_norm_flops(
|
|
93
|
+
input: tuple[int, ...],
|
|
94
|
+
weight: object,
|
|
95
|
+
bias: object,
|
|
96
|
+
N: int,
|
|
97
|
+
C: int,
|
|
98
|
+
HxW: int,
|
|
99
|
+
group: int,
|
|
100
|
+
*args: Any,
|
|
101
|
+
**kwargs: Any,
|
|
102
|
+
) -> int:
|
|
103
|
+
"""Count one normalization domain per sample and group."""
|
|
104
|
+
return normalization_flops(math.prod(input), N * group, weight, bias)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def batch_norm_eval_flops(
|
|
108
|
+
input: tuple[int, ...],
|
|
109
|
+
weight: object,
|
|
110
|
+
bias: object,
|
|
111
|
+
*args: Any,
|
|
112
|
+
**kwargs: Any,
|
|
113
|
+
) -> int:
|
|
114
|
+
"""Count inference normalization from stored statistics, without algebraic folding."""
|
|
115
|
+
total = math.prod(input)
|
|
116
|
+
return 2 * input[1] + total * (2 + (weight is not None) + (bias is not None))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def batch_norm_flops(
|
|
120
|
+
input: tuple[int, ...],
|
|
121
|
+
weight: object,
|
|
122
|
+
bias: object,
|
|
123
|
+
running_mean: object = None,
|
|
124
|
+
running_var: object = None,
|
|
125
|
+
training: bool = True,
|
|
126
|
+
*args: Any,
|
|
127
|
+
**kwargs: Any,
|
|
128
|
+
) -> int | None:
|
|
129
|
+
"""Count inference with stored statistics and flag training updates as uncovered."""
|
|
130
|
+
# The no_stats overload places training/momentum/eps after bias. Those
|
|
131
|
+
# arguments are not running-statistic shapes and are outside this formula.
|
|
132
|
+
if training or not isinstance(running_mean, tuple) or not isinstance(running_var, tuple):
|
|
133
|
+
return None
|
|
134
|
+
return batch_norm_eval_flops(input, weight, bias)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def dot_flops(a: tuple[int, ...], b: tuple[int, ...], **kwargs: Any) -> int:
|
|
138
|
+
"""Count a vector contraction under the dense two-FLOPs-per-MAC convention."""
|
|
139
|
+
return 2 * a[0]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def matrix_vector_flops(matrix: tuple[int, ...], vector: tuple[int, ...], **kwargs: Any) -> int:
|
|
143
|
+
"""Count matrix-vector work without bias additions or scalar scaling."""
|
|
144
|
+
return 2 * math.prod(matrix)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def biased_matrix_vector_flops(
|
|
148
|
+
bias: tuple[int, ...], matrix: tuple[int, ...], vector: tuple[int, ...], **kwargs: Any
|
|
149
|
+
) -> int:
|
|
150
|
+
"""Use the same dense matrix-vector formula for the biased form."""
|
|
151
|
+
return matrix_vector_flops(matrix, vector)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def conv_flops(input: tuple[int, ...], weight: tuple[int, ...], output: tuple[int, ...]) -> int:
|
|
155
|
+
"""Count ordinary convolution products, including implicit padding positions."""
|
|
156
|
+
return 2 * input[0] * math.prod(weight) * math.prod(output[2:])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def cudnn_convolution_flops(
|
|
160
|
+
input: tuple[int, ...],
|
|
161
|
+
weight: tuple[int, ...],
|
|
162
|
+
padding: object,
|
|
163
|
+
stride: object,
|
|
164
|
+
dilation: object,
|
|
165
|
+
groups: int,
|
|
166
|
+
benchmark: bool,
|
|
167
|
+
deterministic: bool,
|
|
168
|
+
allow_tf32: bool,
|
|
169
|
+
*,
|
|
170
|
+
out_shape: tuple[int, ...],
|
|
171
|
+
**kwargs: Any,
|
|
172
|
+
) -> int:
|
|
173
|
+
"""Bind cuDNN's distinct signature without interpreting benchmark as transposed."""
|
|
174
|
+
return conv_flops(input, weight, out_shape)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def slow_conv2d_flops(
|
|
178
|
+
input: tuple[int, ...],
|
|
179
|
+
weight: tuple[int, ...],
|
|
180
|
+
kernel_size: object,
|
|
181
|
+
bias: object,
|
|
182
|
+
stride: object,
|
|
183
|
+
padding: object,
|
|
184
|
+
*,
|
|
185
|
+
out_shape: tuple[int, ...],
|
|
186
|
+
**kwargs: Any,
|
|
187
|
+
) -> int:
|
|
188
|
+
"""Bind the six-argument CPU convolution signature."""
|
|
189
|
+
return conv_flops(input, weight, out_shape)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def efficient_attention_flops(
|
|
193
|
+
query: torch.Tensor,
|
|
194
|
+
key: torch.Tensor,
|
|
195
|
+
value: torch.Tensor,
|
|
196
|
+
bias: torch.Tensor | None,
|
|
197
|
+
cu_seqlens_q: torch.Tensor | None,
|
|
198
|
+
cu_seqlens_k: torch.Tensor | None,
|
|
199
|
+
*args: Any,
|
|
200
|
+
**kwargs: Any,
|
|
201
|
+
) -> int:
|
|
202
|
+
"""Count dense BMHD attention and reuse the native variable-length formula.
|
|
203
|
+
|
|
204
|
+
This raw kernel uses batch, sequence, heads, and width order. The public
|
|
205
|
+
SDPA kernels use batch, heads, sequence, and width order instead.
|
|
206
|
+
"""
|
|
207
|
+
if cu_seqlens_q is not None or cu_seqlens_k is not None:
|
|
208
|
+
return flop_registry[torch.ops.aten._efficient_attention_forward](
|
|
209
|
+
query, key, value, bias, cu_seqlens_q, cu_seqlens_k, *args, **kwargs
|
|
210
|
+
)
|
|
211
|
+
batch, queries, heads, width = query.shape
|
|
212
|
+
return 2 * batch * heads * queries * key.shape[1] * (width + value.shape[-1])
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def arithmetic_formulas() -> dict[object, Callable[..., int | None]]:
|
|
216
|
+
"""Return local additions to the counter, without changing its global registry.
|
|
217
|
+
|
|
218
|
+
None means the formula does not cover this invocation. Comparisons and
|
|
219
|
+
data movement do not contribute floating-point arithmetic.
|
|
220
|
+
"""
|
|
221
|
+
result = {
|
|
222
|
+
getattr(torch.ops.aten, name): elementwise_flops
|
|
223
|
+
for name in (
|
|
224
|
+
"mul",
|
|
225
|
+
"mul_",
|
|
226
|
+
"div",
|
|
227
|
+
"div_",
|
|
228
|
+
"divide",
|
|
229
|
+
"divide_",
|
|
230
|
+
"true_divide",
|
|
231
|
+
"true_divide_",
|
|
232
|
+
"multiply",
|
|
233
|
+
"multiply_",
|
|
234
|
+
"neg",
|
|
235
|
+
"neg_",
|
|
236
|
+
"negative",
|
|
237
|
+
"negative_",
|
|
238
|
+
"abs",
|
|
239
|
+
"abs_",
|
|
240
|
+
"absolute",
|
|
241
|
+
"absolute_",
|
|
242
|
+
"sqrt",
|
|
243
|
+
"sqrt_",
|
|
244
|
+
"rsqrt",
|
|
245
|
+
"rsqrt_",
|
|
246
|
+
"exp",
|
|
247
|
+
"exp_",
|
|
248
|
+
"log",
|
|
249
|
+
"log_",
|
|
250
|
+
"reciprocal",
|
|
251
|
+
"reciprocal_",
|
|
252
|
+
"square",
|
|
253
|
+
"square_",
|
|
254
|
+
"pow",
|
|
255
|
+
"pow_",
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
result.update(
|
|
259
|
+
{
|
|
260
|
+
torch.ops.aten.add: add_flops,
|
|
261
|
+
torch.ops.aten.add_: add_flops,
|
|
262
|
+
torch.ops.aten.sub: add_flops,
|
|
263
|
+
torch.ops.aten.sub_: add_flops,
|
|
264
|
+
torch.ops.aten.subtract: add_flops,
|
|
265
|
+
torch.ops.aten.subtract_: add_flops,
|
|
266
|
+
torch.ops.aten.rsub: add_flops,
|
|
267
|
+
torch.ops.aten.addcmul: add_product_flops,
|
|
268
|
+
torch.ops.aten.addcmul_: add_product_flops,
|
|
269
|
+
torch.ops.aten.addcdiv: add_product_flops,
|
|
270
|
+
torch.ops.aten.addcdiv_: add_product_flops,
|
|
271
|
+
torch.ops.aten.sum: sum_flops,
|
|
272
|
+
torch.ops.aten.mean: mean_flops,
|
|
273
|
+
torch.ops.aten._softmax: softmax_flops,
|
|
274
|
+
torch.ops.aten._safe_softmax: softmax_flops,
|
|
275
|
+
torch.ops.aten._log_softmax: log_softmax_flops,
|
|
276
|
+
torch.ops.aten.native_layer_norm: layer_norm_flops,
|
|
277
|
+
torch.ops.aten.native_group_norm: group_norm_flops,
|
|
278
|
+
torch.ops.aten._native_batch_norm_legit_no_training: batch_norm_eval_flops,
|
|
279
|
+
torch.ops.aten.native_batch_norm: batch_norm_flops,
|
|
280
|
+
torch.ops.aten._native_batch_norm_legit: batch_norm_flops,
|
|
281
|
+
torch.ops.aten.cudnn_batch_norm: batch_norm_flops,
|
|
282
|
+
}
|
|
283
|
+
)
|
|
284
|
+
return result
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def matrix_extra_flops(op: str, args: tuple[Any, ...], kwargs: dict[str, Any], output: Any) -> int:
|
|
288
|
+
"""Count fused bias and scalar scaling separately from the dense MAC formula."""
|
|
289
|
+
if op in (
|
|
290
|
+
"aten::addmm",
|
|
291
|
+
"aten::addmm_",
|
|
292
|
+
"aten::baddbmm",
|
|
293
|
+
"aten::baddbmm_",
|
|
294
|
+
"aten::addmv",
|
|
295
|
+
"aten::addmv_",
|
|
296
|
+
"aten::addbmm",
|
|
297
|
+
"aten::addbmm_",
|
|
298
|
+
):
|
|
299
|
+
beta, alpha = kwargs.get("beta", 1), kwargs.get("alpha", 1)
|
|
300
|
+
return output.numel() * ((beta != 0) + (beta not in (0, 1)) + (alpha != 1))
|
|
301
|
+
positions = {
|
|
302
|
+
"aten::convolution": 2,
|
|
303
|
+
"aten::_convolution": 2,
|
|
304
|
+
"aten::convolution_overrideable": 2,
|
|
305
|
+
"aten::_slow_conv2d_forward": 3,
|
|
306
|
+
}
|
|
307
|
+
if op in positions:
|
|
308
|
+
bias = args[positions[op]] if len(args) > positions[op] else kwargs.get("bias")
|
|
309
|
+
return output.numel() if bias is not None else 0
|
|
310
|
+
return 0
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# Data movement, indexing, comparisons and selection are free under this
|
|
314
|
+
# arithmetic convention.
|
|
315
|
+
FLOP_FREE_OPS = frozenset(
|
|
316
|
+
f"aten::{name}"
|
|
317
|
+
for name in (
|
|
318
|
+
"t",
|
|
319
|
+
"transpose",
|
|
320
|
+
"permute",
|
|
321
|
+
"view",
|
|
322
|
+
"_unsafe_view",
|
|
323
|
+
"reshape",
|
|
324
|
+
"flatten",
|
|
325
|
+
"unflatten",
|
|
326
|
+
"unsqueeze",
|
|
327
|
+
"squeeze",
|
|
328
|
+
"squeeze_",
|
|
329
|
+
"unsqueeze_",
|
|
330
|
+
"expand",
|
|
331
|
+
"expand_as",
|
|
332
|
+
"repeat",
|
|
333
|
+
"repeat_interleave",
|
|
334
|
+
"slice",
|
|
335
|
+
"select",
|
|
336
|
+
"narrow",
|
|
337
|
+
"split",
|
|
338
|
+
"split_with_sizes",
|
|
339
|
+
"chunk",
|
|
340
|
+
"cat",
|
|
341
|
+
"stack",
|
|
342
|
+
"unbind",
|
|
343
|
+
"as_strided",
|
|
344
|
+
"as_strided_",
|
|
345
|
+
"contiguous",
|
|
346
|
+
"clone",
|
|
347
|
+
"copy_",
|
|
348
|
+
"detach",
|
|
349
|
+
"alias",
|
|
350
|
+
"to",
|
|
351
|
+
"_to_copy",
|
|
352
|
+
"type_as",
|
|
353
|
+
"result_type",
|
|
354
|
+
"resolve_conj",
|
|
355
|
+
"resolve_neg",
|
|
356
|
+
"empty",
|
|
357
|
+
"empty_like",
|
|
358
|
+
"empty_strided",
|
|
359
|
+
"zeros",
|
|
360
|
+
"zeros_like",
|
|
361
|
+
"ones",
|
|
362
|
+
"ones_like",
|
|
363
|
+
"full",
|
|
364
|
+
"full_like",
|
|
365
|
+
"fill_",
|
|
366
|
+
"zero_",
|
|
367
|
+
"resize_",
|
|
368
|
+
"arange",
|
|
369
|
+
"scalar_tensor",
|
|
370
|
+
"lift_fresh",
|
|
371
|
+
"embedding",
|
|
372
|
+
"index",
|
|
373
|
+
"index_select",
|
|
374
|
+
"gather",
|
|
375
|
+
"masked_fill",
|
|
376
|
+
"masked_fill_",
|
|
377
|
+
"triu",
|
|
378
|
+
"tril",
|
|
379
|
+
"_nnpack_available",
|
|
380
|
+
"_local_scalar_dense",
|
|
381
|
+
"item",
|
|
382
|
+
"is_nonzero",
|
|
383
|
+
"_reshape_alias",
|
|
384
|
+
"_autocast_to_reduced_precision",
|
|
385
|
+
"_autocast_to_full_precision",
|
|
386
|
+
"eq",
|
|
387
|
+
"ne",
|
|
388
|
+
"lt",
|
|
389
|
+
"le",
|
|
390
|
+
"gt",
|
|
391
|
+
"ge",
|
|
392
|
+
"all",
|
|
393
|
+
"any",
|
|
394
|
+
"logical_not",
|
|
395
|
+
"logical_and",
|
|
396
|
+
"where",
|
|
397
|
+
"isneginf",
|
|
398
|
+
"isinf",
|
|
399
|
+
"isnan",
|
|
400
|
+
"relu",
|
|
401
|
+
"relu_",
|
|
402
|
+
"clamp",
|
|
403
|
+
"clamp_min",
|
|
404
|
+
"clamp_min_",
|
|
405
|
+
"amax",
|
|
406
|
+
"amin",
|
|
407
|
+
"max",
|
|
408
|
+
"min",
|
|
409
|
+
# Forward max pooling selects values and indices. Comparisons are free
|
|
410
|
+
# under this convention. Backward scatter/add kernels require separate rules.
|
|
411
|
+
"max_pool1d",
|
|
412
|
+
"max_pool2d",
|
|
413
|
+
"max_pool3d",
|
|
414
|
+
"max_pool1d_with_indices",
|
|
415
|
+
"max_pool2d_with_indices",
|
|
416
|
+
"max_pool3d_with_indices",
|
|
417
|
+
"adaptive_max_pool1d",
|
|
418
|
+
"adaptive_max_pool2d",
|
|
419
|
+
"adaptive_max_pool3d",
|
|
420
|
+
# Coordinate transforms and padding only copy, select, or fill values.
|
|
421
|
+
"pixel_shuffle",
|
|
422
|
+
"pixel_unshuffle",
|
|
423
|
+
"channel_shuffle",
|
|
424
|
+
"native_channel_shuffle",
|
|
425
|
+
"unfold",
|
|
426
|
+
"im2col",
|
|
427
|
+
"max_unpool2d",
|
|
428
|
+
"max_unpool3d",
|
|
429
|
+
"pad",
|
|
430
|
+
"constant_pad_nd",
|
|
431
|
+
"_pad_circular",
|
|
432
|
+
"reflection_pad1d",
|
|
433
|
+
"reflection_pad2d",
|
|
434
|
+
"reflection_pad3d",
|
|
435
|
+
"replication_pad1d",
|
|
436
|
+
"replication_pad2d",
|
|
437
|
+
"replication_pad3d",
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
FLOP_COMPOSITES = frozenset(
|
|
442
|
+
f"aten::{name}"
|
|
443
|
+
for name in (
|
|
444
|
+
"softmax",
|
|
445
|
+
"_safe_softmax",
|
|
446
|
+
"square",
|
|
447
|
+
"square_",
|
|
448
|
+
"absolute",
|
|
449
|
+
"absolute_",
|
|
450
|
+
"negative",
|
|
451
|
+
"negative_",
|
|
452
|
+
"multiply",
|
|
453
|
+
"multiply_",
|
|
454
|
+
"subtract",
|
|
455
|
+
"subtract_",
|
|
456
|
+
"divide",
|
|
457
|
+
"divide_",
|
|
458
|
+
"true_divide",
|
|
459
|
+
"true_divide_",
|
|
460
|
+
"log_softmax",
|
|
461
|
+
"batch_norm",
|
|
462
|
+
"_batch_norm_impl_index",
|
|
463
|
+
"native_batch_norm",
|
|
464
|
+
"layer_norm",
|
|
465
|
+
"group_norm",
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# Only these audited forward formulas use two FLOPs per dense MAC. Do not
|
|
471
|
+
# convert the counter's total: its registry also accepts arbitrary FLOP formulas.
|
|
472
|
+
MAC_FORMULAS = frozenset(
|
|
473
|
+
f"aten::{name}"
|
|
474
|
+
for name in (
|
|
475
|
+
"mm",
|
|
476
|
+
"addmm",
|
|
477
|
+
"addmm_",
|
|
478
|
+
"bmm",
|
|
479
|
+
"baddbmm",
|
|
480
|
+
"baddbmm_",
|
|
481
|
+
"addbmm",
|
|
482
|
+
"addbmm_",
|
|
483
|
+
"dot",
|
|
484
|
+
"vdot",
|
|
485
|
+
"mv",
|
|
486
|
+
"addmv",
|
|
487
|
+
"addmv_",
|
|
488
|
+
"_scaled_mm",
|
|
489
|
+
"convolution",
|
|
490
|
+
"_convolution",
|
|
491
|
+
"cudnn_convolution",
|
|
492
|
+
"_slow_conv2d_forward",
|
|
493
|
+
"convolution_overrideable",
|
|
494
|
+
"_scaled_dot_product_efficient_attention",
|
|
495
|
+
"_scaled_dot_product_flash_attention",
|
|
496
|
+
"_scaled_dot_product_cudnn_attention",
|
|
497
|
+
"_scaled_dot_product_flash_attention_for_cpu",
|
|
498
|
+
"_flash_attention_forward",
|
|
499
|
+
"_efficient_attention_forward",
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# These wrappers contribute through their constituent matrix/conv calls, not
|
|
504
|
+
# through formulas registered directly on the wrappers.
|
|
505
|
+
MAC_COMPOSITES = frozenset(
|
|
506
|
+
f"aten::{name}"
|
|
507
|
+
for name in (
|
|
508
|
+
"linear",
|
|
509
|
+
"matmul",
|
|
510
|
+
"einsum",
|
|
511
|
+
"conv1d",
|
|
512
|
+
"conv2d",
|
|
513
|
+
"conv3d",
|
|
514
|
+
"_convolution_mode",
|
|
515
|
+
"conv_transpose1d",
|
|
516
|
+
"conv_transpose2d",
|
|
517
|
+
"conv_transpose3d",
|
|
518
|
+
"scaled_dot_product_attention",
|
|
519
|
+
"_scaled_dot_product_attention_math",
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# Explicit exclusions under the matrix/conv MAC convention. Unfamiliar kernels
|
|
524
|
+
# must appear in uncovered_ops["macs"], not silently become zero-cost operations.
|
|
525
|
+
MAC_FREE_OPS = frozenset(
|
|
526
|
+
f"aten::{name}"
|
|
527
|
+
for name in (
|
|
528
|
+
"add",
|
|
529
|
+
"add_",
|
|
530
|
+
"sub",
|
|
531
|
+
"mul",
|
|
532
|
+
"div",
|
|
533
|
+
"div_",
|
|
534
|
+
"neg",
|
|
535
|
+
"pow",
|
|
536
|
+
"sqrt",
|
|
537
|
+
"rsqrt",
|
|
538
|
+
"abs",
|
|
539
|
+
"exp",
|
|
540
|
+
"log",
|
|
541
|
+
"tanh",
|
|
542
|
+
"sigmoid",
|
|
543
|
+
"relu",
|
|
544
|
+
"relu_",
|
|
545
|
+
"gelu",
|
|
546
|
+
"silu",
|
|
547
|
+
"clamp_min",
|
|
548
|
+
"clamp_min_",
|
|
549
|
+
"clamp",
|
|
550
|
+
"where",
|
|
551
|
+
"eq",
|
|
552
|
+
"ne",
|
|
553
|
+
"lt",
|
|
554
|
+
"le",
|
|
555
|
+
"gt",
|
|
556
|
+
"ge",
|
|
557
|
+
"all",
|
|
558
|
+
"any",
|
|
559
|
+
"logical_not",
|
|
560
|
+
"logical_and",
|
|
561
|
+
"softmax",
|
|
562
|
+
"_softmax",
|
|
563
|
+
"_safe_softmax",
|
|
564
|
+
"log_softmax",
|
|
565
|
+
"_log_softmax",
|
|
566
|
+
"batch_norm",
|
|
567
|
+
"_batch_norm_impl_index",
|
|
568
|
+
"_native_batch_norm_legit",
|
|
569
|
+
"_native_batch_norm_legit_no_training",
|
|
570
|
+
"native_batch_norm",
|
|
571
|
+
"cudnn_batch_norm",
|
|
572
|
+
"miopen_batch_norm",
|
|
573
|
+
"layer_norm",
|
|
574
|
+
"native_layer_norm",
|
|
575
|
+
"group_norm",
|
|
576
|
+
"native_group_norm",
|
|
577
|
+
"avg_pool1d",
|
|
578
|
+
"avg_pool2d",
|
|
579
|
+
"avg_pool3d",
|
|
580
|
+
"adaptive_avg_pool1d",
|
|
581
|
+
"adaptive_avg_pool2d",
|
|
582
|
+
"adaptive_avg_pool3d",
|
|
583
|
+
"_adaptive_avg_pool2d",
|
|
584
|
+
"_adaptive_avg_pool3d",
|
|
585
|
+
"max_pool1d",
|
|
586
|
+
"max_pool2d",
|
|
587
|
+
"max_pool3d",
|
|
588
|
+
"max_pool1d_with_indices",
|
|
589
|
+
"max_pool2d_with_indices",
|
|
590
|
+
"max_pool3d_with_indices",
|
|
591
|
+
"adaptive_max_pool1d",
|
|
592
|
+
"adaptive_max_pool2d",
|
|
593
|
+
"adaptive_max_pool3d",
|
|
594
|
+
"mean",
|
|
595
|
+
"sum",
|
|
596
|
+
"amax",
|
|
597
|
+
"amin",
|
|
598
|
+
"max",
|
|
599
|
+
"min",
|
|
600
|
+
"t",
|
|
601
|
+
"transpose",
|
|
602
|
+
"permute",
|
|
603
|
+
"view",
|
|
604
|
+
"_unsafe_view",
|
|
605
|
+
"reshape",
|
|
606
|
+
"flatten",
|
|
607
|
+
"unflatten",
|
|
608
|
+
"unsqueeze",
|
|
609
|
+
"squeeze",
|
|
610
|
+
"squeeze_",
|
|
611
|
+
"unsqueeze_",
|
|
612
|
+
"lgamma",
|
|
613
|
+
"special_gammaln",
|
|
614
|
+
"expand",
|
|
615
|
+
"expand_as",
|
|
616
|
+
"repeat",
|
|
617
|
+
"repeat_interleave",
|
|
618
|
+
"slice",
|
|
619
|
+
"select",
|
|
620
|
+
"narrow",
|
|
621
|
+
"split",
|
|
622
|
+
"split_with_sizes",
|
|
623
|
+
"chunk",
|
|
624
|
+
"cat",
|
|
625
|
+
"stack",
|
|
626
|
+
"unbind",
|
|
627
|
+
"as_strided",
|
|
628
|
+
"as_strided_",
|
|
629
|
+
"contiguous",
|
|
630
|
+
"clone",
|
|
631
|
+
"copy_",
|
|
632
|
+
"detach",
|
|
633
|
+
"alias",
|
|
634
|
+
"to",
|
|
635
|
+
"_to_copy",
|
|
636
|
+
"type_as",
|
|
637
|
+
"resolve_conj",
|
|
638
|
+
"resolve_neg",
|
|
639
|
+
"empty",
|
|
640
|
+
"empty_like",
|
|
641
|
+
"empty_strided",
|
|
642
|
+
"zeros",
|
|
643
|
+
"zeros_like",
|
|
644
|
+
"ones",
|
|
645
|
+
"ones_like",
|
|
646
|
+
"full",
|
|
647
|
+
"full_like",
|
|
648
|
+
"fill_",
|
|
649
|
+
"zero_",
|
|
650
|
+
"resize_",
|
|
651
|
+
"arange",
|
|
652
|
+
"scalar_tensor",
|
|
653
|
+
"lift_fresh",
|
|
654
|
+
"dropout",
|
|
655
|
+
"native_dropout",
|
|
656
|
+
"embedding",
|
|
657
|
+
"index",
|
|
658
|
+
"index_select",
|
|
659
|
+
"gather",
|
|
660
|
+
"masked_fill",
|
|
661
|
+
"isneginf",
|
|
662
|
+
"isinf",
|
|
663
|
+
"isnan",
|
|
664
|
+
"masked_fill_",
|
|
665
|
+
"triu",
|
|
666
|
+
"tril",
|
|
667
|
+
"_nnpack_available",
|
|
668
|
+
"_local_scalar_dense",
|
|
669
|
+
"item",
|
|
670
|
+
"is_nonzero",
|
|
671
|
+
"_reshape_alias",
|
|
672
|
+
"_autocast_to_reduced_precision",
|
|
673
|
+
"_autocast_to_full_precision",
|
|
674
|
+
# Fold adds overlapping patches but performs no dense matrix product.
|
|
675
|
+
"col2im",
|
|
676
|
+
)
|
|
677
|
+
)
|
torch_prisma/latency.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Eager and compiled inference latency on CPU, CUDA and Ascend."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import gc
|
|
5
|
+
import math
|
|
6
|
+
import statistics
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
9
|
+
from contextlib import AbstractContextManager, ExitStack, contextmanager
|
|
10
|
+
from dataclasses import asdict, dataclass, field
|
|
11
|
+
from importlib import import_module
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import torch
|
|
16
|
+
|
|
17
|
+
from .execution import call_arguments
|
|
18
|
+
|
|
19
|
+
_COMPILE_ITERS = 3
|
|
20
|
+
_UNLIMITED_RECOMPILES = 2**31 - 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class LatencyStats:
|
|
25
|
+
"""Summarize forward latency in milliseconds.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
median_ms: Median sample duration.
|
|
29
|
+
mean_ms: Arithmetic mean of the sample durations.
|
|
30
|
+
std_ms: Population standard deviation of the sample durations.
|
|
31
|
+
p90_ms: 90th percentile selected from the measured samples.
|
|
32
|
+
p99_ms: 99th percentile selected from the measured samples.
|
|
33
|
+
min_ms: Shortest sample duration.
|
|
34
|
+
max_ms: Longest sample duration.
|
|
35
|
+
samples_ms: Sample durations in measurement order.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
median_ms: float
|
|
39
|
+
mean_ms: float
|
|
40
|
+
std_ms: float
|
|
41
|
+
p90_ms: float
|
|
42
|
+
p99_ms: float
|
|
43
|
+
min_ms: float
|
|
44
|
+
max_ms: float
|
|
45
|
+
samples_ms: tuple[float, ...] = field(repr=False)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_samples(cls, samples_ms: Iterable[float]) -> "LatencyStats":
|
|
49
|
+
"""Summarize timings in milliseconds.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
samples_ms: A nonempty iterable of finite, nonnegative durations.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Latency statistics and the original sample sequence.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
ValueError: The sequence is empty or contains an invalid duration.
|
|
59
|
+
"""
|
|
60
|
+
samples = tuple(float(sample) for sample in samples_ms)
|
|
61
|
+
if not samples or any(not math.isfinite(sample) or sample < 0 for sample in samples):
|
|
62
|
+
raise ValueError("Latency samples must be nonempty, finite and nonnegative")
|
|
63
|
+
ordered = sorted(samples)
|
|
64
|
+
count = len(ordered)
|
|
65
|
+
return cls(
|
|
66
|
+
median_ms=statistics.median(samples),
|
|
67
|
+
mean_ms=statistics.fmean(samples),
|
|
68
|
+
std_ms=statistics.pstdev(samples),
|
|
69
|
+
p90_ms=ordered[min(int(count * 0.90), count - 1)],
|
|
70
|
+
p99_ms=ordered[min(int(count * 0.99), count - 1)],
|
|
71
|
+
min_ms=ordered[0],
|
|
72
|
+
max_ms=ordered[-1],
|
|
73
|
+
samples_ms=samples,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def to_dict(self) -> dict[str, Any]:
|
|
77
|
+
"""Return statistics and samples as JSON-ready data."""
|
|
78
|
+
result = asdict(self)
|
|
79
|
+
result["samples_ms"] = list(self.samples_ms)
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _timing_device(requested: torch.device | str) -> tuple[torch.device, ModuleType | None]:
|
|
84
|
+
"""Resolve the device and runtime used to record timing events."""
|
|
85
|
+
if isinstance(requested, str) and requested.split(":", 1)[0] == "npu":
|
|
86
|
+
import_module("torch_npu")
|
|
87
|
+
device = torch.device(requested)
|
|
88
|
+
if device.type == "cpu":
|
|
89
|
+
return device, None
|
|
90
|
+
if device.type not in ("cuda", "npu"):
|
|
91
|
+
raise ValueError(f"Unsupported timing device {device}. Expected CPU, CUDA or Ascend NPU")
|
|
92
|
+
runtime = getattr(torch, device.type)
|
|
93
|
+
if device.index is None:
|
|
94
|
+
device = torch.device(device.type, runtime.current_device())
|
|
95
|
+
return device, runtime
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _unlimited_recompile_patch() -> AbstractContextManager[None]:
|
|
99
|
+
"""Raise Dynamo's recompile caps during the compilation forwards."""
|
|
100
|
+
config = torch._dynamo.config
|
|
101
|
+
# PyTorch 2.6 uses the cache_size_limit names.
|
|
102
|
+
overrides = {
|
|
103
|
+
name if hasattr(config, name) else legacy: _UNLIMITED_RECOMPILES
|
|
104
|
+
for name, legacy in (
|
|
105
|
+
("recompile_limit", "cache_size_limit"),
|
|
106
|
+
("accumulated_recompile_limit", "accumulated_cache_size_limit"),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
return config.patch(**overrides)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@contextmanager
|
|
113
|
+
def _fail_on_late_compile() -> Iterator[None]:
|
|
114
|
+
"""Reject compilation during warmup or timing."""
|
|
115
|
+
with torch.compiler.set_stance("fail_on_recompile"):
|
|
116
|
+
try:
|
|
117
|
+
yield
|
|
118
|
+
except RuntimeError as error:
|
|
119
|
+
if not str(error).startswith("Detected recompile when torch.compile stance"):
|
|
120
|
+
raise
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
f"PyTorch requested compilation after {_COMPILE_ITERS} preparation forwards. "
|
|
123
|
+
"The compiled latency measurement would be unreliable. "
|
|
124
|
+
"Set compile=False to measure eager latency."
|
|
125
|
+
) from error
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@functools.cache
|
|
129
|
+
def _default_npu_backend() -> Callable[..., Any]:
|
|
130
|
+
"""Load and reuse the default TorchAir backend."""
|
|
131
|
+
try:
|
|
132
|
+
torchair = import_module("torchair")
|
|
133
|
+
except ModuleNotFoundError as error:
|
|
134
|
+
if error.name != "torchair":
|
|
135
|
+
raise
|
|
136
|
+
raise RuntimeError(
|
|
137
|
+
"Compiled Ascend latency requires a compatible TorchAir installation"
|
|
138
|
+
) from error
|
|
139
|
+
return torchair.get_npu_backend()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _compile_options(device: torch.device, options: dict[str, Any] | None) -> dict[str, Any]:
|
|
143
|
+
"""Select TorchAir for Ascend unless the caller supplied a backend."""
|
|
144
|
+
options = dict(options or {})
|
|
145
|
+
if device.type == "npu" and "backend" not in options:
|
|
146
|
+
options["backend"] = _default_npu_backend()
|
|
147
|
+
return options
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def measure_module_latency(
|
|
151
|
+
target: Callable[..., Any],
|
|
152
|
+
input_args: Any,
|
|
153
|
+
device: torch.device | str,
|
|
154
|
+
input_kwargs: dict[str, Any] | None = None,
|
|
155
|
+
*,
|
|
156
|
+
repetitions: int = 20,
|
|
157
|
+
warmup: int = 5,
|
|
158
|
+
compile: bool = True,
|
|
159
|
+
compile_kwargs: dict[str, Any] | None = None,
|
|
160
|
+
) -> LatencyStats:
|
|
161
|
+
"""Measure the latency of one forward per sample for the supplied batch.
|
|
162
|
+
|
|
163
|
+
Set the model mode and place the model and inputs before calling.
|
|
164
|
+
Forward calls run in inference mode, and their side effects persist.
|
|
165
|
+
Timing starts after compilation preparation and warmup.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
target: A prepared module or callable.
|
|
169
|
+
input_args: One positional input or a tuple/list of positional inputs.
|
|
170
|
+
device: Device used for timing. CUDA and Ascend use its current stream.
|
|
171
|
+
input_kwargs: Optional keyword arguments.
|
|
172
|
+
repetitions: Positive number of timed forwards.
|
|
173
|
+
warmup: Number of untimed forwards after any compilation preparation.
|
|
174
|
+
compile: Enable `torch.compile` and three untimed preparation forwards.
|
|
175
|
+
Ascend defaults to TorchAir. Set `False` for eager timing if
|
|
176
|
+
TorchAir reports a compatibility error.
|
|
177
|
+
compile_kwargs: Keyword arguments forwarded to `torch.compile` when
|
|
178
|
+
compilation is enabled. On Ascend, pass `{"backend": "inductor"}`
|
|
179
|
+
to use Inductor with its default Triton compilation mode. If
|
|
180
|
+
`TORCHINDUCTOR_NPU_BACKEND` is set, it must be `default` for Triton.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Forward durations in sampling order and their summary statistics in milliseconds.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
ValueError: The timing options or timing device are invalid.
|
|
187
|
+
RuntimeError: The requested accelerator or default NPU backend is
|
|
188
|
+
unavailable, or the forward requires compilation after preparation.
|
|
189
|
+
"""
|
|
190
|
+
if type(repetitions) is not int or repetitions <= 0 or type(warmup) is not int or warmup < 0:
|
|
191
|
+
raise ValueError("Require positive repetitions and nonnegative warmup")
|
|
192
|
+
if type(compile) is not bool:
|
|
193
|
+
raise ValueError("compile must be a boolean")
|
|
194
|
+
|
|
195
|
+
args, kwargs = call_arguments(input_args, input_kwargs)
|
|
196
|
+
device, runtime = _timing_device(device)
|
|
197
|
+
forward = target
|
|
198
|
+
with torch.inference_mode(), ExitStack() as timing:
|
|
199
|
+
if compile:
|
|
200
|
+
with _unlimited_recompile_patch():
|
|
201
|
+
forward = torch.compile(target, **_compile_options(device, compile_kwargs))
|
|
202
|
+
# A forward can initialize state and invalidate the first graph.
|
|
203
|
+
for _ in range(_COMPILE_ITERS):
|
|
204
|
+
forward(*args, **kwargs)
|
|
205
|
+
if runtime is not None:
|
|
206
|
+
runtime.synchronize(device)
|
|
207
|
+
timing.enter_context(_fail_on_late_compile())
|
|
208
|
+
for _ in range(warmup):
|
|
209
|
+
forward(*args, **kwargs)
|
|
210
|
+
|
|
211
|
+
events = []
|
|
212
|
+
if runtime is not None:
|
|
213
|
+
stream = runtime.current_stream(device)
|
|
214
|
+
events = [
|
|
215
|
+
(runtime.Event(enable_timing=True), runtime.Event(enable_timing=True))
|
|
216
|
+
for _ in range(repetitions)
|
|
217
|
+
]
|
|
218
|
+
# Event handles may be allocated lazily by the first record.
|
|
219
|
+
for start, end in events:
|
|
220
|
+
start.record(stream)
|
|
221
|
+
end.record(stream)
|
|
222
|
+
runtime.synchronize(device)
|
|
223
|
+
|
|
224
|
+
gc_enabled = gc.isenabled()
|
|
225
|
+
gc.disable()
|
|
226
|
+
try:
|
|
227
|
+
if runtime is None:
|
|
228
|
+
samples = []
|
|
229
|
+
for _ in range(repetitions):
|
|
230
|
+
start_time = time.perf_counter()
|
|
231
|
+
forward(*args, **kwargs)
|
|
232
|
+
samples.append((time.perf_counter() - start_time) * 1000.0)
|
|
233
|
+
else:
|
|
234
|
+
for start, end in events:
|
|
235
|
+
start.record(stream)
|
|
236
|
+
forward(*args, **kwargs)
|
|
237
|
+
end.record(stream)
|
|
238
|
+
runtime.synchronize(device)
|
|
239
|
+
samples = [start.elapsed_time(end) for start, end in events]
|
|
240
|
+
finally:
|
|
241
|
+
if gc_enabled:
|
|
242
|
+
gc.enable()
|
|
243
|
+
else:
|
|
244
|
+
gc.disable()
|
|
245
|
+
|
|
246
|
+
return LatencyStats.from_samples(samples)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: torch-prisma
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Measure PyTorch inference latency, MACs, FLOPs, and parameters
|
|
5
|
+
Keywords: pytorch,inference,benchmarking,latency,model-complexity,macs,flops,cuda,ascend
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 6 - Mature
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Classifier: Topic :: System :: Benchmark
|
|
14
|
+
Requires-Dist: torch>=2.6
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Project-URL: Documentation, https://github.com/ZaberKo/torch-prisma/blob/main/docs/index.md
|
|
17
|
+
Project-URL: Issues, https://github.com/ZaberKo/torch-prisma/issues
|
|
18
|
+
Project-URL: Repository, https://github.com/ZaberKo/torch-prisma
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Torch Prisma
|
|
22
|
+
|
|
23
|
+
Measure PyTorch inference latency, parameters, MACs, and FLOPs on CPU, CUDA, and Huawei Ascend.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Requires Python 3.10+ and PyTorch 2.6+. Install in a virtual environment.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv pip install torch-prisma
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
Latency uses `torch.compile` by default. Set `compile=False` for eager timing.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import torch
|
|
39
|
+
from torch import nn
|
|
40
|
+
|
|
41
|
+
from torch_prisma import calculate_model_complexity, measure_module_latency
|
|
42
|
+
|
|
43
|
+
# For inference, set the model to evaluation mode and place it and the inputs on the device.
|
|
44
|
+
device = "cpu"
|
|
45
|
+
model = nn.Linear(8, 4).eval().to(device)
|
|
46
|
+
x = torch.randn(2, 8, device=device)
|
|
47
|
+
|
|
48
|
+
complexity = calculate_model_complexity(model, x)
|
|
49
|
+
latency = measure_module_latency(model, x, device=device)
|
|
50
|
+
|
|
51
|
+
print(complexity.params, complexity.macs, complexity.flops)
|
|
52
|
+
# Operations with incomplete counts, grouped by MACs and FLOPs.
|
|
53
|
+
print(complexity.uncovered_ops)
|
|
54
|
+
print(latency.median_ms)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
[Measurement API](https://github.com/ZaberKo/torch-prisma/blob/main/docs/measurement.md) · [Latency benchmarks](https://github.com/ZaberKo/torch-prisma/blob/main/docs/benchmarks.md) · [Huawei Ascend](https://github.com/ZaberKo/torch-prisma/blob/main/docs/ascend.md)
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
uv sync
|
|
63
|
+
uv run pre-commit install
|
|
64
|
+
uv run pytest
|
|
65
|
+
uv build
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Preview the documentation with `uv run --group docs zensical serve`.
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
Torch Prisma is distributed under the [MIT License](https://github.com/ZaberKo/torch-prisma/blob/main/LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
torch_prisma/__init__.py,sha256=41t9t0QybdB6hCurDsy2N1Dt0iyj2EW6SohQAqor6YY,468
|
|
2
|
+
torch_prisma/complexity.py,sha256=t7R8lVFzm3ShKROcs4_2VFjDc6kIzbhsDL0iKgr0V8g,12100
|
|
3
|
+
torch_prisma/execution.py,sha256=VDy9r15TZ2KPPUoYo3IuSuj_Dy8FxHKBQ7mM9v2pRzQ,680
|
|
4
|
+
torch_prisma/formulas.py,sha256=URhkFz04AZ1BgRJcXYSRh8MmyFwgGCKbf_eopi9HEys,18731
|
|
5
|
+
torch_prisma/latency.py,sha256=q1JAAfZf-SqfqKyTW7-uWwOBT6KKps-dcJMlbZgwP7s,9411
|
|
6
|
+
torch_prisma-0.1.0.dist-info/licenses/LICENSE,sha256=X12NtdOZ6qnbw8nCphqDwNbZQE4A1ErhkOEqz9MaFaU,1064
|
|
7
|
+
torch_prisma-0.1.0.dist-info/WHEEL,sha256=YUH1mBqsx8Dh2cQG2rlcuRYUhJddG9iClegy4IgnHik,79
|
|
8
|
+
torch_prisma-0.1.0.dist-info/METADATA,sha256=vkK_ipBI5pQMkCgugpsPH1zqtXmqqfSepp-ViQKxag0,2376
|
|
9
|
+
torch_prisma-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ZaberKo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|