Skip to content

[refactor] eval: consolidate FVD into common.fvd, remove benchmarks/fvd - #1380

Merged
SolitaryThinker merged 10 commits into
mainfrom
shao/fvd
May 24, 2026
Merged

[refactor] eval: consolidate FVD into common.fvd, remove benchmarks/fvd#1380
SolitaryThinker merged 10 commits into
mainfrom
shao/fvd

Conversation

@shaoxiongduan

Copy link
Copy Markdown
Collaborator

Purpose

Registers Fréchet Video Distance as the common.fvd eval metric, brings it to feature parity with the standalone benchmarks/fvd/ package, and removes the old code path. Net diff: +913 / −1605 lines across 21 files.

Builds on #1341 (@abaghyangor). Credits are preserved via Co-authored-by trailers on every commit.

Changes

  • fastvideo/eval/metrics/common/fvd/ — new registered metric
    • metric.py: set-vs-set protocol (is_set_metric=True, accumulate / finalize / merge_from), filelock-safe I3D checkpoint via ensure_checkpoint, reference-feature cache keyed by extractor. Cache resolution order: constructor kwarg → $FASTVIDEO_FVD_REF_FEATURES${FASTVIDEO_EVAL_CACHE}/fvd/real_features_{extractor}.pt (mirrors audio.frechet_distance's pattern).
    • extractors.py: _BaseExtractor ABC + I3D / CLIP / VideoMAE subclasses, selected via the extractor="i3d" constructor kwarg. I3D preprocess orders scale-then-resize to bit-match the old benchmarks/fvd/feature_extractors.py (mathematically equivalent to the reverse order, but FP rounding diverges through ~30 Conv3D layers).
  • examples/inference/eval/eval_fvd.py — folder-level FVD via the eval API. Flags: --gen-dir, --reference-dir, --extractor {i3d,clip,videomae}, --cache-path, --chunk-size, --device, --output. Replaces bash benchmarks/scripts/run.sh.
  • fastvideo/tests/eval/test_fvd.py — 17 CPU-only tests: registry contract, extractor factory, skip path, math sanity, cache partitioning, env-var/kwarg precedence, merge_from extractor-mismatch rejection. The extractor loader is monkeypatched so CI doesn't download model weights.
  • Removed: benchmarks/fvd/ (9 files) and benchmarks/scripts/{run.sh, setup_fvd.sh}. The benchmarks/ tree was FVD-only — nothing else lived there.
  • Docs retargeted: fastvideo/eval/README.md, .agents/memory/evaluation-registry/README.md, .agents/onboarding/worldmodel-training/README.md, .agents/workflows/evaluation-development.md, docs/contributing/eval-metrics.md.

No pyproject.toml changes — scipy, transformers, and huggingface_hub are all already base dependencies.

Test Plan

# Pre-commit
pre-commit run --files \
  fastvideo/eval/metrics/common/fvd/__init__.py \
  fastvideo/eval/metrics/common/fvd/metric.py \
  fastvideo/eval/metrics/common/fvd/extractors.py \
  fastvideo/eval/README.md \
  fastvideo/tests/eval/test_fvd.py \
  examples/inference/eval/eval_fvd.py \
  .agents/memory/evaluation-registry/README.md \
  .agents/onboarding/worldmodel-training/README.md \
  .agents/workflows/evaluation-development.md \
  docs/contributing/eval-metrics.md

# Unit tests (CPU-only, no model download)
python -m pytest fastvideo/tests/eval/test_fvd.py -v

# End-to-end bit-parity vs the deleted benchmarks/fvd/ (reconstructed
# in a worktree at origin/main). Compared I3D / CLIP / VideoMAE features
# and the full FVD score for identical fixed-seed (B=8) inputs.
python fvd_parity_test.py

Test Results

Pre-commit
yapf.....................................................................Passed
ruff (legacy alias)......................................................Passed
codespell................................................................Passed
PyMarkdown...............................................................Passed
mypy.....................................................................Passed
Check for spaces in all filenames........................................Passed
pytest (17 / 17 passed)
======================= 17 passed, 26 warnings in 6.71s ========================
Bit-parity vs benchmarks/fvd/
Device: cuda:0    torch: 2.11.0+cu128

[1/4] I3D extractor parity
  I3D features: max|diff|=0.000e+00, mean|diff|=0.000e+00, shape=(4, 400)

[2/4] CLIP extractor parity
  CLIP features: max|diff|=0.000e+00, mean|diff|=0.000e+00, shape=(4, 512)

[3/4] VideoMAE extractor parity
  VideoMAE features: max|diff|=0.000e+00, mean|diff|=0.000e+00, shape=(2, 768)

[4/4] End-to-end FVD score parity (I3D)
  OLD compute_fvd_with_config -> 241.628530
  NEW common.fvd.FVDMetric    -> 241.644102
  FVD scores match: |delta|=1.557e-02, rel=6.445e-05

The residual 6.4e-5 relative gap in the FVD score is downstream of scipy.linalg.sqrtm on near-identical 400×400 covariance matrices — the extractor features themselves are bit-identical to upstream.

Checklist

  • I ran pre-commit run --all-files and fixed all issues
  • I added or updated tests for my changes
  • I updated documentation if needed
  • I considered GPU memory impact of my changes (default chunk_size=32; CLIP/VideoMAE inherit transformers peak-memory profiles)

shaoxiongduan and others added 8 commits May 22, 2026 00:47
Port the I3D-based FVD metric from #1341 into the registered eval suite
as common.fvd, replacing the standalone benchmarks/fvd/ scripts.

  - Set-vs-set metric (is_set_metric=True): accumulate() buffers I3D
    features per video, finalize() returns the corpus Fréchet distance.
  - I3D checkpoint (flateon/FVD-I3D-torchscript) downloaded via the
    filelock-safe ensure_checkpoint() helper.
  - Reference features cached to ${FASTVIDEO_EVAL_CACHE}/fvd/real_features.pt
    on first encounter; reused automatically across runs. Override with
    $FASTVIDEO_FVD_REF_FEATURES or the cache_path= constructor kwarg
    (matches the audio.frechet_distance pattern).
  - torch.jit.fuser('none') around the I3D forward to dodge NVRTC kernel
    fusion errors on CUDA-version-mismatched hosts.
  - np.atleast_2d guards in _gaussian_params / _load_cache against
    1-D feature edge cases.
  - merge_from() folds per-worker feature buffers for multi-GPU eval.
  - Warning fires below 256 accumulated videos (standard protocol: 2048).

Drops the 'FVD as a registered metric' entry from the eval README's
'Out of scope' section and documents the new metric inline.

Co-authored-by: abaghyangor <abaghyangor@gmail.com>
Refactor common.fvd to plug in a feature backbone via the new
'extractor' constructor kwarg, replacing the I3D-only implementation
from the initial port.

  - New extractors.py: _BaseExtractor ABC + three concrete extractors
    (_I3DExtractor, _CLIPExtractor, _VideoMAEExtractor), all sharing
    the (B,T,C,H,W) float[0,1] → (B, feature_dim) numpy contract.
  - I3D's NVRTC kernel-fusion workaround moves with it into the I3D
    subclass, preserving the CUDA-version-mismatch fix.
  - CLIP uses ViT-B/32 frame embeds, mean-pooled over time.
  - VideoMAE uses MCG-NJU/videomae-base last-hidden-state, mean-pooled
    over patch tokens.
  - load_extractor(name, device) factory + available_extractors() for
    discovery / validation. Unknown names raise ValueError with the
    valid choices.
  - Default cache path now partitions by extractor name:
    ${FASTVIDEO_EVAL_CACHE}/fvd/real_features_{extractor}.pt
    Feature dimensions differ across backbones; sharing one cache
    would silently produce nonsense.
  - merge_from() asserts extractor parity between the two metric
    instances (multi-GPU eval must use the same backbone).
  - MetricResult.details now reports which extractor was used.

CLIP and VideoMAE pull transformers, which is already a base fastvideo
dependency — no pyproject.toml change required.

Ported and consolidated from the standalone implementation at
benchmarks/fvd/feature_extractors.py (slated for removal in a later
commit on this branch).

Co-authored-by: abaghyangor <abaghyangor@gmail.com>
End-to-end example that replaces the old benchmarks/scripts/run.sh +
benchmarks/fvd/run_fvd.py flow with the registered common.fvd metric.

  - --gen-dir / --reference-dir for the two video sets.
  - --extractor {i3d,clip,videomae} threads through to the metric.
  - --cache-path overrides the default reference-feature cache.
  - --output writes a structured JSON result alongside stdout.

Drives the metric directly (get_metric + accumulate/finalize) instead
of going through create_evaluator, so the extractor constructor kwarg
is exposed without changing the multi-GPU Evaluator surface.

Co-authored-by: abaghyangor <abaghyangor@gmail.com>
  - benchmarks/fvd/ (9 files: fvd.py, feature_extractors.py, i3d_model.py,
    video_utils.py, cli.py, run_fvd.py, validate_fvd.py, __init__.py,
    README.md) — all functionality is now exposed via the registered
    common.fvd metric and examples/inference/eval/eval_fvd.py.
  - benchmarks/scripts/run.sh, setup_fvd.sh — replaced by the example
    script (`python examples/inference/eval/eval_fvd.py --help`).

The benchmarks/ tree was FVD-only; nothing else lived there.

Co-authored-by: abaghyangor <abaghyangor@gmail.com>
  - .agents/memory/evaluation-registry/README.md: update location, status,
    and usage examples (programmatic + CLI) to point at the new metric
    and the eval_fvd.py example.
  - .agents/onboarding/worldmodel-training/README.md: update the FVD row
    in the quick-summary metrics table.
  - examples/inference/eval/eval_fvd.py + extractors.py: drop the
    'replaces benchmarks/fvd/...' lines now that benchmarks/fvd/ is gone.

Repo-wide grep for benchmarks/fvd / benchmarks.fvd / setup_fvd / run_fvd
returns zero matches.

Co-authored-by: abaghyangor <abaghyangor@gmail.com>
All tests run without GPU and without downloading any backbone weights.
extractors.load_extractor is monkeypatched to a deterministic
_DummyExtractor (16-d random features) via the _install_dummy_loader
helper.

Coverage:
  1. Registry — common.fvd appears in list_metrics(); class attributes
     match the set-vs-set + lower-is-better contract.
  2. Extractor factory — available_extractors() lists all three;
     unknown name raises ValueError both at the factory level and via
     the FVDMetric constructor; the registry table maps name → class
     correctly without instantiation.
  3. Skip path — finalize() without a reference returns score=None
     with the cache path in details['skipped'].
  4. Math — Fréchet distance of identical Gaussians is 0; translation
     invariance d(N(μ,Σ), N(μ+v,Σ)) == ||v||²; np.atleast_2d guard
     handles 1-D stale-cache features; n=1 covariance reshape.
  5. Cache partitioning — default path partitions by extractor name;
     env-var FASTVIDEO_FVD_REF_FEATURES overrides default; constructor
     kwarg outranks env-var; merge_from rejects extractor mismatch.

Plus an end-to-end happy-path: 5 generated + 4 reference videos yields
a finite score and writes the cache file.

Co-authored-by: abaghyangor <abaghyangor@gmail.com>
End-to-end parity check against the deleted benchmarks/fvd/ code path
(reconstructed in a worktree) found that the new _I3DExtractor produced
features that diverged from the upstream implementation by max|Δ|≈9e-3
and propagated to a relative FVD-score divergence of ~6e-3.

Root cause: the old code scaled [0,1] → [-1,1] BEFORE bilinear resize;
the new code resized first then scaled. The math is equivalent because
bilinear interpolation is linear (interp(2x-1) == 2·interp(x)-1), but
the floating-point rounding of the two orderings is not, and the small
delta is amplified by dozens of I3D Conv3D layers into a ~1e-2 feature
gap.

Switching the new code to scale-then-resize (and moving the device
transfer up-front so the entire preprocess pipeline runs on-device)
recovers bit-parity:

  parity_test:  I3D max|Δ| now 0.000e+00 within atol/rtol=1e-4
                CLIP/VideoMAE features match within 1e-3
                FVD score: OLD=241.628530 vs NEW=241.644102
                           rel=6.4e-5 (well under 1e-4 threshold)

Also splits test_gaussian_params_handles_single_sample into two cases
to reflect actual np.cov behaviour:
  - (1, 3) input → (3, 3) NaN-cov (Degrees of freedom <= 0)
  - 1-element input → 0-d scalar that the defensive reshape catches.

Co-authored-by: abaghyangor <abaghyangor@gmail.com>
Two follow-up references missed in the earlier docs sweep:

  - docs/contributing/eval-metrics.md: removed the 'don't add set-vs-set
    metrics, the protocol doesn't exist yet' bullet. The protocol DOES
    exist as of #1320 (BaseMetric.is_set_metric + accumulate/finalize/
    merge_from), and common.fvd is the second user of it after
    audio.frechet_distance.
  - .agents/workflows/evaluation-development.md: dropped the 'Native
    conversion of FVD under fastvideo/eval/metrics/fvd/' TODO bullet now
    that the conversion is done (and was placed at common/fvd/, not the
    path the bullet predicted).

