---
title: metric
---

<div class="python-reference">
<a id="fastvideo.eval.metrics.common.fvd.metric-classes"></a>
<a id="fastvideo.eval.metrics.common.fvd.metric-functions"></a>
<a id="fastvideo.eval.metrics.common.fvd.metric--extractors"></a>
<a id="fastvideo.eval.metrics.common.fvd.metric--reference-features"></a>
<a id="fastvideo.eval.metrics.common.fvd.metric.FVDMetric-functions"></a>
<a id="fastvideo.eval.metrics.common.fvd.metric.FVDMetric--parameters"></a>
<a id="fastvideo.eval.metrics.common.fvd.metric"></a>
<p>common.fvd — Fréchet Video Distance.</p>
<p>Measures distributional similarity between generated and reference videos
using a pluggable feature backbone (I3D / CLIP / VideoMAE). Lower score →
generated videos are closer to the real video distribution.</p>
<p>FVD is a dataset-level metric — it compares the distribution of a collection
of videos rather than scoring each video individually. For statistically
reliable results at least 256 videos are recommended; the standard protocol
uses 2048.</p>
<p>This metric follows the set-vs-set protocol (<code>is_set_metric=True</code>):</p>
<ul>
<li>:meth:<code>accumulate</code> is called once per video to buffer features. Routes
    on <code>sample["role"]</code> — <code>"reference"</code> → real buffer, anything else →
    generated buffer.  This is the same convention
    :class:<code>audio.frechet_distance.FrechetAudioDistanceMetric</code> uses.</li>
<li>:meth:<code>finalize</code> is called once after all videos to compute FVD.</li>
<li>:meth:<code>reset</code>    clears both buffers between evaluation runs.</li>
</ul>
<p>To score two corpora, build a samples list with
:func:<code>fastvideo.eval.io.inputs.samples_from</code> and hand it to
:meth:<code>Evaluator.evaluate</code> — the path-friendly form is::</p>
<pre><code>from fastvideo.eval import create_evaluator, samples_from
ev = create_evaluator(metrics=["common.fvd"], device="cuda:0")
result = ev.evaluate(samples=samples_from(
    video="gen/", reference="ref/",
)).corpus["common.fvd"]
</code></pre>
<h2>Extractors</h2>
<ul>
<li><code>i3d</code> (default) — Kinetics-400 I3D, the standard FVD feature space used
  in the literature.</li>
<li><code>clip</code> — CLIP ViT-B/32 per-frame embeds, mean-pooled over time. Captures
  semantic / content quality.</li>
<li><code>videomae</code> — VideoMAE-base last-hidden-state, mean-pooled over patches.
  Captures structural / motion quality.</li>
</ul>
<p>CLIP and VideoMAE are research-grade and not directly comparable to FVD
scores from the literature; use <code>i3d</code> for paper comparisons.</p>
<h2>Reference features</h2>
<p>Two ways to supply reference features:</p>
<ol>
<li><strong>Stream</strong>: pass <code>reference=</code> to :func:<code>samples_from</code>.  Equal
   cardinality emits paired samples (<code>sample["video"]</code> +
   <code>sample["reference"]</code>); unequal cardinality role-tags the unmatched
   references.  <code>accumulate</code> routes both shapes correctly.  If
   <code>cache_mode="read_write"</code> (default) and no cache exists, the
   extracted features are persisted to <em>cache_path</em> on :meth:<code>finalize</code>.</li>
<li><strong>Cache hit</strong>: a prior run wrote <code>cache_path</code>; :meth:<code>setup</code> loads it
   automatically and streamed references are unnecessary.</li>
</ol>
<p>Streamed references always win over the cache when both are present.</p>
<p>Cache resolution order (first match wins):
  1. <code>cache_path=</code> constructor kwarg, if set.
  2. <code>$FASTVIDEO_FVD_REF_FEATURES</code>, if set.
  3. <code>$&#123;FASTVIDEO_EVAL_CACHE&#125;/fvd/real_features_&#123;extractor&#125;.pt</code> (default).</p>
