Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions docs/user_guide/init.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,10 @@ written later to ``gb.ss.config`` is taken as it is, and with a C compiler that
work, the first operation that needs a new kernel raises ``JitError``, and SuiteSparse
lowers the control to ``"load"``. ``gb.ss.fix_jit_config()`` repairs the compiler setting
where it can and checks it. On Windows, SuiteSparse built with MSVC (as conda-forge
builds it) compiles JIT kernels only through cmake, and python-graphblas does not turn the
JIT on there yet, so a configured ``"on"`` becomes ``"run"``.
builds it) compiles JIT kernels only through cmake, which finds the Visual Studio C
compiler itself. There ``"on"`` needs cmake (``conda install cmake``) and Visual Studio or
its Build Tools rather than the configured compiler, and UDT work does not raise the
default ``"run"``.

With the JIT on:

Expand All @@ -123,9 +125,10 @@ With the JIT on:
instead, by setting the ``GRAPHBLAS_CACHE_PATH`` environment variable before graphblas
initializes. The default will change once SuiteSparse publishes compiled kernels
atomically.
- The first use of each new kernel compiles it, typically in 0.2 to 1 second. Kernels are
cached on disk, in ``gb.ss.config["jit_cache_path"]`` (by default under
``~/.SuiteSparse`` on Linux and macOS), so later runs load them instead.
- The first use of each new kernel compiles it, typically in 0.2 to 1 second, or about 10
seconds through cmake on Windows. Kernels are cached on disk, in
``gb.ss.config["jit_cache_path"]`` (by default under ``~/.SuiteSparse``), so later runs
load them instead.
- The cache is keyed only on the SuiteSparse:GraphBLAS version, so two builds of one
version (a conda build and a source build, say) can load each other's kernels, and get
wrong results or errors if their internals differ. Give each build its own cache path.
Expand Down
4 changes: 4 additions & 0 deletions docs/user_guide/udt.rst
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,10 @@ included:
- ``gb.binary.ss.register_new`` and its ``unary``, ``indexunary``, ``select``,
and ``indexbinary`` siblings, which register an operator from a C definition.

None of these raises it where SuiteSparse compiles through cmake, as it always does
when built with MSVC on Windows: each kernel takes about 10 seconds there, so only an
``'on'`` you set compiles (see :ref:`suitesparse_jit`).

A control you set is kept: ``'off'``, ``'pause'``, ``'load'`` (load and run
cached kernels, never compile), and ``'run'`` too when it was set before init
(``GRAPHBLAS_JIT_C_CONTROL``, ``gb.config.set``, or a YAML config file). With
Expand Down
79 changes: 60 additions & 19 deletions graphblas/core/ss/jit_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
from ``graphblas.config["jit_c_control"]`` (``'run'`` unless configured
otherwise; see :func:`_set_jit_c_control_at_init`). UDT work raises that
default ``'run'`` to ``'on'`` when it needs C compiled, but keeps a ``'run'``
the user set (:func:`_enable_jit_for_udt`).
the user set, and raises nothing where SuiteSparse compiles through cmake
(:func:`_enable_jit_for_udt`).
The trade-offs of the JIT, and what to do when it misbehaves, are in the user
guide's initialization page (docs/user_guide/init.rst).
"""
Expand All @@ -34,6 +35,7 @@
import pathlib
import platform
import re
import shutil
import subprocess

# ``ffi`` and ``lib`` are imported in the functions that use them: importing them
Expand Down Expand Up @@ -72,6 +74,19 @@ def _compiler_exists(cfg):
return bool(cc) and pathlib.Path(cc).exists()


def _compiler_or_cmake_exists(cfg):
"""Whether the C compiler is on disk, or cmake on the PATH where SuiteSparse compiles with it.

SuiteSparse built with MSVC always compiles through cmake, and passes the
configured compiler to it as ``CMAKE_C_COMPILER``, which cmake's Visual Studio
generator ignores. conda-forge's Windows builds name the build machine's
``cl.exe``, which users rarely have, so the compiler path says nothing there.
"""
if cfg.get("jit_use_cmake"):
return shutil.which("cmake") is not None
return _compiler_exists(cfg)


def fix_jit_config(*, use_sysconfig=True, probe=True):
"""Repair the SuiteSparse:GraphBLAS JIT compiler configuration.

