PlixAI commited on
Commit
2757816
verified
1 Parent(s): 8748cce

Create index.py

Browse files
Files changed (1) hide show
  1. index.py +395 -0
index.py ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import bittensor as bt
3
+ import typing
4
+ from bittensor.extrinsics.serving import get_metadata
5
+ from dataclasses import dataclass
6
+ import requests
7
+ import wandb
8
+ import math
9
+ import os
10
+ import datetime
11
+ import time
12
+ import functools
13
+ import multiprocessing
14
+ from dotenv import load_dotenv
15
+ from huggingface_hub import HfApi
16
+ from apscheduler.schedulers.background import BackgroundScheduler
17
+ from tqdm import tqdm
18
+
19
+ load_dotenv()
20
+
21
+ FONT = """<link href="https://fonts.cdnfonts.com/css/jmh-typewriter" rel="stylesheet">"""
22
+ TITLE = """<h1 align="center" id="space-title" class="typewriter">Subnet 6 Leaderboard</h1>"""
23
+ IMAGE = """<a href="https://discord.gg/jqVphNsB4H" target="_blank"><img src="https://i.ibb.co/88wyVQ7/nousgirl.png" alt="nousgirl" style="margin: auto; width: 20%; border: 0;" /></a>"""
24
+ HEADER = """<h2 align="center" class="typewriter"><a href="https://github.com/PlixAI/pixel-subnet-leaderboard" target="_blank">Subnet 17</a> is a <a href="https://bittensor.com/" target="_blank">Bittensor</a> subnet that incentivizes the creation of the best image open models by evaluating submissions on a constant stream of newly generated synthetic MidJourney v6 data. The models with the best <a href="https://github.com/PlixAI/pixel-subnet-leaderboard/blob/master/docs/validator.md" target="_blank">head-to-head loss</a> on the evaluation data receive a steady emission of TAO.</h3>"""
25
+ EVALUATION_DETAILS = """<b>Name</b> is the 馃 Hugging Face model name (click to go to the model card). <b>Rewards / Day</b> are the expected rewards per day for each model. <b>Perplexity</b> is represents the loss on all of the evaluation data for the model as calculated by the validator (lower is better). <b>UID</b> is the Bittensor user id of the submitter. <b>Block</b> is the Bittensor block that the model was submitted in. More stats on <a href="https://taostats.io/subnets/netuid-17/" target="_blank">taostats</a>."""
26
+ EVALUATION_HEADER = """<h3 align="center">Shows the latest internal evaluation statistics as calculated by a validator run by Nous Research</h3>"""
27
+ VALIDATOR_WANDB_PROJECT = os.environ["VALIDATOR_WANDB_PROJECT"]
28
+ H4_TOKEN = os.environ.get("H4_TOKEN", None)
29
+ API = HfApi(token=H4_TOKEN)
30
+ REPO_ID = "PlixAI/pixel-subnet-leaderboard"
31
+ METAGRAPH_RETRIES = 10
32
+ METAGRAPH_DELAY_SECS = 30
33
+ METADATA_TTL = 10
34
+ NETUID = 17
35
+ SUBNET_START_BLOCK = 2225782
36
+ SECONDS_PER_BLOCK = 12
37
+ SUBTENSOR = os.environ.get("SUBTENSOR")
38
+
39
+ @dataclass
40
+ class Competition:
41
+ id: str
42
+ name: str
43
+
44
+ COMPETITIONS = [Competition(id="m6_xl", name="midjourney-6-sdxl")]
45
+ DEFAULT_COMPETITION_ID = "m1"
46
+
47
+ def run_in_subprocess(func: functools.partial, ttl: int) -> typing.Any:
48
+ """Runs the provided function on a subprocess with 'ttl' seconds to complete.
49
+ Args:
50
+ func (functools.partial): Function to be run.
51
+ ttl (int): How long to try for in seconds.
52
+ Returns:
53
+ Any: The value returned by 'func'
54
+ """
55
+
56
+ def wrapped_func(func: functools.partial, queue: multiprocessing.Queue):
57
+ try:
58
+ result = func()
59
+ queue.put(result)
60
+ except (Exception, BaseException) as e:
61
+ # Catch exceptions here to add them to the queue.
62
+ queue.put(e)
63
+
64
+ # Use "fork" (the default on all POSIX except macOS), because pickling doesn't seem
65
+ # to work on "spawn".
66
+ ctx = multiprocessing.get_context("fork")
67
+ queue = ctx.Queue()
68
+ process = ctx.Process(target=wrapped_func, args=[func, queue])
69
+
70
+ process.start()
71
+
72
+ process.join(timeout=ttl)
73
+
74
+ if process.is_alive():
75
+ process.terminate()
76
+ process.join()
77
+ raise TimeoutError(f"Failed to {func.func.__name__} after {ttl} seconds")
78
+
79
+ # Raises an error if the queue is empty. This is fine. It means our subprocess timed out.
80
+ result = queue.get(block=False)
81
+
82
+ # If we put an exception on the queue then raise instead of returning.
83
+ if isinstance(result, Exception):
84
+ raise result
85
+ if isinstance(result, BaseException):
86
+ raise Exception(f"BaseException raised in subprocess: {str(result)}")
87
+
88
+ return result
89
+
90
+
91
+ def get_subtensor_and_metagraph() -> typing.Tuple[bt.subtensor, bt.metagraph]:
92
+ for i in range(0, METAGRAPH_RETRIES):
93
+ try:
94
+ print("Connecting to subtensor...")
95
+ subtensor: bt.subtensor = bt.subtensor(SUBTENSOR)
96
+ print("Pulling metagraph...")
97
+ metagraph: bt.metagraph = subtensor.metagraph(NETUID, lite=False)
98
+ return subtensor, metagraph
99
+ except Exception as e:
100
+ print(e)
101
+ if i == METAGRAPH_RETRIES - 1:
102
+ raise
103
+ print(f"Error connecting to subtensor or pulling metagraph, retry {i + 1} of {METAGRAPH_RETRIES} in {METAGRAPH_DELAY_SECS} seconds...")
104
+ time.sleep(METAGRAPH_DELAY_SECS)
105
+ raise RuntimeError()
106
+
107
+ @dataclass
108
+ class ModelData:
109
+ uid: int
110
+ hotkey: str
111
+ namespace: str
112
+ name: str
113
+ commit: str
114
+ hash: str
115
+ block: int
116
+ incentive: float
117
+ emission: float
118
+ competition: str
119
+
120
+ @classmethod
121
+ def from_compressed_str(cls, uid: int, hotkey: str, cs: str, block: int, incentive: float, emission: float):
122
+ """Returns an instance of this class from a compressed string representation"""
123
+ tokens = cs.split(":")
124
+ return ModelData(
125
+ uid=uid,
126
+ hotkey=hotkey,
127
+ namespace=tokens[0],
128
+ name=tokens[1],
129
+ commit=tokens[2] if tokens[2] != "None" else "",
130
+ hash=tokens[3] if tokens[3] != "None" else "",
131
+ competition=tokens[4] if len(tokens) > 4 and tokens[4] != "None" else DEFAULT_COMPETITION_ID,
132
+ block=block,
133
+ incentive=incentive,
134
+ emission=emission
135
+ )
136
+
137
+ def get_tao_price() -> float:
138
+ for i in range(0, METAGRAPH_RETRIES):
139
+ try:
140
+ return float(requests.get("https://api.mexc.com/api/v3/avgPrice?symbol=TAOUSDT").json()["price"])
141
+ except Exception as e:
142
+ print(e)
143
+ if i == METAGRAPH_RETRIES - 1:
144
+ raise
145
+ time.sleep(METAGRAPH_DELAY_SECS)
146
+ raise RuntimeError()
147
+
148
+ def get_validator_weights(metagraph: bt.metagraph) -> typing.Dict[int, typing.Tuple[float, int, typing.Dict[int, float]]]:
149
+ ret = {}
150
+ for uid in metagraph.uids.tolist():
151
+ vtrust = metagraph.validator_trust[uid].item()
152
+ if vtrust > 0:
153
+ ret[uid] = (vtrust, metagraph.S[uid].item(), {})
154
+ for ouid in metagraph.uids.tolist():
155
+ if ouid == uid:
156
+ continue
157
+ weight = round(metagraph.weights[uid][ouid].item(), 4)
158
+ if weight > 0:
159
+ ret[uid][-1][ouid] = weight
160
+ return ret
161
+
162
+ def get_subnet_data(subtensor: bt.subtensor, metagraph: bt.metagraph) -> typing.List[ModelData]:
163
+ result = []
164
+ for uid in tqdm(metagraph.uids.tolist(), desc="Metadata for hotkeys"):
165
+ hotkey = metagraph.hotkeys[uid]
166
+ try:
167
+ # Wrap calls to the subtensor in a subprocess with a timeout to handle potential hangs.
168
+ partial = functools.partial(get_metadata, subtensor, metagraph.netuid, hotkey)
169
+ metadata = run_in_subprocess(partial, METADATA_TTL)
170
+ except KeyboardInterrupt:
171
+ raise
172
+ except:
173
+ metadata = None
174
+ if not metadata:
175
+ continue
176
+
177
+ commitment = metadata["info"]["fields"][0]
178
+ hex_data = commitment[list(commitment.keys())[0]][2:]
179
+ chain_str = bytes.fromhex(hex_data).decode()
180
+ block = metadata["block"]
181
+ incentive = metagraph.incentive[uid].nan_to_num().item()
182
+ emission = metagraph.emission[uid].nan_to_num().item() * 20 # convert to daily TAO
183
+
184
+ model_data = None
185
+ try:
186
+ model_data = ModelData.from_compressed_str(uid, hotkey, chain_str, block, incentive, emission)
187
+ except:
188
+ continue
189
+
190
+ result.append(model_data)
191
+ return result
192
+
193
+ def floatable(x) -> bool:
194
+ return (isinstance(x, float) and not math.isnan(x) and not math.isinf(x)) or isinstance(x, int)
195
+
196
+ def get_float_score(key: str, history, competition_id: str) -> typing.Tuple[typing.Optional[float], bool]:
197
+ if key in history and "competition_id" in history:
198
+ data = list(history[key])
199
+ if len(data) > 0:
200
+ competitions = list(history["competition_id"])
201
+ while True:
202
+ if competitions.pop() != competition_id:
203
+ data.pop()
204
+ continue
205
+ if floatable(data[-1]):
206
+ return float(data[-1]), True
207
+ else:
208
+ data = [float(x) for x in data if floatable(x)]
209
+ if len(data) > 0:
210
+ return float(data[-1]), False
211
+ break
212
+ return None, False
213
+
214
+ def get_sample(uid, history, competition_id: str) -> typing.Optional[typing.Tuple[str, str, str]]:
215
+ prompt_key = f"sample_prompt_data.{uid}"
216
+ response_key = f"sample_response_data.{uid}"
217
+ truth_key = f"sample_truth_data.{uid}"
218
+ if prompt_key in history and response_key in history and truth_key in history and "competition_id" in history:
219
+ competitions = list(history["competition_id"])
220
+ prompts = list(history[prompt_key])
221
+ responses = list(history[response_key])
222
+ truths = list(history[truth_key])
223
+ while True:
224
+ prompt = prompts.pop()
225
+ response = responses.pop()
226
+ truth = truths.pop()
227
+ if competitions.pop() != competition_id:
228
+ continue
229
+ if isinstance(prompt, str) and isinstance(response, str) and isinstance(truth, str):
230
+ return prompt, response, truth
231
+ break
232
+ return None
233
+
234
+ def get_scores(uids: typing.List[int], competition_id: str) -> typing.Dict[int, typing.Dict[str, typing.Optional[float | str]]]:
235
+ api = wandb.Api()
236
+ runs = list(api.runs(VALIDATOR_WANDB_PROJECT))
237
+
238
+ result = {}
239
+ for run in runs:
240
+ history = run.history()
241
+ for uid in uids:
242
+ if uid in result.keys():
243
+ continue
244
+ perplexity, perplexity_fresh = get_float_score(f"perplexity_data.{uid}", history, competition_id)
245
+ win_rate, win_rate_fresh = get_float_score(f"win_rate_data.{uid}", history, competition_id)
246
+ win_total, win_total_fresh = get_float_score(f"win_total_data.{uid}", history, competition_id)
247
+ weight, weight_fresh = get_float_score(f"weight_data.{uid}", history, competition_id)
248
+ sample = get_sample(uid, history, competition_id)
249
+ result[uid] = {
250
+ "perplexity": perplexity,
251
+ "win_rate": win_rate,
252
+ "win_total": win_total,
253
+ "weight": weight,
254
+ "sample": sample,
255
+ "fresh": perplexity_fresh and win_rate_fresh and win_total_fresh
256
+ }
257
+ if len(result.keys()) == len(uids):
258
+ break
259
+ return result
260
+
261
+ def format_score(uid, scores, key) -> typing.Optional[float]:
262
+ if uid in scores:
263
+ if key in scores[uid]:
264
+ point = scores[uid][key]
265
+ if floatable(point):
266
+ return round(scores[uid][key], 4)
267
+ return None
268
+
269
+ def next_tempo(start_block, tempo, block):
270
+ start_num = start_block + tempo
271
+ intervals = (block - start_num) // tempo
272
+ nearest_num = start_num + ((intervals + 1) * tempo)
273
+ return nearest_num
274
+
275
+ subtensor, metagraph = get_subtensor_and_metagraph()
276
+
277
+ tao_price = get_tao_price()
278
+
279
+ leaderboard_df = get_subnet_data(subtensor, metagraph)
280
+ leaderboard_df.sort(key=lambda x: x.incentive, reverse=True)
281
+
282
+ competition_scores = {
283
+ y.id: get_scores([x.uid for x in leaderboard_df if x.competition == y.id], y.id)
284
+ for y in COMPETITIONS
285
+ }
286
+
287
+ current_block = metagraph.block.item()
288
+ next_update = next_tempo(
289
+ SUBNET_START_BLOCK,
290
+ subtensor.get_subnet_hyperparameters(NETUID).tempo,
291
+ current_block
292
+ )
293
+ blocks_to_go = next_update - current_block
294
+ current_time = datetime.datetime.now()
295
+ next_update_time = current_time + datetime.timedelta(seconds=blocks_to_go * SECONDS_PER_BLOCK)
296
+
297
+ validator_df = get_validator_weights(metagraph)
298
+ weight_keys = set()
299
+ for uid, stats in validator_df.items():
300
+ weight_keys.update(stats[-1].keys())
301
+
302
+ def get_next_update():
303
+ now = datetime.datetime.now()
304
+ delta = next_update_time - now
305
+ return f"""<div align="center" style="font-size: larger;">Next reward update: <b>{blocks_to_go}</b> blocks (~{int(delta.total_seconds() // 60)} minutes)</div>"""
306
+
307
+ def leaderboard_data(show_stale: bool, scores: typing.Dict[int, typing.Dict[str, typing.Optional[float | str]]], competition_id: str):
308
+ value = [
309
+ [
310
+ f'[{c.namespace}/{c.name} ({c.commit[0:8]}, UID={c.uid})](https://huggingface.co/{c.namespace}/{c.name}/commit/{c.commit})',
311
+ format_score(c.uid, scores, "win_rate"),
312
+ format_score(c.uid, scores, "perplexity"),
313
+ format_score(c.uid, scores, "weight"),
314
+ c.uid,
315
+ c.block
316
+ ] for c in leaderboard_df if c.competition == competition_id and (scores[c.uid]["fresh"] or show_stale)
317
+ ]
318
+ return value
319
+
320
+ demo = gr.Blocks(css=".typewriter {font-family: 'JMH Typewriter', sans-serif;}")
321
+ with demo:
322
+ gr.HTML(FONT)
323
+ gr.HTML(TITLE)
324
+ gr.HTML(IMAGE)
325
+ gr.HTML(HEADER)
326
+
327
+ gr.HTML(value=get_next_update())
328
+
329
+ with gr.Tabs():
330
+ for competition in COMPETITIONS:
331
+ with gr.Tab(competition.name):
332
+ scores = competition_scores[competition.id]
333
+ print(scores)
334
+
335
+ class_denominator = sum(leaderboard_df[i].incentive for i in range(0, 10) if leaderboard_df[i].incentive and leaderboard_df[i].competition == competition.id)
336
+
337
+ class_values = {
338
+ f"{leaderboard_df[i].namespace}/{leaderboard_df[i].name} ({leaderboard_df[i].commit[0:8]}, UID={leaderboard_df[i].uid}) 路 ${round(leaderboard_df[i].emission * tao_price, 2):,} (蟿{round(leaderboard_df[i].emission, 2):,})": \
339
+ leaderboard_df[i].incentive / class_denominator for i in range(0, 10) if leaderboard_df[i].incentive and leaderboard_df[i].competition == competition.id
340
+ }
341
+
342
+ gr.Label(
343
+ value=class_values,
344
+ num_top_classes=10,
345
+ )
346
+
347
+ with gr.Accordion("Evaluation Stats"):
348
+ gr.HTML(EVALUATION_HEADER)
349
+
350
+ with gr.Tabs():
351
+ for entry in leaderboard_df:
352
+ if entry.competition == competition.id:
353
+ sample = scores[entry.uid]["sample"]
354
+ if sample is not None:
355
+ name = f"{entry.namespace}/{entry.name} ({entry.commit[0:8]}, UID={entry.uid})"
356
+ with gr.Tab(name):
357
+ gr.Chatbot([(sample[0], sample[1])])
358
+ # gr.Chatbot([(sample[0], f"*{name}*: {sample[1]}"), (None, f"*GPT-4*: {sample[2]}")])
359
+
360
+ show_stale = gr.Checkbox(label="Show Stale", interactive=True)
361
+ leaderboard_table = gr.components.Dataframe(
362
+ value=leaderboard_data(show_stale.value, scores, competition.id),
363
+ headers=["Name", "Win Rate", "Perplexity", "Weight", "UID", "Block"],
364
+ datatype=["markdown", "number", "number", "number", "number", "number"],
365
+ elem_id="leaderboard-table",
366
+ interactive=False,
367
+ visible=True,
368
+ )
369
+ gr.HTML(EVALUATION_DETAILS)
370
+ show_stale.change(lambda x: leaderboard_data(x, scores, competition.id), [show_stale], leaderboard_table)
371
+
372
+ with gr.Accordion("Validator Stats"):
373
+ validator_table = gr.components.Dataframe(
374
+ value=[
375
+ [uid, int(validator_df[uid][1]), round(validator_df[uid][0], 4)] + [validator_df[uid][-1].get(c.uid) for c in leaderboard_df if c.incentive]
376
+ for uid, _ in sorted(
377
+ zip(validator_df.keys(), [validator_df[x][1] for x in validator_df.keys()]),
378
+ key=lambda x: x[1],
379
+ reverse=True
380
+ )
381
+ ],
382
+ headers=["UID", "Stake (蟿)", "V-Trust"] + [f"{c.namespace}/{c.name} ({c.commit[0:8]}, UID={c.uid})" for c in leaderboard_df if c.incentive],
383
+ datatype=["number", "number", "number"] + ["number" for c in leaderboard_df if c.incentive],
384
+ interactive=False,
385
+ visible=True,
386
+ )
387
+
388
+ def restart_space():
389
+ API.restart_space(repo_id=REPO_ID, token=H4_TOKEN)
390
+
391
+ scheduler = BackgroundScheduler()
392
+ scheduler.add_job(restart_space, "interval", seconds=60 * 15) # restart every 15 minutes
393
+ scheduler.start()
394
+
395
+ demo.launch()