CodeSoft commited on
Commit
cdff60f
·
verified ·
1 Parent(s): 43e1f67

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1067 -54
app.py CHANGED
@@ -1,69 +1,1082 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
- from huggingface_hub import InferenceClient
3
-
4
-
5
- def respond(
6
- message,
7
- history: list[dict[str, str]],
8
- system_message,
9
- max_tokens,
10
- temperature,
11
- top_p,
12
- hf_token: gr.OAuthToken,
13
- ):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  """
15
- For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
 
 
16
  """
17
- client = InferenceClient(token=hf_token.token, model="openai/gpt-oss-20b")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- messages = [{"role": "system", "content": system_message}]
 
 
 
 
 
20
 
21
- messages.extend(history)
 
 
22
 
23
- messages.append({"role": "user", "content": message})
24
 
25
- response = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- for message in client.chat_completion(
28
- messages,
29
- max_tokens=max_tokens,
30
- stream=True,
31
- temperature=temperature,
32
- top_p=top_p,
33
- ):
34
- choices = message.choices
35
- token = ""
36
- if len(choices) and choices[0].delta.content:
37
- token = choices[0].delta.content
38
 
39
- response += token
40
- yield response
 
 
 
 
 
 
 
41
 
 
 
 
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  """
44
- For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
45
- """
46
- chatbot = gr.ChatInterface(
47
- respond,
48
- additional_inputs=[
49
- gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
50
- gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
51
- gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
52
- gr.Slider(
53
- minimum=0.1,
54
- maximum=1.0,
55
- value=0.95,
56
- step=0.05,
57
- label="Top-p (nucleus sampling)",
58
- ),
59
- ],
60
- )
61
-
62
- with gr.Blocks() as demo:
63
- with gr.Sidebar():
64
- gr.LoginButton()
65
- chatbot.render()
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
 
 
 
68
  if __name__ == "__main__":
69
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import random
4
+ import math
5
+ import logging
6
+ import traceback
7
+ from pathlib import Path
8
+ from dataclasses import dataclass
9
+ from typing import Dict, List, Tuple, Optional
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+ from transformers import AutoTokenizer, AutoModelForCausalLM
15
+
16
  import gradio as gr
17
+ import pandas as pd
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Logging
21
+ # ---------------------------------------------------------------------------
22
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Constants & Paths
27
+ # ---------------------------------------------------------------------------
28
+ MODEL_IDS: List[str] = [
29
+ "CodeSoft/MetaDiffusion-150M-ChatBase",
30
+ "BananaMind/BananaMind-2-Medium-Chat",
31
+ "SupraLabs/Supra2-100M-Instruct",
32
+ "HuggingFaceTB/SmolLM2-135M-Instruct",
33
+ ]
34
+
35
+ MODEL_DISPLAY: Dict[str, str] = {
36
+ "CodeSoft/MetaDiffusion-150M-ChatBase": "MetaDiffusion-150M-ChatBase",
37
+ "BananaMind/BananaMind-2-Medium-Chat": "BananaMind-2-Medium-Chat",
38
+ "SupraLabs/Supra2-100M-Instruct": "Supra2-100M-Instruct",
39
+ "HuggingFaceTB/SmolLM2-135M-Instruct": "SmolLM2-135M-Instruct",
40
+ }
41
+
42
+ FALLBACK_IDS: Dict[str, str] = {}
43
+
44
+ INIT_RATING = 1000
45
+ K_FACTOR = 32
46
+ SCALE = 400
47
+ BASE = 10
48
+
49
+ # All data in ./data
50
+ try:
51
+ BASE_DIR = Path(__file__).parent
52
+ except NameError:
53
+ BASE_DIR = Path(".")
54
+
55
+ # Prefer /data (HF Space bucket mount) if available, otherwise fallback to ./data
56
+ # Bucket is mounted at /data in Space — use dynamic check each call so late mounts are detected
57
+ def get_data_dir() -> Path:
58
+ bucket = Path("/data")
59
+ if bucket.exists() and bucket.is_dir():
60
+ try:
61
+ # Ensure writable (touch test)
62
+ (bucket / ".write_test").touch(exist_ok=True)
63
+ (bucket / ".write_test").unlink(missing_ok=True)
64
+ return bucket
65
+ except Exception:
66
+ pass
67
+ # Fallback to local ./data
68
+ local = BASE_DIR / "data"
69
+ try:
70
+ local.mkdir(parents=True, exist_ok=True)
71
+ except Exception:
72
+ pass
73
+ return local
74
+
75
+ def get_elo_file() -> Path:
76
+ return get_data_dir() / "elo.json"
77
+
78
+ def get_chat_file() -> Path:
79
+ return get_data_dir() / "chats.jsonl"
80
+
81
+ # Keep legacy globals for backwards compat (now dynamic via functions)
82
+ DATA_DIR = get_data_dir()
83
+ ELO_FILE = get_elo_file()
84
+ CHAT_FILE = get_chat_file()
85
+
86
+ GEN_DEFAULTS: Dict[str, dict] = {
87
+ "HuggingFaceTB/SmolLM2-135M-Instruct": {"max_new_tokens": 64, "temperature": 0.7, "top_p": 0.9, "repetition_penalty": 1.1, "do_sample": True},
88
+ "SupraLabs/Supra2-100M-Instruct": {"max_new_tokens": 64, "temperature": 0.7, "top_p": 0.9, "top_k": 25, "repetition_penalty": 1.1, "do_sample": True, "no_repeat_ngram_size": 3},
89
+ "BananaMind/BananaMind-2-Medium-Chat": {"max_new_tokens": 64, "temperature": 0.7, "top_p": 0.9, "repetition_penalty": 1.1, "do_sample": True},
90
+ "CodeSoft/MetaDiffusion-150M-ChatBase": {"max_new_tokens": 96, "num_steps": 128, "temperature": 0.7, "top_p": 0.9, "repetition_penalty": 1.5},
91
+ }
92
+
93
+ MODEL_CONTEXT: Dict[str, int] = {
94
+ "HuggingFaceTB/SmolLM2-135M-Instruct": 2048,
95
+ "SupraLabs/Supra2-100M-Instruct": 1024,
96
+ "BananaMind/BananaMind-2-Medium-Chat": 3072,
97
+ "CodeSoft/MetaDiffusion-150M-ChatBase": 5120,
98
+ }
99
+
100
+ DEVICE = "cpu"
101
+
102
+ @dataclass
103
+ class MetaDiffusionConfig:
104
+ hidden_size: int = 768
105
+ intermediate_size: int = 2112
106
+ num_hidden_layers: int = 16
107
+ num_attention_heads: int = 12
108
+ num_key_value_heads: int = 6
109
+ head_dim: int = 64
110
+ vocab_size: int = 32000
111
+ mask_vocab_size: int = 32010
112
+ max_position_embeddings: int = 5120
113
+ rope_theta: float = 10000.0
114
+ rms_norm_eps: float = 1e-6
115
+ hidden_act: str = "silu"
116
+ timestep_emb_hidden: int = 768
117
+ mask_token_id: int = 32000
118
+ pad_token_id: int = 1
119
+ mask_ratio_min: float = 0.0
120
+ mask_ratio_max: float = 1.0
121
+ dtype: torch.dtype = torch.float32 # type: ignore
122
+ tie_word_embeddings: bool = False
123
+
124
+
125
+ class _RotaryEmbedding(nn.Module):
126
+ def __init__(self, dim, max_position_embeddings=5120, base=10000.0, device=None):
127
+ super().__init__()
128
+ self.dim = dim
129
+ self.max_position_embeddings = max_position_embeddings
130
+ self.base = base
131
+ inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2, device=device).float() / dim))
132
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
133
+
134
+ @torch.no_grad()
135
+ def forward(self, x, position_ids):
136
+ inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1)
137
+ position_ids_expanded = position_ids[:, None, :].float()
138
+ freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2)
139
+ emb = torch.cat((freqs, freqs), dim=-1)
140
+ cos = emb.cos()
141
+ sin = emb.sin()
142
+ return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
143
+
144
+
145
+ def _rotate_half(x):
146
+ x1, x2 = x.chunk(2, dim=-1)
147
+ return torch.cat((-x2, x1), dim=-1)
148
+
149
+
150
+ def _apply_rotary_pos_emb(q, k, cos, sin):
151
+ cos = cos.unsqueeze(1)
152
+ sin = sin.unsqueeze(1)
153
+ q_embed = (q * cos) + (_rotate_half(q) * sin)
154
+ k_embed = (k * cos) + (_rotate_half(k) * sin)
155
+ return q_embed, k_embed
156
+
157
+
158
+ class _TimestepEmbedding(nn.Module):
159
+ def __init__(self, hidden_size):
160
+ super().__init__()
161
+ self.hidden_size = hidden_size
162
+ self.mlp = nn.Sequential(
163
+ nn.Linear(hidden_size, hidden_size * 4),
164
+ nn.SiLU(),
165
+ nn.Linear(hidden_size * 4, hidden_size),
166
+ )
167
+
168
+ def forward(self, t):
169
+ half_dim = self.hidden_size // 2
170
+ emb = math.log(10000.0) / (half_dim - 1)
171
+ emb = torch.exp(torch.arange(half_dim, device=t.device, dtype=torch.float32) * -emb)
172
+ emb = t[:, None].float() * emb[None, :]
173
+ emb = torch.cat([emb.sin(), emb.cos()], dim=-1)
174
+ return self.mlp(emb).to(t.dtype)
175
+
176
+
177
+ class _TimestepResidual(nn.Module):
178
+ def __init__(self, hidden_size):
179
+ super().__init__()
180
+ self.proj = nn.Linear(hidden_size, hidden_size)
181
+ nn.init.zeros_(self.proj.weight)
182
+ nn.init.zeros_(self.proj.bias)
183
+
184
+ def forward(self, x, emb):
185
+ return x + self.proj(emb)[:, None, :]
186
+
187
+
188
+ class _RMSNorm(nn.Module):
189
+ def __init__(self, hidden_size, eps=1e-6):
190
+ super().__init__()
191
+ self.weight = nn.Parameter(torch.ones(hidden_size))
192
+ self.eps = eps
193
+
194
+ def forward(self, x):
195
+ var = x.pow(2).mean(-1, keepdim=True)
196
+ x = x * torch.rsqrt(var + self.eps)
197
+ return self.weight * x
198
+
199
+
200
+ class _SelfAttention(nn.Module):
201
+ def __init__(self, config: MetaDiffusionConfig):
202
+ super().__init__()
203
+ self.config = config
204
+ self.hidden_size = config.hidden_size
205
+ self.num_heads = config.num_attention_heads
206
+ self.num_kv_heads = config.num_key_value_heads
207
+ self.head_dim = config.head_dim
208
+ self.num_kv_groups = self.num_heads // self.num_kv_heads
209
+ self.q_proj = nn.Linear(config.hidden_size, self.num_heads * config.head_dim, bias=False)
210
+ self.k_proj = nn.Linear(config.hidden_size, self.num_kv_heads * config.head_dim, bias=False)
211
+ self.v_proj = nn.Linear(config.hidden_size, self.num_kv_heads * config.head_dim, bias=False)
212
+ self.o_proj = nn.Linear(self.num_heads * config.head_dim, config.hidden_size, bias=False)
213
+ self.rotary_emb = _RotaryEmbedding(config.head_dim, max_position_embeddings=config.max_position_embeddings, base=config.rope_theta)
214
+
215
+ def forward(self, x, attention_mask=None, position_ids=None):
216
+ batch, seq, _ = x.shape
217
+ q = self.q_proj(x).view(batch, seq, self.num_heads, self.head_dim).transpose(1, 2)
218
+ k = self.k_proj(x).view(batch, seq, self.num_kv_heads, self.head_dim).transpose(1, 2)
219
+ v = self.v_proj(x).view(batch, seq, self.num_kv_heads, self.head_dim).transpose(1, 2)
220
+ cos, sin = self.rotary_emb(x, position_ids)
221
+ q, k = _apply_rotary_pos_emb(q, k, cos, sin)
222
+ if self.num_kv_groups > 1:
223
+ k = k.repeat_interleave(self.num_kv_groups, dim=1)
224
+ v = v.repeat_interleave(self.num_kv_groups, dim=1)
225
+ out = F.scaled_dot_product_attention(q, k, v, attn_mask=attention_mask)
226
+ out = out.transpose(1, 2).contiguous().view(batch, seq, -1)
227
+ return self.o_proj(out)
228
+
229
+
230
+ class _MLP(nn.Module):
231
+ def __init__(self, config: MetaDiffusionConfig):
232
+ super().__init__()
233
+ self.gate_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
234
+ self.up_proj = nn.Linear(config.hidden_size, config.intermediate_size, bias=False)
235
+ self.down_proj = nn.Linear(config.intermediate_size, config.hidden_size, bias=False)
236
+
237
+ def forward(self, x):
238
+ return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
239
+
240
+
241
+ class _TransformerBlock(nn.Module):
242
+ def __init__(self, config: MetaDiffusionConfig):
243
+ super().__init__()
244
+ self.input_layernorm = _RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
245
+ self.self_attn = _SelfAttention(config)
246
+ self.post_attention_layernorm = _RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
247
+ self.mlp = _MLP(config)
248
+ self.timestep_residual = _TimestepResidual(config.hidden_size)
249
+
250
+ def forward(self, x, timestep_emb, attention_mask=None, position_ids=None):
251
+ residual = x
252
+ x = self.input_layernorm(x)
253
+ x = self.self_attn(x, attention_mask, position_ids)
254
+ x = residual + x
255
+ x = self.timestep_residual(x, timestep_emb)
256
+ residual = x
257
+ x = self.post_attention_layernorm(x)
258
+ x = self.mlp(x)
259
+ x = residual + x
260
+ x = self.timestep_residual(x, timestep_emb)
261
+ return x
262
+
263
+
264
+ class MetaDiffusionLM(nn.Module):
265
+ def __init__(self, config: MetaDiffusionConfig):
266
+ super().__init__()
267
+ self.config = config
268
+ self.embed_tokens = nn.Embedding(config.mask_vocab_size, config.hidden_size, padding_idx=config.pad_token_id)
269
+ self.timestep_emb = _TimestepEmbedding(config.timestep_emb_hidden)
270
+ self.layers = nn.ModuleList([_TransformerBlock(config) for _ in range(config.num_hidden_layers)])
271
+ self.norm = _RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
272
+ if config.tie_word_embeddings:
273
+ self.lm_head = None # type: ignore
274
+ else:
275
+ self.lm_head = nn.Linear(config.hidden_size, config.mask_vocab_size, bias=False)
276
+ if self.lm_head is not None:
277
+ nn.init.normal_(self.lm_head.weight, std=0.02)
278
+
279
+ def forward(self, input_ids, timesteps, attention_mask=None):
280
+ batch, seq = input_ids.shape
281
+ position_ids = torch.arange(seq, device=input_ids.device).unsqueeze(0).expand(batch, -1)
282
+ x = self.embed_tokens(input_ids)
283
+ t_emb = self.timestep_emb(timesteps)
284
+ attn_mask = None
285
+ if attention_mask is not None:
286
+ attn_mask = ((1.0 - attention_mask[:, None, None, :].float()) * -1e9).to(x.dtype)
287
+ for layer in self.layers:
288
+ x = layer(x, t_emb, attn_mask, position_ids)
289
+ x = self.norm(x)
290
+ if self.lm_head is not None:
291
+ logits = self.lm_head(x)
292
+ else:
293
+ logits = F.linear(x, self.embed_tokens.weight)
294
+ return logits
295
+
296
+ DIFF_MASK_ID = 32000
297
+ DIFF_CHAT_TOKENS = ["<|im_start|>", "<|im_end|>"] + [f"<|r{i}|>" for i in range(1, 8)]
298
+ DIFF_IM_START, DIFF_IM_END = "<|im_start|>", "<|im_end|>"
299
+
300
+
301
+ def _ensure_diff_chat_tokens(tokenizer):
302
+ """Add ChatML + rainbow tokens if missing (base tokenizer case). Mirrors chat.py."""
303
+ if tokenizer.convert_tokens_to_ids(DIFF_IM_START) == tokenizer.unk_token_id:
304
+ if len(tokenizer) == 32000:
305
+ tokenizer.add_special_tokens({"additional_special_tokens": ["<|reserved|>"]})
306
+ tokenizer.add_special_tokens({"additional_special_tokens": DIFF_CHAT_TOKENS})
307
+ assert tokenizer.convert_tokens_to_ids(DIFF_IM_END) == 32002, "chat token ids wrong (collide with mask id 32000)"
308
+ return tokenizer
309
+
310
+
311
+ def _format_diff_messages(messages):
312
+ parts = []
313
+ for m in messages:
314
+ parts.append(f"{DIFF_IM_START}{m['role']}\n{m['content']}{DIFF_IM_END}")
315
+ return "\n".join(parts)
316
+
317
+
318
+ def _diff_cumulative_unmask_frac(i, N):
319
+ return 0.5 * (1 - math.cos(math.pi * i / N))
320
+
321
+
322
+ def _diff_cut_response(tokens, tokenizer):
323
+ """Cut at <|im_end|> or </s>; drop rainbow/pad. Mirrors chat.py."""
324
+ im_end_id = tokenizer.convert_tokens_to_ids(DIFF_IM_END)
325
+ eos_id = tokenizer.eos_token_id
326
+ rainbow_ids = {tokenizer.convert_tokens_to_ids(f"<|r{i}|>") for i in range(1, 8)}
327
+ out = []
328
+ for t in tokens:
329
+ if t == im_end_id or t == eos_id:
330
+ break
331
+ if t in rainbow_ids or t == tokenizer.pad_token_id:
332
+ continue
333
+ out.append(t)
334
+ return out
335
+
336
+
337
+ @torch.no_grad()
338
+ def _diff_generate_response(model, tokenizer, prompt_ids, gen_len, num_steps, temperature, repetition_penalty, device, stop_on_end=True):
339
+ model.eval()
340
+ total_len = prompt_ids.shape[1] + gen_len
341
+ x = torch.full((1, total_len), DIFF_MASK_ID, device=device, dtype=torch.long)
342
+ x[0, : prompt_ids.shape[1]] = prompt_ids
343
+ mask_id = DIFF_MASK_ID
344
+ im_end_id = tokenizer.convert_tokens_to_ids(DIFF_IM_END)
345
+ eos_id = tokenizer.eos_token_id
346
+ prompt_len = prompt_ids.shape[1]
347
+
348
+ for i in range(num_steps):
349
+ frac_now = _diff_cumulative_unmask_frac(i, num_steps)
350
+ frac_next = _diff_cumulative_unmask_frac(i + 1, num_steps)
351
+ n_masked = (x == mask_id).sum().item()
352
+ n_total = int((frac_next - frac_now) * gen_len + 0.5)
353
+ if i == num_steps - 1:
354
+ n_unmask = n_masked
355
+ else:
356
+ n_unmask = max(n_total, 1) if n_masked > 0 else 0
357
+
358
+ t = 1.0 - frac_now
359
+ logits = model(x, torch.full((1,), t, device=device))
360
+ logits[:, :, mask_id] = -1e9
361
+
362
+ if repetition_penalty != 1.0:
363
+ for tok in x[0].unique():
364
+ ti = int(tok.item())
365
+ if 0 <= ti < logits.shape[-1]:
366
+ logits[0, :, ti] = torch.where(
367
+ logits[0, :, ti] < 0,
368
+ logits[0, :, ti] * repetition_penalty,
369
+ logits[0, :, ti] / repetition_penalty,
370
+ )
371
+
372
+ mask_positions = x == mask_id
373
+ if not mask_positions.any():
374
+ break
375
+ mask_logits = logits[mask_positions]
376
+ probs = F.softmax(mask_logits / max(0.1, temperature), dim=-1)
377
+ sampled = torch.multinomial(probs, 1).squeeze(-1)
378
+ mask_flat = mask_positions.nonzero(as_tuple=False)
379
+
380
+ if n_unmask < int(mask_positions.sum().item()):
381
+ fill_positions = mask_flat[:n_unmask]
382
+ for idx, tok in zip(fill_positions, sampled[:n_unmask]):
383
+ x[idx[0], idx[1]] = tok
384
+ else:
385
+ x[mask_positions] = sampled
386
+
387
+ if stop_on_end and ((x[0, prompt_len:] == im_end_id).any() or (x[0, prompt_len:] == eos_id).any()):
388
+ break
389
+ return x
390
+
391
+
392
+ # ---------------------------------------------------------------------------
393
+ # ELO persistence
394
+ # ---------------------------------------------------------------------------
395
+ def init_elo_state() -> Dict[str, dict]:
396
+ return {mid: {"rating": float(INIT_RATING), "wins": 0, "losses": 0, "battles": 0, "ties": 0} for mid in MODEL_IDS}
397
+
398
+ def load_elo() -> Dict[str, dict]:
399
+ if get_elo_file().exists():
400
+ try:
401
+ with open(get_elo_file(), "r") as f:
402
+ data = json.load(f)
403
+ for mid in MODEL_IDS:
404
+ if mid not in data:
405
+ data[mid] = {"rating": float(INIT_RATING), "wins": 0, "losses": 0, "battles": 0, "ties": 0}
406
+ return data
407
+ except Exception as e:
408
+ logger.warning(f"Failed to load ELO file: {e}, resetting")
409
+ return init_elo_state()
410
+
411
+ def save_elo(state: Dict[str, dict]):
412
+ try:
413
+ get_data_dir().mkdir(parents=True, exist_ok=True)
414
+ with open(get_elo_file(), "w") as f:
415
+ json.dump(state, f, indent=2)
416
+ except Exception as e:
417
+ logger.error(f"Failed to save ELO: {e}")
418
+
419
+ def expected_score(ra: float, rb: float) -> float:
420
+ return 1.0 / (1.0 + BASE ** ((rb - ra) / SCALE))
421
+
422
+ def update_elo(state: Dict[str, dict], model_a: str, model_b: str, winner: Optional[str]) -> Dict[str, dict]:
423
+ if model_a not in state or model_b not in state:
424
+ logger.warning(f"Unknown models in ELO update: {model_a}, {model_b}")
425
+ return state
426
+ ra = state[model_a]["rating"]
427
+ rb = state[model_b]["rating"]
428
+ ea = expected_score(ra, rb)
429
+ eb = expected_score(rb, ra)
430
+ if winner == model_a:
431
+ sa = 1.0
432
+ elif winner == model_b:
433
+ sa = 0.0
434
+ elif winner is None or winner == "tie":
435
+ sa = 0.5
436
+ else:
437
+ raise ValueError(f"Unexpected winner: {winner}")
438
+ sb = 1.0 - sa
439
+ state[model_a]["rating"] = ra + K_FACTOR * (sa - ea)
440
+ state[model_b]["rating"] = rb + K_FACTOR * (sb - eb)
441
+ state[model_a]["battles"] += 1
442
+ state[model_b]["battles"] += 1
443
+ if sa == 1.0:
444
+ state[model_a]["wins"] += 1
445
+ state[model_b]["losses"] += 1
446
+ elif sa == 0.0:
447
+ state[model_b]["wins"] += 1
448
+ state[model_a]["losses"] += 1
449
+ else:
450
+ state[model_a]["ties"] += 1
451
+ state[model_b]["ties"] += 1
452
+ save_elo(state)
453
+ return state
454
+
455
+ def leaderboard_dataframe(state: Optional[Dict[str, dict]] = None) -> pd.DataFrame:
456
+ if state is None:
457
+ state = load_elo()
458
+ rows = []
459
+ for mid in MODEL_IDS:
460
+ info = state.get(mid, {"rating": INIT_RATING, "wins": 0, "losses": 0, "battles": 0, "ties": 0})
461
+ rows.append({
462
+ "Model": MODEL_DISPLAY.get(mid, mid),
463
+ "Model ID": mid,
464
+ "ELO": round(float(info["rating"]), 1),
465
+ "Battles": int(info["battles"]),
466
+ "Wins": int(info["wins"]),
467
+ "Losses": int(info["losses"]),
468
+ "Ties": int(info.get("ties", 0)),
469
+ })
470
+ df = pd.DataFrame(rows)
471
+ df = df.sort_values(by="ELO", ascending=False).reset_index(drop=True)
472
+ df.insert(0, "Rank", range(1, len(df) + 1))
473
+ return df
474
+
475
+ # ---------------------------------------------------------------------------
476
+ # Chat logging to data/chats.jsonl
477
+ # ---------------------------------------------------------------------------
478
+ def log_battle(prompt: str, model_a: str, model_b: str, response_a: str, response_b: str, chosen: str, winner_model: str):
479
  """
480
+ Append one battle record to data/chats.jsonl.
481
+ Fields: prompt, response_a, response_b, model_a, model_b, chosen (A/B), winner_model, timestamp
482
+ Spec: keeps user's message, two responses, each model's names, and what response user chose.
483
  """
484
+ try:
485
+ get_data_dir().mkdir(parents=True, exist_ok=True)
486
+ record = {
487
+ "timestamp": __import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat(),
488
+ "prompt": prompt,
489
+ "model_a": model_a,
490
+ "model_b": model_b,
491
+ "response_a": response_a,
492
+ "response_b": response_b,
493
+ "chosen": chosen, # "A" / "B" / "tie"
494
+ "winner_model": winner_model,
495
+ "chosen_response": response_a if chosen == "A" else response_b if chosen == "B" else "",
496
+ }
497
+ with open(get_chat_file(), "a", encoding="utf-8") as f:
498
+ f.write(json.dumps(record, ensure_ascii=False) + "\n")
499
+ except Exception as e:
500
+ logger.error(f"Failed to log battle: {e}")
501
 
502
+ # ---------------------------------------------------------------------------
503
+ # Model loading (CPU)
504
+ # ---------------------------------------------------------------------------
505
+ models: Dict[str, object] = {}
506
+ tokenizers: Dict[str, object] = {}
507
+ model_load_errors: Dict[str, str] = {}
508
 
509
+ # Diffusion manual instance (if loaded)
510
+ diffusion_model: Optional[MetaDiffusionLM] = None
511
+ diffusion_tokenizer = None
512
 
