Skip to content

Commit 83b66ba

Browse files
committed
New blosc2.validate_dsl_jit(); documented with examples
1 parent 5cd6fa6 commit 83b66ba

4 files changed

Lines changed: 245 additions & 1 deletion

File tree

src/blosc2/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ def _raise(exc):
573573

574574
from .c2array import c2context, C2Array, URLPath
575575

576-
from .dsl_kernel import DSLSyntaxError, DSLKernel, dsl_kernel, validate_dsl
576+
from .dsl_kernel import DSLSyntaxError, DSLKernel, dsl_kernel, validate_dsl, validate_dsl_jit
577577
from .lazyexpr import (
578578
LazyExpr,
579579
lazyudf,
@@ -976,6 +976,7 @@ def _raise(exc):
976976
"lazyexpr",
977977
"dsl_kernel",
978978
"validate_dsl",
979+
"validate_dsl_jit",
979980
"lazyudf",
980981
"lazywhere",
981982
"less",

src/blosc2/blosc2_ext.pyx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,8 @@ cdef extern from "miniexpr.h":
735735
void me_print(const me_expr *n) nogil
736736
void me_free(me_expr *n) nogil
737737

738+
bint me_expr_has_jit_kernel(const me_expr *expr) nogil
739+
738740
ctypedef int (*me_wasm_jit_instantiate_helper)(
739741
const unsigned char *wasm_bytes,
740742
int wasm_len,
@@ -4071,6 +4073,74 @@ cdef class NDArray:
40714073
# alive for the duration of the prefilter-driven update_data loop.
40724074
return cb
40734075

4076+
def _dsl_jit_status(self, expression, inputs):
4077+
"""Compile *expression* with JIT against this array's grid and report whether
4078+
miniexpr produced a runtime JIT kernel (vs an interpreter fallback).
4079+
4080+
Compiles and immediately frees the program; it does not set a prefilter or
4081+
run the kernel. Returns a dict with ``compiled`` (bool), ``jit`` (bool) and
4082+
``status`` (miniexpr compile-status name). ``inputs`` is a name -> NDArray
4083+
mapping; all operands must share this array's shape/chunks/blocks.
4084+
"""
4085+
cdef Py_ssize_t n = len(inputs)
4086+
cdef me_variable* variables = NULL
4087+
if n > 0:
4088+
variables = <me_variable *> malloc(sizeof(me_variable) * n)
4089+
if variables == NULL:
4090+
raise MemoryError()
4091+
cdef me_variable *var
4092+
cdef np.dtype out_np_dtype = np.dtype(self.dtype)
4093+
cdef me_dtype me_output_dtype = _me_dtype_from_numpy_dtype(out_np_dtype)
4094+
if <int>me_output_dtype < 0:
4095+
free(variables)
4096+
raise TypeError(f"miniexpr does not support output dtype: {out_np_dtype}")
4097+
4098+
cdef np.dtype operand_dtype
4099+
cdef bytes var_name
4100+
for i, (k, v) in enumerate(inputs.items()):
4101+
var = &variables[i]
4102+
var_name = k.encode("utf-8") if isinstance(k, str) else k
4103+
var.name = <char *> malloc(strlen(var_name) + 1)
4104+
strcpy(var.name, var_name)
4105+
operand_dtype = np.dtype(v.dtype)
4106+
var.dtype = _me_dtype_from_numpy_dtype(operand_dtype)
4107+
if <int>var.dtype < 0:
4108+
for j in range(i + 1):
4109+
free(variables[j].name)
4110+
free(variables)
4111+
raise TypeError(f"miniexpr does not support operand dtype '{operand_dtype}' for input '{k}'")
4112+
var.address = NULL
4113+
var.type = 0
4114+
var.context = NULL
4115+
var.itemsize = v.dtype.itemsize if v.dtype.num == 19 else 0
4116+
4117+
cdef bytes expression_bytes = (
4118+
(<str>expression).encode("utf-8") if isinstance(expression, str) else expression
4119+
)
4120+
cdef int error = 0
4121+
cdef me_expr *out_expr = NULL
4122+
cdef int ndims = self.array.ndim
4123+
cdef int64_t* shape = &self.array.shape[0]
4124+
cdef int32_t* chunkshape = &self.array.chunkshape[0]
4125+
cdef int32_t* blockshape = &self.array.blockshape[0]
4126+
cdef int rc = me_compile_nd_jit(expression_bytes, variables, n, me_output_dtype, ndims,
4127+
shape, chunkshape, blockshape, ME_JIT_ON,
4128+
&error, &out_expr)
4129+
for i in range(n):
4130+
free(variables[i].name)
4131+
free(variables)
4132+
4133+
cdef bint jit = False
4134+
if rc == ME_COMPILE_SUCCESS and out_expr != NULL:
4135+
jit = me_expr_has_jit_kernel(out_expr)
4136+
if out_expr != NULL:
4137+
me_free(out_expr)
4138+
return {
4139+
"compiled": rc == ME_COMPILE_SUCCESS,
4140+
"jit": bool(jit),
4141+
"status": _me_compile_status_name(rc),
4142+
}
4143+
40744144
def _set_pref_matmul(self, inputs, fp_accuracy):
40754145
# Set prefilter for miniexpr
40764146
cdef blosc2_cparams* cparams = self.array.sc.storage.cparams

src/blosc2/dsl_kernel.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,31 @@ def validate_dsl(func):
635635
- ``dsl_source`` (str | None): extracted DSL source when valid
636636
- ``input_names`` (list[str] | None): input signature names when valid
637637
- ``error`` (str | None): user-facing error message when invalid
638+
639+
Examples
640+
--------
641+
>>> import blosc2
642+
>>> @blosc2.dsl_kernel
643+
... def k(a, b):
644+
... return a * a + b * b
645+
>>> info = blosc2.validate_dsl(k)
646+
>>> info["valid"]
647+
True
648+
>>> info["input_names"]
649+
['a', 'b']
650+
651+
An unsupported construct is reported instead of raising:
652+
653+
>>> @blosc2.dsl_kernel
654+
... def bad(a):
655+
... return 1 if a > 0 else 0
656+
>>> blosc2.validate_dsl(bad)["valid"]
657+
False
658+
659+
See Also
660+
--------
661+
validate_dsl_jit : Additionally probe whether the kernel JIT-compiles (vs falls
662+
back to the interpreter) for given operand/output dtypes.
638663
"""
639664

640665
kernel = func if isinstance(func, DSLKernel) else DSLKernel(func)
@@ -647,6 +672,115 @@ def validate_dsl(func):
647672
}
648673

649674

675+
def validate_dsl_jit(func, operands, out_dtype, *, shape=(64,), chunks=None, blocks=None):
676+
"""Report whether a DSL kernel JIT-compiles (vs. interpreter fallback).
677+
678+
Compiles a tiny probe of the kernel for the given operand/output dtypes and
679+
queries miniexpr for whether it produced a runtime JIT kernel. The kernel is
680+
*not* run on real data, but compilation is dtype-specialized, so the operand and
681+
output dtypes must be supplied.
682+
683+
Parameters
684+
----------
685+
func
686+
A Python callable or :class:`DSLKernel`.
687+
operands
688+
One spec per kernel input. A NumPy dtype (e.g. ``np.float64``, ``"f8"``)
689+
marks an *array* operand; a Python ``int``/``float``/``bool`` marks a *scalar*
690+
parameter inlined as a constant (as happens at run time). Either a dict
691+
mapping input name -> spec, or a sequence matched positionally to the inputs.
692+
out_dtype
693+
Output dtype to specialize the codegen for.
694+
shape
695+
Probe shape; ``len(shape)`` sets the kernel dimensionality (kernels using
696+
``_i1`` etc. need a matching ndim). Defaults to ``(64,)``.
697+
698+
Returns
699+
-------
700+
dict
701+
- ``valid`` (bool): DSL syntax is valid
702+
- ``jit`` (bool): a runtime JIT kernel was produced (vs interpreter fallback)
703+
- ``compiled`` (bool): miniexpr accepted the kernel
704+
- ``status`` (str | None): miniexpr compile-status name
705+
- ``error`` (str | None): syntax error message when ``valid`` is False
706+
707+
Examples
708+
--------
709+
>>> import blosc2
710+
>>> import numpy as np
711+
>>> @blosc2.dsl_kernel
712+
... def k(a, b):
713+
... return a * a + b * b
714+
>>> status = blosc2.validate_dsl_jit(k, [np.float64, np.float64], np.float64)
715+
>>> status["jit"]
716+
True
717+
>>> status["status"]
718+
'ME_COMPILE_SUCCESS'
719+
720+
Pass array operands as dtypes and scalar parameters as values (the scalar is
721+
inlined as a constant, as at run time):
722+
723+
>>> @blosc2.dsl_kernel
724+
... def scaled(a, n):
725+
... return a * float(n)
726+
>>> blosc2.validate_dsl_jit(scaled, {"a": np.float64, "n": 3}, np.float64)["jit"]
727+
True
728+
729+
See Also
730+
--------
731+
validate_dsl : Static DSL-syntax check only (no compilation; no dtypes needed).
732+
"""
733+
import numpy as np
734+
735+
import blosc2
736+
737+
kernel = func if isinstance(func, DSLKernel) else DSLKernel(func)
738+
if kernel.dsl_error is not None:
739+
return {
740+
"valid": False,
741+
"jit": False,
742+
"compiled": False,
743+
"status": None,
744+
"error": str(kernel.dsl_error),
745+
}
746+
747+
names = list(kernel.input_names or [])
748+
if isinstance(operands, dict):
749+
specs = dict(operands)
750+
else:
751+
operands = list(operands)
752+
if len(operands) != len(names):
753+
raise ValueError(f"expected {len(names)} operand specs for inputs {names}, got {len(operands)}")
754+
specs = dict(zip(names, operands, strict=True))
755+
756+
scalars: dict = {}
757+
arrays: dict = {}
758+
for name in names:
759+
if name not in specs:
760+
raise ValueError(f"missing operand spec for input '{name}'")
761+
spec = specs[name]
762+
if isinstance(spec, bool | int | float):
763+
scalars[name] = spec
764+
else:
765+
arrays[name] = np.dtype(spec)
766+
767+
source = kernel.dsl_source
768+
if scalars:
769+
source, _ = specialize_miniexpr_inputs(source, scalars)
770+
if not arrays:
771+
raise ValueError(
772+
"validate_dsl_jit needs at least one array operand (pass a dtype spec); "
773+
"all-scalar probes are not supported"
774+
)
775+
776+
out = blosc2.empty(shape, dtype=np.dtype(out_dtype), chunks=chunks, blocks=blocks)
777+
probe_inputs = {name: np.empty(1, dtype=dt) for name, dt in arrays.items()}
778+
status = out._dsl_jit_status(source, probe_inputs)
779+
status["valid"] = True
780+
status["error"] = None
781+
return status
782+
783+
650784
def kernel_from_source(source: str, name: str | None = None) -> DSLKernel:
651785
"""Reconstruct a :class:`DSLKernel` from its stored source text.
652786

tests/ndarray/test_dsl_kernels.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,3 +1069,42 @@ def wrapped(self, expression, inputs, fp_accuracy, aux_reduc=None, jit=None):
10691069
assert any(line.strip().startswith("out =") for line in captured["expr"].splitlines())
10701070
expected = blosc2.lazyudf(k, (a2, a2, 32), dtype=np.float64, jit=False)[:]
10711071
np.testing.assert_allclose(res, expected)
1072+
1073+
1074+
def test_validate_dsl_jit_reports_compile_and_fallback(monkeypatch):
1075+
@blosc2.dsl_kernel
1076+
def simple(a, b):
1077+
return a * a + b * b
1078+
1079+
st = blosc2.validate_dsl_jit(simple, [np.float64, np.float64], np.float64)
1080+
assert st["valid"]
1081+
assert st["compiled"]
1082+
assert st["jit"]
1083+
assert st["status"] == "ME_COMPILE_SUCCESS"
1084+
1085+
# A scalar param is inlined (passed as a value); a variable named 'out' (the
1086+
# former silent-fallback collision) now JIT-compiles through to miniexpr. Built
1087+
# from source so the linter does not rewrite the deliberate 'out' variable.
1088+
with_out = kernel_from_source(
1089+
"def withk(a, b, niter):\n out = a + b * float(niter)\n return out\n", "withk"
1090+
)
1091+
st = blosc2.validate_dsl_jit(with_out, {"a": np.float64, "b": np.float64, "niter": 3}, np.float64)
1092+
assert st["jit"]
1093+
1094+
# Invalid syntax -> not valid, nothing compiled.
1095+
invalid = kernel_from_source("def k(a, b):\n a = a - b\n return a\n", "k")
1096+
st = blosc2.validate_dsl_jit(invalid, [np.float64, np.float64], np.float64)
1097+
assert not st["valid"]
1098+
assert not st["jit"]
1099+
assert "input parameter 'a'" in st["error"]
1100+
1101+
# JIT disabled in the environment -> compiles but falls back to the interpreter.
1102+
monkeypatch.setenv("ME_DSL_JIT", "0")
1103+
1104+
@blosc2.dsl_kernel
1105+
def other(a, b):
1106+
return a - b * 0.5
1107+
1108+
st = blosc2.validate_dsl_jit(other, [np.float64, np.float64], np.float64)
1109+
assert st["compiled"]
1110+
assert not st["jit"]

0 commit comments

Comments
 (0)