Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.10.0
5.0.0
16 changes: 9 additions & 7 deletions docs/Incentive.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,17 +140,19 @@ The normative implementation details and complete metric field glossary live in

### King of the Hill

Discriminator emission is King of the Hill. Each modality has one reigning model. Validators set that lane's weight on the king's registered hotkey every tempo — not on an escrow wallet.
Discriminator emission is King of the Hill. Each modality has one reigning model. Validators set that lane's weight on registered hotkeys every tempo — not on an escrow wallet.

Current split:

- Image king: 40%
- Video king: 40%
- Audio king: 4%
- Image lane: 40%
- Video lane: 40%
- Audio lane: 4%
- Generators: 16%

A challenger takes the crown when it posts a higher `sn34_score` on the **same** `CURRENT_BENCHMARK_VERSION`. The same `file_hash` can refresh its stored score without resetting the reign.
Each discriminator lane is split **85 / 10 / 5** across the current king and the previous two **distinct** crowned hotkeys. If a lane has no previous king, that residual rolls up to the current king (a first king receives the full lane). An unresolvable current king burns its share; an unresolvable previous king rolls to the current king when that UID is registered.

When a new benchmark version is released the current king keeps receiving weights. The throne is marked `defending` until that exact model completes a full re-eval on the new version. Dethroning is frozen during defense. If the re-eval fails or times out (48 hours), the crown goes to the best successful new-version model, or that lane's share burns until one exists.
A challenger takes the crown when it posts an `sn34_score` at least **0.01** higher than the sitting king on the **same** `CURRENT_BENCHMARK_VERSION`. Empty-lane seeding and failed-defense replacement do not use the margin. The same `file_hash` can refresh its stored score without resetting the reign.

Alpha accrues on the king's hotkey while they hold the lane. There is no end-of-round escrow transfer and no pot.
When a new benchmark version is released the current king keeps receiving weights. The throne is marked `defending` until that exact model completes a full re-eval on the new version. Dethroning is frozen during defense. After a successful defense, deferred challengers still need the 0.01 margin. If the re-eval fails or times out (48 hours), the crown goes to the best successful new-version model, or that lane's share burns until one exists.

Alpha accrues on the chain hotkeys while they hold those residual shares. There is no end-of-round escrow transfer and no pot.
2 changes: 1 addition & 1 deletion gas/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "4.10.0"
__version__ = "5.0.0"

