diff --git a/.github/workflows/modal-runtime-image.yml b/.github/workflows/modal-runtime-image.yml deleted file mode 100644 index c2c1950dc..000000000 --- a/.github/workflows/modal-runtime-image.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Publish Modal runtime image - -on: - workflow_dispatch: - push: - branches: [master] - tags: ["v*"] - paths: - - "docker/modal-runtime.Dockerfile" - - "pyproject.toml" - - "uv.lock" - - ".github/workflows/modal-runtime-image.yml" - -permissions: - contents: read - packages: write - -concurrency: - group: modal-runtime-${{ github.ref }} - cancel-in-progress: true - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: docker/setup-buildx-action@v3 - - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - id: metadata - uses: docker/metadata-action@v5 - with: - images: ghcr.io/borglab/gtsfm-modal-runtime - tags: | - type=ref,event=tag - type=raw,value=latest,enable={{is_default_branch}} - type=sha,prefix=sha- - - - uses: docker/build-push-action@v6 - with: - context: . - file: docker/modal-runtime.Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/README.md b/README.md index 05a593f85..d616072c8 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,17 @@ To submit work to a remote GPU machine, start its workspace with an API key: GTSFM_API_KEY=your-secret gtsfm run --host 0.0.0.0 --no-browser ``` -Choose **Remote VM** in the workspace, then select an API or SSH connection. Modal is the first hosted API option: enter its token ID and token secret separately, or paste the full `modal token set --token-id … --token-secret …` command into either field and the workspace will split it automatically. If the account does not already have a GTSFM deployment, click **Set up & deploy Modal workspace**. GTSFM first pulls the versioned `ghcr.io/borglab/gtsfm-modal-runtime` image, then deploys the protected `gtsfm-studio` web app and persistent `gtsfm-studio-data` volume, discovers its URL, and verifies the selected GPU. If the prebuilt image is unavailable, setup automatically falls back to building the dependency image in Modal. +Choose **Remote VM** in the workspace, then select an API or SSH connection. Modal is the first hosted API option: enter its token ID and token secret separately, or paste the full `modal token set --token-id … --token-secret …` command into either field and the workspace will split it automatically. If the account does not already have a GTSFM deployment, click **Set up & deploy Modal workspace**. GTSFM pulls the configured prebuilt Docker Hub runtime, then deploys the protected `gtsfm-studio` web app and persistent `gtsfm-studio-data` volume, discovers its URL, and verifies the selected GPU. If no prebuilt image is configured, setup builds the dependency image in Modal. -The prebuilt runtime contains the stable CUDA and Python dependency layers but no repository source or model weights. It is published by [the Modal runtime image workflow](.github/workflows/modal-runtime-image.yml) whenever runtime dependencies change on `master`, and for release tags. Normal GTSFM and UI changes are mounted after the image layer and therefore do not reinstall PyTorch or CUDA libraries. After its first publication, a Borglab package administrator must make `gtsfm-modal-runtime` public in GitHub Packages so Modal can pull it anonymously. To test a different runtime image, start the local workspace with `GTSFM_MODAL_RUNTIME_IMAGE=registry/image:tag gtsfm run`; set the variable to an empty string to force Modal's source-build fallback. +The prebuilt runtime contains the stable CUDA and Python dependency layers but no repository source or model weights. Normal GTSFM and UI changes are mounted after the image layer and therefore do not reinstall PyTorch or CUDA libraries. Publish it manually to a public Docker Hub repository using Modal's fast-pull eStargz format: + +```bash +docker login +./docker/publish-modal-runtime.sh su071301/gtsfm-modal-runtime firstclass +GTSFM_MODAL_RUNTIME_IMAGE=docker.io/su071301/gtsfm-modal-runtime:firstclass gtsfm run +``` + +The first command prompts for a Docker Hub username and access token. The publisher builds `linux/amd64`, pushes directly to Docker Hub, and does not require GitHub Actions. Set `GTSFM_MODAL_RUNTIME_IMAGE` to an empty string to force Modal's source-build fallback. GTSFM sample datasets are downloaded directly by the remote workspace. User-selected folders are archived and streamed to the protected workspace before the job starts. Live preview splats and the final `.ply` result are mirrored back to the local viewer. Modal account credentials and the derived workspace API key are kept in memory and are not included in public job state. Lambda Cloud, RunPod, Vast.ai, and AWS EC2 are listed as coming soon. diff --git a/docker/modal-runtime.Dockerfile b/docker/modal-runtime.Dockerfile index de0516d07..63c14562a 100644 --- a/docker/modal-runtime.Dockerfile +++ b/docker/modal-runtime.Dockerfile @@ -28,33 +28,31 @@ RUN apt-get update \ libglib2.0-0 \ libgomp1 \ libx11-6 \ + libzstd-dev \ ninja-build \ + zlib1g-dev \ && rm -rf /var/lib/apt/lists/* RUN curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | sh \ && uv python install ${PYTHON_VERSION} \ - && ln -sf "$(uv python find ${PYTHON_VERSION})" /usr/local/bin/python \ - && ln -sf "$(uv python find ${PYTHON_VERSION})" /usr/local/bin/python3 + && uv venv --python ${PYTHON_VERSION} /opt/gtsfm-venv + +ENV VIRTUAL_ENV=/opt/gtsfm-venv \ + UV_PROJECT_ENVIRONMENT=/opt/gtsfm-venv \ + PATH=/opt/gtsfm-venv/bin:/root/.local/bin:${PATH} WORKDIR /opt/gtsfm-runtime COPY pyproject.toml uv.lock ./ -RUN uv export \ +RUN uv sync \ --frozen \ --no-dev \ - --no-emit-project \ - --no-header \ - --output-file /tmp/gtsfm-runtime-requirements.txt \ - && uv pip install \ - --python /usr/local/bin/python \ - --requirements /tmp/gtsfm-runtime-requirements.txt \ - && rm /tmp/gtsfm-runtime-requirements.txt \ + --no-install-project \ && python -c "import fastapi, gsplat, gtsam, torch; print(torch.__version__)" -ENV HF_HOME=/workspace/cache/huggingface \ - TORCH_HOME=/workspace/cache/torch \ - TORCH_EXTENSIONS_DIR=/workspace/cache/torch-extensions \ - XDG_CACHE_HOME=/workspace/cache \ - PYTHONPATH=/root +# Volume-backed cache locations are assigned by visualization/modal_app.py at +# container startup. They must not be present while Modal extends this image, +# because build tools can otherwise populate the future Volume mount target. +ENV PYTHONPATH=/root WORKDIR /root diff --git a/docker/publish-modal-runtime.sh b/docker/publish-modal-runtime.sh new file mode 100755 index 000000000..fb5ceddc8 --- /dev/null +++ b/docker/publish-modal-runtime.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 1 || $# -gt 2 ]]; then + echo "Usage: $0 DOCKERHUB_NAMESPACE/gtsfm-modal-runtime [TAG]" >&2 + exit 2 +fi + +repository="${1#docker.io/}" +tag="${2:-firstclass}" +image="docker.io/${repository}:${tag}" + +docker info >/dev/null +docker buildx build \ + --platform linux/amd64 \ + --file docker/modal-runtime.Dockerfile \ + --tag "${image}" \ + --output type=registry,compression=estargz,force-compression=true,oci-mediatypes=true \ + . + +echo "Published ${image}" diff --git a/visualization/modal_deployment.py b/visualization/modal_deployment.py index 775eeb085..adde4decc 100644 --- a/visualization/modal_deployment.py +++ b/visualization/modal_deployment.py @@ -28,7 +28,7 @@ "H200", "B200", } -DEFAULT_MODAL_RUNTIME_IMAGE = "ghcr.io/borglab/gtsfm-modal-runtime:latest" +DEFAULT_MODAL_RUNTIME_IMAGE = "docker.io/su071301/gtsfm-modal-runtime:firstclass" def modal_remote_api_key(token_id: str, token_secret: str) -> str: