numba-vulkan 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,79 @@
1
+ """Proof of concept: a Vulkan compute target for Numba."""
2
+
3
+ from numba_vulkan.autotune import autotune
4
+ from numba_vulkan.codegen import CompiledKernel
5
+ from numba_vulkan.dispatcher import VulkanDispatcher, jit
6
+ from numba_vulkan.errors import (
7
+ SpirvCodegenError,
8
+ VulkanPerformanceWarning,
9
+ VulkanPrecisionWarning,
10
+ VulkanSupportError,
11
+ VulkanUnsupportedError,
12
+ VulkanValidationWarning,
13
+ )
14
+ from numba_vulkan.runtime import (
15
+ DeviceArray,
16
+ device_array,
17
+ device_array_like,
18
+ event,
19
+ event_elapsed_time,
20
+ get_device,
21
+ list_devices,
22
+ pinned_array,
23
+ select_device,
24
+ stream,
25
+ synchronize,
26
+ to_device,
27
+ )
28
+ from numba_vulkan.stubs import (
29
+ atomic,
30
+ barrier,
31
+ global_id,
32
+ group_id,
33
+ local,
34
+ local_id,
35
+ local_size,
36
+ num_groups,
37
+ shared,
38
+ subgroup,
39
+ syncthreads,
40
+ )
41
+
42
+ __all__ = [
43
+ "CompiledKernel",
44
+ "DeviceArray",
45
+ "SpirvCodegenError",
46
+ "VulkanDispatcher",
47
+ "VulkanPerformanceWarning",
48
+ "VulkanPrecisionWarning",
49
+ "VulkanSupportError",
50
+ "VulkanUnsupportedError",
51
+ "VulkanValidationWarning",
52
+ "atomic",
53
+ "autotune",
54
+ "barrier",
55
+ "device_array",
56
+ "device_array_like",
57
+ "event",
58
+ "event_elapsed_time",
59
+ "get_device",
60
+ "global_id",
61
+ "group_id",
62
+ "jit",
63
+ "list_devices",
64
+ "local",
65
+ "local_id",
66
+ "local_size",
67
+ "num_groups",
68
+ "pinned_array",
69
+ "select_device",
70
+ "shared",
71
+ "stream",
72
+ "subgroup",
73
+ "synchronize",
74
+ "syncthreads",
75
+ "to_device",
76
+ ]
77
+
78
+ # Registers target="vulkan" with numba.vectorize and numba.guvectorize.
79
+ from numba_vulkan import vectorizers # noqa: F401
numba_vulkan/_emit.py ADDED
@@ -0,0 +1,98 @@
1
+ """Child process that turns LLVM IR into SPIR-V.
2
+
3
+ LLVM's SPIR-V backend aborts the whole process on input it cannot handle,
4
+ and it keeps state between modules that corrupts the second module
5
+ translated by one process. It therefore runs outside the user's
6
+ interpreter, and in a process of its own for every module.
7
+
8
+ Starting Python and importing llvmlite takes much longer than the
9
+ translation, so this process does that once and then forks for every
10
+ request, where the platform allows it. It serves requests until its
11
+ standard input is closed. A request is the length of the IR as an unsigned
12
+ 64-bit little-endian integer followed by the IR; the reply is framed in the
13
+ same way, with the length `FAILED` and no data if the backend failed. Its
14
+ message is then on standard error. See `numba_vulkan.codegen.Emitter`.
15
+ """
16
+
17
+ import os
18
+ import signal
19
+ import struct
20
+ import sys
21
+
22
+ import llvmlite.binding as llvm
23
+
24
+ HEADER = struct.Struct("<Q")
25
+ FAILED = 2**64 - 1
26
+ CAN_FORK = hasattr(os, "fork")
27
+ # Lets the backend use float atomics; they are emitted only for devices that
28
+ # support them (see numba_vulkan.narrowing.Mode).
29
+ EXTENSIONS = "+SPV_EXT_shader_atomic_float_add"
30
+
31
+
32
+ def _read(stream, count):
33
+ """Read exactly `count` bytes, or return ``None`` at end of input."""
34
+ data = b""
35
+ while len(data) < count:
36
+ chunk = stream.read(count - len(data))
37
+ if not chunk:
38
+ return None
39
+ data += chunk
40
+ return data
41
+
42
+
43
+ def _translate(target, text, stdout):
44
+ """Translate one module and write the reply."""
45
+ module = llvm.parse_assembly(text.decode())
46
+ # codemodel="default": with llvmlite's "jitdefault", Windows gets "-elf"
47
+ # appended to the triple, and ELF output makes the backend abort.
48
+ machine = target.create_target_machine(opt=0, codemodel="default")
49
+ spirv = machine.emit_object(module)
50
+ stdout.write(HEADER.pack(len(spirv)) + spirv)
51
+ stdout.flush()
52
+
53
+
54
+ def main():
55
+ """Serve translation requests on standard input and output.
56
+
57
+ The target triple is taken from the first command line argument.
58
+ Without ``fork``, the process translates a single module itself and
59
+ exits.
60
+ """
61
+ triple = sys.argv[1]
62
+ llvm.set_option("", f"--spirv-ext={EXTENSIONS}")
63
+ llvm.initialize_all_targets()
64
+ llvm.initialize_all_asmprinters()
65
+ target = llvm.Target.from_triple(triple)
66
+ stdin, stdout = sys.stdin.buffer, sys.stdout.buffer
67
+ while True:
68
+ header = _read(stdin, HEADER.size)
69
+ if header is None:
70
+ return
71
+ text = _read(stdin, HEADER.unpack(header)[0])
72
+ if text is None:
73
+ return
74
+ if not CAN_FORK:
75
+ _translate(target, text, stdout)
76
+ return
77
+ pid = os.fork()
78
+ if pid == 0:
79
+ status = 1
80
+ try:
81
+ _translate(target, text, stdout)
82
+ status = 0
83
+ except BaseException as exc: # noqa: BLE001 - reported to the parent
84
+ print(f"{type(exc).__name__}: {exc}", file=sys.stderr, flush=True)
85
+ finally:
86
+ os._exit(status)
87
+ status = os.waitpid(pid, 0)[1]
88
+ if os.WIFSIGNALED(status):
89
+ # A crash leaves no message of its own.
90
+ name = signal.Signals(os.WTERMSIG(status)).name
91
+ print(f"the backend was killed by {name}", file=sys.stderr, flush=True)
92
+ if status != 0:
93
+ stdout.write(HEADER.pack(FAILED))
94
+ stdout.flush()
95
+
96
+
97
+ if __name__ == "__main__":
98
+ main()
@@ -0,0 +1,467 @@
1
+ """Reductions over arrays, written in Python on top of element access.
2
+
3
+ Numba's own ``sum``, ``min``... walk the array through its data pointer,
4
+ which arrays on this target do not have. The versions here are overloads
5
+ for the ``vulkan`` target that read the elements by position instead (see
6
+ `numba_vulkan.vkimpl.flat_item`), so they work for arrays and views of any
7
+ dimensionality. They reduce over all elements, or, with a constant
8
+ ``axis``, along one axis: kernels cannot allocate the resulting array, so
9
+ ``a.sum(axis=0)`` is an array expression (see `AxisReduction`), whose
10
+ elements are computed where they are read.
11
+
12
+ Each is available as a method (``a.sum()``) and as the NumPy function
13
+ (``np.sum(a)``).
14
+ """
15
+
16
+ import functools
17
+ from dataclasses import dataclass
18
+
19
+ import numpy as np
20
+ from numba.core import cgutils, errors, types
21
+ from numba.core.extending import intrinsic, overload, overload_method
22
+
23
+ from numba_vulkan.vkimpl import flat_item
24
+ from numba_vulkan.vktypes import VulkanArray, VulkanExpr
25
+
26
+ TARGET = "vulkan"
27
+
28
+
29
+ def _accumulator(dtype):
30
+ """Type in which sums and products of ``dtype`` elements are formed.
31
+
32
+ As in NumPy, integers and booleans widen to the platform integer.
33
+
34
+ Parameters
35
+ ----------
36
+ dtype : numba.types.Type
37
+ Element type.
38
+
39
+ Returns
40
+ -------
41
+ numba.types.Type
42
+ """
43
+ if isinstance(dtype, types.Boolean):
44
+ return types.intp
45
+ if isinstance(dtype, types.Integer):
46
+ return types.intp if dtype.signed else types.uintp
47
+ return dtype
48
+
49
+
50
+ def _check_whole_axis(a, axis):
51
+ """Raise unless `axis` is None, or a valid axis of a 1-d array."""
52
+
53
+
54
+ @overload(_check_whole_axis, target=TARGET)
55
+ def _check_whole_axis_impl(a, axis):
56
+ """Build `_check_whole_axis`: reductions over all elements take it."""
57
+ if axis is None or isinstance(axis, (types.NoneType, types.Omitted)):
58
+ return lambda a, axis: None
59
+
60
+ def impl(a, axis):
61
+ if axis != 0 and axis != -1:
62
+ raise ValueError("axis is out of bounds for an array of dimension 1")
63
+
64
+ return impl
65
+
66
+
67
+ def _sum(a):
68
+ """Build ``sum``: the sum of all elements."""
69
+ acc = _accumulator(a.dtype)
70
+
71
+ def impl(a, axis=None):
72
+ _check_whole_axis(a, axis)
73
+ total = acc(0)
74
+ for k in range(a.size):
75
+ total += flat_item(a, k)
76
+ return total
77
+
78
+ return impl
79
+
80
+
81
+ def _prod(a):
82
+ """Build ``prod``: the product of all elements."""
83
+ acc = _accumulator(a.dtype)
84
+
85
+ def impl(a, axis=None):
86
+ _check_whole_axis(a, axis)
87
+ total = acc(1)
88
+ for k in range(a.size):
89
+ total *= flat_item(a, k)
90
+ return total
91
+
92
+ return impl
93
+
94
+
95
+ def _mean(a):
96
+ """Build ``mean``: float64 for integers, the element type otherwise."""
97
+ acc = a.dtype if isinstance(a.dtype, types.Float) else types.float64
98
+
99
+ def impl(a, axis=None):
100
+ _check_whole_axis(a, axis)
101
+ total = acc(0)
102
+ for k in range(a.size):
103
+ total += flat_item(a, k)
104
+ return total / acc(a.size)
105
+
106
+ return impl
107
+
108
+
109
+ def _min(a):
110
+ """Build ``min``; raises ``ValueError`` for an empty array."""
111
+
112
+ def impl(a, axis=None):
113
+ _check_whole_axis(a, axis)
114
+ if a.size == 0:
115
+ raise ValueError("zero-size array has no minimum")
116
+ best = flat_item(a, 0)
117
+ for k in range(1, a.size):
118
+ value = flat_item(a, k)
119
+ # written so that a NaN, once found, is kept, as in NumPy
120
+ if value < best or value != value: # noqa: PLR0124 - NaN, for any type
121
+ best = value
122
+ return best
123
+
124
+ return impl
125
+
126
+
127
+ def _max(a):
128
+ """Build ``max``; raises ``ValueError`` for an empty array."""
129
+
130
+ def impl(a, axis=None):
131
+ _check_whole_axis(a, axis)
132
+ if a.size == 0:
133
+ raise ValueError("zero-size array has no maximum")
134
+ best = flat_item(a, 0)
135
+ for k in range(1, a.size):
136
+ value = flat_item(a, k)
137
+ if value > best or value != value: # noqa: PLR0124 - NaN, for any type
138
+ best = value
139
+ return best
140
+
141
+ return impl
142
+
143
+
144
+ def _argmin(a):
145
+ """Build ``argmin``: row-major position of the first minimum."""
146
+
147
+ def impl(a, axis=None):
148
+ _check_whole_axis(a, axis)
149
+ if a.size == 0:
150
+ raise ValueError("attempt to get argmin of an empty sequence")
151
+ best, where = flat_item(a, 0), 0
152
+ for k in range(1, a.size):
153
+ value = flat_item(a, k)
154
+ if value < best:
155
+ best, where = value, k
156
+ return where
157
+
158
+ return impl
159
+
160
+
161
+ def _argmax(a):
162
+ """Build ``argmax``: row-major position of the first maximum."""
163
+
164
+ def impl(a, axis=None):
165
+ _check_whole_axis(a, axis)
166
+ if a.size == 0:
167
+ raise ValueError("attempt to get argmax of an empty sequence")
168
+ best, where = flat_item(a, 0), 0
169
+ for k in range(1, a.size):
170
+ value = flat_item(a, k)
171
+ if value > best:
172
+ best, where = value, k
173
+ return where
174
+
175
+ return impl
176
+
177
+
178
+ def _any(a):
179
+ """Build ``any``: whether some element is non-zero."""
180
+
181
+ def impl(a, axis=None):
182
+ _check_whole_axis(a, axis)
183
+ for k in range(a.size):
184
+ if flat_item(a, k):
185
+ return True
186
+ return False
187
+
188
+ return impl
189
+
190
+
191
+ def _all(a):
192
+ """Build ``all``: whether every element is non-zero."""
193
+
194
+ def impl(a, axis=None):
195
+ _check_whole_axis(a, axis)
196
+ for k in range(a.size):
197
+ if not flat_item(a, k):
198
+ return False
199
+ return True
200
+
201
+ return impl
202
+
203
+
204
+ @dataclass(frozen=True)
205
+ class AxisReduction:
206
+ """The operation of an array expression that reduces along an axis.
207
+
208
+ ``a.sum(axis=k)`` is a `numba_vulkan.vktypes.VulkanExpr` with this as
209
+ its operation, and ``a`` and the axis as its operands; the axis is a
210
+ run-time value, as in Numba's ``sum(axis=...)`` on the CPU, since the
211
+ number of dimensions of the result does not depend on it. Its shape is
212
+ that of ``a`` without the axis; each element is a loop along the axis,
213
+ written in Python and compiled where the element is read (see
214
+ `numba_vulkan.vkimpl.expression_element`).
215
+
216
+ Parameters
217
+ ----------
218
+ name : str
219
+ The reduction: a key of ``_REDUCTIONS``.
220
+ acc : numba.types.Type
221
+ Type in which sums, products and means are formed.
222
+ """
223
+
224
+ name: str
225
+ acc: types.Type
226
+ # Tells numba_vulkan.vkimpl how to compute the shape and the elements.
227
+ reduces_axis = True
228
+
229
+ @property
230
+ def __name__(self):
231
+ """Name used in the expression's type name."""
232
+ return f"{self.name}(axis)"
233
+
234
+ def element_function(self, ndim):
235
+ """The function that computes one element.
236
+
237
+ Parameters
238
+ ----------
239
+ ndim : int
240
+ Dimensions of the operand.
241
+
242
+ Returns
243
+ -------
244
+ function
245
+ Takes the operand, the axis (made non-negative, and checked
246
+ when the shape was computed) and one index per dimension of
247
+ the result.
248
+ """
249
+ return _element_function(self, ndim)
250
+
251
+
252
+ # Loops that compute one element of a reduction along an axis; ``{item}``
253
+ # reads the element at position ``k`` along it, ``ACC`` is the accumulator
254
+ # type.
255
+ _ELEMENT_BODIES = {
256
+ "sum": """
257
+ total = ACC(0)
258
+ for k in range(n):
259
+ total += {item}
260
+ return total""",
261
+ "prod": """
262
+ total = ACC(1)
263
+ for k in range(n):
264
+ total *= {item}
265
+ return total""",
266
+ "mean": """
267
+ total = ACC(0)
268
+ for k in range(n):
269
+ total += {item}
270
+ return total / ACC(n)""",
271
+ "min": """
272
+ if n == 0:
273
+ raise ValueError("zero-size array has no minimum")
274
+ k = 0
275
+ best = {item}
276
+ for k in range(1, n):
277
+ value = {item}
278
+ if value < best or value != value: # noqa: PLR0124 - NaN
279
+ best = value
280
+ return best""",
281
+ "max": """
282
+ if n == 0:
283
+ raise ValueError("zero-size array has no maximum")
284
+ k = 0
285
+ best = {item}
286
+ for k in range(1, n):
287
+ value = {item}
288
+ if value > best or value != value: # noqa: PLR0124 - NaN
289
+ best = value
290
+ return best""",
291
+ "argmin": """
292
+ if n == 0:
293
+ raise ValueError("attempt to get argmin of an empty sequence")
294
+ k = 0
295
+ best, where = {item}, 0
296
+ for k in range(1, n):
297
+ value = {item}
298
+ if value < best:
299
+ best, where = value, k
300
+ return where""",
301
+ "argmax": """
302
+ if n == 0:
303
+ raise ValueError("attempt to get argmax of an empty sequence")
304
+ k = 0
305
+ best, where = {item}, 0
306
+ for k in range(1, n):
307
+ value = {item}
308
+ if value > best:
309
+ best, where = value, k
310
+ return where""",
311
+ "any": """
312
+ for k in range(n):
313
+ if {item}:
314
+ return True
315
+ return False""",
316
+ "all": """
317
+ for k in range(n):
318
+ if not {item}:
319
+ return False
320
+ return True""",
321
+ }
322
+
323
+
324
+ @functools.cache
325
+ def _element_function(op, ndim):
326
+ """Write and define the function of `AxisReduction.element_function`."""
327
+ outer = [f"i{d}" for d in range(ndim - 1)]
328
+ position = []
329
+ for d in range(ndim):
330
+ # Index along operand axis d: an index of the result before the
331
+ # reduced axis, k on it, and the next index of the result after it.
332
+ if d == 0:
333
+ position.append("(k if axis == 0 else i0)")
334
+ elif d == ndim - 1:
335
+ position.append(f"(k if axis == {d} else i{d - 1})")
336
+ else:
337
+ position.append(
338
+ f"(i{d} if axis > {d} else (k if axis == {d} else i{d - 1}))"
339
+ )
340
+ body = _ELEMENT_BODIES[op.name].format(item=f"a[{', '.join(position)}]")
341
+ source = f"def element(a, axis, {', '.join(outer)}):\n n = a.shape[axis]{body}\n"
342
+ scope = {"ACC": op.acc}
343
+ exec(source, scope) # noqa: S102 - generated from the table above
344
+ return scope["element"]
345
+
346
+
347
+ def _result_type(name, dtype):
348
+ """Element type of a reduction of `dtype` elements."""
349
+ if name in ("sum", "prod"):
350
+ return _accumulator(dtype)
351
+ if name == "mean":
352
+ return dtype if isinstance(dtype, types.Float) else types.float64
353
+ if name in ("argmin", "argmax"):
354
+ return types.intp
355
+ if name in ("any", "all"):
356
+ return types.boolean
357
+ return dtype
358
+
359
+
360
+ @functools.cache
361
+ def _axis_reducer(name):
362
+ """An intrinsic that makes the expression of a reduction along an axis."""
363
+
364
+ @intrinsic(target=TARGET)
365
+ def reduce_along(typingctx, a, axis):
366
+ acc = _result_type("mean" if name == "mean" else "sum", a.dtype)
367
+ op = AxisReduction(name, acc)
368
+ dtype = _result_type(name, a.dtype)
369
+ exprty = VulkanExpr(op, (a, types.intp), dtype, a.ndim - 1)
370
+
371
+ def codegen(context, builder, sig, args):
372
+ proxy = cgutils.create_struct_proxy(sig.return_type)(context, builder)
373
+ proxy.operand0 = args[0]
374
+ proxy.operand1 = context.cast(builder, args[1], sig.args[1], types.intp)
375
+ return proxy._getvalue()
376
+
377
+ return exprty(a, axis), codegen
378
+
379
+ return reduce_along
380
+
381
+
382
+ _REDUCTIONS = {
383
+ "sum": (_sum, [np.sum]),
384
+ "prod": (_prod, [np.prod]),
385
+ "mean": (_mean, [np.mean]),
386
+ "min": (_min, [np.min, min]),
387
+ "max": (_max, [np.max, max]),
388
+ "argmin": (_argmin, [np.argmin]),
389
+ "argmax": (_argmax, [np.argmax]),
390
+ "any": (_any, [np.any]),
391
+ "all": (_all, [np.all]),
392
+ }
393
+
394
+
395
+ def _register(name, factory, functions):
396
+ """Register a reduction as an array method and for its functions.
397
+
398
+ Parameters
399
+ ----------
400
+ name : str
401
+ Name of the array method.
402
+ factory : callable
403
+ Builds the implementation for an array type.
404
+ functions : list of callable
405
+ Functions that mean the same when called with one array.
406
+ """
407
+
408
+ def typer(a, axis=None):
409
+ """Select the implementation for arrays and expressions of this target."""
410
+ if not isinstance(a, (VulkanArray, VulkanExpr)):
411
+ return None
412
+ if axis is None or isinstance(axis, (types.NoneType, types.Omitted)):
413
+ return factory(a)
414
+ if not isinstance(axis, types.Integer):
415
+ raise errors.TypingError(f"{name}(axis=...) takes an integer axis")
416
+ ndim = a.ndim
417
+ if ndim == 1:
418
+ return factory(a) # a scalar, as in NumPy; it checks the axis
419
+ reduce_along = _axis_reducer(name)
420
+
421
+ def impl(a, axis=None):
422
+ # Checked where the shape is computed: raising here would make
423
+ # the array of the expression depend on the branch taken.
424
+ return reduce_along(a, axis)
425
+
426
+ return impl
427
+
428
+ def builtin_typer(a):
429
+ """``min(a)`` and ``max(a)`` on an array: over all elements."""
430
+ if isinstance(a, (VulkanArray, VulkanExpr)):
431
+ return factory(a) # its axis keeps its default
432
+ return None
433
+
434
+ overload_method(VulkanArray, name, target=TARGET)(typer)
435
+ overload_method(VulkanExpr, name, target=TARGET)(typer)
436
+ for function in functions:
437
+ if function in (min, max):
438
+ overload(function, target=TARGET, strict=False)(builtin_typer)
439
+ else:
440
+ overload(function, target=TARGET)(typer)
441
+
442
+
443
+ for _name, (_factory, _functions) in _REDUCTIONS.items():
444
+ _register(_name, _factory, _functions)
445
+
446
+
447
+ @overload(np.dot, target=TARGET)
448
+ def _dot(a, b):
449
+ """Build ``np.dot`` for two 1-d arrays."""
450
+ if not (
451
+ isinstance(a, (VulkanArray, VulkanExpr))
452
+ and isinstance(b, (VulkanArray, VulkanExpr))
453
+ ):
454
+ return None
455
+ if a.ndim != 1 or b.ndim != 1:
456
+ return None
457
+ acc = np.result_type(str(a.dtype), str(b.dtype)).type
458
+
459
+ def impl(a, b):
460
+ if a.shape[0] != b.shape[0]:
461
+ raise ValueError("incompatible array sizes for np.dot(a, b)")
462
+ total = acc(0)
463
+ for k in range(a.shape[0]):
464
+ total += flat_item(a, k) * flat_item(b, k)
465
+ return total
466
+
467
+ return impl