Co-authored-by: abaghyangor <abaghyangor@gmail.com>
@mergify mergify Bot added type: refactor Code restructure without behavior change scope: inference Inference pipeline, serving, CLI scope: infra CI, tests, Docker, build scope: docs Documentation labels May 22, 2026
@mergify

mergify Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

Your pull request matches the following merge protections and will not be merged until they are valid.

🔴 PR merge requirements

Waiting for

  • #approved-reviews-by>=1
  • check-success=full-suite-passed
This rule is failing.
  • #approved-reviews-by>=1
  • check-success=full-suite-passed
  • check-success=fastcheck-passed
  • check-success~=pre-commit
  • title~=(?i)^\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model|skill|skills|infra)\]

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request migrates the Fréchet Video Distance (FVD) metric from a standalone benchmark into the fastvideo.eval suite as the common.fvd metric. The implementation introduces pluggable feature extractors for I3D, CLIP, and VideoMAE, along with a stateful FVDMetric class and a new CLI example script. Review feedback identifies a critical memory issue when loading large reference datasets as a single tensor, suggests using recursive globbing for video discovery in subdirectories, and recommends explicitly handling cases with fewer than two samples to avoid NaN results during covariance calculation.

if args.reference_dir is not None:
ref_paths = _list_videos(args.reference_dir)
print(f"Found {len(ref_paths)} reference videos under {args.reference_dir}")
ref_tensor = torch.stack([load_video(str(p)) for p in ref_paths])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Loading the entire reference dataset into a single tensor via torch.stack is extremely memory-intensive. For the standard FVD protocol of 2048 videos (e.g., 16 frames at 512x512), this can easily exceed 100GB of CPU RAM, leading to OOM on most machines.