version_split = __version__.split(".")
__spec_version__ = (
Expand Down
22 changes: 15 additions & 7 deletions gas/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,8 +454,11 @@ def discriminator():
@click.option("--chain-endpoint", help="Subtensor network endpoint")
@click.option("--retry-delay", default=60, help="Retry delay in seconds")
@click.option("--vertical", type=click.Choice(["general", "human"]), default="general", help="Competition vertical (default: general)")
@click.option("--upload-endpoint", default=None, help="Model upload URL (default: production https://upload.bitmind.ai/upload)")
@click.option("--skip-chain", is_flag=True, help="Exit after a successful upload without writing on-chain metadata")
@click.option("--max-retries", default=3, type=int, help="Max blockchain registration attempts per modality (0 = retry forever)")
def push_discriminator(
image_model, video_model, audio_model, wallet_name, wallet_hotkey, netuid, chain_endpoint, retry_delay, vertical
image_model, video_model, audio_model, wallet_name, wallet_hotkey, netuid, chain_endpoint, retry_delay, vertical, upload_endpoint, skip_chain, max_retries
):
"""Push discriminator model(s) and register on blockchain. At least one model zip file (image, video, or audio) must be provided."""
if not image_model and not video_model and not audio_model:
Expand Down Expand Up @@ -488,14 +491,19 @@ def push_discriminator(

cmd.extend(["--retry-delay", str(retry_delay)])
cmd.extend(["--vertical", vertical])

# Execute the push_model script
cmd.extend(["--max-retries", str(max_retries)])
if upload_endpoint:
cmd.extend(["--upload-endpoint", upload_endpoint])
if skip_chain:
cmd.append("--skip-chain")

# Execute the push_model script. Always propagate its exit code so a
# successful upload does not leave this click process hanging.
try:
result = subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e:
sys.exit(e.returncode)
except Exception as e:
result = subprocess.run(cmd)
except Exception:
sys.exit(1)
sys.exit(result.returncode)


discriminator.add_command(push_discriminator, name="push")
Expand Down
124 changes: 106 additions & 18 deletions gas/koth_weights.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Build on-chain weights for King-of-the-Hill discriminator lanes."""

from typing import Callable, Dict, Iterable, Optional
from typing import Callable, Dict, Iterable, List, Optional

import numpy as np

Expand All @@ -10,6 +10,10 @@
"audio": 0.04,
"generator": 0.16,
}
# Per-lane residual after the current king: previous, then two-back.
# Unused slots (no prior distinct king) roll up to the current king.
KOTH_LANE_RESIDUAL = (0.85, 0.10, 0.05)
KOTH_CHAIN_ROLES = ("current", "previous", "two_back")


def kings_by_modality(payload: Optional[dict]) -> Dict[str, str]:
Expand All @@ -25,6 +29,54 @@ def kings_by_modality(payload: Optional[dict]) -> Dict[str, str]:
return out


def assign_residual_shares(hotkeys: Iterable[str]) -> List[Dict[str, object]]:
"""Map last-N distinct hotkeys to 85/10/5, rolling unused slots to current."""
keys = []
seen = set()
for key in hotkeys:
if not key or key in seen:
continue
keys.append(key)
seen.add(key)
if len(keys) >= len(KOTH_LANE_RESIDUAL):
break
if not keys:
return []
shares = list(KOTH_LANE_RESIDUAL[: len(keys)])
shares[0] += sum(KOTH_LANE_RESIDUAL[len(keys) :])
return [
{
"ss58_address": key,
"share": shares[index],
"role": KOTH_CHAIN_ROLES[index],
}
for index, key in enumerate(keys)
]


def chains_by_modality(payload: Optional[dict]) -> Dict[str, List[Dict[str, object]]]:
"""Last-3 distinct kings per lane, with residual shares recomputed locally."""
if not payload:
return {}
raw = payload.get("chain") or {}
kings = kings_by_modality(payload)
out: Dict[str, List[Dict[str, object]]] = {}
for modality in ("image", "video", "audio"):
members = raw.get(modality) or []
hotkeys = []
for member in members:
if isinstance(member, str):
hotkeys.append(member)
elif isinstance(member, dict):
hotkeys.append(member.get("ss58_address"))
assigned = assign_residual_shares(hotkeys)
if not assigned and kings.get(modality):
assigned = assign_residual_shares([kings[modality]])
if assigned:
out[modality] = assigned
return out


def build_koth_weights(
n: int,
scores: np.ndarray,
Expand All @@ -33,13 +85,25 @@ def build_koth_weights(
uid_for_hotkey: Callable[[str], Optional[int]],
burn_uid: Optional[int] = None,
split: Optional[Dict[str, float]] = None,
chains: Optional[Dict[str, List[Dict[str, object]]]] = None,
) -> np.ndarray:
"""Return a length-n weight vector. Missing kings go to burn_uid.

`uid_for_hotkey` must resolve at current chain head. Escrow addresses are
never used.
never used. Each discriminator lane is 85/10/5 across the current king and
the previous two distinct kings. Unused residual slots roll to the current
king. An unresolvable current king burns its share; unresolvable previous
kings roll to the current king when that UID resolved.
"""
split = dict(split or KOTH_SPLIT)
split = dict(KOTH_SPLIT if split is None else split)
try:
split = {key: float(split[key]) for key in KOTH_SPLIT}
except (KeyError, TypeError, ValueError) as exc:
raise ValueError("Invalid KOTH split") from exc
if any(
not np.isfinite(value) or value < 0 for value in split.values()
) or not np.isclose(sum(split.values()), 1.0):
raise ValueError("Invalid KOTH split")
weights = np.zeros(n, dtype=np.float64)
if len(scores) < n:
scores = np.append(scores, np.zeros(n - len(scores)))
Expand All @@ -49,19 +113,43 @@ def build_koth_weights(
king_uids = set()
burned = 0.0
for modality in ("image", "video", "audio"):
pct = float(split.get(modality, 0.0))
hotkey = kings.get(modality)
if not hotkey:
burned += pct
continue
uid = uid_for_hotkey(hotkey)
if uid is None or uid < 0 or uid >= n:
pct = split[modality]
members = list((chains or {}).get(modality) or [])
if not members:
hotkey = kings.get(modality)
members = assign_residual_shares([hotkey] if hotkey else [])
if not members:
burned += pct
continue
weights[uid] += pct
king_uids.add(uid)

generator_pct = float(split.get("generator", 0.16))
current_uid = None
leftover = 0.0
current_resolved = False
for index, member in enumerate(members):
hotkey = member.get("ss58_address")
share = float(member.get("share") or 0.0)
if share <= 0:
continue
uid = uid_for_hotkey(hotkey) if hotkey else None
if uid is None or uid < 0 or uid >= n:
if index == 0:
burned += pct * share
else:
leftover += share
continue
weights[uid] += pct * share
king_uids.add(uid)
if index == 0:
current_uid = uid
current_resolved = True

if leftover > 0:
if current_resolved and current_uid is not None:
weights[current_uid] += pct * leftover
else:
burned += pct * leftover

generator_pct = split["generator"]
active = [uid for uid in generator_uids if 0 <= uid < n and uid not in king_uids]
if active and generator_pct > 0:
gen_scores = np.array([max(float(scores[uid]), 0.0) for uid in active])
Expand All @@ -74,11 +162,11 @@ def build_koth_weights(
else:
burned += generator_pct

if (
burned > 0
and burn_uid is not None
and 0 <= burn_uid < n
):
if burned > 0:
if burn_uid is None or not 0 <= burn_uid < n:
raise ValueError(
f"Cannot allocate {burned:.4f} burn weight: burn UID is unavailable"
)
weights[burn_uid] += burned
Comment thread
cursor[bot] marked this conversation as resolved.

return weights
Loading
Loading