252 lines
8.6 KiB
Python
252 lines
8.6 KiB
Python
"""Cross-container smoke for the Modal fastvideo-kernel build cache.
|
|
|
|
Example:
|
|
python -m modal run fastvideo/tests/modal/kernel_cache_smoke.py \
|
|
--git-repo https://github.com/macthecadillac/FastVideo.git \
|
|
--git-commit <commit>
|
|
|
|
This is intentionally opt-in: it builds fastvideo-kernel on a real Modal GPU.
|
|
"""
|
|
|
|
import os
|
|
import shlex
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from typing import Any
|
|
|
|
import modal
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
try:
|
|
from modal_image_utils import ( # noqa: E402
|
|
resolve_image_ref, resolve_uv_torch_backend)
|
|
except ModuleNotFoundError:
|
|
# Remote Modal containers re-import this module but mount only the
|
|
# entrypoint file; the digest resolution already happened at local launch.
|
|
def resolve_image_ref(image_ref: str) -> str:
|
|
return image_ref
|
|
|
|
def resolve_uv_torch_backend(image_tag: str) -> str | None:
|
|
return os.environ.get("UV_TORCH_BACKEND")
|
|
|
|
|
|
app = modal.App("fastvideo-kernel-cache-smoke")
|
|
|
|
REPO_DIR = "/FastVideo"
|
|
KERNEL_CACHE_VOLUME_PATH = "/root/fastvideo-kernel-cache"
|
|
KERNEL_CACHE_VOLUME_NAME = "fastvideo-kernel-build-cache-smoke-v3"
|
|
IMAGE_VERSION = os.getenv("IMAGE_VERSION", "latest")
|
|
IMAGE_TAG = os.environ.get(
|
|
"FASTVIDEO_MODAL_IMAGE",
|
|
f"ghcr.io/hao-ai-lab/fastvideo/fastvideo-dev:{IMAGE_VERSION}",
|
|
)
|
|
IMAGE_REF = resolve_image_ref(IMAGE_TAG)
|
|
|
|
print(f"Using image: {IMAGE_REF}")
|
|
print(f"Using kernel cache volume: {KERNEL_CACHE_VOLUME_NAME}")
|
|
|
|
kernel_cache_vol = modal.Volume.from_name(KERNEL_CACHE_VOLUME_NAME, create_if_missing=True)
|
|
uv_torch_backend_override = resolve_uv_torch_backend(IMAGE_TAG)
|
|
|
|
image = (modal.Image.from_registry(IMAGE_REF, add_python="3.12").apt_install(
|
|
"cmake",
|
|
"pkg-config",
|
|
"build-essential",
|
|
"curl",
|
|
"git",
|
|
"libssl-dev",
|
|
"ffmpeg",
|
|
).run_commands("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable").
|
|
run_commands("echo 'source ~/.cargo/env' >> ~/.bashrc").env({
|
|
"PATH":
|
|
"/root/.cargo/bin:$PATH",
|
|
**({
|
|
"UV_TORCH_BACKEND": uv_torch_backend_override
|
|
} if uv_torch_backend_override else {}),
|
|
}))
|
|
|
|
PRODUCER_KWARGS = dict(
|
|
gpu="L40S:1",
|
|
image=image,
|
|
timeout=5400,
|
|
volumes={KERNEL_CACHE_VOLUME_PATH: kernel_cache_vol},
|
|
)
|
|
CONSUMER_KWARGS = dict(
|
|
gpu="L40S:1",
|
|
image=image,
|
|
timeout=5400,
|
|
volumes={KERNEL_CACHE_VOLUME_PATH: kernel_cache_vol.with_mount_options(read_only=True)},
|
|
)
|
|
|
|
|
|
def _run_local_git_command(args: list[str]) -> str:
|
|
result = subprocess.run(
|
|
["git", *args],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def _normalize_git_repo_url(git_repo: str) -> str:
|
|
if git_repo.startswith("git@github.com:"):
|
|
return "https://github.com/" + git_repo[len("git@github.com:"):]
|
|
if git_repo.startswith("ssh://git@github.com/"):
|
|
return "https://github.com/" + git_repo[len("ssh://git@github.com/"):]
|
|
return git_repo
|
|
|
|
|
|
def _resolve_git_repo(git_repo: str) -> str:
|
|
if git_repo.strip():
|
|
return _normalize_git_repo_url(git_repo.strip())
|
|
env_repo = os.environ.get("BUILDKITE_REPO", "").strip()
|
|
if env_repo:
|
|
return _normalize_git_repo_url(env_repo)
|
|
discovered_repo = _run_local_git_command(["config", "--get", "remote.origin.url"])
|
|
if discovered_repo:
|
|
return _normalize_git_repo_url(discovered_repo)
|
|
raise RuntimeError("Could not resolve git repo URL. Pass --git-repo or set BUILDKITE_REPO.")
|
|
|
|
|
|
def _resolve_git_commit(git_commit: str) -> str:
|
|
if git_commit.strip():
|
|
return git_commit.strip()
|
|
env_commit = os.environ.get("BUILDKITE_COMMIT", "").strip()
|
|
if env_commit:
|
|
return env_commit
|
|
discovered_commit = _run_local_git_command(["rev-parse", "HEAD"])
|
|
if discovered_commit:
|
|
return discovered_commit
|
|
raise RuntimeError("Could not resolve git commit. Pass --git-commit or set BUILDKITE_COMMIT.")
|
|
|
|
|
|
def _run(args: list[str], cwd: str | None = None) -> str:
|
|
print("$ " + " ".join(shlex.quote(arg) for arg in args), flush=True)
|
|
result = subprocess.run(
|
|
args,
|
|
cwd=cwd,
|
|
check=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=sys.stderr,
|
|
text=True,
|
|
)
|
|
if result.stdout:
|
|
print(result.stdout, end="", flush=True)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def _run_shell_capture(command: str, cwd: str) -> str:
|
|
print(f"$ {command}", flush=True)
|
|
result = subprocess.run(
|
|
["/bin/bash", "-lc", command],
|
|
cwd=cwd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.stdout:
|
|
print(result.stdout, end="", flush=True)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"Command failed with exit code {result.returncode}: {command}")
|
|
return result.stdout
|
|
|
|
|
|
def _clone_checkout(git_repo: str, git_commit: str) -> str:
|
|
shutil.rmtree(REPO_DIR, ignore_errors=True)
|
|
_run(["git", "-c", "http.version=HTTP/1.1", "clone", git_repo, REPO_DIR], cwd="/")
|
|
_run(["git", "checkout", git_commit], cwd=REPO_DIR)
|
|
_run(["git", "submodule", "update", "--init", "--recursive"], cwd=REPO_DIR)
|
|
return _run(["git", "rev-parse", "HEAD"], cwd=REPO_DIR)
|
|
|
|
|
|
def _cache_command_output(command: str, cache_root: str) -> str:
|
|
# This smoke exercises the Modal volume cache specifically, so do not let
|
|
# a matching Docker-prebuilt wheel satisfy the miss/hit assertions.
|
|
disabled_prebuilt_info_path = f"{cache_root}/.docker-prebuilt-disabled.json"
|
|
return _run_shell_capture(
|
|
"source $HOME/.local/bin/env && "
|
|
"source /opt/venv/bin/activate && "
|
|
f"python fastvideo/tests/modal/kernel_build_cache.py {shlex.quote(command)} "
|
|
f"--cache-root {shlex.quote(cache_root)} "
|
|
f"--prebuilt-info-path {shlex.quote(disabled_prebuilt_info_path)}",
|
|
cwd=REPO_DIR,
|
|
)
|
|
|
|
|
|
def _assert_in_output(output: str, needle: str) -> None:
|
|
if needle not in output:
|
|
raise RuntimeError(f"Expected output to contain {needle!r}")
|
|
|
|
|
|
def _assert_not_in_output(output: str, needle: str) -> None:
|
|
if needle in output:
|
|
raise RuntimeError(f"Expected output not to contain {needle!r}")
|
|
|
|
|
|
@app.function(**PRODUCER_KWARGS)
|
|
def build_and_store_cache(git_repo: str, git_commit: str, cache_root: str) -> dict[str, Any]:
|
|
checked_out_commit = _clone_checkout(git_repo, git_commit)
|
|
output = _cache_command_output("produce", cache_root)
|
|
_assert_in_output(output, "cache miss:")
|
|
_assert_in_output(output, "./build.sh --wheel-dir")
|
|
_assert_in_output(output, "stored wheel cache entry:")
|
|
print("Committing kernel cache volume after miss/store", flush=True)
|
|
kernel_cache_vol.commit()
|
|
return {
|
|
"checked_out_commit": checked_out_commit,
|
|
"cache_root": cache_root,
|
|
"cache_miss": True,
|
|
"stored": True,
|
|
}
|
|
|
|
|
|
@app.function(**CONSUMER_KWARGS)
|
|
def verify_cache_hit(git_repo: str, git_commit: str, cache_root: str) -> dict[str, Any]:
|
|
checked_out_commit = _clone_checkout(git_repo, git_commit)
|
|
output = _cache_command_output("install", cache_root)
|
|
_assert_in_output(output, "cache hit:")
|
|
_assert_not_in_output(output, "cache miss:")
|
|
_assert_not_in_output(output, "./build.sh --wheel-dir")
|
|
return {
|
|
"checked_out_commit": checked_out_commit,
|
|
"cache_root": cache_root,
|
|
"cache_hit": True,
|
|
}
|
|
|
|
|
|
@app.function(image=image, timeout=300, volumes={KERNEL_CACHE_VOLUME_PATH: kernel_cache_vol})
|
|
def cleanup_cache(cache_root: str) -> None:
|
|
shutil.rmtree(cache_root, ignore_errors=True)
|
|
print(f"Removed smoke cache root {cache_root}", flush=True)
|
|
kernel_cache_vol.commit()
|
|
|
|
|
|
@app.local_entrypoint()
|
|
def main(
|
|
git_repo: str = "",
|
|
git_commit: str = "",
|
|
cache_namespace: str = "",
|
|
cleanup: bool = True,
|
|
):
|
|
resolved_git_repo = _resolve_git_repo(git_repo)
|
|
resolved_git_commit = _resolve_git_commit(git_commit)
|
|
namespace = cache_namespace.strip() or f"smoke-{resolved_git_commit[:12]}-{int(time.time())}"
|
|
cache_root = f"{KERNEL_CACHE_VOLUME_PATH}/{namespace}"
|
|
|
|
print("Launching cross-container kernel cache smoke")
|
|
print(f"Repo: {resolved_git_repo}")
|
|
print(f"Commit: {resolved_git_commit}")
|
|
print(f"Cache root: {cache_root}")
|
|
|
|
miss_result = build_and_store_cache.remote(resolved_git_repo, resolved_git_commit, cache_root)
|
|
hit_result = verify_cache_hit.remote(resolved_git_repo, resolved_git_commit, cache_root)
|
|
if cleanup:
|
|
cleanup_cache.remote(cache_root)
|
|
|
|
print("Kernel cache smoke passed")
|
|
print({"miss": miss_result, "hit": hit_result, "cleanup": cleanup})
|