Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
!README.md
!guild
!tests
!scripts/apply_karmadock_patches.py

# Ignore things that might be inside the folders above
**/*~
Expand Down
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ DATABASE_SCHEMA.md
/.databrickscfg
/.data
/.local
/.azure

# Chache
/.pytest_cache
/.ruff_cache
/.cache
/_receptor_pdbqt_cache

# Software
vina
Expand All @@ -73,3 +75,19 @@ openbabel/*
pdbfixer/*
plip/*
KarmaDock

# Local-only / scratch areas
/notebooks/debug/*.py
/notebooks/database
/guild/OLD
/temp_data

# scripts/ — only the load-bearing scripts are tracked.
# run_guild.py is the Makefile entry point; apply_karmadock_patches.py is
# COPY'd by the Dockerfile during base-build; vina_rescore_all.py is a
# published utility. Anything else under scripts/ is treated as a local
# experiment and should not be committed (avoids leaking machine-specific paths).
scripts/*
!scripts/run_guild.py
!scripts/apply_karmadock_patches.py
!scripts/vina_rescore_all.py
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ repos:
hooks:
- id: nbstripout
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.15.11'
rev: 'v0.15.15'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand All @@ -31,6 +31,6 @@ repos:
- id: debug-statements

- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.11.7
rev: 0.11.18
hooks:
- id: uv-lock
43 changes: 42 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,22 @@
# syntax=docker/dockerfile:1

####################################################################################################
# GNINA — pulled as a curated ~1.4 GB bundle (binary + the .so deps it loads
# at runtime). Build the bundle separately with Dockerfile.gnina-bundle and
# tag it (locally or in your own registry), then point GNINA_BUNDLE_IMAGE at
# that tag:
#
# docker build -f Dockerfile.gnina-bundle -t gnina-bundle:local .
# docker build --build-arg GNINA_BUNDLE_IMAGE=gnina-bundle:local -t guild .
#
# We do NOT consume gnina/gnina:latest directly here: pulling the full ~9 GB
# upstream image into the build graph caused disk exhaustion in CI. The bundle
# is a `FROM scratch` image whose only contents are /export/{bin,lib}, so it
# stays off the main build's dependency graph until the final runtime stage.
ARG GNINA_BUNDLE_IMAGE=gnina-bundle:latest
FROM ${GNINA_BUNDLE_IMAGE} AS gnina-source

####################################################################################################
FROM python:3.10-slim-bookworm AS base

ARG APP_NAME="guild"
Expand Down Expand Up @@ -48,6 +66,13 @@ RUN wget https://github.com/ccsb-scripps/AutoDock-Vina/releases/download/v1.2.5/
-O /usr/local/bin/vina && \
chmod a+x /usr/local/bin/vina

# NOTE: GNINA is intentionally not copied into base-build. The gnina-source
# stage pulls a large upstream image and would force every target that
# transitively depends on base-build (including `test`) to materialize it on
# disk. The gnina bundle is only needed at runtime, so the COPY lives in the
# final `docker` stage instead — buildkit then skips gnina-source entirely for
# the `test` target.

# Python build tools
RUN python -m pip install --upgrade pip setuptools wheel

Expand Down Expand Up @@ -150,6 +175,14 @@ ENV LD_LIBRARY_PATH=/app/.venv/lib/python3.10/site-packages/nvidia/cu13/lib:/opt
# Vina
COPY --from=base-build /usr/local/bin/vina /usr/local/bin/vina

# GNINA — curated bundle (binary + isolated lib tree). Pulled directly from
# the gnina-source stage so base-build (and therefore the `test` target) does
# not have to materialize the large upstream image. Invoked with
# LD_LIBRARY_PATH=/opt/gnina/lib so its torch/openbabel/boost don't conflict
# with the venv-managed torch and the system openbabel.
COPY --from=gnina-source /export /opt/gnina
RUN chmod a+x /opt/gnina/bin/gnina

# LocalColabFold
COPY --from=base-build /opt/localcolabfold /opt/localcolabfold
ENV COLABFOLD_BIN="/opt/localcolabfold/.pixi/envs/default/bin"
Expand Down Expand Up @@ -177,4 +210,12 @@ COPY --chown=appuser:appuser guild /app/guild
RUN git clone https://github.com/schrojunzhang/KarmaDock.git /app/KarmaDock && \
git -C /app/KarmaDock checkout 9a35d0cb7caaa1a4d0a61f6ea96821dc1edefa81 && \
git clone https://github.com/gcorso/DiffDock.git /app/DiffDock && \
git -C /app/DiffDock checkout 85c49b60d3e0b0182a59ee43a34a6d7036981284
git -C /app/DiffDock checkout 85c49b60d3e0b0182a59ee43a34a6d7036981284

# KarmaDock at the pinned commit was written for an older rdkit and crashes on
# the rdkit/torch combo we use today (dimension mismatch in ligand_feature.py
# and the GraphTransformer block). Apply the compatibility patches documented
# in the project README. The script is idempotent.
COPY --chown=appuser:appuser scripts/apply_karmadock_patches.py /tmp/apply_karmadock_patches.py
RUN /app/.venv/bin/python /tmp/apply_karmadock_patches.py /app/KarmaDock && \
rm -f /tmp/apply_karmadock_patches.py
55 changes: 55 additions & 0 deletions Dockerfile.gnina-bundle
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# syntax=docker/dockerfile:1
#
# Curated gnina runtime bundle.
#
# Pulls the upstream gnina/gnina image (~9 GB, mostly static archives + MKL
# we don't need at runtime), then extracts the binary plus the ~1.4 GB of
# shared libraries the binary actually loads — boost 1.71, jsoncpp, ICU 66,
# numa, openblas, gfortran, the CUDA 12 runtime libs, and gnina's own
# libtorch / libmolgrid / libopenbabel.so.7 — into ``/export``.
#
# The final stage is ``FROM scratch`` so the published image is just the
# /export tree. Build it once and tag it (locally or in your own registry),
# then point the main Dockerfile's GNINA_BUNDLE_IMAGE build arg at that tag.
# Consumers (see [Dockerfile](Dockerfile) in this repo) do:
#
# docker build -f Dockerfile.gnina-bundle -t gnina-bundle:local .
# docker build --build-arg GNINA_BUNDLE_IMAGE=gnina-bundle:local -t guild .
#
# The main image then does:
#
# ARG GNINA_BUNDLE_IMAGE
# FROM ${GNINA_BUNDLE_IMAGE} AS gnina-source
# ...
# COPY --from=gnina-source /export /opt/gnina
#
# and invokes ``gnina`` with ``LD_LIBRARY_PATH=/opt/gnina/lib`` so the bundle's
# libtorch / libopenbabel don't clash with the main image's CUDA 13 torch
# and system openbabel.
#
# Rebuild this bundle whenever you want to pick up a newer upstream gnina.

FROM gnina/gnina:latest AS extract
RUN set -eux; \
mkdir -p /export/bin /export/lib; \
cp -L /usr/local/bin/gnina /export/bin/; \
cp -L /usr/local/lib/*.so* /export/lib/ 2>/dev/null || true; \
cp -L /usr/lib/x86_64-linux-gnu/libboost_*.so.1.71.0 /export/lib/ 2>/dev/null || true; \
cp -L /usr/lib/x86_64-linux-gnu/libjsoncpp.so* /export/lib/ 2>/dev/null || true; \
# Ubuntu 20.04 ICU + libnuma + openblas — Debian bookworm ships
# newer/different SONAMEs, so gnina would fail to find these at runtime.
cp -L /usr/lib/x86_64-linux-gnu/libicui18n.so.66* /export/lib/ 2>/dev/null || true; \
cp -L /usr/lib/x86_64-linux-gnu/libicuuc.so.66* /export/lib/ 2>/dev/null || true; \
cp -L /usr/lib/x86_64-linux-gnu/libicudata.so.66* /export/lib/ 2>/dev/null || true; \
cp -L /usr/lib/x86_64-linux-gnu/libnuma.so* /export/lib/ 2>/dev/null || true; \
cp -L /usr/lib/x86_64-linux-gnu/libopenblas.so* /export/lib/ 2>/dev/null || true; \
cp -L /usr/lib/x86_64-linux-gnu/libgfortran.so* /export/lib/ 2>/dev/null || true; \
cp -L /usr/lib/x86_64-linux-gnu/libquadmath.so* /export/lib/ 2>/dev/null || true; \
if [ -d /usr/local/cuda-12.0/targets/x86_64-linux/lib ]; then \
cp -RL /usr/local/cuda-12.0/targets/x86_64-linux/lib/*.so* /export/lib/ 2>/dev/null || true; \
fi

####################################################################################################
# Tiny published image: just /export, nothing else. ~1.4 GB on disk.
FROM scratch AS bundle
COPY --from=extract /export /export
77 changes: 68 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
.PHONY : docker-local docker-test dev test run-boltz run-vina run-diffdock run-guild project-init project-setup
.PHONY : docker-local docker-test dev test run-boltz run-vina run-diffdock run-gnina run-plip run-guild project-init project-setup

# Run the tests locally
test:
uv run pytest -v

# Build a local guild image
# Build a local guild image.
# Uses gnina-bundle:local if already present; otherwise builds it first from
# Dockerfile.gnina-bundle (pulls gnina/gnina:latest once, ~9 GB) so the gnina
# docking method works out of the box without needing an external registry.
docker-local:
@if ! docker image inspect gnina-bundle:local >/dev/null 2>&1; then \
echo "gnina-bundle:local not found — building from Dockerfile.gnina-bundle..."; \
docker build -t gnina-bundle:local -f Dockerfile.gnina-bundle .; \
fi
DOCKER_BUILDKIT=1 \
docker build \
--build-arg APP_NAME=guild \
--build-arg GNINA_BUNDLE_IMAGE=gnina-bundle:local \
-t guild:latest \
-f Dockerfile \
--target=docker \
Expand All @@ -34,8 +42,25 @@ BATCH_SIZE ?= 2
HEAD ?= 0
CLEAN ?=
KNOWN_BINDERS ?=
NO_DECOYS ?=
BOX ?=
N_WORKERS ?=
PASSWD_FILE ?= /tmp/guild_passwd

# GPU toggle. Default 1 (enabled). Set USE_GPU= (empty) to drop
# `--gpus all --shm-size=8g` from docker run AND forward `--no-gpu` to
# run_guild.py — required on hosts without a usable GPU (gnina then falls back
# to CPU; Boltz is genuinely GPU-bound and shouldn't be combined with USE_GPU=).
USE_GPU ?= 1
_GPU_FLAGS = $(if $(USE_GPU),--gpus all --shm-size=8g,)
_NO_GPU_FLAG = $(if $(USE_GPU),,--no-gpu)

# Gnina input mode. Empty (default) → omit the flag (pdbqt default applies).
# Set GNINA_INPUT_MODE=sdf to skip OpenBabel PDBQT prep when gnina is the only
# docking method requested (otherwise BulkRun downgrades to pdbqt with a warning).
GNINA_INPUT_MODE ?=
_GNINA_INPUT_MODE_FLAG = $(if $(GNINA_INPUT_MODE),--gnina-input-mode $(GNINA_INPUT_MODE),)

# Internal docker run flags reused across targets.
# Mounts a generated /etc/passwd so pwd.getpwuid() works for the host UID
# (required by PyTorch / boltz inside the container).
Expand All @@ -60,7 +85,10 @@ _HEAD_FLAG = $(if $(filter-out 0,$(HEAD)),--head $(HEAD),)

# Collect all optional flags into one variable for DRY target definitions
_DECOYS_FLAG = $(if $(DECOYS),--decoys $(DECOYS),)
_OPTIONAL_FLAGS = $(_CLEAN_FLAG) $(_KNOWN_BINDERS_FLAG) $(_HEAD_FLAG) $(_DECOYS_FLAG)
_NO_DECOYS_FLAG = $(if $(NO_DECOYS),--no-decoys,)
_BOX_FLAG = $(if $(BOX),--box $(BOX),)
_N_WORKERS_FLAG = $(if $(N_WORKERS),--n-workers $(N_WORKERS),)
_OPTIONAL_FLAGS = $(_CLEAN_FLAG) $(_KNOWN_BINDERS_FLAG) $(_HEAD_FLAG) $(_DECOYS_FLAG) $(_NO_DECOYS_FLAG) $(_BOX_FLAG) $(_N_WORKERS_FLAG) $(_NO_GPU_FLAG) $(_GNINA_INPUT_MODE_FLAG)

# Generate an /etc/passwd that includes the container's original entries plus
# the host user. This fixes pwd.getpwuid() failures for LDAP/SSSD users
Expand All @@ -78,8 +106,7 @@ METHODS ?= boltz
run-boltz: _prepare-passwd
docker run \
$(DOCKER_COMMON) \
--gpus all \
--shm-size=8g \
$(_GPU_FLAGS) \
-e LD_LIBRARY_PATH=/opt/localcolabfold/.pixi/envs/default/lib:/usr/local/lib:/app/.venv/lib/python3.10/site-packages/nvidia/cu13/lib:/app/.venv/lib/python3.10/site-packages/nvidia/cuda_nvrtc/lib:/app/.venv/lib/python3.10/site-packages/nvidia/cudnn/lib:/app/.venv/lib/python3.10/site-packages/nvidia/cublas/lib \
guild:latest \
python $(MASTER_SCRIPT) \
Expand Down Expand Up @@ -117,13 +144,45 @@ run-diffdock: _prepare-passwd
--batch-size $(BATCH_SIZE) \
$(_OPTIONAL_FLAGS)

# Generic target — pass METHODS="boltz vina karmadock diffdock" as needed.
# Picks up GPU flags automatically when any GPU method is present.
# Run gnina docking inside the local Docker image. Uses the GPU by default for
# CNN rescoring; pass USE_GPU= (empty) on no-GPU hosts to drop --gpus and run
# gnina CPU-only. Requires: make docker-local first.
GNINA_METHODS ?= gnina
run-gnina: _prepare-passwd
docker run \
$(DOCKER_COMMON) \
$(_GPU_FLAGS) \
guild:latest \
python $(MASTER_SCRIPT) \
--project $(PROJECT) \
--combinations $(COMBINATIONS) \
--methods $(GNINA_METHODS) \
--batch-size $(BATCH_SIZE) \
$(_OPTIONAL_FLAGS)

# Re-run only the PLIP interactions step over an existing data/<project>/ tree.
# CPU-safe and skips docking + scoring entirely — useful for regenerating
# plip_interactions.tsv when only the PLIP code changed. Requires the same
# COMBINATIONS / PROJECT used by the original run.
run-plip: _prepare-passwd
docker run \
$(DOCKER_COMMON) \
guild:latest \
python $(MASTER_SCRIPT) \
--project $(PROJECT) \
--combinations $(COMBINATIONS) \
--methods $(METHODS) \
--batch-size $(BATCH_SIZE) \
--plip-only \
$(_OPTIONAL_FLAGS)

# Generic target — pass METHODS="boltz vina karmadock diffdock gnina" as needed.
# GPU on by default; pass USE_GPU= (empty) for CPU-only hosts (drops --gpus all
# and forwards --no-gpu). Boltz requires a GPU regardless.
run-guild: _prepare-passwd
docker run \
$(DOCKER_COMMON) \
--gpus all \
--shm-size=8g \
$(_GPU_FLAGS) \
-e LD_LIBRARY_PATH=/opt/localcolabfold/.pixi/envs/default/lib:/usr/local/lib:/app/.venv/lib/python3.10/site-packages/nvidia/cu13/lib:/app/.venv/lib/python3.10/site-packages/nvidia/cuda_nvrtc/lib:/app/.venv/lib/python3.10/site-packages/nvidia/cudnn/lib:/app/.venv/lib/python3.10/site-packages/nvidia/cublas/lib \
guild:latest \
python $(MASTER_SCRIPT) \
Expand Down
Loading
Loading