tack-core 0.7.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.
- tack/__init__.py +294 -0
- tack/algorithms/__init__.py +60 -0
- tack/algorithms/copy.py +49 -0
- tack/algorithms/scan.py +292 -0
- tack/algorithms/segmented.py +612 -0
- tack/algorithms/sort.py +626 -0
- tack/algorithms/stats.py +282 -0
- tack/arrays.py +820 -0
- tack/cache.py +90 -0
- tack/codegen/__init__.py +2 -0
- tack/codegen/atomics.py +40 -0
- tack/codegen/bounds.py +63 -0
- tack/codegen/c_family.py +219 -0
- tack/codegen/cuda_gen.py +767 -0
- tack/codegen/float_division.py +66 -0
- tack/codegen/hip_gen.py +55 -0
- tack/codegen/identifiers.py +58 -0
- tack/codegen/integer_division.py +47 -0
- tack/codegen/integer_ops.py +138 -0
- tack/codegen/llvm_gen.py +1516 -0
- tack/codegen/msl_gen.py +746 -0
- tack/codegen/opencl_gen.py +579 -0
- tack/codegen/reductions.py +109 -0
- tack/codegen/tables.py +53 -0
- tack/codegen/warp_gen.py +761 -0
- tack/communication/__init__.py +43 -0
- tack/communication/_blocks.py +207 -0
- tack/communication/_kernels.py +83 -0
- tack/communication/_plan.py +136 -0
- tack/communication/_route.py +186 -0
- tack/communication/_sort.py +170 -0
- tack/communication/_world.py +165 -0
- tack/lang/__init__.py +2 -0
- tack/lang/array.py +1338 -0
- tack/lang/ast_copy.py +35 -0
- tack/lang/ast_transform.py +1147 -0
- tack/lang/atomic_support.py +85 -0
- tack/lang/call_bindings.py +102 -0
- tack/lang/constant.py +227 -0
- tack/lang/device_class.py +32 -0
- tack/lang/dlpack.py +651 -0
- tack/lang/func.py +114 -0
- tack/lang/inspect_kernel.py +228 -0
- tack/lang/ir.py +530 -0
- tack/lang/ir_bounds.py +154 -0
- tack/lang/ir_names.py +47 -0
- tack/lang/ir_optimize.py +224 -0
- tack/lang/ir_pack_scalars.py +105 -0
- tack/lang/ir_resolve.py +149 -0
- tack/lang/ir_traversal.py +189 -0
- tack/lang/ir_type_annotate.py +514 -0
- tack/lang/ir_verify.py +280 -0
- tack/lang/kernel.py +379 -0
- tack/lang/lowering/__init__.py +25 -0
- tack/lang/lowering/arrays.py +299 -0
- tack/lang/lowering/bounds.py +107 -0
- tack/lang/lowering/checks.py +190 -0
- tack/lang/lowering/inlining.py +344 -0
- tack/lang/lowering/local_arrays.py +135 -0
- tack/lang/lowering/loops.py +387 -0
- tack/lang/lowering/values.py +86 -0
- tack/lang/lowering/vectors.py +664 -0
- tack/lang/parallel_dims.py +90 -0
- tack/lang/source_validation.py +185 -0
- tack/lang/template_rewrite.py +1176 -0
- tack/lang/type_inference.py +144 -0
- tack/lang/types.py +249 -0
- tack/lang/workgroup_participation.py +208 -0
- tack/lang/workgroup_support.py +51 -0
- tack/math.py +93 -0
- tack/py.typed +0 -0
- tack/random.py +137 -0
- tack/runtime/__init__.py +2 -0
- tack/runtime/aio.py +115 -0
- tack/runtime/backend.py +287 -0
- tack/runtime/buffer_cache.py +90 -0
- tack/runtime/cpu.py +2329 -0
- tack/runtime/cpu_pool.py +804 -0
- tack/runtime/cuda_backend.py +1411 -0
- tack/runtime/debug_bounds.py +122 -0
- tack/runtime/device_fill.py +68 -0
- tack/runtime/disk_cache.py +1001 -0
- tack/runtime/dispatch.py +454 -0
- tack/runtime/gpu_backend.py +184 -0
- tack/runtime/hip_backend.py +1163 -0
- tack/runtime/kernel_utils.py +1518 -0
- tack/runtime/level_zero_backend.py +1997 -0
- tack/runtime/metal.py +890 -0
- tack/runtime/nvrtc_version.py +28 -0
- tack/runtime/reductions.py +38 -0
- tack/runtime/warp_backend.py +266 -0
- tack_core-0.7.0.dist-info/METADATA +116 -0
- tack_core-0.7.0.dist-info/RECORD +95 -0
- tack_core-0.7.0.dist-info/WHEEL +4 -0
- tack_core-0.7.0.dist-info/licenses/LICENSE +29 -0
tack/__init__.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) Kitware Inc.
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
"""Tack: a Python-first compute framework.
|
|
5
|
+
|
|
6
|
+
Kernels are Python functions decorated with ``@tack.kernel``, compiled when
|
|
7
|
+
first called for the device they run on: the CPU (through LLVM), Metal, CUDA,
|
|
8
|
+
HIP or Level Zero. Arrays (``tack.empty``, ``tack.array``, ...) hold typed
|
|
9
|
+
values on a device, and a kernel runs where its arrays are. ``tack.dataset``,
|
|
10
|
+
in the tack-vis package, adds meshes, fields on them, and the algorithms of
|
|
11
|
+
scientific visualization.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# Enable namespace package merging across tack-core and tack-vis
|
|
15
|
+
from importlib.metadata import PackageNotFoundError as _NotInstalled
|
|
16
|
+
from importlib.metadata import version as _installed_version
|
|
17
|
+
from pkgutil import extend_path
|
|
18
|
+
|
|
19
|
+
__path__ = extend_path(__path__, __name__)
|
|
20
|
+
del extend_path
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
__version__ = _installed_version("tack-core")
|
|
24
|
+
except _NotInstalled: # a source tree that was never installed
|
|
25
|
+
__version__ = "0+unknown"
|
|
26
|
+
del _NotInstalled, _installed_version
|
|
27
|
+
|
|
28
|
+
from tack.lang.array import (
|
|
29
|
+
Array,
|
|
30
|
+
ExportedMemory,
|
|
31
|
+
SoaArray,
|
|
32
|
+
Texture3D,
|
|
33
|
+
arange,
|
|
34
|
+
array,
|
|
35
|
+
array_from_ptr,
|
|
36
|
+
asarray,
|
|
37
|
+
concat,
|
|
38
|
+
copyto,
|
|
39
|
+
empty,
|
|
40
|
+
empty_like,
|
|
41
|
+
from_dlpack,
|
|
42
|
+
full,
|
|
43
|
+
full_like,
|
|
44
|
+
memory_space,
|
|
45
|
+
ones,
|
|
46
|
+
ones_like,
|
|
47
|
+
soa,
|
|
48
|
+
zeros,
|
|
49
|
+
zeros_like,
|
|
50
|
+
)
|
|
51
|
+
from tack.lang.constant import constant
|
|
52
|
+
from tack.lang.device_class import device_class
|
|
53
|
+
from tack.lang.func import func
|
|
54
|
+
from tack.lang.inspect_kernel import inspect
|
|
55
|
+
from tack.lang.kernel import kernel
|
|
56
|
+
from tack.lang.types import (
|
|
57
|
+
ElementType,
|
|
58
|
+
ScalarType,
|
|
59
|
+
f32,
|
|
60
|
+
f64,
|
|
61
|
+
get_default_float,
|
|
62
|
+
i8,
|
|
63
|
+
i16,
|
|
64
|
+
i32,
|
|
65
|
+
i64,
|
|
66
|
+
mat,
|
|
67
|
+
mat2,
|
|
68
|
+
mat3,
|
|
69
|
+
mat4,
|
|
70
|
+
set_default_float,
|
|
71
|
+
u8,
|
|
72
|
+
u16,
|
|
73
|
+
u32,
|
|
74
|
+
u64,
|
|
75
|
+
vec,
|
|
76
|
+
vec2,
|
|
77
|
+
vec3,
|
|
78
|
+
vec4,
|
|
79
|
+
)
|
|
80
|
+
from tack.runtime.dispatch import (
|
|
81
|
+
Device,
|
|
82
|
+
device,
|
|
83
|
+
device_count,
|
|
84
|
+
get_default_device,
|
|
85
|
+
set_debug,
|
|
86
|
+
set_default_device,
|
|
87
|
+
sync,
|
|
88
|
+
sync_async,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Shared memory, barrier, thread_id — only usable inside @tack.kernel
|
|
93
|
+
def shared(dtype, size):
|
|
94
|
+
"""Allocate threadgroup shared memory. Only usable inside a @tack.kernel."""
|
|
95
|
+
raise RuntimeError("shared() can only be used inside a @tack.kernel")
|
|
96
|
+
|
|
97
|
+
def shared_like(array, size):
|
|
98
|
+
"""Allocate shared memory with the same dtype as an array. Only usable inside a @tack.kernel."""
|
|
99
|
+
raise RuntimeError("shared_like() can only be used inside a @tack.kernel")
|
|
100
|
+
|
|
101
|
+
def select(mask, a, b):
|
|
102
|
+
"""``a`` where ``mask`` is true, else ``b``, component by component.
|
|
103
|
+
|
|
104
|
+
Only usable inside a kernel or device function. ``mask`` is a scalar
|
|
105
|
+
or a comparison vector such as ``v > 0.0``; ``a`` and ``b`` are scalars
|
|
106
|
+
or vectors of one shape, a scalar standing for every component.
|
|
107
|
+
"""
|
|
108
|
+
raise RuntimeError("tack.select() can only be used inside a @tack.kernel")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def barrier():
|
|
112
|
+
"""Threadgroup synchronization barrier. Only usable inside a @tack.kernel."""
|
|
113
|
+
raise RuntimeError("barrier() can only be used inside a @tack.kernel")
|
|
114
|
+
|
|
115
|
+
def thread_id():
|
|
116
|
+
"""Thread index within workgroup. Only usable inside a @tack.kernel."""
|
|
117
|
+
raise RuntimeError("thread_id() can only be used inside a @tack.kernel")
|
|
118
|
+
|
|
119
|
+
# Atomic operations — only usable inside @tack.kernel
|
|
120
|
+
def atomic_add(array, index, value):
|
|
121
|
+
"""Atomic add: array[index] += value. Only usable inside a @tack.kernel."""
|
|
122
|
+
raise RuntimeError("atomic_add() can only be used inside a @tack.kernel")
|
|
123
|
+
|
|
124
|
+
def atomic_min(array, index, value):
|
|
125
|
+
"""Atomic min: array[index] = min(array[index], value). Only usable inside a @tack.kernel."""
|
|
126
|
+
raise RuntimeError("atomic_min() can only be used inside a @tack.kernel")
|
|
127
|
+
|
|
128
|
+
def atomic_max(array, index, value):
|
|
129
|
+
"""Atomic max: array[index] = max(array[index], value). Only usable inside a @tack.kernel."""
|
|
130
|
+
raise RuntimeError("atomic_max() can only be used inside a @tack.kernel")
|
|
131
|
+
|
|
132
|
+
# ndindex for multi-dimensional parallel iteration
|
|
133
|
+
def ndindex(*args):
|
|
134
|
+
"""Multi-dimensional parallel iteration, as ``np.ndindex``.
|
|
135
|
+
|
|
136
|
+
Used in kernels: ``for i, j in tack.ndindex(w, h)``; each argument is a
|
|
137
|
+
size or a ``(start, end)`` pair. Cannot be called from Python directly.
|
|
138
|
+
"""
|
|
139
|
+
raise RuntimeError("ndindex() can only be used inside a @tack.kernel")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def identity(n):
|
|
143
|
+
"""The n-by-n identity matrix, as ``np.identity``. Only usable inside a @tack.kernel."""
|
|
144
|
+
raise RuntimeError("identity() can only be used inside a @tack.kernel")
|
|
145
|
+
|
|
146
|
+
def block_sum(value):
|
|
147
|
+
"""Sum a value across all threads in the workgroup. Only usable inside a @tack.kernel."""
|
|
148
|
+
raise RuntimeError("block_sum() can only be used inside a @tack.kernel")
|
|
149
|
+
|
|
150
|
+
def block_max(value):
|
|
151
|
+
"""Max of a value across all threads in the workgroup. Only usable inside a @tack.kernel."""
|
|
152
|
+
raise RuntimeError("block_max() can only be used inside a @tack.kernel")
|
|
153
|
+
|
|
154
|
+
def block_min(value):
|
|
155
|
+
"""Min of a value across all threads in the workgroup. Only usable inside a @tack.kernel."""
|
|
156
|
+
raise RuntimeError("block_min() can only be used inside a @tack.kernel")
|
|
157
|
+
|
|
158
|
+
def local_array(dtype, size):
|
|
159
|
+
"""Allocate a per-thread local array. Only usable inside a @tack.kernel.
|
|
160
|
+
|
|
161
|
+
Usage: arr = tack.local_array(tack.f32, 8)
|
|
162
|
+
Then: arr[i] = value, value = arr[i]
|
|
163
|
+
"""
|
|
164
|
+
raise RuntimeError("local_array() can only be used inside a @tack.kernel")
|
|
165
|
+
|
|
166
|
+
def local_array_like(array, size):
|
|
167
|
+
"""Allocate a per-thread local array with the same dtype as an array. Only usable inside a @tack.kernel."""
|
|
168
|
+
raise RuntimeError("local_array_like() can only be used inside a @tack.kernel")
|
|
169
|
+
|
|
170
|
+
def texture3d(source_array, shape=None, interp='linear'):
|
|
171
|
+
"""Create a 3D texture from an array for hardware-accelerated sampling.
|
|
172
|
+
|
|
173
|
+
The texture copies the array's data now. Later writes to the array --
|
|
174
|
+
kernel stores, ``from_numpy``, external or DLPack writes -- are not
|
|
175
|
+
visible to it until ``tex.update()`` copies the array again. That holds
|
|
176
|
+
on every backend, including CPU.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
source_array: A Tack array with f32 dtype holding W*H*D elements,
|
|
180
|
+
x varying fastest.
|
|
181
|
+
shape: (W, H, D) tuple for the 3D dimensions. If the array's shape
|
|
182
|
+
is already 3D, this can be omitted.
|
|
183
|
+
interp: Interpolation mode. Only 'linear' (the default) is
|
|
184
|
+
implemented.
|
|
185
|
+
"""
|
|
186
|
+
if shape is None:
|
|
187
|
+
shape = source_array.shape
|
|
188
|
+
if len(shape) != 3:
|
|
189
|
+
raise ValueError("texture3d requires a 3D shape")
|
|
190
|
+
return Texture3D(source_array, shape_3d=shape, interp=interp)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
#: Public submodules, loaded on first use, so ``import tack`` alone reaches
|
|
194
|
+
#: ``tack.arrays.CountingArray``. ``dataset`` and ``interop`` are tack-vis's.
|
|
195
|
+
#: How to install tack-vis, which is not on PyPI: from the repository, at this
|
|
196
|
+
#: release's tag (a development install has no tag to name).
|
|
197
|
+
_TACK_VIS_INSTALL = (
|
|
198
|
+
"pip install 'tack-vis @ git+https://github.com/Kitware/tack"
|
|
199
|
+
+ ("" if "+" in __version__ or "dev" in __version__ else f"@v{__version__}")
|
|
200
|
+
+ "#subdirectory=packages/tack-vis'")
|
|
201
|
+
|
|
202
|
+
_SUBMODULES = frozenset({"algorithms", "arrays", "cache", "communication", "math", "random",
|
|
203
|
+
"dataset", "interop"})
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def __getattr__(name):
|
|
207
|
+
if name in _SUBMODULES:
|
|
208
|
+
import importlib
|
|
209
|
+
try:
|
|
210
|
+
return importlib.import_module(f"{__name__}.{name}")
|
|
211
|
+
except ModuleNotFoundError as error:
|
|
212
|
+
if error.name != f"{__name__}.{name}":
|
|
213
|
+
raise
|
|
214
|
+
raise AttributeError(f"module 'tack' has no attribute {name!r}: tack.{name} is "
|
|
215
|
+
f"in the tack-vis package, which is not installed. It is "
|
|
216
|
+
f"not on PyPI; install it from the repository:\n"
|
|
217
|
+
f" {_TACK_VIS_INSTALL}") from error
|
|
218
|
+
raise AttributeError(f"module 'tack' has no attribute {name!r}")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
__all__ = [
|
|
222
|
+
"Array",
|
|
223
|
+
"Device",
|
|
224
|
+
"ElementType",
|
|
225
|
+
"ExportedMemory",
|
|
226
|
+
"ScalarType",
|
|
227
|
+
"SoaArray",
|
|
228
|
+
"Texture3D",
|
|
229
|
+
"arange",
|
|
230
|
+
"array",
|
|
231
|
+
"array_from_ptr",
|
|
232
|
+
"asarray",
|
|
233
|
+
"atomic_add",
|
|
234
|
+
"atomic_max",
|
|
235
|
+
"atomic_min",
|
|
236
|
+
"barrier",
|
|
237
|
+
"block_max",
|
|
238
|
+
"block_min",
|
|
239
|
+
"block_sum",
|
|
240
|
+
"concat",
|
|
241
|
+
"constant",
|
|
242
|
+
"copyto",
|
|
243
|
+
"device",
|
|
244
|
+
"device_class",
|
|
245
|
+
"device_count",
|
|
246
|
+
"empty",
|
|
247
|
+
"empty_like",
|
|
248
|
+
"f32",
|
|
249
|
+
"f64",
|
|
250
|
+
"from_dlpack",
|
|
251
|
+
"full",
|
|
252
|
+
"full_like",
|
|
253
|
+
"func",
|
|
254
|
+
"get_default_device",
|
|
255
|
+
"get_default_float",
|
|
256
|
+
"i8",
|
|
257
|
+
"i16",
|
|
258
|
+
"i32",
|
|
259
|
+
"i64",
|
|
260
|
+
"identity",
|
|
261
|
+
"inspect",
|
|
262
|
+
"kernel",
|
|
263
|
+
"local_array",
|
|
264
|
+
"local_array_like",
|
|
265
|
+
"mat",
|
|
266
|
+
"mat2",
|
|
267
|
+
"mat3",
|
|
268
|
+
"mat4",
|
|
269
|
+
"memory_space",
|
|
270
|
+
"ndindex",
|
|
271
|
+
"ones",
|
|
272
|
+
"ones_like",
|
|
273
|
+
"select",
|
|
274
|
+
"set_debug",
|
|
275
|
+
"set_default_device",
|
|
276
|
+
"set_default_float",
|
|
277
|
+
"shared",
|
|
278
|
+
"shared_like",
|
|
279
|
+
"soa",
|
|
280
|
+
"sync",
|
|
281
|
+
"sync_async",
|
|
282
|
+
"texture3d",
|
|
283
|
+
"thread_id",
|
|
284
|
+
"u8",
|
|
285
|
+
"u16",
|
|
286
|
+
"u32",
|
|
287
|
+
"u64",
|
|
288
|
+
"vec",
|
|
289
|
+
"vec2",
|
|
290
|
+
"vec3",
|
|
291
|
+
"vec4",
|
|
292
|
+
"zeros",
|
|
293
|
+
"zeros_like",
|
|
294
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) Kitware Inc.
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
"""tack.algorithms — GPU algorithm primitives.
|
|
5
|
+
|
|
6
|
+
Portable, backend-agnostic building blocks for common parallel patterns:
|
|
7
|
+
scan, sort, segmented reductions, copy, fill, stats. Their inputs are any
|
|
8
|
+
arrays a kernel indexes -- stored Tack arrays or the implicit arrays of
|
|
9
|
+
``tack.arrays`` -- and their outputs stored arrays; they run as kernels on
|
|
10
|
+
their inputs' device (CPU, Metal, CUDA, HIP, Level Zero), reading back at
|
|
11
|
+
most a scalar result.
|
|
12
|
+
|
|
13
|
+
Visualization algorithms are in ``tack.dataset`` (tack-vis).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from pkgutil import extend_path
|
|
17
|
+
|
|
18
|
+
__path__ = extend_path(__path__, __name__)
|
|
19
|
+
|
|
20
|
+
from tack.algorithms.copy import copy, fill_value
|
|
21
|
+
from tack.algorithms.scan import exclusive_scan, inclusive_scan
|
|
22
|
+
from tack.algorithms.segmented import (
|
|
23
|
+
segment_broadcast,
|
|
24
|
+
segment_ids,
|
|
25
|
+
segment_reduce,
|
|
26
|
+
segment_scan,
|
|
27
|
+
)
|
|
28
|
+
from tack.algorithms.sort import argsort, gather, reduce_by_key, sort_by_key, unique
|
|
29
|
+
from tack.algorithms.stats import (
|
|
30
|
+
absmax,
|
|
31
|
+
count_nonzero,
|
|
32
|
+
dot,
|
|
33
|
+
histogram,
|
|
34
|
+
norm,
|
|
35
|
+
std,
|
|
36
|
+
var,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"absmax",
|
|
41
|
+
"argsort",
|
|
42
|
+
"copy",
|
|
43
|
+
"count_nonzero",
|
|
44
|
+
"dot",
|
|
45
|
+
"exclusive_scan",
|
|
46
|
+
"fill_value",
|
|
47
|
+
"gather",
|
|
48
|
+
"histogram",
|
|
49
|
+
"inclusive_scan",
|
|
50
|
+
"norm",
|
|
51
|
+
"reduce_by_key",
|
|
52
|
+
"segment_broadcast",
|
|
53
|
+
"segment_ids",
|
|
54
|
+
"segment_reduce",
|
|
55
|
+
"segment_scan",
|
|
56
|
+
"sort_by_key",
|
|
57
|
+
"std",
|
|
58
|
+
"unique",
|
|
59
|
+
"var",
|
|
60
|
+
]
|
tack/algorithms/copy.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) Kitware Inc.
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
"""Array copy and fill utilities, each a kernel on its arrays' device.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from tack import algorithms
|
|
8
|
+
from tack.algorithms.copy import copy_with_offset
|
|
9
|
+
|
|
10
|
+
algorithms.copy(src, dst, n)
|
|
11
|
+
algorithms.fill_value(array, value, n)
|
|
12
|
+
copy_with_offset(src, dst, dst_offset, n)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import tack
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@tack.kernel
|
|
19
|
+
def _copy_kernel(src, dst, n):
|
|
20
|
+
for i in range(n):
|
|
21
|
+
dst[i] = src[i]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@tack.kernel
|
|
25
|
+
def _fill_kernel(dst, val, n):
|
|
26
|
+
for i in range(n):
|
|
27
|
+
dst[i] = val
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@tack.kernel
|
|
31
|
+
def _copy_offset_kernel(src, dst, dst_offset, n):
|
|
32
|
+
"""Copy n elements from src into dst starting at dst_offset."""
|
|
33
|
+
for i in range(n):
|
|
34
|
+
dst[dst_offset + i] = src[i]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def copy(src, dst, n):
|
|
38
|
+
"""Copy n elements from src array to dst array with a kernel."""
|
|
39
|
+
_copy_kernel(src, dst, n)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def copy_with_offset(src, dst, dst_offset, n):
|
|
43
|
+
"""Copy n elements from src into dst starting at dst_offset with a kernel."""
|
|
44
|
+
_copy_offset_kernel(src, dst, dst_offset, n)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def fill_value(dst, value, n):
|
|
48
|
+
"""Fill n elements of dst array with a scalar value with a kernel."""
|
|
49
|
+
_fill_kernel(dst, value, n)
|
tack/algorithms/scan.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) Kitware Inc.
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
"""Parallel prefix sum (scan) on tack arrays.
|
|
5
|
+
|
|
6
|
+
Reduce, then scan, by chunks: one thread per chunk of ``_CHUNK``
|
|
7
|
+
elements totals it, the chunk totals are scanned the same way, and each
|
|
8
|
+
chunk then writes its running sums from its offset. That is a few launches
|
|
9
|
+
per factor of ``_CHUNK`` -- three levels for a million elements -- where
|
|
10
|
+
the Blelloch scan this replaced made two per factor of two, 44 for a
|
|
11
|
+
million, and most of a filter's time went to them. No shared memory or
|
|
12
|
+
barriers -- works on all backends.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
import tack
|
|
16
|
+
from tack import algorithms
|
|
17
|
+
|
|
18
|
+
total = algorithms.exclusive_scan(counts, offsets, n)
|
|
19
|
+
algorithms.inclusive_scan(input_array, output_array, n)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import tack
|
|
23
|
+
from tack.runtime.dispatch import on_inputs_device
|
|
24
|
+
|
|
25
|
+
_CHUNK = 256 # elements one thread scans in turn
|
|
26
|
+
# A level of at most this many elements is scanned by one thread in one
|
|
27
|
+
# launch: below it the recursion's launches cost more than the work (the
|
|
28
|
+
# chunk totals of a million elements, 3907 of them, took two more levels
|
|
29
|
+
# and five launches of a few microseconds' work each).
|
|
30
|
+
_SERIAL_MAX = 4096
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@tack.kernel
|
|
34
|
+
def _chunk_sums(src, dst, sums, n, chunk, nchunks):
|
|
35
|
+
"""Copy each chunk into ``dst`` -- converting to its dtype on the store --
|
|
36
|
+
and total it in that dtype."""
|
|
37
|
+
for c in range(nchunks):
|
|
38
|
+
start = c * chunk
|
|
39
|
+
end = min(start + chunk, n)
|
|
40
|
+
dst[start] = src[start]
|
|
41
|
+
total = dst[start]
|
|
42
|
+
for i in range(start + 1, end):
|
|
43
|
+
dst[i] = src[i]
|
|
44
|
+
total += dst[i]
|
|
45
|
+
sums[c] = total
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@tack.kernel
|
|
49
|
+
def _chunk_scan(data, offsets, n, chunk, nchunks, inclusive):
|
|
50
|
+
"""Each chunk's running sums, in place, starting from its offset."""
|
|
51
|
+
for c in range(nchunks):
|
|
52
|
+
running = offsets[c]
|
|
53
|
+
for i in range(c * chunk, min(c * chunk + chunk, n)):
|
|
54
|
+
value = data[i]
|
|
55
|
+
if inclusive == 1:
|
|
56
|
+
running += value
|
|
57
|
+
data[i] = running
|
|
58
|
+
else:
|
|
59
|
+
data[i] = running
|
|
60
|
+
running += value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# On backends with workgroups the chunk kernels above run few threads with
|
|
64
|
+
# long serial loops; there a tile of _TILE elements, _PER_LANE to each of a
|
|
65
|
+
# workgroup's 256 lanes, is scanned by the workgroup: each lane totals its
|
|
66
|
+
# elements in order, the lane totals are scanned in shared memory, and each
|
|
67
|
+
# lane then writes running sums from its offset. Same levels and dtype rules
|
|
68
|
+
# as the chunks; between lanes the additions run in tree order.
|
|
69
|
+
|
|
70
|
+
_TILE = tack.constant(2048)
|
|
71
|
+
_PER_LANE = tack.constant(8)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@tack.kernel
|
|
75
|
+
def _tile_sums(src, dst, sums, n, ntiles):
|
|
76
|
+
"""Copy each tile into ``dst``, converting on the store, and total it there."""
|
|
77
|
+
lanes = tack.shared_like(dst, 256)
|
|
78
|
+
for i in range(ntiles * 256):
|
|
79
|
+
t = tack.thread_id()
|
|
80
|
+
start = (i // 256) * _TILE + t * _PER_LANE
|
|
81
|
+
lanes[t] = 0
|
|
82
|
+
if start < n:
|
|
83
|
+
dst[start] = src[start]
|
|
84
|
+
total = dst[start]
|
|
85
|
+
for g in range(start + 1, min(start + _PER_LANE, n)):
|
|
86
|
+
dst[g] = src[g]
|
|
87
|
+
total += dst[g]
|
|
88
|
+
lanes[t] = total
|
|
89
|
+
tack.barrier()
|
|
90
|
+
step = 1
|
|
91
|
+
while step < 256:
|
|
92
|
+
below = lanes[t - step] if t >= step else lanes[t] * 0
|
|
93
|
+
tack.barrier()
|
|
94
|
+
if t >= step:
|
|
95
|
+
lanes[t] = lanes[t] + below
|
|
96
|
+
tack.barrier()
|
|
97
|
+
step = step * 2
|
|
98
|
+
if t == 0:
|
|
99
|
+
sums[i // 256] = lanes[255]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@tack.kernel
|
|
103
|
+
def _tile_apply(data, offsets, n, ntiles, inclusive):
|
|
104
|
+
"""Each tile's running sums, in place, from its offset."""
|
|
105
|
+
lanes = tack.shared_like(data, 256)
|
|
106
|
+
for i in range(ntiles * 256):
|
|
107
|
+
t = tack.thread_id()
|
|
108
|
+
start = (i // 256) * _TILE + t * _PER_LANE
|
|
109
|
+
lanes[t] = 0
|
|
110
|
+
if start < n:
|
|
111
|
+
total = data[start]
|
|
112
|
+
for g in range(start + 1, min(start + _PER_LANE, n)):
|
|
113
|
+
total += data[g]
|
|
114
|
+
lanes[t] = total
|
|
115
|
+
tack.barrier()
|
|
116
|
+
step = 1
|
|
117
|
+
while step < 256:
|
|
118
|
+
below = lanes[t - step] if t >= step else lanes[t] * 0
|
|
119
|
+
tack.barrier()
|
|
120
|
+
if t >= step:
|
|
121
|
+
lanes[t] = lanes[t] + below
|
|
122
|
+
tack.barrier()
|
|
123
|
+
step = step * 2
|
|
124
|
+
# The lanes before this one, read from its neighbour rather than
|
|
125
|
+
# subtracted, so floating sums are not rounded twice.
|
|
126
|
+
running = offsets[i // 256]
|
|
127
|
+
if t > 0:
|
|
128
|
+
running = running + lanes[t - 1]
|
|
129
|
+
for g in range(start, min(start + _PER_LANE, n)):
|
|
130
|
+
value = data[g]
|
|
131
|
+
if inclusive == 1:
|
|
132
|
+
running += value
|
|
133
|
+
data[g] = running
|
|
134
|
+
else:
|
|
135
|
+
data[g] = running
|
|
136
|
+
running += value
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@tack.kernel
|
|
140
|
+
def _scan_serial(src, dst, total, n, inclusive, close):
|
|
141
|
+
"""One thread scans ``[0, n)`` into ``dst`` and leaves the sum in
|
|
142
|
+
``total[0]`` -- and with ``close`` in ``dst[n]`` too, the end of the
|
|
143
|
+
offsets -- every value converted to ``dst``'s dtype on the copy so the
|
|
144
|
+
sums form in it, as the chunk kernels' do."""
|
|
145
|
+
for i in range(1):
|
|
146
|
+
dst[0] = src[0]
|
|
147
|
+
running = dst[0] * 0
|
|
148
|
+
for k in range(n):
|
|
149
|
+
dst[k] = src[k]
|
|
150
|
+
value = dst[k]
|
|
151
|
+
if inclusive == 1:
|
|
152
|
+
running += value
|
|
153
|
+
dst[k] = running
|
|
154
|
+
else:
|
|
155
|
+
dst[k] = running
|
|
156
|
+
running += value
|
|
157
|
+
total[0] = running
|
|
158
|
+
if close == 1:
|
|
159
|
+
dst[n] = running
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _count(n, *arrays):
|
|
163
|
+
"""Check the element count: the scan reads and writes [0, n)."""
|
|
164
|
+
n = int(n)
|
|
165
|
+
for f in arrays:
|
|
166
|
+
if not 0 <= n <= f.size:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"n={n} is outside [0, {f.size}] for an array of {f.size} elements")
|
|
169
|
+
return n
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _total(array, n):
|
|
173
|
+
"""array[n - 1] as a Python number: one element read on the host, which
|
|
174
|
+
on every backend is a copy of just that element and no launch."""
|
|
175
|
+
return array[n - 1]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@tack.kernel
|
|
179
|
+
def _close_from(dst, n, offsets, sums, nchunks):
|
|
180
|
+
# dst[n]: the last chunk's offset plus its sum, the whole's total.
|
|
181
|
+
for i in range(1):
|
|
182
|
+
dst[n] = offsets[nchunks - 1] + sums[nchunks - 1]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@tack.kernel
|
|
186
|
+
def _close_empty(dst):
|
|
187
|
+
for i in range(1):
|
|
188
|
+
dst[0] = dst[0] * 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _scan(src, dst, n, inclusive, want_total=True, close=False):
|
|
192
|
+
"""Scan ``src`` into ``dst`` (which may be ``src``) in ``dst``'s dtype and return
|
|
193
|
+
the total: chunk sums, then -- recursively -- their exclusive scan as the
|
|
194
|
+
chunks' offsets, then each chunk's own running sums. A few launches per
|
|
195
|
+
level, and a level per factor of ``_CHUNK``. Without ``want_total`` the
|
|
196
|
+
total is never read to the host, so nothing waits for the device; the
|
|
197
|
+
result is ``None``."""
|
|
198
|
+
from tack.runtime.dispatch import get_backend
|
|
199
|
+
|
|
200
|
+
tiles = get_backend().supports_workgroups
|
|
201
|
+
size = int(_TILE) if tiles else _CHUNK
|
|
202
|
+
if n == 0:
|
|
203
|
+
return 0 if want_total else None
|
|
204
|
+
if n <= (size if tiles else _SERIAL_MAX):
|
|
205
|
+
# One launch; with workgroups a level of one tile is one thread's
|
|
206
|
+
# worth anyway. The total is read as one element, not a launch.
|
|
207
|
+
total = tack.empty((1,), dst.dtype)
|
|
208
|
+
_scan_serial(src, dst, total, n, 1 if inclusive else 0, 1 if close else 0)
|
|
209
|
+
return total[0] if want_total else None
|
|
210
|
+
nchunks = (n + size - 1) // size
|
|
211
|
+
sums = tack.empty((nchunks,), dst.dtype)
|
|
212
|
+
if tiles:
|
|
213
|
+
_tile_sums(src, dst, sums, n, nchunks)
|
|
214
|
+
else:
|
|
215
|
+
_chunk_sums(src, dst, sums, n, _CHUNK, nchunks)
|
|
216
|
+
offsets = tack.empty((nchunks,), dst.dtype)
|
|
217
|
+
total = _scan(sums, offsets, nchunks, inclusive=False, want_total=want_total)
|
|
218
|
+
if tiles:
|
|
219
|
+
_tile_apply(dst, offsets, n, nchunks, 1 if inclusive else 0)
|
|
220
|
+
else:
|
|
221
|
+
_chunk_scan(dst, offsets, n, _CHUNK, nchunks, 1 if inclusive else 0)
|
|
222
|
+
if close:
|
|
223
|
+
_close_from(dst, n, offsets, sums, nchunks)
|
|
224
|
+
return total
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@on_inputs_device
|
|
228
|
+
def exclusive_scan(input_array, output_array, n, total=True, close=False):
|
|
229
|
+
"""Compute exclusive prefix sum on the inputs' device.
|
|
230
|
+
|
|
231
|
+
output[i] = sum(input[0..i-1]), output[0] = 0.
|
|
232
|
+
|
|
233
|
+
The scan runs in the output array's dtype; input values convert to it
|
|
234
|
+
on the copy, so any dtype the backend allocates works.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
input_array: array with input values.
|
|
238
|
+
output_array: array for the output offsets.
|
|
239
|
+
n: number of elements, at most either array's size. Zero writes
|
|
240
|
+
nothing and returns 0.
|
|
241
|
+
total: whether to return the total. Reading it waits for the
|
|
242
|
+
device (on Metal, for every launch queued so far); a caller
|
|
243
|
+
that needs only the offsets passes ``total=False`` and gets
|
|
244
|
+
``None``, and the scan stays queued.
|
|
245
|
+
close: also write the total at ``output[n]``, so ``output`` is the
|
|
246
|
+
``n + 1`` offsets of CSR rows (``output[0]`` is 0 when ``n`` is
|
|
247
|
+
0). In the scan's own launch where it is one, as for any array
|
|
248
|
+
of at most a tile or 4096 elements; ``output`` needs ``n + 1``
|
|
249
|
+
elements.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
The total of all input elements in the output's dtype, as a Python
|
|
253
|
+
int or float; ``None`` with ``total=False``.
|
|
254
|
+
"""
|
|
255
|
+
n = _count(n, input_array, output_array)
|
|
256
|
+
if close and output_array.size < n + 1:
|
|
257
|
+
raise ValueError(f"exclusive_scan with close writes output[{n}]: the output has "
|
|
258
|
+
f"{output_array.size} elements")
|
|
259
|
+
if n == 0:
|
|
260
|
+
# Nothing to scan; the empty sum is 0, as for Array.sum().
|
|
261
|
+
if close:
|
|
262
|
+
_close_empty(output_array)
|
|
263
|
+
return 0 if total else None
|
|
264
|
+
# Scan in the output's dtype, as the inclusive scan does: an i32 work
|
|
265
|
+
# buffer truncated float inputs and wrapped wider integers.
|
|
266
|
+
return _scan(input_array, output_array, n, inclusive=False, want_total=total, close=close)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@on_inputs_device
|
|
270
|
+
def inclusive_scan(input_array, output_array, n, total=True):
|
|
271
|
+
"""Compute inclusive prefix sum on the inputs' device.
|
|
272
|
+
|
|
273
|
+
output[i] = sum(input[0..i]).
|
|
274
|
+
|
|
275
|
+
The scan runs in the output array's dtype; input values convert to it
|
|
276
|
+
on the copy, so any dtype the backend allocates works.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
input_array: array with input values.
|
|
280
|
+
output_array: array for the output sums.
|
|
281
|
+
n: number of elements, at most either array's size. Zero writes
|
|
282
|
+
nothing and returns 0.
|
|
283
|
+
total: whether to return the total; see ``exclusive_scan``.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
The total of all input elements in the output's dtype, as a Python
|
|
287
|
+
int or float; ``None`` with ``total=False``.
|
|
288
|
+
"""
|
|
289
|
+
n = _count(n, input_array, output_array)
|
|
290
|
+
if n == 0:
|
|
291
|
+
return 0 if total else None
|
|
292
|
+
return _scan(input_array, output_array, n, inclusive=True, want_total=total)
|