Newer
Older
SmTIAS-LightSim / src / sm_tias_lightsim / runtime / llvm_init.py
"""LLVM JIT 自動検出と診断ログモジュール.

Mitsuba 3 の llvm_* variant を使う際に必要な DRJIT_LIBLLVM_PATH 環境変数を
自動設定する機能と,variant 設定後の状態を診断ログに出力する機能を提供する.

mitsuba は import しない(テストの軽量化・副作用なし設計).
os.environ への書き込みのみが副作用.
"""

from __future__ import annotations

import glob
import logging
import os
import re
from pathlib import Path

_logger = logging.getLogger(__name__)

# Debian / Ubuntu / Fedora 系は LLVM-C API を単一の libLLVM-<ver>.so(または libLLVM.so.<ver>)に
# 同梱して配布し,libLLVM-C.so を置かない.Dr.Jit 自身はこれらを dlopen できるため,
# 探索対象に含めないと「正常動作しているのに見つからない」誤警告になる(2026-09-13 Newton で確認).
_POSIX_VERSIONED_LIB_PATTERNS: tuple[str, ...] = (
    "/usr/lib/llvm-*/lib/libLLVM.so",
    "/usr/lib/llvm-*/lib/libLLVM-*.so",
    "/usr/lib/llvm-*/lib/libLLVM-*.so.*",
    "/usr/lib/llvm-*/lib/libLLVM.so.*",
    "/usr/lib/*-linux-gnu/libLLVM-*.so",
    "/usr/lib/*-linux-gnu/libLLVM-*.so.*",
    "/usr/lib/*-linux-gnu/libLLVM.so.*",
    "/usr/lib64/libLLVM-*.so",
    "/usr/lib64/libLLVM.so",
    "/usr/lib64/libLLVM.so.*",
)

_LLVM_VERSION_RE = re.compile(r"(?:llvm-|libLLVM-|libLLVM\.so\.)(\d+)")


def _llvm_version_sort_key(path: str) -> tuple[int, int, str]:
    """LLVM 共有ライブラリのパスを「新しいバージョン優先・.so 優先」で並べるキーを返す.

    パス中の `llvm-<ver>` / `libLLVM-<ver>` / `libLLVM.so.<ver>` のうち最大の数値を
    バージョンとみなす.バージョンが読めないパスは最後に回す.

    Args:
        path: ライブラリのパス文字列.

    Returns:
        sorted() 用のキー(昇順ソートで新しいバージョンが先頭に来る).
    """
    versions = [int(v) for v in _LLVM_VERSION_RE.findall(path)]
    version = max(versions) if versions else -1
    # 同一バージョン内では開発用シンボリックリンク(*.so)を実体(*.so.N)より優先する
    is_suffixed = 0 if path.endswith(".so") else 1
    return (-version, is_suffixed, path)


def _glob_posix_versioned_libs() -> list[str]:
    """Debian / Ubuntu / Fedora 系のバージョン付き libLLVM を探索して返す.

    Returns:
        見つかったパスのリスト(新しいバージョン優先・重複なし).
    """
    found: set[str] = set()
    for pattern in _POSIX_VERSIONED_LIB_PATTERNS:
        found.update(glob.glob(pattern))
    return sorted(found, key=_llvm_version_sort_key)


def _iter_default_candidates() -> list[str]:
    """LLVM-C.dll / libLLVM-C.so / libLLVM-*.so の標準インストール先パス候補を返す.

    OS に応じて Windows / POSIX を分岐する.
    テスト容易化のため独立関数として定義する.

    Returns:
        パス候補文字列のリスト(優先度順・重複なし).
    """
    candidates: list[str] = []

    # 環境変数 LLVM_DIR が設定されていれば bin/ 配下を優先候補に追加する
    llvm_dir = os.environ.get("LLVM_DIR")
    if llvm_dir:
        if os.name == "nt":
            candidates.append(str(Path(llvm_dir) / "bin" / "LLVM-C.dll"))
        else:
            candidates.append(str(Path(llvm_dir) / "lib" / "libLLVM-C.so"))
            candidates.append(str(Path(llvm_dir) / "lib" / "libLLVM-C.dylib"))

    if os.name == "nt":
        # Windows 標準インストール先
        candidates += [
            r"C:\Program Files\LLVM\bin\LLVM-C.dll",
            r"C:\Program Files (x86)\LLVM\bin\LLVM-C.dll",
            r"C:\ProgramData\chocolatey\lib\llvm\tools\LLVM\bin\LLVM-C.dll",
            r"C:\msys64\mingw64\bin\LLVM-C.dll",
            r"C:\msys64\clang64\bin\LLVM-C.dll",
        ]
    else:
        # POSIX 系(Linux / macOS)
        # glob で llvm-* の複数バージョンを一括検索
        candidates += sorted(
            glob.glob("/usr/lib/llvm-*/lib/libLLVM-C.so"), key=_llvm_version_sort_key
        )
        # Debian / Ubuntu(例: /usr/lib/x86_64-linux-gnu/libLLVM-15.so)/ Fedora 系
        candidates += _glob_posix_versioned_libs()
        candidates += [
            "/usr/local/lib/libLLVM-C.so",
            "/opt/homebrew/opt/llvm/lib/libLLVM-C.dylib",
            "/usr/local/opt/llvm/lib/libLLVM-C.dylib",
        ]

    # `libLLVM-*.so` のパターンは libLLVM-C.so にも一致して同じパスが 2 回入るため,
    # 最初の出現(=優先度の高い位置)だけを残す
    return list(dict.fromkeys(candidates))


