Files
2026-07-13 13:26:28 +08:00

219 lines
9.7 KiB
Docker

# ML4T Unified Environment — Multi-Architecture
# Single Dockerfile builds for both linux/amd64 (x86) and linux/arm64 (Apple Silicon).
#
# Architecture strategy (via Docker BuildKit TARGETARCH):
# amd64: CUDA 12.8 PyTorch wheels (~5.4GB) + signatory/esig + LightGBM CUDA
# arm64: CPU-only PyTorch (~200MB), skip signatory/esig (no ARM wheels)
#
# Usage:
# docker compose up ml4t # Start Jupyter Lab (CPU)
# docker compose up ml4t-gpu # Start with GPU passthrough
# docker compose run --rm ml4t ml4t-status # Check environment
#
# See envs/README.md for detailed setup instructions
FROM ubuntu:24.04
# Prevent interactive prompts during package installation
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=UTC
# System tooling only — Python 3.14 comes from uv (python-build-standalone),
# not from the OS. This avoids the deadsnakes PPA's intermittent network
# flakiness on GitHub runners.
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential ca-certificates curl git libhdf5-dev pkg-config wget \
&& rm -rf /var/lib/apt/lists/*
# Install TA-Lib from source
RUN curl -L https://github.com/ta-lib/ta-lib/releases/download/v0.6.4/ta-lib-0.6.4-src.tar.gz | tar xz \
&& cd ta-lib-0.6.4 \
&& ./configure --prefix=/usr \
&& make -j$(nproc) \
&& make install \
&& cd .. && rm -rf ta-lib-0.6.4
# Install uv for fast package management
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# Create non-root user
RUN useradd -m -s /bin/bash ml4t
# Install Python 3.14 via uv (python-build-standalone) and create venv at
# /opt/ml4t. /opt/ml4t is on PATH later, so /opt/ml4t/bin/python becomes the
# default `python` for downstream RUN steps. No system Python needed.
ENV UV_PYTHON_INSTALL_DIR=/opt/uv-python
RUN uv python install 3.14 \
&& uv venv --seed --python 3.14 /opt/ml4t \
&& /opt/ml4t/bin/python --version \
&& /opt/ml4t/bin/pip --version
ARG TARGETARCH
# Install PyTorch FIRST (rarely changes - maximize cache reuse)
# amd64: CUDA 12.8 wheels (~5.4GB) bundle CUDA runtime, work on CPU-only machines too
# arm64: CPU-only wheels (~200MB) — no NVIDIA GPU on Apple Silicon
RUN /opt/ml4t/bin/pip install --no-cache-dir pip setuptools wheel && \
if [ "$TARGETARCH" = "arm64" ]; then \
/opt/ml4t/bin/pip install --no-cache-dir torch torchvision torchaudio; \
else \
/opt/ml4t/bin/pip install --no-cache-dir \
torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128; \
fi
RUN /opt/ml4t/bin/pip freeze | grep -E '^(torch|torchaudio|torchvision)==' > /tmp/ml4t-constraints.txt
# Install ml4t libraries from PyPI
# --pre allows pre-release versions
RUN /opt/ml4t/bin/pip install --no-cache-dir --pre \
"ml4t-data>=0.1.0b15" \
"ml4t-diagnostic>=0.1.0b19" \
"ml4t-engineer>=0.1.0b8" \
"ml4t-live>=0.1.0b3" \
"ml4t-backtest>=0.1.0b16" \
"ml4t-models>=0.1.0a1"
# amd64 only: Install CUDA toolkit (nvcc + headers) for building LightGBM with CUDA
# Minimal install: nvcc-12.6 + runtime dev headers (12.4 not packaged for Ubuntu 24.04)
# CUDA is backward-compatible: 12.6 nvcc compiles for 12.4+ runtime
RUN if [ "$TARGETARCH" = "amd64" ]; then \
apt-get update && \
apt-get install -y --no-install-recommends gnupg && \
wget -q https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb && \
dpkg -i cuda-keyring_1.1-1_all.deb && rm cuda-keyring_1.1-1_all.deb && \
apt-get update && \
apt-get install -y --no-install-recommends cuda-nvcc-12-6 cuda-cudart-dev-12-6 && \
rm -rf /var/lib/apt/lists/*; \
fi
ENV PATH="${PATH:+${PATH}:}/usr/local/cuda-12.6/bin"
# NOW copy pyproject.toml (changes here only rebuild deps, not PyTorch)
# Uses root pyproject.toml — single source of truth for all dependencies.
WORKDIR /build
COPY pyproject.toml /build/
# Remove packages that need special handling from pyproject copy:
# - lightgbm: installed separately with CUDA build on amd64
# - torch: already installed from CUDA wheels above
# Packages stripped here are installed separately or via special build steps
RUN sed -i '/"lightgbm/d; /"torch>=/d' /build/pyproject.toml
# signatory/esig/gensim: moved to py312 image (Python 3.12, x86 only)
# Use: docker compose --profile py312 run --rm py312 python ...
# Install remaining dependencies from pyproject.toml without replacing the
# preinstalled PyTorch stack needed for CUDA/signatory compatibility.
# Use uv for resolution (handles packages that mark Requires-Python <3.14
# but actually work fine, like dowhy and granite-tsfm).
# --override lets uv resolve freely while keeping torch CUDA pins.
# Force cvxpy>=1.5 for numpy 2.x C API compat (riskfolio-lib pins 1.4).
# --prerelease=if-necessary-or-explicit mirrors the [tool.uv] policy used by
# uv.lock, so the image resolves the same STABLE versions we test against rather
# than gratuitous prereleases (e.g. scipy 1.18rc, pydantic 2.14a, matplotlib 3.11rc).
RUN echo 'cvxpy>=1.5' >> /tmp/ml4t-constraints.txt && \
uv pip install --python /opt/ml4t/bin/python --no-cache \
--prerelease=if-necessary-or-explicit \
--override /tmp/ml4t-constraints.txt /build
# LightGBM: two deterministic paths, no fallbacks within a platform
# - amd64: CUDA build from source. Mandatory. Build fails if CUDA doesn't work.
# - arm64 (macOS): pip wheel. No GPU available, no pretense of one.
#
# amd64: build from source with CUDA, verify the CUDA codepath compiled
# NOTE: Full CUDA verification (lgb.train device=cuda) requires a GPU at runtime.
# At build time we only verify the library imports and reports CUDA support.
RUN if [ "$TARGETARCH" = "amd64" ]; then \
set -e && \
apt-get update && apt-get install -y --no-install-recommends \
libnccl-dev libnccl2 gcc-12 g++-12 && \
rm -rf /var/lib/apt/lists/* && \
CC=/usr/bin/gcc-12 CXX=/usr/bin/g++-12 \
CUDACXX=/usr/local/cuda/bin/nvcc CUDAHOSTCXX=/usr/bin/g++-12 \
/opt/ml4t/bin/pip install --no-cache-dir --force-reinstall --no-deps \
--no-binary lightgbm \
--config-settings=cmake.define.USE_CUDA=ON "lightgbm>=4.6" && \
/opt/ml4t/bin/python -c "\
import lightgbm as lgb; \
print(f'LightGBM {lgb.__version__}'); \
print('CUDA build: OK (runtime verification requires GPU)')"; \
fi
# arm64: standard pip wheel (no GPU on macOS)
RUN if [ "$TARGETARCH" = "arm64" ]; then \
/opt/ml4t/bin/pip install --no-cache-dir "lightgbm>=4.6"; \
fi
# Install optional dependency groups (bayesian, advanced, mlops)
# Skip rapids (requires specific CUDA version matching)
RUN /opt/ml4t/bin/pip install --no-cache-dir \
"pymc>=5.14" "arviz>=0.18" \
"opacus>=1.4" \
"feast>=0.61" "mlflow>=2.16" \
|| echo "WARNING: Some optional deps failed"
# Python 3.14 compatibility: protobuf 4.x uses metaclass tp_new (crashes).
# Force protobuf 5+ which uses the pure-Python descriptor pool.
RUN /opt/ml4t/bin/pip install --no-cache-dir "protobuf>=5.0"
WORKDIR /app
ENV PATH="/opt/ml4t/bin:$PATH"
ENV PYTHONPATH="/app"
# For GPU users: CUDA libraries are bundled in PyTorch wheel
# nvidia-container-toolkit passes through /dev/nvidia* and sets these:
# - NVIDIA_VISIBLE_DEVICES
# - LD_LIBRARY_PATH (for libnvidia-ml.so, etc.)
# Jupyter configuration - disable token authentication
ENV JUPYTER_ENABLE_LAB=yes
RUN mkdir -p /etc/jupyter && \
echo "c.ServerApp.token = ''" >> /etc/jupyter/jupyter_server_config.py && \
echo "c.ServerApp.password = ''" >> /etc/jupyter/jupyter_server_config.py && \
echo "c.ServerApp.allow_origin = '*'" >> /etc/jupyter/jupyter_server_config.py && \
echo "c.ServerApp.disable_check_xsrf = True" >> /etc/jupyter/jupyter_server_config.py
# NOTE: jupyterlab-plotly extension is required for Plotly widget rendering
# It is installed automatically by plotly pip package - do NOT delete it
# Disable catboost-widget extension (causes "require is not defined" errors in JupyterLab 4.x)
# The widget is bundled with catboost but not compatible with modern JupyterLab
RUN mkdir -p /etc/jupyter/labconfig && \
echo '{"disabledExtensions": {"catboost-widget": true}}' > /etc/jupyter/labconfig/page_config.json
# Test infrastructure — pytest + helpers so CI doesn't need runtime pip install
RUN /opt/ml4t/bin/pip install --no-cache-dir \
"pytest>=8.0" "pytest-timeout>=2.3"
# Copy import test script
COPY envs/test_all_imports.py /opt/ml4t/bin/test_imports.py
RUN chmod +x /opt/ml4t/bin/test_imports.py
# Health check script - reports GPU status
RUN echo '#!/bin/bash\n\
echo "============================================"\n\
echo "ML4T Environment Status"\n\
echo "============================================"\n\
python -c "\n\
import torch\n\
print(f\"PyTorch version: {torch.__version__}\")\n\
print(f\"CUDA available: {torch.cuda.is_available()}\")\n\
if torch.cuda.is_available():\n\
print(f\"CUDA version: {torch.version.cuda}\")\n\
print(f\"GPU count: {torch.cuda.device_count()}\")\n\
for i in range(torch.cuda.device_count()):\n\
print(f\" GPU {i}: {torch.cuda.get_device_name(i)}\")\n\
else:\n\
print(\"Running in CPU-only mode (this is fine!)\")\n\
"\n\
echo ""\n\
echo "Run: ml4t-test-imports # Test all chapter imports"\n\
echo "Run: ml4t-test-imports 15 # Test specific chapter"\n\
echo "============================================"' > /usr/local/bin/ml4t-status && chmod +x /usr/local/bin/ml4t-status
# Import test wrapper
RUN echo '#!/bin/bash\npython /opt/ml4t/bin/test_imports.py "$@"' > /usr/local/bin/ml4t-test-imports && \
chmod +x /usr/local/bin/ml4t-test-imports
# Default command
# Note: --allow-root removed; docker-compose.yml passes host UID:GID
# Token/password disabled via /etc/jupyter/jupyter_server_config.py
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--no-browser"]