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: 0 additions & 2 deletions deploy/docker-compose.validator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ services:
command:
- epoch
- loop
- --arena
- "${MT_ARENA:?set the arena id}"
- --home
- /var/lib/microtensor
logging:
Expand Down
3 changes: 1 addition & 2 deletions docs/validator_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,14 @@ cd microtensor-subnet/deploy
cat > .env <<EOF
MT_WALLET_NAME=<coldkey>
MT_WALLET_HOTKEY=<hotkey>
MT_ARENA=sql_gen
MT_GPUS=0,1,2
DOCKER_GID=$(getent group docker | cut -d: -f3)
EOF
docker compose -f docker-compose.validator.yml up -d
docker compose -f docker-compose.validator.yml logs -f
```

The loop polls the chain every ten minutes. When an arena's evaluation window opens it downloads the epoch bundle, opens it with your key, sets up the baselines, evaluates the submissions on the signed roster one at a time at their committed revisions, posts each miner's signed result as it finishes, sets weights, and posts the signed epoch report. An epoch already reported is not run again; the record is `/var/lib/microtensor/state/<arena>.epochs.json`.
You do not choose arenas or miners: every ten minutes the loop asks the server which arenas are live on your network, and the signed roster names the miners. When an arena's evaluation window opens it downloads the epoch bundle, opens it with your key, sets up the baselines, evaluates the submissions on the signed roster one at a time at their committed revisions, posts each miner's signed result as it finishes, sets weights, and posts the signed epoch report. An epoch already reported is not run again; the record is `/var/lib/microtensor/state/<arena>.epochs.json`.

## What one epoch does

Expand Down
73 changes: 45 additions & 28 deletions microtensor_subnet/cli/epoch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import argparse
import asyncio
import dataclasses
import json
import logging
import os
Expand Down Expand Up @@ -103,7 +104,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
run.set_defaults(handler=_run)

loop = inner.add_parser("loop", help="run every epoch of an arena as its window opens")
loop.add_argument("--arena", required=True)
loop.add_argument("--server", default=os.environ.get("MT_SERVER_URL", PUBLIC_SERVER_URL))
loop.add_argument("--relay", default=os.environ.get("MT_RELAY_URL", ""))
loop.add_argument(
Expand Down Expand Up @@ -503,7 +503,7 @@ def _settings(args: argparse.Namespace, config: Any) -> EpochConfig:
return EpochConfig(
chain=config,
home=args.home,
arena_id=args.arena,
arena_id=getattr(args, "arena", "") or "",
server_url=args.server,
relay_url=args.relay or args.server,
gpus=tuple(g.strip() for g in str(args.gpus).split(",") if g.strip()),
Expand All @@ -515,6 +515,44 @@ def _settings(args: argparse.Namespace, config: Any) -> EpochConfig:
)


def _poll_arena(
args: argparse.Namespace,
config: Any,
client: Any,
server: ServerClient,
wallet: Any,
state: Path,
arena_id: str,
block: int,
) -> None:
record = state / f"{arena_id}.epochs.json"
done: set[int] = set()
if record.is_file():
done = {int(i) for i in json.loads(record.read_text(encoding="utf-8"))}
arena = server.arena(arena_id, block)
current = dict((arena or {}).get("current_epoch") or {})
index = current.get("index")
if arena is None:
log.warning("arena %s is unknown to %s", arena_id, args.server)
return
if current.get("phase") != "window" or index is None or int(index) in done:
log.info("block %d: %s epoch %s is %s", block, arena_id, index, current.get("phase"))
return
log.info("epoch %s of %s is open at block %d", index, arena_id, block)
settings = dataclasses.replace(_settings(args, config), arena_id=arena_id)
outcome = run_epoch(settings, chain=client, wallet=wallet, epoch_index=int(index))
done.add(int(index))
record.write_text(json.dumps(sorted(done)), encoding="utf-8")
log.info(
"epoch %s of %s done: %d scored, %d refused, %d weights",
index,
arena_id,
len(outcome.scores),
len(outcome.failures),
len(outcome.weights),
)


def _loop(args: argparse.Namespace) -> int:
logging.basicConfig(level=logging.INFO)
config = chain_config(args)
Expand All @@ -523,35 +561,14 @@ def _loop(args: argparse.Namespace) -> int:
server = ServerClient(args.server, hotkey_address(wallet), wallet)
state = Path(args.home) / "state"
state.mkdir(parents=True, exist_ok=True)
record = state / f"{args.arena}.epochs.json"
done: set[int] = set()
if record.is_file():
done = {int(i) for i in json.loads(record.read_text(encoding="utf-8"))}
settings = _settings(args, config)
while True:
try:
block = client.block()
arena = server.arena(args.arena, block)
current = dict((arena or {}).get("current_epoch") or {})
index = current.get("index")
if arena is None:
log.warning("arena %s is unknown to %s", args.arena, args.server)
elif current.get("phase") == "window" and index is not None and int(index) not in done:
log.info("epoch %s of %s is open at block %d", index, args.arena, block)
outcome = run_epoch(settings, chain=client, wallet=wallet, epoch_index=int(index))
done.add(int(index))
record.write_text(json.dumps(sorted(done)), encoding="utf-8")
log.info(
"epoch %s done: %d scored, %d refused, %d weights",
index,
len(outcome.scores),
len(outcome.failures),
len(outcome.weights),
)
else:
log.info(
"block %d: %s epoch %s is %s", block, args.arena, index, current.get("phase")
)
arena_ids = server.live_arenas(config.network)
if not arena_ids:
log.info("block %d: the server lists no live arena on %s", block, config.network)
for arena_id in arena_ids:
_poll_arena(args, config, client, server, wallet, state, arena_id, block)
except (EpochError, ReleaseError, ChainError, ServerError) as exc:
log.error("epoch loop: %s", exc)
if args.once:
Expand Down
12 changes: 12 additions & 0 deletions microtensor_subnet/validator/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
BUNDLE_TIMEOUT_SECONDS: Final[float] = 600.0


ARENA_NETWORKS: dict[str, str] = {"test": "testnet", "finney": "mainnet"}


class ServerError(RuntimeError):
pass

Expand Down Expand Up @@ -64,6 +67,15 @@ def _request(
)
return response.json() if response.content else None

def live_arenas(self, network: str) -> list[str]:
found = self._request("GET", "/v2/arenas") or {}
wanted = ARENA_NETWORKS.get(network, network)
return sorted(
str(a.get("id"))
for a in found.get("arenas") or []
if a.get("state") == "live" and a.get("network") == wanted and a.get("id")
)

def arena(self, arena_id: str, block: int | None = None) -> dict[str, Any] | None:
query = f"?block={block}" if block is not None else ""
found = self._request("GET", f"/v2/arenas/{arena_id}{query}")
Expand Down
Loading