Skip to content

validator: report each miner's evaluation stage to the server as it runs - #179

Merged
BANADDA merged 4 commits into
mainfrom
evaluation-progress
Oct 28, 2026
Merged

BANADDA merged 4 commits into
mainfrom
evaluation-progress

Conversation

@BANADDA

@BANADDA BANADDA commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

What this adds

The validator now tells the server where each miner is in its evaluation, so the Submissions page drawer can show it live. Nothing it sends carries a score of any kind: no pool scores, lower bounds, points, eligibility, item ids, item content or counts that depend on hidden set outcomes. It sends only a stage name, a done and total count against the arena's published pool size, and a refusal reason.

Stages, in order: queued, fetching, checking, slm, harness, escalation, system, scoring, sent, refused.

Wire

POST /v2/reports/progress, signed exactly like POST /v2/reports/artifact (same _request(..., signed=True) path, so the same hotkey, timestamp and signature headers and the same nonce). Body:

{"arena_id": "...", "epoch_index": 12, "miner": "5F...", "stage": "slm", "done": 14, "total": 40, "note": ""}

note is only filled for refused and is cut to 300 characters. total is never below done.

Reporter (validator/progress.py)

  • One daemon worker thread per epoch, started on the first post, fed by a bounded queue of 256 that drops the oldest entry when full.
  • Each post uses a 5 second timeout; any error is logged at debug level and dropped. Evaluation never waits on the server and never fails because of it.
  • Per miner throttling: updates inside the same pool go out at most every 15 seconds; a stage change and the final item (done == total) always go out.
  • Built in run_epoch with enabled=not config.dry_run, so a dry run posts nothing, and handed to set_up_baselines. The Runner default is a disabled reporter, so anything building a Runner directly is unaffected.
  • Runner.close() closes the reporter; the worker drains what is queued and exits. If the queue or the baseline setup fails, run_epoch closes the reporter and raises as before.

Totals are published pool sizes

Every pool reports against EpochContext.pool_size(test), the arena definition's item_counts entry for that test (the counts the server cuts each batch with), falling back to the number of original items only when the definition has none. A Tally takes that total plus the number of steps the loop really runs and scales done onto the total, so done reaches total exactly when the pool ends and never reflects how many items a hidden outcome selected.

  • SLM: one step per original item; twins run inside their original's step.
  • Harness: the miner condition, the bare baseline and every reference variant are steps inside the one published harness total. A variant that fails to start advances done by its share, so the pool still ends at total.
  • Escalation: the loop runs only over the reference SLM's wrong drafts, but the total is the published escalation size, so the wrong draft count is not revealed.
  • System: every floor times every item, inside the published system total.

Hooks (validator/epoch.py, validator/collect.py)

  • run_epoch builds the queue right after load_context and posts queued (done = position + 1, total = queue length) for every queued miner and refused with the reason for every refused hotkey on the roster, before set_up_baselines pulls images, downloads bases, starts the probe agent and runs the reference. The same queue then feeds miner_loop.
  • fetch_and_check: fetching before the artifact download. collect.materialise takes an on_downloaded callback, called once the artifact repository is on disk and before the manifest is read, which posts checking. The stage stays checking through the SLM repository download and checks, the weights digest, turn order and the duplicate claim.
  • slm_phase posts slm at (0, total) before the SLM sandbox opens, so model loading shows under slm.
  • Pools: the SLM, harness, escalation and system runners take an optional progress callback, called with (done, total). The Tally wraps the loop's iterable, so the loop bodies (including their continue branches) are unchanged.
  • scoring before score_miner, then sent once the signed result is accepted, or refused with the failure as the note. This lives in post_result, so the close time retry reports too.

Hot copy into a running validator

The image installs the package in editable mode at /opt/microtensor, so copying these and restarting the container is enough:

  • microtensor_subnet/validator/progress.py (new)
  • microtensor_subnet/validator/client.py
  • microtensor_subnet/validator/collect.py
  • microtensor_subnet/validator/context.py
  • microtensor_subnet/validator/epoch.py
  • microtensor_subnet/validator/pools/slm.py
  • microtensor_subnet/validator/pools/harness.py
  • microtensor_subnet/validator/pools/escalation.py
  • microtensor_subnet/validator/pools/system.py

collect.py on this branch already carries #180 and reads contract.SUBMISSION_LICENCE, so a container built before #180 also needs microtensor_subnet/artifact/contract.py from main, and one built before #178 also needs microtensor_subnet/artifact/validate.py.

Until the server route ships, posts get a 404 or 405 and are dropped at debug level, so this is safe to roll out first.

Checks

ruff check, ruff format and mypy are clean; the local suite passes, with four focused local tests for the scaled tally, the throttle, the stage sequence through the miner loop and queued going out before the baseline setup.

🤖 Generated with Claude Code

@BANADDA
BANADDA merged commit a1a4fad into main Oct 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant