Compare commits

...
5 Commits
Author SHA1 Message Date
flanandClaude Sonnet 4.6 0f86c1054a chore: bump version to 0.4.2, update changelog
CUDA base image downgraded to 12.8.1 (driver 570 compatibility fix),
benchmark script added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 22:19:16 +00:00
flanandClaude Sonnet 4.6 634688fc93 Fix ruff lint errors in benchmark.py
Remove unused imports, fix unsorted imports, remove bare f-strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 22:19:16 +00:00
flanandClaude Sonnet 4.6 9f0a78522f Downgrade GPU base image to CUDA 12.8.1; add benchmark script
CUDA 13.3 requires driver >= 575 but the host only has 570 (error 804).
CUDA 12.8.1 is the highest version supported by driver 570 and works
correctly with the NVIDIA Container Toolkit.

Add scripts/benchmark.py to measure InsightFace + SigLIP latency and
throughput across GPU and CPU modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 22:19:16 +00:00
flanandClaude Sonnet 4.6 8d1f5da05a ci: consolidate Docker builds — eliminate duplicate builds on release
release.yml now calls docker-publish.yml via workflow_call instead of
re-running all four image builds independently. docker-publish.yml gains
workflow_call inputs (tag, version) for release context; branch trigger
is narrowed to dev only (main changes only land via tagged releases).

