Skip to content

Commit 1e3b718

Browse files
Eugene Vinitskyclaude
authored andcommitted
scripts/modal: nightly cron on Modal (stacked on Greene PR)
Adds a Modal app that runs the same nightly fan-out as the Greene launchers, on Modal infrastructure with a built-in cron schedule. - scripts/modal/Dockerfile — CUDA 12.8.1 / Ubuntu 24.04 / Python 3.12 / uv / torch cu128. Multi-arch CUDA (sm_75 / sm_80 / sm_89 / sm_90) so one image runs on T4 / A100 / L4/L40S / H100/H200 without rebuild. - scripts/modal/modal_app.py — Modal app with a per-seed train() on 1x A100-80GB and a nightly() cron at 04:00 UTC that fans out to 6 parallel runs (3 seeds x {single, multi}). Routes to wandb projects nightly-single / nightly-multi, group = today's UTC date. - scripts/modal/README.md — modal token new -> modal secret create wandb-emerge -> modal deploy workflow. Smoke-validated end-to-end on A100-80GB: wandb: https://wandb.ai/emerge_/nightly-modal/runs/n8a85v5z (~200K SPS on single-agent, all 6 metrics pipelines confirmed.) Depends on the Greene PR for the cluster_configs/*.yaml files that the Modal app loads at runtime. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 30ce34a commit 1e3b718

3 files changed

Lines changed: 414 additions & 0 deletions

File tree

