import argparse import gradio as gr import bittensor as bt from typing import Dict, List, Any, Optional, Tuple from dataclasses import dataclass import wandb import math import os import datetime import time import json from dotenv import load_dotenv from huggingface_hub import HfApi from apscheduler.schedulers.background import BackgroundScheduler import pandas as pd from tqdm import tqdm import numpy as np from substrateinterface import Keypair load_dotenv() FONT = ( """""" ) TITLE = """

Subnet 32 Leaderboard

""" HEADER = """

Subnet 32 is a Bittensor subnet that incentivizes the development of distributed solutions aimed at identifying LLM-generated content. Reward calculation integrates F1 score, False Positive score, and Average Precision score to accurately evaluate model performance.

""" EVALUATION_DETAILS = """
More stats on taostats.""" EVALUATION_HEADER = """

Shows the latest internal evaluation statistics as calculated by the "OpenTensor Foundation" validator

""" VALIDATOR_WANDB_PROJECT = "itsai-dev/subnet32" H4_TOKEN = os.environ.get("H4_TOKEN", None) API = HfApi(token=H4_TOKEN) WANDB_TOKEN = os.environ.get("WANDB_API_KEY", None) SUBTENSOR_ENDPOINT=os.environ.get("SUBTENSOR_ENDPOINT", None) REPO_ID = "Infin/ai-detection-leaderboard" MAX_AVG_LOSS_POINTS = 1 RETRIES = 5 DELAY_SECS = 3 NETUID = 32 UID_MAIN_VALIDATOR = 33 BENCHMARK_TOP_AMOUNT = 3 EVALUATION_STATS_AMOUNT = 5 @dataclass class ModelData: uid: int coldkey: str hotkey: str incentive: float emission: float @classmethod def from_compressed_str( cls, uid: int, coldkey: str, hotkey: str, incentive: float, emission: float, ): """Returns an instance of this class from a compressed string representation""" return ModelData( uid=uid, coldkey=coldkey, hotkey=hotkey, incentive=incentive, emission=emission, ) def run_with_retries(func, *args, **kwargs): for i in range(0, RETRIES): try: return func(*args, **kwargs) except (Exception, RuntimeError): if i == RETRIES - 1: raise time.sleep(DELAY_SECS) raise RuntimeError("Should never happen") def get_subtensor_and_metagraph() -> Tuple[bt.subtensor, bt.metagraph]: def _internal() -> Tuple[bt.subtensor, bt.metagraph]: if SUBTENSOR_ENDPOINT: parser = argparse.ArgumentParser() bt.subtensor.add_args(parser) subtensor = bt.subtensor(config=bt.config(parser=parser, args=["--subtensor.chain_endpoint", SUBTENSOR_ENDPOINT])) else: subtensor = bt.subtensor("finney") metagraph = subtensor.metagraph(NETUID, lite=False) return subtensor, metagraph return run_with_retries(_internal) def get_validator_weights( metagraph: bt.metagraph, ) -> Dict[int, Tuple[float, int, Dict[int, float]]]: """Returns a dictionary of validator UIDs to (vtrust, stake, {uid: weight}).""" ret = {} for uid in metagraph.uids.tolist(): vtrust = metagraph.validator_trust[uid].item() if vtrust > 0: ret[uid] = (vtrust, metagraph.S[uid].item(), {}) for ouid in metagraph.uids.tolist(): if ouid == uid: continue weight = round(metagraph.weights[uid][ouid].item(), 6) if weight > 0: ret[uid][-1][ouid] = weight return ret def get_subnet_data( metagraph: bt.metagraph ) -> List[ModelData]: result = [] for uid in tqdm(metagraph.uids.tolist()): if metagraph.validator_trust[uid] != 0: continue coldkey = metagraph.coldkeys[uid] hotkey = metagraph.hotkeys[uid] incentive = metagraph.incentive[uid] emission = ( metagraph.emission[uid] * 20 ) # convert to daily TAO model_data = None try: model_data = ModelData.from_compressed_str( uid, coldkey, hotkey, incentive, emission ) except: continue result.append(model_data) return result def is_floatable(x) -> bool: return ( isinstance(x, float) and not math.isnan(x) and not math.isinf(x) ) or isinstance(x, int) def get_wandb_runs( project: str, filters: Dict[str, Any] ) -> List: """Get the latest runs from Wandb, retrying infinitely until we get them.""" while True: api = wandb.Api(api_key=WANDB_TOKEN) runs = list( api.runs( project, order="-created_at", filters=filters, ) ) print('Runs amount: ', len(runs)) if len(runs) > 0: return runs # WandDB API is quite unreliable. Wait another minute and try again. print("Failed to get runs from Wandb. Trying again in 60 seconds.") time.sleep(10) def is_in_last_7_days(current_time, unix_timestamp): seven_days_ago = current_time - 7 * 24 * 60 * 60 return seven_days_ago <= unix_timestamp <= current_time def is_hash_repeated(current_hash: str, current_index: int, wandb_runs: List): for run in wandb_runs[current_index+1:]: if 'signed_msg' not in run.summary: continue if run.summary['signed_msg'] == current_hash: return True return False def get_scores( wandb_runs: List, hotkeys: List[str] ) -> Dict[int, Dict[str, Optional[float]]]: result = {} # result = [] previous_timestamp = None # Iterate through the runs until we've processed all the uids. current_time = time.time() for i, run in enumerate(wandb_runs): config_data = run.config vali_uid = config_data['uid'] # get only last run of each vali except OTF if int(vali_uid) == UID_MAIN_VALIDATOR: if len(result.get(vali_uid, [])) >= 5: continue else: if vali_uid in result: continue if run.history().empty: continue data = json.loads(run.summary["original_format_json"]) if vali_uid > len(hotkeys): print('VALI UID EXCEEDS NUMBER OF HOTKEYS') continue if not is_in_last_7_days(current_time, int(data['timestamp'])): print("TIMESTAMP NOT IN LAST 7 DAYS", data['timestamp']) continue keypair = Keypair(ss58_address=hotkeys[vali_uid]) if 'signed_msg' not in run.summary: continue s = time.time() if is_hash_repeated(run.summary['signed_msg'], i, wandb_runs): print("THIS HASH ALREADY BEEN SEEN IN RUNS", run.summary['signed_msg']) continue print("TIME CONSUMED FOR HASH CHECKING", int(time.time() - s)) verify_result = keypair.verify(run.summary["original_format_json"], run.summary['signed_msg']) print(vali_uid, hotkeys[vali_uid]) print('Signature is correct: ', verify_result) if not verify_result: print("SIGNATURE IS BROKEN") continue timestamp = data["timestamp"] # Make sure runs are indeed in descending time order. # assert ( # previous_timestamp is None or timestamp < previous_timestamp # ), f"Timestamps are not in descending order: {timestamp} >= {previous_timestamp}" previous_timestamp = timestamp if vali_uid not in result: result[vali_uid] = [] local_vali_uid_iter = {} for miner_data in list(data['uid_metrics'].values()): local_vali_uid_iter[miner_data['uid']] = {} local_vali_uid_iter[miner_data['uid']].update(miner_data) result[vali_uid].append(local_vali_uid_iter) return result def average_scores(data: List): stats = {} for item in data: for key, values in item.items(): if key not in stats: stats[key] = {'sums': {'reward': 0, 'fp_score': 0, 'f1_score': 0, 'ap_score': 0, 'penalty': 0}, 'count': 0} stats[key]['uid'] = values['uid'] stats[key]['weight'] = values['weight'] stats[key]['sums']['reward'] += values['reward'] stats[key]['sums']['fp_score'] += values['fp_score'] stats[key]['sums']['f1_score'] += values['f1_score'] stats[key]['sums']['ap_score'] += values['ap_score'] stats[key]['sums']['penalty'] += values['penalty'] stats[key]['count'] += 1 averages = {} for key, data in stats.items(): averages[key] = {field: data['sums'][field] / data['count'] for field in data['sums']} averages[key] = {**averages[key], 'uid': data['uid'], 'weight': data['weight']} return averages def format_score(uid: int, scores, key) -> Optional[float]: if uid in scores: if key in scores[uid]: point = scores[uid][key] if is_floatable(point): return round(scores[uid][key], 6) return None def next_epoch(subtensor: bt.subtensor, block: int) -> int: return ( block + subtensor.get_subnet_hyperparameters(NETUID).tempo - subtensor.blocks_since_epoch(NETUID, block) ) def get_last_updated_div() -> str: return f"""
Last Updated: {datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")} (UTC)
""" def leaderboard_data( leaderboard: List[ModelData], scores: Dict[int, Dict[str, Optional[float]]], show_stale: bool, ) -> List[List[Any]]: """Returns the leaderboard data, based on models data and UID scores.""" # headers=["Top", "UID", "Reward", "F1 Score", "FP Score", "AP Score"], rows = [ [ f"{c.coldkey[:12]}", c.uid, format_score(c.uid, scores, "reward"), format_score(c.uid, scores, "f1_score"), format_score(c.uid, scores, "fp_score"), format_score(c.uid, scores, "ap_score"), ] for c in leaderboard if c.uid in scores ] sorted_rows = sorted(rows, key=lambda x: (x[2], x[0]), reverse=True) if not show_stale: sorted_rows = sorted_rows[:EVALUATION_STATS_AMOUNT] return sorted_rows def restart_space(): API.restart_space(repo_id=REPO_ID, token=H4_TOKEN) def main(): # To avoid leaderboard failures, infinitely try until we get all data # needed to populate the dashboard while True: # try: subtensor, metagraph = get_subtensor_and_metagraph() model_data: List[ModelData] = get_subnet_data(metagraph) model_data.sort(key=lambda x: x.incentive, reverse=False) time_now = datetime.datetime.now(datetime.timezone.utc) n_days_ago = time_now - datetime.timedelta(hours=24*7) vali_runs = get_wandb_runs(project=VALIDATOR_WANDB_PROJECT, filters={ "$and": [ { 'created_at': { '$gte': n_days_ago.isoformat() } }, { 'state': { "$in": ["finished"] } }, # { # 'config.version': { # '$in': ["2.5.0", "2.6.0", "3.0.0", "3.0.1"] # # } # }, { 'config.uid': { '$nin': [86, 50, 106] } } ] }) s = time.time() scores = get_scores(vali_runs, metagraph.hotkeys) print('TIME FOR get_scores()', time.time() - s) averaged_scores = {} for vali_uid, vali_score in scores.items(): if len(vali_score) > 1: averaged_scores.update({vali_uid: average_scores(vali_score)}) else: averaged_scores.update({vali_uid: vali_score[0]}) scores = averaged_scores rows = [] for validator_uid, miners in scores.items(): for miner_uid, stats in miners.items(): row = {'validator_uid': validator_uid} row.update({k: v for k, v in stats.items()}) rows.append(row) miners_stats = pd.DataFrame(rows) miners_stats['stake_value'] = miners_stats['validator_uid'].apply(lambda x: metagraph.S[x].item()) miners_stats = miners_stats.sort_values(by='stake_value', ascending=False) miners_stats = miners_stats.drop(columns='stake_value') miners_stats.to_csv('miners_stats.csv', index=False) main_validator_scores = scores[UID_MAIN_VALIDATOR] sorted_main_validator_scores = sorted(main_validator_scores.items(), key=lambda x: x[1]['reward'], reverse=True) axons_info = metagraph.axons top_miners = [] top_keys = [] for score_i in sorted_main_validator_scores: if len(top_miners) >= BENCHMARK_TOP_AMOUNT: break coldkey_i = axons_info[score_i[0]].coldkey if coldkey_i in top_keys: continue top_keys.append(coldkey_i) top_miners.append(score_i) top_miner_score = dict(top_miners) data_list = [{'Model': f'miner_{key}', 'Average': value['reward'], 'F1 score': value['f1_score'], 'FP score': value['fp_score'], 'AP score': value['ap_score']} for key, value in top_miner_score.items()] df = pd.DataFrame(data_list) baseline_data = [ {'Model': 'baseline: deberta', 'F1 score': 0.863, 'FP score': 0.896, 'AP score': 0.789, 'Average': 0.849} ] benchmarks = pd.concat([df, pd.DataFrame(baseline_data)], ignore_index=True) benchmarks = benchmarks.sort_values(by='Average', ascending=False) validator_df = get_validator_weights(metagraph) break # except Exception as e: # print(f"Failed to get data: {e}") # time.sleep(30) demo = gr.Blocks(css=".typewriter {font-family: 'JMH Typewriter', sans-serif;}") with demo: gr.HTML(FONT) gr.HTML(TITLE) gr.HTML(HEADER) if benchmarks is not None: with gr.Accordion("Top Model Benchmarks"): gr.components.Dataframe(benchmarks) gr.HTML("""
Fore more information about baseline miner architecture see here for the full code.
""") with gr.Accordion("Evaluation Stats"): gr.HTML(EVALUATION_HEADER) show_stale = gr.Checkbox(label="Show All Miners", interactive=True) leaderboard_table = gr.components.Dataframe( value=leaderboard_data(model_data, main_validator_scores, show_stale.value), headers=["Coldkey", "UID", "Reward", "F1 Score", "FP Score", "AP Score"], datatype=["markdown", "number", "number", "number", "number", "number"], elem_id="leaderboard-table", interactive=False, visible=True, ) gr.HTML(EVALUATION_DETAILS) show_stale.change( lambda stale: leaderboard_data(model_data, main_validator_scores, stale), inputs=[show_stale], outputs=leaderboard_table, ) with gr.Accordion("Validator Stats"): model_data.sort(key=lambda x: x.uid, reverse=False) values = [ [uid, int(validator_df[uid][1]), round(validator_df[uid][0], 6)] + [ validator_df[uid][-1].get(c.uid) for c in model_data if c.incentive ] for uid, _ in sorted( zip( validator_df.keys(), [validator_df[x][1] for x in validator_df.keys()], ), key=lambda x: x[1], reverse=True, ) ] print("VALUES:", values) averages = np.nanmean(np.array(values, dtype=float)[:, 3:], axis=0) averages = np.around(averages, decimals=6) values.append(["Average", 0, 0] + averages.tolist()) gr.components.Dataframe( value=values, headers=["UID", "Stake (τ)", "V-Trust"] + [ f"{c.uid}/reward" for c in model_data if c.incentive ] , datatype=["markdown", "number", "number"] + ["number" for c in model_data if c.incentive] , interactive=False, visible=True, ) def get_miner_stats(uid): local_stats = miners_stats[miners_stats['uid'] == int(uid)] local_stats.drop('penalty', axis=1, inplace=True) local_stats = local_stats[["validator_uid","uid","weight", "reward","fp_score","f1_score","ap_score"]] local_stats.columns = ["Validator UID", "UID", "Weight", "Reward", "FP Score", "F1 Score", "AP Score"] # local_stats = local_stats[["Validator UID", "UID", "Weight", "Reward", "FP Score", "F1 Score", "AP Score"]] return local_stats with gr.Accordion("Get your miner stats"): with gr.Row(): input_text = gr.Textbox(label="Enter your Miner ID") submit_button = gr.Button("Get Stats") output_df = gr.components.DataFrame( headers=["Validator UID", "Miner UID"] + ["Weight", "Reward"] + ["FP Score", "F1 Score", "AP Score"], datatype=["number", "number"] + ["number", "number", "number", "number", "number"], interactive=False, visible=True ) submit_button.click( fn=get_miner_stats, inputs=input_text, outputs=output_df ) gr.HTML(value=get_last_updated_div()) scheduler = BackgroundScheduler() scheduler.add_job( restart_space, "interval", seconds=60 * 60 * 24 ) # restart every 45 minutes scheduler.start() demo.launch() main()