diff --git a/.dockerignore b/.dockerignore index 0ecb00b..38f1110 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,9 @@ # .dockerignore # Цель: контекст сборки должен быть маленьким. Образ собирается из src, -# requirements.txt и run.sh (см. Dockerfile); чекпоинт в образ не копируется, а -# монтируется в /app/models при запуске. Данные, тесты и служебные файлы -# исключаются — DICOM-датасет не должен попадать в образ с медицинскими данными. +# requirements.txt, run.sh и чекпоинта models/dxa_model.pth — он закоммичен в +# репозиторий и копируется внутрь образа (см. Dockerfile), поэтому модели не +# нужно монтировать при запуске. Данные, тесты и служебные файлы исключаются — +# DICOM-датасет не должен попадать в образ с медицинскими данными. __pycache__ **/__pycache__ *.pyc diff --git a/Dockerfile b/Dockerfile index c3daaa5..6776d9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,13 @@ # DXA Quality Assessment — контейнер для инференса (CPU) # -# Сборка: docker build -t rell.ru:5000/rell-bone-2026:latest . -# Запуск: docker run -v /data/bone_2026/models:/app/models:ro -p 8000:8000 dxa-quality +# Сборка: docker build -t dxa-quality . +# Запуск: docker run -p 8000:8000 dxa-quality +# или: docker compose up -d (сервис dxa-cpu) # -# Веса в образ НЕ копируются: swarm монтирует каталог с чекпоинтом в -# /app/models только для чтения, а models/ не попадает в git (см. .gitignore), -# поэтому COPY models ломал бы сборку в CI. Путь к чекпоинту берётся из -# DXA_MODEL_PATH и по умолчанию указывает на смонтированный каталог. +# Чекпоинт лежит ВНУТРИ образа: models/dxa_model.pth отслеживается git (каталог +# models/ не исключён в .gitignore) и копируется на этапе сборки. Монтировать +# пути не нужно — условие приёмки в том, что модели уже в образе. Если всё-таки +# смонтировать каталог в /app/models, он перекроет встроенный чекпоинт. # # Требования методики: зафиксированные версии зависимостей и работа без # внешних сервисов. Inference не скачивает веса из сети — они внутри @@ -40,8 +41,7 @@ COPY run.sh ./run.sh RUN chmod +x ./run.sh # Smoke-проверка импорта: падает на сборке, если расходятся зависимости или -# модуль не найден, вместо тихой 500-й на каждом запросе в рантайме. Модель -# здесь не загружается — чекпоинта в контексте сборки нет. +# модуль не найден, вместо тихой 500-й на каждом запросе в рантайме. RUN python -c "import src.main; print('app import ok')" # --- Проверка статики для офлайн-работы --- @@ -54,6 +54,21 @@ RUN python -c "import os; \ assert not missing, f'missing frontend assets: {missing}'; \ print('frontend assets ok')" +# --- Чекпоинт внутри образа --- +# Условие приёмки: модель уже в образе, чтобы проверяющему не приходилось +# указывать путь к каталогу с весами. models/dxa_model.pth отслеживается git, +# поэтому копирование работает и в CI. +COPY models/dxa_model.pth /app/models/dxa_model.pth + +# Проверка, что чекпоинт читается и содержит всё нужное для инференса: без этого +# битый или забытый файл проявился бы только в рантайме как model_loaded: false +# и HTTP 500 на каждый запрос анализа. +RUN python -c "import torch; \ + ckpt=torch.load('/app/models/dxa_model.pth', map_location='cpu', weights_only=False); \ + missing=[k for k in ('model_state_dict','backbone','head','threshold') if k not in ckpt]; \ + assert not missing, f'checkpoint missing keys: {missing}'; \ + print('checkpoint ok:', ckpt['backbone'], ckpt['head'], 'threshold', ckpt['threshold'])" + # Каталоги монтирований swarm: создаём заранее, чтобы образ вёл себя одинаково # и при обычном `docker run` без volume. RUN mkdir -p /app/data /app/logs diff --git a/Dockerfile_cuda b/Dockerfile_cuda index a1fdcc0..29c53e0 100644 --- a/Dockerfile_cuda +++ b/Dockerfile_cuda @@ -1,71 +1,106 @@ -# DXA Quality Assessment - Docker Container (GPU / CUDA) -# Build: docker build -f Dockerfile_cuda -t dxa-quality-cuda . -# Run: docker run --gpus all -v /path/to/data:/data -v /path/to/models:/app/models \ -# -p 8000:8000 dxa-quality-cuda +# DXA Quality Assessment — контейнер для инференса (GPU / CUDA) +# +# Сборка: docker build -f Dockerfile_cuda -t dxa-quality:cuda . +# Запуск: docker run --gpus all -p 8000:8000 dxa-quality:cuda +# или: docker compose --profile cuda up -d dxa-cuda +# +# База — тот же python:3.11-slim, что и у CPU-варианта, а CUDA и cuDNN приходят +# внутри колёс torch с индексом cu126. Отдельный nvidia/cuda-образ не нужен: +# контейнеру достаточно драйвера хоста, который подставляет +# nvidia-container-toolkit по флагу --gpus all (или секции deploy в compose). +# Требуется драйвер NVIDIA >= 525; для H200 (sm_90) в кластере он заведомо новее. +# +# Индекс cu118 в прежней версии файла не работал: torch 2.8.0 под CUDA 11.8 не +# публикуется (последняя версия там — 2.7.1), поэтому сборка падала на pip. +# Для 2.8.0 доступны только cu126 и cu128; выбран cu126 как наиболее +# распространённый. Под H200 нужен torch>=2.5, sm_90 поддерживается. +# +# Чекпоинт лежит ВНУТРИ образа (см. Dockerfile): монтировать пути не нужно. +FROM python:3.11-slim -# Base image with GPU support -FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04 - -# Неинтерактивный режим для apt -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y \ - python3.10 \ - python3-pip \ - python3.10-dev \ - python3.10-venv \ - build-essential \ - ninja-build \ - libgl1-mesa-glx \ +# libgl1/libglib2.0-0 нужны opencv (импортируется через зависимости проекта), +# libgomp1 — для параллельных циклов torch. Список совпадает с CPU-образом. +RUN apt-get update && apt-get install -y --no-install-recommends \ + libgl1 \ libglib2.0-0 \ - libsm6 \ - libxext6 \ - libxrender1 \ libgomp1 \ && rm -rf /var/lib/apt/lists/* -# Обновляем pip и ставим свежий meson (0.64.0+) — ДО всех Python-пакетов -RUN pip3 install --no-cache-dir --upgrade pip "meson>=0.64.0" - WORKDIR /app -# --- PyTorch под CUDA 11.8 (ставим отдельно, чтобы не тянулся CUDA 12.6) --- -RUN pip3 install --no-cache-dir \ - torch==2.8.0 torchvision==0.23.0 \ - --index-url https://download.pytorch.org/whl/cu118 +# --- PyTorch под CUDA 12.6 --- +# CUDA-колёса ставим первыми и с явным индексом: иначе pip взял бы с PyPI сборку +# под CUDA 12.6/12.8 произвольной ревизии. torchvision 0.23.0 — парная к torch +# 2.8.0 версия (другие комбинации несовместимы). +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir \ + --index-url https://download.pytorch.org/whl/cu126 \ + torch==2.8.0 torchvision==0.23.0 # --- Остальные зависимости --- -# Список версий один на оба образа: requirements.txt. Уже установленный torch -# pip не переустановит — версия из cu118 удовлетворяет требованию. +# Отдельным слоем и после torch: список версий один на оба образа +# (requirements.txt), а уже установленный torch==2.8.0+cu126 pip повторно не +# поставит — требование torch==2.8.0 считается выполненным, потому что локальная +# метка сборки при сравнении версий не учитывается. COPY requirements.txt ./ -RUN pip3 install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt -# --- Код приложения --- +# --- Код --- COPY src ./src + +# Скрипт пакетной обработки (оценка берёт его из корня образа). COPY run.sh ./run.sh RUN chmod +x ./run.sh -RUN mkdir -p models -# Smoke-проверка импорта: падает на сборке, если зависимость недоступна или -# модуль не найден, вместо тихой 500-й в рантайме. Чекпоинт здесь не читается. -RUN python3 -c "import src.main; print('app import ok')" +# Smoke-проверка импорта: падает на сборке, если расходятся зависимости или +# модуль не найден, вместо тихой 500-й на каждом запросе в рантайме. +RUN python -c "import src.main; print('app import ok')" # --- Проверка статики для офлайн-работы --- -# Tailwind и FontAwesome лежат в src/api/static, обращений к CDN быть не должно. -RUN python3 -c "import os; \ +# Веб-интерфейс не должен зависеть от CDN: Tailwind и FontAwesome лежат в +# src/api/static/vendor и src/api/static/webfonts (см. index.html). +RUN python -c "import os; \ files=['src/api/static/vendor/tailwind.js','src/api/static/vendor/fontawesome.css', \ 'src/api/static/webfonts/fa-solid-900.woff2']; \ missing=[f for f in files if not os.path.exists(f)]; \ assert not missing, f'missing frontend assets: {missing}'; \ print('frontend assets ok')" -# Expose API port -EXPOSE 8000 +# --- Чекпоинт внутри образа --- +# Условие приёмки: модель уже в образе, чтобы проверяющему не приходилось +# указывать путь к каталогу с весами. models/dxa_model.pth отслеживается git, +# поэтому копирование работает и в CI. +COPY models/dxa_model.pth /app/models/dxa_model.pth -# Environment variables +# Проверка, что чекпоинт читается и содержит всё нужное для инференса, а также +# что сборка torch видит CUDA: без этого проблемы всплыли бы только в рантайме. +RUN python -c "import torch; \ + ckpt=torch.load('/app/models/dxa_model.pth', map_location='cpu', weights_only=False); \ + missing=[k for k in ('model_state_dict','backbone','head','threshold') if k not in ckpt]; \ + assert not missing, f'checkpoint missing keys: {missing}'; \ + print('checkpoint ok:', ckpt['backbone'], ckpt['head'], 'threshold', ckpt['threshold']); \ + print('torch', torch.__version__, 'cuda built:', torch.version.cuda)" + +# --- Переменные окружения --- +# Читается кодом только DXA_MODEL_PATH (src/main.py). MODEL_PATH, DATA_PATH, +# ANNOTATION_DIR swarm передаёт по инерции от прежней версии — код их не +# использует, и они ни на что не влияют. ENV PYTHONUNBUFFERED=1 \ PYTHONPATH=/app \ - DXA_MODEL_PATH=/app/models/dxa_model.pth + DXA_MODEL_PATH=/app/models/dxa_model.pth \ + MODEL_PATH=/app/models \ + DATA_PATH=/app/data \ + ANNOTATION_DIR=/app/data/annotations -# Default command -CMD ["python3", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] +RUN mkdir -p /app/data /app/logs + +EXPOSE 8000 + +# HEALTHCHECK опирается на /api/v1/health, который отдаёт model_loaded. +# На GPU-устройстве старт дольше: даём запас по start-period. +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD python -c "import urllib.request,sys; \ + r=urllib.request.urlopen('http://127.0.0.1:8000/api/v1/health', timeout=5); \ + sys.exit(0 if r.status==200 else 1)" + +CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/QWEN.md b/QWEN.md index 3a70f20..e1c2ce3 100644 --- a/QWEN.md +++ b/QWEN.md @@ -297,14 +297,19 @@ position_validator,universal_scorer,medical_quality}.py`) удалены 2026-09 ## Docker ```bash -docker build -t dxa-quality . -docker run -v /path/to/data:/data -p 8000:8000 dxa-quality +docker compose up -d # CPU, http://localhost:8000 +docker compose --profile cuda up -d dxa-cuda # GPU (нужен nvidia-container-toolkit) ``` -Dockerfile ставит зафиксированные версии, копирует только `src/` и `run.sh`, -проверяет импорт приложения и наличие офлайн-ассетов фронтенда на этапе сборки и -имеет HEALTHCHECK. Чекпоинт в образ не копируется — он монтируется в `/app/models` -при запуске (`DXA_MODEL_PATH`). Данные, тесты и `labels/` в образ не попадают +Образы: `Dockerfile` (python:3.11-slim, torch из CPU-индекса) и `Dockerfile_cuda` +(та же база, torch и torchvision из индекса `cu126` — под CUDA 11.8 колёс torch +2.8.0 нет, индекс заканчивается на 2.7.1). Оба собираются из `src/`, +`requirements.txt`, `run.sh` и **чекпоинта**: `models/dxa_model.pth` отслеживается +git и копируется в `/app/models` на этапе сборки, поэтому монтировать пути при +запуске не нужно (условие приёмки — модели уже внутри образа). Сборка проверяет +импорт приложения, наличие офлайн-ассетов фронтенда и читаемость чекпоинта, +образ имеет HEALTHCHECK. Данные, тесты и `labels/` в образ не попадают (`.dockerignore`), поэтому `./run.sh train` внутри контейнера возьмёт метки из имён файлов (с предупреждением); для обучения в контейнере смонтируйте `labels/` -или передайте свой `--labels-csv`. +или передайте свой `--labels-csv`. Если внешний `docker-swarm.yml` монтирует +каталог с моделями в `/app/models`, монтирование перекроет встроенный чекпоинт. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0fec134 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,52 @@ +# DXA Quality Assessment — запуск сервиса в контейнере. +# +# Чекпоинт лежит внутри образа (models/dxa_model.pth копируется на этапе сборки), +# поэтому монтировать пути не нужно: ни каталог с моделями, ни данные. +# Данные для анализа загружаются через веб-интерфейс (http://localhost:8000). +# +# CPU — работает на любой машине, ничего кроме docker не требуется: +# docker compose up -d # сервис dxa-cpu, порт 8000 +# +# GPU — H200 и другие ускорители; нужен драйвер NVIDIA и nvidia-container-toolkit: +# docker compose --profile cuda up -d dxa-cuda +# имя сервиса указано явно: без него поднялись бы оба и разошлись по одному порту +# +# Пересобрать после правок кода: docker compose build [dxa-cpu|dxa-cuda] +# Логи: docker compose logs -f dxa-cpu +# Состояние: curl -s localhost:8000/api/v1/health +# Остановить: docker compose down + +name: dxa-quality + +services: + # Обычный инференс на CPU: torch ставится из CPU-индекса, GPU не используется. + dxa-cpu: + image: dxa-quality:cpu + build: + context: . + dockerfile: Dockerfile + ports: + - "8000:8000" + restart: unless-stopped + + # Инференс на GPU: базовый образ тот же, но torch собран под CUDA 12.6. + # Профиль cuda — чтобы на машинах без NVIDIA сервис не мешал запуску по умолчанию. + dxa-cuda: + profiles: ["cuda"] + image: dxa-quality:cuda + build: + context: . + dockerfile: Dockerfile_cuda + ports: + - "8000:8000" + restart: unless-stopped + environment: + NVIDIA_VISIBLE_DEVICES: all + NVIDIA_DRIVER_CAPABILITIES: compute,utility + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] diff --git a/docs/technical-description.html b/docs/technical-description.html index b123fb4..72e9c16 100644 --- a/docs/technical-description.html +++ b/docs/technical-description.html @@ -158,10 +158,10 @@
/api/v1/batch и /api/v1/exportDockerfile (python:3.11-slim), Dockerfile_cuda для GPU, run.shDockerfile (python:3.11-slim, чекпоинт внутри образа), Dockerfile_cuda для GPU, docker-compose.yml на оба случая, run.shrequirements.txt с точными версиями, базовый образ по тегу, torch с CPU-индексомrequirements.txt с точными версиями, базовый образ по тегу, torch с CPU-индексом (CPU-вариант) и cu126 (GPU-вариант)src/quality/run.sh, Dockerfile, Dockerfile_cuda, Jenkinsfilerun.sh, docker-compose.yml, Dockerfile, Dockerfile_cuda, JenkinsfileDockerfile_cudaDockerfile_cudaРазмер образа (измерено): 1.41 ГБ (1 412 385 143 байт) для сборки под
-linux/arm64. Базовый образ python:3.11-slim занимает 150 МБ, остальное —
-зависимости из requirements.txt, включая torch и torchvision из CPU-индекса; CUDA-колёса
-в образ не попадают. Данные, тесты, labels/ и assets/ в образ не входят,
-чекпоинт (43 МБ) монтируется отдельно.
Размер образа (измерено). CPU-вариант: 1.46 ГБ под linux/arm64 и 1.8 ГБ под
+linux/amd64. GPU-вариант: 7.0 ГБ под linux/amd64 — почти всё сверх базы
+занимают CUDA-библиотеки внутри колёс torch (nvidia-cublas-cu12,
+nvidia-cudnn-cu12, nvidia-nccl-cu12 и другие). Базовый образ
+python:3.11-slim — 150 МБ; torch и torchvision берутся из CPU-индекса в CPU-варианте и из
+индекса cu126 в GPU-варианте, поэтому CUDA-колёса в CPU-образ не попадают (там
+torch 2.8.0+cpu и ноль nvidia-пакетов). Данные, тесты, labels/,
+assets/ и docs/ в образ не входят; чекпоинт (43 МБ), наоборот, лежит внутри —
+монтировать каталог с моделями при запуске не нужно.
| Элемент | Значение |
|---|---|
| Базовый образ | python:3.11-slim (фиксированный тег) |
| Зависимости | requirements.txt с точными версиями; torch и torchvision — из CPU-индекса PyTorch, чтобы в образ не попали CUDA-колёса |
| Что копируется в образ | только src/, requirements.txt и run.sh |
| Что не копируется | данные, тесты, labels/, assets/, docs/ (см. .dockerignore), а также чекпоинт |
| Чекпоинт | монтируется при запуске в /app/models; путь задаётся переменной DXA_MODEL_PATH (по умолчанию /app/models/dxa_model.pth) |
| Проверки на этапе сборки | импорт приложения (import src.main) и наличие офлайн-ассетов фронтенда; сборка падает при их отсутствии |
| Базовый образ | python:3.11-slim (фиксированный тег); у GPU-варианта он тот же — CUDA и cuDNN приходят внутри колёс torch, отдельный nvidia/cuda-образ не нужен |
| Зависимости | requirements.txt с точными версиями; CPU-вариант берёт torch и torchvision из CPU-индекса PyTorch, GPU-вариант — из индекса cu126 |
| Что копируется в образ | src/, requirements.txt, run.sh и чекпоинт models/dxa_model.pth |
| Что не копируется | данные, тесты, labels/, assets/, docs/ (см. .dockerignore) |
| Чекпоинт | копируется на этапе сборки и лежит в образе как /app/models/dxa_model.pth; путь задаётся переменной DXA_MODEL_PATH. Монтировать каталог с моделями не нужно |
| Проверки на этапе сборки | импорт приложения (import src.main), наличие офлайн-ассетов фронтенда и читаемость чекпоинта (ключи backbone, head, threshold); сборка падает при их отсутствии |
| Контроль состояния | HEALTHCHECK обращается к /api/v1/health каждые 30 с |
| Запуск | docker-compose.yml: сервис dxa-cpu поднимается по умолчанию, GPU-сервис dxa-cuda — под профилем cuda |
| Точка входа | uvicorn src.main:app на порту 8000 |
Если чекпоинт не смонтирован, сервис всё равно поднимается: /api/v1/health сообщает
+
Модель встроена в образ, поэтому штатно чекпоинт всегда на месте. Если файл всё же недоступен
+(например, образ повреждён), сервис всё равно поднимается: /api/v1/health сообщает
model_loaded: false, а запросы анализа возвращают ошибку вместо тихой неверной оценки.
Это сознательный выбор: отсутствие модели должно быть заметно сразу, а не проявляться как «странные»
результаты.
Что проверено на самом образе. Сборка прошла обе внутренние проверки
-(app import ok, frontend assets ok). Образ запущен и проверен в двух режимах:
-без смонтированного чекпоинта /api/v1/health отдаёт model_loaded: false и
-exists: false, а запрос анализа — HTTP 500 с телом {"error": "Model not loaded"};
-с примонтированным каталогом моделей /api/v1/health сообщает model_loaded: true,
-device: cpu, эпоху 39 и файл разметки labels/labels_images.csv, запрос анализа
-отвечает корректной строкой результата, а /api/v1/export возвращает XLSX с ожидаемым
-набором столбцов. Сквозная проверка выполнялась в контейнере на той же машине, где снимались
-измерения производительности.
Что проверено на самом образе. Сборка проходит три внутренние проверки:
+app import ok, frontend assets ok и
+checkpoint ok: resnet18 linear threshold -0.4930129051208496. Контейнер запущен без единого
+монтирования (docker run -d -p 8000:8000 dxa-quality:cpu): чекпоинт присутствует внутри
+образа (/app/models/dxa_model.pth), /api/v1/health сообщает
+model_loaded: true, device: cpu, эпоху 39 и файл разметки
+labels/labels_images.csv, запрос /api/v1/analyze отвечает корректной строкой
+результата, а /api/v1/export возвращает XLSX с ожидаемым набором столбцов. Сквозная
+проверка выполнена в контейнере на той же машине, где снимались измерения производительности. Оба
+варианта дополнительно собраны под целевой linux/amd64 (эмуляция на той же машине):
+CPU-образ содержит torch 2.8.0+cpu и ни одного nvidia-пакета, GPU-образ —
+torch 2.8.0+cu126 с cuDNN 9.10.2 и библиотекой libtorch_cuda.so; в обоих
+сервис стартует, и /api/v1/health, /api/v1/analyze и
+/api/v1/export отвечают корректно (device: cpu, поскольку ускорителя в машине
+нет). Работа на самом GPU не проверялась: для неё нужна машина с драйвером NVIDIA, где критерий
+приёмки — torch.cuda.is_available() == True и device: cuda в
+/api/v1/health.
Jenkinsfile выполняет три шага: сборка образа, публикация в реестр и выкладка в
кластер. Скрипт run.sh — единая точка входа для разработки и эксплуатации: обучение,
разметка, приведение имён файлов, инференс, сервис, сравнение вариантов разметки и тесты
-(перечень команд — приложение Б).
docker-compose.yml с двумя
+сервисами (14.1). Файл выкладки swarm (/data/deploy/rell-bone-2026/docker-swarm.yml) лежит
+вне репозитория: если он монтирует каталог с моделями в /app/models, монтирование
+перекроет встроенный чекпоинт, поэтому его нужно убрать при обновлении выкладки.
Документ подготовлен по фактическому состоянию репозитория; машинные отчёты обучения —
models/train_report.json, обоснование разметки — assets/labeling.md.