Skip to content

Commit 7f24a20

Browse files
ryan-williamsclaude
andcommitted
Add Modal GPU benchmark workflow and script
- `modal/benchmark.py`: Configurable benchmark using `electrai-data` Volume (dataset_4, 2,885 samples). Filters by file size (≤100MB for L4), subsamples, uses temp data root with symlinks. Gradient checkpointing enabled. Handles `RhoData` trailing-newline bug. - `.github/workflows/gpu-benchmark-modal.yml`: GHA workflow wrapping `modal run modal/benchmark.py` with configurable inputs. - Default config (8ch/2blk) fits on L4; use `--gpu A100 --channels 32 --residual-blocks 16` for production-size benchmarks. Verified: 50 samples, 5 epochs, L4 → green. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 960a3db commit 7f24a20

2 files changed

Lines changed: 122 additions & 21 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: GPU Benchmark (Modal)
2+
3+
on:
4+
push:
5+
branches: [modal-benchmark]
6+
workflow_dispatch:
7+
inputs:
8+
gpu:
9+
description: 'GPU type (L4, A10G, A100, H100)'
10+
default: 'L4'
11+
type: string
12+
epochs:
13+
description: 'Number of training epochs'
14+
default: '5'
15+
type: string
16+
channels:
17+
description: 'Number of model channels (8=tiny/L4, 32=prod/A100, 64=large)'
18+
default: '8'
19+
type: string
20+
residual_blocks:
21+
description: 'Number of residual blocks (2=tiny/L4, 16=prod/A100)'
22+
default: '2'
23+
type: string
24+
samples:
25+
description: 'Number of samples from Volume (0=all eligible)'
26+
default: '50'
27+
type: string
28+
max_file_size:
29+
description: 'Skip samples larger than N MB (0=no limit, 100=safe for L4)'
30+
default: '100'
31+
type: string
32+
wandb_project:
33+
description: 'WandB project name'
34+
default: 'elf-net-ci'
35+
type: string
36+
37+
jobs:
38+
benchmark:
39+
runs-on: ubuntu-latest
40+
env:
41+
MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }}
42+
MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }}
43+
steps:
44+
- uses: actions/checkout@v4
45+
- uses: astral-sh/setup-uv@v5
46+
- uses: actions/setup-python@v5
47+
with:
48+
python-version: "3.12"
49+
50+
- name: Install Modal
51+
run: uv pip install --system modal
52+
53+
- name: Run benchmark
54+
env:
55+
GPU: ${{ inputs.gpu || 'L4' }}
56+
EPOCHS: ${{ inputs.epochs || '5' }}
57+
CHANNELS: ${{ inputs.channels || '8' }}
58+
RESIDUAL_BLOCKS: ${{ inputs.residual_blocks || '2' }}
59+
SAMPLES: ${{ inputs.samples || '50' }}
60+
MAX_FILE_SIZE: ${{ inputs.max_file_size || '100' }}
61+
WANDB_PROJECT: ${{ inputs.wandb_project || 'elf-net-ci' }}
62+
run: |
63+
modal run modal/benchmark.py \
64+
--gpu "$GPU" \
65+
--epochs "$EPOCHS" \
66+
--channels "$CHANNELS" \
67+
--residual-blocks "$RESIDUAL_BLOCKS" \
68+
--samples "$SAMPLES" \
69+
--max-file-size "$MAX_FILE_SIZE" \
70+
--wandb-project "$WANDB_PROJECT"

