Source code for brainevent._op.kernix_compiler

# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

"""Compiler backend abstraction: CompilerBackend ABC, CUDABackend, CPPBackend,
and HIPBackend.

Adding a new backend
--------------------
1. Sub-class ``CompilerBackend`` and implement ``compile_source()``.
2. Detect the new toolchain in ``kernix_toolchain.py``.
3. Select the new backend in ``kernix_pipeline.py`` (or call it directly).
"""

import os
import shlex
import subprocess
import sys
from abc import ABC, abstractmethod
from typing import Any

from brainevent._error import (
    CompilationError, HostCompilerIncompatibleError,
    UnsupportedArchError,
)
from .kernix_toolchain import (
    CppToolchain, CudaToolchain, cxx_shared_flags, cxx_std_flag,
    gencode_flags, nvcc_host_pic_flags,
)


_HOST_INCOMPAT_SIGNALS = (
    "unsupported gnu version",
    "unsupported clang version",
    "is not supported",
    "no longer supported",
)


def _is_host_incompat(output: str) -> bool:
    low = output.lower()
    return any(sig in low for sig in _HOST_INCOMPAT_SIGNALS)


def _allow_unsupported_compiler() -> bool:
    return os.environ.get("BRAINEVENT_ALLOW_UNSUPPORTED_COMPILER", "").strip().lower() in (
        "1", "true", "yes", "on",
    )


def _compile_timeout() -> int:
    """Compile subprocess timeout in seconds (``BRAINEVENT_COMPILE_TIMEOUT``)."""
    try:
        return int(os.environ.get("BRAINEVENT_COMPILE_TIMEOUT", "600"))
    except ValueError:
        return 600


def _cuda_runtime_lib_linker_search_flags(cuda_home: str) -> list[str]:
    """Return linker search flags for CUDA runtime libraries.

    ``cuda_home`` is the root inferred from the selected ``nvcc``.  Existing
    ``lib``/``lib64`` directories under that root are added both for link-time
    lookup (``-L``) and load-time lookup (``rpath``).  For pip split CUDA
    packages, ``nvcc`` lives in ``nvidia/cuda_nvcc`` while the runtime library
    can live in sibling ``nvidia/cuda_runtime``, so that sibling is searched
    only when the selected toolkit root is ``cuda_nvcc``.
    """
    flags: list[str] = []
    seen: set[str] = set()
    if not sys.platform.startswith("linux"):
        return flags

    def add_lib_dirs(root: str) -> None:
        for dirname in ("lib", "lib64"):
            lib_dir = os.path.join(root, dirname)
            if not os.path.isdir(lib_dir) or lib_dir in seen:
                continue
            seen.add(lib_dir)
            flags.append(f"-L{lib_dir}")
            flags.extend(["-rpath", lib_dir])

    add_lib_dirs(cuda_home)
    norm_cuda_home = os.path.normpath(cuda_home)
    if os.path.basename(norm_cuda_home) == "cuda_nvcc":
        add_lib_dirs(os.path.join(os.path.dirname(norm_cuda_home), "cuda_runtime"))
    return flags


def _run(cmd, *, timeout, stage):
    """``subprocess.run`` that maps FileNotFoundError/timeout to CompilationError.

    Output is decoded as UTF-8 with ``errors="replace"`` rather than the
    platform locale.  Compiler diagnostics (nvcc / cl / g++) routinely contain
    bytes that are not valid in a non-UTF-8 locale (e.g. Windows ``cp1252``);
    relying on the implicit locale decoding of ``text=True`` lets a
    ``UnicodeDecodeError`` raise *inside* ``subprocess.run`` -- before this
    function's ``except`` clauses can run -- masking the real compiler error.
    """
    try:
        return subprocess.run(
            cmd, capture_output=True, text=True,
            encoding="utf-8", errors="replace", timeout=timeout,
        )
    except FileNotFoundError as e:
        raise CompilationError(
            f"compiler executable not found: {cmd[0]}",
            command=shlex.join(map(str, cmd)), stage=stage) from e
    except subprocess.TimeoutExpired as e:
        out = ((e.stdout or "") + (e.stderr or "")) if isinstance(e.stdout, str) else ""
        raise CompilationError(
            f"compilation timed out after {timeout}s",
            compiler_output=out, command=shlex.join(map(str, cmd)), stage=stage) from e


