# 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()