Download src/nida/bench/protocol.py from Bilal7Dev/nida-tts: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/Bilal7Dev/nida-tts/resolve/main/src/nida/bench/protocol.py
- Command line
-
hf download hf://Bilal7Dev/nida-tts/src/nida/bench/protocol.py
-
curl -L -o protocol.py https://huggingface.co/Bilal7Dev/nida-tts/resolve/main/src/nida/bench/protocol.py
12.3 kB
| """JSONL protocol between the benchmark orchestrator and model workers. | |
| Each model family runs in its own virtual environment (dependency pins conflict, see | |
| docs/benchmarks/B1-B2-tasarim.md §1.3), so the orchestrator and the workers only share | |
| files. This module is the single definition of those files and must stay stdlib-only: | |
| it is imported inside every worker environment. | |
| Layout for one worker invocation:: | |
| requests.jsonl one ``WorkRequest`` per line (written by the orchestrator) | |
| results.jsonl one ``WorkResult`` per line (appended by the worker) | |
| results.summary.json one ``WorkerSummary`` (written by the worker at the end) | |
| Results are appended one line at a time and flushed + fsynced, so an interrupted worker | |
| leaves a valid prefix plus at most one partial trailing line. On restart the worker | |
| calls :func:`load_results` with ``repair=True``, which drops the partial line, and | |
| skips every request whose ``clip_id`` already has a result. Failed items are recorded | |
| with an ``error`` and are never retried silently: callers decide via ``retry_failed``. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from collections.abc import Iterable, Iterator, Mapping | |
| from dataclasses import asdict, dataclass, field | |
| from pathlib import Path | |
| from typing import IO | |
| __all__ = [ | |
| "ProtocolError", | |
| "WorkRequest", | |
| "WorkResult", | |
| "WorkerSummary", | |
| "append_result", | |
| "drop_results", | |
| "load_requests", | |
| "load_results", | |
| "pending_requests", | |
| "read_summary", | |
| "write_requests", | |
| "write_summary", | |
| ] | |
| class ProtocolError(ValueError): | |
| """A protocol file is malformed or inconsistent.""" | |
| class WorkRequest: | |
| """One unit of work for a worker. | |
| Attributes: | |
| clip_id: Content-addressed clip id; unique within a requests file. | |
| audio_path: Absolute path of the input audio file. | |
| params: Worker-specific string parameters (e.g. ``language``, ``context``, | |
| ``out_path``). Values are strings so the format stays trivially portable. | |
| """ | |
| clip_id: str | |
| audio_path: str | |
| params: Mapping[str, str] = field(default_factory=dict) | |
| def to_json(self) -> str: | |
| return json.dumps( | |
| {"clip_id": self.clip_id, "audio_path": self.audio_path, "params": dict(self.params)}, | |
| ensure_ascii=False, | |
| sort_keys=True, | |
| ) | |
| def from_obj(obj: object, where: str) -> WorkRequest: | |
| if not isinstance(obj, dict): | |
| raise ProtocolError(f"{where}: request must be a JSON object") | |
| clip_id = obj.get("clip_id") | |
| audio_path = obj.get("audio_path") | |
| params = obj.get("params", {}) | |
| if not isinstance(clip_id, str) or not clip_id: | |
| raise ProtocolError(f"{where}: clip_id must be a non-empty string") | |
| if not isinstance(audio_path, str) or not audio_path: | |
| raise ProtocolError(f"{where}: audio_path must be a non-empty string") | |
| if not isinstance(params, dict) or not all( | |
| isinstance(k, str) and isinstance(v, str) for k, v in params.items() | |
| ): | |
| raise ProtocolError(f"{where}: params must map strings to strings") | |
| return WorkRequest(clip_id=clip_id, audio_path=audio_path, params=dict(params)) | |
| class WorkResult: | |
| """Outcome for one request: exactly one of ``output`` or ``error`` is set. | |
| Attributes: | |
| clip_id: Id of the request this answers. | |
| output: Worker-specific JSON-compatible payload (e.g. ``{"text": ...}``). | |
| error: ``"<ExceptionType>: <message>"`` when the item failed. | |
| infer_seconds: Wall-clock inference time attributed to this item (batch time | |
| divided by batch size), excluding model loading. | |
| """ | |
| clip_id: str | |
| output: Mapping[str, object] | None | |
| error: str | None | |
| infer_seconds: float | |
| def __post_init__(self) -> None: | |
| if (self.output is None) == (self.error is None): | |
| raise ProtocolError(f"{self.clip_id}: exactly one of output/error must be set") | |
| if self.infer_seconds < 0: | |
| raise ProtocolError(f"{self.clip_id}: infer_seconds must be >= 0") | |
| def ok(self) -> bool: | |
| return self.error is None | |
| def to_json(self) -> str: | |
| return json.dumps( | |
| { | |
| "clip_id": self.clip_id, | |
| "output": None if self.output is None else dict(self.output), | |
| "error": self.error, | |
| "infer_seconds": self.infer_seconds, | |
| }, | |
| ensure_ascii=False, | |
| sort_keys=True, | |
| ) | |
| def from_obj(obj: object, where: str) -> WorkResult: | |
| if not isinstance(obj, dict): | |
| raise ProtocolError(f"{where}: result must be a JSON object") | |
| clip_id = obj.get("clip_id") | |
| output = obj.get("output") | |
| error = obj.get("error") | |
| seconds = obj.get("infer_seconds") | |
| if not isinstance(clip_id, str) or not clip_id: | |
| raise ProtocolError(f"{where}: clip_id must be a non-empty string") | |
| if output is not None and not isinstance(output, dict): | |
| raise ProtocolError(f"{where}: output must be an object or null") | |
| if error is not None and not isinstance(error, str): | |
| raise ProtocolError(f"{where}: error must be a string or null") | |
| if isinstance(seconds, bool) or not isinstance(seconds, int | float): | |
| raise ProtocolError(f"{where}: infer_seconds must be a number") | |
| return WorkResult(clip_id=clip_id, output=output, error=error, infer_seconds=float(seconds)) | |
| class WorkerSummary: | |
| """Facts about one worker invocation, written once at the end.""" | |
| worker: str | |
| model_id: str | |
| revision: str | |
| load_seconds: float | |
| peak_vram_gb: float | None | |
| n_ok: int | |
| n_error: int | |
| started_at: str | |
| ended_at: str | |
| packages: Mapping[str, str] = field(default_factory=dict) | |
| extra: Mapping[str, str] = field(default_factory=dict) | |
| def _fsync_line(handle: IO[str], line: str) -> None: | |
| handle.write(line + "\n") | |
| handle.flush() | |
| os.fsync(handle.fileno()) | |
| def write_requests(path: Path, requests: Iterable[WorkRequest]) -> int: | |
| """Write a requests file atomically and return the number of requests. | |
| Raises: | |
| ProtocolError: on duplicate clip ids. | |
| """ | |
| from nida.utils.io import atomic_write_text # local import: workers never call this | |
| seen: set[str] = set() | |
| lines: list[str] = [] | |
| for request in requests: | |
| if request.clip_id in seen: | |
| raise ProtocolError(f"duplicate clip_id in requests: {request.clip_id}") | |
| seen.add(request.clip_id) | |
| lines.append(request.to_json()) | |
| atomic_write_text(path, "".join(line + "\n" for line in lines)) | |
| return len(lines) | |
| def _iter_json_lines(path: Path) -> Iterator[tuple[int, str]]: | |
| with path.open("r", encoding="utf-8") as handle: | |
| yield from enumerate(handle, start=1) | |
| def load_requests(path: Path) -> list[WorkRequest]: | |
| """Read and validate a requests file (no partial lines tolerated).""" | |
| requests: list[WorkRequest] = [] | |
| seen: set[str] = set() | |
| for number, line in _iter_json_lines(path): | |
| where = f"{path}:{number}" | |
| if not line.endswith("\n"): | |
| raise ProtocolError(f"{where}: truncated request line") | |
| try: | |
| obj = json.loads(line) | |
| except json.JSONDecodeError as err: | |
| raise ProtocolError(f"{where}: invalid JSON ({err.msg})") from err | |
| request = WorkRequest.from_obj(obj, where) | |
| if request.clip_id in seen: | |
| raise ProtocolError(f"{where}: duplicate clip_id {request.clip_id}") | |
| seen.add(request.clip_id) | |
| requests.append(request) | |
| return requests | |
| def load_results(path: Path, *, repair: bool = False) -> dict[str, WorkResult]: | |
| """Read a results file into ``{clip_id: result}``. | |
| A missing file yields an empty mapping. Only the *last* line may be incomplete (an | |
| interrupted append): with ``repair=True`` it is removed from the file, otherwise a | |
| ``ProtocolError`` is raised. Any malformed line before the last one, and any | |
| duplicate clip id, is always an error. | |
| """ | |
| if not path.exists(): | |
| return {} | |
| raw = path.read_bytes() | |
| # Split at the last newline *in bytes*: an interrupted append may end in the middle of | |
| # a multi-byte UTF-8 character, so the trailing fragment must not be decoded. | |
| keep = raw.rfind(b"\n") + 1 # 0 when there is no complete line | |
| body, trailing = raw[:keep], raw[keep:] | |
| try: | |
| text = body.decode("utf-8") | |
| except UnicodeDecodeError as err: | |
| raise ProtocolError(f"{path}: complete lines are not valid UTF-8 ({err})") from err | |
| lines = text.split("\n")[:-1] if text else [] | |
| results: dict[str, WorkResult] = {} | |
| for number, line in enumerate(lines, start=1): | |
| where = f"{path}:{number}" | |
| try: | |
| obj = json.loads(line) | |
| except json.JSONDecodeError as err: | |
| raise ProtocolError(f"{where}: invalid JSON ({err.msg})") from err | |
| result = WorkResult.from_obj(obj, where) | |
| if result.clip_id in results: | |
| raise ProtocolError(f"{where}: duplicate result for {result.clip_id}") | |
| results[result.clip_id] = result | |
| if trailing: | |
| if not repair: | |
| raise ProtocolError(f"{path}: incomplete trailing line ({len(trailing)} bytes)") | |
| with path.open("r+b") as handle: | |
| handle.truncate(keep) | |
| handle.flush() | |
| os.fsync(handle.fileno()) | |
| return results | |
| def pending_requests( | |
| requests: Iterable[WorkRequest], | |
| results: Mapping[str, WorkResult], | |
| *, | |
| retry_failed: bool = False, | |
| ) -> list[WorkRequest]: | |
| """Requests that still need work, in input order. | |
| Raises: | |
| ProtocolError: if ``results`` contains clip ids that are not in ``requests`` | |
| (the results file belongs to a different request set). | |
| """ | |
| request_list = list(requests) | |
| known = {r.clip_id for r in request_list} | |
| stray = sorted(set(results) - known) | |
| if stray: | |
| raise ProtocolError(f"results contain unknown clip ids, e.g. {stray[:3]}") | |
| pending: list[WorkRequest] = [] | |
| for request in request_list: | |
| done = results.get(request.clip_id) | |
| if done is None or (retry_failed and not done.ok): | |
| pending.append(request) | |
| return pending | |
| def append_result(handle: IO[str], result: WorkResult) -> None: | |
| """Append one result line and fsync, so an interruption loses at most this line.""" | |
| _fsync_line(handle, result.to_json()) | |
| def write_summary(path: Path, summary: WorkerSummary) -> None: | |
| """Write the summary JSON next to the results (stdlib-only atomic replace).""" | |
| tmp = path.with_name(f".{path.name}.tmp") | |
| with tmp.open("w", encoding="utf-8") as handle: | |
| json.dump(asdict(summary), handle, ensure_ascii=False, indent=2, sort_keys=True) | |
| handle.write("\n") | |
| handle.flush() | |
| os.fsync(handle.fileno()) | |
| tmp.replace(path) | |
| def read_summary(path: Path) -> WorkerSummary: | |
| """Read and validate a summary file.""" | |
| obj = json.loads(path.read_text(encoding="utf-8")) | |
| if not isinstance(obj, dict): | |
| raise ProtocolError(f"{path}: summary must be a JSON object") | |
| try: | |
| return WorkerSummary(**obj) | |
| except TypeError as err: | |
| raise ProtocolError(f"{path}: invalid summary fields ({err})") from err | |
| def drop_results(path: Path, clip_ids: Iterable[str]) -> int: | |
| """Remove the given clip ids from a results file (atomic rewrite); returns how many. | |
| Used when a result's side effect is gone (e.g. its output file was lost), so the | |
| item becomes pending again instead of being trusted. | |
| """ | |
| doomed = set(clip_ids) | |
| results = load_results(path, repair=True) | |
| kept = [r.to_json() + "\n" for cid, r in results.items() if cid not in doomed] | |
| removed = len(results) - len(kept) | |
| if removed: | |
| tmp = path.with_name(f".{path.name}.tmp") | |
| with tmp.open("w", encoding="utf-8") as handle: | |
| handle.writelines(kept) | |
| handle.flush() | |
| os.fsync(handle.fileno()) | |
| tmp.replace(path) | |
| return removed | |