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.
Files changed (43) hide show
  1. kernox/__init__.py +101 -0
  2. kernox/_env.py +13 -0
  3. kernox/_plan_key.py +118 -0
  4. kernox/_symox_compat.py +128 -0
  5. kernox/autograd.py +464 -0
  6. kernox/backends/__init__.py +1 -0
  7. kernox/backends/base.py +252 -0
  8. kernox/backends/cuda/__init__.py +10 -0
  9. kernox/backends/cuda/_driver.py +335 -0
  10. kernox/backends/cuda/_kernel.py +886 -0
  11. kernox/backends/cuda/backend.py +796 -0
  12. kernox/backends/hip/__init__.py +11 -0
  13. kernox/backends/hip/_cache.py +85 -0
  14. kernox/backends/hip/_hip.py +252 -0
  15. kernox/backends/hip/_kernel.py +801 -0
  16. kernox/backends/hip/backend.py +711 -0
  17. kernox/backends/hip/diagnostics.py +99 -0
  18. kernox/backends/sycl/__init__.py +601 -0
  19. kernox/backends/sycl/_pair_local.py +67 -0
  20. kernox/backends/sycl/codegen.py +888 -0
  21. kernox/backends/sycl/toolchain.py +179 -0
  22. kernox/builder.py +371 -0
  23. kernox/codegen/__init__.py +1 -0
  24. kernox/codegen/pair.py +512 -0
  25. kernox/complex.py +407 -0
  26. kernox/diagnostics/__init__.py +335 -0
  27. kernox/diagnostics/__main__.py +5 -0
  28. kernox/errors.py +33 -0
  29. kernox/expr.py +615 -0
  30. kernox/linalg.py +388 -0
  31. kernox/ops.py +501 -0
  32. kernox/oracle.py +626 -0
  33. kernox/plan.py +157 -0
  34. kernox/ranges.py +246 -0
  35. kernox/runtime.py +977 -0
  36. kernox/tuning.py +404 -0
  37. kernox-1.0.1.dist-info/METADATA +151 -0
  38. kernox-1.0.1.dist-info/RECORD +43 -0
  39. kernox-1.0.1.dist-info/WHEEL +5 -0
  40. kernox-1.0.1.dist-info/licenses/LICENSE-APACHE +202 -0
  41. kernox-1.0.1.dist-info/licenses/LICENSE-MIT +21 -0
  42. kernox-1.0.1.dist-info/licenses/THIRD_PARTY.md +20 -0
  43. 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()
@@ -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
+ }