From 98ee7dffe2690520a63e2df7d8c4d0c32ba00ee4 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Thu, 14 May 2026 09:42:07 +0900 Subject: [PATCH] =?UTF-8?q?ops(gpu-health):=20GPU=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20health/smoke=20=ED=91=9C=EC=A4=80=ED=99=94=20+=20sy?= =?UTF-8?q?nthetic=20VRAM=20=ED=94=BC=ED=81=AC=20=EA=B0=80=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-GPU-Health-1. 운영 준비성 표준화 PR (모델 성능 개선 아님). - OCR /smoke endpoint 추가 (160x60 OK PNG in-memory, 200/503 분기, Docker healthcheck 미사용) - marker /health endpoint 추가 (stt/ocr 동일 시그니처) - reranker docker-compose healthcheck 추가 (TEI :80/health) - scripts/gpu_service_smoke.sh: docker exec 표준 점검 (OCR/STT expose-only) - scripts/gpu_vram_fixture.sh: Mode A sequential + Mode B light overlap + --stress 옵션 - tests/load/fixtures/: synthetic ocr_ok.png / sine_30s.wav / lorem_1p.pdf OCR 빈 응답 false negative — root cause: ports 미매핑. 결정: ocr-service / stt-service 는 expose-only 유지, 운영 점검은 docker exec 내부 curl 표준. Co-Authored-By: Claude Opus 4.7 (1M context) --- docker-compose.yml | 6 ++ scripts/gpu_service_smoke.sh | 77 ++++++++++++++++ scripts/gpu_vram_fixture.sh | 150 +++++++++++++++++++++++++++++++ services/marker/server.py | 5 ++ services/ocr/server.py | 29 +++++- tests/load/fixtures/lorem_1p.pdf | Bin 0 -> 996 bytes tests/load/fixtures/ocr_ok.png | Bin 0 -> 518 bytes tests/load/fixtures/sine_30s.wav | Bin 0 -> 960044 bytes 8 files changed, 266 insertions(+), 1 deletion(-) create mode 100755 scripts/gpu_service_smoke.sh create mode 100755 scripts/gpu_vram_fixture.sh create mode 100644 tests/load/fixtures/lorem_1p.pdf create mode 100644 tests/load/fixtures/ocr_ok.png create mode 100644 tests/load/fixtures/sine_30s.wav diff --git a/docker-compose.yml b/docker-compose.yml index 40612b1..14af257 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -149,6 +149,12 @@ services: - driver: nvidia count: 1 capabilities: [gpu] + healthcheck: + test: ["CMD", "curl", "-fsS", "http://localhost/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 120s restart: unless-stopped ai-gateway: diff --git a/scripts/gpu_service_smoke.sh b/scripts/gpu_service_smoke.sh new file mode 100755 index 0000000..27de2db --- /dev/null +++ b/scripts/gpu_service_smoke.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# GPU 미디어/검색 서비스 health/ready/smoke 점검 (PR-GPU-Health-1). +# OCR/STT/reranker 는 expose-only 라 docker exec 내부 curl 표준 경로 사용. +# marker 는 ports 매핑이 있지만 일관성을 위해 동일 패턴. +set -uo pipefail + +OCR=hyungi_document_server-ocr-service-1 +MARKER=hyungi_document_server-marker-service-1 +RERANKER=hyungi_document_server-reranker-1 +STT=hyungi_document_server-stt-service-1 + +PASS=0 +FAIL=0 + +vram() { + nvidia-smi --query-gpu=memory.used,memory.free --format=csv,noheader,nounits 2>/dev/null \ + | awk -F',' '{printf "used=%dMiB free=%dMiB\n", $1, $2}' +} + +probe() { + local label="$1" container="$2" path="$3" timeout="${4:-5}" + printf " %-22s " "$label" + if out=$(docker exec "$container" curl -fsS -m "$timeout" "$path" 2>&1); then + echo "OK $(echo "$out" | head -c 120)" + PASS=$((PASS+1)) + else + echo "FAIL $(echo "$out" | head -c 120)" + FAIL=$((FAIL+1)) + fi +} + +probe_post() { + local label="$1" container="$2" url="$3" body="$4" timeout="${5:-30}" expect="${6:-}" + printf " %-22s " "$label" + if out=$(docker exec "$container" curl -fsS -m "$timeout" -H 'Content-Type: application/json' -X POST -d "$body" "$url" 2>&1); then + if [[ -z "$expect" || "$out" == *"$expect"* ]]; then + echo "OK $(echo "$out" | head -c 100)" + PASS=$((PASS+1)) + else + echo "FAIL(unexpected body) $(echo "$out" | head -c 100)" + FAIL=$((FAIL+1)) + fi + else + echo "FAIL $(echo "$out" | head -c 100)" + FAIL=$((FAIL+1)) + fi +} + +echo "=== nvidia-smi baseline ===" +BASE=$(vram); echo " $BASE" +echo + +echo "=== health / ready ===" +probe "OCR /health" "$OCR" "http://127.0.0.1:3200/health" 5 +probe "OCR /ready" "$OCR" "http://127.0.0.1:3200/ready" 5 +probe "marker /health" "$MARKER" "http://127.0.0.1:3300/health" 5 +probe "marker /ready" "$MARKER" "http://127.0.0.1:3300/ready" 5 +probe "reranker /health" "$RERANKER" "http://127.0.0.1:80/health" 5 +probe "stt /health" "$STT" "http://127.0.0.1:3300/health" 5 +probe "stt /ready" "$STT" "http://127.0.0.1:3300/ready" 5 + +echo +echo "=== smoke ===" +probe "OCR /smoke" "$OCR" "http://127.0.0.1:3200/smoke" 30 +probe_post "bge-m3 embed" "$OCR" "http://ollama:11434/api/embeddings" '{"model":"bge-m3","prompt":"smoke test"}' 30 '"embedding"' + +echo +echo "=== nvidia-smi after ===" +AFTER=$(vram); echo " $AFTER" +echo +echo " baseline: $BASE" +echo " after : $AFTER" +echo +echo "=== summary ===" +echo " pass=$PASS fail=$FAIL" + +exit $FAIL diff --git a/scripts/gpu_vram_fixture.sh b/scripts/gpu_vram_fixture.sh new file mode 100755 index 0000000..aef8ba5 --- /dev/null +++ b/scripts/gpu_vram_fixture.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +# synthetic fixture 기반 GPU VRAM 피크 검증 (PR-GPU-Health-1). +# Mode A (sequential) + Mode B (light overlap) 기본. --stress 옵션은 5개 동시 (기본 gate 미포함). +set -uo pipefail + +OCR=hyungi_document_server-ocr-service-1 +MARKER=hyungi_document_server-marker-service-1 +RERANKER=hyungi_document_server-reranker-1 +STT=hyungi_document_server-stt-service-1 + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +FIX="$REPO_ROOT/tests/load/fixtures" +REPORT="$REPO_ROOT/reports/vram_fixture_$(date +%F).md" +mkdir -p "$REPO_ROOT/reports" + +STRESS_MODE=0 +[[ "${1:-}" == "--stress" ]] && STRESS_MODE=1 + +vram() { + nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null | head -1 | tr -d ' ' +} + +copy_fixtures() { + docker cp "$FIX/ocr_ok.png" $OCR:/tmp/ocr_ok.png >/dev/null + docker cp "$FIX/lorem_1p.pdf" $MARKER:/tmp/lorem_1p.pdf >/dev/null + docker cp "$FIX/sine_30s.wav" $STT:/tmp/sine_30s.wav >/dev/null +} + +call_ocr() { + docker exec "$OCR" curl -fsS -m 60 -X POST -H 'Content-Type: application/json' \ + -d '{"filePath":"/tmp/ocr_ok.png"}' http://127.0.0.1:3200/ocr >/dev/null +} +call_marker() { + docker exec "$MARKER" curl -fsS -m 180 -X POST -H 'Content-Type: application/json' \ + -d '{"file_path":"/tmp/lorem_1p.pdf"}' http://127.0.0.1:3300/convert >/dev/null +} +call_stt() { + docker exec "$STT" curl -fsS -m 180 -X POST -H 'Content-Type: application/json' \ + -d '{"filePath":"/tmp/sine_30s.wav","langs":["en"],"beamSize":1}' http://127.0.0.1:3300/transcribe >/dev/null +} +call_rerank() { + docker exec "$RERANKER" curl -fsS -m 30 -X POST -H 'Content-Type: application/json' \ + -d '{"query":"smoke","texts":["foo bar baz","alpha beta gamma"]}' http://127.0.0.1:80/rerank >/dev/null +} +call_embed() { + docker exec "$OCR" curl -fsS -m 30 -X POST -H 'Content-Type: application/json' \ + -d '{"model":"bge-m3","prompt":"smoke test"}' http://ollama:11434/api/embeddings >/dev/null +} + +run_named() { + local name="$1"; local fn="$2" + local before=$(vram) + if $fn; then status="OK"; else status="FAIL"; fi + local after=$(vram) + printf "| %s | %s | %s | %s |\n" "$name" "$before" "$after" "$status" >> "$REPORT" + echo " $name before=$before after=$after $status" +} + +run_overlap() { + local label="$1" fn_a="$2" fn_b="$3" + local before=$(vram) + $fn_a & pid_a=$! + $fn_b & pid_b=$! + wait $pid_a && sa="OK" || sa="FAIL" + wait $pid_b && sb="OK" || sb="FAIL" + local after=$(vram) + printf "| %s | %s | %s | %s+%s |\n" "$label" "$before" "$after" "$sa" "$sb" >> "$REPORT" + echo " $label before=$before after=$after $sa+$sb" +} + +run_stress() { + local before=$(vram) + call_ocr & p1=$! + call_marker & p2=$! + call_stt & p3=$! + call_rerank & p4=$! + call_embed & p5=$! + wait $p1 && s1="OK" || s1="FAIL" + wait $p2 && s2="OK" || s2="FAIL" + wait $p3 && s3="OK" || s3="FAIL" + wait $p4 && s4="OK" || s4="FAIL" + wait $p5 && s5="OK" || s5="FAIL" + local after=$(vram) + printf "| stress (5 concurrent) | %s | %s | %s/%s/%s/%s/%s |\n" "$before" "$after" "$s1" "$s2" "$s3" "$s4" "$s5" >> "$REPORT" + echo " stress before=$before after=$after $s1/$s2/$s3/$s4/$s5" +} + +copy_fixtures + +{ + echo "# VRAM fixture report — $(date '+%F %H:%M:%S')" + echo + echo "- baseline used = $(vram) MiB / total = 16376 MiB" + echo "- stress mode: $([[ $STRESS_MODE -eq 1 ]] && echo enabled || echo disabled)" + echo + echo "## Mode A — sequential smoke" + echo + echo "| call | before (MiB) | after (MiB) | status |" + echo "|---|---|---|---|" +} > "$REPORT" + +echo "[mode A] sequential" +run_named "OCR /ocr (ocr_ok.png)" call_ocr +run_named "STT /transcribe (sine30s)" call_stt +run_named "marker /convert (lorem1p)" call_marker +run_named "reranker /rerank" call_rerank +run_named "embed bge-m3" call_embed + +{ + echo + echo "## Mode B — light overlap" + echo + echo "| pair | before (MiB) | after (MiB) | status |" + echo "|---|---|---|---|" +} >> "$REPORT" + +echo "[mode B] light overlap" +run_overlap "OCR + embedding" call_ocr call_embed +run_overlap "marker + reranker" call_marker call_rerank +run_overlap "STT + embedding" call_stt call_embed + +if [[ $STRESS_MODE -eq 1 ]]; then + { + echo + echo "## Stress (--stress) — 5 concurrent" + echo + echo "| call | before (MiB) | after (MiB) | status |" + echo "|---|---|---|---|" + } >> "$REPORT" + echo "[stress] 5 concurrent" + run_stress +fi + +PEAK=$(awk -F'|' '$0 ~ /^\|/ && $5 ~ /(OK|FAIL)/ {gsub(/ /,"",$4); if ($4+0 > max) max=$4+0} END {print max+0}' "$REPORT") +GATE=$([[ $PEAK -gt 0 && $PEAK -lt 14000 ]] && echo PASS || echo FAIL) + +{ + echo + echo "## Summary" + echo + echo "- peak after = $PEAK MiB" + echo "- safety margin (vs 16376 MiB) = $((16376 - PEAK)) MiB" + echo "- gate (peak < 14000 MiB) = $GATE" +} >> "$REPORT" + +echo +echo "report: $REPORT" +echo "peak=$PEAK gate=$GATE" + +[[ "$GATE" == "PASS" ]] && exit 0 || exit 1 diff --git a/services/marker/server.py b/services/marker/server.py index 39e35f9..62ba851 100644 --- a/services/marker/server.py +++ b/services/marker/server.py @@ -100,6 +100,11 @@ class ConvertResponse(BaseModel): images_truncated: bool = False +@app.get("/health") +def health(): + return {"status": "ok", "service": "marker-service"} + + @app.get("/ready") async def ready(response: Response): """Round 4 #1+#2: Response.status_code 명시 + warmup_error 노출.""" diff --git a/services/ocr/server.py b/services/ocr/server.py index b099482..f8bdb5f 100644 --- a/services/ocr/server.py +++ b/services/ocr/server.py @@ -4,13 +4,16 @@ 모델은 첫 요청 시 lazy loading. """ +import asyncio +import time import unicodedata from pathlib import Path import fitz import torch from fastapi import FastAPI -from PIL import Image +from fastapi.responses import JSONResponse +from PIL import Image, ImageDraw app = FastAPI() @@ -82,6 +85,30 @@ def ready(): } +@app.get("/smoke") +async def smoke(): + """OCR 라운드트립이 예외 없이 완료되는지 운영 verify. Docker healthcheck 미사용.""" + start = time.monotonic() + img = Image.new("RGB", (160, 60), color="white") + draw = ImageDraw.Draw(img) + draw.text((30, 20), "OK", fill="black") + try: + loop = asyncio.get_running_loop() + await asyncio.wait_for( + loop.run_in_executor(None, _ocr_image, img), + timeout=20.0, + ) + except asyncio.TimeoutError: + return JSONResponse(status_code=503, content={"status": "degraded", "reason": "timeout"}) + except Exception as exc: + return JSONResponse( + status_code=503, + content={"status": "degraded", "reason": exc.__class__.__name__}, + ) + elapsed_ms = int((time.monotonic() - start) * 1000) + return {"status": "ok", "service": "ocr-service", "inference": "ok", "elapsed_ms": elapsed_ms} + + @app.post("/ocr") async def ocr_endpoint(body: dict): """PDF/이미지 OCR — 페이지 단위 처리 (전체 일괄 로드 금지)""" diff --git a/tests/load/fixtures/lorem_1p.pdf b/tests/load/fixtures/lorem_1p.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b12b68e98065478213e9564797533e19f78c63cb GIT binary patch literal 996 zcmZuwTSyd97!IWigWU*&UgC7w(n4qF?98rPvgXdtkoRp>Og4qtowMuYHdC{+y5@sW zW)DhKq6HcR6$DZCP$Jrc3E>LSA{Bx!B}TrfftiG1=a`qx`eERl|MGqR|DSJ6ZkL$B zWZEdx(95A-$^DYJ$ewIjVHP1B!ayT%`RUwdQ5)B_&MBUADAcg?;~^W5TG`_Q58+88tOC6<1yja$gJ4)5`d(%8KeJK$(G!GOro1a-4+&0kGy{@w*ZO z>Y9&%SQs09wtw>=g9Wl7&+?yI)@)zTqYPbALP#fnPpNVl0UK7HhNZr&wLF(Mo44db z$nwxG;E&^B7CBojF5cpVJj@jw*`iJ0IG(Yat(;4A@eCBKj8)*W>^9cr5(VfWx(pkV QZY;0B1|elK2_+)+8+?mLhX4Qo literal 0 HcmV?d00001 diff --git a/tests/load/fixtures/ocr_ok.png b/tests/load/fixtures/ocr_ok.png new file mode 100644 index 0000000000000000000000000000000000000000..90f01819ece0350a36ffc3da5556547e97a66116 GIT binary patch literal 518 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 literal 0 HcmV?d00001