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.
@@ -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)]
@@ -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
+ )
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.11
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.