From 26900d0aeee0f252b3925db8a54a9849bb2cf0ee Mon Sep 17 00:00:00 2001 From: Oleksandr Bezdieniezhnykh Date: Mon, 6 Apr 2026 05:00:08 +0300 Subject: [PATCH] Update Docker configurations and dependencies for Jetson deployment - Added image specifications for services in `docker-compose.demo-jetson.yml` and `docker-compose.jetson.yml` to streamline deployment. - Updated `Dockerfile.gpu` to use the development version of the CUDA runtime for enhanced compatibility. - Modified `Dockerfile.jetson` to switch to a newer JetPack base image and adjusted the requirements file to include additional dependencies for improved functionality. - Removed obsolete deployment scripts and calibration cache generation script to clean up the project structure. Made-with: Cursor --- .dockerignore | 18 +++ Dockerfile.gpu | 2 +- Dockerfile.jetson | 7 +- docker-compose.demo-jetson.yml | 3 + docker-compose.jetson.yml | 4 +- requirements-jetson.txt | 15 +- requirements.txt | 2 +- scripts/deploy_demo_jetson.sh | 143 -------------------- scripts/{ => jetson}/generate_int8_cache.py | 0 scripts/jetson/sample_calibration_images.py | 63 +++++++++ 10 files changed, 103 insertions(+), 154 deletions(-) create mode 100644 .dockerignore delete mode 100755 scripts/deploy_demo_jetson.sh rename scripts/{ => jetson}/generate_int8_cache.py (100%) create mode 100644 scripts/jetson/sample_calibration_images.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dcaa74e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.venv +.venv-e2e +_docs +e2e +tests +data +demo +Logs +build +__pycache__ +*.pyc +*.pyd +*.so +*.egg-info +.pytest_cache +.git +*.md +scripts diff --git a/Dockerfile.gpu b/Dockerfile.gpu index 309c532..47dc0b1 100644 --- a/Dockerfile.gpu +++ b/Dockerfile.gpu @@ -1,4 +1,4 @@ -FROM nvidia/cuda:12.2.0-runtime-ubuntu22.04 +FROM nvidia/cuda:12.2.0-devel-ubuntu22.04 RUN apt-get update && apt-get install -y python3 python3-pip python3-dev gcc libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt requirements-gpu.txt ./ diff --git a/Dockerfile.jetson b/Dockerfile.jetson index ca9f6c2..65209ca 100644 --- a/Dockerfile.jetson +++ b/Dockerfile.jetson @@ -1,17 +1,14 @@ -FROM nvcr.io/nvidia/l4t-base:r36.3.0 +FROM nvcr.io/nvidia/l4t-jetpack:r36.2.0 RUN apt-get update && apt-get install -y \ python3 python3-pip python3-dev gcc \ libgl1 libglib2.0-0 \ python3-libnvinfer python3-libnvinfer-dev \ - python3-pycuda \ && rm -rf /var/lib/apt/lists/* -RUN python3 -c "import tensorrt" || \ - (echo "TensorRT Python bindings not found; check PYTHONPATH for JetPack installation" && exit 1) WORKDIR /app -COPY requirements-jetson.txt ./ +COPY requirements.txt requirements-jetson.txt ./ RUN pip3 install --no-cache-dir -r requirements-jetson.txt COPY . . RUN python3 setup.py build_ext --inplace diff --git a/docker-compose.demo-jetson.yml b/docker-compose.demo-jetson.yml index 5aaefa0..20ae884 100644 --- a/docker-compose.demo-jetson.yml +++ b/docker-compose.demo-jetson.yml @@ -2,6 +2,7 @@ name: detections-demo-jetson services: loader: + image: ${REGISTRY:-docker.azaion.com}/detections-loader-mock:${TAG:-latest} build: context: ./e2e/mocks/loader ports: @@ -12,6 +13,7 @@ services: - demo-net annotations: + image: ${REGISTRY:-docker.azaion.com}/detections-annotations-mock:${TAG:-latest} build: context: ./e2e/mocks/annotations ports: @@ -20,6 +22,7 @@ services: - demo-net detections: + image: ${REGISTRY:-docker.azaion.com}/detections-jetson:${TAG:-latest} build: context: . dockerfile: Dockerfile.jetson diff --git a/docker-compose.jetson.yml b/docker-compose.jetson.yml index 390a57e..060be1e 100644 --- a/docker-compose.jetson.yml +++ b/docker-compose.jetson.yml @@ -2,9 +2,7 @@ name: detections-jetson services: detections: - build: - context: . - dockerfile: Dockerfile.jetson + image: ${REGISTRY:-docker.azaion.com}/detections-jetson:${TAG:-latest} ports: - "8080:8080" runtime: nvidia diff --git a/requirements-jetson.txt b/requirements-jetson.txt index bc04b49..5796f85 100644 --- a/requirements-jetson.txt +++ b/requirements-jetson.txt @@ -1 +1,14 @@ --r requirements.txt +fastapi==0.135.2 +uvicorn[standard]==0.42.0 +PyJWT==2.12.1 +h11==0.16.0 +python-multipart==0.0.22 +Cython==3.2.4 +opencv-python==4.10.0.84 +numpy==2.2.6 +pynvml==12.0.0 +requests==2.32.4 +loguru==0.7.3 +av==14.2.0 +xxhash==3.5.0 +pycuda diff --git a/requirements.txt b/requirements.txt index ca41aa5..408a6ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ h11==0.16.0 python-multipart==0.0.22 Cython==3.2.4 opencv-python==4.10.0.84 -numpy==2.3.0 +numpy==2.2.6 onnxruntime==1.22.0 pynvml==12.0.0 requests==2.32.4 diff --git a/scripts/deploy_demo_jetson.sh b/scripts/deploy_demo_jetson.sh deleted file mode 100755 index e76c9b0..0000000 --- a/scripts/deploy_demo_jetson.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/bash -# Deploy Azaion detections demo stack to a Jetson over SSH. -# -# Usage: -# JETSON_HOST=192.168.x.x bash scripts/deploy_demo_jetson.sh \ -# --onnx /local/path/azaion.onnx \ -# --classes /local/path/classes.json \ -# [--int8-cache /local/path/azaion.int8_calib.cache] \ -# [--calibration-images /local/path/images/] -# -# Optional env vars: -# JETSON_HOST (required) IP or hostname of the Jetson -# JETSON_USER SSH user (default: jetson) -# REMOTE_DIR Path on Jetson to deploy into (default: ~/detections) -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" - -JETSON_HOST="${JETSON_HOST:-}" -JETSON_USER="${JETSON_USER:-jetson}" -REMOTE_DIR="${REMOTE_DIR:-~/detections}" - -ONNX_PATH="" -CLASSES_PATH="" -INT8_CACHE_PATH="" -CALIBRATION_IMAGES="" - -usage() { - echo "Usage: JETSON_HOST= bash $0 --onnx --classes [options]" - echo "" - echo "Required:" - echo " --onnx Local path to azaion.onnx" - echo " --classes Local path to classes.json" - echo "" - echo "Optional:" - echo " --int8-cache Local path to azaion.int8_calib.cache (skips calibration)" - echo " --calibration-images Local image directory; rsync to Jetson and run INT8 calibration" - echo " --help Show this message" - echo "" - echo "Env vars:" - echo " JETSON_HOST (required) Jetson IP or hostname" - echo " JETSON_USER SSH user (default: jetson)" - echo " REMOTE_DIR Deploy directory on Jetson (default: ~/detections)" - exit 0 -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --onnx) ONNX_PATH="$2"; shift 2 ;; - --classes) CLASSES_PATH="$2"; shift 2 ;; - --int8-cache) INT8_CACHE_PATH="$2"; shift 2 ;; - --calibration-images) CALIBRATION_IMAGES="$2"; shift 2 ;; - --help) usage ;; - *) echo "Unknown argument: $1"; usage ;; - esac -done - -[[ -z "$JETSON_HOST" ]] && { echo "ERROR: JETSON_HOST is required"; exit 1; } -[[ -z "$ONNX_PATH" ]] && { echo "ERROR: --onnx is required"; exit 1; } -[[ -z "$CLASSES_PATH" ]] && { echo "ERROR: --classes is required"; exit 1; } -[[ -f "$ONNX_PATH" ]] || { echo "ERROR: ONNX file not found: $ONNX_PATH"; exit 1; } -[[ -f "$CLASSES_PATH" ]] || { echo "ERROR: classes.json not found: $CLASSES_PATH"; exit 1; } - -SSH="ssh ${JETSON_USER}@${JETSON_HOST}" -SCP="scp" - -echo "=== Azaion Demo — Jetson Deployment ===" -echo " Host: ${JETSON_USER}@${JETSON_HOST}" -echo " Remote dir: ${REMOTE_DIR}" -echo "" - -# ── 1. Sync project ───────────────────────────────────────────────────────── -echo "--- Syncing project files ---" -$SSH "mkdir -p ${REMOTE_DIR}/demo/models" -rsync -az --exclude='.git' --exclude='__pycache__' --exclude='*.pyc' \ - --exclude='*.egg-info' --exclude='.venv' --exclude='demo/models' \ - "${PROJECT_ROOT}/" "${JETSON_USER}@${JETSON_HOST}:${REMOTE_DIR}/" - -# ── 2. Upload model artifacts ──────────────────────────────────────────────── -echo "--- Uploading model artifacts ---" -$SCP "$ONNX_PATH" "${JETSON_USER}@${JETSON_HOST}:${REMOTE_DIR}/demo/models/azaion.onnx" -$SCP "$CLASSES_PATH" "${JETSON_USER}@${JETSON_HOST}:${REMOTE_DIR}/demo/models/classes.json" - -if [[ -n "$INT8_CACHE_PATH" ]]; then - [[ -f "$INT8_CACHE_PATH" ]] || { echo "ERROR: INT8 cache not found: $INT8_CACHE_PATH"; exit 1; } - echo "--- Uploading INT8 calibration cache ---" - $SCP "$INT8_CACHE_PATH" "${JETSON_USER}@${JETSON_HOST}:${REMOTE_DIR}/demo/models/azaion.int8_calib.cache" -fi - -# ── 3. Optional: run INT8 calibration on Jetson ────────────────────────────── -if [[ -n "$CALIBRATION_IMAGES" ]] && [[ -z "$INT8_CACHE_PATH" ]]; then - [[ -d "$CALIBRATION_IMAGES" ]] || { echo "ERROR: calibration images dir not found: $CALIBRATION_IMAGES"; exit 1; } - echo "--- Syncing calibration images to Jetson ---" - $SSH "mkdir -p ${REMOTE_DIR}/demo/calibration" - rsync -az "${CALIBRATION_IMAGES}/" "${JETSON_USER}@${JETSON_HOST}:${REMOTE_DIR}/demo/calibration/" - - echo "--- Building detections image for calibration ---" - $SSH "cd ${REMOTE_DIR} && docker compose -f docker-compose.demo-jetson.yml build detections" - - echo "--- Running INT8 calibration (this takes several minutes) ---" - $SSH "cd ${REMOTE_DIR} && docker compose -f docker-compose.demo-jetson.yml run --rm \ - -v ${REMOTE_DIR}/demo/calibration:/calibration \ - detections \ - python3 scripts/generate_int8_cache.py \ - --images-dir /calibration \ - --onnx /models/azaion.onnx \ - --output /models/azaion.int8_calib.cache" - echo "--- Calibration cache written to ${REMOTE_DIR}/demo/models/azaion.int8_calib.cache ---" -fi - -# ── 4. Start services ──────────────────────────────────────────────────────── -echo "--- Building and starting services ---" -$SSH "cd ${REMOTE_DIR} && docker compose -f docker-compose.demo-jetson.yml up -d --build" - -# ── 5. Health check ─────────────────────────────────────────────────────────── -echo "--- Health check ---" -HEALTH_URL="http://${JETSON_HOST}:8080/health" -MAX_RETRIES=15 -RETRY_INTERVAL=5 - -for i in $(seq 1 $MAX_RETRIES); do - STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL" 2>/dev/null || true) - if [[ "$STATUS" == "200" ]]; then - echo "" - echo "=== Demo is live at http://${JETSON_HOST}:8080 ===" - echo "" - echo "Endpoints:" - echo " POST http://${JETSON_HOST}:8080/detect/image" - echo " POST http://${JETSON_HOST}:8080/detect/video" - echo " GET http://${JETSON_HOST}:8080/health" - echo "" - echo "Note: On first start the service converts azaion.onnx to a TRT engine." - echo " Check /health until AI status shows 'enabled'." - exit 0 - fi - echo " Waiting for service… (${i}/${MAX_RETRIES}, HTTP ${STATUS})" - sleep "$RETRY_INTERVAL" -done - -echo "ERROR: Health check failed after $((MAX_RETRIES * RETRY_INTERVAL))s" -echo "Check logs with: ssh ${JETSON_USER}@${JETSON_HOST} \"cd ${REMOTE_DIR} && docker compose -f docker-compose.demo-jetson.yml logs detections\"" -exit 1 diff --git a/scripts/generate_int8_cache.py b/scripts/jetson/generate_int8_cache.py similarity index 100% rename from scripts/generate_int8_cache.py rename to scripts/jetson/generate_int8_cache.py diff --git a/scripts/jetson/sample_calibration_images.py b/scripts/jetson/sample_calibration_images.py new file mode 100644 index 0000000..6e2f6d7 --- /dev/null +++ b/scripts/jetson/sample_calibration_images.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +""" +Sample a random subset of images from a YOLO dataset for INT8 calibration. + +Run locally (on your dev machine) before deploying to Jetson: + + python3 scripts/jetson/sample_calibration_images.py \ + --dataset /path/to/dataset-2025-05-22 \ + --output /tmp/calibration \ + --num-samples 500 + +The output directory can then be passed directly to deploy_demo_jetson.sh +via --calibration-images, or to generate_int8_cache.py via --images-dir. +""" +import argparse +import random +import shutil +import sys +from pathlib import Path + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--dataset", required=True, help="Root of the YOLO dataset (must contain images/)") + parser.add_argument("--output", required=True, help="Destination directory for sampled images") + parser.add_argument("--num-samples", type=int, default=500) + parser.add_argument("--seed", type=int, default=42) + return parser.parse_args() + + +def collect_images(dataset_root: Path) -> list[Path]: + images_dir = dataset_root / "images" + if not images_dir.is_dir(): + print(f"ERROR: {images_dir} not found", file=sys.stderr) + sys.exit(1) + images: list[Path] = [] + for pattern in ("**/*.jpg", "**/*.jpeg", "**/*.png"): + images += sorted(images_dir.glob(pattern)) + return images + + +def main(): + args = parse_args() + dataset_root = Path(args.dataset) + output_dir = Path(args.output) + + images = collect_images(dataset_root) + if not images: + print(f"ERROR: no images found in {dataset_root / 'images'}", file=sys.stderr) + sys.exit(1) + + rng = random.Random(args.seed) + sample = rng.sample(images, min(args.num_samples, len(images))) + + output_dir.mkdir(parents=True, exist_ok=True) + for src in sample: + shutil.copy2(src, output_dir / src.name) + + print(f"Sampled {len(sample)} images → {output_dir}") + + +if __name__ == "__main__": + main()