Skip to content

Add video understanding to the VLM path#123

Merged
amazloumi merged 6 commits into
mainfrom
worktree-video-pipeline
Jun 26, 2026
Merged

Add video understanding to the VLM path#123
amazloumi merged 6 commits into
mainfrom
worktree-video-pipeline

Conversation

@amazloumi

@amazloumi amazloumi commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

Extends the existing image-VLM path to ingest video — a clip is an ordered set of frames — through the same registry-driven, composition-over-inheritance design. Trains from scratch end-to-end on WebVid-10M across all four fusion archs (joint-decoder, cross-attention, MoT, MoMa). The text-only and single-image paths are unchanged (bit-exact).

Pooling connector + token-count plumbing

  • Add avgpool and attentional_pool (Molmo2-style, mean-query MHA) adapters via @register_adapter; introduce a typed VisionAdapter base with output_num_tokens() so the visual-token count is adapter-derived.
  • Thread that count through the build path, the four modality strategies, and the three seq-len checks (config/job.py, distributed/parallel.py, model/vlm.py). Projection adapters stay identity → image path bit-exact.

Video data path

  • data/video_io.py: timestamp-based frame sampling (2 fps, uniform, first & last frame kept — Molmo2 §3.1/§A) + PyAV decode. (torchcodec, the paper's decoder, can't load on the cluster — no system FFmpeg + CUDA-lib mismatch — so we use PyAV, whose wheel bundles FFmpeg; lazily imported.)
  • data/video_dataset.py: WebVidVideoDataset (verified id[:2]/id[:4]/id[:6]/id.mp4 mapping, CSV manifest, reuses the image preprocessing) + VideoCollator(B, F, 3, H, W) + frame mask.
  • config/video.py: [video] VideoConfig (data_root, split, fps, max_frames, frame_size, max_samples) wired into JobConfig (+ is_video).
  • Adds av to dependencies.
  • Make the video data side pluggable: [video].dataset_type / sampling_policy select registered builders (webvid / uniform ship), dataset_name de-hardcodes the WebVid corpus dir, and av becomes an optional video dependency group. New dataset styles / policies are additive registrations.