<p>The env-var override mirrors <code>audio.frechet_distance</code>'s
<code>FASTVIDEO_FAD_REF_FEATURES</code> pattern.</p>
<h2 id="fastvideo.eval.metrics.common.fvd.metric.BaseMetric"><code>fastvideo.eval.metrics.common.fvd.metric.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.metrics.common.fvd.metric.BaseMetric.to"><code>fastvideo.eval.metrics.common.fvd.metric.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.metrics.common.fvd.metric.BaseMetric.setup"><code>fastvideo.eval.metrics.common.fvd.metric.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.metrics.common.fvd.metric.BaseMetric.compute"><code>fastvideo.eval.metrics.common.fvd.metric.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.metrics.common.fvd.metric.BaseMetric.reset"><code>fastvideo.eval.metrics.common.fvd.metric.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.metrics.common.fvd.metric.BaseMetric.accumulate"><code>fastvideo.eval.metrics.common.fvd.metric.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.metrics.common.fvd.metric.BaseMetric.finalize"><code>fastvideo.eval.metrics.common.fvd.metric.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.metrics.common.fvd.metric.BaseMetric.merge_from"><code>fastvideo.eval.metrics.common.fvd.metric.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.metrics.common.fvd.metric.available_extractors"><code>fastvideo.eval.metrics.common.fvd.metric.available_extractors</code></h2>
<pre class="doc-signature"><code>available_extractors() -&gt; list&#91;str&#93;</code></pre>

<h2 id="fastvideo.eval.metrics.common.fvd.metric.load_extractor"><code>fastvideo.eval.metrics.common.fvd.metric.load_extractor</code></h2>
<pre class="doc-signature"><code>load_extractor(name: str, device: torch.device) -&gt; _BaseExtractor</code></pre>
<p>Instantiate the named extractor on <em>device</em>. Raises <code>ValueError</code> on unknown names.</p>
<h2 id="fastvideo.eval.metrics.common.fvd.metric.register"><code>fastvideo.eval.metrics.common.fvd.metric.register</code></h2>
<pre class="doc-signature"><code>register(name: str)</code></pre>
<p>Decorator to register a metric class.</p>
<p>Usage::</p>
<pre><code>@register("ssim")
class SSIMMetric(BaseMetric):
    ...
