File size: 14,307 Bytes
9723cc1
cb58ac7
545488b
 
 
5a8ed7f
 
0ba5084
 
5a8ed7f
 
 
545488b
 
 
a72d57f
ed9b01d
7e0fffd
b9798f2
ed9b01d
0ba5084
 
 
 
ed9b01d
 
 
0ba5084
 
ed9b01d
0ba5084
 
 
 
ed9b01d
545488b
5a8ed7f
a72d57f
 
 
 
 
 
 
 
 
 
5a8ed7f
ed9b01d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5a8ed7f
ed9b01d
 
 
 
 
 
0ba5084
ed9b01d
5a8ed7f
ed9b01d
 
 
 
cb58ac7
0ba5084
 
70a6782
 
 
 
0ba5084
70a6782
 
294d72b
cb58ac7
a72d57f
70a6782
a72d57f
0ba5084
ed9b01d
 
 
 
 
 
 
 
 
 
5a8ed7f
0ba5084
 
89d0211
 
5a8ed7f
89d0211
 
0ba5084
 
 
 
 
 
5a8ed7f
a72d57f
ed9b01d
a72d57f
 
 
 
 
 
5a8ed7f
a72d57f
ed9b01d
a72d57f
 
 
 
 
 
 
 
 
5a8ed7f
a72d57f
5a8ed7f
89d0211
12165fa
 
 
0ba5084
a72d57f
5a8ed7f
12165fa
5a8ed7f
a72d57f
 
0ba5084
 
a72d57f
0ba5084
 
 
 
 
70a6782
0ba5084
 
 
 
 
 
ed9b01d
 
 
 
 
545488b
ed9b01d
0ba5084
ed9b01d
 
 
 
 
 
 
 
545488b
0ba5084
 
 
545488b
a72d57f
0ba5084
a72d57f
 
ed9b01d
a72d57f
ed9b01d
0ba5084
ed9b01d
5a8ed7f
0ba5084
 
cb58ac7
294d72b
a72d57f
0ba5084
ed9b01d
0ba5084
 
 
 
70a6782
 
0ba5084
a72d57f
 
 
 
 
 
 
0ba5084
ed9b01d
 
a72d57f
 
ed9b01d
a72d57f
 
 
 
ed9b01d
a72d57f
ed9b01d
a72d57f
ed9b01d
a72d57f
0ba5084
 
294d72b
0ba5084
294d72b
dc2f9b6
a72d57f
0ba5084
 
 
 
 
 
4261d15
0ba5084
 
 
 
 
 
 
 
4261d15
0ba5084
 
 
294d72b
0ba5084
 
 
 
4261d15
0ba5084
89d0211
0ba5084
 
 
 
 
b95a7fd
0ba5084
cb58ac7
 
0455ab0
5a8ed7f
294d72b
a72d57f
294d72b
89d0211
 
 
 
0ba5084
 
89d0211
5a8ed7f
89d0211
 
 
 
 
 
70a6782
 
 
 
 
 
89d0211
 
 
0ba5084
 
 
 
 
89d0211
 
 
 
0ba5084
 
cb58ac7
5a8ed7f
0ba5084
 
cb58ac7
 
5a8ed7f
0ba5084
 
5a8ed7f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
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,
    )