def _raise_compile_error(output: str, command: str, stage: str) -> None:
    low = output.lower()
    if "unsupported gpu architecture" in low:
        raise UnsupportedArchError(
            "the target GPU architecture is not supported by this nvcc.\n"
            "How to fix:\n"
            "  1) Upgrade the CUDA toolkit: pip install -U 'jax[cuda13]'\n"
            "  2) Or pin a supported arch: brainevent.config.set_compute_capability('8.6')",
            compiler_output=output, command=command, stage=stage)
    if _is_host_incompat(output):
        msg = (
            "host C++ compiler is incompatible with the current CUDA/nvcc.\n"
            "How to fix:\n"
            "  1) Install a supported gcc version and set CXX=/path/to/g++\n"
            "  2) Or set BRAINEVENT_ALLOW_UNSUPPORTED_COMPILER=1 and retry"
        )
        raise HostCompilerIncompatibleError(msg, compiler_output=output, command=command, stage=stage)
    raise CompilationError("compilation failed", compiler_output=output, command=command, stage=stage)


# ---------------------------------------------------------------------------
# Abstract base
# ---------------------------------------------------------------------------

[docs] class CompilerBackend(ABC): """Abstract base class for BE compiler backends. Each backend encapsulates one compilation toolchain (nvcc, g++, hipcc, …). The high-level pipeline in :mod:`brainevent._op.kernix_pipeline` selects the appropriate backend based on the requested platform. Subclasses must implement :meth:`compile_source`. Optionally they can override :meth:`platform_name` to provide a human-readable identifier. """ #: Human-readable platform identifier, e.g. ``"cuda"``, ``"cpu"``, ``"hip"``. platform_name: str = "unknown"
[docs] @abstractmethod def compile_source( self, source: str, output_path: str, build_dir: str, *, extra_cflags: list[str] | None = None, extra_ldflags: list[str] | None = None, extra_include_paths: list[str] | None = None, verbose: bool = False, **kwargs: Any, ) -> str: """Compile preprocessed source code to a shared library. Parameters ---------- source : str Preprocessed C++/CUDA source (user code + auto-generated FFI wrappers, as produced by :func:`~brainevent._op.kernix_codegen.preprocess_source`). output_path : str Desired path for the output shared library. build_dir : str Directory for intermediate build artefacts. extra_cflags : list[str], optional Additional compiler flags. extra_ldflags : list[str], optional Additional linker flags. extra_include_paths : list[str], optional Additional header search paths. verbose : bool Print the full compiler command. **kwargs Backend-specific keyword arguments (e.g. ``gpu_arch`` for CUDA, ``optimization_level``, ``use_fast_math``). Returns ------- str Absolute path to the compiled shared library. """
def __repr__(self) -> str: return f"{type(self).__name__}(platform={self.platform_name!r})"
# --------------------------------------------------------------------------- # CUDA backend # ---------------------------------------------------------------------------
[docs] class CUDABackend(CompilerBackend): """Compile CUDA sources directly with nvcc. Parameters ---------- toolchain : CudaToolchain Detected CUDA toolchain (from :func:`~.kernix_toolchain.detect_toolchain`). """ platform_name = "cuda" def __init__(self, toolchain: CudaToolchain) -> None: self.toolchain = toolchain
[docs] def compile_source( self, source: str, output_path: str, build_dir: str, *, extra_cuda_cflags: list[str] | None = None, extra_ldflags: list[str] | None = None, extra_include_paths: list[str] | None = None, verbose: bool = False, gpu_arch: "str | list[str]" = "sm_80", optimization_level: int = 3, use_fast_math: bool = False, **kwargs: Any, ) -> str: """Compile preprocessed source to .so directly with nvcc. Notes ----- Each element of *extra_ldflags* is treated as a single, already-split linker token (the caller is responsible for splitting, exactly as if it were one ``shlex``-split argv entry). Every element is forwarded to the host linker unchanged via one ``-Xlinker <token>`` pair, so a value such as ``"-L/path with spaces"`` reaches the linker as a single argument. This matches the :class:`CPPBackend` semantics, where each element is likewise one already-split token passed straight through. """ os.makedirs(build_dir, exist_ok=True) arches = [gpu_arch] if isinstance(gpu_arch, str) else list(gpu_arch) # Write source to build dir (utf-8: nvcc expects utf-8) src_path = os.path.join(build_dir, "kernel.cu") with open(src_path, "w", encoding="utf-8") as f: f.write(source) return self._compile_direct( src_path, output_path, arches, extra_cuda_cflags, extra_ldflags, extra_include_paths, verbose, optimization_level, use_fast_math)
def _compile_direct( self, src_path, output_path, arches, extra_cuda_cflags, extra_ldflags, extra_include_paths, verbose, optimization_level, use_fast_math, ) -> str: """Compile + link a single .cu directly with nvcc.""" os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) cmd = [ self.toolchain.nvcc, src_path, "-shared", "-o", output_path, *gencode_flags(arches), *nvcc_host_pic_flags(), # -fPIC on Linux/macOS, nothing on Windows "--std=c++17", f"-O{optimization_level}", "-ccbin", self.toolchain.cxx, ] if _allow_unsupported_compiler(): cmd.append("-allow-unsupported-compiler") # Include paths (order matters for override semantics) cmd += ["-I", self.toolchain.brainevent_include_dir, "-I", self.toolchain.xla_ffi_include_dir] for inc in self.toolchain.cuda_include_dirs: cmd += ["-I", inc] if use_fast_math: cmd.append("--use_fast_math") for p in (extra_include_paths or []): cmd.extend(["-I", p]) cmd.extend(extra_cuda_cflags or []) # Each ldflag element is one already-split token forwarded to the # host linker via a single ``-Xlinker <token>`` pair (mirrors how # ``nvcc_host_pic_flags`` forwards host-compiler options one per flag, # and keeps multi-word values such as "-L/path with spaces" intact). linker_flags = _cuda_runtime_lib_linker_search_flags(self.toolchain.cuda_home) + (extra_ldflags or []) for token in linker_flags: cmd.extend(["-Xlinker", token]) cmd_str = shlex.join(cmd) if verbose: print(f"nvcc command:\n {cmd_str}") result = _run(cmd, timeout=_compile_timeout(), stage="compile") if result.returncode != 0: _raise_compile_error(result.stderr + result.stdout, cmd_str, stage="compile") if verbose and result.stderr: print(f"nvcc warnings:\n{result.stderr}") return output_path
# --------------------------------------------------------------------------- # CPU backend # ---------------------------------------------------------------------------
[docs] class CPPBackend(CompilerBackend): """Compile C++ sources with g++ / clang++. Parameters ---------- toolchain : CppToolchain Detected C++ toolchain (from :func:`~.kernix_toolchain.detect_cpp_toolchain`). """ platform_name = "cpu" def __init__(self, toolchain: CppToolchain) -> None: self.toolchain = toolchain
[docs] def compile_source( self, source: str, output_path: str, build_dir: str, *, extra_cflags: list[str] | None = None, extra_ldflags: list[str] | None = None, extra_include_paths: list[str] | None = None, verbose: bool = False, **kwargs: Any, ) -> str: """Compile preprocessed source to a shared library with g++ / clang++. Notes ----- Each element of *extra_ldflags* is treated as a single, already-split linker token and passed through to the compiler driver unchanged (the driver forwards ``-L`` / ``-l`` / ``-Wl,`` flags to the linker). This matches the :class:`CUDABackend` semantics, where each element is likewise one already-split token forwarded to the host linker. """ toolchain = self.toolchain # Honor the caller-provided build_dir for intermediate artefacts (the # CUDABackend does the same); fall back to the output directory only # when no build_dir was supplied. if build_dir: os.makedirs(build_dir, exist_ok=True) else: build_dir = os.path.dirname(os.path.abspath(output_path)) os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) src_path = os.path.join(build_dir, "kernel.cpp") with open(src_path, "w", encoding="utf-8") as f: f.write(source) cmd = [ toolchain.cxx, src_path, *cxx_shared_flags(toolchain.cxx), "-o", output_path, cxx_std_flag(toolchain.cxx), "-I", toolchain.brainevent_include_dir, "-I", toolchain.xla_ffi_include_dir, ] for p in (extra_include_paths or []): cmd.extend(["-I", p]) cmd.extend(extra_cflags or []) # Each extra_ldflags element is one already-split token passed straight # through to the compiler driver (which forwards it to the linker). cmd.extend(extra_ldflags or []) cmd_str = shlex.join(cmd) if verbose: print(f"C++ command:\n {cmd_str}") result = _run(cmd, timeout=_compile_timeout(), stage="compile") if result.returncode != 0: raise CompilationError( "C++ compilation failed", compiler_output=result.stderr + result.stdout, command=cmd_str, ) if verbose and result.stderr: print(f"Compiler warnings:\n{result.stderr}") return output_path
# --------------------------------------------------------------------------- # HIP backend (stub — not yet implemented) # ---------------------------------------------------------------------------
[docs] class HIPBackend(CompilerBackend): """Compile HIP sources for AMD GPUs (stub — not yet implemented). To implement HIP support: 1. Add ``detect_hip_toolchain()`` to :mod:`~.kernix_toolchain` that locates ``hipcc`` and the ROCm include directories. 2. Implement HIP compilation logic in this class using ``hipcc``. 3. Update :func:`~brainevent._op.kernix_pipeline.load_cuda_inline` (or add ``load_hip_inline``) to select :class:`HIPBackend` when ``platform="hip"`` is requested. """ platform_name = "hip"
[docs] def compile_source(self, source: str, output_path: str, build_dir: str, **kwargs: Any) -> str: raise NotImplementedError( "HIP backend is not yet implemented. " "See the class docstring for implementation guidance." )