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.
Files changed (95) hide show
  1. tack/__init__.py +294 -0
  2. tack/algorithms/__init__.py +60 -0
  3. tack/algorithms/copy.py +49 -0
  4. tack/algorithms/scan.py +292 -0
  5. tack/algorithms/segmented.py +612 -0
  6. tack/algorithms/sort.py +626 -0
  7. tack/algorithms/stats.py +282 -0
  8. tack/arrays.py +820 -0
  9. tack/cache.py +90 -0
  10. tack/codegen/__init__.py +2 -0
  11. tack/codegen/atomics.py +40 -0
  12. tack/codegen/bounds.py +63 -0
  13. tack/codegen/c_family.py +219 -0
  14. tack/codegen/cuda_gen.py +767 -0
  15. tack/codegen/float_division.py +66 -0
  16. tack/codegen/hip_gen.py +55 -0
  17. tack/codegen/identifiers.py +58 -0
  18. tack/codegen/integer_division.py +47 -0
  19. tack/codegen/integer_ops.py +138 -0
  20. tack/codegen/llvm_gen.py +1516 -0
  21. tack/codegen/msl_gen.py +746 -0
  22. tack/codegen/opencl_gen.py +579 -0
  23. tack/codegen/reductions.py +109 -0
  24. tack/codegen/tables.py +53 -0
  25. tack/codegen/warp_gen.py +761 -0
  26. tack/communication/__init__.py +43 -0
  27. tack/communication/_blocks.py +207 -0
  28. tack/communication/_kernels.py +83 -0
  29. tack/communication/_plan.py +136 -0
  30. tack/communication/_route.py +186 -0
  31. tack/communication/_sort.py +170 -0
  32. tack/communication/_world.py +165 -0
  33. tack/lang/__init__.py +2 -0
  34. tack/lang/array.py +1338 -0
  35. tack/lang/ast_copy.py +35 -0
  36. tack/lang/ast_transform.py +1147 -0
  37. tack/lang/atomic_support.py +85 -0
  38. tack/lang/call_bindings.py +102 -0
  39. tack/lang/constant.py +227 -0
  40. tack/lang/device_class.py +32 -0
  41. tack/lang/dlpack.py +651 -0
  42. tack/lang/func.py +114 -0
  43. tack/lang/inspect_kernel.py +228 -0
  44. tack/lang/ir.py +530 -0
  45. tack/lang/ir_bounds.py +154 -0
  46. tack/lang/ir_names.py +47 -0
  47. tack/lang/ir_optimize.py +224 -0
  48. tack/lang/ir_pack_scalars.py +105 -0
  49. tack/lang/ir_resolve.py +149 -0
  50. tack/lang/ir_traversal.py +189 -0
  51. tack/lang/ir_type_annotate.py +514 -0
  52. tack/lang/ir_verify.py +280 -0
  53. tack/lang/kernel.py +379 -0
  54. tack/lang/lowering/__init__.py +25 -0
  55. tack/lang/lowering/arrays.py +299 -0
  56. tack/lang/lowering/bounds.py +107 -0
  57. tack/lang/lowering/checks.py +190 -0
  58. tack/lang/lowering/inlining.py +344 -0
  59. tack/lang/lowering/local_arrays.py +135 -0
  60. tack/lang/lowering/loops.py +387 -0
  61. tack/lang/lowering/values.py +86 -0
  62. tack/lang/lowering/vectors.py +664 -0
  63. tack/lang/parallel_dims.py +90 -0
  64. tack/lang/source_validation.py +185 -0
  65. tack/lang/template_rewrite.py +1176 -0
  66. tack/lang/type_inference.py +144 -0
  67. tack/lang/types.py +249 -0
  68. tack/lang/workgroup_participation.py +208 -0
  69. tack/lang/workgroup_support.py +51 -0
  70. tack/math.py +93 -0
  71. tack/py.typed +0 -0
  72. tack/random.py +137 -0
  73. tack/runtime/__init__.py +2 -0
  74. tack/runtime/aio.py +115 -0
  75. tack/runtime/backend.py +287 -0
  76. tack/runtime/buffer_cache.py +90 -0
  77. tack/runtime/cpu.py +2329 -0
  78. tack/runtime/cpu_pool.py +804 -0
  79. tack/runtime/cuda_backend.py +1411 -0
  80. tack/runtime/debug_bounds.py +122 -0
  81. tack/runtime/device_fill.py +68 -0
  82. tack/runtime/disk_cache.py +1001 -0
  83. tack/runtime/dispatch.py +454 -0
  84. tack/runtime/gpu_backend.py +184 -0
  85. tack/runtime/hip_backend.py +1163 -0
  86. tack/runtime/kernel_utils.py +1518 -0
  87. tack/runtime/level_zero_backend.py +1997 -0
  88. tack/runtime/metal.py +890 -0
  89. tack/runtime/nvrtc_version.py +28 -0
  90. tack/runtime/reductions.py +38 -0
  91. tack/runtime/warp_backend.py +266 -0
  92. tack_core-0.7.0.dist-info/METADATA +116 -0
  93. tack_core-0.7.0.dist-info/RECORD +95 -0
  94. tack_core-0.7.0.dist-info/WHEEL +4 -0
  95. 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
+ ]
@@ -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)
@@ -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)