scripts/modal/Dockerfile

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Base image for PufferDrive nightly training on Modal.
2+
#
3+
# Matches the NYU Greene Singularity sif as closely as possible:
4+
# - CUDA 12.8.1 + cuDNN runtime
5+
# - Ubuntu 24.04
6+
# - Python 3.12
7+
#
8+
# The actual repo (and the built .so files) are baked into the image at deploy
9+
# time by scripts/modal/modal_app.py via copy_local_dir + run_commands. This
10+
# Dockerfile only handles the slow, version-stable layer: system libs, Python,
11+
# torch.
12+
#
13+
# Build context is the repo root (not scripts/modal/). Don't reference
14+
# repo files here — modal_app.py copies them in afterward so changes don't
15+
# invalidate the slow base.
16+
17+
FROM nvidia/cuda:12.8.1-cudnn-devel-ubuntu24.04
18+
19+
ENV DEBIAN_FRONTEND=noninteractive \
20+
PYTHONDONTWRITEBYTECODE=1 \
21+
PYTHONUNBUFFERED=1 \
22+
PYTHONNOUSERSITE=1 \
23+
PIP_NO_CACHE_DIR=1 \
24+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
25+
# Multi-arch so the same image runs on T4 (sm_75), A100 (sm_80), L4/L40S
26+
# (sm_89), and H100/H200 (sm_90). Build is ~3-4x slower than single-arch
27+
# but lets us swap Modal gpu= strings without rebuilding the image.
28+
TORCH_CUDA_ARCH_LIST="7.5;8.0;8.9;9.0"
29+
30+
RUN apt-get update && apt-get install -y --no-install-recommends \
31+
build-essential \
32+
clang \
33+
ccache \
34+
ca-certificates \
35+
curl \
36+
git \
37+
ninja-build \
38+
pkg-config \
39+
# OpenMP for the drive C extension
40+
libomp-dev \
41+
# EGL + GL for the headless render path in drive.h (eval.validation_gigaflow)
42+
libegl1 \
43+
libegl-dev \
44+
libgles2-mesa-dev \
45+
libgl1-mesa-dev \
46+
libglvnd-dev \
47+
# Raylib's X11 deps — used by the GLFW fallback when EGL isn't picked
48+
libx11-dev \
49+
libxcursor-dev \
50+
libxinerama-dev \
51+
libxi-dev \
52+
libxrandr-dev \
53+
# Headless display server for the GLFW fallback path
54+
xvfb \
55+
# ffmpeg for validation_gigaflow video encoding
56+
ffmpeg \
57+
# Python toolchain
58+
python3.12 \
59+
python3.12-dev \
60+
python3.12-venv \
61+
python3-pip \
62+
&& rm -rf /var/lib/apt/lists/*
63+
64+
# Install uv — faster, deterministic resolver, replaces pip + venv + the
65+
# constraints-file dance. Modal disallows the Dockerfile ADD directive, so
66+
# fetch via curl.
67+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
68+
ENV PATH="/root/.local/bin:${PATH}"
69+
70+
# Project venv at /opt/venv (kept off the overlay/image filesystem in
71+
# spirit — symbolic of the cluster's /scratch/$USER/venvs layout).
72+
RUN uv venv /opt/venv --python 3.12
73+
ENV PATH="/opt/venv/bin:${PATH}" \
74+
VIRTUAL_ENV="/opt/venv"
75+
76+
RUN uv pip install setuptools wheel
77+
78+
# torch from pypi is built against CUDA 13; pull the cu128 wheel instead so it
79+
# matches the base image's CUDA toolkit. setup.py's CUDAExtension path checks
80+
# this and refuses to build if torch and host CUDA disagree.
81+
RUN uv pip install --index-url https://download.pytorch.org/whl/cu128 torch
82+
83+
# Pin numpy<2 and pandas<2.2 here so the later `uv pip install -e .` resolve
84+
# can't drag numpy 2 in via pandas 3 and break the C extension's numpy 1 ABI
85+
# (NPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION in setup.py). uv's resolver
86+
# respects these caps when computing the install_requires solution, unlike
87+
# pip which would gladly upgrade pre-installed pandas during -e .
88+
RUN uv pip install "numpy<2" "pandas<2.2"
89+
90+
WORKDIR /workspace

scripts/modal/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Modal nightly training
2+
3+
Nightly cron that launches PufferDrive training on Modal — 3 seeds of
4+
`single_agent_speed_run.yaml` plus 3 seeds of `nightly_best.yaml`, all on
5+
1× A100-80GB in parallel. Single-agent runs log to wandb project
6+
`nightly-single`; multi-agent to `nightly-multi`. In each project the
7+
`wandb_group` is today's UTC date so a night's 3 seeds cluster together in
8+
the UI. All under the `emerge_` org.
9+
10+
## Files
11+
12+
| File | Purpose |
13+
|---|---|
14+
| `Dockerfile` | CUDA 12.8.1 + cuDNN + Ubuntu 24.04 base, system libs, Python 3.12, torch. Slow layer — rarely rebuilt. |
15+
| `modal_app.py` | Modal app — bakes the repo + builds C extensions on top of the Dockerfile, defines the per-seed `train` function and the `nightly` cron entrypoint. |
16+
17+
The training yamls themselves live in `scripts/cluster_configs/` and are shared
18+
with the Greene-side launcher.
19+
20+
## One-time setup
21+
22+
```bash
23+
# Install + auth Modal CLI (host machine)
24+
pip install modal
25+
modal token new
26+
27+
# Create the wandb secret. Paste the API key from https://wandb.ai/authorize.
28+
modal secret create wandb-emerge WANDB_API_KEY=<key>
29+
```
30+
31+
## Deploy the nightly cron
32+
33+
```bash
34+
modal deploy scripts/modal/modal_app.py
35+
```
36+
37+
Modal hashes the source — re-run after any code change to rebuild the image
38+
and update the deployed cron. The first deploy builds the Dockerfile (~5 min);
39+
subsequent deploys only rebuild the `pip install -e .` layer when repo files
40+
change (~1 min).
41+
42+
The cron is `0 4 * * *` (04:00 UTC daily). Adjust the `modal.Cron(...)` arg in
43+
`modal_app.py` to change the wall-clock time.
44+
45+
## Trigger runs manually
46+
47+
```bash
48+
# Run the full 6-job fan-out now (without waiting for cron):
49+
modal run scripts/modal/modal_app.py::nightly
50+
51+
# Run a single seed/config (useful for smoke tests):
52+
modal run scripts/modal/modal_app.py::train \
53+
--yaml-path scripts/cluster_configs/single_agent_speed_run.yaml \
54+
--seed 0 --run-name local_smoke --wandb-group smoke
55+
```
56+
57+
## Inspect / cancel
58+
59+
```bash
60+
modal app list # show deployed apps
61+
modal app logs pufferdrive-nightly # tail logs (running app)
62+
modal app stop pufferdrive-nightly # remove the cron
63+
```
64+
65+
Per-container logs (one per training run) appear in the Modal dashboard
66+
under the `pufferdrive-nightly` app.
67+
68+
## Cost note
69+
70+
A100-80GB on Modal is ~$3.20/h. A 12 h training run × 6 jobs = ~$230/night.
71+
Bring down by lowering `train.total_timesteps` in the yamls, dropping the
72+
`--gpu` to `A100` (40GB), or limiting to fewer seeds.

0 commit comments

Comments
 (0)