NeuralAI / services /model_service.py
Subject-Emu-5259's picture
Push NeuralAI project files - training data, scripts, services, knowledge base
38b4eff verified
Raw
History Blame Contribute Delete
7.31 kB
#!/usr/bin/env python3
"""
NeuralAI Model Service
- Loads model once on startup
- Keeps in memory
- Exposes inference API on port 7001
- Handles both sync and streaming responses
"""
import os
import sys
import json
import torch
from pathlib import Path
from flask import Flask, Response, jsonify, request
from datetime import datetime
# CPU optimization
torch.set_num_threads(4)
# Configuration
PORT = int(os.environ.get("MODEL_PORT", "7001"))
MODEL_PATH = os.environ.get("MODEL_PATH", "/home/workspace/Projects/NeuralAI/checkpoints/v2_model")
BASE_MODEL = os.environ.get("BASE_MODEL", "HuggingFaceTB/SmolLM2-360M-Instruct")
app = Flask(__name__)
# Global model state
model = None
tokenizer = None
model_status = "loading"
model_error = None
inference_count = 0
def load_model():
"""Load model once on startup."""
global model, tokenizer, model_status, model_error
print(f"[Model Service] Loading model from {MODEL_PATH}")
print(f"[Model Service] Base model: {BASE_MODEL}")
try:
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
# Load tokenizer
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
tokenizer.pad_token = tokenizer.eos_token
# Check for adapter
adapter_path = Path(MODEL_PATH)
adapter_bin = adapter_path / "adapter_model.bin"
adapter_safetensors = adapter_path / "adapter_model.safetensors"
if adapter_path.exists() and (adapter_bin.exists() or adapter_safetensors.exists()):
print(f"[Model Service] Loading with LoRA adapter...")
base = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
torch_dtype=torch.float32,
device_map=None,
low_cpu_mem_usage=True
)
model = PeftModel.from_pretrained(base, str(adapter_path))
print(f"[Model Service] LoRA adapter loaded!")
else:
print(f"[Model Service] Loading base model only...")
model = AutoModelForCausalLM.from_pretrained(
BASE_MODEL,
torch_dtype=torch.float32,
device_map=None,
low_cpu_mem_usage=True
)
model.eval()
model_status = "ready"
model_error = None
params = sum(p.numel() for p in model.parameters())
print(f"[Model Service] ✓ Model ready! Parameters: {params:,}")
print(f"[Model Service] Listening on port {PORT}")
except Exception as e:
import traceback
model_status = "error"
model_error = str(e)
print(f"[Model Service] ✗ Failed to load model: {e}")
traceback.print_exc()
@app.route("/health", methods=["GET"])
def health():
"""Health check endpoint."""
return jsonify({
"status": model_status,
"error": model_error,
"inference_count": inference_count,
"model": BASE_MODEL,
"port": PORT
})
@app.route("/status", methods=["GET"])
def status():
"""Detailed status endpoint."""
return jsonify({
"status": model_status,
"error": model_error,
"inference_count": inference_count,
"model_loaded": model is not None,
"tokenizer_loaded": tokenizer is not None,
"model_path": MODEL_PATH,
"base_model": BASE_MODEL,
"device": "cpu",
"threads": 4
})
@app.route("/generate", methods=["POST"])
def generate():
"""Generate text response (non-streaming)."""
global inference_count
if model is None or tokenizer is None:
return jsonify({"error": "Model not loaded", "status": model_status}), 503
try:
data = request.get_json()
prompt = data.get("prompt", "")
max_tokens = data.get("max_tokens", 256)
temperature = data.get("temperature", 0.7)
if not prompt:
return jsonify({"error": "No prompt provided"}), 400
# Build full prompt with chat template
if not prompt.startswith("<|im_start|>"):
full_prompt = f"<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"
else:
full_prompt = prompt
# Tokenize
inputs = tokenizer(full_prompt, return_tensors="pt")
# Generate
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_tokens,
do_sample=True,
temperature=temperature,
top_p=0.95,
pad_token_id=tokenizer.eos_token_id
)
# Decode only new tokens
new_tokens = outputs[0][inputs["input_ids"].shape[-1]:]
response = tokenizer.decode(new_tokens, skip_special_tokens=True)
inference_count += 1
return jsonify({
"response": response,
"tokens_generated": len(new_tokens),
"inference_count": inference_count
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/generate/stream", methods=["POST"])
def generate_stream():
"""Generate text response with streaming."""
global inference_count
if model is None or tokenizer is None:
return jsonify({"error": "Model not loaded"}), 503
try:
from transformers import TextIteratorStreamer
import threading
data = request.get_json()
prompt = data.get("prompt", "")
max_tokens = data.get("max_tokens", 256)
temperature = data.get("temperature", 0.7)
if not prompt:
return jsonify({"error": "No prompt provided"}), 400
# Build full prompt
if not prompt.startswith("<|im_start|>"):
full_prompt = f"<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"
else:
full_prompt = prompt
inputs = tokenizer(full_prompt, return_tensors="pt")
# Create streamer
streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
# Run generation in thread
thread = threading.Thread(target=model.generate, kwargs=dict(
**inputs,
streamer=streamer,
max_new_tokens=max_tokens,
do_sample=True,
temperature=temperature,
top_p=0.95,
pad_token_id=tokenizer.eos_token_id
))
thread.start()
def generate():
for token in streamer:
yield f"data: {json.dumps({'token': token})}\n\n"
yield "data: [DONE]\n\n"
inference_count += 1
return Response(
generate(),
mimetype="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
)
except Exception as e:
return jsonify({"error": str(e)}), 500
# Load model on startup
print(f"[Model Service] Starting...")
print(f"[Model Service] Port: {PORT}")
load_model()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=PORT, debug=False, threaded=True)