kernox 1.0.1__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.
- kernox/__init__.py +101 -0
- kernox/_env.py +13 -0
- kernox/_plan_key.py +118 -0
- kernox/_symox_compat.py +128 -0
- kernox/autograd.py +464 -0
- kernox/backends/__init__.py +1 -0
- kernox/backends/base.py +252 -0
- kernox/backends/cuda/__init__.py +10 -0
- kernox/backends/cuda/_driver.py +335 -0
- kernox/backends/cuda/_kernel.py +886 -0
- kernox/backends/cuda/backend.py +796 -0
- kernox/backends/hip/__init__.py +11 -0
- kernox/backends/hip/_cache.py +85 -0
- kernox/backends/hip/_hip.py +252 -0
- kernox/backends/hip/_kernel.py +801 -0
- kernox/backends/hip/backend.py +711 -0
- kernox/backends/hip/diagnostics.py +99 -0
- kernox/backends/sycl/__init__.py +601 -0
- kernox/backends/sycl/_pair_local.py +67 -0
- kernox/backends/sycl/codegen.py +888 -0
- kernox/backends/sycl/toolchain.py +179 -0
- kernox/builder.py +371 -0
- kernox/codegen/__init__.py +1 -0
- kernox/codegen/pair.py +512 -0
- kernox/complex.py +407 -0
- kernox/diagnostics/__init__.py +335 -0
- kernox/diagnostics/__main__.py +5 -0
- kernox/errors.py +33 -0
- kernox/expr.py +615 -0
- kernox/linalg.py +388 -0
- kernox/ops.py +501 -0
- kernox/oracle.py +626 -0
- kernox/plan.py +157 -0
- kernox/ranges.py +246 -0
- kernox/runtime.py +977 -0
- kernox/tuning.py +404 -0
- kernox-1.0.1.dist-info/METADATA +151 -0
- kernox-1.0.1.dist-info/RECORD +43 -0
- kernox-1.0.1.dist-info/WHEEL +5 -0
- kernox-1.0.1.dist-info/licenses/LICENSE-APACHE +202 -0
- kernox-1.0.1.dist-info/licenses/LICENSE-MIT +21 -0
- kernox-1.0.1.dist-info/licenses/THIRD_PARTY.md +20 -0
- kernox-1.0.1.dist-info/top_level.txt +1 -0
kernox/__init__.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""kernox: fused symbolic reductions on GPUs.
|
|
2
|
+
|
|
3
|
+
Documentation: https://keckj.github.io/kernox/. Expressions are symox objects; ``compile`` produces a
|
|
4
|
+
callable that validates its inputs before any native launch.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .errors import (
|
|
8
|
+
BackendUnavailableError,
|
|
9
|
+
ConstantConversionError,
|
|
10
|
+
KernoxError,
|
|
11
|
+
UnsupportedError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
from .expr import (
|
|
15
|
+
Formula,
|
|
16
|
+
Param,
|
|
17
|
+
ReductionExpr,
|
|
18
|
+
Vi,
|
|
19
|
+
Vj,
|
|
20
|
+
abs,
|
|
21
|
+
acos,
|
|
22
|
+
asin,
|
|
23
|
+
atan,
|
|
24
|
+
atan2,
|
|
25
|
+
cat,
|
|
26
|
+
ceiling,
|
|
27
|
+
clamp,
|
|
28
|
+
constant,
|
|
29
|
+
cos,
|
|
30
|
+
cosh,
|
|
31
|
+
eq,
|
|
32
|
+
exp,
|
|
33
|
+
floor,
|
|
34
|
+
log,
|
|
35
|
+
maximum,
|
|
36
|
+
minimum,
|
|
37
|
+
ne,
|
|
38
|
+
rsqrt,
|
|
39
|
+
sign,
|
|
40
|
+
sin,
|
|
41
|
+
sinh,
|
|
42
|
+
sqrt,
|
|
43
|
+
square,
|
|
44
|
+
tan,
|
|
45
|
+
tanh,
|
|
46
|
+
where,
|
|
47
|
+
)
|
|
48
|
+
from .ops import ( # noqa: E402 - also attaches the PyKeOps methods to Formula
|
|
49
|
+
bspline,
|
|
50
|
+
clampint,
|
|
51
|
+
elem,
|
|
52
|
+
extract,
|
|
53
|
+
ifelse,
|
|
54
|
+
inv,
|
|
55
|
+
kron,
|
|
56
|
+
matvecmult,
|
|
57
|
+
mod,
|
|
58
|
+
normalize,
|
|
59
|
+
one_hot,
|
|
60
|
+
relu,
|
|
61
|
+
round,
|
|
62
|
+
sinc,
|
|
63
|
+
sinxdivx,
|
|
64
|
+
softdtw_sqdist,
|
|
65
|
+
grad,
|
|
66
|
+
diff,
|
|
67
|
+
grad_matrix,
|
|
68
|
+
divergence,
|
|
69
|
+
laplacian,
|
|
70
|
+
trace_operator,
|
|
71
|
+
sqdist,
|
|
72
|
+
step,
|
|
73
|
+
tensordot,
|
|
74
|
+
tensorprod,
|
|
75
|
+
vecmatmult,
|
|
76
|
+
weightedsqdist,
|
|
77
|
+
weightedsqnorm,
|
|
78
|
+
xlogx,
|
|
79
|
+
)
|
|
80
|
+
from .complex import ComplexFormula, angle, complex_formula, conj, exp1j, imag, imag2complex, real, real2complex
|
|
81
|
+
from .linalg import LinearOperator, conjugate_gradient
|
|
82
|
+
from .ranges import Ranges, ranges_from_labels
|
|
83
|
+
from .runtime import RangedReduction, Reduction, compile, get_backend, register_backend
|
|
84
|
+
from .tuning import export_wisdom, forget_wisdom, import_wisdom
|
|
85
|
+
from .diagnostics import diagnostics # noqa: E402 - also `python -m kernox.diagnostics`
|
|
86
|
+
|
|
87
|
+
__all__ = [
|
|
88
|
+
"Vi", "Vj", "Param", "Formula", "ReductionExpr", "Reduction", "RangedReduction", "compile",
|
|
89
|
+
"Ranges", "ranges_from_labels", "LinearOperator", "conjugate_gradient",
|
|
90
|
+
"ComplexFormula", "real2complex", "imag2complex", "exp1j", "complex_formula", "conj", "angle", "real", "imag",
|
|
91
|
+
"register_backend", "get_backend", "diagnostics", "export_wisdom", "import_wisdom", "forget_wisdom", "constant", "where", "eq", "ne", "cat",
|
|
92
|
+
"exp", "log", "sqrt", "rsqrt", "sin", "cos", "tan", "asin", "acos", "atan", "atan2",
|
|
93
|
+
"sinh", "cosh", "tanh", "abs", "sign", "floor", "ceiling", "square",
|
|
94
|
+
"minimum", "maximum", "clamp",
|
|
95
|
+
"relu", "step", "sinxdivx", "sinc", "xlogx", "inv", "round", "mod", "clampint", "ifelse",
|
|
96
|
+
"normalize", "sqdist", "weightedsqnorm", "weightedsqdist", "one_hot", "elem", "extract",
|
|
97
|
+
"matvecmult", "vecmatmult", "tensorprod", "tensordot", "kron", "bspline", "softdtw_sqdist",
|
|
98
|
+
"grad", "diff", "grad_matrix", "divergence", "laplacian", "trace_operator",
|
|
99
|
+
"KernoxError", "ValidationError", "ConstantConversionError",
|
|
100
|
+
"BackendUnavailableError", "UnsupportedError",
|
|
101
|
+
]
|
kernox/_env.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Reading of the ``KERNOX_*`` environment variables.
|
|
2
|
+
|
|
3
|
+
An empty value is treated as an unset variable.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def getenv(name: str, default: str | None = None) -> str | None:
|
|
12
|
+
"""Value of ``name`` if it is set and non-empty, otherwise ``default``."""
|
|
13
|
+
return os.environ.get(name) or default
|
kernox/_plan_key.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Stable key of a :class:`kernox.plan.Plan` (contract v0).
|
|
2
|
+
|
|
3
|
+
The key is the hexadecimal sha256 of a canonical JSON serialization of:
|
|
4
|
+
|
|
5
|
+
- the key format version (``PLAN_KEY_FORMAT``);
|
|
6
|
+
- for each argument, in ``plan.args`` order: name, role, dimension and the
|
|
7
|
+
symox ``srepr`` of each component symbol (name + assumptions);
|
|
8
|
+
- the symox ``srepr`` of each output, in order; if the unfolded tree of an
|
|
9
|
+
output exceeds ``DAG_THRESHOLD`` nodes (deep shared DAG: ``srepr`` would be
|
|
10
|
+
exponential), the output is serialized as a graph: ``["dag", nodes]``, the
|
|
11
|
+
list of distinct nodes in postfix order, each one being ``srepr`` for an atom
|
|
12
|
+
or ``[class, head, child indices]`` otherwise. Keys of existing plans
|
|
13
|
+
(smaller trees) are unchanged;
|
|
14
|
+
- reduction (kind, K), ``dtype``, ``acc_dtype``, ``index_dtype``, ``fast_math``, ``fp_contract``;
|
|
15
|
+
- ``sum_scheme`` (amendment v0.7) only if it differs from ``"direct"``: the
|
|
16
|
+
key of an existing plan is unchanged.
|
|
17
|
+
- ``batch_rank`` (amendment v0.6) only if nonzero: the key of a plan
|
|
18
|
+
without batch is unchanged;
|
|
19
|
+
- ``ranges`` (amendment v0.6 §2) only if true.
|
|
20
|
+
|
|
21
|
+
``plan.meta`` is not part of the key (diagnostic information only).
|
|
22
|
+
The key depends neither on ``PYTHONHASHSEED`` nor on object creation order
|
|
23
|
+
(tested in ``tests/test_plan_key.py``). Backends add the generator version,
|
|
24
|
+
compiler, options and architecture to it (see the contract).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import hashlib
|
|
30
|
+
import json
|
|
31
|
+
|
|
32
|
+
PLAN_KEY_FORMAT = "kernox-plan-key-v1"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
#: unfolded tree size beyond which an output is serialized as a graph
|
|
36
|
+
DAG_THRESHOLD = 200_000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _srepr(expr) -> str:
|
|
40
|
+
from symox import srepr
|
|
41
|
+
|
|
42
|
+
return srepr(expr)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _tree_size(expr, limit):
|
|
46
|
+
"""Node count of the unfolded tree (memoized per subexpression), capped at ``limit``."""
|
|
47
|
+
memo = {}
|
|
48
|
+
|
|
49
|
+
def size(e):
|
|
50
|
+
hit = memo.get(e)
|
|
51
|
+
if hit is None:
|
|
52
|
+
hit = min(limit, 1 + sum(size(a) for a in e.args))
|
|
53
|
+
memo[e] = hit
|
|
54
|
+
return hit
|
|
55
|
+
|
|
56
|
+
return size(expr)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _dag(expr) -> list:
|
|
60
|
+
"""Canonical graph serialization: distinct nodes in postfix order."""
|
|
61
|
+
from symox import srepr
|
|
62
|
+
|
|
63
|
+
index, nodes = {}, []
|
|
64
|
+
|
|
65
|
+
def visit(e):
|
|
66
|
+
hit = index.get(e)
|
|
67
|
+
if hit is not None:
|
|
68
|
+
return hit
|
|
69
|
+
if not e.args:
|
|
70
|
+
node = srepr(e)
|
|
71
|
+
else:
|
|
72
|
+
children = [visit(a) for a in e.args]
|
|
73
|
+
node = [type(e).__name__, str(e.func), children]
|
|
74
|
+
index[e] = len(nodes)
|
|
75
|
+
nodes.append(node)
|
|
76
|
+
return index[e]
|
|
77
|
+
|
|
78
|
+
visit(expr)
|
|
79
|
+
return ["dag", nodes]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _output(expr):
|
|
83
|
+
if _tree_size(expr, DAG_THRESHOLD + 1) > DAG_THRESHOLD:
|
|
84
|
+
return _dag(expr)
|
|
85
|
+
return _srepr(expr)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def plan_payload(plan) -> dict:
|
|
89
|
+
"""Canonical JSON structure hashed by :func:`plan_key` (useful for debugging)."""
|
|
90
|
+
payload = {
|
|
91
|
+
"format": PLAN_KEY_FORMAT,
|
|
92
|
+
"args": [
|
|
93
|
+
[a.name, a.role, int(a.dim), [_srepr(s) for s in a.symbols]]
|
|
94
|
+
for a in plan.args
|
|
95
|
+
],
|
|
96
|
+
"outputs": [_output(e) for e in plan.outputs],
|
|
97
|
+
"reduction": [plan.reduction.kind.value, plan.reduction.k],
|
|
98
|
+
"dtype": plan.dtype,
|
|
99
|
+
"acc_dtype": plan.acc_dtype,
|
|
100
|
+
"index_dtype": plan.index_dtype,
|
|
101
|
+
"fast_math": bool(plan.fast_math),
|
|
102
|
+
"fp_contract": bool(plan.fp_contract),
|
|
103
|
+
}
|
|
104
|
+
scheme = getattr(plan, "sum_scheme", "direct")
|
|
105
|
+
if scheme != "direct":
|
|
106
|
+
payload["sum_scheme"] = scheme
|
|
107
|
+
rank = int(getattr(plan, "batch_rank", 0))
|
|
108
|
+
if rank:
|
|
109
|
+
payload["batch_rank"] = rank
|
|
110
|
+
if getattr(plan, "ranges", False):
|
|
111
|
+
payload["ranges"] = True
|
|
112
|
+
return payload
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def plan_key(plan) -> str:
|
|
116
|
+
"""Hex sha256 of :func:`plan_payload`, canonically serialized."""
|
|
117
|
+
text = json.dumps(plan_payload(plan), sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
|
118
|
+
return hashlib.sha256(text.encode("ascii")).hexdigest()
|
kernox/_symox_compat.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Detection of the symox capabilities used by Kernox.
|
|
2
|
+
|
|
3
|
+
Kernox targets symox 0.1.1 from PyPI (built from ``53d9576e`` or later),
|
|
4
|
+
which contains the codegen extensions below, and remains compatible with the
|
|
5
|
+
older development revision ``2e623e66``, which lacks them:
|
|
6
|
+
|
|
7
|
+
- ``cudacode``: logical operators ``And/Or/Xor/Not``;
|
|
8
|
+
- ``cudacode``: ``reciprocal_sqrt`` option (``1/sqrt(x)``, opt-in);
|
|
9
|
+
- ``symox.extra.cudacode_function``: generic CUDA/HIP device function
|
|
10
|
+
(pointer arguments, named output struct, no math macro);
|
|
11
|
+
- native NumPy ``lambdify`` with NumPy broadcasting.
|
|
12
|
+
|
|
13
|
+
Detection tests the **behavior** (not a version number: both revisions
|
|
14
|
+
carry version 0.1.1). Each probe is cached; the environment variable
|
|
15
|
+
``KERNOX_SYMOX_COMPAT=pinned`` forces the compatibility code path, without
|
|
16
|
+
the extensions (regression tests).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from functools import lru_cache
|
|
23
|
+
|
|
24
|
+
from ._env import getenv
|
|
25
|
+
|
|
26
|
+
PINNED_SYMOX = "53d9576e9f39db16e9ca8b3483d754385ebaf986" # https://github.com/keckj/symox (main, printer SYCL sycl::half)
|
|
27
|
+
# Oldest revision still supported (compatibility path, without the
|
|
28
|
+
# codegen extensions); KERNOX_SYMOX_COMPAT=pinned forces this path.
|
|
29
|
+
MIN_SUPPORTED_SYMOX = "2e623e66bb31882e31f6dbfff6f77ae4bd945408"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _forced_pinned() -> bool:
|
|
33
|
+
return getenv("KERNOX_SYMOX_COMPAT", "") == "pinned"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@lru_cache(maxsize=None)
|
|
37
|
+
def _cuda_logic() -> bool:
|
|
38
|
+
import symox
|
|
39
|
+
from symox.extra import cudacode
|
|
40
|
+
|
|
41
|
+
x, y = symox.symbols("x y", real=True)
|
|
42
|
+
try:
|
|
43
|
+
return "&&" in cudacode(symox.And(x < 1, y < 1))
|
|
44
|
+
except NotImplementedError:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@lru_cache(maxsize=None)
|
|
49
|
+
def _cuda_reciprocal_sqrt() -> bool:
|
|
50
|
+
import symox
|
|
51
|
+
from symox.extra import cudacode
|
|
52
|
+
|
|
53
|
+
x = symox.Symbol("x", real=True)
|
|
54
|
+
try:
|
|
55
|
+
return cudacode(1 / symox.sqrt(x) + 1, use_rsqrt=False, reciprocal_sqrt=True) == "1 + 1.0/sqrt(x)"
|
|
56
|
+
except TypeError:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@lru_cache(maxsize=None)
|
|
61
|
+
def _cudacode_function() -> bool:
|
|
62
|
+
import symox.extra
|
|
63
|
+
|
|
64
|
+
return "cudacode_function" in getattr(symox.extra, "__all__", ())
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@lru_cache(maxsize=None)
|
|
68
|
+
def _lambdify_broadcasts() -> bool:
|
|
69
|
+
import numpy as np
|
|
70
|
+
import symox
|
|
71
|
+
|
|
72
|
+
x, y = symox.symbols("x y", real=True)
|
|
73
|
+
f = symox.lambdify((x, y), symox.Piecewise((x, x < y), (y, True)), modules="numpy")
|
|
74
|
+
try:
|
|
75
|
+
return np.shape(f(np.zeros((2, 1)), np.ones((1, 3)))) == (2, 3)
|
|
76
|
+
except ValueError:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@lru_cache(maxsize=None)
|
|
81
|
+
def _sycl_half() -> bool:
|
|
82
|
+
import symox
|
|
83
|
+
from symox import _core
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
return "sycl::half" in _core._syclcode(symox.Symbol("x") + 1, dtype="float16")
|
|
87
|
+
except (ValueError, TypeError):
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def sycl_half() -> bool:
|
|
92
|
+
"""``_syclcode(..., dtype="float16")`` prints ``sycl::half`` (sycl backend)."""
|
|
93
|
+
return not _forced_pinned() and _sycl_half()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cuda_logic() -> bool:
|
|
97
|
+
"""``cudacode`` prints ``And/Or/Xor/Not``."""
|
|
98
|
+
return not _forced_pinned() and _cuda_logic()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cuda_reciprocal_sqrt() -> bool:
|
|
102
|
+
"""``cudacode(..., reciprocal_sqrt=True)`` is available."""
|
|
103
|
+
return not _forced_pinned() and _cuda_reciprocal_sqrt()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def cudacode_function() -> bool:
|
|
107
|
+
"""``symox.extra.cudacode_function`` is available (implies ``cuda_logic``)."""
|
|
108
|
+
return not _forced_pinned() and _cudacode_function() and _cuda_logic()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def lambdify_broadcasts() -> bool:
|
|
112
|
+
"""The native NumPy ``lambdify`` broadcasts shapes like NumPy."""
|
|
113
|
+
return not _forced_pinned() and _lambdify_broadcasts()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def capabilities() -> dict:
|
|
117
|
+
"""Summary (diagnostics, benchmark reports)."""
|
|
118
|
+
import symox
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
"symox_version": getattr(symox, "__version__", "?"),
|
|
122
|
+
"forced_pinned": _forced_pinned(),
|
|
123
|
+
"cuda_logic": cuda_logic(),
|
|
124
|
+
"cuda_reciprocal_sqrt": cuda_reciprocal_sqrt(),
|
|
125
|
+
"cudacode_function": cudacode_function(),
|
|
126
|
+
"lambdify_broadcasts": lambdify_broadcasts(),
|
|
127
|
+
"sycl_half": sycl_half(),
|
|
128
|
+
}
|