Frame-aware model + training wiring

  • Generalize _project_image_features_project_visual_features: folds the frame axis through the encoder + pooler to (B, F·P′, dim) (image is the F == 1 case).
  • Thread frames_per_clip so the static visual-token count equals F·P′ (drives the residual budget and MoT's positional split; static == runtime).
  • scripts/train.py builds the video dataset/collator when [video] is set.
  • Add configs/train/vlm_video_webvid.toml (SigLIP2 + avgpool + WebVid).

Design faithfulness

  • Video is not a new arch, so the four existing ModalityStrategy classes were generalized rather than duplicated; new components arrive via the registry with no edits to the text/image fast paths.
  • FSDP2-only (PP still rejected for VLM); fixed-shape collation for DP-rank consistency; DCP-stable FQNs.

Deferred to follow-up PRs:
per-frame timestamp tokens + special tokens, grounding (<points>/<tracks> + point-F1/track-J&F eval), frame-mask-aware attention, bidirectional visual attention, VLM sequence packing, long-context (blocked on context-parallel being wired), and warm-start from a converted image-VLM checkpoint.

Testing

  • uv run ruff check passes
  • uv run ruff format --check passes
  • uv run pyright kempnerforge/ passes (0 errors)
  • uv run pytest tests/unit/ -v — 1493 passed, 2 skipped
  • uv run torchrun --nproc_per_node=4 -m pytest tests/distributed — 99 passed, 2 skipped, 0 failed (incl. VLM FSDP/MoT/MoMa/cross-attn suites)
  • uv run pytest tests/e2e/ --e2e — 25 passed, 1 skipped (7B --slow). 5 failures (PP / checkpoint-resume / sigterm) are pre-existing on main — verified identical with this branch's changes stashed; they're in code paths this PR doesn't touch.
  • Tested on 4× H100 (FSDP) — from-scratch WebVid training runs end-to-end for all four archs via configs/train/vlm_video_webvid.toml (+ per-arch variants).

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.95466% with 24 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
kempnerforge/data/video_io.py 80.00% 7 Missing and 4 partials ⚠️
kempnerforge/data/video_dataset.py 92.98% 4 Missing and 4 partials ⚠️
kempnerforge/model/adapter.py 96.33% 2 Missing and 2 partials ⚠️
kempnerforge/model/vlm.py 96.87% 1 Missing ⚠️
Files with missing lines Coverage Δ
kempnerforge/config/adapter.py 100.00% <100.00%> (ø)
kempnerforge/config/job.py 88.59% <100.00%> (+1.72%) ⬆️
kempnerforge/config/registry.py 100.00% <100.00%> (ø)
kempnerforge/config/video.py 100.00% <100.00%> (ø)
kempnerforge/distributed/parallel.py 59.06% <100.00%> (+0.24%) ⬆️
kempnerforge/model/vlm.py 99.07% <96.87%> (+0.12%) ⬆️
kempnerforge/model/adapter.py 97.27% <96.33%> (-2.73%) ⬇️
kempnerforge/data/video_dataset.py 92.98% <92.98%> (ø)
kempnerforge/data/video_io.py 80.00% <80.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI 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.

Pull request overview

This PR extends the existing VLM wrapper to support video clips (as ordered frame batches) using the same registry-driven composition approach as the current image-VLM path, and adds token-reducing pooling connectors so multi-frame clips fit within the sequence budget.

Changes:

  • Add pooling connectors (avgpool, attentional_pool) with a VisionAdapter.output_num_tokens() contract, and thread adapter-derived visual token counts through build/strategy/seq-len checks.
  • Add a WebVid-style video data pipeline (timestamp sampling + PyAV decode, dataset + collator, [video] config and JobConfig wiring) and hook it into scripts/train.py.
  • Generalize the VLM visual projection path to accept both (B,3,H,W) and (B,F,3,H,W) and add docs/configs/tests for video training across all four fusion archs.

Reviewed changes

Copilot reviewed 21 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
uv.lock Adds av to the locked dependency set.
pyproject.toml Adds runtime dependency on av>=17.1.0.
README.md Updates VLM docs to describe video + pooling connectors and adds a video training example command.
CHANGELOG.md Documents the new video + pooling-connector features and associated components.
scripts/train.py Wires [video] to build a video dataset/collator and passes frames_per_clip into model build.
kempnerforge/config/video.py Introduces [video] VideoConfig with validation.
kempnerforge/config/job.py Threads video-aware visual token budgeting into seq-len checks; adds is_video and [video]/[vlm] invariant.
kempnerforge/config/adapter.py Adds pooling-related config fields and token-count prediction via output_num_tokens.
kempnerforge/model/vlm.py Generalizes visual projection to 4D/5D inputs and uses adapter-derived token counts for prefix length/splits.
kempnerforge/model/adapter.py Adds VisionAdapter base, pooling adapters, and shared pooled-token-count helper.
kempnerforge/distributed/parallel.py Ensures parallel build sizes Transformer’s image-prefix split using adapter-derived visual_tokens and frames_per_clip.
kempnerforge/data/video_io.py Adds timestamp sampling policy and PyAV-based frame decoding.
kempnerforge/data/video_dataset.py Adds WebVidVideoDataset and VideoCollator producing fixed-shape (B,F,3,H,W) + frame_mask.
docs/how-to/train-on-video.md New guide explaining token budget, config, and usage for video training.
docs/how-to/index.md Links the new “Train on video” guide in the how-to index.
configs/train/vlm_video_webvid.toml Adds a reference training config for video VLM on WebVid-10M using avgpool.
tests/unit/test_vlm.py Adds unit tests for pooling token plumbing and video forward across all four archs.
tests/unit/test_adapter.py Adds unit tests for pooled token counting, pooling adapters, registry wiring, and config integration.
tests/unit/test_video_io.py Adds unit/integration tests for timestamp sampling and frame decoding (with skips when needed).
tests/unit/test_video_dataset.py Adds unit tests for dataset path mapping, masking/padding behavior, and synthetic integration.
tests/unit/test_video_config.py Adds tests for VideoConfig validation and JobConfig [video] wiring.
tests/unit/test_moma.py Updates MoMa stubs to satisfy the new output_num_tokens/frames_per_clip expectations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread kempnerforge/model/vlm.py
Comment thread kempnerforge/model/adapter.py
Comment thread kempnerforge/config/adapter.py Outdated

@camilobrownpinilla camilobrownpinilla left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall looks great. The only thing I noticed is that the data side seems to be hardwired to a specific dataset/format (WebVideo-10M/WebVideo-style); unsure if this was intentional, but perhaps worth flagging.

Comment thread kempnerforge/data/video_dataset.py
Comment thread kempnerforge/data/video_dataset.py Outdated
Comment on lines +61 to +110
data_root: Dataset root (contains ``raw/webvid-10M/data`` and
``raw/videos``).
split: ``"train"`` or ``"validation"``.
tokenizer_path: HF tokenizer id or local path.
max_text_len: Fixed-length text pad target.
max_frames / min_frames / fps: Frame-sampling knobs (see ``video_io``).
frame_size: Square pixel size per frame.
max_samples: Cap the manifest (``0`` = all).
prompt: Optional instruction prepended and masked from the loss.
image_mean / image_std: Per-channel normalization (SigLIP defaults).
"""

def __init__(
self,
data_root: str,
split: str,
tokenizer_path: str,
max_text_len: int,
*,
max_frames: int,
min_frames: int,
fps: float,
frame_size: int = 224,
max_samples: int = 0,
prompt: str = "",
image_mean: tuple[float, float, float] = DEFAULT_IMAGE_MEAN,
image_std: tuple[float, float, float] = DEFAULT_IMAGE_STD,
) -> None:
from transformers import AutoTokenizer

if split not in _VIDEO_SUBDIR:
raise ValueError(f"split must be one of {tuple(_VIDEO_SUBDIR)} (got {split!r})")
self._split = split
self._video_dir = os.path.join(data_root, "raw", "videos", _VIDEO_SUBDIR[split])
csv_dir = os.path.join(
data_root, "raw", "webvid-10M", "data", _CSV_SUBDIR[split], "partitions"
)
self._ids, self._caps = self._load_manifest(csv_dir, max_samples)
self._tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
self._pad_id = _resolve_pad_id(self._tokenizer)
self._max_text_len = max_text_len
self._max_frames = max_frames
self._min_frames = min_frames
self._fps = fps
self._frame_size = frame_size
self._prompt = prompt
self._image_mean = image_mean
self._image_std = image_std
logger.info(
"WebVidVideoDataset: %s [%s], %d clips, max_frames=%d, fps=%s, frame_size=%d",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Isn't webvid-10M a specific dataset in the WebVid-style? Why are we hardcoding it?

Comment thread kempnerforge/data/video_dataset.py Outdated
Comment thread kempnerforge/data/video_dataset.py
Comment thread kempnerforge/data/video_io.py
Comment thread scripts/train.py Outdated
@amazloumi

Copy link
Copy Markdown
Member Author

Overall looks great. The only thing I noticed is that the data side seems to be hardwired to a specific dataset/format (WebVideo-10M/WebVideo-style); unsure if this was intentional, but perhaps worth flagging.

Thanks for the review. Yes, This is a valid point and was intentional for the first working version. My plan to make it pluggable in follow up PRs to avoid long PRs. It is already a large PR, I might consider making it pluggable in the current PR.

Comment thread kempnerforge/data/video_io.py Outdated
Comment thread pyproject.toml Outdated
@amazloumi amazloumi requested a review from Naeemkh June 26, 2026 20:26
@amazloumi amazloumi merged commit 3ad9d9a into main Jun 26, 2026
6 checks passed
@amazloumi amazloumi deleted the worktree-video-pipeline branch June 26, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants