PlayerAI / app.py
YoussefElsafi's picture
Update app.py
7e0fffd verified
Raw
History Blame Contribute Delete
14.3 kB
import os
import gradio as gr
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
from threading import Thread
from concurrent.futures import ThreadPoolExecutor
import logging
import time
import threading
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
MODEL_ID = "YoussefElsafi/PlayerAI-1.2B-v1.5"
# ── CPU threading (set BEFORE any torch ops) ──────────────────────────────────
PHYSICAL_CORES = os.cpu_count() or 2
NUM_MODEL_COPIES = 2 # run 2 model instances in parallel
MAX_CONCURRENT = NUM_MODEL_COPIES * 2 # two requests per model at a time
THREADS_PER_GEN = max(1, PHYSICAL_CORES // NUM_MODEL_COPIES)
torch.set_num_threads(THREADS_PER_GEN)
torch.set_num_interop_threads(2)
for var in ("OMP_NUM_THREADS", "MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS",
"VECLIB_MAXIMUM_THREADS", "NUMEXPR_NUM_THREADS"):
os.environ[var] = str(THREADS_PER_GEN)
logger.info(f"CPU cores: {PHYSICAL_CORES} | "
f"Model copies: {NUM_MODEL_COPIES} | "
f"Concurrent gens: {MAX_CONCURRENT} | "
f"Threads/gen: {THREADS_PER_GEN}")
# ── Model loading ─────────────────────────────────────────────────────────────
print("Loading tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
# Detect device
if torch.cuda.is_available():
DEVICE = "cuda"
DTYPE = torch.float16
logger.info("Device: CUDA — float16")
else:
DEVICE = "cpu"
# INT8 quantization REQUIRES float32 weights as input.
DTYPE = torch.float32
logger.info("Device: CPU — float32 (INT8 quantization will halve this)")
def load_one_model(idx: int):
logger.info(f"Loading model copy #{idx}...")
m = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
dtype = DTYPE,
device_map = "auto" if DEVICE == "cuda" else "cpu",
trust_remote_code = True,
low_cpu_mem_usage = True,
)
m.eval()
# INT8 dynamic quantization (CPU only, before torch.compile)
if DEVICE == "cpu":
try:
m = torch.quantization.quantize_dynamic(
m, {torch.nn.Linear}, dtype=torch.qint8,
)
logger.info(f"Model #{idx}: INT8 quantization applied ✓")
except Exception as e:
logger.warning(f"Model #{idx}: quantization skipped: {e}")
if hasattr(torch, "compile"):
try:
m = torch.compile(m, mode="default")
logger.info(f"Model #{idx}: torch.compile applied ✓")
except Exception as e:
logger.warning(f"Model #{idx}: torch.compile skipped: {e}")
return m
print(f"Loading {NUM_MODEL_COPIES} model copies...")
models = [load_one_model(i) for i in range(NUM_MODEL_COPIES)]
model_locks = [threading.Lock() for _ in range(NUM_MODEL_COPIES)]
print(f"All {NUM_MODEL_COPIES} model copies loaded!")
# ── Generation config ─────────────────────────────────────────────────────────
GEN_CONFIG = dict(
max_new_tokens = 80,
do_sample = True,
temperature = 0.85,
top_p = 0.9,
repetition_penalty = 1.1,
pad_token_id = tokenizer.eos_token_id,
use_cache = True,
)
# ── Thread pool + concurrency guards ─────────────────────────────────────────
executor = ThreadPoolExecutor(max_workers=MAX_CONCURRENT, thread_name_prefix="gen")
queue_counter = threading.Semaphore(8) # max 8 requests waiting
def acquire_free_model(timeout: float = 60.0):
"""Try to grab the first free model lock. Returns (idx, model) or (None, None)."""
deadline = time.time() + timeout
while time.time() < deadline:
for i, lock in enumerate(model_locks):
if lock.acquire(blocking=False):
return i, models[i]
time.sleep(0.05)
return None, None
# ── Auth ──────────────────────────────────────────────────────────────────────
ALLOWED_TOKEN = os.environ.get("PLAYERAI_API_KEYS", "")
def validate_key(api_key: str) -> bool:
if not ALLOWED_TOKEN:
return True
return api_key.strip() == ALLOWED_TOKEN.strip()
# ── System prompt ─────────────────────────────────────────────────────────────
SYSTEM_PROMPT = (
"You are a human-like player in a multiplayer chat environment. "
"Respond casually, with short informal messages and natural tone. "
"Use lowercase, slang, and act like a real person chatting."
)
# ── Core generation (runs in thread pool) ─────────────────────────────────────
def _run_generation(model_instance, prompt: str, streamer: TextIteratorStreamer) -> None:
try:
inputs = tokenizer(
prompt,
return_tensors = "pt",
truncation = True,
max_length = 512,
)
with torch.inference_mode():
model_instance.generate(
input_ids = inputs.input_ids,
attention_mask = inputs.attention_mask,
streamer = streamer,
**GEN_CONFIG,
)
except Exception as e:
logger.error(f"Generation error: {e}")
streamer.text_queue.put(streamer.stop_signal)
raise
# ── Streaming response ────────────────────────────────────────────────────────
def respond(message: str, history: list, api_key: str):
if not validate_key(api_key):
yield "[error] unauthorized"
return
if not queue_counter.acquire(blocking=False):
yield "[error] server busy — try again shortly"
return
try:
# Trim history to keep context short (long context = slow CPU prefill)
trimmed = history[-4:] if len(history) > 4 else history
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages.extend(trimmed)
messages.append({"role": "user", "content": message})
try:
prompt = tokenizer.apply_chat_template(
messages,
tokenize = False,
add_generation_prompt = True,
)
except Exception as e:
yield f"[error] template: {e}"
return
# Get a free model instance
idx, model_instance = acquire_free_model(timeout=60.0)
if model_instance is None:
yield "[error] all models busy — try again"
return
response = ""
try:
streamer = TextIteratorStreamer(
tokenizer,
skip_prompt = True,
skip_special_tokens = True,
timeout = 120.0,
)
future = executor.submit(_run_generation, model_instance, prompt, streamer)
for token in streamer:
response += token
yield response
# Raise any exception from the worker thread
future.result(timeout=10)
except Exception as e:
logger.error(f"respond() error on model #{idx}: {e}")
if not response:
yield "[error] generation failed"
finally:
model_locks[idx].release()
finally:
queue_counter.release()
# ── Warmup (runs in background, errors are non-fatal) ────────────────────────
def _warmup():
logger.info("Warmup starting for all model copies...")
try:
dummy_prompt = tokenizer.apply_chat_template(
[{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": "hi"}],
tokenize = False,
add_generation_prompt = True,
)
inputs = tokenizer(
dummy_prompt,
return_tensors = "pt",
truncation = True,
max_length = 128,
)
warmup_cfg = {**GEN_CONFIG, "max_new_tokens": 10}
for idx, m in enumerate(models):
t0 = time.time()
try:
with torch.inference_mode():
m.generate(
input_ids = inputs.input_ids,
attention_mask = inputs.attention_mask,
**warmup_cfg,
)
logger.info(f"Model #{idx} warmup done in {time.time()-t0:.1f}s ✓")
except Exception as e:
logger.warning(f"Model #{idx} warmup failed (non-fatal): {e}")
logger.info("All warmups complete ✓")
except Exception as e:
logger.warning(f"Warmup failed (non-fatal): {e}")
Thread(target=_warmup, daemon=True).start()
# ── CSS ───────────────────────────────────────────────────────────────────────
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
* { font-family: 'Press Start 2P', monospace !important; border-radius: 0 !important; }
html, body, .gradio-container, .main, .wrap, gradio-app {
background-color: #000000 !important; color: #ffffff !important; }
.gradio-container { max-width: 800px !important; margin: 0 auto !important; padding: 10px !important; }
.chatbot, [data-testid="chatbot"], .message-wrap, .messages {
background-color: #000000 !important; border: none !important; box-shadow: none !important; }
.message.user .message-bubble-border, div[data-testid="user"] .message-bubble-border, .message.user {
background-color: #00d000 !important; color: #000000 !important; border: none !important;
font-size: 12px !important; line-height: 1.6 !important; padding: 10px 14px !important; }
.message.bot .message-bubble-border, div[data-testid="bot"] .message-bubble-border, .message.bot {
background-color: #ffffff !important; color: #000000 !important; border: none !important;
font-size: 12px !important; line-height: 1.6 !important; padding: 10px 14px !important; }
.message.user *, .message.bot * { color: #000000 !important; background-color: transparent !important; }
.avatar-container, .avatar-container img, img.avatar-image {
width: 36px !important; height: 36px !important; min-width: 36px !important;
background: transparent !important; image-rendering: pixelated !important;
border: none !important; border-radius: 0 !important; }
textarea, input[type="text"] {
background-color: #111111 !important; color: #00ff00 !important;
border: 2px solid #00d000 !important; font-size: 12px !important; padding: 12px !important; }
button { background-color: #00d000 !important; color: #000000 !important;
border: 2px solid #00d000 !important; font-size: 10px !important; }
button:hover { background-color: #00ff00 !important; }
h1, h2, h3 { color: #00ff00 !important; text-align: center; font-size: 16px !important; }
#lock-screen {
position: fixed !important; top: 0 !important; left: 0 !important;
width: 100vw !important; height: 100vh !important;
background-color: #000000 !important; z-index: 9999 !important;
display: flex !important; flex-direction: column !important;
align-items: center !important; justify-content: center !important; gap: 20px !important; }
footer { display: none !important; }
.block, .form, .gap { background-color: #000000 !important; border: none !important; }
"""
USER_AVATAR = "face.png"
BOT_AVATAR = "question.png"
# ── UI ────────────────────────────────────────────────────────────────────────
with gr.Blocks(css=custom_css, theme=gr.themes.Base()) as demo:
with gr.Column(elem_id="lock-screen", visible=True) as lock_screen:
gr.Markdown("# >> player_ai <<")
gr.Markdown("### enter access key to continue")
key_input = gr.Textbox(placeholder="access key...", type="password",
show_label=False, max_lines=1)
unlock_btn = gr.Button("UNLOCK")
lock_msg = gr.Markdown("")
with gr.Column(visible=False) as chat_screen:
gr.Markdown("# >> player_ai <<")
stored_key = gr.Textbox(value="", visible=False)
chatbot = gr.Chatbot(
type = "messages",
avatar_images = (USER_AVATAR, BOT_AVATAR),
height = 500,
show_label = False,
bubble_full_width = False,
show_copy_button = False,
)
gr.ChatInterface(
fn = respond,
chatbot = chatbot,
type = "messages",
additional_inputs = [stored_key],
additional_inputs_accordion = gr.Accordion(visible=False),
)
def try_unlock(key):
if validate_key(key):
return gr.update(visible=False), gr.update(visible=True), key, ""
return gr.update(visible=True), gr.update(visible=False), "", "❌ invalid key"
for trigger in (unlock_btn.click, key_input.submit):
trigger(fn=try_unlock, inputs=[key_input],
outputs=[lock_screen, chat_screen, stored_key, lock_msg])
if __name__ == "__main__":
demo.launch(
max_threads = MAX_CONCURRENT * 4,
show_error = True,
)