geoguesser-env / env /scripts /benchmark_throughput.py
AdithyaSK's picture
AdithyaSK HF Staff
Deploy geoguesser environment with train/eval splits
ee44678 verified
Raw
History Blame Contribute Delete
4.45 kB
# SPDX-License-Identifier: BSD-3-Clause
"""Measure how many episodes the environment itself can supply.
Answers the question a training or eval run actually needs: is the environment
the bottleneck, or the model? Runs an agentic-shaped episode — reset, three
looks, optionally a pin, a guess — across a range of worker counts, both as
threads and as processes, and reports episodes and steps per second.
Every worker gets its own environment, because an episode is stateful; sharing
one interleaves resets and silently produces nonsense. A perfect guess is
asserted to score zero distance, so an interference bug fails loudly instead of
reporting excellent throughput.
Usage:
python scripts/benchmark_throughput.py threads 32
REVEAL=0 PIN=0 python scripts/benchmark_throughput.py threads 64
python scripts/benchmark_throughput.py processes 32
Environment variables:
REVEAL=0 skip the guess reveal map, as a training run would
PIN=0 skip the pin, leaving view renders only
"""
import concurrent.futures as cf
import os
import pathlib
import statistics
import sys
import threading
import time
_LOCAL = threading.local()
_ROOT = pathlib.Path(__file__).resolve().parents[2]
sys.path.insert(0, str(_ROOT))
sys.path.insert(0, str(_ROOT.parent / "src"))
INDEX = str(_ROOT / "geoguesser_env" / "tasks" / "pano_v1.jsonl")
CACHE = str(_ROOT / "geoguesser_env" / "data" / "panos")
def episode(task_index: int) -> tuple[float, int]:
"""One agentic-shaped episode: reset, 3 looks, 1 pin, 1 guess."""
from geoguesser_env.models import GuessAction, LookAction, PinAction, to_wire
from geoguesser_env.server.geoguesser_environment import GeoGuesserEnvironment
from geoguesser_env.server.render import minimap
minimap.set_street_detail(False) # training config: never touch Overpass
# An episode is stateful, so every worker needs its own environment. Sharing
# one through a module global interleaves resets between threads and silently
# produces nonsense — which is exactly what the first version of this
# benchmark measured.
if not hasattr(_LOCAL, "env"):
_LOCAL.env = GeoGuesserEnvironment(
index_path=INDEX,
cache_dir=CACHE,
allow_fetch=False,
view_size=640,
reveal_map=os.environ.get("REVEAL", "1") == "1",
)
env = _LOCAL.env
started = time.perf_counter()
env.reset(task_index=task_index)
steps = 1
for heading in (0, 120, 240):
env.step(to_wire(LookAction(heading_deg=heading, fov_deg=90)))
steps += 1
if os.environ.get("PIN", "1") == "1":
env.step(to_wire(PinAction(lat=10.0, lon=10.0, span_deg=7.0)))
steps += 1
truth = env._task.truth
result = env.step(to_wire(GuessAction(lat=truth[0], lon=truth[1])))
steps += 1
# A perfect guess must score ~1 minus costs. If workers interfered, this
# fails instead of quietly reporting a great throughput number.
assert result.distance_km is not None and result.distance_km < 0.01, (
f"cross-worker interference: task {task_index} scored "
f"{result.distance_km} km on a perfect guess"
)
return time.perf_counter() - started, steps
def run(pool_cls, workers: int, episodes: int) -> dict:
tasks = [i % 100 for i in range(episodes)]
wall0 = time.perf_counter()
with pool_cls(max_workers=workers) as pool:
results = list(pool.map(episode, tasks))
wall = time.perf_counter() - wall0
latencies = [r[0] for r in results]
total_steps = sum(r[1] for r in results)
return {
"workers": workers,
"episodes": episodes,
"wall_s": wall,
"episodes_per_s": episodes / wall,
"steps_per_s": total_steps / wall,
"median_episode_s": statistics.median(latencies),
}
if __name__ == "__main__":
kind = sys.argv[1]
episodes = int(sys.argv[2]) if len(sys.argv) > 2 else 24
pool = cf.ThreadPoolExecutor if kind == "threads" else cf.ProcessPoolExecutor
print(f"{kind}, {os.cpu_count()} cpus, warm cache, street detail off")
print(f"{'workers':>7} {'eps/s':>7} {'steps/s':>8} {'wall':>7} {'median ep':>10}")
for workers in (1, 2, 4, 8, 16):
r = run(pool, workers, episodes)
print(
f"{r['workers']:>7} {r['episodes_per_s']:>7.2f} {r['steps_per_s']:>8.1f} "
f"{r['wall_s']:>6.1f}s {r['median_episode_s']:>9.2f}s"
)