513
+ HF_DIFFUSION_REPO = "CodeSoft/MetaDiffusion-150M-ChatBase"
514
 
515
+ def load_diffusion_manual():
516
+ """Load MetaDiffusion from HuggingFace (only) using inline architecture."""
517
+ global diffusion_model, diffusion_tokenizer
518
+ if diffusion_model is not None:
519
+ # Re-register in global dicts if cleared (e.g., after tests)
520
+ if "CodeSoft/MetaDiffusion-150M-ChatBase" not in models:
521
+ models["CodeSoft/MetaDiffusion-150M-ChatBase"] = diffusion_model # type: ignore
522
+ if diffusion_tokenizer is not None and "CodeSoft/MetaDiffusion-150M-ChatBase" not in tokenizers:
523
+ tokenizers["CodeSoft/MetaDiffusion-150M-ChatBase"] = diffusion_tokenizer # type: ignore
524
+ return diffusion_model, diffusion_tokenizer
525
+ try:
526
+ from huggingface_hub import snapshot_download
527
+ repo_id = HF_DIFFUSION_REPO
528
+ local_dir = Path(snapshot_download(repo_id))
529
+ cfg_path = local_dir / "config.json"
530
+ tok_path = local_dir
531
+ model_path = local_dir / "model.safetensors"
532
+ if not cfg_path.exists() or not model_path.exists():
533
+ logger.warning(f"Diffusion files not found in HF snapshot {local_dir}")
534
+ return None, None
535
+ with open(cfg_path, "r") as f:
536
+ cfg_dict = json.load(f)
537
+ valid = {k: v for k, v in cfg_dict.items() if k in MetaDiffusionConfig.__dataclass_fields__}
538
+ cfg = MetaDiffusionConfig(**valid)
539
+ cfg.tie_word_embeddings = False
540
+ mdl = MetaDiffusionLM(cfg).to(DEVICE)
541
+ try:
542
+ from safetensors.torch import load_file
543
+ except ImportError:
544
+ import subprocess, sys
545
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "safetensors", "--quiet", "--break-system-packages"])
546
+ from safetensors.torch import load_file # type: ignore
547
+ state = load_file(str(model_path), device="cpu")
548
+ state = {k[len("model."):] if k.startswith("model.") else k: v for k, v in state.items()}
549
+ missing, unexpected = mdl.load_state_dict(state, strict=False)
550
+ if missing or unexpected:
551
+ logger.info(f" Diffusion load: missing={missing[:3]} unexpected={unexpected[:3]}")
552
+ mdl.to(DEVICE)
553
+ mdl.eval()
554
+ logger.info(f" Loaded {sum(p.numel() for p in mdl.parameters())/1e6:.1f}M params, vocab={cfg.mask_vocab_size}")
555
+ tok = AutoTokenizer.from_pretrained(str(tok_path), trust_remote_code=True)
556
+ tok = _ensure_diff_chat_tokens(tok)
557
+ if tok.pad_token is None:
558
+ tok.pad_token = tok.eos_token
559
+ diffusion_model = mdl
560
+ diffusion_tokenizer = tok
561
+ logger.info(f"[+] Loaded MetaDiffusion manual from HF {repo_id} (vocab {len(tok)})")
562
+ models["CodeSoft/MetaDiffusion-150M-ChatBase"] = mdl # type: ignore
563
+ tokenizers["CodeSoft/MetaDiffusion-150M-ChatBase"] = tok # type: ignore
564
+ return mdl, tok
565
+ except Exception as e:
566
+ logger.warning(f"Manual diffusion load failed: {e}\n{traceback.format_exc()}")
567
+ return None, None
568
 
