# app.py — FastAPI backend with safe model offloading, sliding-window rate limiting, and async history persistence import asyncio import json import logging import os import threading import time from contextlib import asynccontextmanager from pathlib import Path from typing import Any, Dict, List, Optional from fastapi import FastAPI, HTTPException, Request from fastapi.responses import FileResponse, HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from peft import PeftModel from pydantic import BaseModel, Field import torch from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig # ----- Configuration & Paths ----- logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" ) logger = logging.getLogger("colabmind") BASE_MODEL = "deepseek-ai/deepseek-coder-6.7b-base" ADAPTER_REPO = "Agasthya0/colabmind-coder-6.7b-ml-qlora" ROOT = Path(__file__).resolve().parent STATIC_DIR = ROOT / "static" STATIC_DIR.mkdir(parents=True, exist_ok=True) HISTORY_PATH = ROOT / "history.jsonl" OFFLOAD_DIR = Path("/tmp/llm_offload") OFFLOAD_DIR.mkdir(parents=True, exist_ok=True) # Global holder for loaded ML components ml_resources: Dict[str, Any] = {} # ----- Rate Limiter (Thread-safe sliding window per IP) ----- RATE_WINDOW = 60 # Seconds MAX_REQUESTS = 12 # Requests allowed per window _rate_lock = threading.Lock() _client_requests: Dict[str, List[float]] = {} def check_rate_limit(client_ip: str) -> bool: now = time.time() with _rate_lock: timestamps = _client_requests.setdefault(client_ip, []) # Drop timestamps outside the active window while timestamps and timestamps[0] <= now - RATE_WINDOW: timestamps.pop(0) if len(timestamps) >= MAX_REQUESTS: return False timestamps.append(now) return True # ----- History Persistence ----- _history_lock = threading.Lock() def append_history(prompt: str, code: str) -> None: entry = { "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "prompt": prompt, "code": code, } try: with _history_lock: with open(HISTORY_PATH, "a", encoding="utf-8") as f: f.write(json.dumps(entry, ensure_ascii=False) + "\n") except Exception as e: logger.warning("Failed to record query to history: %s", e) # ----- Model Execution ----- def run_generation(prompt: str, max_new_tokens: int, temperature: float, top_p: float) -> str: tokenizer = ml_resources["tokenizer"] model = ml_resources["model"] if not prompt or not prompt.strip(): return "⚠️ Empty prompt" # Multi-device/CPU/Disk offload handling: target first model layer's device target_device = model.device if hasattr(model, "hf_device_map") and "" in model.hf_device_map: target_device = model.hf_device_map[""] elif torch.cuda.is_available(): target_device = "cuda:0" inputs = tokenizer( prompt, return_tensors="pt", truncation=True, max_length=2048 ).to(target_device) # Hugging Face enforces do_sample=False when temperature == 0.0 do_sample = temperature > 0.0 generate_kwargs = { "max_new_tokens": max_new_tokens, "eos_token_id": tokenizer.eos_token_id, "pad_token_id": tokenizer.pad_token_id or tokenizer.eos_token_id, "do_sample": do_sample, } if do_sample: generate_kwargs["temperature"] = temperature generate_kwargs["top_p"] = top_p with torch.no_grad(): output_ids = model.generate(**inputs, **generate_kwargs) # Strip prompt tokens from output if present prompt_len = inputs["input_ids"].shape[-1] response_tokens = output_ids[0][prompt_len:] output_text = tokenizer.decode(response_tokens, skip_special_tokens=True) if torch.cuda.is_available(): torch.cuda.empty_cache() return output_text.strip() # ----- Lifespan for Clean Startup/Shutdown ----- @asynccontextmanager async def lifespan(app: FastAPI): logger.info("Initializing 4-bit BitsAndBytes quantization configuration...") bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=torch.float16, llm_int8_enable_fp32_cpu_offload=True, ) logger.info("Loading tokenizer from %s...", BASE_MODEL) tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True) if tokenizer.pad_token is None: tokenizer.pad_token = tokenizer.eos_token logger.info("Loading base model with auto-dispatch and disk offload...") model_base = AutoModelForCausalLM.from_pretrained( BASE_MODEL, quantization_config=bnb_config, device_map="auto", offload_folder=str(OFFLOAD_DIR), trust_remote_code=True, ) logger.info("Attaching LoRA adapter from %s...", ADAPTER_REPO) # Explicitly pass offload_folder to prevent accelerate dispatch crashes on disk-offloaded layers model = PeftModel.from_pretrained( model_base, ADAPTER_REPO, device_map="auto", offload_folder=str(OFFLOAD_DIR), ) model.eval() ml_resources["tokenizer"] = tokenizer ml_resources["model"] = model logger.info("Model pipeline ready for inference.") yield ml_resources.clear() if torch.cuda.is_available(): torch.cuda.empty_cache() logger.info("Model unloaded from memory.") # ----- Application & Routes ----- app = FastAPI(title="ColabMind Coder API", lifespan=lifespan) app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") class PredictRequest(BaseModel): data: List[Any] = Field(..., min_length=1, description="Expects [prompt, max_tokens, temp, top_p]") class HistoryItem(BaseModel): prompt: Optional[str] = "" code: Optional[str] = "" def _parse_generation_args(data: List[Any]): prompt = str(data[0]) if len(data) > 0 and data[0] is not None else "" max_tokens = int(data[1]) if len(data) > 1 and data[1] else 512 temperature = float(data[2]) if len(data) > 2 and data[2] is not None else 0.2 top_p = float(data[3]) if len(data) > 3 and data[3] is not None else 0.9 max_tokens = max(16, min(2048, max_tokens)) temperature = max(0.0, min(2.0, temperature)) top_p = max(0.0, min(1.0, top_p)) return prompt, max_tokens, temperature, top_p @app.get("/", response_class=HTMLResponse) async def homepage(): index_file = ROOT / "index.html" if index_file.exists(): return FileResponse(str(index_file)) return HTMLResponse("

