Download src/nida/bench/workers/runner.py from Bilal7Dev/nida-tts: direct link, hf CLI and curl.
- Browser
- Download file 13.2 kB
-
https://huggingface.co/Bilal7Dev/nida-tts/resolve/main/src/nida/bench/workers/runner.py
- Command line
-
hf download hf://Bilal7Dev/nida-tts/src/nida/bench/workers/runner.py
-
curl -L -o runner.py https://huggingface.co/Bilal7Dev/nida-tts/resolve/main/src/nida/bench/workers/runner.py
13.2 kB
| """Generic worker loop shared by every model adapter. | |
| The loop is model-agnostic: it reads the requests file, resumes from the results file, | |
| calls ``Adapter.run_batch`` in fixed-size batches, and appends one fsynced result line per | |
| request. A failing batch is retried item by item so a single bad clip cannot take its | |
| neighbours down; every failure is recorded with its exception type and message (never | |
| dropped) and counted in the summary. Model loading failures are fatal: they propagate and | |
| the process exits non-zero. | |
| """ | |
| from __future__ import annotations | |
| import abc | |
| import argparse | |
| import hashlib | |
| import importlib | |
| import importlib.metadata | |
| import json | |
| import logging | |
| import sys | |
| import time | |
| import traceback | |
| from collections.abc import Mapping, Sequence | |
| from dataclasses import dataclass, replace | |
| from datetime import UTC, datetime | |
| from pathlib import Path | |
| from typing import ClassVar | |
| from nida.bench.protocol import ( | |
| ProtocolError, | |
| WorkerSummary, | |
| WorkRequest, | |
| WorkResult, | |
| append_result, | |
| drop_results, | |
| load_requests, | |
| load_results, | |
| pending_requests, | |
| read_summary, | |
| write_summary, | |
| ) | |
| __all__ = [ | |
| "MAX_CONSECUTIVE_FAILURES", | |
| "Adapter", | |
| "ModelRef", | |
| "TooManyFailuresError", | |
| "WorkerOptions", | |
| "main_for", | |
| "run_worker", | |
| ] | |
| log = logging.getLogger(__name__) | |
| #: Abort when this many items in a row fail: the model/device is broken, not the clips. | |
| MAX_CONSECUTIVE_FAILURES = 10 | |
| #: Progress is logged when this many more items are done (and at the end). | |
| PROGRESS_EVERY = 200 | |
| #: When the *same* failure run (same clip ids) aborts more than this many times in a row, | |
| #: it is evidently repeatable rather than transient and is kept as recorded failures so a | |
| #: rerun can move past it. Unrelated aborts never accumulate (see ``_abort_count``). | |
| MAX_ABORTS_BEFORE_KEEPING = 2 | |
| class TooManyFailuresError(RuntimeError): | |
| """Consecutive item failures suggest a broken model or device (e.g. CUDA context).""" | |
| class ModelRef: | |
| """Which model to load. | |
| Attributes: | |
| model_id: Hub id, model card name or local path, interpreted by the adapter. | |
| revision: Pinned revision (commit SHA) or ``""`` when the source has none. | |
| """ | |
| model_id: str | |
| revision: str | |
| class Adapter(abc.ABC): | |
| """One model family. Subclasses keep heavy imports inside their methods.""" | |
| name: ClassVar[str] | |
| #: Distribution names whose installed versions are recorded in the summary. | |
| distributions: ClassVar[tuple[str, ...]] = () | |
| def load(self, model: ModelRef, device: str, batch_size: int) -> None: | |
| """Load the model onto ``device``. Exceptions here abort the worker.""" | |
| def run_batch(self, batch: Sequence[WorkRequest]) -> list[Mapping[str, object]]: | |
| """Return one JSON-compatible output mapping per request, in the same order.""" | |
| def peak_vram_gb(self) -> float | None: | |
| """Peak CUDA memory allocated by this process, if torch with CUDA is present.""" | |
| try: | |
| torch = importlib.import_module("torch") | |
| except ImportError: | |
| return None | |
| if not torch.cuda.is_available(): | |
| return None | |
| return round(float(torch.cuda.max_memory_allocated()) / 1024**3, 2) | |
| class WorkerOptions: | |
| requests: Path | |
| results: Path | |
| summary: Path | |
| model: ModelRef | |
| device: str | |
| batch_size: int | |
| retry_failed: bool | |
| def _now() -> str: | |
| return datetime.now(UTC).isoformat(timespec="seconds") | |
| def _describe(err: BaseException) -> str: | |
| return f"{type(err).__name__}: {err}" | |
| def _package_versions(names: Sequence[str]) -> dict[str, str]: | |
| versions: dict[str, str] = {} | |
| for name in names: | |
| try: | |
| versions[name] = importlib.metadata.version(name) | |
| except importlib.metadata.PackageNotFoundError: | |
| versions[name] = "not-installed" | |
| return versions | |
| def _abort_counter_path(results: Path) -> Path: | |
| return results.with_name(results.name + ".aborts.json") | |
| def _abort_count(results: Path, failure_ids: Sequence[str]) -> int: | |
| """Record an abort and return how often *this* failure run has aborted in a row. | |
| The counter stores a signature of the failing clip ids: a different failure run | |
| restarts at 1, and :func:`_clear_abort_count` removes it after any run that finishes | |
| without aborting, so transient incidents in different sessions never add up. | |
| """ | |
| signature = hashlib.sha1("\n".join(sorted(failure_ids)).encode("utf-8")).hexdigest() | |
| path = _abort_counter_path(results) | |
| count = 1 | |
| if path.exists(): | |
| try: | |
| previous = json.loads(path.read_text(encoding="utf-8")) | |
| if previous.get("signature") == signature: | |
| count = int(previous.get("count", 0)) + 1 | |
| except (ValueError, AttributeError, TypeError) as err: | |
| # A damaged counter must not wedge the worker; recount from this abort. | |
| log.warning("ignoring unreadable abort counter %s (%s); restarting at 1", path, err) | |
| tmp = path.with_name(f".{path.name}.tmp") | |
| tmp.write_text(json.dumps({"signature": signature, "count": count}), encoding="utf-8") | |
| tmp.replace(path) | |
| return count | |
| def _clear_abort_count(results: Path) -> None: | |
| _abort_counter_path(results).unlink(missing_ok=True) | |
| def _drop_failed(path: Path) -> None: | |
| """Rewrite the results file without failed entries (for ``--retry-failed``).""" | |
| results = load_results(path, repair=True) | |
| kept = [r.to_json() + "\n" for r in results.values() if r.ok] | |
| tmp = path.with_name(f".{path.name}.tmp") | |
| tmp.write_text("".join(kept), encoding="utf-8") | |
| tmp.replace(path) | |
| def _validated_outputs(batch: Sequence[WorkRequest], outputs: object) -> list[Mapping[str, object]]: | |
| if not isinstance(outputs, list) or len(outputs) != len(batch): | |
| got = len(outputs) if isinstance(outputs, list) else type(outputs).__name__ | |
| raise ProtocolError(f"adapter returned {got} outputs for a batch of {len(batch)}") | |
| for output in outputs: | |
| if not isinstance(output, Mapping): | |
| raise ProtocolError(f"adapter output must be a mapping, got {type(output).__name__}") | |
| return outputs | |
| def _run_one_batch(adapter: Adapter, batch: Sequence[WorkRequest]) -> list[WorkResult]: | |
| started = time.perf_counter() | |
| try: | |
| outputs = _validated_outputs(batch, adapter.run_batch(batch)) | |
| except Exception as err: # noqa: BLE001 - recorded per item below, never swallowed | |
| if len(batch) == 1: | |
| log.warning("item %s failed: %s", batch[0].clip_id, _describe(err)) | |
| return [ | |
| WorkResult( | |
| clip_id=batch[0].clip_id, | |
| output=None, | |
| error=_describe(err), | |
| infer_seconds=time.perf_counter() - started, | |
| ) | |
| ] | |
| log.warning("batch of %d failed (%s); retrying items one by one", len(batch), err) | |
| results: list[WorkResult] = [] | |
| for request in batch: | |
| results.extend(_run_one_batch(adapter, [request])) | |
| return results | |
| per_item = (time.perf_counter() - started) / len(batch) | |
| return [ | |
| WorkResult(clip_id=r.clip_id, output=dict(o), error=None, infer_seconds=per_item) | |
| for r, o in zip(batch, outputs, strict=True) | |
| ] | |
| def run_worker(adapter: Adapter, options: WorkerOptions) -> WorkerSummary: | |
| """Process every pending request and write the summary. Returns the summary.""" | |
| if options.batch_size < 1: | |
| raise ValueError(f"batch_size must be >= 1, got {options.batch_size}") | |
| started_at = _now() | |
| requests = load_requests(options.requests) | |
| if options.retry_failed and options.results.exists(): | |
| _drop_failed(options.results) | |
| done = load_results(options.results, repair=True) | |
| pending = pending_requests(requests, done) | |
| log.info( | |
| "%s: %d requests, %d already done, %d pending", | |
| adapter.name, | |
| len(requests), | |
| len(done), | |
| len(pending), | |
| ) | |
| previous = read_summary(options.summary) if options.summary.exists() else None | |
| load_seconds = 0.0 | |
| if pending: | |
| load_start = time.perf_counter() | |
| adapter.load(options.model, options.device, options.batch_size) | |
| load_seconds = time.perf_counter() - load_start | |
| options.results.parent.mkdir(parents=True, exist_ok=True) | |
| failure_run: list[WorkResult] = [] | |
| aborted = False | |
| with options.results.open("a", encoding="utf-8") as handle: | |
| for start in range(0, len(pending), options.batch_size): | |
| batch = pending[start : start + options.batch_size] | |
| for result in _run_one_batch(adapter, batch): | |
| append_result(handle, result) | |
| failure_run = [] if result.ok else [*failure_run, result] | |
| if len(failure_run) >= MAX_CONSECUTIVE_FAILURES: | |
| aborted = True | |
| break | |
| if aborted: | |
| break | |
| finished = start + len(batch) | |
| if ( | |
| finished == len(pending) | |
| or finished // PROGRESS_EVERY != start // PROGRESS_EVERY | |
| ): | |
| log.info("%s: %d/%d", adapter.name, finished, len(pending)) | |
| if aborted: | |
| aborts = _abort_count(options.results, [r.clip_id for r in failure_run]) | |
| if aborts <= MAX_ABORTS_BEFORE_KEEPING: | |
| # Most likely infrastructure (e.g. a broken CUDA context), not bad clips: make | |
| # them pending again so a plain rerun retries them instead of scoring them as | |
| # empty hypotheses. | |
| drop_results(options.results, [r.clip_id for r in failure_run]) | |
| outcome = "results removed, they will be retried" | |
| else: | |
| outcome = ( | |
| f"same failure run aborted {aborts} times: kept as recorded failures so " | |
| "the next run moves on; use --retry-failed to try them again" | |
| ) | |
| raise TooManyFailuresError( | |
| f"{adapter.name}: {len(failure_run)} consecutive failures ({outcome}), " | |
| f"last: {failure_run[-1].error}" | |
| ) | |
| _clear_abort_count(options.results) # finished without aborting | |
| final = load_results(options.results) | |
| n_ok = sum(1 for r in final.values() if r.ok) | |
| if not pending and previous is not None: | |
| # No-op resume: keep the measurements of the invocation that did the work. | |
| summary = replace(previous, n_ok=n_ok, n_error=len(final) - n_ok) | |
| else: | |
| peaks = [v for v in (adapter.peak_vram_gb(), previous and previous.peak_vram_gb) if v] | |
| invocations = int(previous.extra.get("invocations", "1")) + 1 if previous else 1 | |
| summary = WorkerSummary( | |
| worker=adapter.name, | |
| model_id=options.model.model_id, | |
| revision=options.model.revision, | |
| load_seconds=round(load_seconds, 3), | |
| peak_vram_gb=max(peaks) if peaks else None, | |
| n_ok=n_ok, | |
| n_error=len(final) - n_ok, | |
| started_at=previous.started_at if previous else started_at, | |
| ended_at=_now(), | |
| packages=_package_versions(adapter.distributions), | |
| extra={ | |
| "python": sys.version.split()[0], | |
| "device": options.device, | |
| "invocations": str(invocations), | |
| }, | |
| ) | |
| write_summary(options.summary, summary) | |
| return summary | |
| def _parse(argv: Sequence[str] | None) -> WorkerOptions: | |
| parser = argparse.ArgumentParser(description="Run a Nida benchmark worker.") | |
| parser.add_argument("--requests", type=Path, required=True) | |
| parser.add_argument("--results", type=Path, required=True) | |
| parser.add_argument("--summary", type=Path, default=None) | |
| parser.add_argument("--model-id", required=True) | |
| parser.add_argument("--revision", default="") | |
| parser.add_argument("--device", default="cuda") | |
| parser.add_argument("--batch-size", type=int, default=8) | |
| parser.add_argument("--retry-failed", action="store_true") | |
| args = parser.parse_args(argv) | |
| summary = args.summary or args.results.with_name(args.results.stem + ".summary.json") | |
| return WorkerOptions( | |
| requests=args.requests, | |
| results=args.results, | |
| summary=summary, | |
| model=ModelRef(model_id=args.model_id, revision=args.revision), | |
| device=args.device, | |
| batch_size=args.batch_size, | |
| retry_failed=args.retry_failed, | |
| ) | |
| def main_for(adapter: Adapter, argv: Sequence[str] | None = None) -> int: | |
| """CLI entry point used by each worker module's ``__main__`` block.""" | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") | |
| options = _parse(argv) | |
| try: | |
| summary = run_worker(adapter, options) | |
| except (ProtocolError, OSError, ValueError, TooManyFailuresError): | |
| traceback.print_exc() | |
| return 2 | |
| print(f"{adapter.name}: ok={summary.n_ok} error={summary.n_error}") | |
| return 0 | |