chess-assistant / app.py
unknown
Deploy Maia3 Chess Prediction API - Chessformer 3M inference server
afd6ba9
Raw
History Blame Contribute Delete
3.65 kB
import os
import logging
from contextlib import asynccontextmanager
import torch
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from maia3_pkg.inference import build_model, predict_moves
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
model = None
model_cfg = None
class PredictRequest(BaseModel):
fen: str = Field(..., description="FEN string of the chess position")
self_elo: int = Field(1500, ge=0, le=5000, description="Side-to-move Elo rating")
oppo_elo: int = Field(1500, ge=0, le=5000, description="Opponent Elo rating")
temperature: float = Field(1.0, ge=0.0, le=5.0, description="Sampling temperature (0 = argmax)")
top_p: float = Field(1.0, ge=0.0, le=1.0, description="Nucleus sampling threshold")
multipv: int = Field(5, ge=1, le=50, description="Number of candidate moves to return")
class PredictResponse(BaseModel):
fen: str
turn: str
self_elo: int
oppo_elo: int
top_moves: list[dict]
wdl: dict
class HealthResponse(BaseModel):
status: str
device: str
model_loaded: bool
@asynccontextmanager
async def lifespan(app: FastAPI):
global model, model_cfg
logger.info("Starting Maia3 inference server...")
try:
ckpt_path = os.environ.get(
"MAIA3_CHECKPOINT",
"/app/maia3-3m.pt",
)
device = os.environ.get("MAIA3_DEVICE", "cuda" if torch.cuda.is_available() else "cpu")
use_amp = os.environ.get("MAIA3_USE_AMP", "1" if device.startswith("cuda") else "0") == "1"
logger.info(f"Loading model from {ckpt_path} on {device} (amp={use_amp})...")
model, model_cfg = build_model(ckpt_path, device=device, use_amp=use_amp)
logger.info("Model loaded successfully")
except Exception as e:
logger.error(f"Failed to load model: {e}")
model = None
model_cfg = None
yield
app = FastAPI(
title="Maia3 Chess Prediction API",
description="Human-like chess move prediction using Maia3-3M (Chessformer)",
version="1.0.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health", response_model=HealthResponse)
async def health():
return HealthResponse(
status="ok" if model is not None else "degraded",
device=str(model_cfg.device) if model_cfg else "unknown",
model_loaded=model is not None,
)
@app.post("/predict", response_model=PredictResponse)
async def predict(req: PredictRequest):
if model is None or model_cfg is None:
raise HTTPException(status_code=503, detail="Model not loaded")
try:
result = predict_moves(
model=model,
cfg=model_cfg,
fen=req.fen,
self_elo=req.self_elo,
oppo_elo=req.oppo_elo,
temperature=req.temperature,
top_p=req.top_p,
multipv=req.multipv,
)
return PredictResponse(**result)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.exception("Prediction failed")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/")
async def root():
return {
"service": "Maia3 Chess Prediction",
"model": "Maia3-ablate-3M (Chessformer, 3M params)",
"endpoints": {
"health": "GET /health",
"predict": "POST /predict",
"docs": "GET /docs",
},
}