Expand Down Expand Up @@ -221,9 +236,16 @@ def _strip_mismatched_arch(flags):
"jit_c_compiler_flags",
"jit_c_linker_flags",
"jit_c_libraries",
"jit_c_cmake_libs",
"jit_use_cmake",
)


def _probe_name(cfg):
toolchain = "\n".join(str(cfg.get(key, "")) for key in _TOOLCHAIN_KEYS)
return "_jit_probe_" + hashlib.sha256(toolchain.encode()).hexdigest()[:16]


def _probe_jit(cfg):
"""Compile a trivial type with the JIT, or load it from the cache, to check the config works.

Expand All @@ -238,14 +260,13 @@ def _probe_jit(cfg):
It goes through the C API, so nothing is registered on the Python side,
and it can run while the backend initializes. Once a kernel is in the
cache, a later probe loads it (about 2 ms) instead of compiling (about
0.3 s). The type is named after the toolchain settings, so a compiler or
flags this cache has not seen compile a probe of their own, rather than
load one that another toolchain left in a shared cache.
0.3 s). The type is named after the toolchain settings (cmake or not among
them), so a compiler or flags this cache has not seen compile a probe of
their own, rather than load one that another toolchain left in a shared cache.
"""
from .. import ffi, lib

toolchain = "\n".join(cfg.get(key, "") for key in _TOOLCHAIN_KEYS)
name = "_jit_probe_" + hashlib.sha256(toolchain.encode()).hexdigest()[:16]
name = _probe_name(cfg)
defn = f"typedef struct {{ int _probe ; }} {name} ;"
type_ptr = ffi.new("GrB_Type*")
# Size 0 asks SuiteSparse to get the size from a compiled kernel.
Expand All @@ -268,8 +289,13 @@ class _CConfig:
"jit_c_compiler_flags": "GxB_JIT_C_COMPILER_FLAGS",
"jit_c_linker_flags": "GxB_JIT_C_LINKER_FLAGS",
"jit_c_libraries": "GxB_JIT_C_LIBRARIES",
"jit_c_cmake_libs": "GxB_JIT_C_CMAKE_LIBS",
"jit_cache_path": "GxB_JIT_CACHE_PATH",
}
_ints = {
"jit_c_control": "GxB_JIT_C_CONTROL",
"jit_use_cmake": "GxB_JIT_USE_CMAKE",
}

def __init__(self):
from .. import lib
Expand All @@ -285,16 +311,18 @@ def __init__(self):
def __getitem__(self, key):
from .. import ffi, lib

if key == "jit_c_control":
if key in self._ints:
val = ffi.new("int32_t*")
info = lib.GxB_Global_Option_get_INT32(lib.GxB_JIT_C_CONTROL, val)
info = lib.GxB_Global_Option_get_INT32(getattr(lib, self._ints[key]), val)
else:
val = ffi.new("char**")
info = lib.GxB_Global_Option_get_CHAR(getattr(lib, self._strings[key]), val)
if info != lib.GrB_SUCCESS: # pragma: no cover (safety)
raise RuntimeError(f"Could not get SuiteSparse:GraphBLAS option {key!r}")
if key == "jit_c_control":
return next(k for k, v in self._controls.items() if v == val[0])
if key == "jit_use_cmake":
return bool(val[0])
return ffi.string(val[0]).decode()

def __setitem__(self, key, value):
Expand All @@ -309,7 +337,7 @@ def __setitem__(self, key, value):
raise RuntimeError(f"Could not set SuiteSparse:GraphBLAS option {key!r}")

def get(self, key, default=None):
return self[key] if key == "jit_c_control" or key in self._strings else default
return self[key] if key in self._ints or key in self._strings else default


