# syntax=docker/dockerfile:1
#
# coach-sam3 — a self-contained, backend-specific SAM3 LoRA training image.
#
# The venv is BAKED at build time (`uv sync --frozen` for one torch backend), so
# at run time there is no resolution, no network, no `uv sync` — the run is fully
# hermetic and reproducible. Build one image per backend and pin by digest:
#
#   podman build -f images/sam3/Containerfile \
#     --build-arg TORCH_BACKEND=cu128 \
#     --build-arg CUDA_IMAGE=docker.io/nvidia/cuda:12.8.0-cudnn-runtime-ubuntu24.04 \
#     -t git.deepvis.ai/deepvis/coach-sam3:cu128 \
#     images/sam3                              # <- CONTEXT = this directory
#
# Everything the build needs is VENDORED under this directory (the build context):
#   sam3-python/   — the SAM3 training project (a vendored snapshot; see
#                    sam3-python/VENDORED.md for provenance + how to refresh)
#   train_coco.py  — coach's own COCO-dataset entrypoint
# The build is self-contained — it never reaches outside the context. Structure
# follows the uv Docker guide (https://docs.astral.sh/uv/guides/integration/docker/):
# copy the pinned uv binary, cache-mounted syncs with UV_LINK_MODE=copy, and a
# deps layer split from the project layer for caching.

ARG CUDA_IMAGE=docker.io/nvidia/cuda:12.8.0-cudnn-runtime-ubuntu24.04

FROM ${CUDA_IMAGE} AS base
ARG TORCH_BACKEND=cu128
ENV DEBIAN_FRONTEND=noninteractive

# git: `sam3` is a git dependency (resolved during sync). gcc/g++: torch's Triton
# JIT compiles GPU kernel launchers at RUN time, so a C/C++ compiler must survive
# into the final image.
RUN apt-get update && apt-get install -y --no-install-recommends \
      ca-certificates git gcc g++ \
 && rm -rf /var/lib/apt/lists/*

# uv: the pinned static binary from the distroless image (the recommended method).
COPY --from=ghcr.io/astral-sh/uv:0.9.0 /uv /uvx /bin/

# Bake the venv at a fixed path and let uv manage the (3.11) interpreter — the
# CUDA base ships 3.12, but this project requires >=3.11,<3.12, so a uv-managed
# 3.11 is required and must persist in the image (the venv points at it). copy
# link-mode lets the venv own its files across the cache mount; compile bytecode
# for faster startup.
ENV UV_PROJECT_ENVIRONMENT=/opt/venv \
    UV_PYTHON_INSTALL_DIR=/opt/uv-python \
    UV_LINK_MODE=copy \
    UV_COMPILE_BYTECODE=1

WORKDIR /app/sam3-python

# ── Layer 1: dependencies only (changes rarely) ──
# Bind-mount just the lock + manifest, so editing the project doesn't re-resolve
# torch. --frozen installs exactly the locked graph; --extra selects the torch
# backend; --no-install-project defers the project itself.
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=sam3-python/uv.lock,target=uv.lock \
    --mount=type=bind,source=sam3-python/pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-dev --no-install-project --extra "${TORCH_BACKEND}"

# ── Layer 2: the project (changes more often) ──
# README.md is needed (the project's wheel build reads it). vendor/sam3_hf is
# needed at RUN time (the bridge + export read the vendored HF metadata).
COPY sam3-python/pyproject.toml sam3-python/uv.lock sam3-python/README.md ./
COPY sam3-python/illuiners_sam3_train ./illuiners_sam3_train
COPY sam3-python/vendor ./vendor

RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev --extra "${TORCH_BACKEND}"

# coach's own entrypoint (the COCO-dataset trainer). It reuses the baked
# illuiners_sam3_train package but reads a mounted COCO dataset instead of a
# database — so this image knows no schema.
COPY train_coco.py /app/train_coco.py

# Optionally bake a base checkpoint. The `base` build context defaults to an
# empty dir (images/sam3/no-base), so a plain build bakes nothing and runs must
# pass an explicit checkpoint. Build with `--build-context base=<dir with
# base.pt>` (the justfile wires COACH_SAM3_BASE) to bake one at /opt/base/base.pt,
# which `Sam3Params { checkpoint: None }` then uses. NOTE: the SAM3 base is
# license-gated — only bake into images for a private registry.
COPY --from=base . /opt/base/

# The image owns its training env (coach only sets mounts + config): run offline
# so the fbr→HF bridge uses the vendored metadata and never hits the gated repo
# (edge boxes have no HF auth); expandable segments avoids SAM3's spurious OOM.
ENV HF_HUB_OFFLINE=1 \
    TRANSFORMERS_OFFLINE=1 \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True

# Entrypoint: run coach's train_coco.py on the baked interpreter. coach appends
# the file args (--config/--pt-out/--onnx-out) and runs it with cwd=/work.
RUN printf '%s\n' \
      '#!/usr/bin/env bash' \
      'set -euo pipefail' \
      'exec /opt/venv/bin/python /app/train_coco.py "$@"' \
      > /usr/local/bin/coach-sam3-train \
 && chmod +x /usr/local/bin/coach-sam3-train

WORKDIR /work
ENTRYPOINT ["coach-sam3-train"]