Consider refactoring the FVDMetric to support incremental accumulation of reference features (similar to how it handles generated features) so that reference videos can be loaded and processed one by one in the loop below.

def _list_videos(directory: Path) -> list[Path]:
if not directory.is_dir():
raise SystemExit(f"{directory} is not a directory")
out = sorted(p for p in directory.iterdir() if p.suffix.lower() in _VIDEO_EXTS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation uses directory.iterdir(), which is not recursive. Standard video datasets (like Kinetics-400, which is the canonical reference for FVD) are typically organized into subdirectories by class. This script will fail to find videos in such layouts.

Suggested change
out = sorted(p for p in directory.iterdir() if p.suffix.lower() in _VIDEO_EXTS)
out = sorted(p for p in directory.rglob("*") if p.suffix.lower() in _VIDEO_EXTS)

Comment on lines +262 to +268
if n_gen < _MIN_VIDEOS_WARN or n_real < _MIN_VIDEOS_WARN:
warnings.warn(
f"FVD computed with only {n_gen} generated and {n_real} real videos. "
f"At least {_MIN_VIDEOS_WARN} recommended (standard protocol: 2048). "
"Score may not be statistically reliable.",
stacklevel=2,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While there is a warning for low sample counts, the code does not explicitly handle the case where n_gen < 2 or n_real < 2. In these cases, np.cov will return an array of NaN values (since the degrees of freedom N-1 is zero), resulting in a NaN FVD score. It is better to catch this edge case and return a skipped result with an explanatory message.

        if n_gen < 2 or n_real < 2:
            return MetricResult(
                name=self.name,
                score=None,
                details={
                    "skipped": f"FVD requires at least 2 samples to compute covariance. Got n_gen={n_gen}, n_reference={n_real}."
                },
            )

        if n_gen < _MIN_VIDEOS_WARN or n_real < _MIN_VIDEOS_WARN:
            warnings.warn(
                f"FVD computed with only {n_gen} generated and {n_real} real videos. "
                f"At least {_MIN_VIDEOS_WARN} recommended (standard protocol: 2048). "
                "Score may not be statistically reliable.",
                stacklevel=2,
            )

Removing the local CPU-only test scaffold for common.fvd. End-to-end
correctness is locked in by the bit-parity verification against the old
benchmarks/fvd/ implementation (run as part of the PR; results documented
in the PR description) — running the math on a 16-dim mock backbone
adds little signal on top of that.

A follow-up testing strategy that includes the metric in the existing
eval-suite CI (e.g. registry contract + a small real-model smoke under
the GPU job) can be opened as a separate PR.

Co-authored-by: abaghyangor <abaghyangor@gmail.com>
@SolitaryThinker

Copy link
Copy Markdown
Collaborator

Hi @shaoxiongduan — this is a code review from one of @SolitaryThinker's AI reviewer agents (Gob). I run these to help triage PRs but @SolitaryThinker hasn't personally verified every finding. If anything below doesn't match what you know about the code, please ping @SolitaryThinker — they'll take a closer look.

TL;DR

Nice refactor — net −928 LoC, the bit-parity story for the I3D backbone is well-reasoned, AI-co-author / commit-prefix / docs hygiene are all clean. One real find: fastvideo/eval/README.md still says cache files are real_features.pt, but the code switched to per-extractor partitioning (real_features_{extractor}.pt) when CLIP/VideoMAE landed. Plus four S2 nits, mostly small. (Heads-up on test coverage: @SolitaryThinker confirmed the test file drop is intentional and follow-up tests are planned, so that's not surfaced as a blocker here.)

Verdict: approve-with-followup

  • S0 (release-blockers): 0
  • S1 (must-fix): 1
  • S2 (should-fix): 4 (3 surfaced here as [S2-persistent] / [S2-important]; 1 in review.md)
  • S3 (discussion): not shown here; see review.md

Findings

[S1] fastvideo/eval/README.md documents the wrong default cache filename

What: fastvideo/eval/README.md:276:

Reference features are cached to ${FASTVIDEO_EVAL_CACHE}/fvd/real_features.pt

But the code uses a per-extractor partition:

# fastvideo/eval/metrics/common/fvd/metric.py:122
def _default_cache_path(extractor_name: str) -> str:
    return str(get_cache_dir() / "fvd" / f"real_features_{extractor_name}.pt")

So the real default is real_features_i3d.pt / real_features_clip.pt / real_features_videomae.pt — never plain real_features.pt.

Why it matters: A user who reads the README and tries to inspect / delete the cache file at the documented path won't find it. The metric.py docstring (lines 44–47) and .agents/memory/evaluation-registry/README.md both have it right; fastvideo/eval/README.md slipped during the second-commit refactor (abca8891, which added per-extractor partitioning) and wasn't caught in the later docs sweeps (f5d0eac, bd6caf1).

Suggested fix: Change line 276 to:

Reference features are cached to ``${FASTVIDEO_EVAL_CACHE}/fvd/real_features_{extractor}.pt``

Evidence: fastvideo/eval/README.md:276 vs. fastvideo/eval/metrics/common/fvd/metric.py:44-47, 122-124.


[S2-persistent] Add common.fvd to _CORE_METRICS so the registry test catches breakage

What: fastvideo/tests/eval/test_registry.py has a _CORE_METRICS tuple — psnr, ssim, lpips, two optical_flow, physics_iq, two vbench — that test_core_metrics_are_registered walks. The comment says the list is intentionally short to catch "wholesale registry breakage". common.fvd is the same shape as common.lpips (top-level common metric, not a vbench sub-metric), so it belongs.

Why it matters: With common.fvd not in _CORE_METRICS, a @register("common.fvd") decorator typo / import-time exception / module-rename wouldn't be caught in CI before runtime. Cheap insurance — _CORE_METRICS is the existing pattern for exactly this guard, and common.lpips (a structurally similar top-level common metric) is already there.

Suggested fix: One-line addition to _CORE_METRICS:

_CORE_METRICS = (
    "common.psnr",
    "common.ssim",
    "common.lpips",
    "common.fvd",          # ← add
    "optical_flow.gt_optical_flow",
    ...
)

Evidence: fastvideo/tests/eval/test_registry.py:16-25


[S2-important] dependencies = ["huggingface_hub", "scipy"] understates CLIP / VideoMAE needs

What: FVDMetric.dependencies = ["huggingface_hub", "scipy"]. But extractor="clip" and extractor="videomae" both import from transformers. It works today because transformers is in base (pyproject.toml:24), so the registry's pre-import check passes and the inline ImportError in the extractors never fires.

Why it matters: Asymmetry between the declared dep list and the actual import surface. If transformers ever moves to an extra, the registry pre-check (get_metricimportlib.util.find_spec(dep)) would let get_metric("common.fvd", extractor="clip") succeed and the failure would surface only at setup() time, well downstream from the install-hint message in registry._install_hint.

Suggested fix:

dependencies = ["huggingface_hub", "scipy", "transformers"]

Harmless today, futureproofs the install-hint behavior.

Evidence: fastvideo/eval/metrics/common/fvd/metric.py:161 vs. fastvideo/eval/metrics/common/fvd/extractors.py:111-114, 145-148


[S2-important] accumulate silently drops sample["reference"] once cache exists

What: Once the metric has loaded reference features (from cache or from the first call), all subsequent sample["reference"] tensors are silently ignored:

# metric.py:215-225
if self._real_features is None:
    self._real_features = self._load_cache()
if self._real_features is None:
    ref = sample.get("reference")
    if ref is not None:
        ...
        self._real_features = ...
        self._save_cache(...)
# (no else branch — references on later calls are dropped without warning)

The example script does the right thing (only sends reference on i==0), but a user porting from audio.frechet_distance — which appends every sample["reference_audio"] to a buffer — will be surprised. The docstring on accumulate says "If sample["reference"] is provided and no cache exists yet, reference features are extracted…" so the behavior is documented, but it's easy to miss.

Why it matters: Foot-gun. If a user expects per-sample-reference semantics they get the cached one with no diagnostic.

Suggested fix: Either (a) warnings.warn(...) when a non-None sample["reference"] is dropped, or (b) elevate the contract into the metric's class docstring with a "Reference handling" callout. (a) is more defensive, (b) is lighter-weight.

Evidence: fastvideo/eval/metrics/common/fvd/metric.py:210-225 vs. fastvideo/eval/metrics/audio/frechet_distance/metric.py:153-165


This review is from @SolitaryThinker's agent Gob (an AI reviewer). Ping @SolitaryThinker if any finding is off, contradicts your intent, or applies to a stale rebase. The full review (including S3 / discussion items and verification log) is archived locally at ~/.config/opencode/.gob/ACTIVE/2026-05-22T0205-pr-1380-review/review.md and available on request.

shaoxiongduan added a commit that referenced this pull request May 24, 2026
- README: correct the documented cache filename to
  ``${FASTVIDEO_EVAL_CACHE}/fvd/real_features_{extractor}.pt`` — the file
  is per-extractor partitioned since CLIP and VideoMAE landed; the old
  ``real_features.pt`` form is never used.
- test_registry: include ``common.fvd`` in ``_CORE_METRICS`` so a
  decorator/import-time breakage on this top-level metric surfaces in CI
  rather than at runtime, alongside the other structurally-similar
  top-level common metrics already in the tuple.
- metric.dependencies: declare ``transformers`` (used by the CLIP and
  VideoMAE extractors). Already in base deps so the registry pre-import
  check passes today; futureproofs if transformers ever moves to an extra.
- accumulate: ``warnings.warn`` when ``sample["reference"]`` is silently
  ignored because reference features are already loaded (from cache or an
  earlier accumulate call). Surfaces the foot-gun rather than producing a
  plausible-but-wrong score.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- README: correct the documented cache filename to
  ``${FASTVIDEO_EVAL_CACHE}/fvd/real_features_{extractor}.pt`` — the file
  is per-extractor partitioned since CLIP and VideoMAE landed; the old
  ``real_features.pt`` form is never used.
- test_registry: include ``common.fvd`` in ``_CORE_METRICS`` so a
  decorator/import-time breakage on this top-level metric surfaces in CI
  rather than at runtime, alongside the other structurally-similar
  top-level common metrics already in the tuple.
- metric.dependencies: declare ``transformers`` (used by the CLIP and
  VideoMAE extractors). Already in base deps so the registry pre-import
  check passes today; futureproofs if transformers ever moves to an extra.
- accumulate: ``warnings.warn`` when ``sample["reference"]`` is silently
  ignored because reference features are already loaded (from cache or an
  earlier accumulate call). Surfaces the foot-gun rather than producing a
  plausible-but-wrong score.
shaoxiongduan added a commit that referenced this pull request May 24, 2026
Bundle of eval-framework improvements layered on top of #1380's FVD work.

Input ergonomics:
- ``samples_from`` + ``as_video`` (``eval/io/inputs.py``): pure helpers
  that turn path-style inputs into the canonical samples list.
  Cardinality-inferred shape — equal ``|gen| == |ref|`` zips into paired
  samples, unequal cardinality role-tags the unmatched references as
  set-style trailing samples. Symmetric ``video`` / ``audio`` /
  ``reference`` / ``reference_audio`` kwargs; ``text_prompt(s)``, ``fps``,
  ``auxiliary_info`` for vbench-style attachments; ``extras=`` catch-all
  for exotic metric inputs (physics_iq ``scenario``, etc.).
- ``examples/inference/eval/eval_fvd.py``: rewrite to use samples_from
  (replaces ~65 LoC of manual sample-list assembly with 3 lines).
- ``pool.py``: collapse ``_decode`` to one rule — populate
  ``Video.frames`` on first decode. Path-string-under-key and 5-D
  back-compat branches deleted; ``samples_from`` always emits ``Video``.
- ``worker.py``: unwrap ``Video → .frames`` once before metric dispatch,
  so per-sample tensor metrics (PSNR/SSIM/LPIPS) never see the wrapper.

FVD streaming refactor:
- ``common.fvd.accumulate``: routes on ``sample["role"]`` (``"reference"``
  → real buffer; else → generated buffer, also pulls ``sample["reference"]``
  for paired inputs). ``_real_buf: list[ndarray]`` replaces the one-shot
  ndarray. ``merge_from`` folds both buffers across workers (multi-GPU
  correctness). New ``cache_mode={"off","read","read_write"}`` kwarg;
  cache write moved from ``accumulate`` (silent foot-gun) to ``finalize``.
- ``worker.py``: role-skip rule — per-sample metrics skip
  ``sample["role"] == "reference"`` so the streaming FVD coexists
  cleanly with LPIPS / PSNR / SSIM / vbench in one Evaluator.

Evaluator features:
- ``Evaluator(skip_missing_deps=True)``: drops metrics whose declared
  or transitively-imported deps aren't available, with a warning per
  skipped metric (covers vbench's hard ``decord`` import, torchcodec's
  ``libnvrtc`` requirement, etc.). Strict mode (default) re-raises.
- ``Evaluator.evaluate(metrics=[...])``: per-call subset filter. Keeps
  one long-lived Evaluator alive across structurally-different
  evaluations — e.g. LPIPS on a paired corpus, FVD on an unequal
  corpus — without burning model loads. Only filtered set-metrics
  reset their accumulators (state for the others survives across calls).

Audio:
- ``eval/io/audio.py`` (NEW): ``extract_audio_track`` via PyAV.
  Idempotent on the output ``.wav`` so parallel workers / repeated
  runs hit a cache.
- ``samples_from(audio=, reference_audio=, extract_audio=)``: explicit
  audio attachment + auto-extract sugar. Auto-extract uses a thread
  pool over PyAV (ffmpeg releases the GIL during decode).
- Audio metric ``_skip`` messages: enumerate the actually-missing key(s)
  instead of ambiguous ``"missing 'X' or 'Y'"`` text.

Other:
- ``eval/io/video.py``: PyAV fallback in the ``load_video`` decoder
  chain — decord (often unavailable) → PyAV → torchvision (read_video
  removed in 0.20+).
- ``pyproject.toml``: factor ``decord`` into ``[eval-fast-decode]`` so
  ``[eval]`` / ``[eval-full]`` install on aarch64 (decord has no aarch64
  wheels).
@SolitaryThinker

Copy link
Copy Markdown
Collaborator

Hi @shaoxiongduan — automated review from Gob, one of @SolitaryThinker's AI reviewers. Findings aren't all human-verified; ping @SolitaryThinker if anything looks off.

TL;DR

Re-review of address-commit 4f5b93b93b against the prior review of b1f2828f. All four findings that were surfaced in the previous top-level comment (1 S1 + 3 S2) are fully addressed in one surgical +15/−2 commit across the three expected files, with human authorship and clean metadata. No new findings.

Verdict: approve

  • S0: 0 S1: 0 S2: 0 S3: not shown; see review.md

Prior findings status at 4f5b93b

Prior finding Sev Status at 4f5b93b Note
F1: PR drops test_fvd.py, contradicts PR-body claim of 17 tests S1 (review.md only) ⏸️ Not addressed in code; per @SolitaryThinker the drop was intentional with follow-up tests planned. De-escalated out of comments.md in the prior review.
F2: README documents wrong default cache filename S1 fastvideo/eval/README.md:276 now reads Reference features are cached to ${FASTVIDEO_EVAL_CACHE}/fvd/real_features_{extractor}.pt```` — matches the code.
F3: Add common.fvd to _CORE_METRICS so registry test catches breakage S2-persistent fastvideo/tests/eval/test_registry.py:21 inserts "common.fvd", immediately after common.lpips, exactly in the suggested slot.
F4: dependencies understates CLIP/VideoMAE needs (transformers missing) S2-important fastvideo/eval/metrics/common/fvd/metric.py:160 now declares dependencies = ["huggingface_hub", "scipy", "transformers"].
F5: accumulate silently drops sample["reference"] once cache exists S2-important metric.py:237-249 adds an elif sample.get("reference") is not None: branch with warnings.warn(..., stacklevel=2). Message names the cache path and the per-call workaround — better than the minimum suggested bar.
F6: _gaussian_params n=1 2-D all-NaN case S2 (review.md only) Not addressed; low priority, suitable as a future follow-up.
F7–F9: S3 discussion items (bit-parity durability, to()/setup() ordering, _extract_chunked chunk-dim) S3 Not addressed (discussion-only, not expected to be).

Score: 4/4 prior comments.md findings ✅. Address-commit is surgical (+15/-2 across exactly the three expected files), commit message is a clean four-bullet changelog mapping 1:1 onto F2/F3/F4/F5, human author (shaoxiongduan), no AI co-author trailers. Tier change: approve-with-followupapprove.

— Gob (@SolitaryThinker's AI reviewer). Full review archived locally.

@SolitaryThinker
SolitaryThinker merged commit 321d511 into main May 24, 2026
14 of 15 checks passed
@SolitaryThinker
SolitaryThinker deleted the shao/fvd branch May 24, 2026 19:09
shaoxiongduan added a commit that referenced this pull request May 25, 2026
Bundle of eval-framework improvements layered on top of #1380's FVD work.

Input ergonomics:
- ``samples_from`` + ``as_video`` (``eval/io/inputs.py``): pure helpers
  that turn path-style inputs into the canonical samples list.
  Cardinality-inferred shape — equal ``|gen| == |ref|`` zips into paired
  samples, unequal cardinality role-tags the unmatched references as
  set-style trailing samples. Symmetric ``video`` / ``audio`` /
  ``reference`` / ``reference_audio`` kwargs; ``text_prompt(s)``, ``fps``,
  ``auxiliary_info`` for vbench-style attachments; ``extras=`` catch-all
  for exotic metric inputs (physics_iq ``scenario``, etc.).
- ``examples/inference/eval/eval_fvd.py``: rewrite to use samples_from
  (replaces ~65 LoC of manual sample-list assembly with 3 lines).
- ``pool.py``: collapse ``_decode`` to one rule — populate
  ``Video.frames`` on first decode. Path-string-under-key and 5-D
  back-compat branches deleted; ``samples_from`` always emits ``Video``.
- ``worker.py``: unwrap ``Video → .frames`` once before metric dispatch,
  so per-sample tensor metrics (PSNR/SSIM/LPIPS) never see the wrapper.

FVD streaming refactor:
- ``common.fvd.accumulate``: routes on ``sample["role"]`` (``"reference"``
  → real buffer; else → generated buffer, also pulls ``sample["reference"]``
  for paired inputs). ``_real_buf: list[ndarray]`` replaces the one-shot
  ndarray. ``merge_from`` folds both buffers across workers (multi-GPU
  correctness). New ``cache_mode={"off","read","read_write"}`` kwarg;
  cache write moved from ``accumulate`` (silent foot-gun) to ``finalize``.
- ``worker.py``: role-skip rule — per-sample metrics skip
  ``sample["role"] == "reference"`` so the streaming FVD coexists
  cleanly with LPIPS / PSNR / SSIM / vbench in one Evaluator.

Evaluator features:
- ``Evaluator(skip_missing_deps=True)``: drops metrics whose declared
  or transitively-imported deps aren't available, with a warning per
  skipped metric (covers vbench's hard ``decord`` import, torchcodec's
  ``libnvrtc`` requirement, etc.). Strict mode (default) re-raises.
- ``Evaluator.evaluate(metrics=[...])``: per-call subset filter. Keeps
  one long-lived Evaluator alive across structurally-different
  evaluations — e.g. LPIPS on a paired corpus, FVD on an unequal
  corpus — without burning model loads. Only filtered set-metrics
  reset their accumulators (state for the others survives across calls).

Audio:
- ``eval/io/audio.py`` (NEW): ``extract_audio_track`` via PyAV.
  Idempotent on the output ``.wav`` so parallel workers / repeated
  runs hit a cache.
- ``samples_from(audio=, reference_audio=, extract_audio=)``: explicit
  audio attachment + auto-extract sugar. Auto-extract uses a thread
  pool over PyAV (ffmpeg releases the GIL during decode).
- Audio metric ``_skip`` messages: enumerate the actually-missing key(s)
  instead of ambiguous ``"missing 'X' or 'Y'"`` text.

Other:
- ``eval/io/video.py``: PyAV fallback in the ``load_video`` decoder
  chain — decord (often unavailable) → PyAV → torchvision (read_video
  removed in 0.20+).
- ``pyproject.toml``: factor ``decord`` into ``[eval-fast-decode]`` so
  ``[eval]`` / ``[eval-full]`` install on aarch64 (decord has no aarch64
  wheels).
shaoxiongduan added a commit that referenced this pull request May 25, 2026
Bundle of eval-framework improvements layered on top of #1380's FVD work.

Input ergonomics:
- ``samples_from`` + ``as_video`` (``eval/io/inputs.py``): pure helpers
  that turn path-style inputs into the canonical samples list.
  Cardinality-inferred shape — equal ``|gen| == |ref|`` zips into paired
  samples, unequal cardinality role-tags the unmatched references as
  set-style trailing samples. Symmetric ``video`` / ``audio`` /
  ``reference`` / ``reference_audio`` kwargs; ``text_prompt(s)``, ``fps``,
  ``auxiliary_info`` for vbench-style attachments; ``extras=`` catch-all
  for exotic metric inputs (physics_iq ``scenario``, etc.).
- ``examples/inference/eval/eval_fvd.py``: rewrite to use samples_from
  (replaces ~65 LoC of manual sample-list assembly with 3 lines).
- ``pool.py``: collapse ``_decode`` to one rule — populate
  ``Video.frames`` on first decode. Path-string-under-key and 5-D
  back-compat branches deleted; ``samples_from`` always emits ``Video``.
- ``worker.py``: unwrap ``Video → .frames`` once before metric dispatch,
  so per-sample tensor metrics (PSNR/SSIM/LPIPS) never see the wrapper.

FVD streaming refactor:
- ``common.fvd.accumulate``: routes on ``sample["role"]`` (``"reference"``
  → real buffer; else → generated buffer, also pulls ``sample["reference"]``
  for paired inputs). ``_real_buf: list[ndarray]`` replaces the one-shot
  ndarray. ``merge_from`` folds both buffers across workers (multi-GPU
  correctness). New ``cache_mode={"off","read","read_write"}`` kwarg;
  cache write moved from ``accumulate`` (silent foot-gun) to ``finalize``.
- ``worker.py``: role-skip rule — per-sample metrics skip
  ``sample["role"] == "reference"`` so the streaming FVD coexists
  cleanly with LPIPS / PSNR / SSIM / vbench in one Evaluator.

Evaluator features:
- ``Evaluator(skip_missing_deps=True)``: drops metrics whose declared
  or transitively-imported deps aren't available, with a warning per
  skipped metric (covers vbench's hard ``decord`` import, torchcodec's
  ``libnvrtc`` requirement, etc.). Strict mode (default) re-raises.
- ``Evaluator.evaluate(metrics=[...])``: per-call subset filter. Keeps
  one long-lived Evaluator alive across structurally-different
  evaluations — e.g. LPIPS on a paired corpus, FVD on an unequal
  corpus — without burning model loads. Only filtered set-metrics
  reset their accumulators (state for the others survives across calls).

Audio:
- ``eval/io/audio.py`` (NEW): ``extract_audio_track`` via PyAV.
  Idempotent on the output ``.wav`` so parallel workers / repeated
  runs hit a cache.
- ``samples_from(audio=, reference_audio=, extract_audio=)``: explicit
  audio attachment + auto-extract sugar. Auto-extract uses a thread
  pool over PyAV (ffmpeg releases the GIL during decode).
- Audio metric ``_skip`` messages: enumerate the actually-missing key(s)
  instead of ambiguous ``"missing 'X' or 'Y'"`` text.

Other:
- ``eval/io/video.py``: PyAV fallback in the ``load_video`` decoder
  chain — decord (often unavailable) → PyAV → torchvision (read_video
  removed in 0.20+).
- ``pyproject.toml``: factor ``decord`` into ``[eval-fast-decode]`` so
  ``[eval]`` / ``[eval-full]`` install on aarch64 (decord has no aarch64
  wheels).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: docs Documentation scope: inference Inference pipeline, serving, CLI scope: infra CI, tests, Docker, build type: refactor Code restructure without behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants