Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| """Load a model family backend and run WER across packed conditions.""" | |
| from __future__ import annotations | |
| import os | |
| from collections.abc import Callable | |
| from functools import lru_cache | |
| from typing import Any | |
| import torch | |
| from benchmark.dataset import ( | |
| accumulate_predictions_for_slice, | |
| evaluate_condition_wer_timed, | |
| is_full_condition_set, | |
| load_packed_condition, | |
| resolve_condition_keys, | |
| wer_result_key, | |
| ) | |
| from backends.registry import build_transcriber, resolve_label | |
| from evaluation.runtime import ( | |
| apply_cpu_thread_settings_once, | |
| disable_broken_torchcodec, | |
| resolve_eval_devices, | |
| use_spaces_gpu_decorator, | |
| ) | |
| def _hub_max_duration_s() -> int: | |
| """Upper bound the Hub accepts for ``spaces.GPU(duration=...)`` (override if HF raises).""" | |
| try: | |
| return max(60, int(os.environ.get("FFASR_ZEROGPU_HUB_MAX_DURATION_S", "600"))) | |
| except ValueError: | |
| return 600 | |
| def _requested_duration_s() -> int: | |
| try: | |
| return max(60, int(os.environ.get("FFASR_ZEROGPU_MAX_DURATION_S", "600"))) | |
| except ValueError: | |
| return 600 | |
| def _effective_gpu_duration_s() -> int: | |
| """``min(FFASR_ZEROGPU_MAX_DURATION_S, FFASR_ZEROGPU_HUB_MAX_DURATION_S)``.""" | |
| return min(_requested_duration_s(), _hub_max_duration_s()) | |
| def _samples_per_gpu_segment() -> int: | |
| """ | |
| When >0 and ``spaces`` is available, each condition is split into slices of this many | |
| samples; each slice runs in its own ``spaces.GPU`` call (model reload per slice). | |
| """ | |
| try: | |
| v = int(os.environ.get("FFASR_ZEROGPU_SAMPLES_PER_SEGMENT", "0")) | |
| except ValueError: | |
| return 0 | |
| return max(0, v) | |
| def _build_transcriber( | |
| family_id: str, | |
| model_id: str, | |
| device_str: str, | |
| device_int: int, | |
| custom_script: str = "", | |
| ) -> tuple[Callable[..., str], Callable[[], None]]: | |
| """Family backend transcriber, or user ``evaluate(Path)`` script when provided.""" | |
| if (custom_script or "").strip(): | |
| from backends.custom_eval import build_transcriber_from_custom_script | |
| return build_transcriber_from_custom_script(custom_script) | |
| return build_transcriber(family_id, model_id, device_str, device_int) | |
| def _zerogpu_run_segment(payload: dict[str, Any]) -> dict[str, Any]: | |
| """Single ZeroGPU segment: build model, transcribe ``[start:end)`` for one condition.""" | |
| model_id = str(payload["model_id"]) | |
| family_id = str(payload["family_id"]) | |
| condition_key = str(payload["condition_key"]) | |
| start = int(payload["start"]) | |
| end = int(payload["end"]) | |
| custom_script = str(payload.get("custom_script") or "") | |
| device_str, device_int = resolve_eval_devices() | |
| transcribe, cleanup = _build_transcriber( | |
| family_id, model_id, device_str, device_int, custom_script=custom_script | |
| ) | |
| try: | |
| num_params = int(getattr(transcribe, "_num_params", 0) or 0) | |
| preds, refs, audio_s, infer_s, n_done = accumulate_predictions_for_slice( | |
| transcribe, condition_key, start, end, progress_cb=None | |
| ) | |
| return { | |
| "predictions": preds, | |
| "references": refs, | |
| "audio_s": audio_s, | |
| "infer_s": infer_s, | |
| "n": n_done, | |
| "num_params": num_params, | |
| } | |
| finally: | |
| cleanup() | |
| if device_str == "cuda" and torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| def _run_evaluation_core( | |
| model_id: str, | |
| family_id: str = "auto", | |
| progress_cb: Callable[[int, int, str], None] | None = None, | |
| custom_script: str = "", | |
| condition_keys: list[str] | None = None, | |
| ) -> dict: | |
| device_str, device_int = resolve_eval_devices() | |
| keys = resolve_condition_keys(condition_keys) | |
| # Precompute totals so the progress bar has a known denominator from the start. | |
| condition_totals: dict[str, int] = {} | |
| grand_total = 0 | |
| for ck in keys: | |
| try: | |
| condition_totals[ck] = len(load_packed_condition(ck)) | |
| except Exception: | |
| condition_totals[ck] = 0 | |
| grand_total += condition_totals[ck] | |
| if progress_cb is not None: | |
| try: | |
| progress_cb(0, grand_total, "") | |
| except Exception: | |
| pass | |
| transcribe, cleanup = _build_transcriber( | |
| family_id, model_id, device_str, device_int, custom_script=custom_script | |
| ) | |
| try: | |
| # Backends attach `transcribe._num_params` via `_model_utils.attach_params`; defaults to 0 | |
| # if the backend couldn't introspect a module. | |
| num_params = int(getattr(transcribe, "_num_params", 0) or 0) | |
| results: dict = { | |
| "model_id": model_id, | |
| "eval_family": resolve_label(family_id), | |
| "num_params": num_params, | |
| } | |
| total_samples = 0 | |
| total_audio_s = 0.0 | |
| total_infer_s = 0.0 | |
| done_so_far = 0 | |
| for condition_key in keys: | |
| condition_base = done_so_far | |
| def _sample_cb(ck: str, i: int, _n: int, _base: int = condition_base) -> None: | |
| if progress_cb is None: | |
| return | |
| try: | |
| progress_cb(_base + i, grand_total, ck) | |
| except Exception: | |
| pass | |
| wer_score, count, audio_s, infer_s = evaluate_condition_wer_timed( | |
| transcribe, condition_key, progress_cb=_sample_cb | |
| ) | |
| results[wer_result_key(condition_key)] = wer_score | |
| total_samples = max(total_samples, count) | |
| total_audio_s += audio_s | |
| total_infer_s += infer_s | |
| done_so_far += count | |
| results["eval_conditions"] = list(keys) | |
| results["eval_full_run"] = is_full_condition_set(condition_keys) | |
| results["num_samples"] = total_samples | |
| results["eval_audio_seconds"] = round(total_audio_s, 3) | |
| results["eval_wall_time_s"] = round(total_infer_s, 3) | |
| # RTF: audio duration / compute time (>1 means faster than real-time for batch totals) | |
| if total_infer_s > 1e-6: | |
| results["eval_rtf"] = round(total_audio_s / total_infer_s, 4) | |
| else: | |
| results["eval_rtf"] = "" | |
| if progress_cb is not None: | |
| try: | |
| progress_cb(done_so_far, grand_total, "") | |
| except Exception: | |
| pass | |
| return results | |
| finally: | |
| cleanup() | |
| if device_str == "cuda" and torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| def _run_evaluation_core_segmented_local( | |
| model_id: str, | |
| family_id: str = "auto", | |
| progress_cb: Callable[[int, int, str], None] | None = None, | |
| *, | |
| segment_samples: int, | |
| custom_script: str = "", | |
| condition_keys: list[str] | None = None, | |
| ) -> dict: | |
| """ | |
| Same semantics as ``_run_evaluation_core_segmented`` but without ``spaces.GPU``: | |
| each slice calls ``_zerogpu_run_segment`` in-process (CPU or CUDA per ``FFASR_DEVICE``). | |
| """ | |
| from jiwer import wer as compute_wer | |
| keys = resolve_condition_keys(condition_keys) | |
| condition_totals: dict[str, int] = {} | |
| grand_total = 0 | |
| for ck in keys: | |
| try: | |
| condition_totals[ck] = len(load_packed_condition(ck)) | |
| except Exception: | |
| condition_totals[ck] = 0 | |
| grand_total += condition_totals[ck] | |
| if progress_cb is not None: | |
| try: | |
| progress_cb(0, grand_total, "") | |
| except Exception: | |
| pass | |
| results: dict = { | |
| "model_id": model_id, | |
| "eval_family": resolve_label(family_id), | |
| "num_params": 0, | |
| } | |
| total_audio_s = 0.0 | |
| total_infer_s = 0.0 | |
| total_samples = 0 | |
| done_so_far = 0 | |
| first_num_params: int | None = None | |
| for condition_key in keys: | |
| data = load_packed_condition(condition_key) | |
| n_items = len(data) | |
| all_p: list[str] = [] | |
| all_r: list[str] = [] | |
| cond_audio = 0.0 | |
| cond_infer = 0.0 | |
| if n_items == 0: | |
| results[wer_result_key(condition_key)] = 0.0 | |
| continue | |
| for start in range(0, n_items, segment_samples): | |
| end = min(n_items, start + segment_samples) | |
| payload: dict[str, Any] = { | |
| "model_id": model_id, | |
| "family_id": family_id, | |
| "condition_key": condition_key, | |
| "start": start, | |
| "end": end, | |
| "custom_script": custom_script, | |
| } | |
| out = _zerogpu_run_segment(payload) | |
| if first_num_params is None: | |
| first_num_params = int(out.get("num_params") or 0) | |
| all_p.extend(out["predictions"]) | |
| all_r.extend(out["references"]) | |
| cond_audio += float(out["audio_s"]) | |
| cond_infer += float(out["infer_s"]) | |
| done_so_far += int(out["n"]) | |
| if progress_cb is not None: | |
| try: | |
| progress_cb(done_so_far, grand_total, condition_key) | |
| except Exception: | |
| pass | |
| wer_score = round(compute_wer(all_r, all_p), 4) if all_p else 0.0 | |
| results[wer_result_key(condition_key)] = wer_score | |
| total_audio_s += cond_audio | |
| total_infer_s += cond_infer | |
| total_samples = max(total_samples, len(all_p)) | |
| results["num_params"] = first_num_params or 0 | |
| results["eval_conditions"] = list(keys) | |
| results["eval_full_run"] = is_full_condition_set(condition_keys) | |
| results["num_samples"] = total_samples | |
| results["eval_audio_seconds"] = round(total_audio_s, 3) | |
| results["eval_wall_time_s"] = round(total_infer_s, 3) | |
| if total_infer_s > 1e-6: | |
| results["eval_rtf"] = round(total_audio_s / total_infer_s, 4) | |
| else: | |
| results["eval_rtf"] = "" | |
| if progress_cb is not None: | |
| try: | |
| progress_cb(done_so_far, grand_total, "") | |
| except Exception: | |
| pass | |
| return results | |
| def _run_evaluation_core_segmented( | |
| model_id: str, | |
| family_id: str = "auto", | |
| progress_cb: Callable[[int, int, str], None] | None = None, | |
| *, | |
| segment_samples: int, | |
| gpu_duration_s: int, | |
| gpu_size: str, | |
| custom_script: str = "", | |
| condition_keys: list[str] | None = None, | |
| ) -> dict: | |
| """ | |
| Long evaluations: each slice of ``segment_samples`` runs under its own ``spaces.GPU`` | |
| budget, then predictions/references are merged and WER is computed once per condition. | |
| """ | |
| from jiwer import wer as compute_wer | |
| import spaces | |
| keys = resolve_condition_keys(condition_keys) | |
| condition_totals: dict[str, int] = {} | |
| grand_total = 0 | |
| for ck in keys: | |
| try: | |
| condition_totals[ck] = len(load_packed_condition(ck)) | |
| except Exception: | |
| condition_totals[ck] = 0 | |
| grand_total += condition_totals[ck] | |
| if progress_cb is not None: | |
| try: | |
| progress_cb(0, grand_total, "") | |
| except Exception: | |
| pass | |
| wrapped_seg = _spaces_gpu_segment_wrapper(gpu_duration_s, gpu_size) | |
| results: dict = { | |
| "model_id": model_id, | |
| "eval_family": resolve_label(family_id), | |
| "num_params": 0, | |
| } | |
| total_audio_s = 0.0 | |
| total_infer_s = 0.0 | |
| total_samples = 0 | |
| done_so_far = 0 | |
| first_num_params: int | None = None | |
| for condition_key in keys: | |
| data = load_packed_condition(condition_key) | |
| n_items = len(data) | |
| all_p: list[str] = [] | |
| all_r: list[str] = [] | |
| cond_audio = 0.0 | |
| cond_infer = 0.0 | |
| if n_items == 0: | |
| results[wer_result_key(condition_key)] = 0.0 | |
| continue | |
| for start in range(0, n_items, segment_samples): | |
| end = min(n_items, start + segment_samples) | |
| payload: dict[str, Any] = { | |
| "model_id": model_id, | |
| "family_id": family_id, | |
| "condition_key": condition_key, | |
| "start": start, | |
| "end": end, | |
| "custom_script": custom_script, | |
| } | |
| out = wrapped_seg(payload) | |
| if first_num_params is None: | |
| first_num_params = int(out.get("num_params") or 0) | |
| all_p.extend(out["predictions"]) | |
| all_r.extend(out["references"]) | |
| cond_audio += float(out["audio_s"]) | |
| cond_infer += float(out["infer_s"]) | |
| done_so_far += int(out["n"]) | |
| if progress_cb is not None: | |
| try: | |
| progress_cb(done_so_far, grand_total, condition_key) | |
| except Exception: | |
| pass | |
| wer_score = round(compute_wer(all_r, all_p), 4) if all_p else 0.0 | |
| results[wer_result_key(condition_key)] = wer_score | |
| total_audio_s += cond_audio | |
| total_infer_s += cond_infer | |
| total_samples = max(total_samples, len(all_p)) | |
| results["num_params"] = first_num_params or 0 | |
| results["eval_conditions"] = list(keys) | |
| results["eval_full_run"] = is_full_condition_set(condition_keys) | |
| results["num_samples"] = total_samples | |
| results["eval_audio_seconds"] = round(total_audio_s, 3) | |
| results["eval_wall_time_s"] = round(total_infer_s, 3) | |
| if total_infer_s > 1e-6: | |
| results["eval_rtf"] = round(total_audio_s / total_infer_s, 4) | |
| else: | |
| results["eval_rtf"] = "" | |
| if progress_cb is not None: | |
| try: | |
| progress_cb(done_so_far, grand_total, "") | |
| except Exception: | |
| pass | |
| return results | |
| def _spaces_gpu_wrapped(duration_s: int, size: str): | |
| import spaces | |
| return spaces.GPU(duration=duration_s, size=size)(_run_evaluation_core) | |
| def _spaces_gpu_segment_wrapper(duration_s: int, size: str): | |
| import spaces | |
| return spaces.GPU(duration=duration_s, size=size)(_zerogpu_run_segment) | |
| def run_evaluation( | |
| model_id: str, | |
| family_id: str = "auto", | |
| progress_cb: Callable[[int, int, str], None] | None = None, | |
| custom_script: str = "", | |
| condition_keys: list[str] | None = None, | |
| ) -> dict: | |
| """ | |
| Evaluate `model_id` on all packed conditions using the selected family backend. | |
| On Hugging Face **Spaces ZeroGPU**, evaluation runs under ``spaces.GPU``: | |
| - ``FFASR_ZEROGPU_MAX_DURATION_S`` — requested max GPU seconds per **decorated** call | |
| (default **600**). Capped by ``FFASR_ZEROGPU_HUB_MAX_DURATION_S`` (default **600**) because | |
| the Hub rejects overly large values (e.g. 1800s). | |
| - ``FFASR_ZEROGPU_GPU_SIZE`` — ``large`` (default) or ``xlarge``. | |
| - ``FFASR_ZEROGPU_SAMPLES_PER_SEGMENT`` — when **>0**, each condition is split into chunks of | |
| this many samples; **each chunk** uses its own ``spaces.GPU`` call with the duration cap | |
| (the model is **reloaded** every chunk). Merge is exact for WER (full-condition preds/refs). | |
| If the ``spaces`` package is not installed (local dev), evaluation runs without the decorator. | |
| **CPU / device overrides** (see also ``evaluation/runtime.py``): | |
| - ``FFASR_DEVICE`` — ``auto`` (default), ``cpu``, or ``cuda``. Forces CPU on GPU hosts when set to ``cpu``. | |
| - ``FFASR_DISABLE_ZEROGPU`` — when ``1``/``true``, never use ``spaces.GPU`` even if ``spaces`` is installed; | |
| segmented eval (``FFASR_ZEROGPU_SAMPLES_PER_SEGMENT`` > 0) runs in-process slices instead. | |
| - ``FFASR_TORCH_NUM_THREADS`` / ``FFASR_TORCH_NUM_INTEROP_THREADS`` — optional torch thread caps (applied once per process). | |
| If ``progress_cb`` is provided, it is called as | |
| ``progress_cb(samples_done_across_all_conditions, samples_total_across_all_conditions, current_condition_key)`` | |
| periodically during the run. | |
| When ``custom_script`` is non-empty, it must define ``evaluate(file: pathlib.Path) -> str``; | |
| that function is called once per packed sample (via a temp WAV) instead of the family backend. | |
| Returns wer_clean, wer_noisy, wer_reverberant, wer_real, wer_difficult, num_samples, model_id, eval_family, plus timing. | |
| """ | |
| apply_cpu_thread_settings_once() | |
| disable_broken_torchcodec() | |
| seg = _samples_per_gpu_segment() | |
| if not use_spaces_gpu_decorator(): | |
| if seg > 0: | |
| return _run_evaluation_core_segmented_local( | |
| model_id, | |
| family_id=family_id, | |
| progress_cb=progress_cb, | |
| segment_samples=seg, | |
| custom_script=custom_script, | |
| condition_keys=condition_keys, | |
| ) | |
| return _run_evaluation_core( | |
| model_id, | |
| family_id=family_id, | |
| progress_cb=progress_cb, | |
| custom_script=custom_script, | |
| condition_keys=condition_keys, | |
| ) | |
| eff = _effective_gpu_duration_s() | |
| size = os.environ.get("FFASR_ZEROGPU_GPU_SIZE", "large").strip().lower() | |
| if size not in ("large", "xlarge"): | |
| size = "large" | |
| if seg <= 0: | |
| wrapped = _spaces_gpu_wrapped(eff, size) | |
| return wrapped( | |
| model_id, | |
| family_id=family_id, | |
| progress_cb=progress_cb, | |
| custom_script=custom_script, | |
| condition_keys=condition_keys, | |
| ) | |
| return _run_evaluation_core_segmented( | |
| model_id, | |
| family_id=family_id, | |
| progress_cb=progress_cb, | |
| segment_samples=seg, | |
| gpu_duration_s=eff, | |
| gpu_size=size, | |
| custom_script=custom_script, | |
| condition_keys=condition_keys, | |
| ) | |