[infra] Add Dreamverse Modal UI image build (#1381)
This commit is contained in:
@@ -16,6 +16,14 @@ on:
|
||||
required: false
|
||||
type: string
|
||||
default: fastvideo-dev
|
||||
build_args:
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
include_latest_tags:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
@@ -71,11 +79,14 @@ jobs:
|
||||
run: |
|
||||
SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7)
|
||||
|
||||
TAGS="type=raw,value=${{ inputs.tag_suffix }}-latest"
|
||||
TAGS="${TAGS}\ntype=raw,value=${{ inputs.tag_suffix }}-sha-${SHORT_SHA}"
|
||||
|
||||
TAGS="type=raw,value=${{ inputs.tag_suffix }}-sha-${SHORT_SHA}"
|
||||
|
||||
if [[ "${{ inputs.include_latest_tags }}" == "true" ]]; then
|
||||
TAGS="type=raw,value=${{ inputs.tag_suffix }}-latest\n${TAGS}"
|
||||
fi
|
||||
|
||||
# Set Python 3.10 as the default image
|
||||
if [[ "${{ inputs.python_version }}" == "3.10" ]]; then
|
||||
if [[ "${{ inputs.include_latest_tags }}" == "true" && "${{ inputs.python_version }}" == "3.10" ]]; then
|
||||
TAGS="${TAGS}\ntype=raw,value=latest"
|
||||
fi
|
||||
|
||||
@@ -101,10 +112,11 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: ${{ inputs.build_args }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Success message
|
||||
run: |
|
||||
echo "✅ Python ${{ inputs.python_version }} image successfully built and pushed to ghcr.io/${{ github.repository }}/${{ inputs.image_name }}:${{ inputs.tag_suffix }}-latest"
|
||||
echo "✅ Python ${{ inputs.python_version }} image successfully built and pushed to ghcr.io/${{ github.repository }}/${{ inputs.image_name }}:${{ inputs.tag_suffix }}-sha-${GITHUB_SHA::7}"
|
||||
echo "To run tests with this image, manually trigger the 'Run Tests' workflow."
|
||||
|
||||
@@ -24,7 +24,7 @@ on:
|
||||
default: false
|
||||
type: boolean
|
||||
dreamverse_cuda_12_9:
|
||||
description: 'Build Dreamverse CUDA 12.9 image'
|
||||
description: 'Build Dreamverse CUDA 12.9 backend-only and UI images'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
@@ -71,12 +71,26 @@ jobs:
|
||||
tag_suffix: py3.12-cuda12.9.1
|
||||
secrets: inherit
|
||||
|
||||
build-dreamverse-cuda-12-9:
|
||||
build-dreamverse-backend-cuda-12-9:
|
||||
if: ${{ github.event.inputs.dreamverse_cuda_12_9 == 'true' }}
|
||||
uses: ./.github/workflows/_template-build-image.yml
|
||||
with:
|
||||
python_version: '3.12'
|
||||
dockerfile_path: apps/dreamverse/docker/Dockerfile
|
||||
tag_suffix: dreamverse-cuda12.9.1
|
||||
tag_suffix: dreamverse-backend-cuda12.9.1
|
||||
image_name: dreamverse
|
||||
build_args: BUILD_DREAMVERSE_UI=0
|
||||
include_latest_tags: false
|
||||
secrets: inherit
|
||||
|
||||
build-dreamverse-ui-cuda-12-9:
|
||||
if: ${{ github.event.inputs.dreamverse_cuda_12_9 == 'true' }}
|
||||
uses: ./.github/workflows/_template-build-image.yml
|
||||
with:
|
||||
python_version: '3.12'
|
||||
dockerfile_path: apps/dreamverse/docker/Dockerfile
|
||||
tag_suffix: dreamverse-ui-cuda12.9.1
|
||||
image_name: dreamverse
|
||||
build_args: BUILD_DREAMVERSE_UI=1
|
||||
include_latest_tags: false
|
||||
secrets: inherit
|
||||
|
||||
@@ -3,6 +3,7 @@ ARG CUDA_TAG=12.9.1-cudnn-devel-ubuntu22.04
|
||||
FROM nvidia/cuda:${CUDA_TAG}
|
||||
|
||||
ARG BUILD_FASTVIDEO_KERNEL_FROM_SOURCE=0
|
||||
ARG BUILD_DREAMVERSE_UI=0
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
@@ -50,6 +51,19 @@ RUN if [[ "${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE}" == "1" ]]; then \
|
||||
echo "Skipping source fastvideo-kernel build; using installed fastvideo-kernel package."; \
|
||||
fi
|
||||
|
||||
RUN if [[ "${BUILD_DREAMVERSE_UI}" == "1" ]]; then \
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& corepack enable \
|
||||
&& cd /opt/FastVideo/apps/dreamverse/web \
|
||||
&& pnpm install --frozen-lockfile --ignore-scripts \
|
||||
&& NEXT_OUTPUT_EXPORT=1 pnpm build \
|
||||
&& rm -rf node_modules .next; \
|
||||
else \
|
||||
echo "Skipping Dreamverse UI build; Building backend-only image."; \
|
||||
fi
|
||||
|
||||
# The monorepo ffmpeg installer force-selects conda compiler triplets for
|
||||
# local dev shells. Inside this image we explicitly opt into the system
|
||||
# gcc/g++ toolchain.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Dreamverse Docker Image
|
||||
|
||||
This folder contains the backend-only Docker image for Dreamverse inside the
|
||||
FastVideo monorepo. Build commands use the FastVideo repository root as the
|
||||
Docker context, so run the helper scripts from this folder or from any path in
|
||||
the checkout.
|
||||
This folder contains the Docker image for Dreamverse inside the FastVideo
|
||||
monorepo. Build commands use the FastVideo repository root as the Docker
|
||||
context, so run the helper scripts from this folder or from any path in the
|
||||
checkout.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -17,6 +17,16 @@ The image defaults to `dreamverse:dev`. Override it with:
|
||||
DREAMVERSE_IMAGE=dreamverse:local apps/dreamverse/docker/docker_build.sh
|
||||
```
|
||||
|
||||
Backend-only remains the default image. To include the static Dreamverse UI
|
||||
served by the backend, set `BUILD_DREAMVERSE_UI=1` and choose a specific image
|
||||
tag:
|
||||
|
||||
```bash
|
||||
BUILD_DREAMVERSE_UI=1 DREAMVERSE_IMAGE=<image-tag> apps/dreamverse/docker/docker_build.sh
|
||||
```
|
||||
|
||||
Prefer SHA-specific tags for deployable images; avoid `latest`.
|
||||
|
||||
The Dockerfile builds a CUDA 12.9.1 image, installs FastVideo from this
|
||||
checkout with the `dreamverse` extra, installs the FA4
|
||||
flash-attention fork, builds native FFmpeg, and installs FlashInfer for NVFP4
|
||||
|
||||
@@ -24,6 +24,7 @@ build_args=()
|
||||
[[ -n "${CUDA_TAG:-}" ]] && build_args+=(--build-arg "CUDA_TAG=${CUDA_TAG}")
|
||||
[[ -n "${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE:-}" ]] && \
|
||||
build_args+=(--build-arg "BUILD_FASTVIDEO_KERNEL_FROM_SOURCE=${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE}")
|
||||
build_args+=(--build-arg "BUILD_DREAMVERSE_UI=${BUILD_DREAMVERSE_UI:-0}")
|
||||
|
||||
if [[ "${DOCKER_BUILDKIT:-}" == "1" ]]; then
|
||||
if [[ -L "${ROOT_DOCKERIGNORE}" ]] && \
|
||||
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
_SERVER_ROOT = Path(__file__).resolve().parent
|
||||
_FASTVIDEO_DREAMVERSE_HOME = os.environ.get("FASTVIDEO_DREAMVERSE_HOME")
|
||||
_FASTVIDEO_DREAMVERSE_FRONTEND_ROOT = os.environ.get("FASTVIDEO_DREAMVERSE_FRONTEND_ROOT")
|
||||
_XDG_STATE_HOME = os.environ.get("XDG_STATE_HOME")
|
||||
_DEFAULT_STATE_ROOT = (Path(_FASTVIDEO_DREAMVERSE_HOME) if _FASTVIDEO_DREAMVERSE_HOME else
|
||||
(Path(_XDG_STATE_HOME) if _XDG_STATE_HOME else Path.home() / ".local/state") /
|
||||
@@ -16,18 +17,39 @@ _APP_ROOT = _REPO_ROOT
|
||||
|
||||
def _resolve_frontend_root() -> Path:
|
||||
for candidate in (
|
||||
Path(_FASTVIDEO_DREAMVERSE_FRONTEND_ROOT) if _FASTVIDEO_DREAMVERSE_FRONTEND_ROOT else None,
|
||||
_APP_ROOT / "web",
|
||||
_APP_ROOT / "prod-ui",
|
||||
):
|
||||
if candidate.is_dir():
|
||||
if candidate is not None and candidate.is_dir():
|
||||
return candidate
|
||||
if _FASTVIDEO_DREAMVERSE_FRONTEND_ROOT:
|
||||
return Path(_FASTVIDEO_DREAMVERSE_FRONTEND_ROOT)
|
||||
return _APP_ROOT / "web"
|
||||
|
||||
|
||||
def _resolve_frontend_static_dir_candidates() -> tuple[str, ...]:
|
||||
roots = (
|
||||
FRONTEND_ROOT,
|
||||
Path.cwd() / "apps/dreamverse/web",
|
||||
Path.cwd() / "web",
|
||||
Path("/opt/FastVideo/apps/dreamverse/web"),
|
||||
)
|
||||
candidates: list[str] = []
|
||||
seen: set[Path] = set()
|
||||
for root in roots:
|
||||
resolved_root = root.resolve(strict=False)
|
||||
if resolved_root in seen:
|
||||
continue
|
||||
seen.add(resolved_root)
|
||||
candidates.extend(str(resolved_root / dirname) for dirname in ("out", "dist"))
|
||||
return tuple(candidates)
|
||||
|
||||
|
||||
FRONTEND_ROOT = _resolve_frontend_root()
|
||||
_CLIENT_PROMPTS_ROOT = FRONTEND_ROOT / "prompts"
|
||||
_CLIENT_PROMPTS_LOCAL_ROOT = FRONTEND_ROOT / "prompts.local"
|
||||
FRONTEND_STATIC_DIR_CANDIDATES = tuple(str(FRONTEND_ROOT / dirname) for dirname in ("out", "dist"))
|
||||
FRONTEND_STATIC_DIR_CANDIDATES = _resolve_frontend_static_dir_candidates()
|
||||
|
||||
# Model registry
|
||||
MODEL_REGISTRY = {
|
||||
|
||||
@@ -124,12 +124,21 @@ def test_config_uses_local_overlay_paths_when_devtools_enabled(monkeypatch, tmp_
|
||||
assert module.CURATED_PRESETS_FALLBACK_FILE_PATH.endswith(
|
||||
"apps/dreamverse/web/prompts/selected_ltx2_continuation_story_presets.json"
|
||||
)
|
||||
assert module.FRONTEND_STATIC_DIR_CANDIDATES == (
|
||||
assert module.FRONTEND_STATIC_DIR_CANDIDATES[:2] == (
|
||||
str(module.FRONTEND_ROOT / "out"),
|
||||
str(module.FRONTEND_ROOT / "dist"),
|
||||
)
|
||||
|
||||
|
||||
def test_config_includes_docker_checkout_static_frontend_candidates(monkeypatch):
|
||||
_set_required_prompt_keys(monkeypatch)
|
||||
|
||||
module = _load_config_module()
|
||||
|
||||
assert "/opt/FastVideo/apps/dreamverse/web/out" in module.FRONTEND_STATIC_DIR_CANDIDATES
|
||||
assert "/opt/FastVideo/apps/dreamverse/web/dist" in module.FRONTEND_STATIC_DIR_CANDIDATES
|
||||
|
||||
|
||||
def test_config_enables_prompt_safety_when_requested(monkeypatch):
|
||||
_set_required_prompt_keys(monkeypatch)
|
||||
monkeypatch.setenv("FASTVIDEO_ENABLE_PROMPT_SAFETY", "true")
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Dreamverse Modal deployment
|
||||
|
||||
This directory contains the Modal wrapper for running the registry-built
|
||||
Dreamverse Docker image on a Modal B200 container. The wrapper pulls a prebuilt Docker image and exposes the dreamverse application running inside.
|
||||
|
||||
|
||||
## 1. Install Modal CLI
|
||||
|
||||
Install and configure the Modal CLI for the target workspace/profile:
|
||||
|
||||
```bash
|
||||
pip install modal
|
||||
modal token set --token-id <token-id> --token-secret <token-secret> --profile=<profile-name>
|
||||
modal profile activate <profile-name>
|
||||
```
|
||||
|
||||
## 2. Create the API key secret
|
||||
|
||||
Dreamverse needs several API keys for prompt-rewriter LLMs and model access in the Modal secret named
|
||||
`dreamverse-api-keys`. Create or replace it with placeholders like this:
|
||||
|
||||
```bash
|
||||
modal secret create dreamverse-api-keys \
|
||||
CEREBRAS_API_KEY=<your-cerebras-key> \
|
||||
GROQ_API_KEY=<your-groq-key> \
|
||||
HF_TOKEN=<your-huggingface-token> \
|
||||
--force
|
||||
```
|
||||
|
||||
## 3. Deploy with Docker image
|
||||
|
||||
`DREAMVERSE_IMAGE` is required at deploy time:
|
||||
|
||||
```bash
|
||||
DREAMVERSE_IMAGE=ghcr.io/<org>/<repo>/dreamverse:<tag> \
|
||||
modal deploy apps/dreamverse/scripts/modal/modal_app.py
|
||||
```
|
||||
|
||||
Use a SHA-specific tag, not `latest`. Use a `dreamverse-backend-cuda12.9.1-sha-*`
|
||||
tag for backend-only deploys, or a `dreamverse-ui-cuda12.9.1-sha-*` tag for an
|
||||
image that includes the static UI served by the backend.
|
||||
|
||||
For local image build details, see
|
||||
`apps/dreamverse/docker/README.md` and `apps/dreamverse/docker/docker_build.sh`.
|
||||
|
||||
|
||||
## 4. Validate the deployment
|
||||
|
||||
Set the URL returned by `modal deploy`, then probe the backend:
|
||||
|
||||
```bash
|
||||
URL=https://<workspace>--dreamverse-b200-serve.modal.run
|
||||
curl -fsS --max-time 300 "$URL/healthz"
|
||||
curl -fsS --max-time 300 "$URL/status"
|
||||
curl -fsS --max-time 300 "$URL/readyz"
|
||||
```
|
||||
|
||||
Check logs with function and container IDs so you can confirm only one container
|
||||
is active:
|
||||
|
||||
```bash
|
||||
modal app logs dreamverse-b200 \
|
||||
--tail 200 \
|
||||
--timestamps \
|
||||
--show-function-id \
|
||||
--show-container-id
|
||||
```
|
||||
|
||||
Useful Modal commands:
|
||||
|
||||
```bash
|
||||
modal app list
|
||||
modal app list --json
|
||||
modal billing report --for today --resolution h
|
||||
modal app stop dreamverse-b200
|
||||
```
|
||||
|
||||
## Runtime details
|
||||
|
||||
### Volumes and assumed locations
|
||||
|
||||
`modal_app.py` creates the named volumes if they do not exist and mounts them at
|
||||
the locations expected by the image:
|
||||
|
||||
| Modal volume | Container path | Environment variable | Purpose |
|
||||
| --- | --- | --- | --- |
|
||||
| `dreamverse-hf-cache` | `/root/.cache/huggingface` | `HF_HOME` | Hugging Face model/cache data |
|
||||
| `dreamverse-state` | `/var/lib/dreamverse` | `FASTVIDEO_DREAMVERSE_HOME` | Dreamverse outputs, session logs, and runtime state |
|
||||
|
||||
### Useful dev env vars
|
||||
|
||||
Most deploys only need the Modal secret and required `DREAMVERSE_IMAGE`.
|
||||
`DREAMVERSE_IMAGE` is read from your local shell at deploy time; the runtime
|
||||
knobs below live in the image or `modal_app.py` unless you intentionally change them:
|
||||
|
||||
- `DREAMVERSE_IMAGE`: deploy a prebuilt SHA-specific image tag without editing `modal_app.py`.
|
||||
- `FASTVIDEO_ENABLE_DEVTOOLS`: enable Dreamverse devtools behavior.
|
||||
- `FASTVIDEO_PROMPT_PROVIDER` / `FASTVIDEO_PROMPT_*_MODEL`: try prompt-rewriter provider or model choices.
|
||||
- `FASTVIDEO_ENABLE_STARTUP_WARMUP`: trade slower startup for a warmer first request.
|
||||
|
||||
### Autoscaling and cost safety
|
||||
|
||||
The current deployed script is capped with `max_containers=1`. If uncapped, concurrent requests could
|
||||
spawn multiple containers, while `max_containers=1` queued requests onto one
|
||||
container instead of multiplying B200 cost.
|
||||
|
||||
### Common gotchas
|
||||
|
||||
- Modal profile/workspace matters: create the secret and deploy with the same active profile.
|
||||
- `modal secret create ... --force` replaces the existing `dreamverse-api-keys` secret.
|
||||
- Use the exact URL printed by `modal deploy`; the placeholder URL above is only an example shape.
|
||||
- B200 cold starts and first model downloads can make `/readyz` slow. Check logs before redeploying.
|
||||
- `modal app stop dreamverse-b200` stops the app; it is not a read-only inspection command.
|
||||
@@ -0,0 +1,49 @@
|
||||
# pyright: reportAttributeAccessIssue=false
|
||||
"""Modal deployment entrypoint for the Dreamverse B200 backend."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import modal
|
||||
|
||||
IMAGE = os.environ.get("DREAMVERSE_IMAGE")
|
||||
if not IMAGE:
|
||||
raise RuntimeError(
|
||||
"DREAMVERSE_IMAGE is required. Set it to a published SHA-specific Dreamverse image, "
|
||||
"for example a dreamverse-backend-cuda12.9.1-sha-* tag or a "
|
||||
"dreamverse-ui-cuda12.9.1-sha-* tag if serving the static UI."
|
||||
)
|
||||
|
||||
image = modal.Image.from_registry(IMAGE)
|
||||
image = image.env({
|
||||
"HF_HOME": "/root/.cache/huggingface",
|
||||
"FASTVIDEO_DREAMVERSE_HOME": "/var/lib/dreamverse",
|
||||
"FASTVIDEO_ENABLE_STARTUP_WARMUP": "0",
|
||||
"FASTVIDEO_GPU_COUNT": "1",
|
||||
"ENABLE_TORCH_COMPILE": "0",
|
||||
"STREAM_MODE": "av_fmp4",
|
||||
})
|
||||
|
||||
app = modal.App("dreamverse-b200")
|
||||
|
||||
hf_cache = modal.Volume.from_name("dreamverse-hf-cache", create_if_missing=True)
|
||||
dreamverse_state = modal.Volume.from_name("dreamverse-state", create_if_missing=True)
|
||||
|
||||
|
||||
@app.function(
|
||||
image=image,
|
||||
gpu="B200",
|
||||
cpu=16,
|
||||
memory=65536,
|
||||
timeout=7200,
|
||||
startup_timeout=4800,
|
||||
max_containers=1,
|
||||
secrets=[modal.Secret.from_name("dreamverse-api-keys")],
|
||||
volumes={
|
||||
"/root/.cache/huggingface": hf_cache,
|
||||
"/var/lib/dreamverse": dreamverse_state,
|
||||
},
|
||||
)
|
||||
@modal.web_server(8009, startup_timeout=4800)
|
||||
def serve():
|
||||
subprocess.Popen(["dreamverse-server", "--host", "0.0.0.0", "--port", "8009"])
|
||||
@@ -6,10 +6,13 @@ const backendHost = process.env.BACKEND_HOST || '127.0.0.1';
|
||||
const backendPort = Number(process.env.BACKEND_PORT) || 8009;
|
||||
const backendUrl = `http://${backendHost}:${backendPort}`;
|
||||
const configDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const staticExport = process.env.NEXT_OUTPUT_EXPORT === '1';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
...(staticExport ? { output: 'export' as const } : {}),
|
||||
...(staticExport ? { images: { unoptimized: true } } : {}),
|
||||
outputFileTracingRoot: path.join(configDir, '..', '..', '..'),
|
||||
async rewrites() {
|
||||
...(staticExport ? {} : { async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/ws',
|
||||
@@ -48,7 +51,7 @@ const nextConfig: NextConfig = {
|
||||
destination: `${backendUrl}/curated-presets/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
} }),
|
||||
webpack: (config) => {
|
||||
config.module.rules.push({
|
||||
test: /\.jsonl$/,
|
||||
|
||||
Reference in New Issue
Block a user