forked from pytorch/pytorch
-
Notifications
You must be signed in to change notification settings - Fork 81
Expand file tree
/
Copy path_tensor_iterator.py
More file actions
401 lines (336 loc) · 14.3 KB
/
Copy path_tensor_iterator.py
File metadata and controls
401 lines (336 loc) · 14.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
"""Python access to ATen's TensorIterator build pipeline.
:class:`TensorIterator` builds an iterator from a set of operands and flags
that mirror ``at::TensorIteratorConfig``, then exposes the post-build
shape / dtype / device / stride information for inspection.
This is a build-pipeline-only surface: there is no ``for_each`` here. Use it
to debug shape and dtype inference, validate custom-op contracts, or inspect
how ATen would lay out a kernel's iteration.
Construction is canonical, *not* a faithful replay of arbitrary
``TensorIteratorConfig`` call sequences. Operands are always registered in
the order ``outputs -> inputs -> const_inputs`` and setters are applied in
one fixed order. Notably:
* The C++ builder distinguishes ``add_input(a); add_const_input(b)`` from
``add_const_input(b); add_input(a)`` -- ``input(0)`` refers to different
operands. This Python surface cannot express that distinction: every
``inputs[i]`` precedes every ``const_inputs[j]``.
* Some C++ setters have order-dependent side effects (e.g.
``promote_inputs_to_common_dtype(true)`` also flips
``check_all_same_dtype`` to ``false``). The Python surface materializes
the *final* boolean state of each knob, not the call order, so it can't
reproduce a sequence where an intermediate setter observed a
since-overwritten value.
Every in-tree caller of ``at::TensorIteratorConfig`` fits the canonical-
recipe shape; the lossiness is theoretical, not practical.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from torch._C import (
_TensorIterator as _CTensorIterator,
_TensorIteratorSpec as _CTensorIteratorSpec,
)
if TYPE_CHECKING:
from collections.abc import Sequence
__all__ = [
"TensorIterator",
"binary_op",
"binary_float_op",
"comparison_op",
"unary_op",
"unary_float_op",
"nullary_op",
"reduce_op",
]
class TensorIterator:
"""A built TensorIterator. Read-only view of the build-pipeline result.
Constructor kwargs mirror ``at::TensorIteratorConfig``; defaults match
the C++ defaults. See module docstring for canonical-recipe caveats.
Examples
--------
Build a TI for a binary op with type promotion and inspect the result::
>>> import torch
>>> from torch._tensor_iterator import TensorIterator
>>> a = torch.zeros(3, 4, dtype=torch.float32)
>>> b = torch.zeros(3, 4, dtype=torch.float64)
>>> it = TensorIterator(
... outputs=[None],
... const_inputs=[a, b],
... promote_inputs_to_common_dtype=True,
... cast_common_dtype_to_outputs=True,
... )
>>> it.common_dtype
torch.float64
>>> it.numel
12
>>> it.ndim # contiguous (3, 4) collapses to a single dim
1
Declare a static shape (the iterator skips broadcast/coalesce)::
>>> a = torch.zeros(2, 6)
>>> out = torch.empty(2, 6)
>>> it = TensorIterator(
... outputs=[out],
... const_inputs=[a],
... resize_outputs=False,
... static_shape=(2, 6),
... )
Build a reduction TI (output must be pre-allocated to the reduced shape)::
>>> a = torch.zeros(3, 4)
>>> out = torch.empty(3, 1)
>>> it = TensorIterator(
... outputs=[out],
... const_inputs=[a],
... resize_outputs=False,
... is_reduction=True,
... )
Inspect post-coalesce strides (in bytes; use ``element_strides`` for
element units)::
>>> a = torch.zeros(3, 4)
>>> b = torch.zeros(3, 4)
>>> it = TensorIterator(outputs=[None], const_inputs=[a, b])
>>> tuple(it.strides(0)) # output byte strides
(4,)
>>> it.element_strides(0) # ... in elements
(1,)
"""
def __init__(
self,
*,
outputs: list[torch.Tensor | None] | None = None,
inputs: list[torch.Tensor] | None = None,
const_inputs: list[torch.Tensor] | None = None,
check_all_same_dtype: bool = True,
check_all_same_device: bool = True,
promote_inputs_to_common_dtype: bool = False,
promote_integer_inputs_to_float: bool = False,
cast_common_dtype_to_outputs: bool = False,
enforce_safe_casting_to_output: bool = False,
enforce_linear_iteration: bool = False,
resize_outputs: bool = True,
check_mem_overlap: bool = True,
allow_cpu_scalars: bool = False,
is_reduction: bool = False,
static_dtype: torch.dtype | None = None,
static_device: torch.device | None = None,
static_shape: Sequence[int] | None = None,
squash_dims: Sequence[int] = (),
) -> None:
spec = _CTensorIteratorSpec()
spec.outputs = list(outputs) if outputs is not None else []
spec.inputs = list(inputs) if inputs is not None else []
spec.const_inputs = list(const_inputs) if const_inputs is not None else []
spec.check_all_same_dtype = check_all_same_dtype
spec.check_all_same_device = check_all_same_device
spec.promote_inputs_to_common_dtype = promote_inputs_to_common_dtype
spec.promote_integer_inputs_to_float = promote_integer_inputs_to_float
spec.cast_common_dtype_to_outputs = cast_common_dtype_to_outputs
spec.enforce_safe_casting_to_output = enforce_safe_casting_to_output
spec.enforce_linear_iteration = enforce_linear_iteration
spec.resize_outputs = resize_outputs
spec.check_mem_overlap = check_mem_overlap
spec.allow_cpu_scalars = allow_cpu_scalars
spec.is_reduction = is_reduction
if static_dtype is not None:
spec.static_dtype = static_dtype
if static_device is not None:
spec.static_device = static_device
if static_shape is not None:
spec.static_shape = list(static_shape)
spec.squash_dims = list(squash_dims)
self._impl: _CTensorIterator = spec.build()
@property
def ndim(self) -> int:
return self._impl.ndim
@property
def shape(self) -> memoryview:
"""Iterator shape (post coalesce/reorder), as a zero-copy
``memoryview`` of ``int64`` elements. The view holds a reference
to this iterator and keeps it alive for as long as the view is
reachable; copy via ``tuple(it.shape)`` if you need a snapshot
you can outlive the iterator with. Hot-path readers (dispatch
conditionals) get index access without an allocation."""
return self._impl.shape
@property
def numel(self) -> int:
return self._impl.numel
@property
def ntensors(self) -> int:
return self._impl.ntensors
@property
def ninputs(self) -> int:
return self._impl.ninputs
@property
def noutputs(self) -> int:
return self._impl.noutputs
@property
def is_contiguous(self) -> bool:
return self._impl.is_contiguous
@property
def is_trivial_1d(self) -> bool:
return self._impl.is_trivial_1d
@property
def common_dtype(self) -> torch.dtype | None:
"""The inferred computation dtype, or ``None`` if no single dtype
was inferred. Populated whenever TensorIterator can resolve a
common dtype -- under promotion flags, or when every input
already shares a dtype. ``None`` does not mean "promotion was
not requested"; it means inference produced no answer."""
return self._impl.common_dtype
def tensor(self, index: int) -> torch.Tensor:
"""Return the iterator's current operand at ``index``.
Note: under ``promote_inputs_to_common_dtype`` /
``cast_common_dtype_to_outputs`` (CPU paths), this may be a
promoted/cast kernel temporary rather than the tensor that was
registered with the config -- it's the iterator's view of the
operand a kernel would actually iterate over."""
return self._impl.tensor(index)
def input(self, index: int = 0) -> torch.Tensor:
"""Return the iterator's current input. See :meth:`tensor` for the
promoted-temporary caveat."""
return self._impl.input(index)
def output(self, index: int = 0) -> torch.Tensor:
"""Return the iterator's current output. See :meth:`tensor` for the
cast-temporary caveat."""
return self._impl.output(index)
def dtype(self, index: int = 0) -> torch.dtype:
return self._impl.dtype(index)
def device(self, index: int = 0) -> torch.device:
return self._impl.device(index)
def strides(self, index: int) -> memoryview:
"""Per-operand strides in bytes (post reorder/coalesce), as a
zero-copy ``memoryview`` of ``int64`` elements. The view holds a
reference to this iterator and keeps it alive for as long as the
view is reachable; copy via ``tuple(it.strides(i))`` if you need
a snapshot you can outlive the iterator with."""
return self._impl.strides(index)
def element_strides(self, index: int) -> tuple[int, ...]:
"""Per-operand strides in elements (byte stride / element size).
Allocates a fresh tuple on every call. Don't use on a hot path:
cache the result, or read :meth:`strides` once and divide by
:meth:`element_size` of the operand inline."""
return self._impl.element_strides(index)
def __repr__(self) -> str:
return repr(self._impl)
# --- Factory shortcuts. These mirror the C++ named constructors at
# aten/src/ATen/TensorIterator.cpp:1069+. Pass ``out=None`` to ask the iterator
# to allocate a fresh output tensor of the inferred shape/dtype/device.
def binary_op(
out: torch.Tensor | None, a: torch.Tensor, b: torch.Tensor
) -> TensorIterator:
"""Equivalent of ``at::TensorIterator::binary_op``."""
return TensorIterator(
outputs=[out],
const_inputs=[a, b],
allow_cpu_scalars=True,
promote_inputs_to_common_dtype=True,
cast_common_dtype_to_outputs=True,
enforce_safe_casting_to_output=True,
)
def binary_float_op(
out: torch.Tensor | None, a: torch.Tensor, b: torch.Tensor
) -> TensorIterator:
"""Equivalent of ``at::TensorIterator::binary_float_op``."""
return TensorIterator(
outputs=[out],
const_inputs=[a, b],
allow_cpu_scalars=True,
promote_inputs_to_common_dtype=True,
cast_common_dtype_to_outputs=True,
enforce_safe_casting_to_output=True,
promote_integer_inputs_to_float=True,
)
def comparison_op(
out: torch.Tensor | None, a: torch.Tensor, b: torch.Tensor
) -> TensorIterator:
"""Equivalent of ``at::TensorIterator::comparison_op``.
When ``out`` is ``None``, the output dtype is forced to bool. When ``out``
is a defined non-bool tensor, the common dtype is cast back to its dtype
via ``cast_common_dtype_to_outputs``. The bool-output case skips that cast
as a performance optimization.
"""
static_dtype = torch.bool if out is None else None
cast_to_outputs = out is not None and out.dtype != torch.bool
return TensorIterator(
outputs=[out],
const_inputs=[a, b],
allow_cpu_scalars=True,
promote_inputs_to_common_dtype=True,
cast_common_dtype_to_outputs=cast_to_outputs,
static_dtype=static_dtype,
)
def unary_op(out: torch.Tensor | None, a: torch.Tensor) -> TensorIterator:
"""Equivalent of ``at::TensorIterator::unary_op``."""
return TensorIterator(outputs=[out], const_inputs=[a])
def unary_float_op(out: torch.Tensor | None, a: torch.Tensor) -> TensorIterator:
"""Equivalent of ``at::TensorIterator::unary_float_op``."""
return TensorIterator(
outputs=[out],
const_inputs=[a],
promote_inputs_to_common_dtype=True,
cast_common_dtype_to_outputs=True,
enforce_safe_casting_to_output=True,
promote_integer_inputs_to_float=True,
)
def nullary_op(out: torch.Tensor) -> TensorIterator:
"""Equivalent of ``at::TensorIterator::nullary_op``.
Unlike the binary/unary factories, ``out`` must be a defined tensor;
the C++ named constructor takes a non-undefined output.
"""
if out is None:
raise TypeError(
"nullary_op requires a defined output tensor; None is not allowed."
)
return TensorIterator(
outputs=[out],
check_all_same_dtype=False,
resize_outputs=False,
)
def reduce_op(
out: torch.Tensor,
a: torch.Tensor,
*,
out2: torch.Tensor | None = None,
) -> TensorIterator:
"""Equivalent of ``at::TensorIterator::reduce_op``.
Pass ``out2`` for the two-output reduction overload (e.g. ``min`` returning
values + indices). The output tensor(s) must be pre-allocated and shaped
correctly: this factory does not allocate or resize. With ``out2``, both
outputs must live on ``a``'s device and share its sizes/strides -- the
C++ named constructor asserts this and the same checks are mirrored
here.
"""
# Mirror the TORCH_INTERNAL_ASSERTs in TensorIterator::reduce_op. We
# surface them as RuntimeError up front since the binding doesn't
# rebind the named constructor itself; without this, mismatched
# out1/out2 would build a kernel-shape that doesn't match the
# tensors and corrupt memory once a kernel is invoked.
if out2 is not None:
if not out.device == a.device == out2.device:
raise RuntimeError(
"reduce_op: out, out2, and the input must share a device, "
f"got out={out.device}, out2={out2.device}, a={a.device}"
)
if out.dim() != out2.dim():
raise RuntimeError(
f"reduce_op: out and out2 must have the same dim, got "
f"{out.dim()} and {out2.dim()}"
)
if out.shape != out2.shape or out.stride() != out2.stride():
raise RuntimeError(
"reduce_op: out and out2 must have identical sizes and strides"
)
return TensorIterator(
outputs=[out, out2],
const_inputs=[a],
check_all_same_dtype=False,
resize_outputs=False,
check_mem_overlap=False,
is_reduction=True,
)
return TensorIterator(
outputs=[out],
const_inputs=[a],
promote_inputs_to_common_dtype=True,
resize_outputs=False,
check_mem_overlap=False,
is_reduction=True,
)