---
title: worker
---

<div class="python-reference">
<a id="fastvideo.eval.worker-classes"></a>
<a id="fastvideo.eval.worker-functions"></a>
<a id="fastvideo.eval.worker.EvalWorker-attributes"></a>
<a id="fastvideo.eval.worker.EvalWorker-functions"></a>
<a id="fastvideo.eval.worker"></a>
<p>EvalWorker: a single-GPU bag of metric replicas.</p>
<p>One <code>EvalWorker</code> per GPU. Holds an instance of every requested metric on
its own device, scores one sample at a time. Stateless across calls — the
worker never sees more than one <code>evaluate(...)</code> invocation at a time
(the parent :class:<code>Evaluator</code> ensures this by handing the worker to one
thread at a time).</p>
<p>This mirrors FastVideo's <code>Worker</code> layer under :class:<code>VideoGenerator</code>,
but in-process (threads, not processes) — eval metrics are independent
and need no NCCL / TP / SP / distributed init, so process isolation is
unnecessary overhead.</p>
<h2 id="fastvideo.eval.worker.clear_cache"><code>fastvideo.eval.worker.clear_cache</code></h2>
<pre class="doc-signature"><code>clear_cache() -&gt; None</code></pre>
<p>Free GPU cache + run garbage collection.</p>
<h2 id="fastvideo.eval.worker.BaseMetric"><code>fastvideo.eval.worker.BaseMetric</code></h2>
<pre class="doc-signature"><code>BaseMetric()</code></pre>
<p>Abstract base class for all eval metrics.</p>
<p>Two execution shapes:</p>
<ul>
<li>
<p><strong>Per-sample</strong> (<code>is_set_metric=False</code>, default) — implement
  :meth:<code>compute</code>. The Evaluator calls it once per input sample and
  returns one :class:<code>MetricResult</code> per sample.</p>
</li>
<li>
<p><strong>Set-vs-set</strong> (<code>is_set_metric=True</code>) — implement
  :meth:<code>accumulate</code> (called once per sample to buffer features)
  and :meth:<code>finalize</code> (called once after all samples to compute
  the corpus-level result). Use :meth:<code>reset</code> to clear buffers and
  :meth:<code>merge_from</code> to fold multi-GPU per-worker state together.</p>
</li>
</ul>
<p>Optionally override :meth:<code>setup</code> to eagerly load models. Metrics
that chunk along the time dim for memory hardcode their own chunk
size in <code>__init__</code> (see <code>optical_flow</code> for the canonical
example). Eval always processes one video per
:meth:<code>Evaluator.evaluate</code> call; <code>compute</code> / <code>accumulate</code>
receive a single sample, not a batch.</p>

<h3 id="fastvideo.eval.worker.BaseMetric.to"><code>fastvideo.eval.worker.BaseMetric.to</code></h3>
<pre class="doc-signature"><code>to(device: str | torch.device) -&gt; BaseMetric</code></pre>
<p>Move metric (and its internal models) to <em>device</em>.</p>
<h3 id="fastvideo.eval.worker.BaseMetric.setup"><code>fastvideo.eval.worker.BaseMetric.setup</code></h3>
<pre class="doc-signature"><code>setup() -&gt; None</code></pre>
<p>Eagerly load models. Called once by :class:<code>EvalWorker</code>.</p>
<p>Default is a no-op; metrics with no eager state (pixel math,
closed-form ops) inherit this. Override only if your metric
needs to load weights.</p>
<h3 id="fastvideo.eval.worker.BaseMetric.compute"><code>fastvideo.eval.worker.BaseMetric.compute</code></h3>
<pre class="doc-signature"><code>compute(sample: dict) -&gt; MetricResult</code></pre>
<p>Per-sample metrics: compute the score for one sample.</p>
<p><code>sample["video"]</code> is <code>(T, C, H, W)</code> float in <code>[0, 1]</code>.
<code>sample["reference"]</code> (if used) has the same shape. Return
<code>self._skip(sample, reason)</code> for missing inputs.</p>
<h3 id="fastvideo.eval.worker.BaseMetric.reset"><code>fastvideo.eval.worker.BaseMetric.reset</code></h3>
<pre class="doc-signature"><code>reset() -&gt; None</code></pre>
<p>Clear accumulator state at the start of each evaluate() call.</p>
<h3 id="fastvideo.eval.worker.BaseMetric.accumulate"><code>fastvideo.eval.worker.BaseMetric.accumulate</code></h3>
<pre class="doc-signature"><code>accumulate(sample: dict) -&gt; None</code></pre>
<p>Buffer per-sample features for a corpus-level metric.</p>
<h3 id="fastvideo.eval.worker.BaseMetric.finalize"><code>fastvideo.eval.worker.BaseMetric.finalize</code></h3>
<pre class="doc-signature"><code>finalize() -&gt; MetricResult</code></pre>
<p>Compute the corpus-level result from buffered state.</p>
<h3 id="fastvideo.eval.worker.BaseMetric.merge_from"><code>fastvideo.eval.worker.BaseMetric.merge_from</code></h3>
<pre class="doc-signature"><code>merge_from(other: BaseMetric) -&gt; None</code></pre>
<p>Multi-GPU: fold another worker's accumulator state into this one.</p>
<h2 id="fastvideo.eval.worker.get_metric"><code>fastvideo.eval.worker.get_metric</code></h2>
<pre class="doc-signature"><code>get_metric(name: str, **kwargs: Any) -&gt; BaseMetric</code></pre>
<p>Instantiate a registered metric by name.</p>
<p>Checks that optional dependencies are installed before instantiation
and gives a clear install hint pointing at the right extra group.</p>
<h2 id="fastvideo.eval.worker.MetricResult"><code>fastvideo.eval.worker.MetricResult</code></h2>
<pre class="doc-signature"><code>MetricResult(name: str, score: float | None, details: dict&#91;str, Any&#93; = dict())</code></pre>
<p>Standard result container returned by all metrics.</p>
<p><code>score</code> is <code>None</code> when the metric was skipped (e.g. missing
required input).  Check <code>details["skipped"]</code> for the reason.</p>

<h2 id="fastvideo.eval.worker.Video"><code>fastvideo.eval.worker.Video</code></h2>
<pre class="doc-signature"><code>Video(source: Any, fps: float | None = None, frames: Any = None, audio: Any = None, audio_sr: int | None = None)</code></pre>
<p>Path-backed media handle. The :class:<code>VideoPool</code> populates
<code>frames</code> (and optionally <code>audio</code>) before the metric loop sees
the sample.</p>

<h3 id="fastvideo.eval.worker.Video.has_frames"><code>fastvideo.eval.worker.Video.has_frames</code></h3>
<pre class="doc-signature"><code>has_frames() -&gt; bool</code></pre>

<h3 id="fastvideo.eval.worker.Video.has_audio"><code>fastvideo.eval.worker.Video.has_audio</code></h3>
<pre class="doc-signature"><code>has_audio() -&gt; bool</code></pre>

<h2 id="fastvideo.eval.worker.EvalWorker"><code>fastvideo.eval.worker.EvalWorker</code></h2>
<pre class="doc-signature"><code>EvalWorker(metric_names: list&#91;str&#93;, device: str, *, compile: bool = False, pre_upload: bool = True, skip_missing_deps: bool = False)</code></pre>
<p>Owns metric replicas on one device. Single-GPU, single-sample.</p>
<p>Per-sample metrics (<code>is_set_metric=False</code>) return one
:class:<code>MetricResult</code> per <code>evaluate</code> call. Set metrics
(<code>is_set_metric=True</code>) accumulate state on the worker's own
instance and contribute nothing to the per-sample return — the
Evaluator finalizes them after the pool drains.</p>

<details><summary>Source</summary><pre><code>class EvalWorker:
    &quot;&quot;&quot;Owns metric replicas on one device. Single-GPU, single-sample.

    Per-sample metrics (``is_set_metric=False``) return one
    :class:`MetricResult` per ``evaluate`` call. Set metrics
    (``is_set_metric=True``) accumulate state on the worker&#x27;s own
    instance and contribute nothing to the per-sample return — the
    Evaluator finalizes them after the pool drains.
    &quot;&quot;&quot;

    def __init__(self,
                 metric_names: list&#91;str&#93;,
                 device: str,
                 *,
                 compile: bool = False,
                 pre_upload: bool = True,
                 skip_missing_deps: bool = False) -&gt; None:
        self._names = list(metric_names)
        self._device = device
        self._compile = compile
        self._pre_upload = pre_upload
        self._skip_missing_deps = skip_missing_deps
        self._metrics: dict&#91;str, BaseMetric&#93; = &#123;&#125;
        self._unloaded = False
        self._load()

    @property
    def device(self) -&gt; str:
        return self._device

    @property
    def metric_names(self) -&gt; list&#91;str&#93;:
        &quot;&quot;&quot;Names of the metrics this worker owns, in load order.&quot;&quot;&quot;
        return list(self._metrics.keys())

    def _load(self) -&gt; None:
        for name in self._names:
            try:
                m = get_metric(name)
                m.to(self._device)
                m.setup()
            except (ImportError, ModuleNotFoundError) as e:
                # Declared deps are checked up-front in get_metric; this
                # catches transitive imports done lazily in setup() (e.g.
                # vbench upstream pulling decord from its utils.py).
                if self._skip_missing_deps:
                    import logging
                    logging.getLogger(__name__).warning(&quot;eval: skipping %s; setup-time import failed: %s&quot;, name, e)
                    continue
                raise
            if self._compile and getattr(m, &quot;_model&quot;, None) is not None:
                m._model = torch.compile(m._model)
            self._metrics&#91;name&#93; = m
        # Skip pre_upload when no loaded metric actually runs on GPU
        # (e.g. an all-physics_iq run) — it would just pay a wasted
        # host→device→host round-trip per sample.
        self._any_needs_gpu = any(m.needs_gpu for m in self._metrics.values())
        self._unloaded = False

    def evaluate(self, *, metrics: list&#91;str&#93; | None = None, **kwargs) -&gt; dict&#91;str, MetricResult&#93;:
        &quot;&quot;&quot;Score one already-decoded sample.

        Inputs (``video`` / ``reference`` paths) are decoded and
        normalized upstream by the :class:`VideoPool`. The worker
        handles the optional pre-upload to its device and dispatches to
        each metric&#x27;s ``compute`` or ``accumulate``.

        Samples tagged ``role=&quot;reference&quot;`` are corpus context for set
        metrics only — per-sample metrics skip them so a mixed
        Evaluator (e.g. FVD + LPIPS + vbench) doesn&#x27;t produce spurious
        per-sample scores on the reference half of the corpus.

        ``metrics`` (when not ``None``) further restricts dispatch to
        that subset of registered names — used by the per-call
        ``Evaluator.evaluate(metrics=...)`` filter.
        &quot;&quot;&quot;
        if self._unloaded:
            raise RuntimeError(&quot;EvalWorker was unloaded; call reload() before evaluating.&quot;)

        sample = dict(kwargs)
        # Unwrap ``Video`` → its ``.frames`` tensor for per-sample metric
        # consumers (PSNR/SSIM/LPIPS/optical_flow). The pool decodes Video
        # instances but leaves the wrapper in place so source-aware metrics
        # (audio.imagebind_score) can still read ``Video.source``; those
        # metrics should additionally pass ``sample&#91;&quot;video_path&quot;&#93;`` if they
        # need the path. Per-sample tensor consumers get a tensor either way.
        for _k in (&quot;video&quot;, &quot;reference&quot;):
            _v = sample.get(_k)
            if isinstance(_v, Video):
                sample&#91;_k&#93; = _v.frames
        if self._pre_upload and self._any_needs_gpu:
            sample&#91;&quot;video&quot;&#93; = _to_device(sample.get(&quot;video&quot;), self._device)
            if &quot;reference&quot; in sample:
                sample&#91;&quot;reference&quot;&#93; = _to_device(sample&#91;&quot;reference&quot;&#93;, self._device)

        is_ref = sample.get(&quot;role&quot;) == &quot;reference&quot;
        filter_set: set&#91;str&#93; | None = set(metrics) if metrics is not None else None
        results: dict&#91;str, MetricResult&#93; = &#123;&#125;
        broken: list&#91;str&#93; = &#91;&#93;
        for name, m in self._metrics.items():
            if filter_set is not None and name not in filter_set:
                continue
            try:
                if m.is_set_metric:
                    m.accumulate(sample)
                elif not is_ref:
                    results&#91;name&#93; = m.compute(sample)
            except (ImportError, ModuleNotFoundError, FileNotFoundError) as e:
                # In skip_missing_deps mode, compute/accumulate-time failures
                # whose root cause is a missing dependency (lazy import of a
                # lib like torchcodec) or a missing resource (model checkpoint
                # not on disk) drop the metric for the remainder of this
                # Evaluator instead of bringing the whole run down. Strict
                # mode re-raises. Programmer bugs, OOM, and other runtime
                # failures are intentionally NOT caught here — they surface.
                if not self._skip_missing_deps:
                    raise
                import logging
                logging.getLogger(__name__).exception(&quot;eval: dropping %s after %s: %s&quot;, name, type(e).__name__, e)
                broken.append(name)
        for n in broken:
            self._metrics.pop(n, None)
        return results

    def set_metrics(self) -&gt; dict&#91;str, BaseMetric&#93;:
        &quot;&quot;&quot;Return ``&#123;name: instance&#125;`` for set metrics on this worker.&quot;&quot;&quot;
        return &#123;n: m for n, m in self._metrics.items() if m.is_set_metric&#125;

    def reset_set_metrics(self) -&gt; None:
        &quot;&quot;&quot;Clear accumulator state on every set metric.&quot;&quot;&quot;
        for m in self._metrics.values():
            if m.is_set_metric:
                m.reset()

    def release_cuda_memory(self) -&gt; None:
        &quot;&quot;&quot;Free CUDA caches without dropping models.&quot;&quot;&quot;
        clear_cache()
        if torch.cuda.is_available():
            with contextlib.suppress(Exception):
                torch.cuda.ipc_collect()

    def unload(self) -&gt; None:
        &quot;&quot;&quot;Drop metric refs so models become GC-able. Reverse with reload().&quot;&quot;&quot;
        self._metrics = &#123;&#125;
        self._unloaded = True
        self.release_cuda_memory()

    def reload(self) -&gt; None:
        &quot;&quot;&quot;Rebuild metrics dropped by :meth:`unload`.&quot;&quot;&quot;
        if self._unloaded:
            self._load()</code></pre></details>
<h3 id="fastvideo.eval.worker.EvalWorker.metric_names"><code>fastvideo.eval.worker.EvalWorker.metric_names</code></h3>
<pre class="doc-signature"><code>metric_names: list&#91;str&#93;</code></pre>
<p>Names of the metrics this worker owns, in load order.</p>
<details><summary>Source</summary><pre><code>def metric_names(self) -&gt; list&#91;str&#93;:
    &quot;&quot;&quot;Names of the metrics this worker owns, in load order.&quot;&quot;&quot;
    return list(self._metrics.keys())</code></pre></details>
<h3 id="fastvideo.eval.worker.EvalWorker.evaluate"><code>fastvideo.eval.worker.EvalWorker.evaluate</code></h3>
<pre class="doc-signature"><code>evaluate(*, metrics: list&#91;str&#93; | None = None, **kwargs) -&gt; dict&#91;str, MetricResult&#93;</code></pre>
<p>Score one already-decoded sample.</p>
<p>Inputs (<code>video</code> / <code>reference</code> paths) are decoded and
normalized upstream by the :class:<code>VideoPool</code>. The worker
handles the optional pre-upload to its device and dispatches to
each metric's <code>compute</code> or <code>accumulate</code>.</p>
<p>Samples tagged <code>role="reference"</code> are corpus context for set
metrics only — per-sample metrics skip them so a mixed
Evaluator (e.g. FVD + LPIPS + vbench) doesn't produce spurious
per-sample scores on the reference half of the corpus.</p>
<p><code>metrics</code> (when not <code>None</code>) further restricts dispatch to
that subset of registered names — used by the per-call
<code>Evaluator.evaluate(metrics=...)</code> filter.</p>
<details><summary>Source</summary><pre><code>def evaluate(self, *, metrics: list&#91;str&#93; | None = None, **kwargs) -&gt; dict&#91;str, MetricResult&#93;:
    &quot;&quot;&quot;Score one already-decoded sample.

    Inputs (``video`` / ``reference`` paths) are decoded and
    normalized upstream by the :class:`VideoPool`. The worker
    handles the optional pre-upload to its device and dispatches to
    each metric&#x27;s ``compute`` or ``accumulate``.

    Samples tagged ``role=&quot;reference&quot;`` are corpus context for set
    metrics only — per-sample metrics skip them so a mixed
    Evaluator (e.g. FVD + LPIPS + vbench) doesn&#x27;t produce spurious
    per-sample scores on the reference half of the corpus.

    ``metrics`` (when not ``None``) further restricts dispatch to
    that subset of registered names — used by the per-call
    ``Evaluator.evaluate(metrics=...)`` filter.
    &quot;&quot;&quot;
    if self._unloaded:
        raise RuntimeError(&quot;EvalWorker was unloaded; call reload() before evaluating.&quot;)

    sample = dict(kwargs)
    # Unwrap ``Video`` → its ``.frames`` tensor for per-sample metric
    # consumers (PSNR/SSIM/LPIPS/optical_flow). The pool decodes Video
    # instances but leaves the wrapper in place so source-aware metrics
    # (audio.imagebind_score) can still read ``Video.source``; those
    # metrics should additionally pass ``sample&#91;&quot;video_path&quot;&#93;`` if they
    # need the path. Per-sample tensor consumers get a tensor either way.
    for _k in (&quot;video&quot;, &quot;reference&quot;):
        _v = sample.get(_k)
        if isinstance(_v, Video):
            sample&#91;_k&#93; = _v.frames
    if self._pre_upload and self._any_needs_gpu:
        sample&#91;&quot;video&quot;&#93; = _to_device(sample.get(&quot;video&quot;), self._device)
        if &quot;reference&quot; in sample:
            sample&#91;&quot;reference&quot;&#93; = _to_device(sample&#91;&quot;reference&quot;&#93;, self._device)

    is_ref = sample.get(&quot;role&quot;) == &quot;reference&quot;
    filter_set: set&#91;str&#93; | None = set(metrics) if metrics is not None else None
    results: dict&#91;str, MetricResult&#93; = &#123;&#125;
    broken: list&#91;str&#93; = &#91;&#93;
    for name, m in self._metrics.items():
        if filter_set is not None and name not in filter_set:
            continue
        try:
            if m.is_set_metric:
                m.accumulate(sample)
            elif not is_ref:
                results&#91;name&#93; = m.compute(sample)
        except (ImportError, ModuleNotFoundError, FileNotFoundError) as e:
            # In skip_missing_deps mode, compute/accumulate-time failures
            # whose root cause is a missing dependency (lazy import of a
            # lib like torchcodec) or a missing resource (model checkpoint
            # not on disk) drop the metric for the remainder of this
            # Evaluator instead of bringing the whole run down. Strict
            # mode re-raises. Programmer bugs, OOM, and other runtime
            # failures are intentionally NOT caught here — they surface.
            if not self._skip_missing_deps:
                raise
            import logging
            logging.getLogger(__name__).exception(&quot;eval: dropping %s after %s: %s&quot;, name, type(e).__name__, e)
            broken.append(name)
    for n in broken:
        self._metrics.pop(n, None)
    return results</code></pre></details>
<h3 id="fastvideo.eval.worker.EvalWorker.set_metrics"><code>fastvideo.eval.worker.EvalWorker.set_metrics</code></h3>
<pre class="doc-signature"><code>set_metrics() -&gt; dict&#91;str, BaseMetric&#93;</code></pre>
<p>Return <code>&#123;name: instance&#125;</code> for set metrics on this worker.</p>
<details><summary>Source</summary><pre><code>def set_metrics(self) -&gt; dict&#91;str, BaseMetric&#93;:
    &quot;&quot;&quot;Return ``&#123;name: instance&#125;`` for set metrics on this worker.&quot;&quot;&quot;
    return &#123;n: m for n, m in self._metrics.items() if m.is_set_metric&#125;</code></pre></details>
<h3 id="fastvideo.eval.worker.EvalWorker.reset_set_metrics"><code>fastvideo.eval.worker.EvalWorker.reset_set_metrics</code></h3>
<pre class="doc-signature"><code>reset_set_metrics() -&gt; None</code></pre>
<p>Clear accumulator state on every set metric.</p>
<details><summary>Source</summary><pre><code>def reset_set_metrics(self) -&gt; None:
    &quot;&quot;&quot;Clear accumulator state on every set metric.&quot;&quot;&quot;
    for m in self._metrics.values():
        if m.is_set_metric:
            m.reset()</code></pre></details>
<h3 id="fastvideo.eval.worker.EvalWorker.release_cuda_memory"><code>fastvideo.eval.worker.EvalWorker.release_cuda_memory</code></h3>
<pre class="doc-signature"><code>release_cuda_memory() -&gt; None</code></pre>
<p>Free CUDA caches without dropping models.</p>
<details><summary>Source</summary><pre><code>def release_cuda_memory(self) -&gt; None:
    &quot;&quot;&quot;Free CUDA caches without dropping models.&quot;&quot;&quot;
    clear_cache()
    if torch.cuda.is_available():
        with contextlib.suppress(Exception):
            torch.cuda.ipc_collect()</code></pre></details>
<h3 id="fastvideo.eval.worker.EvalWorker.unload"><code>fastvideo.eval.worker.EvalWorker.unload</code></h3>
<pre class="doc-signature"><code>unload() -&gt; None</code></pre>
<p>Drop metric refs so models become GC-able. Reverse with reload().</p>
<details><summary>Source</summary><pre><code>def unload(self) -&gt; None:
    &quot;&quot;&quot;Drop metric refs so models become GC-able. Reverse with reload().&quot;&quot;&quot;
    self._metrics = &#123;&#125;
    self._unloaded = True
    self.release_cuda_memory()</code></pre></details>
<h3 id="fastvideo.eval.worker.EvalWorker.reload"><code>fastvideo.eval.worker.EvalWorker.reload</code></h3>
<pre class="doc-signature"><code>reload() -&gt; None</code></pre>
<p>Rebuild metrics dropped by :meth:<code>unload</code>.</p>
<details><summary>Source</summary><pre><code>def reload(self) -&gt; None:
    &quot;&quot;&quot;Rebuild metrics dropped by :meth:`unload`.&quot;&quot;&quot;
    if self._unloaded:
        self._load()</code></pre></details>
</div>