def ensure_drjit_libllvm_path(extra_candidates: list[str] | None = None) -> str | None:
    """DRJIT_LIBLLVM_PATH が未設定なら標準パスを探索して os.environ に設定する.

    優先順位:
      1. 既に os.environ['DRJIT_LIBLLVM_PATH'] が設定されている場合はそれを尊重して
         何もしない(戻り値はその既存値).
      2. 環境変数 LLVM_DIR が設定されていれば `$LLVM_DIR/bin/LLVM-C.dll`(Windows)
         または `$LLVM_DIR/lib/libLLVM-C.so` 等(POSIX)を試す.
      3. Windows の標準インストール先:
         - C:\\Program Files\\LLVM\\bin\\LLVM-C.dll
         - C:\\Program Files (x86)\\LLVM\\bin\\LLVM-C.dll
         - C:\\ProgramData\\chocolatey\\lib\\llvm\\tools\\LLVM\\bin\\LLVM-C.dll
         - C:\\msys64\\mingw64\\bin\\LLVM-C.dll
         - C:\\msys64\\clang64\\bin\\LLVM-C.dll
      4. POSIX 系:
         - /usr/lib/llvm-*/lib/libLLVM-C.so(glob)
         - Debian / Ubuntu / Fedora 系のバージョン付き libLLVM(glob.新しいバージョン優先):
           /usr/lib/llvm-*/lib/libLLVM{,-*}.so*,/usr/lib/*-linux-gnu/libLLVM-*.so*,
           /usr/lib/*-linux-gnu/libLLVM.so.*,/usr/lib64/libLLVM*.so*
         - /usr/local/lib/libLLVM-C.so
         - /opt/homebrew/opt/llvm/lib/libLLVM-C.dylib
         - /usr/local/opt/llvm/lib/libLLVM-C.dylib
      5. extra_candidates が指定されていればそれを最後に試す.

    Args:
        extra_candidates: 追加で試すパス候補のリスト(任意).
            標準候補で見つからない場合に最後に試す.

    Returns:
        発見・設定したパス文字列.既設定だった場合はその値.
        何も見つからなければ None.

    Side effect:
        os.environ['DRJIT_LIBLLVM_PATH'] を設定する(未設定かつ発見時のみ).
    """
    # 1. 既に設定されている場合はそのまま返す
    existing = os.environ.get("DRJIT_LIBLLVM_PATH")
    if existing:
        _logger.debug("DRJIT_LIBLLVM_PATH は既に設定されています: %s", existing)
        return existing

    # 標準候補 + 追加候補を順番に探索する
    all_candidates = _iter_default_candidates()
    if extra_candidates:
        all_candidates += extra_candidates

    for candidate in all_candidates:
        if Path(candidate).is_file():
            os.environ["DRJIT_LIBLLVM_PATH"] = candidate
            _logger.info(
                "DRJIT_LIBLLVM_PATH を自動設定しました: %s",
                candidate,
            )
            return candidate

    _logger.warning(
        "LLVM-C.dll / libLLVM-C.so / libLLVM-*.so が標準パスに見つかりませんでした."
        "LLVM をインストールするか,DRJIT_LIBLLVM_PATH 環境変数を手動で設定してください."
        "詳細は docs/02_ENV/ENV_02_環境構築手順.md を参照してください."
    )
    return None


def log_variant_status(logger: logging.Logger, requested_variant: str, actual_variant: str) -> None:
    """variant 設定後の状態を INFO / WARNING でログ出力する.

    actual_variant が requested_variant と異なる(scalar fallback 等)場合は
    WARNING で明示する.

    Args:
        logger: 出力先の Logger オブジェクト.
        requested_variant: 要求した variant 文字列(YAML の variant フィールド等).
        actual_variant: `mi.variant()` で確認した実際の variant 文字列.
    """
    if actual_variant.startswith("llvm_"):
        llvm_path = os.environ.get("DRJIT_LIBLLVM_PATH", "(未設定)")
        logger.info(
            "Mitsuba variant=%s, LLVM JIT 経路で実行(DRJIT_LIBLLVM_PATH=%s)",
            actual_variant,
            llvm_path,
        )
    elif actual_variant.startswith("cuda_"):
        logger.info("Mitsuba variant=%s, CUDA GPU 経路で実行", actual_variant)
    elif actual_variant.startswith("scalar_"):
        logger.info("Mitsuba variant=%s, scalar 経路で実行(単スレッド)", actual_variant)
    else:
        logger.info("Mitsuba variant=%s", actual_variant)

    # 要求が llvm_* なのに scalar_* に落ちている場合は WARNING を追加する
    if requested_variant.startswith("llvm_") and actual_variant.startswith("scalar_"):
        logger.warning(
            "要求 variant=%s に対し実 variant=%s"
            "(LLVM 経路に乗れていません."
            "LLVM-C.dll の場所と DRJIT_LIBLLVM_PATH を確認してください)",
            requested_variant,
            actual_variant,
        )