Kalpokoch commited on
Commit
6ca35e1
Β·
1 Parent(s): 7ebedec

perf: aggressive build optimizations - remove albumentations, use plain uvicorn, multi-stage docker build

Browse files
Files changed (3) hide show
  1. Dockerfile +30 -11
  2. inference.py +23 -9
  3. requirements.txt +1 -2
Dockerfile CHANGED
@@ -1,28 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  FROM python:3.10-slim
2
 
3
- # Install system deps needed by OpenCV (minimal set)
4
  RUN apt-get update && apt-get install -y --no-install-recommends \
5
  libglib2.0-0 \
6
  libsm6 \
7
  libxext6 \
8
- libxrender-dev \
 
9
  libgl1 \
10
- && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
11
 
12
  WORKDIR /app
13
 
14
- # ── Install dependencies FIRST (most stable layer) ──
15
- COPY requirements.txt .
16
- RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
17
- pip install --no-cache-dir --default-timeout=1000 -r requirements.txt
18
 
19
- # ── Copy model files SECOND (slower to change than code) ──
20
  COPY trainedmodels/ ./trainedmodels/
21
 
22
- # ── Copy app code LAST (fastest layer, changes most often) ──
23
  COPY app.py inference.py ./
24
 
25
- # HuggingFace Spaces requires port 7860
 
 
 
 
26
  EXPOSE 7860
27
 
28
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
1
+ # ═══════════════════════════════════════════════════════════════════════════
2
+ # Multi-stage build: Smaller final image, faster rebuilds
3
+ # ═══════════════════════════════════════════════════════════════════════════
4
+
5
+ # ─── Stage 1: Build dependencies ──────────────────────────────────────────
6
+ FROM python:3.10-slim as builder
7
+
8
+ WORKDIR /build
9
+
10
+ # Copy and install Python packages
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
13
+ pip install --no-cache-dir --default-timeout=1000 --prefix=/install -r requirements.txt
14
+
15
+ # ─── Stage 2: Runtime (minimal) ───────────────────────────────────────────
16
  FROM python:3.10-slim
17
 
18
+ # Install only runtime system deps (minimal OpenCV requirements)
19
  RUN apt-get update && apt-get install -y --no-install-recommends \
20
  libglib2.0-0 \
21
  libsm6 \
22
  libxext6 \
23
+ libxrender1 \
24
+ libgomp1 \
25
  libgl1 \
26
+ && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/cache/apt/*
27
 
28
  WORKDIR /app
29
 
30
+ # Copy installed packages from builder stage
31
+ COPY --from=builder /install /usr/local
 
 
32
 
33
+ # Copy model files (cached unless model changes)
34
  COPY trainedmodels/ ./trainedmodels/
35
 
36
+ # Copy application code (changes most frequently)
37
  COPY app.py inference.py ./
38
 
39
+ # Environment optimizations
40
+ ENV PYTHONUNBUFFERED=1 \
41
+ PYTHONDONTWRITEBYTECODE=1 \
42
+ PIP_NO_CACHE_DIR=1
43
+
44
  EXPOSE 7860
45
 
46
+ # Use exec form for better signal handling
47
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
inference.py CHANGED
@@ -5,7 +5,6 @@ import torch
5
  import torch.nn as nn
6
  import torch.nn.functional as F
7
  from torchvision import models
8
- import albumentations as A
9
  from pathlib import Path
10
 
11
  # ─── Architecture (must match training notebook exactly) ───────────────────────
@@ -165,12 +164,27 @@ def apply_jet_colormap(gray_img):
165
  return (np.stack([r, g, b], axis=-1) * 255).astype(np.uint8)
166
 
167
 
168
- def get_transform(image_size=512):
169
- return A.Compose([
170
- A.CLAHE(clip_limit=2.0, tile_grid_size=(8, 8), p=1.0),
171
- A.CenterCrop(height=350, width=350),
172
- A.Resize(height=image_size, width=image_size),
173
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
 
176
  def preprocess_image(image_path: str, meta: dict) -> torch.Tensor:
@@ -179,8 +193,8 @@ def preprocess_image(image_path: str, meta: dict) -> torch.Tensor:
179
  if image is None:
180
  raise ValueError(f"Could not read image: {image_path}")
181
 
182
- transform = get_transform(meta["image_size"])
183
- image = transform(image=image)["image"]
184
 
185
  image = image.astype(np.float32) / 255.0
186
  image = (image - meta["global_mean"]) / meta["global_std"]
 
5
  import torch.nn as nn
6
  import torch.nn.functional as F
7
  from torchvision import models
 
8
  from pathlib import Path
9
 
10
  # ─── Architecture (must match training notebook exactly) ───────────────────────
 
164
  return (np.stack([r, g, b], axis=-1) * 255).astype(np.uint8)
165
 
166
 
167
+ def apply_clahe_cv2(image, clip_limit=2.0, tile_grid_size=(8, 8)):
168
+ """Apply CLAHE using OpenCV (replaces albumentations)."""
169
+ clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=tile_grid_size)
170
+ return clahe.apply(image)
171
+
172
+
173
+ def preprocess_with_cv2(image, image_size=512):
174
+ """Preprocessing pipeline using pure OpenCV (replaces albumentations)."""
175
+ # CLAHE
176
+ image = apply_clahe_cv2(image, clip_limit=2.0, tile_grid_size=(8, 8))
177
+
178
+ # Center crop 350x350
179
+ h, w = image.shape[:2]
180
+ start_y = (h - 350) // 2
181
+ start_x = (w - 350) // 2
182
+ image = image[start_y:start_y+350, start_x:start_x+350]
183
+
184
+ # Resize to target size
185
+ image = cv2.resize(image, (image_size, image_size), interpolation=cv2.INTER_LINEAR)
186
+
187
+ return image
188
 
189
 
190
  def preprocess_image(image_path: str, meta: dict) -> torch.Tensor:
 
193
  if image is None:
194
  raise ValueError(f"Could not read image: {image_path}")
195
 
196
+ # Use OpenCV preprocessing instead of albumentations
197
+ image = preprocess_with_cv2(image, image_size=meta["image_size"])
198
 
199
  image = image.astype(np.float32) / 255.0
200
  image = (image - meta["global_mean"]) / meta["global_std"]
requirements.txt CHANGED
@@ -4,9 +4,8 @@ torch==2.0.1+cpu
4
  torchvision==0.15.2+cpu
5
 
6
  fastapi==0.111.0
7
- uvicorn[standard]==0.29.0
8
  python-multipart==0.0.9
9
  opencv-python-headless==4.9.0.80
10
- albumentations==1.4.2
11
  numpy==1.26.4
12
  Pillow==10.3.0
 
4
  torchvision==0.15.2+cpu
5
 
6
  fastapi==0.111.0
7
+ uvicorn==0.29.0
8
  python-multipart==0.0.9
9
  opencv-python-headless==4.9.0.80
 
10
  numpy==1.26.4
11
  Pillow==10.3.0