Spaces:
Running
Running
| 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, | |
| ) |