Files
hao-ai-lab-FastVideo/fastvideo/tests/modal/kernel_build_cache.py
T

570 lines
22 KiB
Python

"""Build and reuse FastVideo kernel wheels in the dormant Modal rollback path.
This module stays standalone because Modal launchers execute it from a freshly
cloned checkout after dependency installation. Shared cache consumers are
read-only; only the explicit ``produce`` command may mutate cache state.
"""
import argparse
import datetime
import hashlib
import json
import os
import platform
import re
import shlex
import shutil
import subprocess
import sys
import sysconfig
import tempfile
import zipfile
from email.parser import Parser
from pathlib import Path
CACHE_SCHEMA_VERSION = 4
DEFAULT_PREBUILT_INFO_PATH = "/opt/fastvideo-kernel-prebuilt"
KERNEL_RELATIVE_DIR = "fastvideo-kernel"
DEFAULT_BUILD_INFO_OUTPUT = "/opt/fastvideo-kernel-prebuilt/default/metadata.json"
METADATA_FILE = "metadata.json"
EXPECTED_DISTRIBUTION = "fastvideo-kernel"
def _utc_now_isoformat() -> str:
return datetime.datetime.now(datetime.timezone.utc).isoformat()
def _log(message: str) -> None:
print(f"[fastvideo-kernel-cache] {message}", flush=True)
def _run(args: list[str], *, cwd: Path | None = None, env: dict[str, str] | None = None, capture: bool = False) -> str:
_log("$ " + " ".join(str(arg) for arg in args))
result = subprocess.run(
args,
cwd=str(cwd) if cwd else None,
env=env,
check=True,
text=True,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.STDOUT if capture else None,
)
if capture:
return result.stdout.strip()
return ""
def _run_optional(args: list[str], *, cwd: Path | None = None) -> str:
try:
return _run(args, cwd=cwd, capture=True)
except (FileNotFoundError, subprocess.CalledProcessError, OSError) as error:
return f"<unavailable: {error}>"
def _sha256_text(text: str) -> str:
return hashlib.sha256(text.encode("utf-8", errors="replace")).hexdigest()
def _sha256_file(path: Path) -> str:
hasher = hashlib.sha256()
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
hasher.update(chunk)
return hasher.hexdigest()
def _hash_directory(root: Path) -> str:
hasher = hashlib.sha256()
skip_dirs = {".git", ".mypy_cache", ".pytest_cache", "__pycache__", "build", "dist"}
for current_root, dirnames, filenames in os.walk(root):
dirnames[:] = sorted(dirname for dirname in dirnames
if dirname not in skip_dirs and not dirname.endswith(".egg-info"))
for filename in sorted(filenames):
path = Path(current_root) / filename
hasher.update(path.relative_to(root).as_posix().encode("utf-8"))
hasher.update(b"\0")
with path.open("rb") as file:
for chunk in iter(lambda: file.read(1024 * 1024), b""):
hasher.update(chunk)
hasher.update(b"\0")
return hasher.hexdigest()
def _kernel_source_hash(repo_root: Path) -> dict[str, str]:
kernel_root = repo_root / KERNEL_RELATIVE_DIR
try:
tree_hash = _run(["git", "rev-parse", f"HEAD:{KERNEL_RELATIVE_DIR}"], cwd=repo_root, capture=True)
diff = _run_optional(["git", "diff", "--binary", "--", KERNEL_RELATIVE_DIR], cwd=repo_root)
cached_diff = _run_optional(["git", "diff", "--cached", "--binary", "--", KERNEL_RELATIVE_DIR], cwd=repo_root)
submodules = _run_optional(
[
"git",
"submodule",
"status",
"--",
"fastvideo-kernel/include/cutlass",
"fastvideo-kernel/include/tk",
],
cwd=repo_root,
)
descriptor = {
"mode": "git",
"tree_hash": tree_hash,
"diff_hash": _sha256_text(diff) if diff else "",
"cached_diff_hash": _sha256_text(cached_diff) if cached_diff else "",
"submodules_hash": _sha256_text(submodules),
}
descriptor["source_hash"] = _sha256_text(json.dumps(descriptor, sort_keys=True))
return descriptor
except (FileNotFoundError, subprocess.CalledProcessError):
descriptor = {
"mode": "directory",
"tree_hash": _hash_directory(kernel_root),
"diff_hash": "",
"cached_diff_hash": "",
"submodules_hash": "",
}
descriptor["source_hash"] = descriptor["tree_hash"]
return descriptor
def _read_kernel_version(repo_root: Path) -> str:
pyproject = repo_root / KERNEL_RELATIVE_DIR / "pyproject.toml"
for line in pyproject.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if stripped.startswith("version"):
_, _, value = stripped.partition("=")
return value.strip().strip("\"'")
return "<unknown>"
def _detect_arch_from_torch() -> str:
try:
import torch
if not torch.cuda.is_available():
return "<unavailable>"
major, minor = torch.cuda.get_device_capability(0)
if major == 9 and minor == 0:
return "9.0a"
if major == 12 and minor == 0:
return "12.0a"
return f"{major}.{minor}"
except Exception as error: # noqa: BLE001 - metadata should explain fallback
return f"<unavailable: {error}>"
def _torch_metadata() -> dict[str, object]:
try:
import torch
compiled_with_cxx11_abi = getattr(torch, "compiled_with_cxx11_abi", None)
if callable(compiled_with_cxx11_abi):
cxx11_abi: object = bool(compiled_with_cxx11_abi())
else:
cxx11_abi = getattr(getattr(torch, "_C", object()), "_GLIBCXX_USE_CXX11_ABI", "<unavailable>")
return {
"torch_version": str(torch.__version__),
"torch_cuda_version": str(torch.version.cuda),
"torch_git_version": str(getattr(torch.version, "git_version", "")),
"torch_file": str(getattr(torch, "__file__", "")),
"torch_config": str(torch.__config__.show()),
"cxx11_abi": cxx11_abi,
}
except Exception as error: # noqa: BLE001 - report in metadata
return {
"torch_version": f"<unavailable: {error}>",
"torch_cuda_version": "<unavailable>",
"torch_git_version": "<unavailable>",
"torch_file": "<unavailable>",
"torch_config": "<unavailable>",
"cxx11_abi": "<unavailable>",
}
def _selected_command_metadata(environment_name: str, default_command: str) -> dict[str, str]:
raw = os.environ.get(environment_name, "").strip() or default_command
try:
command = shlex.split(raw)
except ValueError as error:
return {
"raw": raw,
"command": "<invalid>",
"resolved_executable": "<invalid>",
"version": f"<unavailable: {error}>",
}
if not command:
command = [default_command]
resolved_executable = shutil.which(command[0]) or command[0]
return {
"raw": raw,
"command": json.dumps(command),
"resolved_executable": resolved_executable,
"version": _run_optional([*command, "--version"]),
}
def _compiler_libc_metadata() -> dict[str, object]:
return {
"compiler": {
"cc": _selected_command_metadata("CC", "cc"),
"cxx": _selected_command_metadata("CXX", "c++"),
},
"build_tools": {
"cmake_version": _run_optional(["cmake", "--version"]),
"ninja_version": _run_optional(["ninja", "--version"]),
"linker": _selected_command_metadata("LD", "ld"),
},
"libc": {
"platform_libc": list(platform.libc_ver()),
"ldd_version": _run_optional(["ldd", "--version"]),
},
}
def _build_metadata(repo_root: Path) -> dict[str, object]:
explicit_arch = os.environ.get("TORCH_CUDA_ARCH_LIST", "").strip()
resolved_arch = explicit_arch or _detect_arch_from_torch()
torch_metadata = _torch_metadata()
torch_cache_metadata = {
name: torch_metadata[name]
for name in ("torch_version", "torch_cuda_version", "torch_git_version", "cxx11_abi")
}
cache_key_build = {
"gpu_backend": os.environ.get("GPU_BACKEND", "CUDA"),
"resolved_torch_cuda_arch_list": resolved_arch,
"cmake_args": os.environ.get("CMAKE_ARGS", ""),
"cflags": os.environ.get("CFLAGS", ""),
"cxxflags": os.environ.get("CXXFLAGS", ""),
"ldflags": os.environ.get("LDFLAGS", ""),
}
cache_key_metadata: dict[str, object] = {
"schema_version": CACHE_SCHEMA_VERSION,
"kernel_version": _read_kernel_version(repo_root),
"source": _kernel_source_hash(repo_root),
"python": {
"version": platform.python_version(),
"implementation": platform.python_implementation(),
"cache_tag": sys.implementation.cache_tag,
"platform": sysconfig.get_platform(),
"machine": platform.machine(),
},
"torch": torch_cache_metadata,
"cuda": {
"cuda_home": os.environ.get("CUDA_HOME", ""),
"nvcc": _selected_command_metadata("CUDACXX", "nvcc"),
},
"abi": _compiler_libc_metadata(),
"build": cache_key_build,
}
metadata = {
**cache_key_metadata,
"torch": torch_metadata,
"build": {
**cache_key_build,
"torch_cuda_arch_list": explicit_arch,
},
}
metadata["cache_key"] = _sha256_text(json.dumps(cache_key_metadata, sort_keys=True, separators=(",", ":")))
return metadata
def _find_wheel(directory: Path) -> Path:
candidates = sorted(path for pattern in ("fastvideo_kernel-*.whl", "fastvideo-kernel-*.whl")
for path in directory.glob(pattern))
if not candidates:
raise RuntimeError(f"No fastvideo-kernel wheel found in {directory}")
return candidates[-1]
def _normalize_distribution(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).lower()
def _wheel_artifact(wheel: Path) -> dict[str, object]:
if not wheel.is_file() or wheel.is_symlink():
raise RuntimeError(f"wheel is missing or is not a regular file: {wheel}")
try:
with zipfile.ZipFile(wheel) as archive:
bad_member = archive.testzip()
if bad_member is not None:
raise RuntimeError(f"wheel archive has a corrupt member: {bad_member}")
names = archive.namelist()
wheel_metadata = [name for name in names if name.endswith(".dist-info/WHEEL")]
package_metadata = [name for name in names if name.endswith(".dist-info/METADATA")]
if len(wheel_metadata) != 1 or len(package_metadata) != 1:
raise RuntimeError("wheel archive must contain one WHEEL and one METADATA file")
parsed_metadata = Parser().parsestr(archive.read(package_metadata[0]).decode("utf-8"))
except (OSError, UnicodeDecodeError, zipfile.BadZipFile) as error:
raise RuntimeError(f"invalid wheel archive: {error}") from error
distribution = _normalize_distribution(parsed_metadata.get("Name", ""))
if distribution != EXPECTED_DISTRIBUTION:
raise RuntimeError(f"unexpected wheel distribution {distribution!r}")
return {
"wheel_name": wheel.name,
"size_bytes": wheel.stat().st_size,
"sha256": _sha256_file(wheel),
"distribution": distribution,
}
def _validate_wheel(wheel: Path, expected_artifact: object) -> tuple[bool, str]:
if not isinstance(expected_artifact, dict):
return False, "artifact metadata is missing"
try:
actual_artifact = _wheel_artifact(wheel)
except RuntimeError as error:
return False, str(error)
for field in ("wheel_name", "size_bytes", "sha256", "distribution"):
if actual_artifact[field] != expected_artifact.get(field):
return False, f"artifact {field} mismatch"
return True, ""
def _install_wheel(wheel: Path) -> None:
_log(f"installing wheel {wheel}")
_run([
"uv",
"pip",
"install",
str(wheel),
"--reinstall-package",
"fastvideo-kernel",
"--no-deps",
])
def _load_metadata(path: Path) -> tuple[dict[str, object] | None, str]:
if path.is_symlink():
return None, "metadata is a symlink"
try:
metadata = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
return None, f"metadata is unreadable: {error}"
if not isinstance(metadata, dict):
return None, "metadata is not an object"
return metadata, ""
def _validated_payload_wheel(payload: dict[str, object], wheel: Path, cache_key: str) -> tuple[Path | None, str]:
if payload.get("schema_version") != CACHE_SCHEMA_VERSION:
return None, "schema version mismatch"
if payload.get("cache_key") != cache_key:
return None, "cache key mismatch"
valid, reason = _validate_wheel(wheel, payload.get("artifact"))
if not valid:
return None, reason
return wheel, ""
def _cache_entry_wheel(cache_entry: Path, cache_key: str) -> tuple[Path | None, str]:
if cache_entry.is_symlink():
return None, "cache entry is a symlink"
metadata, reason = _load_metadata(cache_entry / METADATA_FILE)
if metadata is None:
return None, reason
artifact = metadata.get("artifact")
wheel_name = artifact.get("wheel_name") if isinstance(artifact, dict) else None
if not isinstance(wheel_name, str):
return None, "artifact wheel name is missing"
wheel = cache_entry / wheel_name
return _validated_payload_wheel(metadata, wheel, cache_key)
def _prebuilt_metadata_paths(prebuilt_info_path: Path) -> list[Path]:
if prebuilt_info_path.is_dir() and not prebuilt_info_path.is_symlink():
return sorted(prebuilt_info_path.glob(f"*/{METADATA_FILE}"))
if prebuilt_info_path.exists() or prebuilt_info_path.is_symlink():
return [prebuilt_info_path]
return []
def _try_install_prebuilt(metadata: dict[str, object], prebuilt_info_path: Path) -> bool:
cache_key = str(metadata["cache_key"])
for metadata_path in _prebuilt_metadata_paths(prebuilt_info_path):
prebuilt, reason = _load_metadata(metadata_path)
if prebuilt is None:
_log(f"Docker-prebuilt kernel metadata is invalid at {metadata_path}: {reason}")
continue
wheel_path = Path(str(prebuilt.get("wheel_path", "")))
wheel, reason = _validated_payload_wheel(prebuilt, wheel_path, cache_key)
if wheel is None:
_log(f"Docker-prebuilt kernel artifact rejected at {metadata_path}: {reason}")
continue
try:
_log(f"Docker-prebuilt cache hit for key {cache_key}: {wheel}")
_install_wheel(wheel)
except (FileNotFoundError, OSError, subprocess.CalledProcessError) as error:
_log(f"Docker-prebuilt kernel installation failed: {error}; falling back")
return False
return True
_log(f"Docker-prebuilt cache miss for key {cache_key}")
return False
def _store_cache_entry(cache_root: Path, metadata: dict[str, object], wheel: Path) -> Path:
cache_key = str(metadata["cache_key"])
cache_entry = cache_root / cache_key
if cache_entry.exists() or cache_entry.is_symlink():
cached_wheel, reason = _cache_entry_wheel(cache_entry, cache_key)
if cached_wheel is not None:
_log(f"cache entry {cache_entry} appeared after build; leaving existing entry in place")
return cache_entry
_log(f"evicting invalid cache entry {cache_entry}: {reason}")
if cache_entry.is_dir() and not cache_entry.is_symlink():
shutil.rmtree(cache_entry)
else:
cache_entry.unlink()
temp_entry = Path(tempfile.mkdtemp(prefix=f".{cache_key}.tmp-", dir=cache_root))
try:
stored_wheel = temp_entry / wheel.name
shutil.copy2(wheel, stored_wheel)
stored_metadata = {
**metadata,
"created_at_utc": _utc_now_isoformat(),
"artifact": _wheel_artifact(stored_wheel),
}
(temp_entry / METADATA_FILE).write_text(json.dumps(stored_metadata, indent=2, sort_keys=True), encoding="utf-8")
temp_entry.rename(cache_entry)
except OSError as error:
shutil.rmtree(temp_entry, ignore_errors=True)
cached_wheel, _ = _cache_entry_wheel(cache_entry, cache_key)
if cached_wheel is not None:
_log(f"cache entry {cache_entry} was created concurrently; leaving existing entry in place")
return cache_entry
raise RuntimeError(f"Failed to store kernel cache entry at {cache_entry}") from error
cached_wheel, reason = _cache_entry_wheel(cache_entry, cache_key)
if cached_wheel is None:
shutil.rmtree(cache_entry, ignore_errors=True)
raise RuntimeError(f"Stored kernel cache entry {cache_entry} could not be validated: {reason}")
return cache_entry
def _build_wheel(repo_root: Path, metadata: dict[str, object]) -> Path:
with tempfile.TemporaryDirectory(prefix="fastvideo-kernel-wheel-") as temp_dir:
wheel_dir = Path(temp_dir)
env = os.environ.copy()
build_metadata = metadata["build"]
if not isinstance(build_metadata, dict):
raise RuntimeError("build metadata is invalid")
resolved_arch = str(build_metadata["resolved_torch_cuda_arch_list"])
if not env.get("TORCH_CUDA_ARCH_LIST") and not resolved_arch.startswith("<unavailable"):
env["TORCH_CUDA_ARCH_LIST"] = resolved_arch
_run(["./build.sh", "--wheel-dir", str(wheel_dir)], cwd=repo_root / KERNEL_RELATIVE_DIR, env=env)
wheel = _find_wheel(wheel_dir)
persisted = Path(tempfile.mkdtemp(prefix="fastvideo-kernel-built-wheel-")) / wheel.name
shutil.copy2(wheel, persisted)
return persisted
def _build_and_install_local(repo_root: Path, metadata: dict[str, object]) -> None:
wheel = _build_wheel(repo_root, metadata)
try:
_wheel_artifact(wheel)
_install_wheel(wheel)
finally:
shutil.rmtree(wheel.parent, ignore_errors=True)
def install_cached_or_build(repo_root: Path, cache_root: Path | None, prebuilt_info_path: Path) -> None:
"""Install from a validated artifact or build locally without writing shared state."""
metadata = _build_metadata(repo_root)
cache_key = str(metadata["cache_key"])
_log(f"resolved cache key {cache_key}")
if _try_install_prebuilt(metadata, prebuilt_info_path):
return
if cache_root is not None:
cache_entry = cache_root / cache_key
cached_wheel, reason = _cache_entry_wheel(cache_entry, cache_key)
if cached_wheel is not None:
try:
_log(f"cache hit: {cached_wheel}")
_install_wheel(cached_wheel)
return
except (FileNotFoundError, OSError, subprocess.CalledProcessError) as error:
_log(f"cached wheel installation failed: {error}; building locally")
else:
_log(f"cache miss: {cache_entry} ({reason})")
else:
_log("shared cache disabled; using Docker-prebuilt artifact or a job-local build")
_build_and_install_local(repo_root, metadata)
def produce_cached_wheel(repo_root: Path, cache_root: Path) -> None:
"""Build and store one validated cache entry from a trusted producer."""
metadata = _build_metadata(repo_root)
cache_key = str(metadata["cache_key"])
_log(f"resolved cache key {cache_key}")
cache_root.mkdir(parents=True, exist_ok=True)
cache_entry = cache_root / cache_key
cached_wheel, reason = _cache_entry_wheel(cache_entry, cache_key)
if cached_wheel is not None:
_log(f"cache hit: {cached_wheel}")
return
if cache_entry.exists() or cache_entry.is_symlink():
_log(f"producer will repair invalid cache entry {cache_entry}: {reason}")
_log(f"cache miss: {cache_entry}")
wheel = _build_wheel(repo_root, metadata)
try:
_wheel_artifact(wheel)
_install_wheel(wheel)
stored_entry = _store_cache_entry(cache_root, metadata, wheel)
_log(f"stored wheel cache entry: {stored_entry}")
finally:
shutil.rmtree(wheel.parent, ignore_errors=True)
def write_build_info(repo_root: Path, wheel_dir: Path, output: Path) -> None:
metadata = _build_metadata(repo_root)
wheel = _find_wheel(wheel_dir)
output.parent.mkdir(parents=True, exist_ok=True)
payload = {
**metadata,
"created_at_utc": _utc_now_isoformat(),
"artifact": _wheel_artifact(wheel),
"wheel_path": str(wheel),
}
output.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
_log(f"wrote Docker-prebuilt kernel metadata: {output}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("command", choices=("install", "produce", "write-build-info", "print-key"))
parser.add_argument("--repo-root", default=os.getcwd())
parser.add_argument("--cache-root", default=os.environ.get("FASTVIDEO_KERNEL_CACHE_ROOT", ""))
parser.add_argument("--prebuilt-info-path",
default=os.environ.get("FASTVIDEO_KERNEL_PREBUILT_INFO", DEFAULT_PREBUILT_INFO_PATH))
parser.add_argument("--wheel-dir", default="")
parser.add_argument("--output", default=DEFAULT_BUILD_INFO_OUTPUT)
return parser.parse_args()
def main() -> None:
args = parse_args()
repo_root = Path(args.repo_root).resolve()
cache_root = Path(args.cache_root) if args.cache_root else None
if args.command == "install":
install_cached_or_build(repo_root, cache_root, Path(args.prebuilt_info_path))
elif args.command == "produce":
if cache_root is None:
raise RuntimeError("--cache-root is required for produce")
produce_cached_wheel(repo_root, cache_root)
elif args.command == "write-build-info":
if not args.wheel_dir:
raise RuntimeError("--wheel-dir is required for write-build-info")
write_build_info(repo_root, Path(args.wheel_dir), Path(args.output))
elif args.command == "print-key":
print(json.dumps(_build_metadata(repo_root), indent=2, sort_keys=True))
if __name__ == "__main__":
main()