modal/benchmark.py

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
electrai-data Volume, logs metrics to WandB, and reports wall-clock time.
55
66
Usage:
7-
# Default: 50 samples, 5 epochs, 32 channels, L4
7+
# Default: 50 samples, 5 epochs, 8 channels, 2 blocks, L4
88
modal run modal/benchmark.py
99
10-
# Production model on A100
11-
modal run modal/benchmark.py --gpu A100 --channels 64 --residual-blocks 16
10+
# Production model on A100 (32ch/16blk needs >=40GB VRAM)
11+
modal run modal/benchmark.py --gpu A100 --channels 32 --residual-blocks 16
1212
1313
# All 2,885 Volume samples
1414
modal run modal/benchmark.py --samples 0
@@ -59,10 +59,10 @@
5959
)
6060
def run_benchmark(
6161
epochs: int = 5,
62-
channels: int = 32,
63-
residual_blocks: int = 16,
62+
channels: int = 8,
63+
residual_blocks: int = 2,
6464
samples: int = 50,
65-
max_file_size: float = 25,
65+
max_file_size: float = 100,
6666
seed: int = 42,
6767
wandb_project: str = "elf-net-ci",
6868
gpu_type: str = "L4",
@@ -77,22 +77,52 @@ def run_benchmark(
7777
sys.path.insert(0, "/root/electrai/scripts")
7878
from e2e_train import run_training
7979

80-
# Subsample from volume filelist if requested
80+
# Filter and subsample from volume filelist
8181
filelist = Path(DATA_ROOT) / "mp_filelist.txt"
82-
all_ids = filelist.read_text().strip().splitlines()
83-
if samples > 0 and samples < len(all_ids):
82+
all_ids = [s for s in filelist.read_text().strip().splitlines() if s]
83+
84+
# Filter by file size first (avoid OOM on large grids)
85+
if max_file_size > 0:
86+
max_bytes = int(max_file_size * 1024 * 1024)
87+
eligible = [
88+
sid
89+
for sid in all_ids
90+
if (Path(DATA_ROOT) / "data" / f"{sid}.CHGCAR").stat().st_size <= max_bytes
91+
]
92+
log.info(
93+
"File size filter: %d/%d eligible (<=%.0fMB)",
94+
len(eligible),
95+
len(all_ids),
96+
max_file_size,
97+
)
98+
else:
99+
eligible = all_ids
100+
101+
# Subsample from eligible
102+
if 0 < samples < len(eligible):
84103
import random
85104

86105
random.seed(seed)
87-
subset = sorted(random.sample(all_ids, samples))
88-
sub_filelist = Path(DATA_ROOT) / "mp_filelist_benchmark.txt"
89-
sub_filelist.write_text("\n".join(subset) + "\n")
90-
data_root = DATA_ROOT
91-
log.info("Subsampled %d/%d samples", samples, len(all_ids))
106+
subset = sorted(random.sample(eligible, samples))
107+
log.info("Subsampled %d/%d eligible samples", samples, len(eligible))
92108
else:
93-
data_root = DATA_ROOT
94-
samples = len(all_ids)
95-
log.info("Using all %d samples", samples)
109+
subset = eligible
110+
samples = len(subset)
111+
log.info("Using all %d eligible samples", samples)
112+
113+
if not subset:
114+
raise ValueError(
115+
f"No eligible samples (total={len(all_ids)}, max_file_size={max_file_size}MB)"
116+
)
117+
118+
# Create temp data root with symlinks + clean filelist (no trailing newline)
119+
data_root = "/tmp/benchmark_data"
120+
Path(data_root).mkdir(parents=True, exist_ok=True)
121+
for subdir in ["data", "label"]:
122+
link = Path(data_root) / subdir
123+
if not link.exists():
124+
link.symlink_to(Path(DATA_ROOT) / subdir)
125+
Path(data_root, "mp_filelist.txt").write_text("\n".join(subset))
96126

97127
log.info(
98128
"Benchmark: gpu=%s, epochs=%d, channels=%d, blocks=%d, samples=%d",
@@ -109,8 +139,9 @@ def run_benchmark(
109139
epochs=epochs,
110140
seed=seed,
111141
gpu=True,
142+
gradient_checkpoint=True, # save VRAM for large grids
112143
data_root=data_root,
113-
max_file_size=max_file_size,
144+
max_file_size=0, # already filtered above
114145
wandb_project=wandb_project,
115146
verbose=True,
116147
)
@@ -127,10 +158,10 @@ def run_benchmark(
127158
def main(
128159
gpu: str = "L4",
129160
epochs: int = 5,
130-
channels: int = 32,
131-
residual_blocks: int = 16,
161+
channels: int = 8,
162+
residual_blocks: int = 2,
132163
samples: int = 50,
133-
max_file_size: float = 25,
164+
max_file_size: float = 100,
134165
seed: int = 42,
135166
wandb_project: str = "elf-net-ci",
136167
):

0 commit comments

Comments
 (0)