569
+ LOCAL_PATHS: Dict[str, str] = {}
 
 
 
 
 
 
 
 
 
 
570
 
571
+ def load_models():
572
+ global models, tokenizers, model_load_errors
573
+ # If already populated (including diffusion manual), return
574
+ # But we want to ensure all 5 attempted
575
+ if models and len(models) >= 3:
576
+ # Already loaded, but ensure diffusion tried
577
+ if "CodeSoft/MetaDiffusion-150M-ChatBase" not in models:
578
+ load_diffusion_manual()
579
+ return models, tokenizers
580
 
581
+ logger.info(f"Loading {len(MODEL_IDS)} models on {DEVICE} ...")
582
+ # Try diffusion manual first (bypass HF Auto which fails on unknown type)
583
+ if "CodeSoft/MetaDiffusion-150M-ChatBase" not in models:
584
+ load_diffusion_manual()
585
 
586
+ for mid in MODEL_IDS:
587
+ if mid in models:
588
+ continue # already loaded (diffusion)
589
+ load_id = LOCAL_PATHS.get(mid, mid) if os.path.exists(LOCAL_PATHS.get(mid, "")) else mid
590
+ candidates = [load_id]
591
+ if mid in FALLBACK_IDS:
592
+ candidates.append(FALLBACK_IDS[mid])
593
+ success = False
594
+ last_err = None
595
+ for cand in candidates:
596
+ try:
597
+ logger.info(f"[*] Loading {mid} (candidate {cand})...")
598
+ tok = AutoTokenizer.from_pretrained(cand, trust_remote_code=True)
599
+ if tok.pad_token is None:
600
+ tok.pad_token = tok.eos_token
601
+ mdl = AutoModelForCausalLM.from_pretrained(
602
+ cand,
603
+ trust_remote_code=True,
604
+ torch_dtype=torch.float32,
605
+ low_cpu_mem_usage=True,
606
+ )
607
+ mdl.to(DEVICE)
608
+ mdl.eval()
609
+ tokenizers[mid] = tok
610
+ models[mid] = mdl
611
+ logger.info(f"[+] Loaded {mid} from {cand} (tok vocab {len(tok)})")
612
+ success = True
613
+ break
614
+ except Exception as e:
615
+ last_err = f"{e}\n{traceback.format_exc()}"
616
+ logger.warning(f"Failed to load {mid} from {cand}: {e}")
617
+ continue
618
+ if not success:
619
+ err_msg = f"Failed candidates {candidates}: {last_err}"
620
+ model_load_errors[mid] = err_msg
621
+ logger.warning(f"[!] {mid} failed to load — generation will error. Error: {err_msg[:600]}")
622
+
623
+ logger.info(f"Model loading complete. Loaded: {list(models.keys())} | Failed: {list(model_load_errors.keys())}")
624
+ return models, tokenizers
625
+
626
+ def ensure_models_loaded():
627
+ # Load if not already attempted
628
+ if not models and not model_load_errors:
629
+ load_models()
630
+ elif "CodeSoft/MetaDiffusion-150M-ChatBase" not in models and not model_load_errors.get("CodeSoft/MetaDiffusion-150M-ChatBase"):
631
+ # Try diffusion again if not yet loaded
632
+ load_diffusion_manual()
633
+
634
+ # ---------------------------------------------------------------------------
635
+ # Prompt formatting & generation
636
+ # ---------------------------------------------------------------------------
637
+ def build_inputs(tokenizer, model_id: str, prompt: str):
638
+ ctx = MODEL_CONTEXT.get(model_id, 2048)
639
+ gen_budget = GEN_DEFAULTS.get(model_id, {}).get("max_new_tokens", 128)
640
+ max_prompt_tokens = max(32, ctx - gen_budget - 16)
641
+ try:
642
+ if hasattr(tokenizer, "chat_template") and tokenizer.chat_template is not None:
643
+ messages = [{"role": "user", "content": prompt}]
644
+ inputs = tokenizer.apply_chat_template(
645
+ messages, add_generation_prompt=True, return_tensors="pt", truncation=True, max_length=max_prompt_tokens
646
+ )
647
+ if isinstance(inputs, torch.Tensor):
648
+ inputs = {"input_ids": inputs}
649
+ for k in list(inputs.keys()):
650
+ if isinstance(inputs[k], torch.Tensor):
651
+ inputs[k] = inputs[k].to(DEVICE)
652
+ return inputs
653
+ elif hasattr(tokenizer, "apply_chat_template"):
654
+ try:
655
+ messages = [{"role": "user", "content": prompt}]
656
+ inputs = tokenizer.apply_chat_template(
657
+ messages, add_generation_prompt=True, return_tensors="pt", truncation=True, max_length=max_prompt_tokens
658
+ )
659
+ if isinstance(inputs, torch.Tensor):
660
+ inputs = {"input_ids": inputs}
661
+ for k in list(inputs.keys()):
662
+ if isinstance(inputs[k], torch.Tensor):
663
+ inputs[k] = inputs[k].to(DEVICE)
664
+ return inputs
665
+ except Exception:
666
+ pass
667
+ except Exception as e:
668
+ logger.debug(f"Chat template failed for {model_id}: {e}")
669
+ inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=max_prompt_tokens)
670
+ for k in list(inputs.keys()):
671
+ if isinstance(inputs[k], torch.Tensor):
672
+ inputs[k] = inputs[k].to(DEVICE)
673
+ return inputs
674
+
675
+ def is_diffusion_model(model_id: str) -> bool:
676
+ return "metadiffusion" in model_id.lower()
677
+
678
+ def generate_for_model(model_id: str, prompt: str) -> str:
679
+ ensure_models_loaded()
680
+ if model_id not in models or model_id not in tokenizers:
681
+ short = MODEL_DISPLAY.get(model_id, model_id)
682
+ err = model_load_errors.get(model_id, "model not loaded")
683
+ err_short = str(err).splitlines()[0][:800] if err else "model not loaded"
684
+ return f"[Error: {model_id} not loaded: {err_short}]"
685
+ tokenizer = tokenizers[model_id]
686
+ model = models[model_id]
687
+ cfg = GEN_DEFAULTS.get(model_id, {})
688
+ max_new = cfg.get("max_new_tokens", 128)
689
+ try:
690
+ if is_diffusion_model(model_id):
691
+ return generate_diffusion(model, tokenizer, prompt, cfg) # type: ignore
692
+ inputs = build_inputs(tokenizer, model_id, prompt)
693
+ input_len = inputs["input_ids"].shape[1]
694
+ gen_kwargs = {
695
+ "max_new_tokens": max_new,
696
+ "do_sample": cfg.get("do_sample", True),
697
+ "temperature": cfg.get("temperature", 0.7),
698
+ "top_p": cfg.get("top_p", 0.9),
699
+ "repetition_penalty": cfg.get("repetition_penalty", 1.1),
700
+ "pad_token_id": tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id,
701
+ "eos_token_id": tokenizer.eos_token_id,
702
+ "use_cache": False,
703
+ }
704
+ if "top_k" in cfg:
705
+ gen_kwargs["top_k"] = cfg["top_k"]
706
+ if "no_repeat_ngram_size" in cfg:
707
+ gen_kwargs["no_repeat_ngram_size"] = cfg["no_repeat_ngram_size"]
708
+ ctx = MODEL_CONTEXT.get(model_id, 2048)
709
+ if input_len + max_new > ctx:
710
+ gen_kwargs["max_new_tokens"] = max(16, ctx - input_len - 4)
711
+ with torch.inference_mode():
712
+ outputs = model.generate(**inputs, **gen_kwargs) # type: ignore
713
+ new_tokens = outputs[0, input_len:]
714
+ text = tokenizer.decode(new_tokens, skip_special_tokens=True).strip()
715
+ if not text:
716
+ text = tokenizer.decode(outputs[0], skip_special_tokens=True).strip()
717
+ prompt_text = tokenizer.decode(inputs["input_ids"][0], skip_special_tokens=True).strip()
718
+ if text.startswith(prompt_text):
719
+ text = text[len(prompt_text):].strip()
720
+ return text if text else "[Empty response]"
721
+ except Exception as e:
722
+ logger.error(f"Generation failed for {model_id}: {e}\n{traceback.format_exc()}")
723
+ return f"[Error generating from {MODEL_DISPLAY.get(model_id, model_id)}: {str(e)[:200]}]"
724
+
725
+ def generate_diffusion(model, tokenizer, prompt: str, cfg: dict) -> str:
726
+ try:
727
+ tokenizer = _ensure_diff_chat_tokens(tokenizer)
728
+ messages = [{"role": "user", "content": prompt}]
729
+ prompt_str = _format_diff_messages(messages) + f"\n{DIFF_IM_START}assistant\n"
730
+ prompt_ids = torch.tensor([tokenizer.encode(prompt_str, add_special_tokens=False)], device=DEVICE)
731
+ gen_len = int(cfg.get("max_new_tokens", 96))
732
+ num_steps = int(cfg.get("num_steps", 128))
733
+ temperature = float(cfg.get("temperature", 0.7))
734
+ repetition_penalty = float(cfg.get("repetition_penalty", 1.5))
735
+ max_ctx = MODEL_CONTEXT.get("CodeSoft/MetaDiffusion-150M-ChatBase", 5120)
736
+ if prompt_ids.shape[1] + gen_len > max_ctx:
737
+ gen_len = max(16, max_ctx - prompt_ids.shape[1] - 4)
738
+ if gen_len > 256:
739
+ gen_len = 256
740
+
741
+ for attempt in range(3):
742
+ cur_temp = temperature * (1 + 0.15 * attempt)
743
+ x = _diff_generate_response(
744
+ model, tokenizer, prompt_ids, gen_len, num_steps, cur_temp, repetition_penalty, DEVICE, stop_on_end=True
745
+ )
746
+ response_tokens = x[0, prompt_ids.shape[1]:].tolist()
747
+ response_tokens = _diff_cut_response(response_tokens, tokenizer)
748
+ text = tokenizer.decode(response_tokens, skip_special_tokens=True).strip()
749
+ if text:
750
+ return text
751
+ return "(empty response)"
752
+ except Exception as e:
753
+ logger.warning(f"Diffusion chat failed: {e}\n{traceback.format_exc()}")
754
+ return f"[Diffusion error] {str(e)[:200]}"
755
+
756
+ # ---------------------------------------------------------------------------
757
+ # Gradio UI
758
+ # ---------------------------------------------------------------------------
759
+ CSS = """
760
+ .gradio-container {max-width: 1450px !important; width: 95% !important;}
761
+ .vote-btn {font-weight: 700 !important;}
762
+ /* Leaderboard: prevent ELO wrapping, give it fixed width */
763
+ #leaderboard { overflow-x: auto; }
764
+ #leaderboard table { table-layout: auto; width: 100%; }
765
+ #leaderboard th:nth-child(4), #leaderboard td:nth-child(4) {
766
+ min-width: 95px;
767
+ width: 95px;
768
+ white-space: nowrap;
769
+ text-align: center;
770
+ font-variant-numeric: tabular-nums;
771
+ }
772
+ #leaderboard th:nth-child(1), #leaderboard td:nth-child(1) { min-width: 55px; width: 55px; text-align: center; }
773
+ #leaderboard td { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
774
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
775
 
776
+ def pick_random_pair(exclude_pair: Optional[Tuple[str, str]] = None) -> Tuple[str, str]:
777
+ state = load_elo()
778
+ models_list = MODEL_IDS[:]
779
+ weights = []
780
+ C = 5
781
+ K = 100
782
+ for m in models_list:
783
+ games = state.get(m, {}).get("battles", 0)
784
+ w = K / (games + C)
785
+ weights.append(w)
786
+ a = random.choices(models_list, weights=weights, k=1)[0]
787
+ remaining = [m for m in models_list if m != a]
788
+ remaining_weights = [w for m, w in zip(models_list, weights) if m != a]
789
+ b = random.choices(remaining, weights=remaining_weights, k=1)[0]
790
+ if exclude_pair and set((a, b)) == set(exclude_pair):
791
+ a, b = random.sample(MODEL_IDS, 2)
792
+ return a, b
793
+
794
+ def create_demo() -> gr.Blocks:
795
+ state_init = load_elo()
796
+ df_init = leaderboard_dataframe(state_init)
797
+
798
+ with gr.Blocks(title="SLM Arena") as demo:
799
+ gr.Markdown(
800
+ """
801
+ # ⚔️ SLM Arena
802
+ """
803
+ )
804
+
805
+ last_pair = gr.State(None)
806
+
807
+ with gr.Tabs():
808
+ with gr.Tab("Arena", id=0):
809
+ prompt = gr.Textbox(
810
+ label="Your prompt",
811
+ placeholder="Ask anything... e.g. 'Explain quantum computing in simple terms' or 'Write a haiku about rain'",
812
+ lines=3,
813
+ )
814
+ with gr.Row():
815
+ submit_btn = gr.Button("⚔️ Battle", variant="primary", scale=1)
816
+ clear_btn = gr.Button("Clear", variant="secondary", scale=1)
817
+ with gr.Row():
818
+ with gr.Column():
819
+ response_a = gr.Textbox(
820
+ label="Model A", lines=10, max_lines=14, interactive=False,
821
+ placeholder="Response A will appear here..."
822
+ )
823
+ reveal_a = gr.Markdown(visible=False)
824
+ with gr.Column():
825
+ response_b = gr.Textbox(
826
+ label="Model B", lines=10, max_lines=14, interactive=False,
827
+ placeholder="Response B will appear here..."
828
+ )
829
+ reveal_b = gr.Markdown(visible=False)
830
+
831
+ with gr.Row():
832
+ vote_a = gr.Button("👈 Vote for A", variant="secondary", interactive=False, elem_classes=["vote-btn"])
833
+ vote_tie = gr.Button("🤝 Tie", variant="secondary", interactive=False, elem_classes=["vote-btn"])
834
+ vote_b = gr.Button("Vote for B", variant="secondary", interactive=False, elem_classes=["vote-btn"])
835
+
836
+ status = gr.Markdown(visible=False)
837
+ new_round_btn = gr.Button("🔄 New Round", visible=False, variant="secondary")
838
+
839
+ model_a_state = gr.State("")
840
+ model_b_state = gr.State("")
841
+ voted_state = gr.State(False)
842
+ prompt_state = gr.State("")
843
+
844
+ leaderboard_tab = gr.Tab("Leaderboard", id=1)
845
+ with leaderboard_tab:
846
+ gr.Markdown("### 🏆 ELO Leaderboard")
847
+ leaderboard = gr.Dataframe(
848
+ value=df_init,
849
+ headers=["Rank", "Model", "Model ID", "ELO", "Battles", "Wins", "Losses", "Ties"],
850
+ datatype=["number", "str", "str", "number", "number", "number", "number", "number"],
851
+ interactive=False,
852
+ wrap=False,
853
+ column_widths=["5%", "20%", "35%", "12%", "7%", "7%", "7%", "7%"],
854
+ elem_id="leaderboard",
855
+ )
856
+ with gr.Row():
857
+ refresh_btn = gr.Button("🔄 Refresh", variant="secondary")
858
+
859
+ # -------------------------------------------------------------------
860
+ # Event handlers
861
+ # -------------------------------------------------------------------
862
+ def on_submit(user_prompt: str, last_pair_val):
863
+ user_prompt = (user_prompt or "").strip()
864
+ if not user_prompt:
865
+ return (
866
+ gr.update(value="", placeholder="Please enter a prompt first!"),
867
+ gr.update(value=""),
868
+ gr.update(value=""),
869
+ gr.update(visible=False),
870
+ gr.update(visible=False),
871
+ gr.update(visible=False, value=""),
872
+ gr.update(interactive=False),
873
+ gr.update(interactive=False),
874
+ gr.update(interactive=False),
875
+ gr.update(visible=False),
876
+ "", "", False, user_prompt, last_pair_val,
877
+ leaderboard_dataframe(load_elo())
878
+ )
879
+ a, b = pick_random_pair(exclude_pair=last_pair_val)
880
+ if random.random() < 0.5:
881
+ a, b = b, a
882
+ ensure_models_loaded()
883
+ resp_a = generate_for_model(a, user_prompt)
884
+ resp_b = generate_for_model(b, user_prompt)
885
+ if not resp_a.strip():
886
+ resp_a = "[No output... model returned empty]"
887
+ if not resp_b.strip():
888
+ resp_b = "[No output... model returned empty]"
889
+ return (
890
+ gr.update(value=resp_a),
891
+ gr.update(value=resp_b),
892
+ gr.update(visible=False),
893
+ gr.update(visible=False),
894
+ gr.update(visible=False, value=""),
895
+ gr.update(interactive=True),
896
+ gr.update(interactive=True),
897
+ gr.update(interactive=True),
898
+ gr.update(visible=False),
899
+ a, b, False, user_prompt, (a, b),
900
+ leaderboard_dataframe(load_elo())
901
+ )
902
+
903
+ def on_vote(choice: str, model_a: str, model_b: str, resp_a: str, resp_b: str, user_prompt: str, voted: bool):
904
+ if voted or not model_a or not model_b:
905
+ return (
906
+ gr.update(visible=False),
907
+ gr.update(visible=False),
908
+ gr.update(visible=False, value=""),
909
+ gr.update(interactive=False),
910
+ gr.update(interactive=False),
911
+ gr.update(interactive=False),
912
+ gr.update(visible=False),
913
+ voted,
914
+ leaderboard_dataframe(load_elo())
915
+ )
916
+ if choice == "A":
917
+ winner = model_a
918
+ win_label = "A"
919
+ chosen = "A"
920
+ elif choice == "B":
921
+ winner = model_b
922
+ win_label = "B"
923
+ chosen = "B"
924
+ elif choice == "Tie":
925
+ winner = None
926
+ win_label = "Tie"
927
+ chosen = "tie"
928
+ else:
929
+ winner = model_b
930
+ win_label = "B"
931
+ chosen = "B"
932
+ state = load_elo()
933
+ ra_before = state[model_a]["rating"]
934
+ rb_before = state[model_b]["rating"]
935
+ update_elo(state, model_a, model_b, winner)
936
+ ra_after = state[model_a]["rating"]
937
+ rb_after = state[model_b]["rating"]
938
+ delta_a = ra_after - ra_before
939
+ delta_b = rb_after - rb_before
940
+ reveal_a_text = f"**Model A:** `{model_a}` ({MODEL_DISPLAY.get(model_a, model_a)}) — ELO {ra_after:.1f} ({delta_a:+.1f})"
941
+ reveal_b_text = f"**Model B:** `{model_b}` ({MODEL_DISPLAY.get(model_b, model_b)}) — ELO {rb_after:.1f} ({delta_b:+.1f})"
942
+ if choice == "Tie":
943
+ status_text = (
944
+ f"You voted **Tie**: no winner\n\n"
945
+ f"**ELO update:** {MODEL_DISPLAY.get(model_a, model_a)} {ra_before:.1f} → {ra_after:.1f} ({delta_a:+.1f}) | "
946
+ f"{MODEL_DISPLAY.get(model_b, model_b)} {rb_before:.1f} → {rb_after:.1f} ({delta_b:+.1f})"
947
+ )
948
+ else:
949
+ status_text = (
950
+ f"You voted **{win_label}**: the winner is `{winner}`\n\n"
951
+ f"**ELO update:** {MODEL_DISPLAY.get(model_a, model_a)} {ra_before:.1f} → {ra_after:.1f} ({delta_a:+.1f}) | "
952
+ f"{MODEL_DISPLAY.get(model_b, model_b)} {rb_before:.1f} → {rb_after:.1f} ({delta_b:+.1f})"
953
+ )
954
+ # Log chat to data/chats.jsonl
955
+ log_battle(user_prompt, model_a, model_b, resp_a, resp_b, chosen, winner)
956
+ df = leaderboard_dataframe(state)
957
+ return (
958
+ gr.update(value=reveal_a_text, visible=True),
959
+ gr.update(value=reveal_b_text, visible=True),
960
+ gr.update(value=status_text, visible=True),
961
+ gr.update(interactive=False),
962
+ gr.update(interactive=False),
963
+ gr.update(interactive=False),
964
+ gr.update(visible=True),
965
+ True,
966
+ df
967
+ )
968
+
969
+ def on_new_round():
970
+ return (
971
+ gr.update(value=""),
972
+ gr.update(value=""),
973
+ gr.update(value="", visible=False),
974
+ gr.update(value="", visible=False),
975
+ gr.update(value="", visible=False),
976
+ gr.update(interactive=False),
977
+ gr.update(interactive=False),
978
+ gr.update(interactive=False),
979
+ gr.update(visible=False),
980
+ "", "", False, ""
981
+ )
982
+
983
+ def on_clear():
984
+ return (
985
+ gr.update(value=""),
986
+ gr.update(value=""),
987
+ gr.update(value=""),
988
+ gr.update(value="", visible=False),
989
+ gr.update(value="", visible=False),
990
+ gr.update(value="", visible=False),
991
+ gr.update(interactive=False),
992
+ gr.update(interactive=False),
993
+ gr.update(interactive=False),
994
+ gr.update(visible=False),
995
+ "", "", False, ""
996
+ )
997
+
998
+ def on_refresh():
999
+ return leaderboard_dataframe(load_elo())
1000
+
1001
+ submit_btn.click(
1002
+ fn=on_submit,
1003
+ inputs=[prompt, last_pair],
1004
+ outputs=[response_a, response_b, reveal_a, reveal_b, status, vote_a, vote_tie, vote_b, new_round_btn, model_a_state, model_b_state, voted_state, prompt_state, last_pair, leaderboard],
1005
+ )
1006
+
1007
+ prompt.submit(
1008
+ fn=on_submit,
1009
+ inputs=[prompt, last_pair],
1010
+ outputs=[response_a, response_b, reveal_a, reveal_b, status, vote_a, vote_tie, vote_b, new_round_btn, model_a_state, model_b_state, voted_state, prompt_state, last_pair, leaderboard],
1011
+ )
1012
+
1013
+ vote_a.click(
1014
+ fn=lambda ma, mb, ra, rb, pr, vd: on_vote("A", ma, mb, ra, rb, pr, vd),
1015
+ inputs=[model_a_state, model_b_state, response_a, response_b, prompt_state, voted_state],
1016
+ outputs=[reveal_a, reveal_b, status, vote_a, vote_tie, vote_b, new_round_btn, voted_state, leaderboard],
1017
+ )
1018
+ vote_tie.click(
1019
+ fn=lambda ma, mb, ra, rb, pr, vd: on_vote("Tie", ma, mb, ra, rb, pr, vd),
1020
+ inputs=[model_a_state, model_b_state, response_a, response_b, prompt_state, voted_state],
1021
+ outputs=[reveal_a, reveal_b, status, vote_a, vote_tie, vote_b, new_round_btn, voted_state, leaderboard],
1022
+ )
1023
+ vote_b.click(
1024
+ fn=lambda ma, mb, ra, rb, pr, vd: on_vote("B", ma, mb, ra, rb, pr, vd),
1025
+ inputs=[model_a_state, model_b_state, response_a, response_b, prompt_state, voted_state],
1026
+ outputs=[reveal_a, reveal_b, status, vote_a, vote_tie, vote_b, new_round_btn, voted_state, leaderboard],
1027
+ )
1028
+
1029
+ new_round_btn.click(
1030
+ fn=on_new_round,
1031
+ inputs=[],
1032
+ outputs=[response_a, response_b, reveal_a, reveal_b, status, vote_a, vote_tie, vote_b, new_round_btn, model_a_state, model_b_state, voted_state, prompt_state],
1033
+ )
1034
+ clear_btn.click(
1035
+ fn=on_clear,
1036
+ inputs=[],
1037
+ outputs=[prompt, response_a, response_b, reveal_a, reveal_b, status, vote_a, vote_tie, vote_b, new_round_btn, model_a_state, model_b_state, voted_state, prompt_state],
1038
+ )
1039
+
1040
+ refresh_btn.click(fn=on_refresh, inputs=[], outputs=[leaderboard])
1041
+
1042
+ # Refresh when Leaderboard tab is selected (fixes stale df_init)
1043
+ # Also refresh on page load but without global spinner (demo.load caused "loading..." until refresh when bucket slow)
1044
+ try:
1045
+ leaderboard_tab.select(fn=on_refresh, inputs=[], outputs=[leaderboard])
1046
+ except Exception:
1047
+ pass
1048
+ # Page-load refresh without blocking UI (hidden progress)
1049
+ try:
1050
+ demo.load(fn=on_refresh, inputs=[], outputs=[leaderboard], show_progress="hidden")
1051
+ except Exception:
1052
+ # Fallback: no page-load auto-refresh, rely on tab select + initial df_init (now dynamic via get_data_dir)
1053
+ pass
1054
+
1055
+ return demo
1056
 
1057
+ # ---------------------------------------------------------------------------
1058
+ # Main
1059
+ # ---------------------------------------------------------------------------
1060
  if __name__ == "__main__":
1061
+ print("=" * 60)
1062
+ print("SLM Arena starting, attempting to load 4 models on CPU...")
1063
+ print(f"Models: {MODEL_IDS}")
1064
+ print(f"Data dir: {get_data_dir().resolve()} (bucket /data if mounted)")
1065
+ print("=" * 60)
1066
+ try:
1067
+ load_models()
1068
+ except Exception as e:
1069
+ logger.error(f"Model loading encountered error: {e}")
1070
+ try:
1071
+ df = leaderboard_dataframe(load_elo())
1072
+ print(df.to_string(index=False))
1073
+ print(f"\nChat log: {get_chat_file().resolve()} (exists={get_chat_file().exists()})")
1074
+ if get_chat_file().exists():
1075
+ with open(get_chat_file()) as f:
1076
+ lines = sum(1 for _ in f)
1077
+ print(f"Previous battles logged: {lines}")
1078
+ except Exception as e:
1079
+ logger.warning(f"Leaderboard preview failed: {e}")
1080
+ demo = create_demo()
1081
+ demo.queue(max_size=20)
1082
+ demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True, theme=gr.themes.Base(), css=CSS)