Upload 14 files
Browse files- .gitattributes +1 -0
- Dockerfile +23 -0
- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-313.pyc +0 -0
- app/__pycache__/main.cpython-313.pyc +0 -0
- app/__pycache__/model.cpython-313.pyc +0 -0
- app/__pycache__/schemas.cpython-313.pyc +0 -0
- app/main.py +35 -0
- app/model.py +44 -0
- app/schemas.py +11 -0
- models/resnet18.onnx +3 -0
- models/resnet18.onnx.data +3 -0
- models/resnet18_quantized.onnx +3 -0
- models/resnet18_temp.onnx +3 -0
- requirements.txt +13 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
models/resnet18.onnx.data filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Create a user to avoid running as root (Required for Hugging Face Spaces)
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
USER user
|
| 6 |
+
ENV PATH="/home/user/.local/bin:${PATH}"
|
| 7 |
+
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
|
| 10 |
+
# Copy requirements and install dependencies
|
| 11 |
+
COPY --chown=user requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir --user -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy the application code and models
|
| 15 |
+
COPY --chown=user app/ ./app/
|
| 16 |
+
COPY --chown=user models/ ./models/
|
| 17 |
+
|
| 18 |
+
# Use port 7860 (Standard for Hugging Face Spaces)
|
| 19 |
+
EXPOSE 7860
|
| 20 |
+
|
| 21 |
+
# Run with multiple workers for higher throughput
|
| 22 |
+
# Note: On Linux/Docker, uvicorn workers work perfectly
|
| 23 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "4", "--log-level", "warning"]
|
app/__init__.py
ADDED
|
File without changes
|
app/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (162 Bytes). View file
|
|
|
app/__pycache__/main.cpython-313.pyc
ADDED
|
Binary file (2.03 kB). View file
|
|
|
app/__pycache__/model.cpython-313.pyc
ADDED
|
Binary file (2.4 kB). View file
|
|
|
app/__pycache__/schemas.cpython-313.pyc
ADDED
|
Binary file (836 Bytes). View file
|
|
|
app/main.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException
|
| 2 |
+
from concurrent.futures import ProcessPoolExecutor
|
| 3 |
+
import asyncio
|
| 4 |
+
from app.model import run_inference
|
| 5 |
+
from app.schemas import PredictionResponse
|
| 6 |
+
|
| 7 |
+
app = FastAPI(title="ResNet-18 Image Classifier", version="1.0.0")
|
| 8 |
+
|
| 9 |
+
# ใช้ 6 Workers ตามจำนวน Physical Cores ของ Ryzen 7500F
|
| 10 |
+
executor = ProcessPoolExecutor(max_workers=6)
|
| 11 |
+
|
| 12 |
+
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
|
| 13 |
+
ALLOWED_CONTENT_TYPES = {"image/jpeg", "image/png", "image/webp", "image/gif"}
|
| 14 |
+
|
| 15 |
+
@app.get("/health")
|
| 16 |
+
async def health():
|
| 17 |
+
return {"status": "ok"}
|
| 18 |
+
|
| 19 |
+
@app.post("/predict", response_model=PredictionResponse)
|
| 20 |
+
async def predict(file: UploadFile = File(...)):
|
| 21 |
+
if file.content_type not in ALLOWED_CONTENT_TYPES:
|
| 22 |
+
raise HTTPException(status_code=415, detail="Unsupported media type")
|
| 23 |
+
|
| 24 |
+
image_bytes = await file.read()
|
| 25 |
+
|
| 26 |
+
if len(image_bytes) > MAX_FILE_SIZE:
|
| 27 |
+
raise HTTPException(status_code=413, detail="File too large")
|
| 28 |
+
|
| 29 |
+
# รัน Inference ใน ProcessPoolExecutor เพื่อกระจายโหลดลง 6 Cores
|
| 30 |
+
loop = asyncio.get_event_loop()
|
| 31 |
+
try:
|
| 32 |
+
result = await loop.run_in_executor(executor, run_inference, image_bytes)
|
| 33 |
+
return result
|
| 34 |
+
except Exception as e:
|
| 35 |
+
raise HTTPException(status_code=500, detail=f"Inference error: {str(e)}")
|
app/model.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import onnxruntime as ort
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
import io
|
| 5 |
+
import time
|
| 6 |
+
from transformers import AutoImageProcessor, ResNetForImageClassification
|
| 7 |
+
|
| 8 |
+
# Load feature extractor
|
| 9 |
+
processor = AutoImageProcessor.from_pretrained("microsoft/resnet-18")
|
| 10 |
+
|
| 11 |
+
# Optimize session for multi-process environment
|
| 12 |
+
sess_options = ort.SessionOptions()
|
| 13 |
+
sess_options.intra_op_num_threads = 1 # One thread per process worker
|
| 14 |
+
sess_options.inter_op_num_threads = 1
|
| 15 |
+
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 16 |
+
|
| 17 |
+
# Load ONNX session
|
| 18 |
+
session = ort.InferenceSession(
|
| 19 |
+
"models/resnet18_quantized.onnx",
|
| 20 |
+
sess_options=sess_options,
|
| 21 |
+
providers=["CPUExecutionProvider"]
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# Load label mapping
|
| 25 |
+
cfg = ResNetForImageClassification.from_pretrained("microsoft/resnet-18").config
|
| 26 |
+
|
| 27 |
+
def run_inference(image_bytes: bytes) -> dict:
|
| 28 |
+
img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
|
| 29 |
+
inputs = processor(images=img, return_tensors="np")
|
| 30 |
+
pixel_values = inputs["pixel_values"].astype(np.float32)
|
| 31 |
+
|
| 32 |
+
t0 = time.perf_counter()
|
| 33 |
+
outputs = session.run(["logits"], {"pixel_values": pixel_values})
|
| 34 |
+
elapsed = (time.perf_counter() - t0) * 1000
|
| 35 |
+
|
| 36 |
+
logits = outputs[0][0]
|
| 37 |
+
predicted_class_id = int(np.argmax(logits))
|
| 38 |
+
|
| 39 |
+
return {
|
| 40 |
+
"label": cfg.id2label[predicted_class_id],
|
| 41 |
+
"score": float(np.exp(logits[predicted_class_id]) / np.sum(np.exp(logits))),
|
| 42 |
+
"label_id": predicted_class_id,
|
| 43 |
+
"inference_time_ms": elapsed
|
| 44 |
+
}
|
app/schemas.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
|
| 3 |
+
class PredictionResponse(BaseModel):
|
| 4 |
+
label: str
|
| 5 |
+
score: float
|
| 6 |
+
label_id: int
|
| 7 |
+
inference_time_ms: float
|
| 8 |
+
|
| 9 |
+
class ErrorResponse(BaseModel):
|
| 10 |
+
detail: str
|
| 11 |
+
error_code: str
|
models/resnet18.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:67045d45797b2be759e40bb17417045699bab836398694cdf7a2b5ff144d7ab6
|
| 3 |
+
size 180730
|
models/resnet18.onnx.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:30b1aa4bdeba9751b30d868ef749ccb8c7fcb1be491e854eeca3f52369a00267
|
| 3 |
+
size 46792704
|
models/resnet18_quantized.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5dc64fbf9cf470ed1db8c391577040069500cce3a1b7139a9194a07a0c98d63d
|
| 3 |
+
size 11828572
|
models/resnet18_temp.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:108be9f23944c1ee1a0c9e97d97ac27550c06eb468a9cf1265cdc4a8a686be43
|
| 3 |
+
size 46916000
|
requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
python-multipart
|
| 4 |
+
onnxruntime
|
| 5 |
+
numpy
|
| 6 |
+
Pillow
|
| 7 |
+
transformers
|
| 8 |
+
torch
|
| 9 |
+
torchvision
|
| 10 |
+
pydantic
|
| 11 |
+
pytest
|
| 12 |
+
httpx
|
| 13 |
+
onnx
|