Each release previously built all four variants twice (~90 min) — once on
merge to main, once on tag push. Now it builds once.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 22:19:16 +00:00
github-actions[bot] a7257cf031 chore: update lockfiles 2026-06-13 19:59:04 +00:00
7 changed files with 337 additions and 216 deletions
+119 -30
View File
@@ -2,7 +2,7 @@ name: Publish Docker Image
on: on:
push: push:
branches: ["main", "dev"] branches: ["dev"]
paths-ignore: paths-ignore:
- "**.md" - "**.md"
- "docs/**" - "docs/**"
@@ -15,6 +15,16 @@ on:
- "uv-cpu.lock" - "uv-cpu.lock"
- "uv-rocm.lock" - "uv-rocm.lock"
- "uv-intel.lock" - "uv-intel.lock"
workflow_call:
inputs:
tag:
type: string
required: false
description: "Release tag, e.g. v0.4.1 — triggers :latest + versioned image tags"
version:
type: string
required: false
description: "Version string without v prefix, e.g. 0.4.1"
concurrency: concurrency:
group: docker-${{ github.ref }} group: docker-${{ github.ref }}
@@ -50,6 +60,8 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v6
with:
ref: ${{ inputs.tag || github.ref }}
- name: Set up QEMU - name: Set up QEMU
if: matrix.platform == 'linux/arm64' if: matrix.platform == 'linux/arm64'
@@ -65,6 +77,15 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Compute build version
id: version
run: |
if [ -n "${{ inputs.version }}" ]; then
echo "value=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
else
echo "value=dev" >> "$GITHUB_OUTPUT"
fi
- name: Build and push by digest - name: Build and push by digest
id: build id: build
uses: docker/build-push-action@v7 uses: docker/build-push-action@v7
@@ -72,6 +93,7 @@ jobs:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
build-args: VERSION=${{ steps.version.outputs.value }}
cache-from: type=gha,scope=${{ matrix.platform }} cache-from: type=gha,scope=${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=${{ matrix.platform }} cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -120,22 +142,26 @@ jobs:
- name: Determine image tags - name: Determine image tags
id: tags id: tags
run: | run: |
if [ "${{ github.ref_name }}" = "dev" ]; then IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev" >> "$GITHUB_OUTPUT" INPUT_TAG="${{ inputs.tag }}"
if [ -n "$INPUT_TAG" ]; then
echo "tag_args=-t ${IMAGE}:latest -t ${IMAGE}:${INPUT_TAG}" >> "$GITHUB_OUTPUT"
echo "inspect_tag=${IMAGE}:latest" >> "$GITHUB_OUTPUT"
else else
echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> "$GITHUB_OUTPUT" echo "tag_args=-t ${IMAGE}:dev" >> "$GITHUB_OUTPUT"
echo "inspect_tag=${IMAGE}:dev" >> "$GITHUB_OUTPUT"
fi fi
- name: Create and push multi-arch manifest - name: Create and push multi-arch manifest
working-directory: /tmp/digests working-directory: /tmp/digests
run: | run: |
docker buildx imagetools create \ docker buildx imagetools create \
-t ${{ steps.tags.outputs.tags }} \ ${{ steps.tags.outputs.tag_args }} \
$(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image - name: Inspect image
run: | run: |
docker buildx imagetools inspect ${{ steps.tags.outputs.tags }} docker buildx imagetools inspect ${{ steps.tags.outputs.inspect_tag }}
- name: Ensure package is public - name: Ensure package is public
run: | run: |
@@ -162,6 +188,8 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v6
with:
ref: ${{ inputs.tag || github.ref }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v4
@@ -176,13 +204,30 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Determine CPU image tag - name: Determine CPU image tags
id: cpu-tag id: cpu-tags
run: | run: |
if [ "${{ github.ref_name }}" = "dev" ]; then IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
echo "tag=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev-cpu" >> "$GITHUB_OUTPUT" INPUT_TAG="${{ inputs.tag }}"
if [ -n "$INPUT_TAG" ]; then
{
echo "tags<<EOF"
printf '%s\n' "${IMAGE}:cpu" "${IMAGE}:${INPUT_TAG}-cpu"
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "inspect_tag=${IMAGE}:cpu" >> "$GITHUB_OUTPUT"
else else
echo "tag=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cpu" >> "$GITHUB_OUTPUT" echo "tags=${IMAGE}:dev-cpu" >> "$GITHUB_OUTPUT"
echo "inspect_tag=${IMAGE}:dev-cpu" >> "$GITHUB_OUTPUT"
fi
- name: Compute build version
id: version
run: |
if [ -n "${{ inputs.version }}" ]; then
echo "value=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
else
echo "value=dev" >> "$GITHUB_OUTPUT"
fi fi
- name: Build and push CPU image - name: Build and push CPU image
@@ -191,16 +236,18 @@ jobs:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: VARIANT=cpu build-args: |
VARIANT=cpu
VERSION=${{ steps.version.outputs.value }}
cache-from: type=gha,scope=cpu cache-from: type=gha,scope=cpu
cache-to: type=gha,mode=max,scope=cpu cache-to: type=gha,mode=max,scope=cpu
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
push: true push: true
tags: ${{ steps.cpu-tag.outputs.tag }} tags: ${{ steps.cpu-tags.outputs.tags }}
- name: Inspect CPU image - name: Inspect CPU image
run: | run: |
docker buildx imagetools inspect ${{ steps.cpu-tag.outputs.tag }} docker buildx imagetools inspect ${{ steps.cpu-tags.outputs.inspect_tag }}
- name: Ensure package is public - name: Ensure package is public
run: | run: |
@@ -227,6 +274,8 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v6
with:
ref: ${{ inputs.tag || github.ref }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v4
@@ -238,13 +287,30 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Determine ROCm image tag - name: Determine ROCm image tags
id: rocm-tag id: rocm-tags
run: | run: |
if [ "${{ github.ref_name }}" = "dev" ]; then IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
echo "tag=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev-rocm" >> "$GITHUB_OUTPUT" INPUT_TAG="${{ inputs.tag }}"
if [ -n "$INPUT_TAG" ]; then
{
echo "tags<<EOF"
printf '%s\n' "${IMAGE}:rocm" "${IMAGE}:${INPUT_TAG}-rocm"
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "inspect_tag=${IMAGE}:rocm" >> "$GITHUB_OUTPUT"
else else
echo "tag=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:rocm" >> "$GITHUB_OUTPUT" echo "tags=${IMAGE}:dev-rocm" >> "$GITHUB_OUTPUT"
echo "inspect_tag=${IMAGE}:dev-rocm" >> "$GITHUB_OUTPUT"
fi
- name: Compute build version
id: version
run: |
if [ -n "${{ inputs.version }}" ]; then
echo "value=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
else
echo "value=dev" >> "$GITHUB_OUTPUT"
fi fi
- name: Build and push ROCm image - name: Build and push ROCm image
@@ -253,16 +319,18 @@ jobs:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64 platforms: linux/amd64
build-args: VARIANT=rocm build-args: |
VARIANT=rocm
VERSION=${{ steps.version.outputs.value }}
cache-from: type=gha,scope=linux/amd64-rocm cache-from: type=gha,scope=linux/amd64-rocm
cache-to: type=gha,mode=max,scope=linux/amd64-rocm cache-to: type=gha,mode=max,scope=linux/amd64-rocm
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
push: true push: true
tags: ${{ steps.rocm-tag.outputs.tag }} tags: ${{ steps.rocm-tags.outputs.tags }}
- name: Inspect ROCm image - name: Inspect ROCm image
run: | run: |
docker buildx imagetools inspect ${{ steps.rocm-tag.outputs.tag }} docker buildx imagetools inspect ${{ steps.rocm-tags.outputs.inspect_tag }}
- name: Ensure package is public - name: Ensure package is public
run: | run: |
@@ -289,6 +357,8 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v6
with:
ref: ${{ inputs.tag || github.ref }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v4
@@ -300,13 +370,30 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Determine Intel image tag - name: Determine Intel image tags
id: intel-tag id: intel-tags
run: | run: |
if [ "${{ github.ref_name }}" = "dev" ]; then IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
echo "tag=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev-intel" >> "$GITHUB_OUTPUT" INPUT_TAG="${{ inputs.tag }}"
if [ -n "$INPUT_TAG" ]; then
{
echo "tags<<EOF"
printf '%s\n' "${IMAGE}:intel" "${IMAGE}:${INPUT_TAG}-intel"
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "inspect_tag=${IMAGE}:intel" >> "$GITHUB_OUTPUT"
else else
echo "tag=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:intel" >> "$GITHUB_OUTPUT" echo "tags=${IMAGE}:dev-intel" >> "$GITHUB_OUTPUT"
echo "inspect_tag=${IMAGE}:dev-intel" >> "$GITHUB_OUTPUT"
fi
- name: Compute build version
id: version
run: |
if [ -n "${{ inputs.version }}" ]; then
echo "value=${{ inputs.version }}" >> "$GITHUB_OUTPUT"
else
echo "value=dev" >> "$GITHUB_OUTPUT"
fi fi
- name: Build and push Intel image - name: Build and push Intel image
@@ -315,16 +402,18 @@ jobs:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64 platforms: linux/amd64
build-args: VARIANT=intel build-args: |
VARIANT=intel
VERSION=${{ steps.version.outputs.value }}
cache-from: type=gha,scope=linux/amd64-intel cache-from: type=gha,scope=linux/amd64-intel
cache-to: type=gha,mode=max,scope=linux/amd64-intel cache-to: type=gha,mode=max,scope=linux/amd64-intel
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
push: true push: true
tags: ${{ steps.intel-tag.outputs.tag }} tags: ${{ steps.intel-tags.outputs.tags }}
- name: Inspect Intel image - name: Inspect Intel image
run: | run: |
docker buildx imagetools inspect ${{ steps.intel-tag.outputs.tag }} docker buildx imagetools inspect ${{ steps.intel-tags.outputs.inspect_tag }}
- name: Ensure package is public - name: Ensure package is public
run: | run: |
+8 -182
View File
@@ -127,188 +127,14 @@ jobs:
prerelease: false, prerelease: false,
}); });
build-gpu: build-images:
name: Build GPU image name: Build and push Docker images
needs: release needs: release
runs-on: ubuntu-latest uses: ./.github/workflows/docker-publish.yml
with:
tag: ${{ needs.release.outputs.tag }}
version: ${{ needs.release.outputs.version }}
secrets: inherit
permissions: permissions:
packages: write packages: write
steps: contents: read
- name: Free up disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
echo "Disk space freed."
- name: Checkout
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push GPU image (latest)
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
build-args: VERSION=${{ needs.release.outputs.version }}
cache-from: type=gha,scope=release-gpu
cache-to: type=gha,mode=max,scope=release-gpu
tags: |
ghcr.io/sudolulo/winnow:latest
ghcr.io/sudolulo/winnow:${{ needs.release.outputs.tag }}
build-cpu:
name: Build CPU image
needs: release
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Free up disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
echo "Disk space freed."
- name: Checkout
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push CPU image
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
build-args: |
VARIANT=cpu
VERSION=${{ needs.release.outputs.version }}
cache-from: type=gha,scope=release-cpu
cache-to: type=gha,mode=max,scope=release-cpu
tags: |
ghcr.io/sudolulo/winnow:cpu
ghcr.io/sudolulo/winnow:${{ needs.release.outputs.tag }}-cpu
build-rocm:
name: Build ROCm image
needs: release
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Free up disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
echo "Disk space freed."
- name: Checkout
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push ROCm image
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
build-args: |
VARIANT=rocm
VERSION=${{ needs.release.outputs.version }}
cache-from: type=gha,scope=release-rocm
cache-to: type=gha,mode=max,scope=release-rocm
tags: |
ghcr.io/sudolulo/winnow:rocm
ghcr.io/sudolulo/winnow:${{ needs.release.outputs.tag }}-rocm
build-intel:
name: Build Intel image
needs: release
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Free up disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
echo "Disk space freed."
- name: Checkout
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GHCR
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Intel image
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
build-args: |
VARIANT=intel
VERSION=${{ needs.release.outputs.version }}
cache-from: type=gha,scope=release-intel
cache-to: type=gha,mode=max,scope=release-intel
tags: |
ghcr.io/sudolulo/winnow:intel
ghcr.io/sudolulo/winnow:${{ needs.release.outputs.tag }}-intel
+10
View File
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.4.2] - 2026-06-13
### Changed
- **GPU image now uses CUDA 12.8.1** (was 13.3): CUDA 13.3 requires driver ≥ 575; driver 570 (the current stable release) was incorrectly rejected with "CUDA driver version is insufficient" at startup. The `:latest` image now works with any NVIDIA driver ≥ 570.
### Added
- **`scripts/benchmark.py`**: measures InsightFace and SigLIP inference latency and throughput across GPU and CPU modes. Run inside the container with `python /app/scripts/benchmark.py`. RTX 2070 SUPER results: InsightFace 12.8 ms / 78 img/s (8× CPU), SigLIP batch 32 at 5.4 ms/img / 187 img/s (33× CPU).
## [0.4.1] - 2026-06-13 ## [0.4.1] - 2026-06-13
### Fixed ### Fixed
+2 -2
View File
@@ -1,5 +1,5 @@
# ── Base images ─────────────────────────────────────────────────────────────── # ── Base images ───────────────────────────────────────────────────────────────
# amd64 + gpu: NVIDIA CUDA 13.3 + cuDNN (GPU acceleration via NVIDIA Container Toolkit) # amd64 + gpu: NVIDIA CUDA 12.8 + cuDNN (GPU acceleration via NVIDIA Container Toolkit)
# amd64 + rocm: Ubuntu 22.04 (AMD GPU via ROCm — pass /dev/kfd and /dev/dri) # amd64 + rocm: Ubuntu 22.04 (AMD GPU via ROCm — pass /dev/kfd and /dev/dri)
# amd64 + intel: Ubuntu 22.04 (Intel Arc / iGPU via OpenVINO — pass /dev/dri) # amd64 + intel: Ubuntu 22.04 (Intel Arc / iGPU via OpenVINO — pass /dev/dri)
# amd64 + cpu: Ubuntu 22.04 (CPU-only, ~2 GB smaller image) # amd64 + cpu: Ubuntu 22.04 (CPU-only, ~2 GB smaller image)
@@ -7,7 +7,7 @@
ARG VARIANT=gpu ARG VARIANT=gpu
FROM --platform=$BUILDPLATFORM nvidia/cuda:13.3.0-cudnn-runtime-ubuntu22.04 AS base-amd64-gpu FROM --platform=$BUILDPLATFORM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu22.04 AS base-amd64-gpu
FROM ubuntu:22.04 AS base-amd64-rocm FROM ubuntu:22.04 AS base-amd64-rocm
FROM ubuntu:22.04 AS base-amd64-intel FROM ubuntu:22.04 AS base-amd64-intel
FROM ubuntu:22.04 AS base-amd64-cpu FROM ubuntu:22.04 AS base-amd64-cpu
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "winnow" name = "winnow"
version = "0.4.1" version = "0.4.2"
description = "Selects diverse, high-quality photos from Immich as training data for Frigate face recognition and object classification." description = "Selects diverse, high-quality photos from Immich as training data for Frigate face recognition and object classification."
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
requires-python = ">=3.13" requires-python = ">=3.13"
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env python3
"""
winnow inference benchmark: GPU vs CPU throughput.
Measures InsightFace (face mode) and SigLIP (object mode) latency and
throughput. Run with FORCE_CPU=true for CPU-only baseline.
Usage inside container:
# GPU mode:
docker exec winnow python /app/scripts/benchmark.py
# CPU mode:
docker exec -e FORCE_CPU=true winnow python /app/scripts/benchmark.py
"""
import os
import sys
import time
import numpy as np
from PIL import Image, ImageDraw
def _mode_label() -> str:
if os.getenv("FORCE_CPU", "").lower() in ("true", "1", "yes"):
return "CPU (FORCE_CPU=true)"
return "GPU (auto)"
def make_face_image(size: int = 640) -> Image.Image:
"""Synthetic face-like image: skin-tone rectangle with landmark blobs."""
img = Image.new("RGB", (size, size), (200, 170, 140))
draw = ImageDraw.Draw(img)
# Head oval
cx, cy = size // 2, size // 2
hw, hh = int(size * 0.3), int(size * 0.38)
draw.ellipse([cx - hw, cy - hh, cx + hw, cy + hh], fill=(220, 185, 155))
# Eyes
for ex in [cx - int(size * 0.1), cx + int(size * 0.1)]:
ey = cy - int(size * 0.05)
r = max(4, size // 40)
draw.ellipse([ex - r, ey - r, ex + r, ey + r], fill=(40, 30, 20))
# Nose
draw.ellipse([cx - 5, cy + 5, cx + 5, cy + 15], fill=(180, 140, 110))
# Mouth
draw.arc([cx - 20, cy + 25, cx + 20, cy + 45], start=0, end=180, fill=(160, 80, 80), width=3)
return img
def make_random_image(width: int = 224, height: int = 224) -> Image.Image:
rng = np.random.default_rng(42)
return Image.fromarray(rng.integers(0, 256, (height, width, 3), dtype=np.uint8), "RGB")
def _stats(times_s: list[float]) -> dict:
arr = np.array(times_s) * 1000 # ms
return {
"median_ms": float(np.median(arr)),
"mean_ms": float(np.mean(arr)),
"min_ms": float(np.min(arr)),
"p95_ms": float(np.percentile(arr, 95)),
"ips": 1000.0 / float(np.median(arr)),
}
def bench_insightface(n_warmup: int = 5, n_runs: int = 30) -> None:
import cv2
import winnow.embeddings as emb_mod
from winnow.embeddings import get_insightface_app
# Reset singleton so we get a fresh load
emb_mod._insightface_app = None
emb_mod._insightface_loaded = False
print(" Loading model...")
t_load = time.perf_counter()
app = get_insightface_app()
load_s = time.perf_counter() - t_load
if app is None:
print(" SKIP: InsightFace failed to load")
return
img_pil = make_face_image(640)
img_bgr = cv2.cvtColor(np.asarray(img_pil), cv2.COLOR_RGB2BGR)
# Warmup
for _ in range(n_warmup):
app.get(img_bgr)
# Timed — single image 640×640
times: list[float] = []
for _ in range(n_runs):
t0 = time.perf_counter()
app.get(img_bgr)
times.append(time.perf_counter() - t0)
s = _stats(times)
print(f" Model load time : {load_s:.2f} s")
print(" Input size : 640×640")
print(f" Runs : {n_runs} (after {n_warmup} warmup)")
print(f" Median latency : {s['median_ms']:.1f} ms")
print(f" Mean / p95 : {s['mean_ms']:.1f} ms / {s['p95_ms']:.1f} ms")
print(f" Min latency : {s['min_ms']:.1f} ms")
print(f" Throughput : {s['ips']:.1f} images/s")
# Also test at 320×320
img_sm = make_face_image(320)
img_sm_bgr = cv2.cvtColor(np.asarray(img_sm), cv2.COLOR_RGB2BGR)
for _ in range(n_warmup):
app.get(img_sm_bgr)
times_sm: list[float] = []
for _ in range(n_runs):
t0 = time.perf_counter()
app.get(img_sm_bgr)
times_sm.append(time.perf_counter() - t0)
s2 = _stats(times_sm)
print(f" 320×320 median : {s2['median_ms']:.1f} ms ({s2['ips']:.1f} img/s)")
def bench_siglip(
n_warmup: int = 3,
n_runs: int = 20,
batch_sizes: tuple = (1, 4, 8, 16, 32),
) -> None:
import torch
import winnow.embeddings as emb_mod
emb_mod._siglip_model = None
emb_mod._siglip_processor = None
emb_mod._siglip_loaded = False
print(" Loading model...")
t_load = time.perf_counter()
model, processor = emb_mod.get_siglip_model()
load_s = time.perf_counter() - t_load
if model is None:
print(" SKIP: SigLIP failed to load")
return
device = next(model.parameters()).device
print(f" Model load time : {load_s:.2f} s (device: {device})")
print(f" {'Batch':>5} {'ms/batch':>10} {'ms/img':>8} {'img/s':>8} {'p95/img':>9}")
for bs in batch_sizes:
imgs = [make_random_image(224, 224) for _ in range(bs)]
inputs = processor(images=imgs, return_tensors="pt")
inputs = {k: v.to(device) for k, v in inputs.items()}
# Warmup
for _ in range(n_warmup):
with torch.no_grad():
model(**inputs)
if str(device) != "cpu":
torch.cuda.synchronize()
times: list[float] = []
for _ in range(n_runs):
if str(device) != "cpu":
torch.cuda.synchronize()
t0 = time.perf_counter()
with torch.no_grad():
model(**inputs)
if str(device) != "cpu":
torch.cuda.synchronize()
times.append(time.perf_counter() - t0)
s = _stats(times)
print(
f" {bs:>5} {s['median_ms']:>10.1f} {s['median_ms']/bs:>8.2f}"
f" {bs * 1000 / s['median_ms']:>8.1f} {s['p95_ms']/bs:>9.2f}"
)
def main() -> None:
print("=" * 56)
print(" winnow inference benchmark")
print(f" Mode: {_mode_label()}")
print("=" * 56)
print()
print("── InsightFace Buffalo_L (face detection + ArcFace) ──")
bench_insightface()
print()
print("── SigLIP google/siglip-base-patch16-224 (objects) ───")
bench_siglip()
print()
if __name__ == "__main__":
# Add winnow to path when run directly inside container
sys.path.insert(0, "/app")
main()
Generated
+1 -1
View File
@@ -2348,7 +2348,7 @@ wheels = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.4.0" version = "0.4.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "croniter" }, { name = "croniter" },