</code></pre>
<h2 id="fastvideo.eval.metrics.common.fvd.metric.MetricResult"><code>fastvideo.eval.metrics.common.fvd.metric.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.metrics.common.fvd.metric.FVDMetric"><code>fastvideo.eval.metrics.common.fvd.metric.FVDMetric</code></h2>
<pre class="doc-signature"><code>FVDMetric(extractor: str = &#x27;i3d&#x27;, cache_path: str | None = None, cache_mode: str = &#x27;read_write&#x27;, chunk_size: int = 32)</code></pre>
<p>Fréchet Video Distance (FVD) over a pluggable feature backbone.</p>
<p>Set-vs-set metric (<code>is_set_metric=True</code>). The Evaluator calls
:meth:<code>accumulate</code> once per video and :meth:<code>finalize</code> once after
all videos have been processed.</p>
<p>For meaningful scores, evaluate over ≥ 256 videos (2048 is the
standard protocol used in the literature).</p>
<h2>Parameters</h2>
<p>extractor : str
    Feature backbone name. One of <code>"i3d"</code> (default), <code>"clip"</code>,
    <code>"videomae"</code>. See module docstring for guidance.
cache_path : str, optional
    Where extracted reference features are cached. Resolution order:
    constructor kwarg → <code>$FASTVIDEO_FVD_REF_FEATURES</code> env-var →
    <code>$&#123;FASTVIDEO_EVAL_CACHE&#125;/fvd/real_features_&#123;extractor&#125;.pt</code>
    (default, resolved at :meth:<code>setup</code> time so the env-var can be set
    after import).
cache_mode : str
    <code>"read_write"</code> (default) — read at setup, write at finalize on
    cache miss. <code>"read"</code> — read at setup, never write.  <code>"off"</code> —
    ignore the cache entirely.  Auto-write happens only when the cache
    was missing AND reference features streamed in this run.
chunk_size : int
    Videos per forward pass. Reduce if GPU runs OOM.</p>

<details><summary>Source</summary><pre><code>@register(&quot;common.fvd&quot;)
class FVDMetric(BaseMetric):
    &quot;&quot;&quot;Fréchet Video Distance (FVD) over a pluggable feature backbone.

    Set-vs-set metric (``is_set_metric=True``). The Evaluator calls
    :meth:`accumulate` once per video and :meth:`finalize` once after
    all videos have been processed.

    For meaningful scores, evaluate over ≥ 256 videos (2048 is the
    standard protocol used in the literature).

    Parameters
    ----------
    extractor : str
        Feature backbone name. One of ``&quot;i3d&quot;`` (default), ``&quot;clip&quot;``,
        ``&quot;videomae&quot;``. See module docstring for guidance.
    cache_path : str, optional
        Where extracted reference features are cached. Resolution order:
        constructor kwarg → ``$FASTVIDEO_FVD_REF_FEATURES`` env-var →
        ``$&#123;FASTVIDEO_EVAL_CACHE&#125;/fvd/real_features_&#123;extractor&#125;.pt``
        (default, resolved at :meth:`setup` time so the env-var can be set
        after import).
    cache_mode : str
        ``&quot;read_write&quot;`` (default) — read at setup, write at finalize on
        cache miss. ``&quot;read&quot;`` — read at setup, never write.  ``&quot;off&quot;`` —
        ignore the cache entirely.  Auto-write happens only when the cache
        was missing AND reference features streamed in this run.
    chunk_size : int
        Videos per forward pass. Reduce if GPU runs OOM.
    &quot;&quot;&quot;

    name = &quot;common.fvd&quot;
    is_set_metric = True
    requires_reference = False  # reference is supplied via role=&quot;reference&quot; samples or cache
    higher_is_better = False  # lower FVD = better
    needs_gpu = True
    dependencies = &#91;&quot;huggingface_hub&quot;, &quot;scipy&quot;, &quot;transformers&quot;&#93;

    def __init__(
        self,
        extractor: str = &quot;i3d&quot;,
        cache_path: str | None = None,
        cache_mode: str = &quot;read_write&quot;,
        chunk_size: int = 32,
    ) -&gt; None:
        super().__init__()
        if extractor not in available_extractors():
            raise ValueError(f&quot;Unknown FVD extractor &#x27;&#123;extractor&#125;&#x27;. &quot;
                             f&quot;Available: &#123;available_extractors()&#125;&quot;)
        if cache_mode not in &#123;&quot;off&quot;, &quot;read&quot;, &quot;read_write&quot;&#125;:
            raise ValueError(f&quot;cache_mode must be one of &#x27;off&#x27;, &#x27;read&#x27;, &#x27;read_write&#x27;; got &#123;cache_mode!r&#125;&quot;)
        self._extractor_name = extractor
        self._cache_path_arg = cache_path
        self._cache_mode = cache_mode
        self._chunk = chunk_size
        self._extractor: _BaseExtractor | None = None

        # Accumulated feature buffers — cleared by reset()
        self._gen_buf: list&#91;np.ndarray&#93; = &#91;&#93;
        self._real_buf: list&#91;np.ndarray&#93; = &#91;&#93;
        # Cache loaded at setup() time; survives reset() (the cached corpus
        # is part of the metric&#x27;s configured state, not its run state).
        self._cached_real: np.ndarray | None = None

    def to(self, device: str | torch.device) -&gt; FVDMetric:
        super().to(device)
        if self._extractor is not None:
            self._extractor.to(self.device)
        return self

    def setup(self) -&gt; None:
        if self._extractor is not None:
            return
        # Resolve cache_path at runtime so $FASTVIDEO_EVAL_CACHE /
        # $FASTVIDEO_FVD_REF_FEATURES are both honoured even when set after
        # import. Precedence: kwarg &gt; env-var &gt; default.
        if self._cache_path_arg is not None:
            self.cache_path = os.path.expanduser(self._cache_path_arg)
        elif env_path := envs.FASTVIDEO_FVD_REF_FEATURES.get():
            self.cache_path = os.path.expanduser(env_path)
        else:
            self.cache_path = _default_cache_path(self._extractor_name)
        self._extractor = load_extractor(self._extractor_name, self.device)
        if self._cache_mode != &quot;off&quot;:
            self._cached_real = self._load_cache()

    # ------------------------------------------------------------------
    # Set-vs-set protocol
    # ------------------------------------------------------------------

    def reset(self) -&gt; None:
        &quot;&quot;&quot;Clear generated and streamed-reference buffers. Cache survives.&quot;&quot;&quot;
        self._gen_buf = &#91;&#93;
        self._real_buf = &#91;&#93;

    def accumulate(self, sample: dict) -&gt; None:
        &quot;&quot;&quot;Extract features from one video and route by ``sample&#91;&quot;role&quot;&#93;``.

        Routing rules:

        * ``role=&quot;reference&quot;`` → real buffer; the reference key is ignored.
        * Otherwise → generated buffer.  If ``sample&#91;&quot;reference&quot;&#93;`` is
          also present, its features also go into the real buffer — this
          lets a paired samples list (the shape per-sample metrics like
          LPIPS need) feed FVD in the same pass without a second decode.

        Accepts either a raw tensor or a populated :class:`Video` instance
        under either key.
        &quot;&quot;&quot;
        if self._extractor is None:
            self.setup()
        assert self._extractor is not None  # for type narrowing

        if sample.get(&quot;role&quot;) == &quot;reference&quot;:
            self._real_buf.append(self._extract(sample&#91;&quot;video&quot;&#93;))
            return

        self._gen_buf.append(self._extract(sample&#91;&quot;video&quot;&#93;))
        if &quot;reference&quot; in sample:
            self._real_buf.append(self._extract(sample&#91;&quot;reference&quot;&#93;))

    def _extract(self, video: torch.Tensor) -&gt; np.ndarray:
        &quot;&quot;&quot;Batchify (4-D → 5-D) and run the chunked extractor forward.

        Pre-condition: ``video`` is already a tensor — :class:`EvalWorker`
        unwraps :class:`Video` instances before any metric sees the sample.
        &quot;&quot;&quot;
        if video.dim() == 4:
            video = video.unsqueeze(0)  # (T, C, H, W) → (1, T, C, H, W)
        assert self._extractor is not None
        return _extract_chunked(self._extractor, video, self._chunk)

    def finalize(self) -&gt; MetricResult:
        &quot;&quot;&quot;Compute FVD from buffered generated features vs. reference features.

        Reference source priority: streamed (``_real_buf``) &gt; disk cache
        (``_cached_real``).  On a fresh cache + streamed refs, write the
        accumulated real features back to disk if ``cache_mode=&quot;read_write&quot;``.
        &quot;&quot;&quot;
        if not self._gen_buf:
            return MetricResult(
                name=self.name,
                score=None,
                details=&#123;&quot;skipped&quot;: &quot;No generated videos accumulated before finalize().&quot;&#125;,
            )
        all_gen = np.concatenate(self._gen_buf, axis=0)

        if self._real_buf:
            all_real = np.concatenate(self._real_buf, axis=0)
            ref_source = &quot;streamed&quot;
            # Persist to cache only when we built real features fresh and
            # the cache slot was empty — never silently overwrite an
            # existing cache.
            if self._cache_mode == &quot;read_write&quot; and self._cached_real is None:
                self._save_cache(all_real)
        elif self._cached_real is not None:
            all_real = self._cached_real
            ref_source = &quot;cached&quot;
        else:
            return MetricResult(
                name=self.name,
                score=None,
                details=&#123;
                    &quot;skipped&quot;: (&quot;No reference features available. Pass role=&#x27;reference&#x27; samples &quot;
                                &quot;(or a paired samples list with &#x27;reference&#x27; key) to &quot;
                                f&quot;Evaluator.evaluate(), or pre-build the cache at: &#123;self.cache_path&#125;&quot;)
                &#125;,
            )

        n_gen = len(all_gen)
        n_real = len(all_real)

        if n_gen &lt; _MIN_VIDEOS_WARN or n_real &lt; _MIN_VIDEOS_WARN:
            warnings.warn(
                f&quot;FVD computed with only &#123;n_gen&#125; generated and &#123;n_real&#125; real videos. &quot;
                f&quot;At least &#123;_MIN_VIDEOS_WARN&#125; recommended (standard protocol: 2048). &quot;
                &quot;Score may not be statistically reliable.&quot;,
                stacklevel=2,
            )

        mu_gen, sigma_gen = _gaussian_params(all_gen)
        mu_real, sigma_real = _gaussian_params(all_real)
        fvd = _frechet_distance(mu_gen, sigma_gen, mu_real, sigma_real)

        return MetricResult(
            name=self.name,
            score=fvd,
            details=&#123;
                &quot;extractor&quot;: self._extractor_name,
                &quot;n_generated&quot;: n_gen,
                &quot;n_reference&quot;: n_real,
                &quot;ref_source&quot;: ref_source,
            &#125;,
        )

    def merge_from(self, other: BaseMetric) -&gt; None:
        &quot;&quot;&quot;Fold another worker&#x27;s accumulated features into this one (multi-GPU).

        Both gen and ref buffers concatenate.  The disk cache is the same
        across workers, so we only adopt *other*&#x27;s ``_cached_real`` if our
        own slot is empty (defensive — should always already match).
        &quot;&quot;&quot;
        assert isinstance(other, FVDMetric)
        assert other._extractor_name == self._extractor_name, (
            f&quot;merge_from extractor mismatch: &#123;other._extractor_name!r&#125; vs &#123;self._extractor_name!r&#125;&quot;)
        self._gen_buf.extend(other._gen_buf)
        self._real_buf.extend(other._real_buf)
        if self._cached_real is None and other._cached_real is not None:
            self._cached_real = other._cached_real

    # ------------------------------------------------------------------
    # Cache helpers
    # ------------------------------------------------------------------

    def _load_cache(self) -&gt; np.ndarray | None:
        if os.path.exists(self.cache_path):
            data = torch.load(self.cache_path, map_location=&quot;cpu&quot;, weights_only=True)
            return np.atleast_2d(data.numpy())  # guard against stale 1-D cache
        return None

    def _save_cache(self, features: np.ndarray) -&gt; None:
        os.makedirs(os.path.dirname(self.cache_path), exist_ok=True)
        torch.save(torch.from_numpy(features), self.cache_path)</code></pre></details>
<h3 id="fastvideo.eval.metrics.common.fvd.metric.FVDMetric.to"><code>fastvideo.eval.metrics.common.fvd.metric.FVDMetric.to</code></h3>
<pre class="doc-signature"><code>to(device: str | torch.device) -&gt; FVDMetric</code></pre>

<details><summary>Source</summary><pre><code>def to(self, device: str | torch.device) -&gt; FVDMetric:
    super().to(device)
    if self._extractor is not None:
        self._extractor.to(self.device)
    return self</code></pre></details>
<h3 id="fastvideo.eval.metrics.common.fvd.metric.FVDMetric.setup"><code>fastvideo.eval.metrics.common.fvd.metric.FVDMetric.setup</code></h3>
<pre class="doc-signature"><code>setup() -&gt; None</code></pre>

<details><summary>Source</summary><pre><code>def setup(self) -&gt; None:
    if self._extractor is not None:
        return
    # Resolve cache_path at runtime so $FASTVIDEO_EVAL_CACHE /
    # $FASTVIDEO_FVD_REF_FEATURES are both honoured even when set after
    # import. Precedence: kwarg &gt; env-var &gt; default.
    if self._cache_path_arg is not None:
        self.cache_path = os.path.expanduser(self._cache_path_arg)
    elif env_path := envs.FASTVIDEO_FVD_REF_FEATURES.get():
        self.cache_path = os.path.expanduser(env_path)
    else:
        self.cache_path = _default_cache_path(self._extractor_name)
    self._extractor = load_extractor(self._extractor_name, self.device)
    if self._cache_mode != &quot;off&quot;:
        self._cached_real = self._load_cache()</code></pre></details>
<h3 id="fastvideo.eval.metrics.common.fvd.metric.FVDMetric.reset"><code>fastvideo.eval.metrics.common.fvd.metric.FVDMetric.reset</code></h3>
<pre class="doc-signature"><code>reset() -&gt; None</code></pre>
<p>Clear generated and streamed-reference buffers. Cache survives.</p>
<details><summary>Source</summary><pre><code>def reset(self) -&gt; None:
    &quot;&quot;&quot;Clear generated and streamed-reference buffers. Cache survives.&quot;&quot;&quot;
    self._gen_buf = &#91;&#93;
    self._real_buf = &#91;&#93;</code></pre></details>
<h3 id="fastvideo.eval.metrics.common.fvd.metric.FVDMetric.accumulate"><code>fastvideo.eval.metrics.common.fvd.metric.FVDMetric.accumulate</code></h3>
<pre class="doc-signature"><code>accumulate(sample: dict) -&gt; None</code></pre>
<p>Extract features from one video and route by <code>sample["role"]</code>.</p>
<p>Routing rules:</p>
<ul>
<li><code>role="reference"</code> → real buffer; the reference key is ignored.</li>
<li>Otherwise → generated buffer.  If <code>sample["reference"]</code> is
  also present, its features also go into the real buffer — this
  lets a paired samples list (the shape per-sample metrics like
  LPIPS need) feed FVD in the same pass without a second decode.</li>
</ul>
<p>Accepts either a raw tensor or a populated :class:<code>Video</code> instance
under either key.</p>
<details><summary>Source</summary><pre><code>def accumulate(self, sample: dict) -&gt; None:
    &quot;&quot;&quot;Extract features from one video and route by ``sample&#91;&quot;role&quot;&#93;``.

    Routing rules:

    * ``role=&quot;reference&quot;`` → real buffer; the reference key is ignored.
    * Otherwise → generated buffer.  If ``sample&#91;&quot;reference&quot;&#93;`` is
      also present, its features also go into the real buffer — this
      lets a paired samples list (the shape per-sample metrics like
      LPIPS need) feed FVD in the same pass without a second decode.

    Accepts either a raw tensor or a populated :class:`Video` instance
    under either key.
    &quot;&quot;&quot;
    if self._extractor is None:
        self.setup()
    assert self._extractor is not None  # for type narrowing

    if sample.get(&quot;role&quot;) == &quot;reference&quot;:
        self._real_buf.append(self._extract(sample&#91;&quot;video&quot;&#93;))
        return

    self._gen_buf.append(self._extract(sample&#91;&quot;video&quot;&#93;))
    if &quot;reference&quot; in sample:
        self._real_buf.append(self._extract(sample&#91;&quot;reference&quot;&#93;))</code></pre></details>
<h3 id="fastvideo.eval.metrics.common.fvd.metric.FVDMetric.finalize"><code>fastvideo.eval.metrics.common.fvd.metric.FVDMetric.finalize</code></h3>
<pre class="doc-signature"><code>finalize() -&gt; MetricResult</code></pre>
<p>Compute FVD from buffered generated features vs. reference features.</p>
<p>Reference source priority: streamed (<code>_real_buf</code>) &gt; disk cache
(<code>_cached_real</code>).  On a fresh cache + streamed refs, write the
accumulated real features back to disk if <code>cache_mode="read_write"</code>.</p>
<details><summary>Source</summary><pre><code>def finalize(self) -&gt; MetricResult:
    &quot;&quot;&quot;Compute FVD from buffered generated features vs. reference features.

    Reference source priority: streamed (``_real_buf``) &gt; disk cache
    (``_cached_real``).  On a fresh cache + streamed refs, write the
    accumulated real features back to disk if ``cache_mode=&quot;read_write&quot;``.
    &quot;&quot;&quot;
    if not self._gen_buf:
        return MetricResult(
            name=self.name,
            score=None,
            details=&#123;&quot;skipped&quot;: &quot;No generated videos accumulated before finalize().&quot;&#125;,
        )
    all_gen = np.concatenate(self._gen_buf, axis=0)

    if self._real_buf:
        all_real = np.concatenate(self._real_buf, axis=0)
        ref_source = &quot;streamed&quot;
        # Persist to cache only when we built real features fresh and
        # the cache slot was empty — never silently overwrite an
        # existing cache.
        if self._cache_mode == &quot;read_write&quot; and self._cached_real is None:
            self._save_cache(all_real)
    elif self._cached_real is not None:
        all_real = self._cached_real
        ref_source = &quot;cached&quot;
    else:
        return MetricResult(
            name=self.name,
            score=None,
            details=&#123;
                &quot;skipped&quot;: (&quot;No reference features available. Pass role=&#x27;reference&#x27; samples &quot;
                            &quot;(or a paired samples list with &#x27;reference&#x27; key) to &quot;
                            f&quot;Evaluator.evaluate(), or pre-build the cache at: &#123;self.cache_path&#125;&quot;)
            &#125;,
        )

    n_gen = len(all_gen)
    n_real = len(all_real)

    if n_gen &lt; _MIN_VIDEOS_WARN or n_real &lt; _MIN_VIDEOS_WARN:
        warnings.warn(
            f&quot;FVD computed with only &#123;n_gen&#125; generated and &#123;n_real&#125; real videos. &quot;
            f&quot;At least &#123;_MIN_VIDEOS_WARN&#125; recommended (standard protocol: 2048). &quot;
            &quot;Score may not be statistically reliable.&quot;,
            stacklevel=2,
        )

    mu_gen, sigma_gen = _gaussian_params(all_gen)
    mu_real, sigma_real = _gaussian_params(all_real)
    fvd = _frechet_distance(mu_gen, sigma_gen, mu_real, sigma_real)

    return MetricResult(
        name=self.name,
        score=fvd,
        details=&#123;
            &quot;extractor&quot;: self._extractor_name,
            &quot;n_generated&quot;: n_gen,
            &quot;n_reference&quot;: n_real,
            &quot;ref_source&quot;: ref_source,
        &#125;,
    )</code></pre></details>
<h3 id="fastvideo.eval.metrics.common.fvd.metric.FVDMetric.merge_from"><code>fastvideo.eval.metrics.common.fvd.metric.FVDMetric.merge_from</code></h3>
<pre class="doc-signature"><code>merge_from(other: BaseMetric) -&gt; None</code></pre>
<p>Fold another worker's accumulated features into this one (multi-GPU).</p>
<p>Both gen and ref buffers concatenate.  The disk cache is the same
across workers, so we only adopt <em>other</em>'s <code>_cached_real</code> if our
own slot is empty (defensive — should always already match).</p>
<details><summary>Source</summary><pre><code>def merge_from(self, other: BaseMetric) -&gt; None:
    &quot;&quot;&quot;Fold another worker&#x27;s accumulated features into this one (multi-GPU).

    Both gen and ref buffers concatenate.  The disk cache is the same
    across workers, so we only adopt *other*&#x27;s ``_cached_real`` if our
    own slot is empty (defensive — should always already match).
    &quot;&quot;&quot;
    assert isinstance(other, FVDMetric)
    assert other._extractor_name == self._extractor_name, (
        f&quot;merge_from extractor mismatch: &#123;other._extractor_name!r&#125; vs &#123;self._extractor_name!r&#125;&quot;)
    self._gen_buf.extend(other._gen_buf)
    self._real_buf.extend(other._real_buf)
    if self._cached_real is None and other._cached_real is not None:
        self._cached_real = other._cached_real</code></pre></details>
</div>
