motif64

m64_5a4e4d2fbe6f / sana_server.py

141 lines, 4.3 KBraw
1"""OpenAI-style /v1/images/generations server for SANA 1.5 1.6B (m64 capsule).23Ported from an earlier standalone SANA server. Differences from the original:4- weights load from /weights/sana (an m64 artifact mount), port from M64_PORT;5- size, steps and guidance are baked constants (the capsule's [params]); a request carries6  only `prompt` and `seed` (the m64 front rejects anything else with a 400);7- progress is reported as JSON lines appended to M64_STATUS.8"""910import base6411import io12import json13import os14import secrets15import threading16import time17import traceback18from typing import Optional1920import torch21import uvicorn22from fastapi import FastAPI, HTTPException23from fastapi.responses import JSONResponse24from pydantic import BaseModel, Field2526SERVED_MODEL_ID = "sana-1.5-1.6b"27WEIGHTS = "/weights/sana"28PORT = int(os.environ["M64_PORT"])29STATUS = os.environ["M64_STATUS"]3031# Baked generation settings. Keep in sync with [params] in capsule.toml.32SIZE = "1024x1024"33NUM_INFERENCE_STEPS = 2034GUIDANCE_SCALE = 4.535WIDTH, HEIGHT = (int(x) for x in SIZE.split("x"))36MAX_SEED = 2**63 - 13738app = FastAPI()39pipe = None40lock = threading.Lock()  # one generation at a time on the single pipeline414243def status(phase: str, **extra) -> None:44    with open(STATUS, "a") as f:45        f.write(json.dumps({"phase": phase, **extra}) + "\n")464748def load_model() -> None:49    global pipe50    from diffusers import SanaPipeline5152    status("loading", progress=0.0, detail="loading SANA 1.5 1.6B (bf16)")53    print("Loading SANA 1.5 1.6B...", flush=True)54    t0 = time.time()55    pipe = SanaPipeline.from_pretrained(WEIGHTS, torch_dtype=torch.bfloat16)56    pipe.to("cuda")57    pipe.text_encoder.to(torch.bfloat16)58    print(f"Model loaded in {time.time() - t0:.1f}s.", flush=True)5960    status("warming", detail="one short warm-up generation")61    t0 = time.time()62    with lock:63        pipe(prompt="warm-up", width=WIDTH, height=HEIGHT, num_inference_steps=2,64             guidance_scale=GUIDANCE_SCALE, generator=torch.Generator("cuda").manual_seed(0))65    print(f"Warm-up done in {time.time() - t0:.1f}s.", flush=True)666768class ImageRequest(BaseModel):69    prompt: str = Field(..., min_length=1, description="Text prompt")70    seed: Optional[int] = Field(None, ge=0, le=MAX_SEED, description="RNG seed. null = random; the one used is returned.")717273def baked() -> dict:74    return {"size": SIZE, "num_inference_steps": NUM_INFERENCE_STEPS, "guidance_scale": GUIDANCE_SCALE}757677@app.get("/health")78def health():79    if pipe is None:80        raise HTTPException(503, "Model not loaded")81    return {"status": "ok", "model": SERVED_MODEL_ID}828384@app.get("/v1/config")85def config():86    return {87        "model": SERVED_MODEL_ID,88        "params": baked(),89        "request_fields": {90            "prompt": {"required": True, "description": "Text prompt"},91            "seed": {"required": False, "default": None, "description": "RNG seed; null = random"},92        },93    }949596@app.post("/v1/images/generations")97def generate(req: ImageRequest):98    if pipe is None:99        raise HTTPException(503, "Model not loaded")100    seed = req.seed if req.seed is not None else secrets.randbelow(2**31)101    t0 = time.time()102    with lock:103        gen = torch.Generator("cuda").manual_seed(seed)104        images = pipe(105            prompt=req.prompt,106            width=WIDTH, height=HEIGHT,107            num_inference_steps=NUM_INFERENCE_STEPS,108            guidance_scale=GUIDANCE_SCALE,109            num_images_per_prompt=1,110            generator=gen,111        )[0]112    elapsed = time.time() - t0113    print(f"Generated {len(images)} image(s) in {elapsed:.1f}s (seed {seed})", flush=True)114    data = []115    for img in images:116        buf = io.BytesIO()117        img.save(buf, format="PNG")118        data.append({"b64_json": base64.b64encode(buf.getvalue()).decode()})119    return JSONResponse({120        "created": int(time.time()),121        "model": SERVED_MODEL_ID,122        "seed": seed,123        "elapsed_seconds": round(elapsed, 2),124        "data": data,125        "params_used": {**baked(), "seed": seed},126    })127128129def main() -> None:130    try:131        load_model()132    except Exception:133        traceback.print_exc()134        status("error", detail="model load failed; see container logs")135        raise136    status("ready")137    uvicorn.run(app, host="0.0.0.0", port=PORT)  # the gate reaches us over the capsule network138139140if __name__ == "__main__":141    main()