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 @@
5.0.8
5.0.10
54 changes: 54 additions & 0 deletions docs/Generative-Mining.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,60 @@ gascli generator restart
gascli generator delete
```

### Checking Your Performance

```bash
gascli g perf # Image/video charts and verification stats
gascli g perf --modality image # Image only
gascli g perf --lookback-days 14 # Two weeks of chart history
gascli g perf --json # Raw API response, without terminal formatting
gascli g perf --no-chain # Skip chain RPC; keep benchmark charts
gascli g perf --netuid 379 --chain-endpoint test # Explicit testnet chain context
```

Rich panels and Plotext's high-resolution curves show **sample-weighted rolling
seven-day fool rates**, with muted threshold lines and dated axes. Each modality
needs at least 20 benchmark evaluations and a
rate strictly above its threshold: 2% for images, 1% for videos. Changing
`--lookback-days` changes the chart span (and the legacy aggregate window), not
the seven-day qualification window.

Counts are evaluations, not unique media: the same media can be evaluated in
multiple benchmark runs. Missing data is shown as a gap, not a zero. Historical
points are reconstructed from currently available benchmark results; they are
not a record of what validators knew or paid at that time.

Fool-rate eligibility is **not a guarantee of on-chain incentive**. Verified
activity, validator updates, and weight reveal/epoch timing also affect rewards.
Not participating in video does not disqualify an image miner. The CLI does not
infer participation from missing benchmark data.

The on-chain panel reads the selected hotkey's current normalized incentive
score (not a token amount), current revealed weights toward it, and the latest
submission block/time for the five largest permitted validators by stake.
Amber vertical chart markers match those submission timestamps. These are the
**latest submission per validator, not a complete event history**. On commit-reveal
subnets, `LastUpdate` records commits, not reveals; the currently revealed weight
may belong to an earlier commit. No pending-payment or reveal ETA is inferred.

Chain values are read at one metagraph block, with its timestamp shown separately
from the benchmark snapshot. Timestamp lookups older than 7,200 blocks are skipped
to avoid archive-node delays; their exact submission blocks remain visible.
Missing/failed RPC data is shown as unknown, not zero. `--chain-timeout` bounds the
lookup (default 30 seconds); partial snapshots remain useful on timeout.
`--netuid` / `--chain-endpoint` override `BT_NETUID` / `BT_CHAIN_ENDPOINT`, falling
back to subnet 34 on finney. Select the correct network when using a non-production
API. `--json` remains the raw API response and does not perform chain queries.

The API supplies the chart history. Older API deployments still work, displaying
the aggregate summary until the history addition is deployed. Existing CLI
versions continue to read the unchanged verification and aggregate fields.

Charts use the available terminal width; very narrow terminals show the numbers
without a plot. Redirected output remains free of color escape sequences, and
`NO_COLOR` disables colors. To preview the layout without a wallet or API call,
run `python -m scripts.preview_generator_performance --width 100` (demo data).

### API Endpoints

Your miner exposes these endpoints for validators:
Expand Down
2 changes: 1 addition & 1 deletion gas/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "5.0.8"
__version__ = "5.0.10"

version_split = __version__.split(".")
__spec_version__ = (
Expand Down
6 changes: 5 additions & 1 deletion gas/cache/content_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,10 @@ def get_verification_stats_last_n_hours(

Returns:
Dict mapping miner hotkey to verification stats, with per-modality breakdown.

Raises:
Exception: If reward data cannot be read or aggregated. Callers must
not interpret unavailable data as a successfully read empty window.
"""
try:
return self.challenges.get_outcome_stats_last_n_hours(
Expand All @@ -731,7 +735,7 @@ def get_verification_stats_last_n_hours(
bt.logging.error(f"Error getting verification stats for last {lookback_hours}h: {e}")
import traceback
bt.logging.error(traceback.format_exc())
return {}
raise

def upload_batch_to_huggingface(
self,
Expand Down
4 changes: 3 additions & 1 deletion gas/cache/db/challenge_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ def get_outcomes_last_n_hours(

Reward scoring must not let other miners' newer outcomes displace
eligible work within the requested time window.

Read failures propagate: an unavailable window is not an empty window.
"""
try:
cutoff = time.time() - (lookback_hours * 3600)
Expand Down Expand Up @@ -197,7 +199,7 @@ def get_outcomes_last_n_hours(
]
except Exception as e:
bt.logging.error(f"Error getting recent challenge outcomes: {e}")
return []
raise

def get_outcome_stats_last_n_hours(
self, lookback_hours: float = 2.0, limit: Optional[int] = None
Expand Down
32 changes: 25 additions & 7 deletions gas/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -896,10 +896,14 @@ def info():
help="Bittensor hotkey name (default: BT_WALLET_HOTKEY from .env.gen_miner, else 'default')",
)
@click.option("--modality", type=click.Choice(["image", "video", "audio"]), default=None, help="Filter fool-rate aggregate by modality")
@click.option("--lookback-days", default=7, type=int, show_default=True, help="Fool-rate parquet lookback (days)")
@click.option("--lookback-days", default=7, type=click.IntRange(1, 90), show_default=True, help="Chart/aggregate lookback; qualification always uses 7 days")
@click.option("--api-url", default=None, help="GAS API base URL (default: GAS_API_URL env or production)")
@click.option("--chain/--no-chain", default=True, help="Show current incentive and latest validator submissions (text output only)")
@click.option("--netuid", type=click.IntRange(0, 65535), default=None, help="Chain subnet (default: BT_NETUID or 34)")
@click.option("--chain-endpoint", default=None, help="Chain network/endpoint (default: BT_CHAIN_ENDPOINT or finney)")
@click.option("--chain-timeout", type=click.IntRange(1, 120), default=30, show_default=True, help="Maximum seconds for the optional chain lookup")
@click.option("--json", "as_json", is_flag=True, help="Print raw API JSON")
def gen_perf(wallet_name, wallet_hotkey, modality, lookback_days, api_url, as_json):
def gen_perf(wallet_name, wallet_hotkey, modality, lookback_days, api_url, chain, netuid, chain_endpoint, chain_timeout, as_json):
"""Query your generator verification stats + aggregated fool rate (Epistula-authenticated GAS API).

The request is signed with your hotkey (same Epistula flow as discriminator ``d perf``).
Expand All @@ -920,10 +924,6 @@ def gen_perf(wallet_name, wallet_hotkey, modality, lookback_days, api_url, as_js
sys.exit(1)

addr = wallet.hotkey.ss58_address
click.echo()
click.echo(f" ⛽ generator {addr}")
click.echo()

result = fetch_generator_performance(
wallet,
modality=modality,
Expand All @@ -940,6 +940,24 @@ def gen_perf(wallet_name, wallet_hotkey, modality, lookback_days, api_url, as_js
click.echo(json_lib.dumps(data, indent=2, default=str))
return

chain_data = {"status": "disabled"}
if chain:
from gas.protocol.generator_chain import fetch_generator_chain
selected_netuid = netuid if netuid is not None else click.IntRange(0, 65535).convert(
os.environ.get("BT_NETUID", "34"), None, click.get_current_context())
chain_data = fetch_generator_chain(
addr, network=chain_endpoint or os.environ.get("BT_CHAIN_ENDPOINT") or "finney",
netuid=selected_netuid, timeout=chain_timeout)

from gas.utils.generator_performance import render_fool_history, render_chain_context
if render_fool_history(data, modality, hotkey=addr, lookback_days=lookback_days, chain=chain_data):
return
render_chain_context(chain_data)
click.echo()
click.echo(f" ⛽ generator {addr}")
click.echo(" History unavailable from this API; showing aggregate only.")
click.echo()

ver = data.get("verification") or {}
fool = data.get("fool_aggregate") or {}

Expand Down Expand Up @@ -967,7 +985,7 @@ def gen_perf(wallet_name, wallet_hotkey, modality, lookback_days, api_url, as_js
click.echo(" (no verification rows yet)")
click.echo()

click.echo(f" Fool rate aggregate (benchmark parquets, last {lookback_days}d)")
click.echo(f" Fool rate aggregate (benchmark evaluations, last {lookback_days}d)")
ts = fool.get("total_samples", 0)
if ts:
click.echo(
Expand Down
28 changes: 27 additions & 1 deletion gas/evaluation/generator_scores.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,24 @@


class GeneratorScoreState:
"""Persist modality EMAs and their hotkey qualification fallback together."""
"""Persist modality EMAs, qualification, and the last payable hotkey scores."""

def __init__(self):
self.by_hotkey: Dict[str, Dict[str, float]] = {}
self.qualification: Dict[str, GeneratorQualification] = {}
# None means no safe fallback exists; {} means a successful empty payout.
# Unlike the EMA history, this contains only miners eligible for payout.
self.last_payout: Optional[Dict[str, float]] = None

def resolve_last_payout(self, hotkeys: Sequence[str]) -> Optional[Dict[int, float]]:
"""Resolve the last good payout against current identities, never old UIDs."""
if self.last_payout is None:
return None
return {
uid: self.last_payout[hotkey]
for uid, hotkey in enumerate(hotkeys)
if hotkey in self.last_payout
}

def update(
self,
Expand Down Expand Up @@ -62,12 +75,14 @@ def save_state(self, save_dir: str, filename: str) -> None:
"version": 1,
"by_hotkey": self.by_hotkey,
"qualification": {hotkey: asdict(q) for hotkey, q in self.qualification.items()},
"last_payout": self.last_payout,
}, stream, allow_nan=False)

def load_state(self, save_dir: str, filename: str) -> bool:
# Missing/invalid new-format state must never reuse legacy scalar EMA.
self.by_hotkey = {}
self.qualification = {}
self.last_payout = None
path = Path(save_dir) / filename
if not path.exists():
return False
Expand Down Expand Up @@ -104,8 +119,19 @@ def load_state(self, save_dir: str, filename: str) -> bool:
if not 0 <= fooled <= n or (qualified and n == 0):
return False
qualification[hotkey] = GeneratorQualification(**row)
# Older snapshots cannot establish payout eligibility from EMA alone.
payout = payload.get("last_payout")
if payout is not None:
if not isinstance(payout, dict):
return False
for hotkey, score in payout.items():
if (not isinstance(hotkey, str) or not hotkey
or type(score) not in (int, float)
or not math.isfinite(score) or score <= 0):
return False
self.by_hotkey = restored
self.qualification = qualification
self.last_payout = payout
return True
except (OSError, ValueError, TypeError, KeyError, AttributeError):
return False
Loading
Loading