Ass3 / server.py
launch-calcium's picture
Update server.py
573e34c verified
Raw
History Blame Contribute Delete
6.14 kB
# server.py (robust: try ONNX if available, fallback to diffusers)
import os
import time
import base64
from io import BytesIO
from pathlib import Path
from fastapi import FastAPI
from pydantic import BaseModel
import numpy as np
from PIL import Image
import torch
# optional imports; may fail if packages not installed but we'll handle that
try:
import onnxruntime as ort
ONNX_AVAILABLE = True
except Exception:
ONNX_AVAILABLE = False
from transformers import CLIPTokenizer
from diffusers import LMSDiscreteScheduler, StableDiffusionPipeline
from huggingface_hub import snapshot_download
app = FastAPI()
MODEL_DIR = Path(os.environ.get("MODEL_DIR", "onnx_models_quant"))
HF_ONNX_REPO = os.environ.get("HF_ONNX_REPO") # optional HF repo id to download ONNX artifacts
HF_TOKEN = os.environ.get("HF_TOKEN") # optional token for private repo
INTRA_THREADS = int(os.environ.get("INTRA_THREADS", "4"))
# globals
use_onnx = False
onnx_sessions = {}
tokenizer = None
diffusers_pipe = None
scheduler = LMSDiscreteScheduler(beta_start=0.00085, beta_end=0.012, beta_schedule="scaled_linear", num_train_timesteps=1000)
def download_onnx_if_needed():
if MODEL_DIR.exists() and any((MODEL_DIR / f).exists() for f in ("text_encoder.onnx", "unet.onnx", "vae_decoder.onnx")):
return True
if not HF_ONNX_REPO:
return False
# attempt to download snapshot into MODEL_DIR
try:
snapshot_download(repo_id=HF_ONNX_REPO, cache_dir=str(MODEL_DIR), token=HF_TOKEN)
return MODEL_DIR.exists()
except Exception as e:
print("Failed to download ONNX artifacts from HF Hub:", e)
return False
def init_onnx():
global onnx_sessions, tokenizer, use_onnx
if not ONNX_AVAILABLE:
print("onnxruntime not available; skipping ONNX init")
use_onnx = False
return
# Ensure files exist or try download
ok = download_onnx_if_needed()
if not ok:
print(f"ONNX models not found in {MODEL_DIR} and no HF_ONNX_REPO configured; skipping ONNX")
use_onnx = False
return
# load tokenizer from model dir if available else fallback
try:
tokenizer = CLIPTokenizer.from_pretrained(str(MODEL_DIR))
except Exception:
tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14")
sess_opts = ort.SessionOptions()
sess_opts.intra_op_num_threads = INTRA_THREADS
sess_opts.inter_op_num_threads = 1
sess_opts.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
onnx_sessions["text"] = ort.InferenceSession(str(MODEL_DIR / "text_encoder.onnx"), sess_opts, providers=["CPUExecutionProvider"])
onnx_sessions["unet"] = ort.InferenceSession(str(MODEL_DIR / "unet.onnx"), sess_opts, providers=["CPUExecutionProvider"])
onnx_sessions["vae"] = ort.InferenceSession(str(MODEL_DIR / "vae_decoder.onnx"), sess_opts, providers=["CPUExecutionProvider"])
use_onnx = True
print("Loaded ONNX sessions from", MODEL_DIR)
def init_diffusers():
global diffusers_pipe, tokenizer
if diffusers_pipe is None:
model_id = os.environ.get("DIFFUSERS_MODEL", "runwayml/stable-diffusion-v1-5")
print("Loading diffusers pipeline:", model_id)
diffusers_pipe = StableDiffusionPipeline.from_pretrained(model_id)
device = "cuda" if torch.cuda.is_available() else "cpu"
diffusers_pipe.to(device)
tokenizer = diffusers_pipe.tokenizer
# Initialize best available backend
init_onnx()
if not use_onnx:
init_diffusers()
class Prompt(BaseModel):
prompt: str
steps: int = 20
scale: float = 7.5
# ONNX helpers
def run_text_encoder_onnx(prompt: str):
toks = tokenizer(prompt, return_tensors="pt", padding="max_length", max_length=77, truncation=True)
input_ids = toks["input_ids"].cpu().numpy()
attention_mask = toks["attention_mask"].cpu().numpy()
ort_inputs = {"input_ids": input_ids, "attention_mask": attention_mask}
out = onnx_sessions["text"].run(None, ort_inputs)
return out[0].astype(np.float32)
def unet_predict_onnx(latent: np.ndarray, t: int, encoder_hidden_states: np.ndarray):
ort_inputs = {
"latent": latent.astype(np.float32),
"timestep": np.array([int(t)], dtype=np.int64),
"encoder_hidden_states": encoder_hidden_states.astype(np.float32)
}
out = onnx_sessions["unet"].run(None, ort_inputs)
return out[0].astype(np.float32)
def decode_vae_onnx(latents: np.ndarray):
out = onnx_sessions["vae"].run(None, {"latent": latents.astype(np.float32)})
return out[0].astype(np.float32)
# Generation endpoints use either ONNX or diffusers
@app.post("/generate")
def generate(p: Prompt):
start = time.time()
steps = int(p.steps)
if use_onnx:
scheduler.set_timesteps(steps)
encoder_hidden_states = run_text_encoder_onnx(p.prompt)
latents = np.random.randn(1, 4, 64, 64).astype(np.float32)
for t in scheduler.timesteps:
noise_pred = unet_predict_onnx(latents, int(t), encoder_hidden_states)
noise_pred_t = torch.from_numpy(noise_pred)
latents_t = torch.from_numpy(latents)
step_output = scheduler.step(noise_pred_t, t, latents_t, return_dict=False)
latents = step_output[0].numpy()
images = decode_vae_onnx(latents)
image = images[0]
image = (np.clip(image, -1, 1) + 1.0) / 2.0
image = (image * 255).round().astype(np.uint8)
image = np.transpose(image, (1, 2, 0))
pil = Image.fromarray(image)
elapsed = time.time() - start
return {"image_base64": base64.b64encode(pil_to_bytes(pil)).decode("utf-8"), "elapsed_s": elapsed, "mode": "onnx"}
else:
# diffusers fallback
out = diffusers_pipe(p.prompt, num_inference_steps=steps, guidance_scale=float(p.scale))
pil = out.images[0]
elapsed = time.time() - start
return {"image_base64": base64.b64encode(pil_to_bytes(pil)).decode("utf-8"), "elapsed_s": elapsed, "mode": "diffusers"}
def pil_to_bytes(pil):
buf = BytesIO()
pil.save(buf, format="PNG")
return buf.getvalue()