def _cache_is_writable(cfg):
Expand Down Expand Up @@ -338,8 +366,9 @@ def _set_jit_c_control_at_init(control):
control is set to ``'run'`` (where the conda-forge and PyPI builds of
SuiteSparse start it), with no error or warning: a library built without
the JIT, which clamps every write to ``'run'``; no compiler on disk, even
after :func:`_repair_jit_compiler`; a cache directory it cannot write to;
or a compiler that fails the probe.
after :func:`_repair_jit_compiler`, or no cmake where SuiteSparse compiles
through cmake (:func:`_compiler_or_cmake_exists`); a cache directory it
cannot write to; or a compiler that fails the probe.
The probe is load-bearing: from SuiteSparse 9.4 on a failed compile raises
``JitError`` (once; SuiteSparse then drops the control to ``'load'``), and
without the probe that would be the first user operation that needs a new
Expand All @@ -359,7 +388,7 @@ def _set_jit_c_control_at_init(control):
if not _compiler_exists(cfg):
_repair_jit_compiler(cfg)
_jit_enabled_for_udt = False
if _compiler_exists(cfg) and _cache_is_writable(cfg):
if _compiler_or_cmake_exists(cfg) and _cache_is_writable(cfg):
cfg["jit_c_control"] = "on"
_jit_enabled_for_udt = cfg["jit_c_control"] == "on" and _probe_jit(cfg)
if not _jit_enabled_for_udt:
Expand Down Expand Up @@ -428,17 +457,22 @@ def _enable_jit_for_udt():
``'off'``, ``'pause'``, and ``'load'``, and ``'load'`` is also where
SuiteSparse leaves the control after a compile fails, so raising it would
retry that compile at every later UDT op (raising ``JitError`` each time on
SuiteSparse 9.4 and later). Returns True iff a kernel armed now will be
compiled. Whether this process can compile at all is probed once and
cached.
SuiteSparse 9.4 and later). Nor is the default raised where SuiteSparse
compiles through cmake (``jit_use_cmake``, always True when it is built
with MSVC): there each kernel took about 10 s on Windows CI, so only an
``'on'`` the user set compiles, even where the configured compiler exists.
Returns True iff a kernel armed now will be compiled. Whether this process
can compile at all is probed once and cached.
"""
global _jit_enabled_for_udt
cfg = _ss_config()
control = cfg.get("jit_c_control")
if control in (None, "off", "pause", "load") or (control == "run" and _user_set_run):
# ``None`` is a library with no JIT at all. The others are honored and
# not cached, so they decide nothing once the control is back at the
# default.
if control in (None, "off", "pause", "load") or (
control == "run" and (_user_set_run or cfg.get("jit_use_cmake"))
):
# ``None`` is a library with no JIT at all. The others, and the default
# 'run' where SuiteSparse compiles through cmake, are honored and not
# cached, so they decide nothing once the control is back at the default.
return False
if _jit_enabled_for_udt is None:
if not jit_compiler_is_usable():
Expand Down Expand Up @@ -519,6 +553,13 @@ def _compiling_c_source(name):
"or None for the default 'run', which python-graphblas raises to 'on' "
"for C source."
)
elif after == before == "run" and cfg.get("jit_use_cmake"):
reason = (
"jit_c_control is still 'run', which neither compiles nor loads: "
"SuiteSparse compiles through cmake here, which python-graphblas "
"does not start unasked (or SuiteSparse was built without the JIT). To "
"compile it through cmake, set gb.ss.config['jit_c_control'] = 'on'."
)
elif after == before == "run":
reason = (
"jit_c_control is still 'run', which neither compiles nor loads: "
Expand Down
14 changes: 8 additions & 6 deletions graphblas/ss/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,16 @@ class GlobalConfig(BaseConfig):
Only available for SuiteSparse:GraphBLAS >=8.
python-graphblas sets it at init from ``graphblas.config["jit_c_control"]``,
or to "run" when that is not set, and UDT work raises that default "run" to
"on" when it needs a kernel compiled. A "run" configured there is kept, and
so is one set here over another configured value; with nothing configured,
"run" set here is the default again, as None is in any case. The user
guide's initialization page covers the trade-offs, and when "on" is safe.
"on" when it needs a kernel compiled, unless jit_use_cmake is True. A "run"
configured there is kept, and so is one set here over another configured
value; with nothing configured, "run" set here is the default again, as
None is in any case. The user guide's initialization page covers the
trade-offs, and when "on" is safe.
jit_use_cmake : bool
Whether to use cmake to compile the JIT kernels.
Always True for SuiteSparse built with MSVC (on Windows), where
python-graphblas does not turn the JIT on yet.
Always True for SuiteSparse built with MSVC (on Windows), where a
configured "on" needs cmake and Visual Studio, not the configured
compiler. Where it is True, UDT work does not raise "run" to "on".
Only available for SuiteSparse:GraphBLAS >=8
jit_c_compiler_name : str
C compiler for JIT kernels.
Expand Down
Loading
Loading