Upload index.html to the application root directory.

", status_code=404) @app.post("/run/predict") async def run_predict(req: PredictRequest, request: Request): client_ip = request.client.host if request.client else "unknown" if not check_rate_limit(client_ip): raise HTTPException(status_code=429, detail="Too many requests — please slow down.") try: prompt, max_tokens, temperature, top_p = _parse_generation_args(req.data) logger.info("Prediction requested | Length: %d | Client: %s", len(prompt), client_ip) # Offload heavy synchronous PyTorch call to background thread pool output = await asyncio.to_thread( run_generation, prompt, max_tokens, temperature, top_p ) # Non-blocking history record asyncio.create_task(asyncio.to_thread(append_history, prompt, output)) return JSONResponse({"data": [output]}) except Exception as e: logger.exception("Inference failed unexpectedly") raise HTTPException(status_code=500, detail=str(e)) @app.post("/run/explain") async def run_explain(req: PredictRequest, request: Request): client_ip = request.client.host if request.client else "unknown" if not check_rate_limit(client_ip): raise HTTPException(status_code=429, detail="Too many requests — please slow down.") try: raw_code, max_tokens, temperature, top_p = _parse_generation_args(req.data) prompt = ( "### Instruction:\n" "Explain the following Python code in detail, line-by-line, and highlight potential bugs or improvements.\n\n" f"### Code:\n{raw_code}\n\n" "### Explanation:\n" ) output = await asyncio.to_thread( run_generation, prompt, max_tokens, temperature, top_p ) return JSONResponse({"data": [output]}) except Exception as e: logger.exception("Code explanation run failed") raise HTTPException(status_code=500, detail=str(e)) @app.post("/run/history_append") async def route_history_append(payload: HistoryItem): try: await asyncio.to_thread(append_history, payload.prompt or "", payload.code or "") return JSONResponse({"ok": True}) except Exception as e: logger.exception("Manual history append failed") raise HTTPException(status_code=500, detail=str(e)) @app.post("/run/history_clear") async def route_history_clear(): try: with _history_lock: if HISTORY_PATH.exists(): HISTORY_PATH.unlink() return JSONResponse({"ok": True}) except Exception as e: logger.exception("History clear operation failed") raise HTTPException(status_code=500, detail=str(e)) @app.get("/health") async def health(): model = ml_resources.get("model") if model is None: return {"status": "starting", "device": "uninitialized"} device_info = getattr(model, "hf_device_map", str(getattr(model, "device", "unknown"))) return {"status": "ok", "device_distribution": device_info}