Compare commits

..
Author SHA1 Message Date
kevin314 63a02d55f4 Add clip fix 2026-08-08 12:39:46 +00:00
kevin314 3f7d495fbd Update stage a/b script 2026-08-02 10:26:16 +00:00
kevin314 808bb8e697 Fix ckpt resuming 2026-08-01 09:28:57 +00:00
kevin314 661fa51d9e Update stage 1 2026-07-31 01:35:05 +00:00
kevin314 59843ee2d1 Merge branch 'trackwan_bidir' into klin/trackwan 2026-07-28 09:13:22 +00:00
kevin314 cb80364aaa Update preprocessing 2026-07-24 23:47:31 +00:00
shaoxiongduan 92cc893cfc trackwan: 14B teacher pipeline, layer-wise track LR, and overfit tooling
Training:
- finetune.py: optional track/base param groups (WANTRACK_TRACK_GROUP) so the
  track pathway can take a different LR (WANTRACK_TRACK_LR_MULT); patch_embedding
  gradient masked to the track slot [:, 36:]. Raises if nothing matches.
- track_warmup.py callback: hold the pretrained DiT at lr 0 while the track
  pathway warms up, then ramp it back in.

14B configs + held-alloc launchers:
- finetune_wantrack_{synth_sparse_fixed,synth_sparse_random,openvid_sparse_merged_bias,
  synth_stage2}_14b + openvid_14b_{warmup,lwlr} + 1.3B stage2 configs.
- run_wan14b_held.sh (held alloc + auto-restart + rank cleanup, per-stage WANTRACK
  env), acquire_nodes.sh, chain_stepAB.sh, run_stepB_seed.sh, run_stepC_merge.sh,
  watch_gate.sh, run_synth_stage2_slurm.sh.

Init / diagnostics:
- convert_trackwan_init_v2.py: --pe-src to lift patch_embedding[:, 36:] and
  --track-src to lift track_encoder together (co-adapted merge); sharded-source support.
- check_gate_growth.py, gradient_flow_analysis.py, gradient_flow_summarize.py.

Data pipeline: segment_tracks.py sharding; decode/segment/tracks slurm scripts;
precompute_null_text, two_track_val, cfg_ablation, lwlr_experiment.

Inference: app_multi_trace.py updates.
2026-07-23 03:52:56 +00:00
kevin314 6ef5b6b28f CoTracker compile flag + decode prefetch + benchmark knobs 2026-07-17 11:22:15 +00:00
kevin314 948e9d7610 Fuse preprocessing operations 2026-07-17 03:41:06 +00:00
kevin314 ec5a7c7e73 Update scripts 2026-07-16 10:10:35 +00:00
kevin314 c31dd6853e Optimize preproc 2026-07-16 07:43:56 +00:00
shaoxiongduan ac919b1c87 [feat] WanTrack: OpenVid preprocess + Wan2.2 synthetic-gen pipeline, training configs, first-frame saturation fix
- data_pipeline: SAM+CoTracker preprocessing, sharded Slurm launchers, OpenVid download/filter,
  and a data-parallel Wan2.2-T2V-A14B synthetic video generator with the first-frame
  over-brightness fix (over-generate 129 -> drop first 8 -> keep 121).
- examples/train: OpenVid bidir/stage2/stage3 launchers + finetune YAMLs (sparse, merged,
  merged_bias, stage2v2 hi-lr, stage3A/B/C).
- research_log: first-frame saturation root-cause writeup (DiT position-0 latent, not VAE decode).
- fastvideo: trackwan model / track-encoder / preprocessing updates, dcp_to_diffusers export, FA4.
2026-07-14 08:58:22 +00:00
kevin314 d4eab03809 Fix tracking 2026-07-10 07:13:01 +00:00
kevin314 c1332cb4fb Init 2026-07-08 07:29:59 +00:00
shaoxiongduan 8bc4e538d0 [feat] WanTrack tooling: sparse interactive app + warmstart YAML
examples/inference/gradio/trackwan/app_action.py:
  Bring the interactive drawing app in line with the sparse training
  regime our overfit runs used (WANTRACK_SPARSE=1). New 'Track budget'
  radio: 'dense' keeps the old full 50x50 grid; 'sparse' (new default)
  builds a much smaller [Tpx, num_moving + num_bg, 2] tensor -- the
  moving handle points from the drawn path + num_bg random background
  points from the training grid (seeded by the seed slider). New
  'num_bg' slider (0-60, default 20) matches WANTRACK_EXTRA_RANDOM=20.
  Overlay logic updated to draw every point in sparse mode instead of
  grid-strided subsampling that assumed N==2500.

examples/train/scenario/worldmodel/finetune_wantrack_synth_sparse_random_warmstart.yaml:
  Test config for the curriculum idea: start from sparse-fixed@1000
  and let random-sampling sparse training take over. Compared cold-
  start sparse-random (~0.8%) vs warmstart (~3.2%) vs fixed (~8.8%)
  divergence.
2026-07-07 03:24:11 +00:00
shaoxiongduan 495dd27d33 [feat] WanTrack: golf/synth overfit YAMLs for A/B ablations
Add the training configs used for the golf-overfit and 20-clip synthetic
overfit series exercising every combination of the recent changes:

  golf_nonzero / golf_overfit_nonzero     -- non-zero-init proj ablation
  synth_generic / synth_varied             -- prompt-conditioning ablation
                                              (generic vs per-clip caption)
  synth_varied_d64                         -- id_dim=64 (paper spec)
  synth_varied_d64_nobias                  -- + bias=False on track head
  synth_varied_d64_nobias_fixed            -- + WANTRACK_FIXED_SAMPLE=1
  synth_curriculum                         -- random-sample warmstart from
                                              a fixed-sample checkpoint
  synth_varied_full_val                    -- max_train_steps=1 harness that
                                              regenerates all 20 val clips
                                              in 3 modes from a checkpoint
  synth_sparse_fixed / synth_sparse_random -- 1-per-object + 20 background
                                              extras (Yongqi's recipe)

All resume from the trackwan_1.3b_i2v_control_init family (or the
d64_nobias variant) and enable the new joint text+motion CFG (w_t=3,
w_m=1.5) in track_validation.
2026-07-06 04:11:32 +00:00
shaoxiongduan 4d0571eabf [feat] WanTrack tooling: I2V gen flag, joint-CFG gradio app, sparse dashboard
data_pipeline/generate_videos.py:
  Add --image arg -> when set, forwards to VideoGenerator.generate_video
  via image_path=... so the same script can drive either T2V or I2V. Used
  to generate the shared-first-frame synthetic-toy overfit dataset from a
  single seed image.

examples/inference/gradio/trackwan/app_action.py:
  Bring the interactive drawing app in line with the training-time val
  recipe: joint text+motion CFG (MotionStream Eq. 2) with w_text and
  w_motion sliders + a mode radio (joint / text_only / no_track).
  `on_generate` now branches on mode instead of the old bare single
  forward, and text_only / no_track let the user compare against the
  same counterfactuals the val callback logs.

data_pipeline/sparse_sampling_dashboard.py:
  New interactive dashboard for eyeballing the sparse recipe
  (1 track per SAM object + num_sampled background extras). Overlays
  the FastSAM segmentation on frame 0 in the same tab20 palette used
  for the tracked points, so mask/point correspondence is visible at a
  glance. Sliders for num_sampled, random-vs-weighted extras mode, and
  seed; a button to render the full kept-tracks overlay video.
2026-07-06 04:11:04 +00:00
shaoxiongduan 7f0c06e7f2 [feat] WanTrack: paper-aligned track head + fixed-sample + sparse sampler
Changes to the track pathway inspired by (and now matching or extending)
the MotionStream paper:

track_encoder.py:
  * bias=False on temporal_conv and proj (paper Eq. 1 requires zero at
    empty cells; nn.Conv3d's default bias=True was leaving a per-channel
    constant term across every spatial cell, drowning the sparse signal).
  * zero_init default now False (paper explicitly rejects ControlNet
    zero-conv; matches Yongqi's recipe).
  * WANTRACK_FIXED_SAMPLE=1 -> deterministic sinusoidal IDs (arange(N))
    instead of a fresh random draw every forward. Same video always
    scatters the same phi_n at the same cells across steps.

wantrack.py:
  * WANTRACK_FIXED_SAMPLE plumbing: per-sample seeds derived from
    info_list[i]['id'] so the same clip -> same subset every step.
  * WANTRACK_SPARSE=1 sampler: 1 track per SAM object + WANTRACK_EXTRA_RANDOM
    background extras (uniform or lowrank-weighted via WANTRACK_EXTRA_MODE).
    Bypasses the dense 1000-2500 code path.
  * on_train_start hook: WANTRACK_NONZERO_INIT=1 re-inits track_encoder.proj
    with Kaiming after checkpoint load, avoiding the load/save round-trip
    that corrupted our earlier attempts to force non-zero.

track_validation.py:
  * paired 'no_motion_cfg' third variant per sample (with-tracks + text CFG
    only, drops the motion arm) alongside the existing 'generated' (joint
    text+motion CFG) and 'no_track' branches -- isolates how much of gen's
    quality comes from motion CFG amplification vs the base pathway.
2026-07-06 04:10:18 +00:00
shaoxiongduan aea5cadb5f [feat] WanTrack val: MotionStream-aligned joint text+motion CFG
Wire compositional text + motion CFG into TrackValidationCallback._sample,
following MotionStream Eq. 2 exactly:

  v_no_text   = v(∅, c_m)           # drop text, keep tracks
  v_no_motion = v(c_t, ∅)           # keep text, drop tracks
  v_base      = α·v_no_text + (1-α)·v_no_motion,  α = wt/(wt+wm)
  v̂           = v_base + wt·(v_full - v_no_text) + wm·(v_full - v_no_motion)

3 NFE/step when either wt or wm != 1. At wt=wm=1 collapses to single v_full
(no CFG). No-track counterfactual branch keeps plain text CFG only (motion
CFG undefined without tracks). "Unconditional" text = zero embedding, matching
WANTRACK_TEXT_DROP training.

Configs updated to MotionStream's recommended wt=3.0, wm=1.5.
2026-07-04 03:22:52 +00:00
shaoxiongduan c5444621f9 [feat] WanTrack val: paired no-track counterfactual generation
TrackValidation now also generates a NO-TRACK video per sample (same first-frame +
prompt, track_points=None -> zero track map), logged as track_val/no_track alongside
track_val/generated. If the two match, the model is ignoring the tracks; their
divergence quantifies track influence. Sanity-checked: identical at step 0 (proj=0),
should separate as the track pathway trains. Toggle via paired_no_track (default on).
2026-07-03 08:54:31 +00:00
shaoxiongduan c6d0233eab [bugfix] WanTrack: track subsampling was a no-op when max_points == N
_augment_tracks gated the object-coverage + low-rank-weighted + uniform draw on
`0 < max_points < N`, so with the default WANTRACK_MAX_POINTS=2500 and the full
2500-track grid (max_points == N) the entire subsample block was skipped -- the
model saw all 2500 tracks every step and only chunk-masking ran. Gate on
`min_points < N` instead so K~U[1000,2500] actually samples (verified: kept
1179/1437/1573 across steps, was a constant 2500).

Also: TrackValidation now applies the same sampler (loads object_ids/track_weights,
runs _augment_tracks deterministically) so validation is conditioned on -- and
overlays -- the sampled subset, not the full grid. Env-gated WANTRACK_DEBUG=1 logs
per-step sampling stats (kept count, object coverage, coord ranges). Adds a
single-video (golf) overfit config for debugging.
2026-07-03 08:18:44 +00:00
shaoxiongduan de8e9e20b4 [feat] WanTrack: EPIC-200 / static-EPIC-200 / Wan-200-sampler training configs
Sampler-on finetune configs (coverage + low-rank-weighted + uniform track
sampling, p_mask chunk masking) across three data regimes: unfiltered
egocentric EPIC-200, camera-static-filtered EPIC-200 (single kitchen), and the
Wan-200 synthetic set re-run with the sampler on (vs the AUG=0 overfit baseline).
2026-07-03 07:08:33 +00:00
shaoxiongduan 4fe7638101 [feat] WanTrack: track-informativeness, sampler-viz, static-window dataset tooling
- track_informativeness.py: compare abs-motion vs affine/low-rank residual vs
  motion/subspace clustering vs DINOv2 semantic embeddings for scoring which
  point tracks are informative; per-clip camera-motion metric; gradio viz.
- sampling_viz.py: render the exact _augment_tracks sampler (coverage + low-rank
  weighted + uniform) on a preprocessed dataset to inspect which traces it keeps.
- find_static_windows.py: phase-correlation per-frame camera-motion signal (recovers
  after turns) + greedy non-overlapping static-window extraction to build a
  relatively-still clip dataset from long untrimmed videos.
- segment_viz.py: FastSAM config sweep + fixes (config/clip filters, allowed_paths).
2026-07-03 07:08:19 +00:00
shaoxiongduan 6c23b4552a [feat] WanTrack: low-rank informativeness track sampler + weights plumbing
segment_tracks.py precomputes a per-track low-rank motion-residual weight
(track_weights) alongside object_ids; schema + i2v_track preprocess carry it
into the parquet. _augment_tracks now samples the 1000-2500 track subset as
object-coverage (>=1 pt/SAM segment) + informativeness-weighted draw
(track_weights, abs-motion fallback) + a uniform draw (WANTRACK_UNIFORM_FRAC)
so static/background context survives.
2026-07-03 07:08:03 +00:00
shaoxiongduan 7c6f8f3f23 [feat] WanTrack: SAM object segmentation for track coverage + viz
- segment_tracks.py: FastSAM on frame-0 -> per-grid-point object_ids ([N], -1=bg)
  added to each tracks .npz (smallest containing mask wins).
- Plumb object_ids through i2v_track preprocess + schema (optional/empty on
  pre-segmentation datasets; collate already guards missing keys) -> prepare_batch
  -> _augment_tracks object-coverage sampling.
- segment_viz.py: gradio viz of SAM masks + chosen per-object points + CoTracker
  tracks (colored by object) on the source videos.
Deps: ultralytics (FastSAM) + opencv-python-headless.
2026-07-02 07:14:23 +00:00
shaoxiongduan 21f25791e2 [feat] WanTrack: MotionStream-aligned track sampling + motion-CFG + Wan-200 cfg
- _augment_tracks: default random 1000-2500 track sampling (was 1-200), contiguous
  chunk masking (was per-frame), diversity weighting (favor moving/unique tracks,
  keep some static), and an object-coverage hook (>=1 track per SAM segment via
  optional object_ids). Sampling ON by default; CFG dropout now default off.
- trackwan_infer.generate: motion classifier-free guidance (guidance_scale) using
  the base no-tracks I2V branch as the unconditional.
- test_motion_cfg.py: quick guidance-scale EPE sweep.
- finetune_wantrack_wan200.yaml: 50->200 data-scale run (same params).
2026-07-02 07:05:14 +00:00
shaoxiongduan d203b5f557 [feat] WanTrack: dataset-inspection dashboard + interactive draw-trace upgrades
- droid_dataset_dashboard.py: render per-clip CoTracker overlays + motion-coverage
  stats (frac_moving, mean/max motion) + near-duplicate grouping, and a gradio
  gallery sorted least-motion-first to surface weak/duplicate clips.
- app_action.py: DTensor-safe checkpoint hot-swap (copy into each param's local
  shard instead of load_state_dict, which errors on FSDP DTensors); a Preview-traces
  button that overlays the synthetic control on the frozen first frame before
  generating; and a streaming per-step denoise log.
2026-07-01 06:36:44 +00:00
shaoxiongduan b588be05d1 [feat] WanTrack: DROID track-conditioning dataset + overfit config
convert_droid.py builds a set where the first frame is similar but motion is
diverse, the design that forces trace reliance: it scans DROID (LeRobot mirror),
clusters frame-0 embeddings, and writes the tightest cluster as 121-frame libx264
mp4s + manifest with ONE generic caption, dropping straight into extract_tracks ->
i2v_track preprocess. Adds finetune_wantrack_droid_overfit.yaml (Fun-Control init,
WANTRACK_AUG=0 clean overfit, fresh output_dir) and gitignores .gradio/.
2026-07-01 06:36:43 +00:00
shaoxiongduan 42d4f524a0 [feat] WanTrack: controllability eval + diagnostics tooling
Add the instruments used to diagnose whether the model follows the tracks:
- track_sensitivity.py: per-checkpoint swap-sensitivity (relative velocity shift
  when only the tracks / first-frame / text change) -- intrinsic input attribution.
- control_sensitivity metric: intervention-diff (generate twice, same conditioning,
  different control) scored in the moving ROI, so a static background can't dilute it.
- gen_eval_artifacts.py + app_viewer.py: pre-generate counterfactual videos
  (input vs re-extracted tracks, EPE heatmap) and browse them with no GPU.
- app_action.py: mouse-draw a motion and hot-swap checkpoints.
- test_controllability.py / cotracker_epe / app.py: clip_feature + minor updates.
2026-07-01 06:36:43 +00:00
shaoxiongduan 7349e07cec [feat] WanTrack: Fun-Control init to break the track-encoder deadlock
Initialize the 52ch TrackWan from Wan2.1-Fun-1.3B-Control so the track-slot
patch-embed channels start from Fun-Control's PRETRAINED control channels
(non-zero). Combined with the zero-init track head this gives step-0 == teacher
while still letting gradient reach the track encoder -- avoiding the double
zero-init deadlock of the from-scratch Fun-InP path (where both the track
patch-embed channels and the track-head proj are zero, so the track pathway
never trains). Adds the converter, the matching track_config channel handling,
and the InP init updates.
2026-07-01 06:36:42 +00:00
shaoxiongduan 8af0c43a65 [fix] WanTrack: enforce I2V via Fun-InP base + CLIP cross-attention
The overfit teacher ignored the first frame (validation looked T2V): the
init came from FastWan2.1-T2V-1.3B, whose image-conditioning channels are
zero/untrained and which has no CLIP pathway, so first-frame and track
conditioning received ~no gradient.

Re-base on Wan2.1-Fun-1.3B-InP (pretrained concat-I2V + CLIP):
- convert_trackwan_init: widen patch-embed base_in(36)->52, keep the 36
  pretrained image-cond channels, zero-init only the 16 track channels;
  inherit image_dim from the base config; merge sharded safetensors.
- trackwan config: image_dim=1280 builds the DiT image_embedder.
- preprocess: compute + store clip_feature (CLIP frame-0 embedding) and
  add image_encoder/image_processor to required modules.
- schema: clip_feature_{bytes,shape,dtype} in pyarrow_schema_i2v_track.
- wantrack / track_validation / trackwan_infer: feed
  encoder_hidden_states_image through train, validation, and inference.
- yaml: point at the Fun-InP init + re-preprocessed data.

Verified: step-0 validation reproduces the GT first frame (PSNR ~24-26 dB
vs ~9-11 before), i.e. I2V is enforced before any training.
2026-07-01 06:36:41 +00:00
shaoxiongduan 34a0a81be1 [feat] WanTrack: bidirectional I2V + point-track (MotionStream) finetune + eval
TrackWan = Wan2.2 + MotionStream point-track motion control. Stage-1 bidirectional
I2V finetune (text kept), built on the new modular trainer.

- DiT: TrackWanTransformer3DModel (52ch in = 16 noisy + 20 I2V + 16 track), TrackEncoder
  (sinusoidal track-ID embed, scatter, 4x temporal conv, zero-init head); config + registry.
- Trainer: WanTrackModel wrapper, i2v_track preprocess pipeline + pyarrow schema,
  TrackValidationCallback, finetune_wantrack_i2v.yaml.
- data_pipeline/: video gen (Wan2.2-A14B), CoTracker3 track extraction, 52ch init
  conversion, synthetic track authoring, controllability (CoTracker EPE) eval, standalone
  inference, Gradio demo.
- eval: motion.cotracker_epe metric. research_log/ lab notebook.
- fixes: FA3 flash_attn_func tuple unwrap; decord fallback for removed torchvision.io.read_video;
  preprocess flush write_remainder.
2026-07-01 06:36:40 +00:00
William Lin 0356205b84 [bugfix] revert fastvideo kernel version to 0.2.6 (#1495) 2026-06-25 03:52:43 -07:00
Kevin Lin 719a1879bd [bugfix] Remove incorrect V-row permutation in scaled_fp4_quant_trans_kernel (#1493) 2026-06-25 03:05:12 -07:00
Utkarsh RanjanandClaude Opus 4.8 b57180bf97 [bugfix] Warn when a requested attention backend is unsupported by a layer (#1254) (#1486)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:56:39 -07:00
William Lin 7cebf5f82c [ci] cap kernel wheel build parallelism to avoid runner OOM (#1483) 2026-06-23 14:17:56 -07:00
William Lin dd0f4b6753 [misc] cleanup misc files (#1484) 2026-06-23 14:17:22 -07:00
William Lin d303b4e03a [docs] update README (#1482) 2026-06-23 12:07:21 -07:00
xsankandmergify[bot] 31b719ae49 [perf] optimize compress & topk kernel (#1421)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-06-23 18:51:22 +00:00
William Lin 887aaf3d3e [ci] bump cuda-toolkit action to v0.2.35 to fix kernel cu130 publish (#1481) 2026-06-23 10:33:31 -07:00
William Lin b1d89eba1f [chore] release fastvideo-kernel 0.3.0 (#1478) 2026-06-23 10:02:27 -07:00
Loay RashidandSolitaryThinker 995a5fdf97 [bugfix] fixing denoising time in the fastwan script (#1480)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-06-23 10:01:23 -07:00
William Lin 70a70b689e [ci] mergify: stop auto-syncing ready PRs (#1477) 2026-06-23 03:23:58 -07:00
Shao Duan 4d6ac89b43 [ci] eval: add metric regression + identity-invariant ci tests (#1451) 2026-06-23 01:27:34 -07:00
Raghav K 4171cacd93 [bugfix] Wire Cosmos-Predict2.5 2B to its sampling preset (#1468) 2026-06-23 01:25:35 -07:00
b2ade71467 [Bugfix] QAD 5090: Torch.compile and other optimizations (15/12) (#1466)
Co-authored-by: alexzms <3036648523@qq.com>
Co-authored-by: Kaiqin Kong <k1kong@ucsd.edu>
Co-authored-by: William Lin <SolitaryThinker@users.noreply.github.com>
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-06-23 01:23:57 -07:00
Kevin Lin 82ed9fe58d [feat] QAD 5090: FP8 linear layer inference (#1465) 2026-06-22 18:25:06 -07:00
Satyam Srivastava 3d8cc4f0a0 [bugfix] Fix performance component timing extraction (#1473) 2026-06-22 13:05:35 -07:00
Satyam Srivastava 0557f7a7d9 [ci] Add performance dashboard metadata and visualizations (#1470) 2026-06-19 14:27:01 -07:00
dc66cd97ef [feat] QAD 5090: FP8 QAT linear training (14/12) (#1464)
Co-authored-by: William Lin <SolitaryThinker@users.noreply.github.com>
Co-authored-by: Loay Rashid <42599591+loaydatrain@users.noreply.github.com>
Co-authored-by: Kaiqin Kong <k1kong@ucsd.edu>
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-06-19 01:03:51 -07:00
6da206e196 [feat] QAD 5090: FP4 QAT linear STE for training (13/12) (#1463)
Co-authored-by: William Lin <SolitaryThinker@users.noreply.github.com>
Co-authored-by: Loay Rashid <42599591+loaydatrain@users.noreply.github.com>
Co-authored-by: Kaiqin Kong <k1kong@ucsd.edu>
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-06-18 15:55:47 -07:00
sumyyyyy 87f98c9b8b [kernel] Add varlen support for block-sparse attention (#1319) 2026-06-18 01:06:00 +00:00
e60601df7f [feat] QAD 5090: QAT training recipe — finetune + DMD distillation (12/12) (#1462)
Co-authored-by: William Lin <SolitaryThinker@users.noreply.github.com>
Co-authored-by: Loay Rashid <42599591+loaydatrain@users.noreply.github.com>
Co-authored-by: Kaiqin Kong <k1kong@ucsd.edu>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-06-17 15:18:52 -07:00
eed9c4bfbf [kernel] QAD 5090: Add Attn-QAT training Triton kernels (11/12) (#1460)
Co-authored-by: William Lin <SolitaryThinker@users.noreply.github.com>
Co-authored-by: Loay Rashid <42599591+loaydatrain@users.noreply.github.com>
Co-authored-by: Kaiqin Kong <k1kong@ucsd.edu>
2026-06-17 13:35:12 -07:00
1dee77f4a4 [feat] QAD 5090: Wire the Attn-QAT training attention backend (10/12) (#1459)
Co-authored-by: William Lin <SolitaryThinker@users.noreply.github.com>
Co-authored-by: Loay Rashid <42599591+loaydatrain@users.noreply.github.com>
Co-authored-by: Kaiqin Kong <k1kong@ucsd.edu>
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
Co-authored-by: alexzms <26690162+alexzms@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-06-17 13:32:56 -07:00
Satyam Srivastava b80148819c [ci] Add performance dashboard visualisation scripts (#1469) 2026-06-16 23:29:29 -07:00
c3b971488e [docs] QAD 5090: Add NVFP4 + Attn-QAT inference example and how-to (9/12) (#1458)
Co-authored-by: William Lin <SolitaryThinker@users.noreply.github.com>
Co-authored-by: Loay Rashid <42599591+loaydatrain@users.noreply.github.com>
Co-authored-by: Kaiqin Kong <k1kong@ucsd.edu>
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
Co-authored-by: alexzms <26690162+alexzms@users.noreply.github.com>
2026-06-16 14:36:50 -07:00
88e753f281 [feat] QAD 5090: Wire the Attn-QAT inference attention backend (8/12) (#1457)
Co-authored-by: William Lin <SolitaryThinker@users.noreply.github.com>
Co-authored-by: Loay Rashid <42599591+loaydatrain@users.noreply.github.com>
Co-authored-by: Kaiqin Kong <k1kong@ucsd.edu>
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
Co-authored-by: alexzms <26690162+alexzms@users.noreply.github.com>
2026-06-16 13:12:33 -07:00
77832059cc [kernel] QAD 5090: Add modified SageAttention3 FP4 inference kernels (7/12) (#1455)
Co-authored-by: William Lin <SolitaryThinker@users.noreply.github.com>
Co-authored-by: Loay Rashid <42599591+loaydatrain@users.noreply.github.com>
Co-authored-by: Kaiqin Kong <k1kong@ucsd.edu>
Co-authored-by: Edenzzzz <wtan45@wisc.edu>
2026-06-16 12:02:30 -07:00
alexzmsandmergify[bot] 633d393568 [ci] layer-0 grad-norm regression for per-method training tests (5a-ii) (#1396)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-06-12 04:45:07 +00:00
Junda SuandPeiyuan Zhang 5854aec2ce [feat] Add Wan RL DiffusionNFT training (#1450)
Co-authored-by: Peiyuan Zhang <a1286225768@gmail.com>
2026-06-11 21:18:59 -07:00
Mook 30e45c2411 [bugfix] Classify new config/sampling fields in schema parity inventory (#1446) 2026-06-10 13:38:36 -07:00
alexzms 2a4fe697a6 [docs] LTX-2.3 distilled i2v: typed-API example (from_config + generate) (#1448) 2026-06-10 10:58:14 -07:00
alexzms 921db7479d [perf] LTX-2.3 distilled i2v: drop max-autotune from compile kwargs (#1445) 2026-06-10 10:06:41 -07:00
Aryan KumarandAryan Kumar 7f539424cb [feat]: add Lucy Edit inference scaffold (#1363)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
2026-06-09 15:21:43 -07:00
19a838f54f [bugfix]: release VSA tile cache during training (#1434)
Co-authored-by: Aryan Kumar <aryan5v@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-06-09 15:01:22 -07:00
d922ab2cbc [model] Flux2 Klein Port (#1349)
Co-authored-by: Gnav3852 <63612880+Gnav3852@users.noreply.github.com>
Co-authored-by: Mac Lee <macthecadillac@gmail.com>
2026-06-09 14:55:55 -07:00
Kaiqin Kongandmergify[bot] 9ea77d37f3 [bugfix] EMA shadow on resume and EMA under MoE path (#1441)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-06-09 00:55:43 +00:00
2e35b0c6bd [refactor]: linear/mlp FP4 path additions for Wan-2.1 (Attn-QAT 6/12) (#1390)
Co-authored-by: Peiyuan Zhang <a1286225768@gmail.com>
Co-authored-by: Matthew Noto <notomatthew31@gmail.com>
2026-06-08 14:51:57 -07:00
William Lin 1c627a3f98 [bugfix]: build fastvideo-kernel on GPU-less Docker runners (#1437) 2026-06-06 17:52:45 -07:00
Kaiqin Kong a931efe33a [bugfix] EMA in distillation pipeline (#1440) 2026-06-06 17:38:03 -07:00
William Lin 041e5e9029 [bugfix]: unblock PyPI publish (flash-attn-cute direct dep) (#1436) 2026-06-05 10:07:12 -07:00
Kaiqin Kongandmergify[bot] efcc245c2e [feat] VLM as judge for WM (#1429)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-06-05 04:25:54 +00:00
alexzmsandmergify[bot] 922e7e0813 [docs] LTX-2.3 distilled i2v example with compile + timing breakdown (#1430)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-06-05 01:26:50 +00:00
William Lin c62a8514b0 [chore]: release v0.2.0 (#1432) 2026-06-04 14:22:49 -07:00
William Lin 3eb8081801 [chore]: unpin runtime deps in pyproject.toml (#1431) 2026-06-04 13:46:49 -07:00
Raghav K 3505d09564 [bugfix] tests: include ltx2_3_base in expected LTX2 preset set (#1427) (#1428) 2026-06-04 12:17:18 -07:00
Shao DuanandSolitaryThinker 570607945c [feat] dreamverse: sequence parallelism for serving (#1424)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-06-01 23:13:49 -07:00
Kaiqin KongandSolitaryThinker d3a821cdcf [feat] LoRA controls and integration for Dreamverse (#1420)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-06-01 18:54:11 -07:00
Kaiqin Kong 3f24578139 [bugfix] LTX2: honor video_position_offset_sec in the DiT (#1422) 2026-06-01 16:59:15 -07:00
Kevin Lin 89fcf08378 [bugfix] Fix STFT dtype mismatch (#1419) 2026-05-31 20:56:07 -07:00
c2930b2aa1 [ci] Add additional Dreamverse UI tests (#1417)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-05-31 19:53:29 -07:00
KUAN-HAO HUANGandSolitaryThinker 019239690b [perf] Add Adaptive Guidance (CFG gating) for stale-uncond reuse (#1372)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-05-30 13:58:55 -07:00
alexzms d6119c1f82 [bugfix]: dreamverse modal bypasses ENTRYPOINT — set ffmpeg env + key check (#1413) 2026-05-29 16:50:01 -07:00
alexzms 84214c80bb [feat] LTX-2.3 audio: BWE vocoder path (#1398) 2026-05-29 16:43:26 -07:00
alexzmsandmergify[bot] c2d7143c72 [feat] LTX-2.3 transformer support (config-gated extension of LTX-2) (#1397)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-05-29 16:42:37 -07:00
Kaiqin KongandSolitaryThinker afdb6fbfa5 [feat] Add MatrixGame3.0 (#1201)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-05-27 21:07:01 +00:00
alexzms ba4c02d883 [docs]: highlight Dreamverse deployment paths + add Server B200 (SSH) guide (#1409) 2026-05-27 09:30:58 -07:00
Junda Su 2c137931f3 [bugfix] Fix Dreamverse Modal compile warmup latency (#1394) 2026-05-26 17:58:38 -07:00
Shao Duan 36682797a0 [feat] eval: input ergonomics + Evaluator features + bug fixes (#1392) 2026-05-26 13:59:45 -07:00
William Lin 0ef1357a77 [docs]: surface activation-trace utility in add-model skills (#1399) 2026-05-26 13:45:12 -07:00
William Lin a75d19786a [docs]: Wire activation trace into mkdocs nav + perf/troubleshooting (#1304) 2026-05-26 13:14:36 -07:00
alexzms be548a78ea [feat] VSA-256 fastpath on Blackwell via FA4 CuTe block-sparse attention (#1354) 2026-05-26 12:58:29 -07:00
alexzms 6a610e2bc9 [ci] add per-method single-step training tests for fastvideo.train (#1343) 2026-05-25 17:09:54 -07:00
Shao Duanandabaghyangor 321d5112b4 [refactor] eval: consolidate FVD into common.fvd, remove benchmarks/fvd (#1380)
Co-authored-by: abaghyangor <abaghyangor@gmail.com>
2026-05-24 12:09:47 -07:00
William Lin ba75ad82db [refactor]: shared attention infra additions for QAT-compat (Attn-QAT 5/12) (#1383) 2026-05-23 16:19:24 -07:00
Junda Su 58caa5109f [ci] Add DreamVerse app CI tests (#1386) 2026-05-23 14:28:26 -07:00
2f3ca8aaad [perf]: register FA2/FA3 default flash_attn_func as a torch.library custom op (#1373)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-05-23 00:39:06 -07:00
Junda Su 3a67319cb6 [infra] Use npm for Dreamverse web builds (#1385) 2026-05-22 18:50:10 -07:00
Junda Su 266fa044b3 [infra] Add Dreamverse Modal UI image build (#1381) 2026-05-22 11:36:53 -07:00
William Lin f3398db868 chore: pin dreamverse npm deps to address Dependabot alerts (#1359) 2026-05-22 11:29:33 -07:00
fda02036bc [feat]: Attn-QAT inference + training backends (deadcode) (Attn-QAT 4/12) (#1358)
Co-authored-by: jzhang38 <42993249+jzhang38@users.noreply.github.com>
Co-authored-by: RandNMR73 <99706358+RandNMR73@users.noreply.github.com>
2026-05-22 02:59:25 -07:00
Raghav K 68179cd752 [docs] Document enable_torch_compile (+ A/B example) (#1366) 2026-05-22 00:25:42 -07:00
Satyam SrivastavaandSolitaryThinker 2dd5760291 [docs] Document performance benchmark workflow (#1376)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-05-21 20:25:58 -07:00
Wenxuan TanandSolitaryThinker af2ee9c78a [feat] Optimize distributed weight loading in multi-node training (#572)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-05-21 16:59:48 +00:00
Satyam SrivastavaandSatyam Srivastava 1c80371b27 [ci] Component time performance + reseed hf baseline skill (#1292)
Co-authored-by: Satyam Srivastava <satyam53@Satyams-MacBook-Air.local>
2026-05-20 13:51:11 -07:00
Junda Su eef473225d [ci] Add Dreamverse Docker image workflow (#1369) 2026-05-20 13:50:02 -07:00
Junda Su 44fb84ef6a [bugfix]: shrink Dreamverse Docker context (#1368) 2026-05-20 13:38:52 -07:00
e8597b7448 [Bugfix] FP4 FA4 installation fix (#1367)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-05-19 22:45:24 -07:00
Raghav Kandmergify[bot] e2252c0a5e [perf] Mark LayerwiseOffloadHook entry points torch.compiler.disable (remove per-layer graph break) (#1365)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-05-20 03:09:23 +00:00
Junda Su 72cb427cd9 [feat]: add FastLTX-2.3 Gradio demo package (draft) (#1247) 2026-05-17 23:03:16 -07:00
Mingjia Huo 63030cf6ec [fix] Fix causal self-forcing attention settings (#1355) 2026-05-17 22:59:47 -07:00
Kaiqin Kongandmergify[bot] 773d44b875 [misc] Rename MatrixGame to MatrixGame2 (#1357)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-05-17 22:57:03 -07:00
1df513922f [feat] Add minimal LoRA finetuning support to the YAML training stack (#1242)
Co-authored-by: alexzms <3036648523@qq.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-05-17 14:49:13 -07:00
William LinandDavids048 30c45620a2 [infra] [dreamverse]: add instruction to install nasm and update ffmpeg installer to work in plain venv (#1361)
Co-authored-by: Davids048 <jundasu@ucsd.edu>
2026-05-17 01:10:55 +00:00
Shao Duanandklhhhhh 6b2c731596 [feat] eval: add audio metrics (#1352)
Co-authored-by: klhhhhh <1412841649@qq.com>
2026-05-16 14:47:37 -07:00
William Lin e6022c20b2 [misc]: demote ROCm-unavailable startup message to DEBUG (#1360) 2026-05-15 22:12:16 -07:00
460f6e398e [feat]: Add NVFP4QAT linear layer (Attn-QAT 3/12) (#1350)
Co-authored-by: jzhang38 <42993249+jzhang38@users.noreply.github.com>
Co-authored-by: RandNMR73 <99706358+RandNMR73@users.noreply.github.com>
2026-05-15 18:42:02 -07:00
d2ffec5cce [perf] Dreamverse 14/14: Add LTX2 profile speedups (#1337)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com>
Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com>
Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com>
2026-05-15 18:10:47 -07:00
alexzmsandmergify[bot] cb12e88713 [perf] shallow-copy VSA attn_metadata in train model plugins (#1342)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2026-05-15 16:10:12 -07:00
0958c344b8 [feat] Dreamverse 13/14: Activate LTX2 integration (#1336)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com>
Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com>
Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com>
2026-05-15 15:35:05 -07:00
Junda Su 71dc27ea7b [misc]: Add Dreamverse deploy skill frontmatter (#1353) 2026-05-15 15:23:17 -07:00
Junda SuandSolitaryThinker 1263449d2a [docs] Add copy page action (#1351)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-05-15 14:59:04 -07:00
b4de5a9f1b [feat] Dreamverse 12/14: Add LTX2 refine and upsampler support (#1335)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com>
Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com>
Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com>
2026-05-15 12:21:40 -07:00
8acd8e21f9 [feat]: Add NVFP4QAT quantization config (Attn-QAT 2/12) (#1348)
Co-authored-by: jzhang38 <42993249+jzhang38@users.noreply.github.com>
Co-authored-by: RandNMR73 <99706358+RandNMR73@users.noreply.github.com>
2026-05-14 16:53:48 -07:00
d45d82334f [infra] Dreamverse 11/14: Add NVFP4 quantization support (#1334)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com>
Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com>
Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com>
2026-05-14 16:26:39 -07:00
6392bd40a9 [feat] Dreamverse 10/14: Add serving API contracts (#1333)
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
Co-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com>
Co-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com>
Co-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com>
2026-05-14 10:07:01 -07:00
Shao Duan 17f07bc313 [feat] eval: async VideoPool + metric streamlines (#1320) 2026-05-13 15:57:37 -07:00
William Lin 325861fb99 [misc]: PR-1225 sync — housekeeping (1/12) (#1347) 2026-05-13 15:21:52 -07:00
William Lin c5088670c8 [misc]: empty __init__.py files with no logic (#1346) 2026-05-13 13:09:40 -07:00
796 changed files with 94824 additions and 21373 deletions
@@ -0,0 +1,41 @@
---
date: 2026-05-22
experiment: PR #1386 DreamVerse app CI backend tests
category: infrastructure
severity: important
---
# DreamVerse App CI Streaming Imports Need GPU
## What Happened
DreamVerse app CI backend pytest collection imports FastVideo streaming surfaces.
When those tests run in a CPU-only Modal environment, collection can fail before
any app assertions run with Triton reporting:
```text
RuntimeError: 0 active drivers
```
## Root Cause
Some streaming import paths can import `fastvideo_kernel` at module import time.
Triton then probes for an active GPU driver during pytest collection. A CPU-only
Modal container has no active driver, so the failure appears as an import-time
collection error rather than a DreamVerse app behavior failure.
## Fix / Workaround
For PR #1386, use a surgical CI fix: allocate a GPU to
`run_dreamverse_app_tests`. Do not refactor core streaming/kernel imports just to
unstick this app CI path.
Keep `build_kernel=False` for this job. The DreamVerse app backend test imports
streaming surfaces but does not need to rebuild or exercise custom kernels.
## Prevention
When adding or modifying DreamVerse app CI jobs that import FastVideo streaming
modules, make the GPU requirement explicit if the import graph may touch
`fastvideo_kernel`. Prefer small CI resource fixes for app test collection issues
unless the product code genuinely requires lazy import cleanup.
+3 -3
View File
@@ -31,7 +31,7 @@ FastVideo-WorldModel/
│ │ │ └── distribution_matching/ # DMD2Method, SelfForcingMethod
│ │ ├── models/ # Per-role model wrappers (ModelBase, CausalModelBase)
│ │ │ ├── wan/ # WanModel, WanCausalModel
│ │ │ └── matrixgame/ # MatrixGameModel, MatrixGameCausalModel
│ │ │ └── matrixgame2/ # MatrixGame2Model, MatrixGame2CausalModel
│ │ ├── callbacks/ # Composable hooks (grad_clip, ema, validation)
│ │ └── utils/ # Config, builder, checkpoint, optimizer, tracking
│ ├── training/ # Legacy training infrastructure (being phased out)
@@ -44,7 +44,7 @@ FastVideo-WorldModel/
│ │ ├── wan_distillation_pipeline.py # Wan distillation
│ │ ├── self_forcing_distillation_pipeline.py # Self-forcing distill
│ │ ├── ltx2_training_pipeline.py # LTX-2 training
│ │ └── matrixgame_training_pipeline.py # MatrixGame training
│ │ └── matrixgame2_training_pipeline.py # Matrix-Game 2.0 training
│ ├── attention/ # Attention backends
│ ├── distributed/ # Sequence/tensor parallel utilities
│ ├── layers/ # Tensor-parallel layers
@@ -95,7 +95,7 @@ FastVideo-WorldModel/
| Wan distillation (DMD) | `fastvideo/training/wan_distillation_pipeline.py` | `torchrun --nproc_per_node N` |
| Self-forcing distill | `fastvideo/training/wan_self_forcing_distillation_pipeline.py` | `torchrun --nproc_per_node N` |
| LTX-2 finetune | `fastvideo/training/ltx2_training_pipeline.py` | `torchrun --nproc_per_node N` |
| MatrixGame | `fastvideo/training/matrixgame_training_pipeline.py` | `torchrun --nproc_per_node N` |
| Matrix-Game 2.0 | `fastvideo/training/matrixgame2_training_pipeline.py` | `torchrun --nproc_per_node N` |
## W&B Integration
@@ -8,6 +8,8 @@ follow-up actions see [open-threads.md](open-threads.md).
**Last updated:** 2026-05-06 (added D-21 — chunk-stutter root cause is software libx264 encoding consuming ~22% of segment wall-time, NOT a migration regression; verified by 3 parallel explore agents that NVFP4 + torch.compile coverage matches FastVideo-internal exactly; landed opt-in NVENC build path in install_native_ffmpeg.sh + `--nvenc`/`--no-nvenc` flag in dreamverse-deploy.sh + `apps/dreamverse/server/benchmarks/benchmark_av_streaming.py` regression test + memory dir update; default codec stays `libx264` for backward compat, opt-in via `--nvenc`. Earlier: added D-12 — GpuPool layer separation, Oracle review post-#1257-merge; added D-13 — prompt enhancer / LLMProvider abstraction shape, Oracle review pre-#1258-merge; added D-14 — streaming auxiliaries cohesion, Oracle review during #1284 review cycle; added D-15 — streaming router placement + sticky/active-active deferral, Oracle review during #1286 review cycle; added D-16 — streaming router polish round 2, second-pass review on top of D-15 covering bridge cancellation hygiene, registry state machine, httpx hard-fail, replica YAML parsing, and `websockets` dep; added D-17 — strategy reversal: abandon 6-PR split in favor of single mega-PR #1288 on `will/ltx2_sr_port`; added D-18 — Option B+ chosen: Dreamverse FE+product-server move into FastVideo as `apps/dreamverse/` subfolder while generic backend stays at `fastvideo.entrypoints.streaming.*`; integration-review.md deprecated, integration-plan.md is the executable migration plan; added D-19 — D-18 executed: 5 commits land on `will/dreamverse-monorepo`, fix-up commits corrected the integration-plan's invalid "delete generic-merged, import public substitutes" assumption — generic-merged files carried product-local instead, e2e passes against migrated code with /proc-verified evidence; added D-20 — segment-2 BrokenPipe root cause was a TWO-direction silent drop of LTX-2 audio kwargs in public `VideoGenerator`).
**Update 2026-05:** Dreamverse frontend tooling migrated from standalone pnpm to standalone npm. `apps/dreamverse/web/package-lock.json` is authoritative; see PR #1385.
## Status legend
- ✅ **Resolved** — decision made and implementation complete (or no implementation needed)
@@ -297,14 +299,14 @@ Playwright (8/8 PASS in 5.1s):
- **Python ML library** stays at root: `fastvideo/`, `fastvideo-kernel/`.
- **Generic backend** stays at `fastvideo.entrypoints.streaming.*` (already there per #1257/#1258/#1284/#1286/#1288).
- **Dreamverse product** moves into `apps/dreamverse/{server,web,prompts,serve_configs,scripts}/`.
- **Tooling**: uv workspace for Python (`[tool.uv.workspace] members = ["apps/dreamverse/server"]`), standalone pnpm for the FE (no root `package.json`), split CI workflows with path-filter triggers.
- **Tooling**: uv workspace for Python (`[tool.uv.workspace] members = ["apps/dreamverse/server"]`), standalone npm for the FE (no root `package.json`), split CI workflows with path-filter triggers.
**Rationale:**
- Drops the cross-repo coordination overhead identified in the post-#1286 rebase cycle (D-17 handled by consolidating into mega-PR; D-18 prevents the next round of cross-repo coordination from happening).
- Keeps the architectural separation Option D recommended (FastVideo owns reusable runtime; product owns product). The boundary is now `apps/dreamverse/` directory rather than two repos.
- Single repo means atomic cross-cutting refactors (e.g. GpuPool API change + Dreamverse adoption) ship as one PR.
- OSS precedents support the shape (chainlit uv-workspace + pnpm; open-webui Python + Svelte with paths-ignore CI). The librarian explicitly noted no precedent for "Python ML library + Next.js product merged into library namespace" — but this isn't that pattern. Dreamverse goes into a sibling directory, NOT into `fastvideo.entrypoints.dreamverse.*`. Library namespace stays clean.
- OSS precedents support the shape (chainlit uv-workspace + frontend package manager; open-webui Python + Svelte with paths-ignore CI). The librarian explicitly noted no precedent for "Python ML library + Next.js product merged into library namespace" — but this isn't that pattern. Dreamverse goes into a sibling directory, NOT into `fastvideo.entrypoints.dreamverse.*`. Library namespace stays clean.
**Why not Option D (separate repos):**
@@ -311,7 +311,7 @@ Notes:
development and CI.
- Product server release is Docker/deploy workflow, not PyPI.
### Frontend build: standalone pnpm
### Frontend build: standalone npm
Do not add a root `package.json`.
@@ -319,14 +319,14 @@ Keep all frontend tooling under:
```text
apps/dreamverse/web/package.json
apps/dreamverse/web/pnpm-lock.yaml
apps/dreamverse/web/package-lock.json
apps/dreamverse/web/playwright.config.*
```
Rationale:
- FastVideo remains primarily a Python ML library.
- Python contributors should not need Node or pnpm for normal work.
- Python contributors should not need Node or npm for normal work.
- This intentionally diverges from chainlit's root JS workspace pattern and
follows the simpler open-webui-style split.
@@ -455,31 +455,24 @@ jobs:
steps:
- uses: actions/checkout@v4
# IMPORTANT: pnpm/action-setup MUST run BEFORE setup-node when using
# cache: pnpm — setup-node otherwise can't find pnpm to populate cache.
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22'
cache: pnpm
cache-dependency-path: apps/dreamverse/web/pnpm-lock.yaml
cache: npm
cache-dependency-path: apps/dreamverse/web/package-lock.json
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
- name: Typecheck
run: pnpm run typecheck --if-present
run: npm run typecheck --if-present
- name: Unit tests
run: pnpm run test --if-present
run: npm run test --if-present
- name: Build
run: pnpm run build
run: npm run build
# NOTE: Playwright tests require a running backend. Until Phase 4 lands
# `/healthz`/`/readyz`/`/status`/`/prompt-system-config`/`/curated-presets`
@@ -489,13 +482,13 @@ jobs:
# `fastvideo.entrypoints.streaming.build_app` and add product routes).
#
# - name: Install Playwright browsers
# run: pnpm exec playwright install --with-deps chromium
# run: npm exec -- playwright install --with-deps chromium
#
# - name: Playwright (re-enable in Phase 4)
# run: pnpm exec playwright test
# run: npm exec -- playwright test
```
**Playwright config update needed** when moving FE: `Dreamverse/apps/web/playwright.config.ts` line 39 currently uses `npm run dev`; change to `pnpm run dev` post-move ([source](file:///home/william5lin/Dreamverse/apps/web/playwright.config.ts#L39)).
**Playwright config update needed** when moving FE: keep `Dreamverse/apps/web/playwright.config.ts` line 39 on `npm run dev` after the move ([source](file:///home/william5lin/Dreamverse/apps/web/playwright.config.ts#L39)).
#### `.github/workflows/ci-dreamverse-backend.yml`
@@ -604,7 +597,7 @@ monorepo CI path.
| uv workspaces | https://docs.astral.sh/uv/concepts/workspaces/ | Authoritative Python workspace model. |
| Hatch monorepo | https://hatch.pypa.io/latest/how-to/environment/workspace/ | Alternative workspace model; not selected. |
| chainlit | https://github.com/Chainlit/chainlit | uv workspace precedent plus **per-language CI split** (separate `check-frontend.yaml` / `check-backend.yaml` workflows path-filtered by directory). Not a PR-level split — independent of D-17 single-mega-PR decision. |
| open-webui | https://github.com/open-webui/open-webui | Frontend path filtering (`paths-ignore` on backend-only changes) and separate release tracks. **Note:** open-webui has a root `package.json`; we are choosing standalone-pnpm despite the precedent, to avoid forcing Python-only contributors to install Node. |
| open-webui | https://github.com/open-webui/open-webui | Frontend path filtering (`paths-ignore` on backend-only changes) and separate release tracks. **Note:** open-webui has a root `package.json`; we are choosing standalone npm under `apps/dreamverse/web/` despite the precedent, to avoid forcing Python-only contributors to install Node. |
| streamlit | https://github.com/streamlit/streamlit | Split Python and JS testing in one repo. |
| gradio | https://github.com/gradio-app/gradio | Python package plus JS workspace precedent. |
| full-stack-fastapi-template-nextjs | https://github.com/nemanjam/full-stack-fastapi-template-nextjs | Separate frontend/backend build and deploy workflows. |
@@ -623,8 +616,8 @@ class runs where, because not all tests can run on `ubuntu-latest` CI.
| **Unit** | (none / `unit`) | `ci-dreamverse-backend.yml` (ubuntu-latest CI) + locally | Pure logic, no GPU, no live service. Mocked FastVideo backends, schema validation, helper functions. | `test_config.py`, `test_rewrite_prompt_payload.py`, `test_session_init_image.py`, the new `test_import_contract.py` |
| **Integration (fakes)** | `integration` | `ci-dreamverse-backend.yml` + locally | FastAPI test client + in-process fakes/mocks for GPU pool. Validates routes, request/response shapes, session state machine. | `test_health_endpoints.py`, `test_mock_server.py`, `test_entrypoints.py`, `test_prompt_safety.py`, `test_batching.py` (deleted) |
| **Live-service GPU** | `gpu` (skip-by-default in CI) | **Local GPU4 manual QA** + Buildkite-Modal (when added) | Real `fastvideo serve` process + real model weights + real WebSocket round-trips. Validates LTX-2 streaming, NVFP4 wiring, continuation state, frame emission. | `test_realtime_stress.py` (947 LOC), `test_session_logging.py` (1278 LOC) — these spin up real workers per their current shape |
| **Frontend unit / build** | (n/a — pnpm) | `ci-dreamverse-frontend.yml` (ubuntu-latest, no GPU) | Vitest + tsc + Next.js build. No backend needed. | `apps/dreamverse/web/src/**/*.test.ts(x)` |
| **Frontend Playwright E2E** | (n/a — pnpm) | **Local GPU4 manual QA** until Phase 4 lands public health routes; then `ci-dreamverse-frontend.yml` against a mock backend OR a deployed staging | Real browser → real backend WebSocket flow. Requires `/healthz`, `/readyz`, `/status`, `/prompt-system-config`, `/curated-presets`, `/v1/stream`. | `apps/dreamverse/web/e2e/{backend-health,frontend-shell,preset-prompt-generation}.spec.ts` |
| **Frontend unit / build** | (n/a — npm) | `ci-dreamverse-frontend.yml` (ubuntu-latest, no GPU) | Vitest + tsc + Next.js build. No backend needed. | `apps/dreamverse/web/src/**/*.test.ts(x)` |
| **Frontend Playwright E2E** | (n/a — npm) | **Local GPU4 manual QA** until Phase 4 lands public health routes; then `ci-dreamverse-frontend.yml` against a mock backend OR a deployed staging | Real browser → real backend WebSocket flow. Requires `/healthz`, `/readyz`, `/status`, `/prompt-system-config`, `/curated-presets`, `/v1/stream`. | `apps/dreamverse/web/e2e/{backend-health,frontend-shell,preset-prompt-generation}.spec.ts` |
| **FastVideo public contract** | (none) | Existing FastVideo CI (`ci-precommit` + Buildkite for GPU) | Schema/shape guards that this migration must not break. | `fastvideo/tests/contract/test_dreamverse_shape.py`, `test_dynamo_shape.py`, `test_generate_async.py` |
| **FastVideo SSIM regression** | (Buildkite path-filter) | Buildkite-Modal | Inference-quality gates for ported models. | `fastvideo/tests/ssim/test_*.py` |
@@ -709,7 +702,7 @@ CUDA_VISIBLE_DEVICES=4 uv run --locked --package dreamverse-server \
# Smoke-test from another terminal
curl -s http://localhost:8009/health | jq .
curl -s http://localhost:8009/readyz | jq . # Phase 4+ only
# Drive the FE against it: cd apps/dreamverse/web && pnpm run dev
# Drive the FE against it: cd apps/dreamverse/web && npm run dev
```
This is the **manual QA gate** for any phase that touches the live-service
@@ -907,7 +900,7 @@ mass move. This should be a small PR.
6. Add `apps/dreamverse/web/.*` to pre-commit global exclude.
7. Add `docs/contributing/dreamverse-development.md` with local dev commands:
backend `uv run --locked --package dreamverse-server --extra test pytest ...`;
frontend `cd apps/dreamverse/web && pnpm install && pnpm run build`.
frontend `cd apps/dreamverse/web && npm ci && npm run build`.
8. Run `uv lock` to regenerate `uv.lock` with the new workspace member; commit
the lock change in the same PR.
9. Land D-12-A docstring caveat: mark `GpuPool` experimental/server-internal
@@ -990,7 +983,7 @@ These are **explicit shims** — Phase 4 is responsible for their promotion to p
- [`config.py:13`](file:///home/william5lin/Dreamverse/server/config.py#L13): `_APP_ROOT / "apps" / "web"` → `_APP_ROOT / "web"` (since `_APP_ROOT` will resolve to `apps/dreamverse/` in the new layout).
- [`apps/web/next.config.ts:11`](file:///home/william5lin/Dreamverse/apps/web/next.config.ts#L11): `outputFileTracingRoot: path.resolve(__dirname, "../..")` → `path.resolve(__dirname, "../../..")` (one extra `..` since the FE is one level deeper in the monorepo).
- [`playwright.config.ts:39`](file:///home/william5lin/Dreamverse/apps/web/playwright.config.ts#L39): `command: "npm run dev"` → `command: "pnpm run dev"` (matches Phase 1 tooling decision).
- [`playwright.config.ts:39`](file:///home/william5lin/Dreamverse/apps/web/playwright.config.ts#L39): keep `command: "npm run dev"` (matches Phase 1 tooling decision).
- Any hardcoded `../FastVideo` paths in scripts/configs → make repo-root-relative since they now share a repo.
**Steps:**
@@ -1084,9 +1077,9 @@ scripts, and product docs after backend tests are green.
**Verification gate:**
- `cd apps/dreamverse/web && pnpm install --frozen-lockfile` succeeds.
- `cd apps/dreamverse/web && pnpm run build` succeeds.
- `cd apps/dreamverse/web && pnpm run test --if-present` succeeds (Vitest + tsc).
- `cd apps/dreamverse/web && npm ci` succeeds.
- `cd apps/dreamverse/web && npm run build` succeeds.
- `cd apps/dreamverse/web && npm run test --if-present` succeeds (Vitest + tsc).
- **Frontend CI Playwright is intentionally DEFERRED to Phase 4** — at Phase 3
the public `build_app` does not yet expose `/healthz`+`/readyz`+`/status`+
`/prompt-system-config`+`/curated-presets`. The `ci-dreamverse-frontend.yml`
@@ -1094,7 +1087,7 @@ scripts, and product docs after backend tests are green.
reactivates them. No PR note required.
- **Manual GPU4 Playwright smoke** (recommended): on this dev node, run
`apps/dreamverse/web/e2e/frontend-shell.spec.ts` against the GPU4-deployed
backend from Phase 2 manual QA + a `pnpm run dev` frontend at port 5274
backend from Phase 2 manual QA + a `npm run dev` frontend at port 5274
to confirm shell hydration. `backend-health.spec.ts` and
`preset-prompt-generation.spec.ts` will fail until Phase 4 — that is
expected; document as deferred.
@@ -1249,7 +1242,7 @@ product-specific prompt orchestration.
| CI cost increase | Medium | Medium | Add Dreamverse path-specific workflows; add `paths-ignore` to broad workflows; rely on Buildkite monorepo diff watch lists. | CI owner |
| FastVideo PyPI release accidentally includes Dreamverse app | High | Low | Add `apps*` to `[tool.setuptools.packages.find]` and `[tool.wheel]` excludes; verify built wheel contents. | Release owner |
| Release cadence coupling | Medium | Medium | Keep FastVideo PyPI version release unchanged; Dreamverse uses Docker/Vercel deploy from app paths. | Release owner |
| Frontend tooling drift | Medium | Medium | Pin pnpm lockfile in `apps/dreamverse/web/`; no root JS workspace. | Frontend owner |
| Frontend tooling drift | Medium | Medium | Pin npm lockfile in `apps/dreamverse/web/`; no root JS workspace. | Frontend owner |
| Security surface enlargement | Medium | Medium | Product routes stay in `apps/dreamverse/server`; only generic health/streaming routes go into FastVideo. | Backend owner |
| Product-specific API leakage into `fastvideo.*` | High | Medium | Enforce import/module boundary; keep curated presets and prompt UX product-local. | Architecture owner |
| Migration regression | High | Medium | Phase gates; backend before frontend; can stop after any phase with Dreamverse repo still usable until Phase 7. | Migration owner |
@@ -6,7 +6,10 @@ recommended next action.
For why each item is open see [decisions-log.md](decisions-log.md). For
PR-level context see [pr-roadmap.md](pr-roadmap.md).
**Last updated:** 2026-05-12 (added DR-4 follow-up for PR #1330's skipped
**Last updated:** 2026-05-14 (added DR-7 follow-up to remove LTX2 debug
logging env vars and replace them with per-pipeline/model state. Earlier: 2026-05-14 added DR-6 follow-up for PR #1335's Gemma
lazy-load/device-placement behavior outside compiled `forward`. Earlier: 2026-05-13 added DR-5 follow-up for PR #1333's LTX2
distilled SSIM reference refresh. Earlier: 2026-05-12 added DR-4 follow-up for PR #1330's skipped
`App websocket integration` suite. Earlier: 2026-05-05 D-20 broken-pipe root cause + fix landed on
`will/dreamverse-monorepo` @ `5eaf0a13`; added new thread D-20-CP for
cherry-picking the public-API audio routing fix to `will/ltx2_sr_port`
@@ -34,6 +37,9 @@ different vehicle.).
| **DR-2** | Med | Decide `cerebras_ifm` provider path: (a) public Literal + `CerebrasIFMProvider` shipped, OR (b) Dreamverse-side custom provider via `enhancer.register_provider(...)` | S (decision) + S-M (impl) | Resolves the cerebras_ifm gap left by PR #1258. Same item as legacy #3 below; DR-2 is the Dreamverse-side framing. |
| **DR-3** | Low | Replace Dreamverse `PromptEnhancer._run_blocking_request` manual thread/queue polling with `asyncio.to_thread` after the PR #1327 prompt-enhancer compatibility surface is retired or isolated | S | Review comment #1327 (`prompt_enhancer.py`) is valid, but deferred to avoid patching the local fork in this PR. |
| **DR-4** | Low | Investigate unskipping PR #1330's skipped public `App websocket integration` suite | S-M | Public PR #1330 has `describe.skip(...)` around 27 websocket tests while the internal equivalent suite is active with 25 tests. The 2 public-only tests cover backend unreachable / GPU workers not ready. Not blocking while skipped, but stale assertions may need safe refresh before unskip. |
| **DR-5** | Low | Regenerate LTX2-Distilled latent SSIM references under the intended neutral/distilled defaults, then remove the PR #1333 historical full-guidance pins | S-M | PR #1333 changed public LTX2 distilled defaults to neutral/distilled values, but existing LTX2 latent SSIM references appear to have been generated with historical full-guidance defaults. The current PR pins the SSIM test to old values to keep CI compatible until references are refreshed. |
| **DR-6** | Low | Decide whether `LTX2GemmaTextEncoderModel` needs a non-forward device-placement hook after lazy Gemma load | S | PR #1335 should remove the `model.device` / `model.to(...)` guard from `forward` for Dynamo/fullgraph compatibility. Non-compiled runs probably do not need it because `gemma_model` moves Gemma at first load, but a later wrapper `.to(...)` after lazy load could leave Gemma on the old device unless lifecycle placement handles it. |
| **DR-7** | Low | Remove LTX2 debug logging env-var plumbing and replace it with per-pipeline/model debug state | S-M | PR #1335 review flagged that `initialize_pipeline()` mutates process-global LTX2 debug env vars, which can leak/race across pipeline instances. Deleting only the mutation is low-risk but loses config-driven debug logging; deleting all reads without replacement would remove useful SSIM/latent drift diagnostics. |
| **3** | Med | Add `cerebras_ifm` to `PromptEnhancerConfig.provider` Literal + provider | S-M | Public-side resolution if DR-2 picks (a) |
| **4** | Med | Expose `layer_profile` on typed `engine.quantization` | M | Removes Dreamverse's `experimental["pipeline_config"]` dodge for stage profiles |
| **5** | Med | Design typed `dit_config.quant_config` carrier | L design + L impl | Removes broader `experimental["pipeline_config"]` escape hatch |
@@ -421,6 +427,65 @@ current PR #1330 review.
stack end-to-end and compare the public assertions against the internal active
suite.
### Item DR-6: Gemma lazy-load device placement outside `forward`
**Why:** PR #1335 review flagged this pattern in
`fastvideo/models/encoders/gemma.py::LTX2GemmaTextEncoderModel.forward`:
```py
if model.device != target_device:
model.to(device=target_device)
```
The immediate concern is the compiled/Dynamo path: `model.device` and
`model.to(...)` inside `forward` can introduce non-tensor/device parsing work
that fullgraph tracing should not see. Removing the guard from `forward` is the
right PR #1335 review fix.
For non-compiled execution, the guard is mostly defensive rather than required:
`gemma_model` already moves the lazily loaded HF Gemma model to the wrapper's
current parameter device on first load. The remaining edge case is a lifecycle
sequence where Gemma is loaded, then the parent wrapper is later moved to a
different device; in that case Gemma could stay behind unless placement is
handled outside `forward`.
**Action:** After PR #1335 review is unblocked, decide whether FastVideo needs a
small lifecycle hook/helper for this class so lazy Gemma is moved whenever the
wrapper/device placement changes. If yes, implement it outside `forward`; if no,
document that Gemma must be loaded after final device placement.
**Effort:** Small.
**Dependencies:** Not blocking PR #1335 if the forward-path guard is removed and
the normal load path keeps placing Gemma on the wrapper's current device.
### Item DR-7: Remove LTX2 debug logging env-var plumbing
**Why:** PR #1335 review flagged that
`fastvideo/pipelines/basic/ltx2/ltx2_pipeline.py::initialize_pipeline()` sets
and pops LTX2 debug env vars such as `LTX2_PIPELINE_DEBUG_LOG`,
`LTX2_PIPELINE_DEBUG_PATH`, `LTX2_DEBUG_DETAIL`, and
`LTX2_PIPELINE_DEBUG_DETAIL_PATH`. Those env vars are process-global, so one
pipeline instance can enable, overwrite, or clear debug behavior for another
pipeline instance running in the same process.
Deleting only the `initialize_pipeline()` env mutation is low risk for normal
generation, but it would stop config-driven debug logging unless replaced.
Deleting all env-var reads without replacement is riskier because these logs are
useful for SSIM/latent drift diagnosis and may be used by local debug scripts.
**Action:** Replace LTX2 debug env-var plumbing with per-pipeline/model debug
state. Use pipeline/model config for construction-time hooks and a
`ForwardContext`/`ForwardBatch`-style carrier for forward-time logging. Keep
external env-var compatibility only if there is a documented operator workflow
that still needs it.
**Effort:** Small-Medium.
**Dependencies:** Not blocking PR #1335 if the immediate fix is limited to
removing process-global mutation from pipeline initialization while preserving
existing externally supplied env-var reads.
### Item D-12-C: Avoid locking `PoolAssignment.gpu_id: int` as public
**Why:** Today `PoolAssignment` exposes `gpu_id: int`, assuming
+29 -20
View File
@@ -12,7 +12,7 @@ _Last updated: 2026-03-02_
| Metric | Category | Status | Location | Trust |
|--------|----------|--------|----------|-------|
| **FVD** | Distribution | ✅ Implemented | `benchmarks/fvd/` | High |
| **FVD** | Distribution | ✅ Implemented | `fastvideo/eval/metrics/common/fvd/` | High |
| **SSIM** | Reference | ✅ Implemented | `fastvideo/tests/ssim/` | High |
| **LPIPS** | Perceptual | ✅ Implemented | `scripts/lora_extraction/` | Medium |
| **Loss trajectory** | Training signal | ✅ Implemented | W&B `train_loss` | Medium |
@@ -27,8 +27,8 @@ _Last updated: 2026-03-02_
### FVD — Fréchet Video Distance
**Category**: Distribution-level quality metric
**Status**: ✅ Fully implemented in `benchmarks/fvd/`
**Trust**: High — standard protocol, I3D feature extractor
**Status**: ✅ Registered as the `common.fvd` eval metric in `fastvideo/eval/metrics/common/fvd/`
**Trust**: High — standard protocol, I3D feature extractor (CLIP / VideoMAE backbones also available, research-grade)
#### What It Measures
FVD measures the distance between the **distribution** of generated videos and
@@ -59,30 +59,39 @@ Lower FVD = generated videos are more statistically similar to real videos.
#### How to Use
```python
# Programmatic
from benchmarks.fvd import compute_fvd_with_config, FVDConfig
# Programmatic — drive the metric directly for custom kwargs
from fastvideo.eval import get_metric
config = FVDConfig.fvd2048_16f() # Standard: 2048 videos, 16 frames
results = compute_fvd_with_config('data/real/', 'outputs/gen/', config)
print(f"FVD: {results['fvd']:.2f}")
metric = get_metric("common.fvd", extractor="i3d") # or "clip" / "videomae"
metric.to("cuda")
metric.setup()
metric.reset()
# First sample carries the reference set; later samples reuse the cache.
metric.accumulate({"video": gen_tensors[0], "reference": real_tensors})
for gen in gen_tensors[1:]:
metric.accumulate({"video": gen})
result = metric.finalize()
print(f"FVD: {result.score:.2f}")
```
```bash
# CLI
python -m benchmarks.fvd.cli \
--real-path data/real/ \
--gen-path outputs/gen/ \
--protocol fvd2048_16f
# CLI — folder of generated mp4s vs a reference folder
python examples/inference/eval/eval_fvd.py \
--gen-dir outputs/gen/ \
--reference-dir data/real/ \
--extractor i3d \
--output fvd_scores.json
```
**Preset protocols**:
| Protocol | Videos | Frames | Use Case |
|----------|--------|--------|----------|
| `fvd2048_16f` | 2048 | 16 | Standard benchmark (papers) |
| `fvd2048_128f` | 2048 | 128 | Long video evaluation |
| `quick_test` | 100 | 16 | Fast dev iteration |
**Feature extractors**: `i3d` (default, standard FVD spec used in papers),
`clip`, `videomae` (research-grade; not directly comparable to published
FVD numbers).
**Feature extractors**: `i3d` (default, standard), `clip`, `videomae`
**Protocol**: standard FVD uses 2048 generated + 2048 reference videos at
16 frames each. A warning fires below 256 — the score becomes
statistically unreliable.
#### Interpretation
| FVD Range | Interpretation |
+331
View File
@@ -0,0 +1,331 @@
# Performance Dashboard Memory
Date: 2026-06-16
Branch: `ci/dashboard`
## Purpose
This branch adds a local live dashboard for FastVideo performance benchmark
history. It is intended for maintainer/operator use: inspect latest benchmark
status, compare current values with recent baseline context, and view trends
from the Hugging Face performance-tracking dataset.
The dashboard is a FastAPI + React app. It is separate from the existing
Svelte `ui/` app.
## Main Files Added Or Changed
Backend:
- `fastvideo/performance_dashboard/__init__.py`
- `fastvideo/performance_dashboard/__main__.py`
- `fastvideo/performance_dashboard/api.py`
- `fastvideo/performance_dashboard/metrics.py`
- `fastvideo/performance_dashboard/service.py`
Frontend:
- `performance_dashboard/frontend/package.json`
- `performance_dashboard/frontend/package-lock.json`
- `performance_dashboard/frontend/tsconfig.json`
- `performance_dashboard/frontend/vite.config.ts`
- `performance_dashboard/frontend/index.html`
- `performance_dashboard/frontend/scripts/build.mjs`
- `performance_dashboard/frontend/src/main.tsx`
- `performance_dashboard/frontend/src/api.ts`
- `performance_dashboard/frontend/src/App.tsx`
- `performance_dashboard/frontend/src/styles.css`
Docs/tests:
- `performance_dashboard/README.md`
- `docs/contributing/performance_benchmarks.md`
- `fastvideo/tests/performance/test_dashboard_service.py`
- `fastvideo/tests/performance/test_dashboard_api.py`
Shared HF utility change:
- `fastvideo/tests/performance/hf_store.py`
## Data Source
The source of truth remains the Hugging Face dataset repo used by existing
performance CI:
```text
HF_REPO_ID=FastVideo/performance-tracking
```
The dataset stores normalized JSON records emitted by
`fastvideo/tests/performance/compare_baseline.py`. The current v1 normalized
schema includes:
- `model_id`
- `timestamp`
- `commit_sha`
- `gpu_type`
- `latency`
- `throughput`
- `memory`
- `text_encoder_time_s`
- `dit_time_s`
- `vae_decode_time_s`
- `success`
Records are grouped by `(model_id, gpu_type)` for v1 dashboard behavior.
## Local Cache
The backend syncs the HF dataset to a local cache directory:
```text
PERFORMANCE_TRACKING_ROOT=/tmp/fastvideo-perf-dashboard
```
If `PERFORMANCE_TRACKING_ROOT` is not set, the dashboard defaults to:
```text
/tmp/fastvideo-perf-dashboard
```
The sync is performed through the existing helper:
```python
fastvideo.tests.performance.hf_store.sync_from_hf(...)
```
The dashboard then loads JSON files from the local cache through:
```python
fastvideo.tests.performance.hf_store.load_records(...)
```
## Authentication
Originally `hf_store.py` only read `HF_API_KEY`. This caused local dashboard
runs to fail when users had standard Hugging Face token variables set.
`hf_store.py` now resolves tokens from the first available variable in:
```text
HF_API_KEY
HUGGINGFACE_HUB_TOKEN
HF_TOKEN
```
For local use:
```bash
export HF_TOKEN=hf_...
```
If the HF repo is private or gated, the token must have dataset read access.
## Backend API
The FastAPI app is created by:
```python
fastvideo.performance_dashboard.api:create_app
```
The module-level app is:
```python
fastvideo.performance_dashboard.api:app
```
Endpoints:
- `GET /api/performance/health`
- `POST /api/performance/refresh`
- `GET /api/performance/records?days=90`
- `GET /api/performance/summary?days=90`
- `GET /api/performance/trends?days=90`
`POST /api/performance/refresh` forces a fresh HF sync.
## Status Semantics
Important: the dashboard intentionally separates stored CI status from
recomputed context.
Stored status:
- Comes directly from the latest JSON record's `success` field.
- This is what the dashboard displays as `Stored Status`.
- This is the primary latest status.
Recomputed status:
- Calculated locally from cached records for explanatory context.
- Uses the latest record's metric values compared to the median of the latest
five previous successful records in the same `(model_id, gpu_type)` group.
- Displayed separately as `Recomputed`.
- Does not override the stored JSON `success` status.
This distinction was added after observing that recomputing pass/fail from the
local cache can disagree with the status originally uploaded by CI.
## Time Window Behavior
The default dashboard time window is 90 days.
The selected `days` value affects:
- trend charts
- record browsing/filtering
The selected `days` value does not affect:
- latest stored status
- latest summary baseline context
Reason: latest status should not change when users widen or narrow the trend
window. The API keeps `days` on `/summary` only for shared frontend filter
state, but summary loading uses all cached records.
This fixed a bug where changing from roughly 35 days to 42 days could change
the latest status from pass to fail because older records entered the local
baseline window.
## Metric Logic
Dashboard metric definitions live in:
```text
fastvideo/performance_dashboard/metrics.py
```
Tracked metrics:
- `latency` lower is better
- `throughput` higher is better
- `memory` lower is better
- `text_encoder_time_s` lower is better
- `dit_time_s` lower is better
- `vae_decode_time_s` lower is better
Baseline context uses the median of up to five previous successful records for
the same `(model_id, gpu_type)`.
## Frontend Behavior
The React app:
- fetches `/api/performance/summary`
- fetches `/api/performance/trends`
- displays summary cards
- displays latest rows by model/GPU
- displays native SVG trend charts
- has model/GPU/day filters
- includes a refresh button
- auto-refreshes every five minutes
The UI is implemented without a charting library. Trend charts are native SVG
in `performance_dashboard/frontend/src/App.tsx`.
The production frontend build uses `esbuild` through
`performance_dashboard/frontend/scripts/build.mjs`. Vite is still used for the
dev server and `/api` proxy.
Why esbuild for production build:
- Vite/Rollup hit a local macOS native optional dependency code-signing issue
in this environment.
- Direct esbuild worked reliably and is sufficient for this small dashboard.
## Static Serving
After frontend build, the FastAPI server serves:
- static JS/CSS from `performance_dashboard/frontend/dist/assets`
- `performance_dashboard/frontend/dist/index.html` for the dashboard page
This allows a single local port to serve both the API and UI.
## Local Run Workflow
Build frontend:
```bash
cd performance_dashboard/frontend
conda run -n fastvideo env PATH=/Applications/Codex.app/Contents/Resources/cua_node/bin:/usr/local/bin:/usr/bin:/bin \
/Applications/Codex.app/Contents/Resources/cua_node/bin/npm install
conda run -n fastvideo env PATH=/Applications/Codex.app/Contents/Resources/cua_node/bin:/usr/local/bin:/usr/bin:/bin \
/Applications/Codex.app/Contents/Resources/cua_node/bin/npm run build
```
Run dashboard:
```bash
export HF_TOKEN=hf_...
python -m fastvideo.performance_dashboard --host 0.0.0.0 --port 8000
```
Open locally:
```text
http://127.0.0.1:8000
```
## ngrok Workflow
`python -m fastvideo.performance_dashboard --host 0.0.0.0 --port 8000`
starts the actual local dashboard server.
`ngrok http 8000` does not start the dashboard. It exposes the already-running
local server through a temporary public URL.
Typical flow:
```bash
python -m fastvideo.performance_dashboard --host 0.0.0.0 --port 8000
ngrok http 8000
```
Use the HTTPS URL printed by ngrok to view the dashboard remotely.
## Verification Commands
Backend tests:
```bash
conda run -n fastvideo python -m pytest \
fastvideo/tests/performance/test_dashboard_service.py \
fastvideo/tests/performance/test_dashboard_api.py \
-q
```
Expected after latest changes:
```text
8 passed
```
Frontend build:
```bash
cd performance_dashboard/frontend
conda run -n fastvideo env PATH=/Applications/Codex.app/Contents/Resources/cua_node/bin:/usr/local/bin:/usr/bin:/bin \
/Applications/Codex.app/Contents/Resources/cua_node/bin/npm run build
```
Expected:
```text
tsc && node scripts/build.mjs
```
with exit code 0.
## Known Notes
- `performance_dashboard/frontend/node_modules/` and
`performance_dashboard/frontend/dist/` are ignored by git.
- `npm install` reported two high-severity audit findings in dependency tree.
`npm audit fix --force` was not run because it can introduce breaking
dependency upgrades.
- Existing `fastvideo` package imports may emit platform warnings such as NPU
or macOS torch distributed messages. These are not dashboard-specific errors.
@@ -14,7 +14,7 @@ based on **Wan2.1** (SkyReels-V2) DiT models with causal attention for
auto-regressive streaming generation.
**Key techniques you will work with:**
- Full finetuning and LoRA on Wan / LTX-2 / MatrixGame models
- Full finetuning and LoRA on Wan / LTX-2 / Matrix-Game 2.0 models
- DMD-based distillation (few-step generation)
- Self-Forcing distillation (causal streaming)
- Diffusion-Forcing SFT (DFSFT) for causal models
@@ -225,10 +225,10 @@ callbacks:
|----------|-----------|----------|
| Wan T2V finetune | `fastvideo/training/wan_training_pipeline.py` | Standard text-to-video finetune / LoRA |
| Wan I2V finetune | `fastvideo/training/wan_i2v_training_pipeline.py` | Image-to-video (first frame conditioned) |
| MatrixGame finetune | `fastvideo/training/matrixgame_training_pipeline.py` | Action-conditioned world model |
| MatrixGame AR diffusion | `fastvideo/training/matrixgame_ar_diffusion_pipeline.py` | AR diffusion-forcing training |
| MatrixGame ODE-init | `fastvideo/training/matrixgame_ode_causal_pipeline.py` | ODE-trajectory init |
| MatrixGame self-forcing distill | `fastvideo/training/matrixgame_self_forcing_distillation_pipeline.py` | Self-forcing distillation |
| Matrix-Game 2.0 finetune | `fastvideo/training/matrixgame2_training_pipeline.py` | Action-conditioned world model |
| Matrix-Game 2.0 AR diffusion | `fastvideo/training/matrixgame2_ar_diffusion_pipeline.py` | AR diffusion-forcing training |
| Matrix-Game 2.0 ODE-init | `fastvideo/training/matrixgame2_ode_causal_pipeline.py` | ODE-trajectory init |
| Matrix-Game 2.0 self-forcing distill | `fastvideo/training/matrixgame2_self_forcing_distillation_pipeline.py` | Self-forcing distillation |
| LTX-2 finetune | `fastvideo/training/ltx2_training_pipeline.py` | LTX-2 architecture finetuning |
| Wan DMD distillation | `fastvideo/training/wan_distillation_pipeline.py` | Few-step distillation via DMD |
| Self-Forcing distill | `fastvideo/training/wan_self_forcing_distillation_pipeline.py` | Causal streaming distillation |
@@ -258,7 +258,7 @@ Read `.agents/memory/evaluation-registry/README.md` for the full metric catalog.
|--------|-------------|-------|
| **Loss trajectory** | Every run, real-time from W&B | Medium |
| **SSIM** | When comparing against reference outputs | High |
| **FVD** | For benchmarking model quality (`benchmarks/fvd/`) | High |
| **FVD** | For benchmarking model quality (`common.fvd` eval metric; example: `examples/inference/eval/eval_fvd.py`) | High |
| **LPIPS** | LoRA merge validation | Medium |
| **Human preference** | Major checkpoints | Highest |
@@ -278,8 +278,8 @@ Read `.agents/memory/evaluation-registry/README.md` for the full metric catalog.
## World Model–Specific Concepts
### Action Injection (MatrixGame)
The MatrixGame pipeline adds **action modules** to each DiT block, enabling
### Action Injection (Matrix-Game 2.0)
The Matrix-Game 2.0 pipeline adds **action modules** to each DiT block, enabling
frame-level mouse/keyboard input conditioning. The action sequence is injected
per-frame alongside the latent video tokens.
@@ -197,6 +197,12 @@ Tolerance guide:
Element-wise `assert_close` alone is not enough for deep full-DiT parity. Also
log global abs-mean drift and per-modality summaries.
When a non-skip component parity run is numerically red after weight/input
checks, invoke `../add-model-08-trace/SKILL.md` before adding bespoke forward
hooks. Use `docs/contributing/activation_trace.md` to keep
`FASTVIDEO_TRACE_LAYERS`, `FASTVIDEO_TRACE_STATS`, and `FASTVIDEO_TRACE_STEPS`
identical across FastVideo and upstream traces.
Useful local commands:
```bash
@@ -94,8 +94,12 @@ Run the shared parity-debug loop. The component test command is:
pytest <parity_test> -v -s
```
For numerical drift, narrow the first divergent block with per-block hooks or
intermediate tensor comparisons before changing layers.
For numerical drift, use `../add-model-08-trace/SKILL.md` before writing bespoke
hooks. Start with FastVideo's activation trace (`fastvideo/hooks/activation_trace.py`;
`docs/contributing/activation_trace.md`) and a block-level regex such as
`FASTVIDEO_TRACE_LAYERS="^block\.layers\.[0-9]+$"`. Only fall back to custom
per-block hooks if the needed boundary or statistic is not exposed by
`FASTVIDEO_TRACE_STATS`.
## Escape Hatches
+82 -90
View File
@@ -1,6 +1,6 @@
---
name: add-model-08-trace
description: Use during /add-model Phase 6 when component parity has failed and root cause requires layer-by-layer divergence analysis. Instruments both the official reference and FastVideo port with forward hooks to find the first numerical divergence point.
description: Use during /add-model Phase 6 when component parity has failed and root cause requires layer-by-layer divergence analysis. Uses FastVideo activation trace first, falling back to custom hooks only for boundaries or stats the utility cannot observe.
---
# Add-Model Trace
@@ -38,104 +38,90 @@ Required inputs before starting:
- Shared deterministic test inputs (same tensors on both sides).
- The component parity test file path and its current failure output.
## Hard Rules: Instrumentation Hierarchy
## Primary Path: FastVideo Activation Trace
Apply these in priority order. Use the highest-priority method that works for
the target site.
Use FastVideo's first-class activation trace before writing custom hooks:
`fastvideo/hooks/activation_trace.py`, documented in
`docs/contributing/activation_trace.md`.
### (1) Forward hooks (PREFERRED)
Pipeline runs attach trace to the transformer during pipeline initialization.
Component-only parity harnesses may call `attach_activation_trace(model)` from
local test/debug code; do not add trace calls to production model code.
Prefix the failing parity command with a tight layer regex:
```bash
FASTVIDEO_TRACE_ACTIVATIONS=1 \
FASTVIDEO_TRACE_LAYERS="^block\.layers\.[0-9]+$" \
FASTVIDEO_TRACE_STATS="abs_mean,sum,max,shape" \
FASTVIDEO_TRACE_STEPS="0" \
FASTVIDEO_TRACE_OUTPUT="/tmp/opencode/fv_trace.jsonl" \
pytest tests/local_tests -k "parity" -v -s
```
Match the layer regex to the actual `model.named_modules()` names. Empty or
broad regexes are expensive; prefer block-level names first, then narrow to
submodules after the first divergent block is known.
## Trace Compare Contract
One JSONL file per side. FastVideo output should use `FASTVIDEO_TRACE_OUTPUT`;
the upstream harness should emit the same JSONL shape:
```json
{"module":"block.layers.0","tensor":"out","step":0,"abs_mean":0.0123,"sum":1.0,"max":0.5,"shape":[1,16,32]}
```
Compare rows by `(module, step, tensor)`. The first row whose `shape`,
`abs_mean`, or `max` diverges beyond the component tolerance is the first broken
boundary. Keep `FASTVIDEO_TRACE_LAYERS`, `FASTVIDEO_TRACE_STATS`, and
`FASTVIDEO_TRACE_STEPS` identical between sides; if row order differs, sort or
normalize before diffing.
## Drill-Down Loop
**Initial run:** trace every top-level block (`^block\.layers\.[0-9]+$` or the
family's equivalent). Identify the first block index where `abs_mean` or `max`
drifts beyond tolerance while earlier blocks match.
**Drill run:** tighten `FASTVIDEO_TRACE_LAYERS` to submodules inside the first
divergent block: attention output, MLP projections, norm outputs, modality
adapters, or other named boundaries exposed by `named_modules()`.
**Iterate:** if the first divergent operation is a free function or tensor op not
visible as an `nn.Module`, use the fallback instrumentation hierarchy below.
The loop ends when the first divergent submodule or operation is identified with
a file:line citation in the official source.
## Fallback Instrumentation Hierarchy
Use these only when activation trace cannot observe the needed boundary or
statistic.
### (1) Custom forward hooks
`module.register_forward_hook(...)` and `register_forward_pre_hook(...)`.
Always within `try/finally` with `handle.remove()`. Zero source residue.
```python
handle = module.register_forward_hook(fn)
try:
output = model(inputs)
finally:
handle.remove()
```
### (2) Runtime monkey-patch (PREFERRED over source edits)
### (2) Runtime monkey-patch
`module.attr = wrapped_func` or `cls.method = wrapped_method`, restored via
`try/finally` (save original first). Use for free functions and non-Module
sites such as activation functions (`swiglu`, `apply_rotary_emb`) that cannot
be hooked as `nn.Module` submodules.
```python
original = cls.method
cls.method = wrapped
try:
output = model(inputs)
finally:
cls.method = original
```
`try/finally` (save original first). Use for free functions and non-Module sites
such as activation functions (`swiglu`, `apply_rotary_emb`).
### (3) Source edits in FastVideo's own code
Only when (1) and (2) are insufficient. Track all edits within a single named
`git stash` boundary OR a temporary branch. Run `git diff` before closing the
investigation to confirm the stash or branch is clean. The cleanup gate
enforces this.
investigation to confirm cleanup.
### (4) Source edits in official repo source
Allowed if EITHER:
- (a) The official repo is a git-tracked clone (e.g. `daVinci-MagiHuman/` at
the repo root): use `git diff` in the clone path to verify cleanup.
- (b) It's installed editable (`pip install -e .`): use `git diff` in the
editable source path to verify cleanup.
If the official repo is installed non-editable in site-packages: back up the
target file (`cp original.py original.py.trace-backup`) before editing, then
restore from backup at the end (or `pip install --force-reinstall <pkg>`).
The cleanup gate verifies via diff-against-backup or zero-diff-in-clone.
## Logging Contract
One log file per side. Paths:
```
/tmp/opencode/<family>_<component>_up_layers.log
/tmp/opencode/<family>_<component>_fv_layers.log
```
Format: one line per captured tensor, space-separated:
```
<name> <shape> <abs_mean> <sum> <min> <max>
```
Example:
```
block[00] (1,512,1024) 0.012345 6.3210 -0.4321 0.4321
```
Keep the format diff-friendly. Running `diff /tmp/opencode/x_up.log
/tmp/opencode/x_fv.log` should highlight the first divergent line directly.
Retain side-by-side stdout output alongside the per-side files for human
review.
## Drill-Down Loop
**Initial run:** attach hooks to every top-level block (`model.block.layers[i]`
or equivalent). Identify the first block index `NN` where abs_mean relative
drift exceeds 0.5% compared to the previous block.
**Drill run:** set `<FAMILY>_DEBUG_DRILL_LAYER=NN` and re-run. The script
attaches submodule hooks inside block `NN`: attention output, mlp.pre_norm,
mlp.up_gate_proj, mlp.down_proj input (via pre-hook) and output, mlp output,
attn_post_norm (if present), mlp_post_norm (if present).
**Iterate:** if the drill run points to a free function (e.g. an activation
not wrapped in an `nn.Module`), switch to a monkey-patch (method 2) to
intercept its output via the next module's pre-hook.
The loop ends when the first divergent submodule is identified with a
file:line citation in the official source.
Allowed only when hook and monkey-patch approaches cannot capture the site.
For git-tracked or editable official clones, use `git diff` in the clone path to
verify cleanup. For non-editable site-packages, back up the target file before
editing and restore it before handoff.
## Hypothesis Toggles
@@ -194,11 +180,13 @@ Escalate to the calling bucket skill when:
Return to the calling subagent with:
- File paths to per-side logs (`/tmp/opencode/<family>_<component>_{up,fv}_layers.log`).
- The identified first divergent layer or submodule name.
- FastVideo trace JSONL path and upstream trace JSONL path.
- Trace settings used: `FASTVIDEO_TRACE_LAYERS`, `FASTVIDEO_TRACE_STATS`, and
`FASTVIDEO_TRACE_STEPS`.
- The first divergent `(module, step, tensor)` row and observed drift.
- The upstream file:line citation where the divergence originates.
- Hypothesis verdict if an A/B toggle was used (e.g. "PATCH_LINEAR=1 closes
the gap, confirming dtype-cast difference in PackedExpertLinear").
- Fallback hook/patch verdict if activation trace could not observe the boundary.
- Hypothesis verdict if an A/B toggle was used, for example `PATCH_LINEAR=1`.
- Cleanup-gate status: `[cleanup-gate] PASS` or a list of unresolved items.
The calling agent uses this to scope the production fix in the FastVideo
@@ -206,10 +194,14 @@ component file.
## References
- `templates/block_trace_debug.py` in this skill directory: the canonical
template this skill generalizes.
- `docs/contributing/activation_trace.md` for canonical activation-trace env vars,
JSONL output, cost model, and troubleshooting.
- `fastvideo/hooks/activation_trace.py` for the implementation and
`attach_activation_trace(model)` entry point.
- `templates/block_trace_debug.py` in this skill directory: fallback custom-hook
template when activation trace cannot observe the needed boundary or stat.
- `tests/local_tests/transformers/_debug_magi_human_block_parity.py` in the
FastVideo3 repo: the worked magi-human example this skill was extracted from.
FastVideo3 repo: historical worked example for custom hook/patch debugging.
- `add-model/SKILL.md` Phase 6: the calling context for this skill.
- `add-model-03-port-dit/SKILL.md`, `add-model-04-port-vae/SKILL.md`,
`add-model-05-port-encoder/SKILL.md`, `add-model-06-port-generic/SKILL.md`:
@@ -157,6 +157,13 @@ Debug pipeline drift in this order:
channel order, sample rate, FPS, and final slicing.
7. Add targeted stage-level diagnostics to identify the first divergent stage.
If stage diagnostics show the first bad stage is transformer/denoising or a
mid-DiT block, enable activation trace before adding ad hoc pipeline prints; see
`docs/contributing/activation_trace.md` and `../add-model-08-trace/SKILL.md`.
Keep `FASTVIDEO_TRACE_LAYERS`, `FASTVIDEO_TRACE_STATS`, and
`FASTVIDEO_TRACE_STEPS` identical across reruns so pipeline parity traces diff
one-to-one.
If the first divergence belongs to component implementation, strict loading, or
conversion mapping, stop pipeline edits and return `next_step=return_to_phase_6`
with the exact failing evidence. Do not patch conversion from this skill.
+5
View File
@@ -267,6 +267,11 @@ conversion, route it through `../add-model-07-conversion/SKILL.md` with a retry
request matching `contracts/conversion_request.md`, then resume the component
skill with the updated conversion handoff.
When a component failure narrows to layer-by-layer numerical drift, load
`../add-model-08-trace/SKILL.md` before writing custom hooks. It uses
`fastvideo/hooks/activation_trace.py`; canonical env vars and JSONL format are
documented in `docs/contributing/activation_trace.md`.
Phase 6 ends only when every required component handoff reports
`parity_status=non_skip_pass`, or when a precise blocker or escape hatch is
recorded in `port_state_file`.
+32
View File
@@ -0,0 +1,32 @@
---
name: add-reward-model
description: Use when adding reusable reward models under fastvideo/train/methods/rl/rewards for RLHF or online RL training.
---
# Add Reward Model
Use for reward models consumed by RL methods.
## Placement
- Put reusable reward code under `fastvideo/train/methods/rl/rewards/`.
- Expose public builders from `fastvideo/train/methods/rl/rewards/__init__.py`.
- Keep method-specific aggregation or advantage logic out of reward classes.
## Media Inputs
- Reward callables receive decoded media tensors.
- Accept single-frame tensors as `[B, C, H, W]` and multi-frame tensors as `[B, C, T, H, W]` when practical.
- Frame selection is reward-specific. Frame scorers such as PickScore and CLIPScore should explicitly select frame `0`; temporal rewards should inspect whichever frames they need.
- Return one scalar reward per prompt/sample.
## Attribution
- If code is ported or closely adapted from another repo, add a short comment or docstring naming the source file/function.
- Preserve SPDX headers used by FastVideo files.
## Tests
- Unit-test tensor layout handling without loading large reward checkpoints.
- Allow fake scorer injection for multi-reward tests.
- Test weighted reward aggregation and metric keys.
+38
View File
@@ -0,0 +1,38 @@
---
name: add-rl-method
description: Use when adding or modifying an RL/RLHF method under fastvideo/train/methods/rl, including DiffusionNFT-like methods.
---
# Add RL Method
Use for new RL methods in the modular `fastvideo/train` stack.
## Required Shape
- Add the method under `fastvideo/train/methods/rl/`.
- Subclass `TrainingMethod`.
- Keep model-family logic in `ModelBase` wrappers.
- Decode generated latents through `ModelBase.decode_latents`; add that hook to the new model wrapper instead of decoding inside the RL method.
- Use `fastvideo/train/methods/rl/common/sampling.py` for generation unless the method has a documented reason to avoid sampling.
- Use `fastvideo/train/methods/rl/common/prompt_sampling.py` for reusable grouped prompt sampling patterns such as DiffusionNFT K-repeat.
- Use `fastvideo/train/methods/rl/rewards/` for reward models.
## Optimization
- Return `manages_optimization() == True` only when the method must own a nonstandard outer/inner loop.
- If using managed optimization, implement `managed_train_step(data_stream, iteration)`.
- Existing trainer callbacks, checkpointing, tracking, and validation should still work.
## Config
- Put method knobs under `method`.
- Put sampler knobs under `method.sampling`.
- Do not put scheduler or trajectory policy into model configs.
- Do not split a diffusers-style scheduler from its built-in `step()` solver in YAML; use `trajectory` only for higher-level ODE vs re-noise behavior.
- Avoid fixed timestep lists in examples unless reproducing a known baseline; prefer scheduler-generated defaults.
## Tests
- Add fake-model tests for sampler/method behavior.
- Add config parse tests for the public YAML.
- Confirm existing train methods stay on the default Trainer path.
+9 -4
View File
@@ -1,3 +1,8 @@
---
name: dreamverse-deploy
description: Use when redeploying the migrated Dreamverse app backend and frontend on a chosen local GPU; tears down existing ports, launches services, and waits for readiness checks.
---
# dreamverse-deploy — redeploy migrated Dreamverse on a chosen GPU
**Scope:** project (lives in this repo at `.agents/skills/dreamverse-deploy/`)
@@ -17,7 +22,7 @@ both `/readyz` and the FE root to return 200.
`cerebras-cloud-sdk`, `openai` installed (override the default path with
`DREAMVERSE_PYTHON=/path/to/python`)
- `~/.env` exporting `CEREBRAS_API_KEY`, `GROQ_API_KEY`, etc.
- pnpm installed at `/home/william5lin/.local/share/pnpm/pnpm` (or in `$PATH`)
- npm available in `$PATH` (or set `NPM=/path/to/npm`)
- `gcc-13` + `g++-13` at `/usr/bin/` (workaround for nvcc gcc-15 rejection)
- **Recommended:** native ffmpeg env file at `apps/dreamverse/scripts/ffmpeg-env.sh`
(built once via `bash apps/dreamverse/scripts/install_native_ffmpeg.sh`).
@@ -105,7 +110,7 @@ Flags can appear in any position relative to the positional args. Explicit flag
5. Launches the backend via `apps/dreamverse/scripts/dreamverse-server` in a
detached `setsid` session, captures PID.
6. Polls `/readyz` until 200 (max 5 min).
7. Launches the frontend via `pnpm run dev:devtools` in a detached session,
7. Launches the frontend via `npm run dev:devtools` in a detached session,
captures PID.
8. Polls FE `/` until 200 (max 60s).
9. Prints URLs, PIDs, and log paths.
@@ -120,7 +125,7 @@ Flags can appear in any position relative to the positional args. Explicit flag
PLAYWRIGHT_SKIP_WEBSERVER=1 BACKEND_URL=http://127.0.0.1:8009 \
PLAYWRIGHT_BASE_URL=http://127.0.0.1:5274 \
NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 \
pnpm exec playwright test
npm exec -- playwright test
```
The fast suite (8 specs, ~5s) runs by default; the long-running
two-segment audio-continuation spec is gated behind
@@ -146,7 +151,7 @@ PLAYWRIGHT_SKIP_WEBSERVER=1 \
PLAYWRIGHT_BASE_URL=http://127.0.0.1:5274 \
NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 \
PLAYWRIGHT_LONG_RUNNING=1 \
pnpm exec playwright test e2e/long-running-segments.spec.ts
npm exec -- playwright test e2e/long-running-segments.spec.ts
```
Expected runtime: ~7-9 minutes on a B200 (torch.compile max-autotune
@@ -185,17 +185,10 @@ CONDA_ENV_PYTHON="${DREAMVERSE_PYTHON:-${HOME}/miniconda3/envs/fv-main/bin/pytho
"${CONDA_ENV_PYTHON}" -c 'import flashinfer' 2>/dev/null \
|| bail "flashinfer-python not installed in ${CONDA_ENV_PYTHON} (run: ${CONDA_ENV_PYTHON} -m pip install flashinfer-python --no-build-isolation)"
PNPM="${PNPM:-}"
if [[ -n "${PNPM}" ]]; then
PNPM_REQUESTED="${PNPM}"
PNPM="$(command -v "${PNPM}" 2>/dev/null || true)"
[[ -n "${PNPM}" ]] || bail "pnpm not executable or not in PATH: ${PNPM_REQUESTED} (set PNPM to override)"
elif [[ -x "${HOME}/.local/share/pnpm/pnpm" ]]; then
PNPM="${HOME}/.local/share/pnpm/pnpm"
else
PNPM="$(command -v pnpm 2>/dev/null || true)"
fi
[[ -n "${PNPM}" ]] && [[ -x "${PNPM}" ]] || bail "pnpm not found. Set PNPM, install at ${HOME}/.local/share/pnpm/pnpm, or add pnpm to PATH"
NPM="${NPM:-npm}"
NPM_REQUESTED="${NPM}"
NPM="$(command -v "${NPM}" 2>/dev/null || true)"
[[ -n "${NPM}" ]] && [[ -x "${NPM}" ]] || bail "npm not executable or not in PATH: ${NPM_REQUESTED} (set NPM to override)"
GCC13="$(command -v "${GCC13:-gcc-13}" 2>/dev/null || true)"
GPP13="$(command -v "${GPP13:-g++-13}" 2>/dev/null || true)"
@@ -386,7 +379,7 @@ frontend_log="${LOG_DIR}/frontend-port${FRONTEND_PORT}.log"
# requested port differs, run `next dev --port` directly with devtools env.
fe_cmd="run dev:devtools"
if [[ "${FRONTEND_PORT}" != "5274" ]]; then
fe_cmd="exec next dev --port ${FRONTEND_PORT}"
fe_cmd="exec -- next dev --port ${FRONTEND_PORT}"
fi
setsid bash -c "
@@ -395,7 +388,7 @@ setsid bash -c "
export BACKEND_URL=http://127.0.0.1:${BACKEND_PORT}
export BACKEND_HOST=127.0.0.1
export BACKEND_PORT=${BACKEND_PORT}
exec '${PNPM}' ${fe_cmd}
exec '${NPM}' ${fe_cmd}
" > "${frontend_log}" 2>&1 < /dev/null &
disown
@@ -462,5 +455,5 @@ cat <<SUMMARY
BACKEND_URL=http://127.0.0.1:${BACKEND_PORT} \\
PLAYWRIGHT_BASE_URL=http://127.0.0.1:${FRONTEND_PORT} \\
NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 \\
pnpm exec playwright test
npm exec -- playwright test
SUMMARY
+4
View File
@@ -6,4 +6,8 @@
{"name": "seed-ssim-references", "description": "Run a new or updated fastvideo/tests/ssim/ test on Modal, pull generated videos, and upload them to FastVideo/ssim-reference-videos so the test has a regression baseline", "path": "seed-ssim-references/SKILL.md", "status": "draft", "trust": "low"}
{"name": "reseed-ssim-references", "description": "Re-seed (overwrite) HF reference videos for an existing fastvideo/tests/ssim/ test and a single model id on Modal L40S. Always backs up current refs first, regenerates on Modal, pauses for the user to eyeball before-vs-after, then uploads with --force scoped to --model-id. Sister skill to seed-ssim-references; use when intentional code change has invalidated existing refs", "path": "reseed-ssim-references/SKILL.md", "status": "draft", "trust": "low"}
{"name": "decompose-pipeline-pr", "description": "Decompose an oversized FastVideo pipeline PR into a stack of independently-reviewable PRs. Tiers the diff by blast radius (invisible / dead code / cross-cutting infra / activation), produces a branch graph and worktree bootstrap, drafts the AGENTS.md manifest, flags missing tests on cross-cutting infra changes, and extracts lessons from the PR body. Worked example: PR #1280 daVinci-MagiHuman (9.8k LOC) decomposed into 10 stacked PRs.", "path": "decompose-pipeline-pr/SKILL.md", "status": "tested", "trust": "medium"}
{"name": "reseed-performance-baseline", "description": "Re-seed the HF performance-tracking baseline for an intentional runtime, dependency, or environment-caused benchmark shift. Use when performance CI fails because metrics such as latency, throughput, component time, or peak memory changed for an accepted reason and the rolling median baseline must be advanced by replicating one reviewed shifted source result into three success=true records, or five records when explicitly requested", "path": "reseed-performance-baseline/SKILL.md", "status": "draft", "trust": "low"}
{"name": "add-model", "description": "Add a new model (or variant) to FastVideo: DiT + configs + pipeline + presets + registry + tests. Walks through FastVideo's single stage-based pipeline architecture with exact file paths and registration hooks.", "path": "add-model/SKILL.md", "status": "draft", "trust": "low"}
{"name": "rlhf-training-abstractions", "description": "Use when changing FastVideo RLHF/RL training infrastructure, especially sampler, reward, scheduler trajectory, or method boundaries under fastvideo/train.", "path": "rlhf-training-abstractions/SKILL.md", "status": "draft", "trust": "low"}
{"name": "add-rl-method", "description": "Use when adding or modifying an RL/RLHF method under fastvideo/train/methods/rl, including DiffusionNFT-like methods.", "path": "add-rl-method/SKILL.md", "status": "draft", "trust": "low"}
{"name": "add-reward-model", "description": "Use when adding reusable reward models under fastvideo/train/methods/rl/rewards for RLHF or online RL training.", "path": "add-reward-model/SKILL.md", "status": "draft", "trust": "low"}
+1 -1
View File
@@ -38,7 +38,7 @@ entrypoint, and applying defaults from the closest example script.
| `finetune` (Wan T2V) | `fastvideo/training/wan_training_pipeline.py` |
| `finetune` (Wan I2V) | `fastvideo/training/wan_i2v_training_pipeline.py` |
| `finetune` (LTX-2) | `fastvideo/training/ltx2_training_pipeline.py` |
| `finetune` (MatrixGame) | `fastvideo/training/matrixgame_training_pipeline.py` |
| `finetune` (Matrix-Game 2.0) | `fastvideo/training/matrixgame2_training_pipeline.py` |
| `distill-dmd` | `fastvideo/training/wan_distillation_pipeline.py` |
| `self-forcing` | `fastvideo/training/wan_self_forcing_distillation_pipeline.py` |
@@ -0,0 +1,476 @@
---
name: reseed-performance-baseline
description: Re-seed the HF performance-tracking baseline for an intentional runtime, dependency, or environment-caused benchmark shift using one or more reviewed normalized performance JSONs. Use when performance CI fails because metrics such as latency, throughput, component time, or peak memory changed for an accepted reason and the rolling median baseline in FastVideo/performance-tracking must be advanced from a consistent batch of reviewed source results. The workflow backs up existing history under /tmp, validates all source JSONs for the same (model_id, gpu_type), rejects internally inconsistent source batches, uploads one success=true reseed record per accepted source JSON, and offers to clean local temp state after a successful upload.
---
# Re-seed Performance Baseline
## Purpose
Replace or advance the rolling performance baseline for a single
`(model_id, gpu_type)` pair in the HF dataset
`FastVideo/performance-tracking`.
Performance comparison uses the median of up to the last 5 successful records
for the same model and GPU. Failed records are useful audit history, but they
do not move the future baseline because `compare_baseline.py` loads records
with `successful_only=True`.
This skill now reseeds from a reviewed batch of one or more source performance
JSONs. It uploads one new `success=true` record per accepted source JSON; it
does not blindly replicate one measurement into 3 or 5 records. The effective
reseed size is therefore dynamic and equals the number of provided, validated,
internally consistent source JSONs.
If the operator provides fewer than 3 records, call out that the last-5 rolling
median may not move immediately. If the operator provides 3 consistent shifted
records, the rolling median usually moves immediately. If the operator provides
5 consistent shifted records, the last-5 window is effectively reset to the new
runtime profile.
These records are intentional operator-approved baseline resets, not ordinary
independent main-branch persistence. Mark them clearly with provenance fields
so the HF history remains auditable.
Use this skill when a performance test fails for an intentional and reviewed
reason, such as a torch/runtime/container upgrade that legitimately increases
peak memory or changes timings. This is the performance equivalent of
`reseed-ssim-references`: backup first, scope tightly, require explicit human
approval, then upload reviewed accepted baseline records.
## When to use
- A PR or main run failed the rolling performance comparison by more than the
allowed regression threshold, and maintainers agree the shift is caused by
an intentional runtime, dependency, hardware image, or benchmark environment
change rather than a FastVideo logic regression.
- One or more shifted source result JSONs have been reviewed and accepted, and
the operator wants to use those exact reviewed results to advance the rolling
baseline.
- The source batch is internally consistent: no provided source JSON regresses
against the batch median by more than the configured tolerance.
## When not to use
- The benchmark failure might be a real code regression. Fix or investigate
the code path first.
- The fixed benchmark thresholds in
`.buildkite/performance-benchmarks/tests/*.json` are too low. Those are a
separate gate from the rolling HF baseline and may need a code review change.
- There is no clear source run, commit, and rationale. Baseline history is a
production signal; do not edit it without provenance.
- The provided source JSONs disagree materially with each other. Rerun or
investigate instead of uploading a noisy reseed batch.
## Inputs
| Parameter | Required | Description |
|-----------|----------|-------------|
| `model_id` | Yes | Benchmark id, e.g. `wan-t2v-1.3b-2gpu`. This maps to the HF subdirectory after `sanitize(model_id)`. |
| `gpu_type` | Yes | Exact GPU device string from the performance record, e.g. the L40S device name emitted by CI. Baselines are GPU-specific. |
| `source_results` | Yes | One or more local paths or Buildkite artifact URLs for accepted shifted performance JSONs. Prefer normalized `normalized_perf_*.json` artifacts emitted by `compare_baseline.py`. Accept `source_result` as an alias only for a single JSON. |
| `max_intra_batch_regression` | No | Maximum allowed regression of any source JSON against the source batch median. Default: `PERF_MAX_REGRESSION` if set, otherwise `0.05` (5%). |
| `intent_rationale` | Yes | One-line explanation for why the baseline shift is legitimate. This is written into provenance and should be reused in the PR. |
Hardcoded defaults:
- HF repo: `FastVideo/performance-tracking` (`HF_REPO_ID` override is
supported by the code, but use the default unless the user explicitly asks).
- Local sync root: `/tmp/perf-tracking` (`PERFORMANCE_TRACKING_ROOT` override
is supported).
- Backup root: `/tmp/performance_reseed_backup`.
- Download scratch root for source artifact URLs: `/tmp/performance_reseed_source`.
- Baseline window: last 5 `success=true` records for the same
`(model_id, gpu_type)`.
- Reseed count: dynamic. Upload exactly one accepted seed record per validated
source JSON.
## Steps
### 1. Validate the target and source results
Normalize `source_results` to a list. If the user passes a single
`source_result`, treat it as a one-element `source_results` list and report
that a single record may not move the last-5 median immediately.
If any source result is a Buildkite artifact URL, download it first into a
local scratch directory under `/tmp/performance_reseed_source/` and use the
downloaded JSON path for the rest of the workflow. If the agent cannot access
the artifact because Buildkite authentication is missing, ask the user to
download the artifact manually and provide the local path.
Prefer the normalized Buildkite artifact emitted by `compare_baseline.py`:
```text
perf_reports/results/normalized_perf_*.json
```
That file is already in the HF tracking schema. Load each normalized JSON
directly:
```python
import json
with open(source_result, encoding="utf-8") as f:
record = json.load(f)
```
Stop if any normalized record's `model_id` or `gpu_type` does not match the
requested `model_id` and `gpu_type`.
The source records may have `success: false` when they came from failed
rolling baseline comparisons. That is expected; only the reviewed reseed
records become new `success: true` baseline records after explicit approval.
Sort validated source records by their original `timestamp` ascending before
preparing the seed records. If a source timestamp is missing or unparsable,
preserve input order for those records and print a warning. This makes the
fresh reseed timestamps deterministic and makes it clear which records enter
the last-5 window when more than 5 source JSONs are provided.
Check that `HF_API_KEY` is exported. The sync path may be public, but the
upload path requires write access.
### 1a. Check source batch consistency
Before syncing or preparing uploads, reject source batches that are internally
inconsistent. Use the same metric direction as `compare_baseline.py`:
- Lower is better: `latency`, `memory`, `text_encoder_time_s`, `dit_time_s`,
`vae_decode_time_s`.
- Higher is better: `throughput`.
For each metric with at least two non-null source values:
1. Compute the source batch median.
2. For lower-is-better metrics, compute `(source_value - batch_median) / batch_median`.
3. For `throughput`, compute `(batch_median - source_value) / batch_median`.
4. Stop if any source record regresses against the batch median by more than
`max_intra_batch_regression`.
Default `max_intra_batch_regression` to `PERF_MAX_REGRESSION` when set,
otherwise `0.05`. Print a table with per-source values, batch median, and
worst intra-batch regression.
This check prevents uploading a mixed batch where one JSON is materially
slower or faster than the others. If the batch fails this check, ask the user
to provide a cleaner batch or explicitly investigate the variance. Do not
silently drop outliers unless the user gives a concrete reviewed reason and a
new source list.
### 1b. How to obtain source results from CI
The performance CI exports normalized source results for failed rolling
baseline comparisons when `compare_baseline.py` ran. The preferred artifacts
come from:
```text
perf_reports/results/normalized_perf_*.json
```
The normal operator flow is:
1. Open the failed Buildkite performance job or several reruns of the same
benchmark after the accepted environment shift.
2. Download the `normalized_perf_*.json` artifacts for the target benchmark.
3. Pass all reviewed local paths or artifact URLs as `source_results`.
Do not scrape the Markdown performance summary to reconstruct JSON. The
normalized JSON artifacts are the only supported source of truth for reseed
metrics and provenance. Raw `fastvideo/tests/performance/results/perf_*.json`
artifacts are not accepted by this skill. If no normalized JSON artifact is
present, that run is not a valid source for baseline reseeding.
### 2. Sync and back up existing HF records under /tmp
Use `fastvideo/tests/performance/hf_store.py` helpers directly. Do **not** use
`compare_baseline.py` as a sync shortcut; on full main runs it can persist
records, while this step must only fetch and back up existing history.
The sync command pattern is:
```bash
export PERFORMANCE_TRACKING_ROOT="${PERFORMANCE_TRACKING_ROOT:-/tmp/perf-tracking}"
export HF_REPO_ID="${HF_REPO_ID:-FastVideo/performance-tracking}"
PYTHONPATH=fastvideo/tests/performance python -c 'from hf_store import sync_from_hf; import os; sync_from_hf(os.environ["PERFORMANCE_TRACKING_ROOT"], strict=True)'
```
Then back up only the sanitized model directory under `/tmp`:
```bash
SHORT_COMMIT=$(git rev-parse --short=12 HEAD)
TIMESTAMP=$(date -u +%Y%m%d_%H%M%S)
MODEL_SAFE=$(PYTHONPATH=fastvideo/tests/performance python - <<'PY'
from hf_store import sanitize
print(sanitize("<model_id>"))
PY
)
BACKUP_DIR="/tmp/performance_reseed_backup/${TIMESTAMP}_${SHORT_COMMIT}_${MODEL_SAFE}"
mkdir -p "$BACKUP_DIR"
cp -R "${PERFORMANCE_TRACKING_ROOT}/${MODEL_SAFE}" "$BACKUP_DIR/" 2>/dev/null || true
```
Write provenance next to the backup:
```bash
cat > "$BACKUP_DIR/PROVENANCE.txt" <<EOF
model_id: <model_id>
gpu_type: <gpu_type>
source_results:
- <source_result_1>
- <source_result_2>
reseed_record_count: <len(source_results)>
max_intra_batch_regression: <threshold>
head_commit: $(git rev-parse HEAD)
timestamp_utc: $(date -u +%FT%TZ)
reason: <intent_rationale>
EOF
```
If the backup has no prior records, this is not a destructive reseed; it is a
first baseline seed. Continue, but report that baseline history was empty.
### 3. Compute old baseline and candidate shift
Load the last 5 successful records for the target:
```python
from hf_store import load_records_for_model
records = load_records_for_model(
"/tmp/perf-tracking",
"<model_id>",
"<gpu_type>",
last_n=5,
successful_only=True,
)
```
Print a small table showing old medians, source batch medians, candidate
medians after appending the proposed seed records, and source batch spread for:
- `latency`
- `throughput`
- `memory`
- `text_encoder_time_s`
- `dit_time_s`
- `vae_decode_time_s`
Also print how many successful old records exist. Make clear:
- 1 seed record usually does not move a last-5 median by itself.
- 3 consistent seed records usually move the last-5 median immediately.
- 5 consistent seed records effectively reset the last-5 window.
- The records are intentional approved baseline resets and must be labeled
that way.
### 4. Confirm intent
Require an explicit confirmation phrase before preparing the upload:
> About to RE-SEED performance baseline for `<model_id>` on `<gpu_type>`.
> This will upload `<N>` new `success=true` records to
> `FastVideo/performance-tracking/<sanitize(model_id)>/`, one per accepted
> source JSON.
>
> Reason: `<intent_rationale>`
> Source results: `<source_results>`
> Reseed record count: `<N>`
> Max intra-batch regression: `<threshold>`
> Note: these records come from a reviewed source batch and are intended to
> move the rolling median to the accepted runtime profile. They are not
> ordinary main-branch persistence.
> HEAD: `<git rev-parse --short=12 HEAD>`
> Backup: `<BACKUP_DIR>`
>
> Reply `confirm performance reseed` to proceed, anything else to abort.
Do not continue unless the user types exactly `confirm performance reseed`.
### 5. Create the accepted seed records
Create one seed record from each normalized source result. Do not copy the
source JSON wholesale.
Infer the baseline field allowlist from all existing HF records for the target
`(model_id, gpu_type)` after syncing, including both `success=true` and
`success=false` records. Use the union of non-provenance keys present in those
target records, preserving only fields that also exist in the normalized
source record or are explicitly set by the reseed workflow. Always include
`model_id`, `timestamp`, and `success` because the upload path and baseline
loader depend on them. Always set `timestamp` to a fresh reseed timestamp and
`success` to `true`. Do not include unrelated source-only fields that are
absent from existing HF records.
Exclude existing provenance or operator metadata from the inferred baseline
field allowlist. At minimum, exclude keys prefixed with `baseline_reseed` and
any fields known to be local-only audit metadata.
If there are no previous HF records for the target model/GPU, fall back to this
default baseline field list:
- `model_id`
- `timestamp`
- `commit_sha`
- `gpu_type`
- `latency`
- `throughput`
- `memory`
- `text_encoder_time_s`
- `dit_time_s`
- `vae_decode_time_s`
- `success`
Do not upload extra fields from the source artifact.
Optional provenance fields are allowed and useful:
- `baseline_reseed: true`
- `baseline_reseed_reason`
- `baseline_reseed_source_result`
- `baseline_reseed_source_timestamp`
- `baseline_reseed_source_success`
- `baseline_reseed_batch_size`
- `baseline_reseed_batch_index`
- `baseline_reseed_operator`
- `baseline_reseed_max_intra_batch_regression`
Use a fresh reseed timestamp for each seed record, not the original source
result timestamp. This is required because
`load_records_for_model(..., last_n=5)` keeps the last records after loading
the model directory; stale filenames/timestamps may not enter the last-5
window and therefore may not move the median. Preserve the original source
timestamp in `baseline_reseed_source_timestamp`.
Use the existing filename convention from `_write_tracking_record()`:
`<sanitize(timestamp)>_<sanitize(commit_sha)>.json` under the sanitized model
directory, but include a deterministic suffix such as `_reseed_01`,
`_reseed_02`, and so on before `.json` so multiple records from the same
batch do not overwrite each other.
If a source record already exists on HF with `success=false`, do not edit it
in place unless the user explicitly asked for an audit-preserving correction.
Prefer uploading new accepted seed records so failed history remains visible.
### 6. Pause before upload
Print:
- Backup directory path under `/tmp`.
- Prepared local record paths under `PERFORMANCE_TRACKING_ROOT`.
- HF paths that will receive the new records.
- Old rolling medians.
- Source batch medians, source batch spread, reseed count, and candidate
medians.
- Rationale.
Ask the user to reply exactly `upload`. Anything else aborts and leaves the
prepared records plus backup on disk.
### 7. Upload only the scoped records
Use the shared storage helper so the path and repo type match CI:
```python
from hf_store import upload_record
upload_record("<local_record_path>", record, strict=True)
```
Run it once per prepared record. Each upload goes to:
```text
FastVideo/performance-tracking/<sanitize(model_id)>/<record_filename>.json
```
Never bulk upload the whole tracking root. Never modify another model's
directory in the same operation.
### 8. Report outcome and offer cleanup
Report:
- Uploaded HF paths.
- Backup directory under `/tmp`.
- Local tracking root, usually `/tmp/perf-tracking`.
- Old baseline window count and medians.
- Source batch medians, source batch spread, reseed count, and candidate
medians.
- Expected effect based on reseed count.
- Any separate threshold changes still needed in
`.buildkite/performance-benchmarks/tests/*.json`.
Include the `intent_rationale` in the PR or follow-up comment so reviewers can
distinguish an accepted baseline shift from a hidden regression.
After the upload is verified, ask whether the user wants to clear temporary
local state. Explain what each directory is for:
- `PERFORMANCE_TRACKING_ROOT`, usually `/tmp/perf-tracking`: local synced
mirror of `FastVideo/performance-tracking` plus the prepared local seed
records used for scoped upload.
- `/tmp/performance_reseed_backup/<...>`: local backup of the target model's
pre-reseed HF history plus `PROVENANCE.txt`, kept so a bad reseed can be
audited or corrected.
- `/tmp/performance_reseed_source/<...>` when used: downloaded source JSON
artifacts from Buildkite URLs.
Ask:
> Reseed succeeded. Do you want me to delete the local temp tracking mirror,
> source downloads, and reseed backup under `/tmp`? These files are local
> safety/audit artifacts only; HF already has the uploaded records.
>
> Reply `cleanup reseed temp` to delete them, anything else to keep them.
Do not delete anything unless the user replies exactly
`cleanup reseed temp`. If cleanup is requested, remove only the specific
directories created for this reseed. Never remove unrelated `/tmp` contents.
## Failure modes and handling
- **`HF_API_KEY` unset.** Stop before upload. Do not create an untracked
process that appears to have reseeded but never reached HF.
- **Source result does not match target.** Stop. The wrong benchmark or GPU
would poison a separate baseline.
- **Source batch is internally inconsistent.** Stop if any source regresses
against the source batch median by more than `max_intra_batch_regression`.
Ask for cleaner sources or a reviewed explanation before continuing.
- **Too few source records to move the median.** Continue only after making
clear that one or two records may not immediately move the last-5 median.
- **The source results are noisy or suspicious.** Stop. Reseeding amplifies
those measurements into the baseline, so they must be reviewed first.
- **HF sync fails.** Stop for destructive reseeds. A stale or empty sync can
make the old baseline look missing.
- **Candidate still violates fixed thresholds.** Report that this skill only
handles the rolling HF baseline; update benchmark JSON thresholds in code
review if maintainers accept the new absolute limit.
- **The user aborts at either confirmation.** Leave the backup and prepared
records on disk. Nothing should be uploaded.
- **The user declines cleanup.** Keep `/tmp/perf-tracking`, the source
download directory if any, and `/tmp/performance_reseed_backup/<...>` in
place for audit/debugging.
- **A bad seed was uploaded.** Use the backup and HF history to identify the
uploaded file, then remove or supersede it with an explicitly reviewed
corrective record. Do not silently rewrite unrelated history.
## References
- `.agents/skills/reseed-ssim-references/SKILL.md` — safety pattern for
intentional baseline replacement.
- `fastvideo/tests/performance/compare_baseline.py` — normalization, rolling
median comparison, and persistence rules.
- `fastvideo/tests/performance/hf_store.py` — HF sync, record loading,
`sanitize()`, and `upload_record()`.
- `fastvideo/tests/performance/test_inference_performance.py` — source result
JSON schema.
- `.buildkite/performance-benchmarks/tests/*.json` — fixed absolute benchmark
thresholds, separate from rolling baseline comparisons.
## Changelog
| Date | Change |
|------|--------|
| 2026-05-03 | Initial version. Sister workflow to `reseed-ssim-references`, scoped to one performance `(model_id, gpu_type)` baseline seed with backup, confirmation, provenance, and `success=true` upload. |
| 2026-05-03 | Previous policy: replicate one approved shifted source result into 3 success records by default, or 5 only when explicitly requested. Add provenance marker for replicated-source reseeds. Superseded by the 2026-05-08 dynamic multi-source policy. |
| 2026-05-08 | Replace fixed 3/5 replication with dynamic multi-source reseeding: upload one seed record per reviewed source JSON, validate intra-batch consistency, move backup/source scratch under `/tmp`, and ask whether to clean temp state after successful upload. |
@@ -0,0 +1,41 @@
---
name: rlhf-training-abstractions
description: Use when changing FastVideo RLHF/RL training infrastructure, especially sampler, reward, scheduler trajectory, or method boundaries under fastvideo/train.
---
# RLHF Training Abstractions
Use this skill before editing RLHF-style training code in `fastvideo/train`.
## Boundaries
- RL methods live under `fastvideo/train/methods/rl/` and own algorithm logic: reward collection, advantage computation, policy loss, KL/reference terms, and optimizer cadence.
- Rewards live under `fastvideo/train/methods/rl/rewards/` and must be reusable across RL methods.
- RL methods pass decoded media to rewards; each reward decides whether to use the first frame, sampled frames, or the full video.
- Sampling lives under `fastvideo/train/methods/rl/common/` and must use `ModelBase` primitives plus scheduler math, not model-family inference pipelines.
- Model wrappers under `fastvideo/train/models/` own model-specific forward details.
- Model wrappers also own model-specific latent decoding via `ModelBase.decode_latents`; RL methods should not reach into VAE normalization internals.
- Shared RL helpers such as K-repeat prompt sampling belong under `fastvideo/train/methods/rl/common/` when they are reusable across RL methods.
## Anti-Patterns
- Do not bind RL methods to inference pipeline classes such as `WanDMDPipeline`.
- Do not hardcode timestep lists in a method when the scheduler can generate them.
- Do not put reward-model code inside one RL method.
- Do not make existing non-RL methods use method-managed optimization unless explicitly requested.
## Sampling Policy
- Prefer YAML-configured `method.sampling` with `scheduler`, `trajectory`, `num_steps`, `timesteps`, and `sigmas`.
- Treat diffusers-style scheduler classes as owning both the timestep schedule and their `step()` update rule; avoid a separate `solver` field unless a new sampler truly implements solver math outside the scheduler object.
- Missing `timesteps` means “ask the scheduler”; explicit `timesteps` or `sigmas` are overrides.
- ODE-style trajectories should not re-noise between denoising steps.
- SDE/re-noise behavior must be explicit in config.
## Validation
- Run focused local tests for sampler config and Trainer opt-in behavior.
- Verify existing train methods still report `manages_optimization() == False`.
- Keep fixed-prompt validation helpers in `fastvideo/train/methods/rl/common/validation.py` so new RL methods can reuse sharding and captions.
- Test distributed prompt grouping helpers separately from heavyweight model loading.
- Run `pre-commit run --files <changed paths>`; respect configured excludes.
@@ -89,5 +89,4 @@ The following land in follow-up PRs:
- **MIND** metrics (depends on a separate `vipe` submodule).
- **VBench-2.0** sibling package.
- Native conversion of **FVD** under `fastvideo/eval/metrics/fvd/`.
- The training-time `EvalCallback`.
@@ -36,7 +36,10 @@
"thresholds": {
"L40S": {
"max_generation_time_s": 34.0,
"max_peak_memory_mb": 11000.0
"max_peak_memory_mb": 11000.0,
"max_text_encoder_time_s": 5.0,
"max_dit_time_s": 10.0,
"max_vae_decode_time_s": 10.0
},
"default": {
"max_generation_time_s": 120.0,
+44
View File
@@ -77,6 +77,17 @@ steps:
limit: 2
agents:
queue: "default"
- label: ":microscope: DreamVerse App Tests"
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "dreamverse_app"
command: "timeout 90m .buildkite/scripts/pr_test.sh"
retry:
automatic:
- exit_status: 128
limit: 3
- exit_status: -1
limit: 2
agents:
queue: "default"
# --- Full-suite-scope direct tests ---
- label: ":bar_chart: SSIM Tests"
@@ -206,6 +217,17 @@ steps:
limit: 2
agents:
queue: "default"
- label: ":test_tube: Eval Metrics Tests"
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "eval"
command: "timeout 90m .buildkite/scripts/pr_test.sh"
retry:
automatic:
- exit_status: 128
limit: 3
- exit_status: -1
limit: 2
agents:
queue: "default"
# ============================================================
# Fastcheck: Runs on every PR (~10-15 min parallel)
@@ -289,6 +311,16 @@ steps:
- TEST_TYPE=unit_test
agents:
queue: "default"
- path:
- "apps/dreamverse/**"
- "pyproject.toml"
config:
command: "timeout 30m .buildkite/scripts/pr_test.sh"
label: ":microscope: DreamVerse App Tests"
env:
- TEST_TYPE=dreamverse_app
agents:
queue: "default"
# ============================================================
# Full Suite: Runs when TEST_SCOPE=full
@@ -462,3 +494,15 @@ steps:
- TEST_TYPE=train_framework
agents:
queue: "default"
- path:
- "fastvideo/eval/**"
- "fastvideo/tests/eval/**"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config:
command: "timeout 90m .buildkite/scripts/pr_test.sh"
label: ":test_tube: Eval Metrics Tests"
env:
- TEST_TYPE=eval
agents:
queue: "default"
+23 -1
View File
@@ -76,7 +76,7 @@ EFFECTIVE_PR=${BUILDKITE_PULL_REQUEST:-false}
if [ "$EFFECTIVE_PR" = "false" ] && [ -n "${PR_NUMBER:-}" ]; then
EFFECTIVE_PR=$PR_NUMBER
fi
MODAL_ENV="BUILDKITE_REPO=$BUILDKITE_REPO BUILDKITE_COMMIT=$BUILDKITE_COMMIT BUILDKITE_PULL_REQUEST=$EFFECTIVE_PR BUILDKITE_BRANCH=${BUILDKITE_BRANCH:-} TEST_SCOPE=${TEST_SCOPE:-} IMAGE_VERSION=$IMAGE_VERSION"
MODAL_ENV="BUILDKITE_REPO=$BUILDKITE_REPO BUILDKITE_COMMIT=$BUILDKITE_COMMIT BUILDKITE_PULL_REQUEST=$EFFECTIVE_PR BUILDKITE_BRANCH=${BUILDKITE_BRANCH:-} TEST_SCOPE=${TEST_SCOPE:-} BUILDKITE_BUILD_URL=${BUILDKITE_BUILD_URL:-} BUILDKITE_BUILD_ID=${BUILDKITE_BUILD_ID:-} BUILDKITE_JOB_ID=${BUILDKITE_JOB_ID:-} IMAGE_VERSION=$IMAGE_VERSION"
POST_RUN_HOOK=""
@@ -121,6 +121,19 @@ upload_performance_artifacts() {
fi
}
_upload_normalized_perf_results() {
local found=0
while IFS= read -r -d '' target; do
found=1
log "Found normalized performance result: $target. Uploading to Buildkite..."
buildkite-agent artifact upload "$target"
done < <(find "$LOCAL_DIR" -path "*/results/normalized_perf_*.json" -print0)
if [ "$found" -eq 0 ]; then
log "No normalized performance result artifacts found. This is expected when the rolling performance comparison did not run."
fi
}
_cleanup_modal_volume() {
log "Cleaning up perf_reports/ from Modal Volume..."
if modal volume rm hf-model-weights "perf_reports/" --recursive; then
@@ -139,6 +152,7 @@ upload_performance_artifacts() {
_download_reports || { _cleanup_local; return 1; }
_upload_dashboard
_upload_perf_summary
_upload_normalized_perf_results
_cleanup_modal_volume
_cleanup_local
}
@@ -197,10 +211,18 @@ case "$TEST_TYPE" in
log "Running unit tests..."
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_unit_test"
;;
"dreamverse_app")
log "Running DreamVerse app tests..."
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_dreamverse_app_tests"
;;
"train_framework")
log "Running fastvideo.train framework tests..."
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_train_framework_tests"
;;
"eval")
log "Running eval metric tests..."
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_eval_tests"
;;
"lora_extraction")
log "Running LoRA extraction tests..."
MODAL_COMMAND="$MODAL_ENV HF_API_KEY=$HF_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_lora_extraction_tests"
+1 -11
View File
@@ -272,7 +272,7 @@ pull_request_rules:
remove: [needs-rebase]
# ============================================================
# Auto-merge and auto-rebase
# Auto-merge
# ============================================================
- name: auto-merge when ready and all checks pass
@@ -290,16 +290,6 @@ pull_request_rules:
merge:
method: squash
- name: auto-update when ready
conditions:
- label=ready
- "#approved-reviews-by>=1"
- -conflict
- -closed
- -draft
actions:
update: {}
# ============================================================
# PR title format help
# ============================================================
+23 -7
View File
@@ -12,6 +12,18 @@ on:
tag_suffix:
required: true
type: string
image_name:
required: false
type: string
default: fastvideo-dev
build_args:
required: false
type: string
default: ''
include_latest_tags:
required: false
type: boolean
default: true
jobs:
build-and-push:
@@ -67,11 +79,14 @@ jobs:
run: |
SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7)
TAGS="type=raw,value=${{ inputs.tag_suffix }}-latest"
TAGS="${TAGS}\ntype=raw,value=${{ inputs.tag_suffix }}-sha-${SHORT_SHA}"
TAGS="type=raw,value=${{ inputs.tag_suffix }}-sha-${SHORT_SHA}"
if [[ "${{ inputs.include_latest_tags }}" == "true" ]]; then
TAGS="type=raw,value=${{ inputs.tag_suffix }}-latest\n${TAGS}"
fi
# Set Python 3.10 as the default image
if [[ "${{ inputs.python_version }}" == "3.10" ]]; then
if [[ "${{ inputs.include_latest_tags }}" == "true" && "${{ inputs.python_version }}" == "3.10" ]]; then
TAGS="${TAGS}\ntype=raw,value=latest"
fi
@@ -85,7 +100,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/fastvideo-dev
images: ghcr.io/${{ github.repository }}/${{ inputs.image_name }}
tags: ${{ steps.prepare-tags.outputs.tags }}
- name: Build and push Docker image
@@ -97,10 +112,11 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: ${{ inputs.build_args }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Success message
run: |
echo "✅ Python ${{ inputs.python_version }} image successfully built and pushed to ghcr.io/${{ github.repository }}/fastvideo-dev:${{ inputs.tag_suffix }}-latest"
echo "To run tests with this image, manually trigger the 'Run Tests' workflow."
echo "✅ Python ${{ inputs.python_version }} image successfully built and pushed to ghcr.io/${{ github.repository }}/${{ inputs.image_name }}:${{ inputs.tag_suffix }}-sha-${GITHUB_SHA::7}"
echo "To run tests with this image, manually trigger the 'Run Tests' workflow."
+3 -3
View File
@@ -125,7 +125,7 @@ jobs:
set -euo pipefail
TEST_NAME=$(echo "$COMMENT" | grep -oP '(?<=/test\s)\S+' | head -1 || true)
VALID="encoder vae transformer kernel unit ssim training lora-inference lora-training distillation self-forcing vsa vmoba performance api train-framework full fastcheck pre-commit"
VALID="encoder vae transformer kernel unit dreamverse ssim training lora-inference lora-training distillation self-forcing vsa vmoba performance api train-framework eval full fastcheck pre-commit"
if [ -z "$TEST_NAME" ] || ! echo "$VALID" | grep -qw "$TEST_NAME"; then
echo "Unknown test: '$TEST_NAME'. Valid: $VALID"
exit 1
@@ -133,13 +133,13 @@ jobs:
declare -A MAP=(
[encoder]=encoder [vae]=vae [transformer]=transformer
[kernel]=kernel_tests [unit]=unit_test
[kernel]=kernel_tests [unit]=unit_test [dreamverse]=dreamverse_app
[ssim]=ssim [training]=training
[lora-inference]=inference_lora [lora-training]=training_lora
[distillation]=distillation_dmd [self-forcing]=self_forcing
[vsa]=training_vsa [vmoba]=inference_vmoba
[performance]=performance [api]=api_server
[train-framework]=train_framework
[train-framework]=train_framework [eval]=eval
)
if [ "$TEST_NAME" = "full" ]; then
+29
View File
@@ -23,6 +23,11 @@ on:
required: false
default: false
type: boolean
dreamverse_cuda_12_9:
description: 'Build Dreamverse CUDA 12.9 backend-only and UI images'
required: false
default: false
type: boolean
permissions:
@@ -65,3 +70,27 @@ jobs:
dockerfile_path: docker/Dockerfile.python3.12.cuda12.9.1
tag_suffix: py3.12-cuda12.9.1
secrets: inherit
build-dreamverse-backend-cuda-12-9:
if: ${{ github.event.inputs.dreamverse_cuda_12_9 == 'true' }}
uses: ./.github/workflows/_template-build-image.yml
with:
python_version: '3.12'
dockerfile_path: apps/dreamverse/docker/Dockerfile
tag_suffix: dreamverse-backend-cuda12.9.1
image_name: dreamverse
build_args: BUILD_DREAMVERSE_UI=0
include_latest_tags: false
secrets: inherit
build-dreamverse-ui-cuda-12-9:
if: ${{ github.event.inputs.dreamverse_cuda_12_9 == 'true' }}
uses: ./.github/workflows/_template-build-image.yml
with:
python_version: '3.12'
dockerfile_path: apps/dreamverse/docker/Dockerfile
tag_suffix: dreamverse-ui-cuda12.9.1
image_name: dreamverse
build_args: BUILD_DREAMVERSE_UI=1
include_latest_tags: false
secrets: inherit
+36 -24
View File
@@ -53,23 +53,15 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-22.04]
python-version: ['3.10', '3.11', '3.12']
python-version: ['3.12']
torch-cuda:
# - torch-version: '2.5.1'
# cuda-version: '12.4.1'
# torch-cuda-short: 'cu124'
# - torch-version: '2.6.0'
# cuda-version: '12.6.3'
# torch-cuda-short: 'cu126'
# - torch-version: '2.7.1'
# cuda-version: '12.8.0'
# torch-cuda-short: 'cu128'
# - torch-version: '2.9.1'
# cuda-version: '12.8.0'
# torch-cuda-short: 'cu128'
- torch-version: '2.10.0'
cuda-version: '12.8.0'
torch-cuda-short: 'cu128'
# torch 2.12 dropped cu128; cu130 (CUDA 13) is the default wheel, cu126 covers older drivers.
- torch-version: '2.12.0'
cuda-version: '12.6.3'
torch-cuda-short: 'cu126'
- torch-version: '2.12.0'
cuda-version: '13.0.0'
torch-cuda-short: 'cu130'
steps:
- name: Free up disk space
@@ -104,7 +96,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install CUDA ${{ matrix.torch-cuda.cuda-version }}
uses: Jimver/cuda-toolkit@v0.2.21
uses: Jimver/cuda-toolkit@v0.2.35
id: cuda-toolkit
with:
cuda: ${{ matrix.torch-cuda.cuda-version }}
@@ -152,9 +144,27 @@ jobs:
cd fastvideo-kernel
git submodule update --init --recursive # Ensure ThunderKittens submodule is initialized
# Release builds are produced on GPU-less runners, so force-enable TK and target Hopper.
export TORCH_CUDA_ARCH_LIST="9.0a"
export CMAKE_ARGS="${CMAKE_ARGS:-} -DFASTVIDEO_KERNEL_BUILD_TK=ON -DCMAKE_CUDA_ARCHITECTURES=90a"
# Release builds run on GPU-less runners, so force-enable TK and set the arch explicitly.
# The CUDA 13 (cu130) leg additionally targets Blackwell sm_120a and builds the
# attn_qat_infer FP4 inference kernels (they need CUDA Toolkit 12.8+, so the cu126 leg
# stays Hopper-only and ships without them).
# Cap Ninja build concurrency to fit the 16 GB standard runner. CUTLASS FP4
# and ThunderKittens template TUs each need several GB; Ninja's default
# (cores+2 ≈ 6 parallel jobs) exhausts RAM and the host OOM-kills the build
# (SIGTERM / exit 143, surfaced as "runner lost communication").
if [ "${{ matrix.torch-cuda.torch-cuda-short }}" = "cu130" ]; then
export TORCH_CUDA_ARCH_LIST="9.0a;12.0a"
export CMAKE_ARGS="${CMAKE_ARGS:-} -DFASTVIDEO_KERNEL_BUILD_TK=ON -DFASTVIDEO_KERNEL_BUILD_ATTN_QAT_INFER=ON -DCMAKE_CUDA_ARCHITECTURES=90a"
# A single FP4 TU (attn_qat_infer) can use ~8-12 GB on its own, so serialize.
export CMAKE_BUILD_PARALLEL_LEVEL=1
else
export TORCH_CUDA_ARCH_LIST="9.0a"
export CMAKE_ARGS="${CMAKE_ARGS:-} -DFASTVIDEO_KERNEL_BUILD_TK=ON -DCMAKE_CUDA_ARCHITECTURES=90a"
# No FP4 here; only the two TK TUs are heavy (single-arch) and they fit
# side by side. Uncapped (cores+2=6) has built but occasionally OOMs when
# the light TUs pile on; -j4 overlaps them safely (~12-14 GB peak).
export CMAKE_BUILD_PARALLEL_LEVEL=4
fi
# Build standard wheel (no local version suffix) for PyPI
python -m build --wheel --outdir dist
@@ -183,8 +193,8 @@ jobs:
mv fixed_dist/*.whl dist/
- name: Upload wheel artifact
# Only upload if it's the "main" CUDA version we want on PyPI
# We upload all to artifacts for inspection/GH releases, but give them distinct artifact names
# Upload every matrix leg as a distinct artifact (for inspection / GitHub releases).
# The publish job below selects which CUDA build is pushed to PyPI.
uses: actions/upload-artifact@v4
with:
name: fastvideo_kernel-py${{ matrix.python-version }}-${{ matrix.torch-cuda.torch-cuda-short }}-torch${{ matrix.torch-cuda.torch-version }}
@@ -204,13 +214,15 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.12'
- name: Download PyPI wheels
# Publish only the cu130 (CUDA 13) wheel to PyPI — it includes the Blackwell FP4 kernels.
# The cu126 wheel stays available as a build artifact / GitHub-release asset.
uses: actions/download-artifact@v4
with:
path: fastvideo-kernel/dist/
pattern: 'fastvideo_kernel-py*'
pattern: 'fastvideo_kernel-py*-cu130-*'
merge-multiple: true
- name: Install uv
+10
View File
@@ -34,6 +34,8 @@ env
*.log
weights/
logs/
official_weights/
converted_weights/
# SSIM test outputs
fastvideo/tests/ssim/generated_videos/
@@ -128,3 +130,11 @@ apps/dreamverse/web/.env.production.local
.sisyphus/
openspec/
fastvideo/tests/ssim/reference_videos/**
# Editor logs and local Python version pins (accidentally committed)
*.nvimlog
.nvimlog
.python-version
# Gradio runtime/flagging artifacts
.gradio/
-1
View File
@@ -1 +0,0 @@
WRN 2026-03-26T13:46:33.469 ?.19646 server_start:193: Failed to start server: operation not permitted: /var/folders/z_/h_6myyk14d1b7z87z3vy4mjh0000gn/T/nvim.dsynkd/iSe0el/nvim.19646.0
-1
View File
@@ -1 +0,0 @@
3.12
+18 -2
View File
@@ -9,8 +9,9 @@
**FastVideo is a unified post-training and real-time inference framework for accelerated video generation.**
## NEWS
- `2026/03/17`: Release Live demo: [Into the Dreamverse: Vibe Directing in FastVideo](https://dreamverse.fastvideo.org/), check out the [Blog](https://haoailab.com/blogs/dreamverse/).
- `2026/03/13`: Release Live demo: [Create a 5s 1080p Video in 4.5s with FastVideo on a Single GPU](https://1080p.fastvideo.org/), check out the [Blog](https://haoailab.com/blogs/fastvideo_realtime_1080p/).
- `2026/06/23`: Release FastWan-QAD: 5s of Video generated in 1.8s E2E. [FastWan-QAD models](https://huggingface.co/FastVideo/FastWan-QAD-FP8-1.3B), check out the [Blog](https://haoailab.com/blogs/fastwan-qad/).
- `2026/03/17`: Release demo: [Into the Dreamverse: Vibe Directing in FastVideo](), check out the [Blog](https://haoailab.com/blogs/dreamverse/).
- `2026/03/13`: Release demo: [Create a 5s 1080p Video in 4.5s with FastVideo on a Single GPU](), check out the [Blog](https://haoailab.com/blogs/fastvideo_realtime_1080p/).
- `2025/11/19`: Release [CausalWan2.2 I2V A14B Preview](https://huggingface.co/FastVideo/CausalWan2.2-I2V-A14B-Preview-Diffusers) models, [Blog](https://hao-ai-lab.github.io/blogs/fastvideo_causalwan_preview/) and [Inference Code!](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_self_forcing_causal_wan2_2_i2v.py).
- `2025/08/04`: Release [FastWan](https://hao-ai-lab.github.io/FastVideo/distillation/dmd) models and [Sparse-Distillation](https://hao-ai-lab.github.io/blogs/fastvideo_post_training/).
@@ -42,6 +43,8 @@ FastVideo has the following features:
- Support H100, A100, 4090
- Support Linux, Windows, MacOS
- See this [page](https://hao-ai-lab.github.io/FastVideo/inference/support_matrix/) for full list of supported models, hardware assumptions, and optimization compatibility.
- Realtime video generation & editing
- [Dreamverse](apps/dreamverse/README.md): stream and "vibe direct" video in realtime ([live demo](https://dreamverse.fastvideo.org/)), deployable on local GPU, a self-hosted B200 server, Docker, or serverless Modal
## Getting Started
@@ -69,6 +72,19 @@ See below for recipes and datasets:
| [FastWan2.1-T2V-1.3B](https://huggingface.co/FastVideo/FastWan2.1-T2V-1.3B-Diffusers) | [Recipe](https://github.com/hao-ai-lab/FastVideo/tree/main/examples/distill/Wan2.1-T2V/Wan-Syn-Data-480P) | [FastVideo Synthetic Wan2.1 480P](https://huggingface.co/datasets/FastVideo/Wan-Syn_77x448x832_600k) |
| [FastWan2.2-TI2V-5B](https://huggingface.co/FastVideo/FastWan2.2-TI2V-5B-Diffusers) | [Recipe](https://github.com/hao-ai-lab/FastVideo/tree/main/examples/distill/Wan2.2-TI2V-5B-Diffusers/Data-free) | [FastVideo Synthetic Wan2.2 720P](https://huggingface.co/datasets/FastVideo/Wan2.2-Syn-121x704x1280_32k) |
## Dreamverse — Realtime Video Generation & Editing
[Dreamverse](apps/dreamverse/README.md) is FastVideo's realtime video generation
and editing platform — "vibe directing" a video as it streams. It lives in the
monorepo under [`apps/dreamverse/`](apps/dreamverse/) and ships its own backend
(`dreamverse-server`) plus a web UI.
Try the [live demo](https://dreamverse.fastvideo.org/), read the
[blog](https://haoailab.com/blogs/dreamverse/), or run it yourself. Dreamverse
deploys on a local GPU, a self-hosted B200 server over SSH, Docker, or
serverless [Modal](apps/dreamverse/scripts/modal/README.md) — see the
[Dreamverse README](apps/dreamverse/README.md).
## Inference
### Generating Your First Video
+68 -11
View File
@@ -2,6 +2,8 @@
Dreamverse is the FastVideo realtime video generation & editing platform. It lives in this monorepo under `apps/dreamverse/`.
**Deploy on:** [local GPU](#quick-start-local-gpu) · [self-hosted B200 (SSH)](#server-b200-deployment-ssh) · [Docker](docker/README.md) · [Modal](scripts/modal/README.md)
## Install Dreamverse
You can install Dreamverse using one of the methods below.
@@ -43,13 +45,36 @@ See `apps/dreamverse/docker/README.md` for Docker build and run option details.
## Optional: Building FFmpeg For Better Performance
For full streaming performance in a non-Docker install, build a custom FFmpeg
binary:
binary from a FastVideo source checkout. The command below is repo-relative,
so run it from the repository root:
```bash
bash apps/dreamverse/scripts/install_native_ffmpeg.sh
```
This builds and installs into `~/opt/ffmpeg-native/` and writes
The installer supports Linux `x86_64` and `aarch64`. It prefers conda-forge
triplet compilers when those commands are on `PATH`, otherwise it falls back to
system `gcc`/`g++` (plain venv). On `x86_64`, x264's hand-tuned SIMD also
requires `nasm`; install via whichever path fits your host:
```bash
sudo apt install nasm # Debian/Ubuntu
conda install -c conda-forge nasm # inside an active conda env
```
No sudo and no conda? Build `nasm` from source (~30s, installs into `$HOME`):
```bash
(
mkdir -p "$HOME/src" "$HOME/opt" && cd "$HOME/src"
curl -fsSL -O https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/nasm-2.16.03.tar.gz
tar -xf nasm-2.16.03.tar.gz && cd nasm-2.16.03
./configure --prefix="$HOME/opt/nasm" && make -j"$(nproc)" && make install
)
export PATH="$HOME/opt/nasm/bin:$PATH" # add to ~/.bashrc to persist
```
The installer writes to `~/opt/ffmpeg-native/` and emits
`apps/dreamverse/scripts/ffmpeg-env.sh`. Source it before starting the backend
so Dreamverse uses the custom FFmpeg binary:
@@ -59,8 +84,7 @@ dreamverse-server
```
Docker images already run this FFmpeg build during image creation and source the
generated environment file at container startup. The installer supports Linux
`x86_64` and `aarch64`.
generated environment file at container startup.
## Launch Dreamverse
@@ -71,17 +95,24 @@ dreamverse-server --port 8009
dreamverse-mock-server --port 8009
```
> **Expect a slow first boot.** With `torch.compile` and startup warmup enabled
> (the default), the backend compiles the segment 1 and segment 2 inference
> paths before it reports ready — this can take **tens of minutes on a cold
> cache**, regardless of how you deploy (local, server, Docker, or Modal).
> `/healthz` responds as soon as the process is up; `/readyz` stays `503` until
> warmup finishes. For a faster, uncompiled startup while testing, set
> `FASTVIDEO_ENABLE_STARTUP_WARMUP=0` before starting the backend.
## Frontend Setup
Install the web dependencies once from the FastVideo checkout:
```bash
cd apps/dreamverse/web
pnpm install --frozen-lockfile
npm ci
```
The frontend package also has an npm lockfile, but the bundled launch scripts
use `pnpm`.
The frontend package uses `package-lock.json`; use npm for installs and scripts.
## Quick Start: Local GPU
@@ -137,11 +168,37 @@ Start the frontend:
```bash
cd apps/dreamverse/web
BACKEND_HOST=localhost BACKEND_PORT=8009 pnpm run dev
BACKEND_HOST=localhost BACKEND_PORT=8009 npm run dev
```
Open `http://localhost:5299`.
## Server B200 deployment (SSH)
Deploying on a remote GPU host (for example a B200 box) is a local install run
over SSH, plus a few server-specific concerns. Two paths:
### Option A: Native (source install)
SSH in, then follow [Install → From source](#method-2-from-source) and
(recommended) [Building FFmpeg](#optional-building-ffmpeg-for-better-performance),
then start the backend as in [Quick Start: Local GPU](#quick-start-local-gpu).
For a remote host, a few things differ from localhost:
- Bind all interfaces: `dreamverse-server --host 0.0.0.0 --port 8009`.
- Point the frontend/client at the host: `BACKEND_HOST=<b200-host> BACKEND_PORT=8009 npm run dev`.
- Keep the backend alive across SSH sessions (`tmux` / `systemd` / `nohup`).
- Expose / firewall port `8009`, or front it with a reverse proxy + auth.
### Option B: Docker (on the server)
SSH in, then follow [Install → Using Docker](#method-3-using-docker) and the run
steps in [`docker/README.md`](docker/README.md):
```bash
CEREBRAS_API_KEY="<key>" GROQ_API_KEY="<key>" apps/dreamverse/docker/docker_run.sh
```
## Quick Start: Mock Backend (For UI development)
The mock server emulates the Dreamverse backend protocol and streams a
@@ -173,14 +230,14 @@ Run the frontend tests:
```bash
cd apps/dreamverse/web
pnpm test
npm test
```
Run the frontend e2e tests:
```bash
cd apps/dreamverse/web
pnpm run e2e
npm run e2e
```
## Troubleshooting
@@ -216,7 +273,7 @@ Only one GPU is used
Frontend cannot connect to backend
- confirm the backend is running on `8009`; if not, point the frontend at it
with `BACKEND_HOST=<host> BACKEND_PORT=<port> pnpm run dev`
with `BACKEND_HOST=<host> BACKEND_PORT=<port> npm run dev`
- confirm `http://localhost:8009/healthz` responds before starting the frontend
- confirm `http://localhost:8009/readyz` returns `200` before clicking Generate
- use `apps/dreamverse/scripts/smoke_local.sh` for a repeatable local startup
-40
View File
@@ -1,40 +0,0 @@
.git/
**/.git/
.venv/
**/.venv/
**/__pycache__/
**/*.pyc
**/*.pyo
**/*.egg-info/
.pytest_cache/
**/.pytest_cache/
.mypy_cache/
.ruff_cache/
.cache/
apps/dreamverse/web/node_modules/
apps/dreamverse/web/.next/
apps/dreamverse/web/out/
apps/dreamverse/web/dist/
apps/dreamverse/web/test-results/
apps/dreamverse/web/playwright-report/
apps/dreamverse/outputs/
apps/dreamverse/dreamverse/outputs/
apps/dreamverse/dreamverse/prompts.local/
apps/dreamverse/logs/
outputs/
logs/
slurm-logs/
wandb/
.env
.env.*
**/prompts.local/
.codex/
.agents/exploration/
.vscode/
.idea/
*.log
*.tmp
*.pdf
+13
View File
@@ -3,6 +3,7 @@ ARG CUDA_TAG=12.9.1-cudnn-devel-ubuntu22.04
FROM nvidia/cuda:${CUDA_TAG}
ARG BUILD_FASTVIDEO_KERNEL_FROM_SOURCE=0
ARG BUILD_DREAMVERSE_UI=0
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
@@ -50,6 +51,18 @@ RUN if [[ "${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE}" == "1" ]]; then \
echo "Skipping source fastvideo-kernel build; using installed fastvideo-kernel package."; \
fi
RUN if [[ "${BUILD_DREAMVERSE_UI}" == "1" ]]; then \
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/* \
&& cd /opt/FastVideo/apps/dreamverse/web \
&& npm ci --ignore-scripts \
&& NEXT_OUTPUT_EXPORT=1 npm run build \
&& rm -rf node_modules .next; \
else \
echo "Skipping Dreamverse UI build; Building backend-only image."; \
fi
# The monorepo ffmpeg installer force-selects conda compiler triplets for
# local dev shells. Inside this image we explicitly opt into the system
# gcc/g++ toolchain.
@@ -2,6 +2,7 @@
**/.git/
.venv/
**/.venv/
.*dreamverse*/
**/__pycache__/
**/*.pyc
**/*.pyo
@@ -24,6 +25,9 @@ apps/dreamverse/dreamverse/outputs/
apps/dreamverse/dreamverse/prompts.local/
apps/dreamverse/logs/
outputs/
outputs_video/
quality_check_outputs/
data/
logs/
slurm-logs/
wandb/
@@ -32,7 +36,9 @@ wandb/
.env.*
**/prompts.local/
.codex/
.agents/exploration/
.agents/
.opencode/
.agent_tmp/
.vscode/
.idea/
*.log
+15 -5
View File
@@ -1,9 +1,9 @@
# Dreamverse Docker Image
This folder contains the backend-only Docker image for Dreamverse inside the
FastVideo monorepo. Build commands use the FastVideo repository root as the
Docker context, so run the helper scripts from this folder or from any path in
the checkout.
This folder contains the Docker image for Dreamverse inside the FastVideo
monorepo. Build commands use the FastVideo repository root as the Docker
context, so run the helper scripts from this folder or from any path in the
checkout.
## Build
@@ -17,12 +17,22 @@ The image defaults to `dreamverse:dev`. Override it with:
DREAMVERSE_IMAGE=dreamverse:local apps/dreamverse/docker/docker_build.sh
```
Backend-only remains the default image. To include the static Dreamverse UI
served by the backend, set `BUILD_DREAMVERSE_UI=1` and choose a specific image
tag:
```bash
BUILD_DREAMVERSE_UI=1 DREAMVERSE_IMAGE=<image-tag> apps/dreamverse/docker/docker_build.sh
```
Prefer SHA-specific tags for deployable images; avoid `latest`.
The Dockerfile builds a CUDA 12.9.1 image, installs FastVideo from this
checkout with the `dreamverse` extra, installs the FA4
flash-attention fork, builds native FFmpeg, and installs FlashInfer for NVFP4
quantization.
FastVideo's pinned `fastvideo-kernel==0.2.6` package is installed by default.
FastVideo's pinned `fastvideo-kernel==0.3.0` package is installed by default.
To rebuild `fastvideo-kernel` from this checkout during the image build, set:
```bash
+32 -1
View File
@@ -4,13 +4,44 @@ set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../../.." && pwd)"
IMAGE="${DREAMVERSE_IMAGE:-dreamverse:dev}"
ROOT_DOCKERIGNORE="${REPO_ROOT}/.dockerignore"
DOCKERFILE_DOCKERIGNORE="${SCRIPT_DIR}/Dockerfile.dockerignore"
CREATED_ROOT_DOCKERIGNORE_SYMLINK=0
cleanup_root_dockerignore_symlink() {
if [[ "${CREATED_ROOT_DOCKERIGNORE_SYMLINK}" == "1" && -L "${ROOT_DOCKERIGNORE}" ]] && \
[[ "$(readlink "${ROOT_DOCKERIGNORE}")" == "${DOCKERFILE_DOCKERIGNORE}" ]]; then
rm -- "${ROOT_DOCKERIGNORE}"
fi
}
trap cleanup_root_dockerignore_symlink EXIT INT TERM
if [[ -z "${DOCKER_BUILDKIT:-}" ]] && docker buildx version >/dev/null 2>&1; then
export DOCKER_BUILDKIT=1
fi
build_args=()
[[ -n "${CUDA_TAG:-}" ]] && build_args+=(--build-arg "CUDA_TAG=${CUDA_TAG}")
[[ -n "${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE:-}" ]] && \
build_args+=(--build-arg "BUILD_FASTVIDEO_KERNEL_FROM_SOURCE=${BUILD_FASTVIDEO_KERNEL_FROM_SOURCE}")
build_args+=(--build-arg "BUILD_DREAMVERSE_UI=${BUILD_DREAMVERSE_UI:-0}")
exec docker build \
if [[ "${DOCKER_BUILDKIT:-}" == "1" ]]; then
if [[ -L "${ROOT_DOCKERIGNORE}" ]] && \
[[ "$(readlink "${ROOT_DOCKERIGNORE}")" == "${DOCKERFILE_DOCKERIGNORE}" ]]; then
rm -- "${ROOT_DOCKERIGNORE}"
printf 'Removed stale temporary root .dockerignore symlink: %s\n' "${ROOT_DOCKERIGNORE}"
fi
elif [[ -e "${ROOT_DOCKERIGNORE}" || -L "${ROOT_DOCKERIGNORE}" ]]; then
printf 'Using existing root .dockerignore: %s\n' "${ROOT_DOCKERIGNORE}"
else
ln -s -- "${DOCKERFILE_DOCKERIGNORE}" "${ROOT_DOCKERIGNORE}"
CREATED_ROOT_DOCKERIGNORE_SYMLINK=1
printf 'Created temporary root .dockerignore symlink for legacy Docker builder: %s -> %s\n' \
"${ROOT_DOCKERIGNORE}" "${DOCKERFILE_DOCKERIGNORE}"
fi
docker build \
-f "${SCRIPT_DIR}/Dockerfile" \
-t "${IMAGE}" \
"${build_args[@]}" \
+101 -3
View File
@@ -4,6 +4,7 @@ from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[1]
_SERVER_ROOT = Path(__file__).resolve().parent
_FASTVIDEO_DREAMVERSE_HOME = os.environ.get("FASTVIDEO_DREAMVERSE_HOME")
_FASTVIDEO_DREAMVERSE_FRONTEND_ROOT = os.environ.get("FASTVIDEO_DREAMVERSE_FRONTEND_ROOT")
_XDG_STATE_HOME = os.environ.get("XDG_STATE_HOME")
_DEFAULT_STATE_ROOT = (Path(_FASTVIDEO_DREAMVERSE_HOME) if _FASTVIDEO_DREAMVERSE_HOME else
(Path(_XDG_STATE_HOME) if _XDG_STATE_HOME else Path.home() / ".local/state") /
@@ -16,18 +17,39 @@ _APP_ROOT = _REPO_ROOT
def _resolve_frontend_root() -> Path:
for candidate in (
Path(_FASTVIDEO_DREAMVERSE_FRONTEND_ROOT) if _FASTVIDEO_DREAMVERSE_FRONTEND_ROOT else None,
_APP_ROOT / "web",
_APP_ROOT / "prod-ui",
):
if candidate.is_dir():
if candidate is not None and candidate.is_dir():
return candidate
if _FASTVIDEO_DREAMVERSE_FRONTEND_ROOT:
return Path(_FASTVIDEO_DREAMVERSE_FRONTEND_ROOT)
return _APP_ROOT / "web"
def _resolve_frontend_static_dir_candidates() -> tuple[str, ...]:
roots = (
FRONTEND_ROOT,
Path.cwd() / "apps/dreamverse/web",
Path.cwd() / "web",
Path("/opt/FastVideo/apps/dreamverse/web"),
)
candidates: list[str] = []
seen: set[Path] = set()
for root in roots:
resolved_root = root.resolve(strict=False)
if resolved_root in seen:
continue
seen.add(resolved_root)
candidates.extend(str(resolved_root / dirname) for dirname in ("out", "dist"))
return tuple(candidates)
FRONTEND_ROOT = _resolve_frontend_root()
_CLIENT_PROMPTS_ROOT = FRONTEND_ROOT / "prompts"
_CLIENT_PROMPTS_LOCAL_ROOT = FRONTEND_ROOT / "prompts.local"
FRONTEND_STATIC_DIR_CANDIDATES = tuple(str(FRONTEND_ROOT / dirname) for dirname in ("out", "dist"))
FRONTEND_STATIC_DIR_CANDIDATES = _resolve_frontend_static_dir_candidates()
# Model registry
MODEL_REGISTRY = {
@@ -35,18 +57,24 @@ MODEL_REGISTRY = {
"name": "FastLTX2",
"model_path": "FastVideo/LTX2-Distilled-Diffusers",
"config_model_path": "FastVideo/LTX2-Distilled-Diffusers",
"lora_repo": "FastVideo/LTX2-OmniNFT-LoRA",
},
"fast-ltx23": {
"name": "FastLTX23",
"model_path": "FastVideo/LTX-2.3-Distilled-Diffusers",
"config_model_path": "FastVideo/LTX-2.3-Distilled-Diffusers",
"lora_repo": "FastVideo/LTX-2.3-OmniNFT-LoRA",
},
}
DEFAULT_MODEL_ID = "fast-ltx2"
ACTIVE_MODEL_ID = (os.getenv("DREAMVERSE_MODEL_ID", "").strip() or DEFAULT_MODEL_ID)
if ACTIVE_MODEL_ID not in MODEL_REGISTRY:
ACTIVE_MODEL_ID = DEFAULT_MODEL_ID
# Active model configuration
MODEL_CONFIG = MODEL_REGISTRY[DEFAULT_MODEL_ID]
MODEL_CONFIG = MODEL_REGISTRY[ACTIVE_MODEL_ID]
# Generation limits
SESSION_TIMEOUT_SECONDS = 300
@@ -142,6 +170,76 @@ def _optional_env(*names: str) -> str | None:
DEVTOOLS_ENABLED = _env_bool("FASTVIDEO_ENABLE_DEVTOOLS", False)
PROMPT_SAFETY_ENABLED = _env_bool("FASTVIDEO_ENABLE_PROMPT_SAFETY", False)
DREAMVERSE_MAX_AUTOTUNE = _env_bool("DREAMVERSE_MAX_AUTOTUNE", True)
DREAMVERSE_SP_SIZE = max(1, _env_int("DREAMVERSE_SP_SIZE", 1))
DREAMVERSE_MODEL_PATH = (os.getenv("DREAMVERSE_MODEL_PATH", "").strip() or None)
if DREAMVERSE_MODEL_PATH:
MODEL_CONFIG = {
**MODEL_CONFIG,
"model_path": DREAMVERSE_MODEL_PATH,
"config_model_path": DREAMVERSE_MODEL_PATH,
}
AVAILABLE_LORAS = {
"pixar": {
"repo": "vrgamedevgirl84/LTX_2.3_Pixar_Toon_Style_LoRa",
"trigger": "P1x4r",
"model": "fast-ltx23",
"position": "prepend",
"label": "Pixar Toon",
},
"transition": {
"repo": "valiantcat/LTX-2.3-Transition-LORA",
"trigger": "zhuanchang",
"model": "fast-ltx23",
"position": "append",
"label": "Transition",
},
}
def _active_model_key() -> str:
return ACTIVE_MODEL_ID
def _available_styles_for_active_model() -> list[str]:
active = _active_model_key()
return [k for k, v in AVAILABLE_LORAS.items() if v.get("model") == active]
def _resolve_lora_spec(spec: str) -> str | None:
spec = (spec or "").strip()
if not spec:
return None
if spec.lower() == "omninft":
return MODEL_CONFIG.get("lora_repo")
if spec.lower() in AVAILABLE_LORAS:
return AVAILABLE_LORAS[spec.lower()]["repo"]
return spec
def _parse_lora_stack(raw: str) -> list[tuple[str, float]]:
out: list[tuple[str, float]] = []
for item in (raw or "").split(","):
item = item.strip()
if not item:
continue
spec, _, stren = item.partition("@")
resolved = _resolve_lora_spec(spec)
if resolved:
try:
strength = float(stren) if stren.strip() else 1.0
except ValueError:
strength = 1.0
out.append((resolved, strength))
return out
DREAMVERSE_LORA_PATH = _resolve_lora_spec(os.getenv("DREAMVERSE_LORA_PATH", ""))
DREAMVERSE_LORA_NICKNAME = (os.getenv("DREAMVERSE_LORA_NICKNAME", "omninft").strip() or "omninft")
DREAMVERSE_LORA_STRENGTH = _env_float("DREAMVERSE_LORA_STRENGTH", 1.0)
DREAMVERSE_LORA_STACK = _parse_lora_stack(os.getenv("DREAMVERSE_LORA_STACK", ""))
def _resolve_devtools_paths(
+82 -2
View File
@@ -13,6 +13,7 @@ from multiprocessing import Process, Queue
from dreamverse.config import (
DEFAULT_MODEL_ID,
DREAMVERSE_SP_SIZE,
MODEL_REGISTRY,
STARTUP_WARMUP_ENABLED,
STARTUP_WARMUP_PROMPT,
@@ -33,6 +34,8 @@ from dreamverse.worker_ipc import (
InitAck,
JoinAck,
LeaveAck,
LoraAck,
LoraStackPayload,
MediaChunk,
MediaComplete,
MediaInit,
@@ -80,6 +83,7 @@ class CommandType(Enum):
USER_STEP = "user_step"
USER_LEAVE = "user_leave"
RELOAD_MODEL = "reload_model"
APPLY_LORA = "apply_lora"
@dataclass
@@ -282,6 +286,25 @@ def gpu_worker_process(
message=str(e),
))
elif cmd.type == CommandType.APPLY_LORA:
try:
assert isinstance(cmd.payload, LoraStackPayload), (f"APPLY_LORA requires LoraStackPayload, "
f"got {type(cmd.payload).__name__}")
trigger, position = worker.apply_lora_stack(cmd.payload.stack)
response_queue.put(
LoraAck(
user_id=cmd.user_id,
style_trigger=trigger,
style_trigger_position=position,
))
except Exception as e:
print(f"[GPU {gpu_id}] Apply LoRA error: {e}")
traceback.print_exc()
response_queue.put(WorkerError(
user_id=cmd.user_id,
message=str(e),
))
return True # Continue loop
if first_cmd is not None:
@@ -359,6 +382,25 @@ def gpu_worker_process(
message=str(e),
))
elif cmd.type == CommandType.APPLY_LORA:
try:
assert isinstance(cmd.payload, LoraStackPayload), (f"APPLY_LORA requires LoraStackPayload, "
f"got {type(cmd.payload).__name__}")
trigger, position = worker.apply_lora_stack(cmd.payload.stack)
response_queue.put(
LoraAck(
user_id=cmd.user_id,
style_trigger=trigger,
style_trigger_position=position,
))
except Exception as e:
print(f"[GPU {gpu_id}] Apply LoRA error: {e}")
traceback.print_exc()
response_queue.put(WorkerError(
user_id=cmd.user_id,
message=str(e),
))
elif cmd.type in (CommandType.USER_JOIN, CommandType.USER_STEP, CommandType.USER_LEAVE):
event_loop(first_cmd=cmd)
break
@@ -729,6 +771,25 @@ class GPUSlot:
raise RuntimeError(f"Unexpected step response for {user_id[:8]}: "
f"{type(response).__name__}")
async def apply_lora_stack(
self,
stack: list[tuple[str, float]],
) -> LoraAck:
"""Re-apply a runtime LoRA stack on this GPU's worker."""
self._active = True
user_id = "__lora__"
payload = LoraStackPayload(stack=stack)
response = await self._send_command_tagged(Command(CommandType.APPLY_LORA, payload=payload, user_id=user_id),
timeout=120.0)
match response:
case LoraAck() as ack:
return ack
case WorkerError(message=msg):
raise RuntimeError(f"Apply LoRA failed on GPU {self.gpu_id}: {msg}")
case _:
raise RuntimeError(f"Unexpected LoRA response on GPU {self.gpu_id}: "
f"{type(response).__name__}")
async def leave_user(self, user_id: str) -> None:
"""Remove a user from this GPU."""
try:
@@ -785,8 +846,16 @@ class GPUPool:
"""Manages multiple GPU worker subprocesses."""
def __init__(self, gpu_ids: list[int]):
self.gpu_ids = gpu_ids
self.slots: dict[int, GPUSlot] = {gpu_id: GPUSlot(gpu_id, str(gpu_id)) for gpu_id in gpu_ids}
sp_size = DREAMVERSE_SP_SIZE
groups = [gpu_ids[i:i + sp_size] for i in range(0, len(gpu_ids), sp_size)]
groups = [g for g in groups if len(g) == sp_size]
if not groups:
raise RuntimeError(f"Not enough GPUs for DREAMVERSE_SP_SIZE={sp_size}: available={gpu_ids}")
if sp_size > 1:
print(f"[INFO] Sequence-parallel slots (sp_size={sp_size}): " + ", ".join("{" + ",".join(map(str, g)) + "}"
for g in groups))
self.gpu_ids = [g[0] for g in groups]
self.slots: dict[int, GPUSlot] = {g[0]: GPUSlot(g[0], ",".join(str(x) for x in g)) for g in groups}
self.waiting_list: list[tuple[str, asyncio.Event, WebSocket]] = []
self.client_gpu_map: dict[str, int] = {}
self._pool_lock = asyncio.Lock()
@@ -903,6 +972,17 @@ class GPUPool:
except Exception:
pass
async def apply_lora_stack(
self,
stack: list[tuple[str, float]],
) -> dict[int, str | None]:
"""Re-apply a runtime LoRA stack across all ready GPU workers."""
ready_slots = [slot for slot in self.slots.values() if slot.ready]
if not ready_slots:
raise RuntimeError("No ready GPU workers to apply LoRA stack.")
results = await asyncio.gather(*(slot.apply_lora_stack(stack) for slot in ready_slots))
return {slot.gpu_id: ack.style_trigger for slot, ack in zip(ready_slots, results, strict=False)}
async def shutdown(self):
"""Shutdown all GPU workers."""
print("Shutting down GPU pool...")
+82 -1
View File
@@ -6,18 +6,22 @@ import os
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, WebSocket
from fastapi import FastAPI, HTTPException, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from fastvideo.entrypoints.streaming import build_health_router
from dreamverse.gpu_pool import GPUPool, get_available_gpus
from dreamverse.session_logger import SessionEventLogger
from dreamverse.config import (
AVAILABLE_LORAS,
DEVTOOLS_ENABLED,
FRONTEND_STATIC_DIR_CANDIDATES,
PROMPT_SAFETY_ENABLED,
SESSION_LOG_ROOT,
_available_styles_for_active_model,
_resolve_lora_spec,
)
from dreamverse.prompt_enhancer import PromptEnhancer
from dreamverse.prompt_safety import PromptSafetyFilter
@@ -104,6 +108,83 @@ async def websocket_endpoint(websocket: WebSocket):
await controller.run()
class LoraRequest(BaseModel):
strength: float = 1.0
styles: dict[str, float] = {}
style: str = ""
@app.get("/lora/options")
async def lora_options() -> dict:
if not DEVTOOLS_ENABLED:
raise HTTPException(status_code=404, detail="Not found")
styles = _available_styles_for_active_model()
has_base_lora = _resolve_lora_spec("omninft") is not None
labels = {"none": "None"}
labels.update({k: AVAILABLE_LORAS[k].get("label", k) for k in styles})
return {
"styles": ["none", *styles],
"labels": labels,
"has_base_lora": has_base_lora,
}
@app.post("/lora")
async def apply_lora(request: LoraRequest) -> dict:
if not DEVTOOLS_ENABLED:
raise HTTPException(status_code=404, detail="Not found")
if runtime.gpu_pool is None:
raise HTTPException(status_code=503, detail="GPU pool not ready")
strength = max(0.0, min(1.0, float(request.strength)))
allowed_styles = _available_styles_for_active_model()
requested = dict(request.styles) if request.styles else {}
if not requested and request.style and request.style.strip().lower() != "none":
requested = {request.style: 1.0}
styles_map: dict[str, float] = {}
for name, intensity in requested.items():
key = str(name).strip().lower()
if key in ("", "none"):
continue
if key not in allowed_styles:
raise HTTPException(status_code=400, detail=f"Unknown style for active model: {key}")
value = max(0.0, min(1.0, float(intensity)))
if value <= 0.0:
continue
styles_map[key] = value
stack: list[tuple[str, float]] = []
if _resolve_lora_spec("omninft") is not None:
stack.append(("omninft", strength))
for key, intensity in styles_map.items():
stack.append((key, intensity))
if not stack:
raise HTTPException(status_code=400,
detail="Active model has no OmniNFT LoRA and no style selected; nothing to apply.")
try:
triggers = await runtime.gpu_pool.apply_lora_stack(stack)
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
return {
"applied": True,
"strength": strength,
"styles": styles_map,
"triggers": {
key: AVAILABLE_LORAS[key]["trigger"]
for key in styles_map
},
"gpus": {
str(gpu_id): trigger
for gpu_id, trigger in triggers.items()
},
}
# Serve an exported frontend bundle when present.
for static_dir in FRONTEND_STATIC_DIR_CANDIDATES:
if os.path.isdir(static_dir):
+62 -2
View File
@@ -41,7 +41,7 @@ MOCK_FPS = 24
MOCK_DURATION_SECONDS = 5.0
MOCK_STREAM_CHUNK_SIZE_BYTES = 256 * 1024
MOCK_STREAM_CHUNK_DELAY_MS = 15
MOCK_AV_MIME = 'video/mp4; codecs="avc1.42E01E"'
MOCK_AV_MIME = 'video/mp4; codecs="avc1.42E01E,mp4a.40.2"'
FFMPEG_BIN = shutil.which(os.getenv("FASTVIDEO_FFMPEG_BIN", "ffmpeg"))
MOCK_SEGMENT_BYTES: bytes | None = None
@@ -75,9 +75,14 @@ def _build_mock_segment_bytes() -> bytes:
"lavfi",
"-i",
f"testsrc2=size={MOCK_FRAME_WIDTH}x{MOCK_FRAME_HEIGHT}:rate={MOCK_FPS}",
# Silent stereo AAC track. We don't need audible content — the FE's
# audio-decode coverage only needs a real AAC track present
"-f",
"lavfi",
"-i",
"anullsrc=channel_layout=stereo:sample_rate=48000",
"-t",
f"{MOCK_DURATION_SECONDS}",
"-an",
"-c:v",
"libx264",
"-preset",
@@ -90,6 +95,12 @@ def _build_mock_segment_bytes() -> bytes:
"baseline",
"-level",
"3.0",
"-c:a",
"aac",
"-b:a",
"128k",
"-ar",
"48000",
"-movflags",
"+empty_moov+default_base_moof+frag_keyframe",
"-frag_duration",
@@ -183,6 +194,55 @@ async def status():
"queue_size": 0,
"available_gpus": 1,
"total_gpus": 1,
"warmup_enabled": False,
"warmup_successful_gpus": 1,
"warmup_failed_gpus": 0,
"gpu_status": {
0: {
"ready": True,
"available": True,
"client_count": 0,
"current_model_id": "mock-ltx2",
"process_alive": True,
"warmup_enabled": False,
"warmup_success": True,
"warmup_error": None,
"warmup_timings": {},
},
},
}
@app.get("/prompt-system-config")
async def prompt_system_config():
return {
"next_segment_system_prompt": "Mock next segment prompt.",
"auto_extension_system_prompt": "Mock auto extension prompt.",
"rewrite_window_system_prompt": "Mock rewrite window prompt.",
"rewrite_user_system_prompt": "Mock rewrite user prompt.",
"rewrite_model": "mock-rewrite-model",
"rewrite_temperature": 0.0,
}
@app.get("/curated-presets")
async def curated_presets():
presets = [
{
"id": "mock_story",
"label": "Mock Story",
"description": "CI-safe mock story preset.",
"segment_prompts": [
"A tiny robot opens a glowing door.",
"The robot waves at a friendly moon.",
],
},
]
return {
"presets": presets,
"count": len(presets),
"file_path": None,
"fallback_file_path": None,
}
@@ -124,12 +124,21 @@ def test_config_uses_local_overlay_paths_when_devtools_enabled(monkeypatch, tmp_
assert module.CURATED_PRESETS_FALLBACK_FILE_PATH.endswith(
"apps/dreamverse/web/prompts/selected_ltx2_continuation_story_presets.json"
)
assert module.FRONTEND_STATIC_DIR_CANDIDATES == (
assert module.FRONTEND_STATIC_DIR_CANDIDATES[:2] == (
str(module.FRONTEND_ROOT / "out"),
str(module.FRONTEND_ROOT / "dist"),
)
def test_config_includes_docker_checkout_static_frontend_candidates(monkeypatch):
_set_required_prompt_keys(monkeypatch)
module = _load_config_module()
assert "/opt/FastVideo/apps/dreamverse/web/out" in module.FRONTEND_STATIC_DIR_CANDIDATES
assert "/opt/FastVideo/apps/dreamverse/web/dist" in module.FRONTEND_STATIC_DIR_CANDIDATES
def test_config_enables_prompt_safety_when_requested(monkeypatch):
_set_required_prompt_keys(monkeypatch)
monkeypatch.setenv("FASTVIDEO_ENABLE_PROMPT_SAFETY", "true")
@@ -171,3 +171,69 @@ def test_mock_server_cli_updates_latency(monkeypatch):
assert mock_server.LATENCY_MS == 321
finally:
mock_server.LATENCY_MS = old_latency_ms
def _lora_client(monkeypatch, captured):
server_main = _import_server_main(monkeypatch)
monkeypatch.setattr(server_main, "DEVTOOLS_ENABLED", True)
monkeypatch.setattr(server_main, "_available_styles_for_active_model", lambda: ["pixar", "transition"])
monkeypatch.setattr(
server_main,
"_resolve_lora_spec",
lambda spec: "FastVideo/OmniNFT" if str(spec).strip().lower() == "omninft" else spec,
)
class _FakePool:
async def apply_lora_stack(self, stack):
captured["stack"] = list(stack)
return {0: "trigger"}
server_main.runtime.gpu_pool = _FakePool()
return TestClient(server_main.app)
def test_apply_lora_stacks_multiple_styles_with_intensities(monkeypatch):
captured: dict = {}
client = _lora_client(monkeypatch, captured)
response = client.post("/lora", json={"strength": 0.8, "styles": {"pixar": 0.45, "transition": 0.5}})
assert response.status_code == 200
body = response.json()
assert body["applied"] is True
assert body["styles"] == {"pixar": 0.45, "transition": 0.5}
assert captured["stack"] == [("omninft", 0.8), ("pixar", 0.45), ("transition", 0.5)]
def test_apply_lora_rejects_unknown_style(monkeypatch):
captured: dict = {}
client = _lora_client(monkeypatch, captured)
response = client.post("/lora", json={"strength": 0.5, "styles": {"watercolor": 0.5}})
assert response.status_code == 400
assert "stack" not in captured
def test_apply_lora_accepts_legacy_single_style(monkeypatch):
captured: dict = {}
client = _lora_client(monkeypatch, captured)
response = client.post("/lora", json={"strength": 0.6, "style": "pixar"})
assert response.status_code == 200
assert captured["stack"] == [("omninft", 0.6), ("pixar", 1.0)]
def test_apply_lora_clamps_and_drops_zero_intensity(monkeypatch):
captured: dict = {}
client = _lora_client(monkeypatch, captured)
response = client.post("/lora", json={"strength": 1.5, "styles": {"pixar": 2.0, "transition": 0.0}})
assert response.status_code == 200
body = response.json()
assert body["strength"] == 1.0
assert body["styles"] == {"pixar": 1.0}
assert captured["stack"] == [("omninft", 1.0), ("pixar", 1.0)]
def test_lora_endpoints_hidden_without_devtools(monkeypatch):
server_main = _import_server_main(monkeypatch)
monkeypatch.setattr(server_main, "DEVTOOLS_ENABLED", False)
client = TestClient(server_main.app)
assert client.get("/lora/options").status_code == 404
assert client.post("/lora", json={"strength": 0.5}).status_code == 404
@@ -20,9 +20,11 @@ os.environ.setdefault("GROQ_API_KEY", "dummy")
import dreamverse.main as server_main # noqa: E402
import dreamverse.runtime as runtime # noqa: E402
from dreamverse.config import PROMPT_TIMEOUT_MS # noqa: E402
from dreamverse.session_logger import SessionEventLogger # noqa: E402
from dreamverse.worker_ipc import MediaChunk, MediaComplete, MediaInit # noqa: E402
from dreamverse.session import controller as session_controller # noqa: E402
from dreamverse.utils import PROMPT_EXTENSION_FAILURE_USER_MESSAGE # noqa: E402
pytestmark = pytest.mark.gpu
@@ -338,7 +340,7 @@ def test_session_event_logger_initializes_hostname_folder_and_utc_filename(
logger = SessionEventLogger(tmp_path)
assert logger.directory == tmp_path / socket.gethostname()
assert logger.directory.is_dir()
assert re.match(r"^\d{6}_\d{6}\.jsonl$", logger.path.name)
assert re.match(r"^\d{6}_\d{6}_\d{6}\.jsonl$", logger.path.name)
assert logger.path.is_file()
@@ -833,7 +835,7 @@ def test_initial_custom_rollout_prompt_generates_seed_window_before_streaming():
"rewrite_instruction": "A moonbase corridor thriller with flooding",
"rewrite_model": "gpt-4.1-mini",
"rewrite_temperature": 0.4,
"timeout_ms": server_main.PROMPT_TIMEOUT_MS,
"timeout_ms": PROMPT_TIMEOUT_MS,
"system_prompt_override": "Session-specific rewrite prompt",
}
assert fake_pool._slot.calls
@@ -1256,7 +1258,7 @@ def test_single5s_enhancement_fallback_does_not_start_generation():
assert fallback_payloads[0]["prompt_id"] == "simple_custom_prompt"
assert (
fallback_payloads[0]["error"]
== server_main.PROMPT_EXTENSION_FAILURE_USER_MESSAGE
== PROMPT_EXTENSION_FAILURE_USER_MESSAGE
)
assert fallback_payloads[0]["source"] == "user_enhancement_failed"
+105 -2
View File
@@ -12,6 +12,7 @@ module import time.
# mypy: ignore-errors
import gc
import os
import re
import time
from dataclasses import dataclass
from typing import Any
@@ -20,11 +21,19 @@ import numpy as np
import torch
from dreamverse.config import (
AVAILABLE_LORAS,
FRAME_HEIGHT,
FRAME_WIDTH,
MODEL_CONFIG,
NUM_FRAMES,
NUM_INFERENCE_STEPS,
DREAMVERSE_MAX_AUTOTUNE,
DREAMVERSE_SP_SIZE,
DREAMVERSE_LORA_PATH,
DREAMVERSE_LORA_NICKNAME,
DREAMVERSE_LORA_STRENGTH,
DREAMVERSE_LORA_STACK,
_resolve_lora_spec,
)
# Multi-frame decoded continuation defaults from
@@ -62,6 +71,15 @@ DEFAULT_LTX2_AUDIO_HOP_LENGTH = 160
DEFAULT_LTX2_AUDIO_DOWNSAMPLE = 4
def _reset_lora_registry(worker) -> dict:
pipeline = getattr(worker, "pipeline", None)
if pipeline is None:
return {"status": "no_pipeline"}
pipeline.cur_adapter_name = ""
pipeline.cur_adapter_strength = 1.0
return {"status": "lora_registry_reset"}
@dataclass
class StepResult:
"""Output of one generation step.
@@ -199,6 +217,8 @@ class VideoGenerationWorker:
self.continuation = ContinuationState()
self.audio_encoder_module = None
self.audio_processor_module = None
self.active_style_prepend: list[str] = []
self.active_style_append: list[str] = []
def _gpu_mem(self) -> str:
a = torch.cuda.memory_allocated() / 1024**3
@@ -257,6 +277,7 @@ class VideoGenerationWorker:
or self.current_model_config["model_path"])
enable_compile = os.getenv("ENABLE_TORCH_COMPILE", "1") == "1"
compile_mode = "max-autotune-no-cudagraphs" if DREAMVERSE_MAX_AUTOTUNE else None
components = ComponentConfig(
config_root=config_model_path,
@@ -269,7 +290,7 @@ class VideoGenerationWorker:
generator_config = GeneratorConfig(
model_path=model_root,
engine=EngineConfig(
num_gpus=1,
num_gpus=DREAMVERSE_SP_SIZE,
offload=OffloadConfig(
dit=False,
dit_layerwise=False,
@@ -280,9 +301,10 @@ class VideoGenerationWorker:
compile=CompileConfig(
enabled=enable_compile,
text_encoder_enabled=enable_compile,
vae_enabled=enable_compile,
backend="inductor",
fullgraph=True,
mode="max-autotune-no-cudagraphs",
mode=compile_mode,
dynamic=False,
),
use_fsdp_inference=False,
@@ -304,9 +326,88 @@ class VideoGenerationWorker:
self.generator = VideoGenerator.from_pretrained(config=generator_config)
print(f"[GPU {self.gpu_id}] After model load: {self._gpu_mem()}")
lora_stack = DREAMVERSE_LORA_STACK or ([(DREAMVERSE_LORA_PATH,
DREAMVERSE_LORA_STRENGTH)] if DREAMVERSE_LORA_PATH else [])
for i, (lora_path, lora_strength) in enumerate(lora_stack):
nickname = DREAMVERSE_LORA_NICKNAME if i == 0 else f"{DREAMVERSE_LORA_NICKNAME}_{i}"
print(f"[GPU {self.gpu_id}] Applying LoRA '{nickname}' from {lora_path} "
f"@{lora_strength} accumulate={i > 0}")
self.generator.set_lora_adapter(
lora_nickname=nickname,
lora_path=lora_path,
strength=lora_strength,
accumulate=(i > 0),
)
if lora_stack:
print(f"[GPU {self.gpu_id}] LoRA stack applied ({len(lora_stack)})")
self._load_audio_encoder(model_root)
print(f"[GPU {self.gpu_id}] LTX2 model loaded (warmup pending)")
def apply_lora_stack(
self,
stack: list[tuple[str, float]],
) -> tuple[str | None, str | None]:
"""Re-apply a runtime LoRA stack and update the active style trigger."""
if self.generator is None:
raise RuntimeError("Generator not initialized; cannot apply LoRA stack.")
try:
self.generator.unmerge_lora_weights()
except Exception as exc:
print(f"[GPU {self.gpu_id}] unmerge_lora_weights skipped: {exc}")
try:
self.generator.executor.collective_rpc(_reset_lora_registry)
except Exception as exc:
print(f"[GPU {self.gpu_id}] lora registry reset skipped: {exc}")
resolved_stack: list[tuple[str, float]] = []
for spec, strength in stack:
resolved = _resolve_lora_spec(spec)
if resolved:
resolved_stack.append((resolved, float(strength)))
for i, (lora_path, lora_strength) in enumerate(resolved_stack):
nickname = DREAMVERSE_LORA_NICKNAME if i == 0 else f"{DREAMVERSE_LORA_NICKNAME}_{i}"
print(f"[GPU {self.gpu_id}] Re-applying LoRA '{nickname}' from {lora_path} "
f"@{lora_strength} accumulate={i > 0}")
self.generator.set_lora_adapter(
lora_nickname=nickname,
lora_path=lora_path,
strength=lora_strength,
accumulate=(i > 0),
)
print(f"[GPU {self.gpu_id}] Runtime LoRA stack applied ({len(resolved_stack)})")
prepend: list[str] = []
append: list[str] = []
for spec, _ in stack:
key = (spec or "").strip().lower()
if key in AVAILABLE_LORAS:
trigger = AVAILABLE_LORAS[key]["trigger"]
if AVAILABLE_LORAS[key].get("position", "append") == "prepend":
prepend.append(trigger)
else:
append.append(trigger)
self.active_style_prepend = prepend
self.active_style_append = append
combined = " ".join(prepend + append) or None
return combined, None
def _inject_style_trigger(self, prompt: str) -> str:
result = prompt
for trigger in self.active_style_prepend:
trigger = (trigger or "").strip()
if trigger and not re.search(rf"\b{re.escape(trigger)}\b", result, re.IGNORECASE):
result = f"{trigger} {result}".strip()
for trigger in self.active_style_append:
trigger = (trigger or "").strip()
if trigger and not re.search(rf"\b{re.escape(trigger)}\b", result, re.IGNORECASE):
result = f"{result} {trigger}".strip()
return result
def _load_audio_encoder(self, model_root: str) -> None:
if not ENABLE_AUDIO_RE_ENCODE:
return
@@ -375,6 +476,8 @@ class VideoGenerationWorker:
"""Execute one generation step; snapshot state for the next segment."""
timings: dict = {}
prompt = self._inject_style_trigger(prompt)
request_kwargs = dict(
prompt=prompt,
negative_prompt="",
+14 -1
View File
@@ -48,6 +48,13 @@ class ReloadAck:
user_id: str
@dataclass(frozen=True)
class LoraAck:
user_id: str | None
style_trigger: str | None
style_trigger_position: str | None
@dataclass(frozen=True)
class WarmupComplete:
user_id: str | None
@@ -117,6 +124,7 @@ WorkerEvent = (StepComplete
| JoinAck
| LeaveAck
| ReloadAck
| LoraAck
| WarmupComplete
| MediaInit
| MediaChunk
@@ -154,4 +162,9 @@ class ReloadModelPayload:
model_config: dict
CommandPayload = UserStepPayload | WarmupPayload | ReloadModelPayload
@dataclass(frozen=True)
class LoraStackPayload:
stack: list[tuple[str, float]]
CommandPayload = (UserStepPayload | WarmupPayload | ReloadModelPayload | LoraStackPayload)
@@ -32,14 +32,21 @@
# FFMPEG_NATIVE_CC explicit C compiler command for native builds
# FFMPEG_NATIVE_CXX explicit C++ compiler command for native builds
#
# Toolchain selection: CC / CXX / AS are pinned to the conda-forge
# triplet matching `uname -m`. Inherited values are intentionally
# ignored — conda envs that have BOTH `gcc_linux-64` and
# `gcc_linux-aarch64` installed export the cross-compiler triplet on
# every `conda activate` (the `aarch64` activation script sorts later
# and wins), which silently breaks x264's compiler probe on the
# opposite host. If you genuinely need a non-host toolchain, set
# FFMPEG_NATIVE_CC and/or FFMPEG_NATIVE_CXX explicitly.
# Toolchain selection (precedence, highest first):
# 1. FFMPEG_NATIVE_CC / FFMPEG_NATIVE_CXX, if set — explicit override.
# 2. The conda-forge triplet matching `uname -m`
# (`x86_64-conda-linux-gnu-cc` or `aarch64-conda-linux-gnu-cc`),
# if present on PATH. Pinning the triplet sidesteps a bug where
# conda envs with BOTH `gcc_linux-64` and `gcc_linux-aarch64`
# installed export the cross-compiler triplet on every
# `conda activate` (the `aarch64` activation script sorts later
# and wins), silently breaking x264's compiler probe on the
# opposite host.
# 3. System `gcc` / `g++` — fallback so the script also works in a
# plain venv or bare shell with no conda toolchain installed.
# Inherited bare CC / CXX from the caller environment are ignored in
# cases (2) and (3); use FFMPEG_NATIVE_CC/CXX to inject a non-host
# toolchain.
set -euo pipefail
# ─── Defaults (override via env) ──────────────────────────────────────────
@@ -61,8 +68,8 @@ MAKE_JOBS="${MAKE_JOBS:-$(( NPROC < 16 ? NPROC : 16 ))}"
ARCH="$(uname -m)"
case "$ARCH" in
x86_64)
DEFAULT_CC=x86_64-conda-linux-gnu-cc
DEFAULT_CXX=x86_64-conda-linux-gnu-c++
CONDA_CC=x86_64-conda-linux-gnu-cc
CONDA_CXX=x86_64-conda-linux-gnu-c++
AS=nasm
X264_CFLAGS="-O3 -march=native -mtune=native -fPIC -flto"
X264_LDFLAGS="-flto -fuse-linker-plugin"
@@ -70,8 +77,8 @@ case "$ARCH" in
FFMPEG_LDFLAGS="-flto -Wl,-rpath,$INSTALL_PREFIX/lib"
;;
aarch64)
DEFAULT_CC=aarch64-conda-linux-gnu-cc
DEFAULT_CXX=aarch64-conda-linux-gnu-c++
CONDA_CC=aarch64-conda-linux-gnu-cc
CONDA_CXX=aarch64-conda-linux-gnu-c++
unset AS # GNU as on ARM
X264_CFLAGS="-O3 -mcpu=native -fPIC -flto"
X264_LDFLAGS="-flto -fuse-linker-plugin"
@@ -84,8 +91,27 @@ case "$ARCH" in
;;
esac
# Prefer the conda-forge triplet when it's actually on PATH; otherwise fall
# back to system gcc/g++ so a plain venv works too. FFMPEG_NATIVE_CC/CXX
# overrides both.
if command -v -- "$CONDA_CC" >/dev/null 2>&1 \
&& command -v -- "$CONDA_CXX" >/dev/null 2>&1; then
DEFAULT_CC="$CONDA_CC"
DEFAULT_CXX="$CONDA_CXX"
default_source="conda-forge ($ARCH triplet)"
else
DEFAULT_CC=gcc
DEFAULT_CXX=g++
default_source="system gcc/g++"
fi
CC="${FFMPEG_NATIVE_CC:-$DEFAULT_CC}"
CXX="${FFMPEG_NATIVE_CXX:-$DEFAULT_CXX}"
if [[ -n "${FFMPEG_NATIVE_CC:-}" || -n "${FFMPEG_NATIVE_CXX:-}" ]]; then
toolchain_source="FFMPEG_NATIVE_CC/CXX override"
else
toolchain_source="$default_source"
fi
require_compiler() {
local name="$1" compiler="$2"
@@ -95,6 +121,8 @@ require_compiler() {
fi
if ! command -v -- "$compiler" >/dev/null 2>&1; then
echo "[install_native_ffmpeg] $name is unavailable: $compiler" >&2
echo "[install_native_ffmpeg] install a compiler, or set FFMPEG_NATIVE_CC/FFMPEG_NATIVE_CXX" \
"to compiler commands on PATH." >&2
exit 1
fi
if ! "$compiler" --version >/dev/null 2>&1; then
@@ -107,7 +135,7 @@ require_compiler CC "$CC"
require_compiler CXX "$CXX"
export CC CXX
[[ -n "${AS:-}" ]] && export AS
echo "[install_native_ffmpeg] toolchain: CC=$CC CXX=$CXX AS=${AS:-<gnu-as>} (uname -m=$ARCH)"
echo "[install_native_ffmpeg] toolchain: CC=$CC CXX=$CXX AS=${AS:-<gnu-as>} (uname -m=$ARCH, source: $toolchain_source)"
# ─── Step 0: probe required tools ─────────────────────────────────────────
required=("$CC" "$CXX" make pkg-config git)
@@ -118,7 +146,12 @@ for cmd in "${required[@]}"; do
done
if (( ${#missing[@]} > 0 )); then
echo "[install_native_ffmpeg] missing required tools: ${missing[*]}" >&2
echo "[install_native_ffmpeg] install them, or set FFMPEG_NATIVE_CC/FFMPEG_NATIVE_CXX explicitly." >&2
echo "[install_native_ffmpeg] install the missing tools. FFMPEG_NATIVE_CC/FFMPEG_NATIVE_CXX" \
"only override compiler selection; they do not provide make, pkg-config, git, or nasm." >&2
if [[ " ${missing[*]} " == *" nasm "* ]]; then
echo "[install_native_ffmpeg] nasm is required on x86_64 for x264 SIMD; install nasm" \
"(for example via apt or conda-forge) and retry." >&2
fi
exit 1
fi
+1 -1
View File
@@ -28,7 +28,7 @@ NO_BROWSER=1 apps/dreamverse/scripts/launch/launch_demo.sh
`launch_backend_dreamverse.sh` starts the full Dreamverse backend path used by
the web app.
`launch_frontend.sh` starts the Next.js frontend and installs `pnpm`
`launch_frontend.sh` starts the Next.js frontend and installs npm
dependencies when `node_modules/` is missing.
`launch_backend_fastvideo.sh` starts the typed `fastvideo serve --config` path
@@ -7,8 +7,8 @@
# FRONTEND_MODE=dev bash launch_frontend.sh # plain dev (5299, no devtools)
# FRONTEND_MODE=single5s bash launch_frontend.sh # single-5s product mode
#
# The script ``cd``'s into ``web`` and shells out to pnpm. It runs
# ``pnpm install --frozen-lockfile`` only when ``node_modules/`` is missing so repeat
# The script ``cd``'s into ``web`` and shells out to npm. It runs
# ``npm ci`` only when ``node_modules/`` is missing so repeat
# launches are fast.
set -euo pipefail
@@ -34,21 +34,21 @@ fi
cd "${WEB_ROOT}"
if [[ ! -d node_modules ]]; then
echo "[launch-demo] node_modules missing — running pnpm install --frozen-lockfile"
pnpm install --frozen-lockfile
echo "[launch-demo] node_modules missing — running npm ci"
npm ci
fi
case "${FRONTEND_MODE}" in
devtools)
echo "[launch-demo] starting Next.js dev:devtools (port 5274)"
exec pnpm run dev:devtools -- "$@"
exec npm run dev:devtools -- "$@"
;;
dev)
echo "[launch-demo] starting Next.js dev (port 5299)"
exec pnpm run dev -- "$@"
exec npm run dev -- "$@"
;;
single5s)
echo "[launch-demo] starting Next.js dev:single5s (port 5274)"
exec pnpm run dev:single5s -- "$@"
exec npm run dev:single5s -- "$@"
;;
esac
+127
View File
@@ -0,0 +1,127 @@
# Dreamverse Modal deployment
This directory contains the Modal wrapper for running the registry-built
Dreamverse Docker image on a Modal B200 container. The wrapper pulls a prebuilt Docker image and exposes the dreamverse application running inside.
## 1. Install Modal CLI
Install and configure the Modal CLI for the target workspace/profile:
```bash
pip install modal
modal token set --token-id <token-id> --token-secret <token-secret> --profile=<profile-name>
modal profile activate <profile-name>
```
## 2. Create the API key secret
Dreamverse needs several API keys for prompt-rewriter LLMs and model access in the Modal secret named
`dreamverse-api-keys`. Create or replace it with placeholders like this:
```bash
modal secret create dreamverse-api-keys \
CEREBRAS_API_KEY=<your-cerebras-key> \
GROQ_API_KEY=<your-groq-key> \
HF_TOKEN=<your-huggingface-token> \
--force
```
## 3. Deploy with Docker image
`DREAMVERSE_IMAGE` is required at deploy time:
```bash
DREAMVERSE_IMAGE=ghcr.io/<org>/<repo>/dreamverse:<tag> \
modal deploy apps/dreamverse/scripts/modal/modal_app.py
```
Use a SHA-specific tag, not `latest`. Use a `dreamverse-backend-cuda12.9.1-sha-*`
tag for backend-only deploys, or a `dreamverse-ui-cuda12.9.1-sha-*` tag for an
image that includes the static UI served by the backend.
For local image build details, see
`apps/dreamverse/docker/README.md` and `apps/dreamverse/docker/docker_build.sh`.
## 4. Validate the deployment
Set the URL returned by `modal deploy`, then probe the backend:
```bash
URL=https://<workspace>--dreamverse-b200-serve.modal.run
curl -fsS --max-time 300 "$URL/healthz"
curl -fsS --max-time 300 "$URL/status"
curl -fsS --max-time 300 "$URL/readyz"
```
Check logs with function and container IDs so you can confirm only one container
is active:
```bash
modal app logs dreamverse-b200 \
--tail 200 \
--timestamps \
--show-function-id \
--show-container-id
```
Useful Modal commands:
```bash
modal app list
modal app list --json
modal billing report --for today --resolution h
modal app stop dreamverse-b200
```
## Runtime details
### Volumes and assumed locations
`modal_app.py` creates the named volumes if they do not exist and mounts them at
the locations expected by the image:
| Modal volume | Container path | Environment variable | Purpose |
| --- | --- | --- | --- |
| `dreamverse-hf-cache` | `/root/.cache/huggingface` | `HF_HOME` | Hugging Face model/cache data |
| `dreamverse-state` | `/var/lib/dreamverse` | `FASTVIDEO_DREAMVERSE_HOME` | Dreamverse outputs, session logs, and runtime state |
### Useful dev env vars
Most deploys only need the Modal secret and required `DREAMVERSE_IMAGE`.
`DREAMVERSE_IMAGE` is read from your local shell at deploy time; the runtime
knobs below live in the image or `modal_app.py` unless you intentionally change them:
- `DREAMVERSE_IMAGE`: deploy a prebuilt SHA-specific image tag without editing `modal_app.py`.
- `FASTVIDEO_ENABLE_DEVTOOLS`: enable Dreamverse devtools behavior.
- `FASTVIDEO_PROMPT_PROVIDER` / `FASTVIDEO_PROMPT_*_MODEL`: try prompt-rewriter provider or model choices.
- `FASTVIDEO_ENABLE_STARTUP_WARMUP`: trade slower startup for a warmer first request.
- `DREAMVERSE_MAX_AUTOTUNE`: enable or disable PyTorch Inductor max-autotune for the compiled Dreamverse runtime.
The Modal wrapper defaults to torch compile with Inductor max-autotune enabled
for the fastest generation after startup warmup. If you need shorter
compile/warmup time, disable max-autotune via:
```bash
DREAMVERSE_IMAGE=ghcr.io/<org>/<repo>/dreamverse:<tag> \
DREAMVERSE_MAX_AUTOTUNE=0 \
modal deploy apps/dreamverse/scripts/modal/modal_app.py
```
### Autoscaling and cost safety
The current deployed script keeps exactly one B200 container warm by setting
`min_containers=1` and `max_containers=1`. `min_containers=1` prevents Modal
from scaling the deployment down to zero after idle periods, which avoids paying
the expensive torch-compile/startup-warmup cost again on the next request.
`max_containers=1` caps concurrency so concurrent requests queue onto the warm
container instead of spawning additional B200 containers and multiplying cost.
### Common gotchas
- Modal profile/workspace matters: create the secret and deploy with the same active profile.
- `modal secret create ... --force` replaces the existing `dreamverse-api-keys` secret.
- Use the exact URL printed by `modal deploy`; the placeholder URL above is only an example shape.
- B200 cold starts and first model downloads can make `/readyz` slow. Check logs before redeploying.
- `modal app stop dreamverse-b200` stops the app; it is not a read-only inspection command.
@@ -0,0 +1,77 @@
# pyright: reportAttributeAccessIssue=false
"""Modal deployment entrypoint for the Dreamverse B200 backend."""
import os
import subprocess
import modal
IMAGE = os.environ.get("DREAMVERSE_IMAGE")
if not IMAGE:
raise RuntimeError(
"DREAMVERSE_IMAGE is required. Set it to a published SHA-specific Dreamverse image, "
"for example a dreamverse-backend-cuda12.9.1-sha-* tag or a "
"dreamverse-ui-cuda12.9.1-sha-* tag if serving the static UI."
)
# ``@modal.web_server`` invokes ``serve()`` directly and bypasses the image
# ENTRYPOINT (``docker/docker_entrypoint.sh``). That entrypoint normally
# ``:?``-validates these secret entries and ``source``-s ``ffmpeg-env.sh``;
# neither runs on Modal, so we replicate both here.
_REQUIRED_SECRET_KEYS: tuple[str, ...] = ("CEREBRAS_API_KEY", "GROQ_API_KEY")
image = modal.Image.from_registry(IMAGE)
image = image.env({
"DREAMVERSE_IMAGE": IMAGE,
"HF_HOME": "/root/.cache/huggingface",
"FASTVIDEO_DREAMVERSE_HOME": "/var/lib/dreamverse",
"FASTVIDEO_ENABLE_STARTUP_WARMUP": "1",
"FASTVIDEO_GPU_COUNT": "1",
"ENABLE_TORCH_COMPILE": "1",
"DREAMVERSE_MAX_AUTOTUNE": os.environ.get("DREAMVERSE_MAX_AUTOTUNE", "1"),
"STREAM_MODE": "av_fmp4",
# Native ffmpeg is built into ``/opt/ffmpeg-native`` by the Dockerfile but
# is not on the image's ``PATH``. Without these, ``av_streaming``'s
# ``shutil.which("ffmpeg")`` returns ``None`` and ``av_fmp4`` muxing has
# no encoder. ``ffmpeg-env.sh`` would normally set them.
"FASTVIDEO_FFMPEG_BIN": "/opt/ffmpeg-native/bin/ffmpeg",
"FASTVIDEO_VIDEO_CODEC": "libx264",
})
app = modal.App("dreamverse-b200")
hf_cache = modal.Volume.from_name("dreamverse-hf-cache", create_if_missing=True)
dreamverse_state = modal.Volume.from_name("dreamverse-state", create_if_missing=True)
@app.function(
image=image,
gpu="B200",
cpu=16,
memory=65536,
timeout=7200,
startup_timeout=4800,
min_containers=1,
max_containers=1,
secrets=[modal.Secret.from_name("dreamverse-api-keys")],
volumes={
"/root/.cache/huggingface": hf_cache,
"/var/lib/dreamverse": dreamverse_state,
},
)
@modal.web_server(8009, startup_timeout=4800)
def serve():
# ``or ""`` collapses ``None`` (unset) into an empty string, ``.strip()``
# collapses whitespace-only values (e.g. ``" "``) — both should be
# treated as missing.
missing = [
k for k in _REQUIRED_SECRET_KEYS
if not (os.environ.get(k) or "").strip()
]
if missing:
raise RuntimeError(
"dreamverse-api-keys secret is missing required entries: "
f"{', '.join(missing)}. Add them with `modal secret create "
"dreamverse-api-keys ... --force` and redeploy "
"(see apps/dreamverse/scripts/modal/README.md).")
subprocess.Popen(["dreamverse-server", "--host", "0.0.0.0", "--port", "8009"])
+2 -2
View File
@@ -87,9 +87,9 @@ if [[ "${started_backend}" == "1" ]]; then
echo "Backend log: ${BACKEND_LOG_PATH}"
echo "Backend is still running so you can launch the frontend:"
echo " cd ${ROOT_DIR}/web"
echo " BACKEND_HOST=${HOST} BACKEND_PORT=${PORT} pnpm run dev"
echo " BACKEND_HOST=${HOST} BACKEND_PORT=${PORT} npm run dev"
else
echo "You can now launch the frontend:"
echo " cd ${ROOT_DIR}/web"
echo " BACKEND_HOST=${HOST} BACKEND_PORT=${PORT} pnpm run dev"
echo " BACKEND_HOST=${HOST} BACKEND_PORT=${PORT} npm run dev"
fi
@@ -11,7 +11,7 @@ test.describe('backend health', () => {
expect(response.ok()).toBeTruthy();
const body = await response.json();
expect(body.status).toBe('ok');
expect(body.service).toBe('ltx2-streaming-backend');
expect(['ltx2-streaming-backend', 'ltx2-streaming-mock-server']).toContain(body.service);
});
test('readyz reports gpu pool state', async ({ request }) => {
@@ -18,7 +18,7 @@ import { test, expect, type WebSocket as PWWebSocket } from '@playwright/test';
* PLAYWRIGHT_BASE_URL=http://127.0.0.1:5274 \
* NEXT_PUBLIC_INCLUDE_DEVTOOLS=1 \
* PLAYWRIGHT_LONG_RUNNING=1 \
* pnpm exec playwright test e2e/long-running-segments.spec.ts
* npm exec -- playwright test e2e/long-running-segments.spec.ts
*
* The full run takes ~7-9 minutes on a B200: ~3-4min for torch.compile
* max-autotune to warm both DiT + text-encoder graphs, then ~30s for
@@ -0,0 +1,311 @@
import { stat } from 'node:fs/promises';
import { test, expect, type WebSocket as PWWebSocket } from '@playwright/test';
test.describe('mock-backed generation smoke', () => {
test('accepts a custom prompt and enters the streaming state', async ({ page, request }) => {
const health = await request.get('/healthz');
const body = health.ok() ? await health.json() : {};
test.skip(
body.service !== 'ltx2-streaming-mock-server',
'Mock-backed smoke requires dreamverse.mock_server on BACKEND_PORT (default 8009).',
);
const wsEventTypes: string[] = [];
page.on('websocket', (ws: PWWebSocket) => {
ws.on('framereceived', ({ payload }) => {
if (typeof payload !== 'string') return;
try {
const parsed = JSON.parse(payload);
if (typeof parsed?.type === 'string') wsEventTypes.push(parsed.type);
} catch {
}
});
});
await page.goto('/');
await expect(page.getByRole('img', { name: 'FastVideo' })).toBeVisible();
const continuation = page.getByLabel('Continuation prompt');
await expect(continuation).toBeVisible();
await continuation.fill('A glass whale swims above a neon forest');
await page.getByRole('button', { name: /^generate$/i }).click();
await expect(continuation).toBeDisabled({ timeout: 30_000 });
await expect(continuation).toHaveAttribute('placeholder', /generating video/i);
await expect(page.getByRole('button', { name: /^leave$/i })).toBeVisible({ timeout: 30_000 });
await expect(page.locator('video').first()).toHaveCount(1);
await expect.poll(() => wsEventTypes).toEqual(
expect.arrayContaining(['ltx2_stream_start', 'ltx2_segment_start', 'media_init']),
);
});
test('streams, plays, and surfaces a downloadable clip', async ({ page, request }) => {
const health = await request.get('/healthz');
const body = health.ok() ? await health.json() : {};
test.skip(
body.service !== 'ltx2-streaming-mock-server',
'Mock-backed playback assertions require dreamverse.mock_server on BACKEND_PORT (default 8009).',
);
await page.addInitScript(() => {
(window as unknown as { __sharedFiles: unknown }).__sharedFiles = null;
const stub = async (data: { files?: File[] }) => {
const files = Array.isArray(data?.files) ? data.files : [];
(window as unknown as { __sharedFiles: unknown }).__sharedFiles = files.map((f) => ({
name: f.name,
type: f.type,
size: f.size,
}));
};
Object.defineProperty(Navigator.prototype, 'share', {
value: stub,
configurable: true,
writable: true,
});
Object.defineProperty(Navigator.prototype, 'canShare', {
value: (data: { files?: File[] }) => Array.isArray(data?.files),
configurable: true,
writable: true,
});
});
await page.goto('/');
const continuation = page.getByLabel('Continuation prompt');
await expect(continuation).toBeVisible();
await continuation.fill('A glass whale swims above a neon forest');
await page.getByRole('button', { name: /^generate$/i }).click();
const liveVideo = page.locator('video:not(.hidden)');
await expect(liveVideo).toHaveCount(1);
await test.step('MSE pipeline attaches the live <video>', async () => {
// Chromium/Firefox attach via blob: URL; Safari uses srcObject with ManagedMediaSource.
await expect
.poll(
async () =>
liveVideo.evaluate(
(v: HTMLVideoElement) =>
v.src.startsWith('blob:') || v.srcObject !== null,
),
{ timeout: 30_000 },
)
.toBe(true);
});
await test.step('SourceBuffer accepts fMP4 and decoder produces frames', async () => {
await expect
.poll(
async () =>
liveVideo.evaluate((v: HTMLVideoElement) =>
v.buffered.length > 0 ? v.buffered.end(0) : 0,
),
{ timeout: 60_000 },
)
.toBeGreaterThan(0);
// readyState >= 3 = HAVE_FUTURE_DATA.
await expect
.poll(
async () => liveVideo.evaluate((v: HTMLVideoElement) => v.readyState),
{ timeout: 60_000 },
)
.toBeGreaterThanOrEqual(3);
});
await test.step('<video> playback advances past the first second', async () => {
await liveVideo.evaluate(async (v: HTMLVideoElement) => {
if (v.paused) {
try {
await v.play();
} catch {
}
}
});
await expect
.poll(
async () => liveVideo.evaluate((v: HTMLVideoElement) => v.currentTime),
{ timeout: 30_000 },
)
.toBeGreaterThan(0);
await expect
.poll(
async () =>
liveVideo.evaluate((v: HTMLVideoElement) =>
v.ended ? v.duration : v.currentTime,
),
{ timeout: 10_000 },
)
.toBeGreaterThanOrEqual(1.0);
});
await test.step('the AAC audio track is demuxed and decoded', async () => {
// The mock emits AAC (mp4a.40.2), matching the real backend
// (av_streaming.py). Proves the FE decoded the audio track.
await expect
.poll(
async () =>
liveVideo.evaluate((el: HTMLVideoElement) => {
const v = el as HTMLVideoElement & {
audioTracks?: { length: number };
mozHasAudio?: boolean;
webkitAudioDecodedByteCount?: number;
};
return (
(v.audioTracks?.length ?? 0) > 0 ||
v.mozHasAudio === true ||
(v.webkitAudioDecodedByteCount ?? 0) > 0
);
}),
{ timeout: 10_000 },
)
.toBe(true);
});
await test.step('completed clip surfaces a working download', async () => {
const downloadButton = page.getByRole('button', { name: /download video|share video/i });
await expect(downloadButton).toBeVisible({ timeout: 30_000 });
const buttonLabel = (await downloadButton.getAttribute('aria-label')) ?? '';
const isShareFlow = /share video/i.test(buttonLabel);
if (isShareFlow) {
await downloadButton.click();
await expect
.poll(
async () => page.evaluate(() => (window as { __sharedFiles?: unknown }).__sharedFiles),
{ timeout: 10_000 },
)
.not.toBeNull();
const shared = (await page.evaluate(
() => (window as { __sharedFiles?: Array<{ name: string; type: string; size: number }> }).__sharedFiles,
)) ?? [];
expect(shared).toHaveLength(1);
expect(shared[0].name).toMatch(/\.(mp4|webm)$/);
expect(shared[0].size).toBeGreaterThan(0);
} else {
const downloadPromise = page.waitForEvent('download');
await downloadButton.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.(mp4|webm)$/);
const savedPath = await download.path();
expect(savedPath).not.toBeNull();
const { size } = await stat(savedPath!);
expect(size).toBeGreaterThan(0);
}
});
await test.step('project history sidebar lists the current session', async () => {
const sidebar = page.getByRole('complementary', { name: 'Project history' });
await page.getByRole('button', { name: 'Toggle sidebar' }).click();
await expect(sidebar).toBeInViewport();
await expect(sidebar.getByText('Current', { exact: true })).toBeVisible();
await expect(sidebar.getByText('Active', { exact: true })).toBeVisible();
});
const lastError = await liveVideo.evaluate((v: HTMLVideoElement) =>
v.error ? `${v.error.code}: ${v.error.message ?? ''}` : null,
);
expect(lastError).toBeNull();
});
test('starts a new project and switches back to the prior session', async ({ page, request }) => {
const health = await request.get('/healthz');
const body = health.ok() ? await health.json() : {};
test.skip(
body.service !== 'ltx2-streaming-mock-server',
'Mock-backed project lifecycle assertions require dreamverse.mock_server on BACKEND_PORT (default 8009).',
);
await page.goto('/');
await test.step('complete a generation so there is a project to save', async () => {
const continuation = page.getByLabel('Continuation prompt');
await expect(continuation).toBeVisible();
await continuation.fill('Aurora over a frozen lake');
await page.getByRole('button', { name: /^generate$/i }).click();
await expect(
page.getByRole('button', { name: /download video|share video/i }),
).toBeVisible({ timeout: 60_000 });
});
const sidebar = page.getByRole('complementary', { name: 'Project history' });
await test.step('"New project" closes the sidebar and resets the composer', async () => {
await page.getByRole('button', { name: 'Toggle sidebar' }).click();
await expect(sidebar).toBeInViewport();
await sidebar.getByRole('button', { name: /^new project$/i }).click();
await expect(sidebar).not.toBeInViewport();
await expect(page.getByRole('button', { name: /^generate$/i })).toBeVisible({ timeout: 30_000 });
const continuation = page.getByLabel('Continuation prompt');
await expect(continuation).toBeEnabled();
await expect(continuation).toHaveValue('');
});
await test.step('the prior session appears under timeline', async () => {
await page.getByRole('button', { name: 'Toggle sidebar' }).click();
await expect(sidebar).toBeInViewport();
await expect(sidebar.getByText('Previous', { exact: true })).toBeVisible({ timeout: 30_000 });
await expect(sidebar.getByText(/^(just now|\d+m ago)$/).first()).toBeVisible();
});
await test.step('clicking the prior session enters viewing mode', async () => {
const priorRow = sidebar.locator('div[role="button"]').filter({ hasText: /just now|\d+m ago/ }).first();
await priorRow.click();
await expect(sidebar).not.toBeInViewport();
await expect(page.locator('video[autoplay][loop]')).toBeVisible({ timeout: 30_000 });
});
});
test('saved projects persist across a page reload', async ({ page, request }) => {
const health = await request.get('/healthz');
const body = health.ok() ? await health.json() : {};
test.skip(
body.service !== 'ltx2-streaming-mock-server',
'Mock-backed persistence assertions require dreamverse.mock_server on BACKEND_PORT (default 8009).',
);
await page.goto('/');
await test.step('complete a generation', async () => {
const continuation = page.getByLabel('Continuation prompt');
await expect(continuation).toBeVisible();
await continuation.fill('Aurora over a frozen lake');
await page.getByRole('button', { name: /^generate$/i }).click();
await expect(
page.getByRole('button', { name: /download video|share video/i }),
).toBeVisible({ timeout: 60_000 });
});
const sidebar = page.getByRole('complementary', { name: 'Project history' });
await test.step('persist via "New project" and confirm it lands under Previous', async () => {
await page.getByRole('button', { name: 'Toggle sidebar' }).click();
await expect(sidebar).toBeInViewport();
await sidebar.getByRole('button', { name: /^new project$/i }).click();
await expect(page.getByRole('button', { name: /^generate$/i })).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: 'Toggle sidebar' }).click();
await expect(sidebar).toBeInViewport();
await expect(sidebar.getByText('Previous', { exact: true })).toBeVisible({ timeout: 30_000 });
await expect(sidebar.getByText(/^(just now|\d+m ago)$/).first()).toBeVisible();
});
await test.step('after page reload, the prior project is still in Previous', async () => {
await page.reload();
await page.getByRole('button', { name: 'Toggle sidebar' }).click();
await expect(sidebar).toBeInViewport();
await expect(sidebar.getByText('Previous', { exact: true })).toBeVisible({ timeout: 30_000 });
await expect(sidebar.getByText(/^(just now|\d+m ago)$/).first()).toBeVisible();
});
});
});
+13 -2
View File
@@ -6,10 +6,13 @@ const backendHost = process.env.BACKEND_HOST || '127.0.0.1';
const backendPort = Number(process.env.BACKEND_PORT) || 8009;
const backendUrl = `http://${backendHost}:${backendPort}`;
const configDir = path.dirname(fileURLToPath(import.meta.url));
const staticExport = process.env.NEXT_OUTPUT_EXPORT === '1';
const nextConfig: NextConfig = {
...(staticExport ? { output: 'export' as const } : {}),
...(staticExport ? { images: { unoptimized: true } } : {}),
outputFileTracingRoot: path.join(configDir, '..', '..', '..'),
async rewrites() {
...(staticExport ? {} : { async rewrites() {
return [
{
source: '/ws',
@@ -47,8 +50,16 @@ const nextConfig: NextConfig = {
source: '/curated-presets/:path*',
destination: `${backendUrl}/curated-presets/:path*`,
},
{
source: '/lora',
destination: `${backendUrl}/lora`,
},
{
source: '/lora/:path*',
destination: `${backendUrl}/lora/:path*`,
},
];
},
} }),
webpack: (config) => {
config.module.rules.push({
test: /\.jsonl$/,
+11669 -11443
View File
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -38,7 +38,7 @@
"geist": "^1.7.0",
"lucide-react": "^0.577.0",
"mp4box": "^2.3.0",
"next": "^15.3.3",
"next": "15.5.18",
"radix-ui": "^1.4.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
@@ -59,9 +59,16 @@
"@vitest/coverage-v8": "^3.2.4",
"jsdom": "^26.1.0",
"mock-socket": "^9.3.1",
"postcss": "^8.5.8",
"postcss": "8.5.10",
"tailwindcss": "^4.2.1",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
},
"overrides": {
"esbuild": "0.25.12",
"picomatch": "4.0.4",
"postcss": "8.5.10",
"rollup": "4.59.0",
"vite": "7.3.2"
}
}
+30 -15
View File
@@ -1,12 +1,16 @@
import { defineConfig, devices } from '@playwright/test';
const backendHost = process.env.BACKEND_HOST || '127.0.0.1';
const backendPort = process.env.BACKEND_PORT || '8009';
/**
* Playwright config for Dreamverse end-to-end tests.
*
* Tests assume the Dreamverse Python server is reachable at
* BACKEND_HOST:BACKEND_PORT (default 127.0.0.1:8009) and the Next.js frontend
* runs on port 5299. The webServer block boots `pnpm run dev` if no
* server is already listening so tests work both locally and in CI.
* runs on port 5299. The webServer block boots the mock backend and
* `npm run dev` if no server is already listening so tests work both
* locally and in CI.
*/
export default defineConfig({
testDir: './e2e',
@@ -26,23 +30,34 @@ export default defineConfig({
viewport: { width: 1280, height: 720 },
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
video: process.env.CI ? 'retain-on-failure' : 'on',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'msedge', use: { ...devices['Desktop Edge'], channel: 'msedge' } },
{ name: 'mobile-safari', use: { ...devices['iPhone 14'] } },
{ name: 'mobile-chromium', use: { ...devices['Pixel 7'] } },
],
webServer: process.env.PLAYWRIGHT_SKIP_WEBSERVER
? undefined
: {
command: 'pnpm run dev',
url: 'http://127.0.0.1:5299',
reuseExistingServer: true,
timeout: 120_000,
env: {
BACKEND_HOST: process.env.BACKEND_HOST || '127.0.0.1',
BACKEND_PORT: process.env.BACKEND_PORT || '8009',
: [
{
command: `PYTHONPATH=.. python3 -m uvicorn dreamverse.mock_server:app --host ${backendHost} --port ${backendPort}`,
url: `http://${backendHost}:${backendPort}/healthz`,
reuseExistingServer: true,
timeout: 120_000,
},
},
{
command: 'npm run dev',
url: 'http://127.0.0.1:5299',
reuseExistingServer: true,
timeout: 120_000,
env: {
BACKEND_HOST: backendHost,
BACKEND_PORT: backendPort,
},
},
],
});
-5199
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -2489,6 +2489,8 @@ export default function Page() {
if (devtoolsMode) {
return (
<DevtoolsShell
videoRef={videoRefCallback}
archivedPlaybackRef={archivedPlaybackRefCallback}
connected={connected as boolean}
gpuAssigned={gpuAssigned as boolean}
sessionStarted={sessionStarted as boolean}
@@ -161,6 +161,7 @@ export default function VideoPlayer({
}}
size="icon"
variant="outline"
aria-label={canShare ? "Share video" : "Download video"}
className="absolute top-3 left-3 z-10 cursor-pointer bg-slate-800/50 text-white/90 shadow-md backdrop-blur-sm transition-all border-white/30 hover:bg-slate-800/85 hover:border-white/50 hover:text-white hover:scale-105"
>
{canShare ? <Share className="size-5" /> : <Download className="size-5" />}
@@ -9,6 +9,7 @@ import { Label } from '@/components/ui/label';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import LoraControls from '@/components/devtools/LoraControls';
interface DevtoolsDrawerProps {
editableMode?: boolean;
@@ -126,6 +127,8 @@ export default function DevtoolsDrawer({
</div>
<div className="grid gap-4 xl:grid-cols-2">
<LoraControls />
{/* Prompt window memory */}
<details className="group overflow-hidden rounded-2xl border border-border bg-card shadow-sm" open>
<summary className="flex cursor-pointer list-none items-start justify-between gap-4 px-5 py-4">
@@ -113,6 +113,8 @@ interface DevtoolsShellProps {
formatTime?: (seconds: number) => string;
formatDurationMs?: (durationMs: number) => string;
onPlaying?: () => void;
videoRef?: React.RefCallback<HTMLVideoElement>;
archivedPlaybackRef?: React.RefCallback<HTMLVideoElement>;
}
export default function DevtoolsShell({
@@ -209,6 +211,8 @@ export default function DevtoolsShell({
formatTime = (seconds) => `${seconds}`,
formatDurationMs = (durationMs) => `${durationMs}`,
onPlaying = () => {},
videoRef,
archivedPlaybackRef,
}: DevtoolsShellProps) {
return (
<WorkspaceShell
@@ -235,6 +239,8 @@ export default function DevtoolsShell({
workspace={
<div className="flex flex-col gap-4">
<VideoPlayer
videoRef={videoRef}
archivedPlaybackRef={archivedPlaybackRef}
activeClip={activeClip}
sessionStarted={sessionStarted}
avPlaybackStarted={avPlaybackStarted}
@@ -0,0 +1,181 @@
'use client';
import React, { useEffect, useRef, useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label';
const STYLE_LABELS: Record<string, string> = {
none: 'None',
pixar: 'Pixar Toon',
transition: 'Transition',
};
export default function LoraControls() {
const [styleKeys, setStyleKeys] = useState<string[]>([]);
const [labels, setLabels] = useState<Record<string, string>>(STYLE_LABELS);
const [strength, setStrength] = useState(0.8);
const [enabled, setEnabled] = useState<Record<string, boolean>>({});
const [intensity, setIntensity] = useState<Record<string, number>>({});
const [status, setStatus] = useState('idle');
const strengthRef = useRef(0.8);
const enabledRef = useRef<Record<string, boolean>>({});
const intensityRef = useRef<Record<string, number>>({});
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const requestIdRef = useRef(0);
useEffect(() => {
fetch('/lora/options')
.then((response) => (response.ok ? response.json() : null))
.then((data) => {
if (data && Array.isArray(data.styles)) {
const keys = data.styles.filter((s: string) => s !== 'none');
setStyleKeys(keys);
const initIntensity: Record<string, number> = {};
keys.forEach((k: string) => {
initIntensity[k] = 1.0;
});
setIntensity(initIntensity);
intensityRef.current = initIntensity;
}
if (data && data.labels && typeof data.labels === 'object') {
setLabels((prev) => ({ ...prev, ...data.labels }));
}
})
.catch(() => setStatus('options unavailable'));
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, []);
const applyNow = () => {
const reqId = ++requestIdRef.current;
const stylesPayload: Record<string, number> = {};
for (const key of Object.keys(enabledRef.current)) {
if (enabledRef.current[key]) {
stylesPayload[key] = intensityRef.current[key] ?? 1.0;
}
}
setStatus('applying…');
fetch('/lora', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ strength: strengthRef.current, styles: stylesPayload }),
})
.then(async (response) => {
if (!response.ok) {
throw new Error(await response.text());
}
return response.json();
})
.then((data) => {
if (reqId !== requestIdRef.current) return;
const active = Object.keys(data?.styles ?? {});
const desc = active.length
? active.map((k) => `${labels[k] ?? k}@${data.styles[k]}`).join(' + ')
: 'no style';
setStatus(`applied OmniNFT@${data.strength} + ${desc}`);
})
.catch((error) => {
if (reqId !== requestIdRef.current) return;
setStatus(`error: ${String(error).slice(0, 120)}`);
});
};
const scheduleApply = () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = setTimeout(applyNow, 250);
};
return (
<details
className="group overflow-hidden rounded-2xl border border-border bg-card shadow-sm xl:col-span-2"
open
>
<summary className="flex cursor-pointer list-none items-start justify-between gap-4 px-5 py-4">
<div className="space-y-1">
<span className="block text-lg font-semibold text-foreground">
LoRA stack
</span>
<span className="block text-sm leading-6 text-muted-foreground">
OmniNFT strength + stackable style adapters, each with its own intensity (live).
</span>
</div>
<Badge variant="secondary">{status}</Badge>
</summary>
<div className="space-y-5 border-t border-border px-5 py-4">
<div className="space-y-2">
<Label htmlFor="lora-strength">
OmniNFT strength · {strength.toFixed(2)}
</Label>
<input
id="lora-strength"
type="range"
min={0}
max={1}
step={0.05}
value={strength}
onChange={(event) => {
const next = Number(event.target.value);
setStrength(next);
strengthRef.current = next;
scheduleApply();
}}
className="w-full accent-sky-400"
/>
</div>
{styleKeys.length === 0 ? (
<p className="text-sm text-muted-foreground">
No style adapters for the active model.
</p>
) : (
styleKeys.map((key) => (
<div key={key} className="space-y-2">
<div className="flex items-center justify-between">
<label className="flex cursor-pointer items-center gap-2 text-sm font-medium text-foreground">
<input
type="checkbox"
checked={!!enabled[key]}
onChange={(event) => {
const next = { ...enabledRef.current, [key]: event.target.checked };
enabledRef.current = next;
setEnabled(next);
scheduleApply();
}}
className="accent-sky-400"
/>
{labels[key] ?? STYLE_LABELS[key] ?? key}
</label>
<span className="text-xs text-muted-foreground">
{(intensity[key] ?? 1).toFixed(2)}
</span>
</div>
<input
type="range"
min={0}
max={1}
step={0.05}
value={intensity[key] ?? 1}
disabled={!enabled[key]}
onChange={(event) => {
const next = { ...intensityRef.current, [key]: Number(event.target.value) };
intensityRef.current = next;
setIntensity(next);
scheduleApply();
}}
className="w-full accent-sky-400 disabled:opacity-40"
/>
</div>
))
)}
</div>
</details>
);
}
-106
View File
@@ -1,106 +0,0 @@
# FVD (Fréchet Video Distance) Benchmark
Evaluate generated video quality using FVD with the I3D feature extractor.
## Quick Start
**Run the benchmark:**
```bash
bash benchmarks/scripts/run.sh
```
That's it! The script auto-installs dependencies and runs the benchmark.
**To customize:** Edit `benchmarks/fvd/run_fvd.py` to change:
- Video paths (`real_dir`, `gen_dir`)
- Number of videos, frames, sampling strategy
- Device, batch size, caching, etc.
## Advanced Usage (CLI)
For more control without editing Python files, use the CLI.
**First-time setup** (one-time per pod/environment):
```bash
bash benchmarks/scripts/setup_fvd.sh
```
Then run any configuration you want:
```bash
# Custom configuration
python -m benchmarks.fvd.cli \
--real-path data/real/ \
--gen-path outputs/gen/ \
--num-videos 1024 \
--num-frames 32 \
--clip-strategy random \
--batch-size 32 \
--seed 42 \
--extractor clip
```
**Standard protocols:**
```bash
# Use predefined protocols
python -m benchmarks.fvd.cli \
--real-path data/real/ \
--gen-path outputs/gen/ \
--protocol fvd2048_16f # or fvd2048_128f, quick_test, etc.
```
This would use i3d model by default as the feature extractor
**Feature caching** (speed up repeated evaluations):
```bash
python -m benchmarks.fvd.cli \
--real-path data/real/ \
--gen-path outputs/gen/ \
--protocol fvd2048_16f \
--cache-real-features fvd-cache/extractor_name # Directory path (will save/load fvd-cache/extractor_name/extractor-name_real_features.pkl)
```
Run `python -m benchmarks.fvd.cli --help` for all options.
## Available Protocols
- `fvd2048_16f` - Standard (2048 videos, 16 frames)
- `fvd2048_128f` - Long videos (128 frames)
- `fvd2048_128f_subsample8` - Subsampled long videos
- `quick_test` - Fast testing (10 videos)
## Configuration Options
Key options in `FVDConfig`:
```python
num_videos=2048, # Videos to evaluate
num_frames_per_clip=16, # Frames per clip
clip_strategy='beginning', # beginning|random|uniform|middle|sliding
frame_stride=1, # Frame subsampling
batch_size=32, # GPU batch size
device='cuda', # cuda|cpu
cache_real_features=None, # Cache path for speed
seed=42, # Reproducibility
extractor='i3d', # i3d|clip|videomae
```
## Programmatic Usage
```python
from benchmarks.fvd import compute_fvd_with_config, FVDConfig
config = FVDConfig.fvd2048_16f() # or custom config
results = compute_fvd_with_config('data/real/', 'outputs/gen/', config)
print(f"FVD: {results['fvd']:.2f}")
```
## Notes
- Requires minimum 10 frames per clip
- Supports both video files (.mp4, .avi, etc.) and frame directories
- `--cache-real-features` expects a **directory path** (e.g., `cache/real`), it will automatically create/load `real_features.pkl` inside that directory
-37
View File
@@ -1,37 +0,0 @@
"""
FastVideo Frechet Video Distance (FVD) Benchmark Module.
>>> from fastvideo.benchmarks.fvd import compute_fvd_with_config, FVDConfig
>>> config = FVDConfig.fvd2048_16f() # Standard protocol
>>> results = compute_fvd_with_config('data/real/', 'outputs/gen/', config)
>>> print(f"FVD: {results['fvd']:.2f}")
"""
from .fvd import (
compute_fvd,
compute_fvd_with_config,
compute_frechet_distance,
compute_statistics,
FVDConfig,
)
from .feature_extractors import (BaseFeatureExtractor, I3DFeatureExtractor, load_extractor)
from .video_utils import (
load_video_auto,
sample_clips_from_video,
load_video_clips_streaming,
ClipSamplingStrategy,
)
__all__ = [
'compute_fvd',
'compute_fvd_with_config',
'compute_frechet_distance',
'compute_statistics',
'FVDConfig',
'BaseFeatureExtractor',
'I3DFeatureExtractor',
'load_extractor',
'load_video_auto',
'sample_clips_from_video',
'load_video_clips_streaming',
'ClipSamplingStrategy',
]
-77
View File
@@ -1,77 +0,0 @@
import argparse
import sys
import traceback
from .fvd import compute_fvd_with_config, FVDConfig
def main() -> int:
parser = argparse.ArgumentParser(description='Compute Fréchet Video Distance (FVD)')
# Required arguments
parser.add_argument('--real-path', type=str, required=True, help='Path to real videos')
parser.add_argument('--gen-path', type=str, required=True, help='Path to generated videos')
# Extractor selection
parser.add_argument('--extractor',
type=str,
default='i3d',
choices=['i3d', 'clip', 'videomae'],
help='Feature extractor model to use (default: i3d)')
# Standard args
parser.add_argument('--seed', type=int, default=None, help='Random seed for reproducibility')
parser.add_argument('--protocol',
type=str,
default=None,
choices=['fvd2048_16f', 'fvd2048_128f', 'quick_test'],
help='Use standard protocol (overrides other settings)')
parser.add_argument('--num-videos', type=int, default=2048, help='Number of videos to use')
parser.add_argument('--num-frames', type=int, default=16, help='Number of frames per clip')
parser.add_argument('--clip-strategy', type=str, default='beginning', help='Clip sampling strategy')
parser.add_argument('--batch-size', type=int, default=32, help='Batch size for feature extraction')
parser.add_argument('--device', type=str, default='cuda', help='Device to use (cuda or cpu)')
parser.add_argument('--cache-real-features', type=str, default=None, help='Path to cache real video features')
parser.add_argument('--quiet', action='store_true', help='Suppress progress output')
args = parser.parse_args()
# Create config
if args.protocol:
protocol_map = {
'fvd2048_16f': FVDConfig.fvd2048_16f,
'fvd2048_128f': FVDConfig.fvd2048_128f,
'quick_test': FVDConfig.quick_test,
}
config = protocol_map[args.protocol]()
# Apply overrides
config.device = args.device
config.cache_real_features = args.cache_real_features
config.extractor_model = args.extractor # Apply extractor arg
else:
config = FVDConfig(
num_videos=args.num_videos,
num_frames_per_clip=args.num_frames,
extractor_model=args.extractor, # Apply extractor arg
clip_strategy=args.clip_strategy,
batch_size=args.batch_size,
device=args.device,
cache_real_features=args.cache_real_features,
seed=args.seed)
try:
_ = compute_fvd_with_config(
args.real_path, # noqa: F841
args.gen_path,
config,
verbose=not args.quiet)
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
return 1
if __name__ == '__main__':
sys.exit(main())
-232
View File
@@ -1,232 +0,0 @@
"""
Pluggable Feature Extractors for FVD Computation.
Supports I3D (standard), CLIP, and VideoMAE via a common interface.
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from abc import ABC, abstractmethod
from huggingface_hub import hf_hub_download
from tqdm import tqdm
try:
from transformers import CLIPModel, CLIPProcessor, VideoMAEModel
TRANSFORMERS_AVAILABLE = True
except ImportError:
TRANSFORMERS_AVAILABLE = False
class BaseFeatureExtractor(ABC, nn.Module):
"""Abstract base class for all video feature extractors."""
def __init__(self, device: str = 'cuda'):
super().__init__()
self.device = torch.device(device if torch.cuda.is_available() else 'cpu')
@property
@abstractmethod
def feature_dim(self) -> int:
"""Dimension of the output feature vector."""
pass
@abstractmethod
def preprocess(self, videos: torch.Tensor) -> torch.Tensor:
"""
Args:
videos: [B, T, C, H, W] in [0, 255] range.
Returns:
Preprocessed tensor ready for the model.
"""
pass
@abstractmethod
def extract_features_batch(self, videos: torch.Tensor) -> torch.Tensor:
"""
Extract features for a single batch.
Args:
videos: [B, T, C, H, W] (raw input)
Returns:
Features: [B, feature_dim]
"""
pass
@torch.no_grad()
def extract_features(self, videos: torch.Tensor, batch_size: int = 32, verbose: bool = True) -> torch.Tensor:
"""
Extract features for a large tensor of videos by batching.
"""
N = len(videos)
all_features = []
iterator = range(0, N, batch_size)
if verbose:
iterator = tqdm(iterator, desc=f"Extracting features ({self.__class__.__name__})")
for i in iterator:
batch = videos[i:i + batch_size].to(self.device)
features = self.extract_features_batch(batch)
all_features.append(features.cpu())
return torch.cat(all_features, dim=0)
# 1. I3D Extractor (The Standard FVD Metric)
class I3DFeatureExtractor(BaseFeatureExtractor):
REPO_ID = 'flateon/FVD-I3D-torchscript'
MODEL_FILENAME = 'i3d_torchscript.pt'
def __init__(self, device: str = 'cuda', cache_dir: str | None = None):
super().__init__(device)
self.cache_dir = cache_dir
self.model = self._load_model()
self.model.eval()
self.model.to(self.device)
@property
def feature_dim(self) -> int:
return 400
def _load_model(self) -> torch.nn.Module:
try:
model_path = hf_hub_download(repo_id=self.REPO_ID, filename=self.MODEL_FILENAME, cache_dir=self.cache_dir)
return torch.jit.load(model_path, map_location=self.device)
except Exception as e:
raise RuntimeError(f"Failed to load I3D model: {e}") from e
def preprocess(self, videos: torch.Tensor) -> torch.Tensor:
"""Standard I3D preprocessing: Resize to 224, Norm to [-1, 1]."""
B, T, C, H, W = videos.shape
if T < 10:
raise ValueError(f"I3D requires at least 10 frames, got {T}")
# Normalize to [0, 1]
if videos.max() > 1.0:
videos = videos / 255.0
# Scale to [-1, 1]
videos = videos * 2.0 - 1.0
# Resize to 224x224
if H != 224 or W != 224:
videos = videos.reshape(B * T, C, H, W)
videos = F.interpolate(videos, size=(224, 224), mode='bilinear', align_corners=False)
videos = videos.reshape(B, T, C, 224, 224)
# [B, T, C, H, W] -> [B, C, T, H, W]
return videos.permute(0, 2, 1, 3, 4).contiguous()
def extract_features_batch(self, videos: torch.Tensor) -> torch.Tensor:
batch = self.preprocess(videos)
# TorchScript I3D returns raw logits when return_features=True
return self.model(batch, rescale=False, resize=False, return_features=True)
# 2. CLIP Extractor (Semantic/Content Quality)
class CLIPFeatureExtractor(BaseFeatureExtractor):
def __init__(self, device: str = 'cuda', model_name: str = "openai/clip-vit-base-patch32"):
if not TRANSFORMERS_AVAILABLE:
raise ImportError("Please install transformers: uv pip install transformers")
super().__init__(device)
self.processor = CLIPProcessor.from_pretrained(model_name)
self.model = CLIPModel.from_pretrained(model_name).to(self.device)
self.model.eval()
self._feature_dim = self.model.config.projection_dim
@property
def feature_dim(self) -> int:
return self._feature_dim
def preprocess(self, videos: torch.Tensor) -> torch.Tensor:
# Ensure values are [0, 255]
if videos.max() <= 1.0:
videos = videos * 255.0
return videos.to(torch.uint8)
def extract_features_batch(self, videos: torch.Tensor) -> torch.Tensor:
# Input: [B, T, C, H, W]
B, T, C, H, W = videos.shape
videos = self.preprocess(videos)
# Flatten B*T to treat frames as images
images = videos.view(B * T, C, H, W)
# HF Processor
inputs = self.processor(images=images, return_tensors="pt", padding=True)
inputs = {k: v.to(self.device) for k, v in inputs.items()}
# Extract features [B*T, Dim]
outputs = self.model.get_image_features(**inputs)
# Reshape [B, T, Dim] and Average Pooling over time
outputs = outputs.view(B, T, -1)
return outputs.mean(dim=1)
# 3. VideoMAE Extractor (Structure/Motion Quality)
class VideoMAEFeatureExtractor(BaseFeatureExtractor):
def __init__(self, device: str = 'cuda', model_name: str = "MCG-NJU/videomae-base"):
if not TRANSFORMERS_AVAILABLE:
raise ImportError("Please install transformers: uv pip install transformers")
super().__init__(device)
self.model = VideoMAEModel.from_pretrained(model_name).to(self.device)
self.model.eval()
self.register_buffer('mean', torch.tensor([0.485, 0.456, 0.406], device=self.device).view(1, 1, 3, 1, 1))
self.register_buffer('std', torch.tensor([0.229, 0.224, 0.225], device=self.device).view(1, 1, 3, 1, 1))
@property
def feature_dim(self) -> int:
return self.model.config.hidden_size
def preprocess(self, videos: torch.Tensor) -> torch.Tensor:
"""
Efficient GPU-based preprocessing.
Input: [B, T, C, H, W] in range [0, 255]
"""
B, T, C, H, W = videos.shape
# 1. Resize to 224x224
if H != 224 or W != 224:
videos = videos.view(B * T, C, H, W)
videos = F.interpolate(videos, size=(224, 224), mode='bilinear', align_corners=False)
videos = videos.view(B, T, C, 224, 224)
# 2. Normalize to [0, 1]
if videos.dtype != torch.float32:
videos = videos.float()
if videos.max() > 1.0:
videos = videos / 255.0
# 3. Apply ImageNet Mean/Std
return (videos - self.mean) / self.std
def extract_features_batch(self, videos: torch.Tensor) -> torch.Tensor:
# Input: [B, T, C, H, W]
# Fast GPU Preprocessing
pixel_values = self.preprocess(videos)
# Forward pass
outputs = self.model(pixel_values)
# Global Average Pooling of last hidden state [B, T_patches, 768] -> [B, 768]
return outputs.last_hidden_state.mean(dim=1)
# Factory
def load_extractor(name: str, device: str = 'cuda') -> BaseFeatureExtractor:
name = name.lower()
if name == 'i3d':
return I3DFeatureExtractor(device)
elif name == 'clip':
return CLIPFeatureExtractor(device)
elif name == 'videomae':
return VideoMAEFeatureExtractor(device)
else:
raise ValueError(f"Unknown extractor: {name}. Options: i3d, clip, videomae")
-384
View File
@@ -1,384 +0,0 @@
import numpy as np
import scipy.linalg
import torch
from pathlib import Path
from collections.abc import Iterator
import pickle
from dataclasses import dataclass, field
from .feature_extractors import BaseFeatureExtractor, load_extractor
from .video_utils import ClipSamplingStrategy, load_video_clips_streaming
def compute_statistics(features: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Compute mean and covariance."""
mu = np.mean(features, axis=0)
sigma = np.cov(features, rowvar=False)
return mu, sigma
def compute_frechet_distance(mu1: np.ndarray,
sigma1: np.ndarray,
mu2: np.ndarray,
sigma2: np.ndarray,
eps: float = 1e-6) -> float:
"""
Compute Fréchet distance between two Gaussians.
"""
sigma1 = sigma1 + eps * np.eye(sigma1.shape[0])
sigma2 = sigma2 + eps * np.eye(sigma2.shape[0])
diff = mu1 - mu2
mean_distance = np.sum(diff**2)
trace_sum = np.trace(sigma1 + sigma2)
covmean = scipy.linalg.sqrtm(sigma1 @ sigma2)
if np.iscomplexobj(covmean):
if not np.allclose(np.diagonal(covmean).imag, 0, atol=1e-3):
print(f"Warning: Imaginary component: {np.max(np.abs(covmean.imag))}")
covmean = covmean.real
trace_product = np.trace(covmean)
fvd = mean_distance + trace_sum - 2 * trace_product
return float(fvd)
@dataclass
class FVDConfig:
# default configuration for FVD computation:
# Video selection
num_videos: int = 2048
# Feature Extractor Selection
extractor_model: str = 'i3d' # Options: 'i3d', 'clip', 'videomae'
# Clip sampling
num_frames_per_clip: int = 16
num_clips_per_video: int = 1
clip_strategy: str | ClipSamplingStrategy = 'beginning'
# Temporal subsampling
frame_stride: int = 1 # 1=no subsampling, 2=every 2nd, 8=every 8th
temporal_stride: int = 1 # For sliding window clips
# Data processing
video_extensions: list[str] = field(default_factory=lambda: ['.mp4', '.avi', '.mov', '.mkv'])
support_frame_dirs: bool = True
# Computation
batch_size: int = 32
device: str = 'cuda'
use_streaming: bool = True
resize_before_extraction: bool = True
# Caching
cache_real_features: str | None = None
i3d_model_path: str | None = None
# Reproducibility
seed: int | None = None
@classmethod
def fvd2048_16f(cls) -> 'FVDConfig':
"""Standard FVD protocol: 2048 videos, 16 frames, beginning clip."""
return cls(num_videos=2048, num_frames_per_clip=16, clip_strategy='beginning', use_streaming=True)
@classmethod
def fvd2048_128f(cls) -> 'FVDConfig':
"""Long video protocol: 2048 videos, 128 frames."""
return cls(num_videos=2048, num_frames_per_clip=128, clip_strategy='beginning', use_streaming=True)
@classmethod
def quick_test(cls) -> 'FVDConfig':
"""Quick test config: 100 videos, 16 frames."""
return cls(num_videos=100, num_frames_per_clip=16, clip_strategy='beginning')
def to_dict(self) -> dict:
"""Export config to dict for logging"""
d = self.__dict__.copy()
d['clip_strategy'] = str(self.clip_strategy)
return d
def __str__(self) -> str:
"""Human-readable protocol name"""
desc = f"FVD_{self.extractor_model.upper()}_{self.num_videos}_{self.num_frames_per_clip}f"
if self.frame_stride > 1:
desc += f"_subsample{self.frame_stride}"
if self.num_clips_per_video > 1:
desc += f"_{self.num_clips_per_video}clips"
if self.clip_strategy != 'beginning':
desc += f"_{self.clip_strategy}"
return desc
def extract_features_streaming(video_generator: Iterator[torch.Tensor],
extractor: BaseFeatureExtractor,
batch_size: int = 32,
max_clips: int | None = None,
verbose: bool = True) -> np.ndarray:
"""
Extract features from a video clip generator using streaming.
"""
all_features = []
batch = []
if verbose:
print(f"Extracting features with batch_size={batch_size}...")
with torch.no_grad():
for clip_count, clip in enumerate(video_generator):
batch.append(clip)
# Process batch when full
if len(batch) == batch_size:
batch_tensor = torch.stack(batch).to(extractor.device)
features = extractor.extract_features_batch(batch_tensor)
all_features.append(features.detach().cpu().numpy())
batch = []
if verbose and clip_count % (batch_size * 10) == 0:
print(f"Processed {clip_count} clips...")
if max_clips is not None and clip_count >= max_clips:
break
# Process remaining clips
if len(batch) > 0:
batch_tensor = torch.stack(batch).to(extractor.device)
features = extractor.extract_features_batch(batch_tensor)
all_features.append(features.detach().cpu().numpy())
if len(all_features) == 0:
raise RuntimeError("No features extracted - check video loading")
features = np.concatenate(all_features, axis=0)
if verbose:
print(f"Extracted {len(features)} feature vectors")
return features
def load_or_compute_features(videos: str | Path | torch.Tensor,
extractor: BaseFeatureExtractor,
config: FVDConfig,
cache_path: str | None = None,
cache_name: str = "real_features") -> np.ndarray:
"""Load features from cache or compute (with streaming support)"""
if cache_path is not None:
script_dir = Path(__file__).parent
cache_dir = script_dir / cache_path
cache_file = cache_dir / f"{config.extractor_model}_{cache_name}.pkl"
if cache_file.exists():
print(f"Loading cached features from {cache_file}")
with open(cache_file, 'rb') as f:
features = pickle.load(f)
# Validate and limit based on config
max_features = config.num_videos * config.num_clips_per_video
if len(features) < max_features:
print(f"WARNING: Cache has {len(features)} features but need {max_features}")
print("Cached features insufficient - will recompute...")
elif len(features) > max_features:
print(f"Using {max_features} features from cache (truncated from {len(features)})")
features = features[:max_features]
return features
else:
print(f"Using all {len(features)} cached features")
return features
print("Computing features from scratch...")
if isinstance(videos, (str | Path)):
target_size = (224, 224) if config.resize_before_extraction else None
video_generator = load_video_clips_streaming(videos,
num_frames=config.num_frames_per_clip,
max_videos=config.num_videos,
clip_strategy=config.clip_strategy,
frame_stride=config.frame_stride,
num_clips_per_video=config.num_clips_per_video,
video_extensions=config.video_extensions,
support_frame_dirs=config.support_frame_dirs,
target_size=target_size,
verbose=True)
max_clips = config.num_videos * config.num_clips_per_video
features = extract_features_streaming(video_generator,
extractor,
batch_size=config.batch_size,
max_clips=max_clips,
verbose=True)
else:
print(f"Extracting features from {len(videos)} video tensors...")
features = extractor.extract_features(videos, batch_size=config.batch_size, verbose=True)
features = features.numpy()
# Validate feature count
expected_count = config.num_videos * config.num_clips_per_video
if len(features) < expected_count:
raise ValueError(f"ERROR: Only extracted {len(features)} features, but need {expected_count}!\n"
f"Found fewer videos than expected. Check your video directory.")
elif len(features) > expected_count:
print(f"Truncating {len(features)} features to {expected_count}")
features = features[:expected_count]
# Cache features if requested
if cache_path is not None:
script_dir = Path(__file__).parent
cache_dir = script_dir / cache_path
cache_dir.mkdir(parents=True, exist_ok=True)
cache_file = cache_dir / f"{config.extractor_model}_{cache_name}.pkl"
print(f"Caching features to {cache_file}")
with open(cache_file, 'wb') as f:
pickle.dump(features, f)
return features
def compute_fvd_with_config(real_videos: str | Path | torch.Tensor,
gen_videos: str | Path | torch.Tensor,
config: FVDConfig,
verbose: bool = True) -> dict:
"""
Compute FVD using a standardized configuration.
This is the recommended way to compute FVD for reproducibility.
Args:
real_videos: Path or tensors
gen_videos: Path or tensors
config: FVDConfig specifying protocol
verbose: Print progress
Returns:
results: Dictionary with:
- 'fvd': FVD score (float)
- 'protocol': Protocol name (str)
- 'model': Feature extractor model name (str)
- 'config': Configuration dict
Example:
>>> config = FVDConfig.fvd2048_16f()
>>> results = compute_fvd_with_config('data/real/', 'outputs/gen/', config)
>>> print(f"FVD: {results['fvd']:.2f}")
"""
# Seed for reproducibility
if config.seed is not None:
import random as _rnd
_rnd.seed(config.seed)
np.random.seed(config.seed)
torch.manual_seed(config.seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(config.seed)
if verbose:
print("=" * 70)
print(f"Computing FVD with protocol: {config}")
print(f"Model: {config.extractor_model.upper()}")
print("=" * 70)
print("\nConfiguration:")
for key, value in config.to_dict().items():
print(f" {key}: {value}")
print()
# Initialize Extractor using Factory
if verbose:
print(f"\nInitializing {config.extractor_model.upper()} model on {config.device}...")
extractor = load_extractor(config.extractor_model, device=config.device)
# Extract features
if verbose:
print(f"\n{'='*70}")
print("Extracting REAL video features...")
print(f"{'='*70}")
real_features = load_or_compute_features(videos=real_videos,
extractor=extractor,
config=config,
cache_path=config.cache_real_features,
cache_name="real_features")
if verbose:
print(f"\n{'='*70}")
print("Extracting GENERATED video features...")
print(f"{'='*70}")
gen_features = load_or_compute_features(videos=gen_videos,
extractor=extractor,
config=config,
cache_path=None,
cache_name="gen_features")
if verbose:
print(f"\nReal videos/clips: {len(real_features)}")
print(f"Generated videos/clips: {len(gen_features)}")
print(f"\n{'='*70}")
print("Computing statistics...")
print(f"{'='*70}")
mu_real, sigma_real = compute_statistics(real_features)
mu_gen, sigma_gen = compute_statistics(gen_features)
if verbose:
print(f"\n{'='*70}")
print("Computing Fréchet distance...")
print(f"{'='*70}")
fvd = compute_frechet_distance(mu_real, sigma_real, mu_gen, sigma_gen)
if verbose:
print(f"\n{'='*70}")
print(f"FVD Score ({config.extractor_model.upper()}): {fvd:.4f}")
print(f"Protocol: {config}")
print(f"{'='*70}\n")
results = {
'fvd': fvd,
'protocol': str(config),
'model': config.extractor_model,
'config': config.to_dict(),
}
return results
def compute_fvd(real_videos: str | Path | torch.Tensor,
gen_videos: str | Path | torch.Tensor,
num_frames: int = 16,
batch_size: int = 32,
device: str = 'cuda',
num_videos: int | None = 2048,
cache_real_features: str | None = None,
i3d_model_path: str | None = None,
seed: int | None = None,
verbose: bool = True) -> float:
"""
Backward compatibility wrapper for computing FVD (defaults to I3D).
"""
num_videos = num_videos if num_videos is not None else 2048
config = FVDConfig(
num_videos=num_videos,
num_frames_per_clip=num_frames,
extractor_model='i3d', # Default to I3D
batch_size=batch_size,
device=device,
cache_real_features=cache_real_features,
i3d_model_path=i3d_model_path,
seed=seed,
)
result = compute_fvd_with_config(real_videos, gen_videos, config, verbose)
return result['fvd']
-124
View File
@@ -1,124 +0,0 @@
"""I3D Feature Extractor for FVD Computation"""
import torch
import torch.nn as nn
import torch.nn.functional as F
from pathlib import Path
from huggingface_hub import hf_hub_download
from tqdm import tqdm
from contextlib import suppress
class I3DFeatureExtractor(nn.Module):
"""
I3D feature extractor for FVD computation.
Extracts 400-dimensional features from videos using I3D model
trained on Kinetics-400.
"""
REPO_ID = 'flateon/FVD-I3D-torchscript'
MODEL_FILENAME = 'i3d_torchscript.pt'
def __init__(self, device: str = 'cuda', cache_dir: str | Path | None = None):
super().__init__()
self.device_str = device
if device == 'cuda' and not torch.cuda.is_available():
print("Warning: CUDA requested but not available – falling back to CPU")
self.device = torch.device('cpu')
else:
self.device = torch.device(device)
self.cache_dir: str | None
if cache_dir is not None:
self.cache_dir = str(Path(cache_dir).resolve())
else:
self.cache_dir = None # Use HF default cache
self.model = self._load_model()
self.model.eval()
with suppress(Exception):
self.model.to(self.device)
def _load_model(self) -> torch.nn.Module:
"""Download and load I3D TorchScript model from Hugging Face Hub."""
print(f"Loading I3D model from Hugging Face Hub ({self.REPO_ID})...")
try:
# Download model from Hugging Face Hub
model_path = hf_hub_download(repo_id=self.REPO_ID, filename=self.MODEL_FILENAME, cache_dir=self.cache_dir)
# Load directly to chosen device
model = torch.jit.load(model_path, map_location=self.device)
print("I3D model loaded successfully")
return model
except Exception as e:
raise RuntimeError(f"Failed to load I3D model from Hugging Face Hub. Error: {e}\n"
f"Ensure you have internet connection and huggingface_hub installed:\n"
f"uv pip install huggingface_hub") from e
def preprocess(self, videos: torch.Tensor) -> torch.Tensor:
"""
Preprocess videos for I3D.
Args:
videos: [B, T, C, H, W], values in [0, 255]
Returns:
Preprocessed videos [B, C, T, 224, 224] (normalized and resized)
"""
B, T, C, H, W = videos.shape
if T < 10:
raise ValueError(f"I3D requires at least 10 frames, got {T}")
# Normalize to [0, 1] if needed
if videos.max() > 1.0:
videos = videos / 255.0
# Resize to 224x224 if needed
if H != 224 or W != 224:
videos = videos.reshape(B * T, C, H, W)
videos = F.interpolate(videos, size=(224, 224), mode='bilinear', align_corners=False)
videos = videos.reshape(B, T, C, 224, 224)
# Convert to [B, C, T, H, W] format
videos = videos.permute(0, 2, 1, 3, 4).contiguous()
return videos
@torch.no_grad()
def extract_features(self, videos: torch.Tensor, batch_size: int = 32, verbose: bool = True) -> torch.Tensor:
"""
Extract I3D features
Args:
videos: [N, T, C, H, W], values in [0, 255]
batch_size: Batch size for processing
verbose: Show progress bar
Returns:
Features [N, 400]
"""
N = len(videos)
all_features = []
iterator = range(0, N, batch_size)
if verbose:
iterator = tqdm(iterator, desc="Extracting I3D features")
for i in iterator:
batch = videos[i:i + batch_size].to(self.device)
batch = self.preprocess(batch) # Now returns [B, C, T, H, W]
# Use the HF model without rescale/resize (we handle it in preprocess)
features = self.model(batch, rescale=False, resize=False, return_features=True)
all_features.append(features.cpu())
return torch.cat(all_features, dim=0)
def __call__(self, videos: torch.Tensor, batch_size: int = 32) -> torch.Tensor:
return self.extract_features(videos, batch_size=batch_size)
-51
View File
@@ -1,51 +0,0 @@
import sys
from pathlib import Path
root_dir = Path(__file__).parent.parent.parent
sys.path.insert(0, str(root_dir))
from benchmarks.fvd.fvd import FVDConfig, compute_fvd_with_config # noqa: E402
def main() -> None:
script_dir = Path(__file__).parent.resolve()
# Define directories
real_dir = "benchmarks/data/real_videos"
gen_dir = "benchmarks/data/generated_videos"
# Compare all 3 models
models_to_test = ['i3d', 'clip', 'videomae']
print(f"\n{'='*60}")
print("STARTING COMPARISON BENCHMARK")
print(f"{'='*60}")
for model_name in models_to_test:
print(f"\n>>> Running evaluation with {model_name.upper()}...")
try:
cfg = FVDConfig(
num_videos=650,
num_frames_per_clip=16,
extractor_model=model_name,
clip_strategy='beginning',
device='cuda',
seed=42,
# Use separate cache folders for each model to avoid conflicts
cache_real_features=str(script_dir / f'fvd-cache/{model_name}'),
)
results = compute_fvd_with_config(real_dir, gen_dir, cfg, verbose=False)
print(f"FVD: {results['fvd']}\nModel: {results['model']}")
except Exception as e:
print(f"{model_name.upper()} Failed: {e}")
print(f"\n{'='*60}")
print("BENCHMARK COMPLETE")
print(f"{'='*60}")
if __name__ == '__main__':
main()
-89
View File
@@ -1,89 +0,0 @@
#!/usr/bin/env python3
import sys
from pathlib import Path
import shutil
import random
from fvd import compute_fvd_with_config, FVDConfig
script_path = Path(__file__).resolve()
fastvideo_root = script_path.parent.parent.parent
sys.path.insert(0, str(fastvideo_root))
def split_videos(video_dir: Path, n_per_subset: int = 128, seed: int = 42):
subset_a = video_dir.parent / 'bair_full_subset_A'
subset_b = video_dir.parent / 'bair_full_subset_B'
if subset_a.exists():
shutil.rmtree(subset_a)
if subset_b.exists():
shutil.rmtree(subset_b)
subset_a.mkdir(parents=True)
subset_b.mkdir(parents=True)
videos = sorted(video_dir.glob('*.mp4'))
random.seed(seed)
shuffled = list(videos)
random.shuffle(shuffled)
needed = n_per_subset * 2
if len(shuffled) > needed:
shuffled = shuffled[:needed]
mid = len(shuffled) // 2
print(f"\nSplitting {len(shuffled)} BAIR FULL videos:")
print(f" Subset A: {mid} videos")
print(f" Subset B: {len(shuffled) - mid} videos")
for v in shuffled[:mid]:
shutil.copy2(v, subset_a / v.name)
for v in shuffled[mid:]:
shutil.copy2(v, subset_b / v.name)
return subset_a, subset_b, mid
def validate_fvd(subset_a: Path, subset_b: Path, num_videos: int):
config = FVDConfig(num_videos=num_videos,
num_frames_per_clip=16,
clip_strategy='beginning',
batch_size=8,
device='cuda',
seed=42)
print("\n" + "=" * 70)
print("TEST 1: Identity Test")
print("=" * 70)
result1 = compute_fvd_with_config(real_videos=str(subset_a), gen_videos=str(subset_a), config=config, verbose=False)
fvd_identity = result1['fvd']
print(f"\nIdentity FVD: {fvd_identity:.2f}")
print("\n" + "=" * 70)
print("TEST 2: Real vs Real")
print("=" * 70)
result2 = compute_fvd_with_config(real_videos=str(subset_a), gen_videos=str(subset_b), config=config, verbose=False)
fvd_real = result2['fvd']
print(f"\nReal vs Real FVD: {fvd_real:.2f}")
print("\n" + "=" * 70)
print("RESULTS")
print("=" * 70)
print(f"Identity: {fvd_identity:.2f}")
print(f"Real vs Real: {fvd_real:.2f}")
def main() -> None:
bair_dir = Path('benchmarks/data/bair_full_videos')
subset_a, subset_b, count = split_videos(bair_dir, n_per_subset=128, seed=42)
validate_fvd(subset_a, subset_b, count)
if __name__ == '__main__':
main()
-460
View File
@@ -1,460 +0,0 @@
import torch
import cv2
import numpy as np
from pathlib import Path
from collections.abc import Iterator
from tqdm import tqdm
from enum import Enum
class ClipSamplingStrategy(Enum):
"""Clip sampling strategies for FVD evaluation."""
BEGINNING = 'beginning' # Take first N frames (most common)
RANDOM = 'random' # Random N consecutive frames
UNIFORM = 'uniform' # Uniformly spaced frames across video
MIDDLE = 'middle' # Middle N frames
SLIDING = 'sliding' # Multiple sliding windows
ALL = 'all' # All possible clips
def _load_video_cv2(video_path: str | Path,
num_frames: int | None = 16,
sample_strategy: str = 'uniform') -> torch.Tensor:
"""
Load video from video file using OpenCV.
Args:
video_path: Path to video file (MP4, AVI, MOV, MKV)
num_frames: Number of frames to extract
sample_strategy: 'uniform' or 'random'
Returns:
video: [T, C, H, W]
"""
video_path = str(video_path)
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise RuntimeError(f"Cannot open video: {video_path}")
frames = []
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
if num_frames is None:
# Read all available frames
while True:
ret, frame = cap.read()
if not ret:
break
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frames.append(frame)
cap.release()
if len(frames) == 0:
raise RuntimeError(f"Video has 0 frames: {video_path}")
frames = np.stack(frames) # [T, H, W, C]
frames = torch.from_numpy(frames).permute(0, 3, 1, 2).float() # [T, C, H, W]
return frames
if total_frames == 0:
raise RuntimeError(f"Video has 0 frames: {video_path}")
# Determine frame indices for sampling
if total_frames < num_frames:
frame_indices = list(range(total_frames)) + [total_frames - 1] * (num_frames - total_frames)
elif sample_strategy == 'uniform':
frame_indices = np.linspace(0, total_frames - 1, num_frames, dtype=int).tolist()
elif sample_strategy == 'random':
frame_indices = sorted(np.random.choice(total_frames, num_frames, replace=False))
else:
raise ValueError(f"Unknown sample_strategy: {sample_strategy}")
# Extract frames
for idx in frame_indices:
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
ret, frame = cap.read()
if not ret:
if len(frames) > 0:
frames.append(frames[-1].copy())
else:
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
frames.append(np.zeros((h, w, 3), dtype=np.uint8))
continue
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frames.append(frame)
cap.release()
frames = np.stack(frames) # [T, H, W, C]
frames = torch.from_numpy(frames).permute(0, 3, 1, 2).float() # [T, C, H, W]
return frames
def _load_video_from_frames(frame_dir: str | Path,
num_frames: int | None = 16,
sample_strategy: str = 'uniform',
frame_extensions: list[str] | None = None) -> torch.Tensor:
"""
Load video from directory of frame images.
Args:
frame_dir: Directory containing frames
num_frames: Number of frames to sample
sample_strategy: 'uniform' or 'random'
frame_extensions: Image file extensions to look for
Returns:
video: [T, C, H, W]
"""
if frame_extensions is None:
frame_extensions = ['.jpg', '.png', '.jpeg', '.bmp']
frame_dir = Path(frame_dir)
if not frame_dir.exists():
raise FileNotFoundError(f"Frame directory not found: {frame_dir}")
# Find all frames
frame_files: list[Path] = []
for ext in frame_extensions:
frame_files.extend(frame_dir.glob(f"*{ext}"))
if len(frame_files) == 0:
raise ValueError(f"No frames found in {frame_dir} with extensions {frame_extensions}")
frame_files = sorted(frame_files, key=lambda x: x.name)
total_frames = len(frame_files)
# Determine frame indices
if num_frames is None:
frame_indices = list(range(total_frames))
else:
if total_frames < num_frames:
frame_indices = list(range(total_frames)) + [total_frames - 1] * (num_frames - total_frames)
elif sample_strategy == 'uniform':
frame_indices = np.linspace(0, total_frames - 1, num_frames, dtype=int).tolist()
elif sample_strategy == 'random':
frame_indices = sorted(np.random.choice(total_frames, num_frames, replace=False))
else:
raise ValueError(f"Unknown sample_strategy: {sample_strategy}")
# Load frames
frames = []
for idx in frame_indices:
frame_path = frame_files[idx]
frame = cv2.imread(str(frame_path))
if frame is None:
raise RuntimeError(f"Failed to load frame: {frame_path}")
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frames.append(frame)
# Stack and convert to tensor
frames = np.stack(frames) # [T, H, W, C]
frames = torch.from_numpy(frames).permute(0, 3, 1, 2).float() # [T, C, H, W]
return frames
def _detect_video_format(path: str | Path) -> str:
"""
Detect if path is a video file or frame directory.
Returns:
'video_file', 'frame_directory', or 'unknown'
"""
path = Path(path)
if path.is_file():
return 'video_file'
elif path.is_dir():
# Check if contains image files
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp']
for ext in image_extensions:
if list(path.glob(f"*{ext}")):
return 'frame_directory'
return 'unknown'
else:
raise ValueError(f"Path does not exist: {path}")
def load_video_auto(video_path: str | Path,
num_frames: int | None = 16,
sample_strategy: str = 'uniform') -> torch.Tensor:
"""
Automatically detect format and load video.
Supports:
- Video files (MP4, AVI, MOV, MKV)
- Frame directories (JPG, PNG)
Args:
video_path: Path to video file or frame directory
num_frames: Number of frames to extract
sample_strategy: 'uniform' or 'random'
Returns:
video: [T, C, H, W]
"""
format_type = _detect_video_format(video_path)
if format_type == 'video_file':
return _load_video_cv2(video_path, num_frames, sample_strategy)
elif format_type == 'frame_directory':
return _load_video_from_frames(video_path, num_frames, sample_strategy)
else:
raise ValueError(f"Unknown video format at {video_path}")
def sample_clips_from_video(video: torch.Tensor,
num_frames_per_clip: int = 16,
num_clips: int = 1,
strategy: str | ClipSamplingStrategy = ClipSamplingStrategy.BEGINNING,
frame_stride: int = 1,
temporal_stride: int = 1) -> list[torch.Tensor]:
"""
Sample clips from a video with various strategies.
Args:
video: [T, C, H, W] full video
num_frames_per_clip: Frames per clip
num_clips: Number of clips to extract
strategy: ClipSamplingStrategy or string ('beginning', 'random', etc.)
frame_stride: Skip frames (FPS control: 1=all, 2=every 2nd, 8=every 8th)
temporal_stride: Stride between clips for sliding window
Returns:
List of clips, each [num_frames_per_clip, C, H, W]
Examples:
>>> # Beginning clip (most common for FVD)
>>> clips = sample_clips_from_video(video, 16, strategy='beginning')
>>> # Multiple random clips
>>> clips = sample_clips_from_video(video, 16, num_clips=4, strategy='random')
>>> # Subsample FPS by 2x (every 2nd frame)
>>> clips = sample_clips_from_video(video, 16, frame_stride=2)
>>> # Sliding window with overlap
>>> clips = sample_clips_from_video(video, 16, strategy='sliding', temporal_stride=8)
"""
# Convert string to enum if needed
if isinstance(strategy, str):
strategy = ClipSamplingStrategy(strategy)
T, C, H, W = video.shape
# Apply frame stride (FPS subsampling)
if frame_stride > 1:
video = video[::frame_stride]
T = len(video)
effective_clip_length = num_frames_per_clip
# Handle videos shorter than clip length
if effective_clip_length > T:
pad_length = effective_clip_length - T
last_frame = video[-1:].repeat(pad_length, 1, 1, 1)
video = torch.cat([video, last_frame], dim=0)
T = len(video)
clips = []
if strategy == ClipSamplingStrategy.BEGINNING:
# Take first clip (most common for FVD evaluation)
clip = video[:effective_clip_length]
clips.append(clip)
elif strategy == ClipSamplingStrategy.MIDDLE:
# Take middle clip
start = (T - effective_clip_length) // 2
clip = video[start:start + effective_clip_length]
clips.append(clip)
elif strategy == ClipSamplingStrategy.RANDOM:
# Sample N random clips
for _ in range(num_clips):
start = 0 if effective_clip_length == T else np.random.randint(0, T - effective_clip_length + 1)
clip = video[start:start + effective_clip_length]
clips.append(clip)
elif strategy == ClipSamplingStrategy.UNIFORM:
# Uniformly spaced clips
if num_clips == 1:
# Single clip from middle
start = (T - effective_clip_length) // 2
clip = video[start:start + effective_clip_length]
clips.append(clip)
else:
# Multiple uniformly spaced clips
step = (T - effective_clip_length) / (num_clips - 1) if num_clips > 1 else 0
for i in range(num_clips):
start = int(i * step)
start = min(start, T - effective_clip_length)
clip = video[start:start + effective_clip_length]
clips.append(clip)
elif strategy == ClipSamplingStrategy.SLIDING:
# Sliding window with stride
for start in range(0, T - effective_clip_length + 1, temporal_stride):
clip = video[start:start + effective_clip_length]
clips.append(clip)
if len(clips) >= num_clips:
break
elif strategy == ClipSamplingStrategy.ALL:
# All possible clips (overlapping)
for start in range(T - effective_clip_length + 1):
clip = video[start:start + effective_clip_length]
clips.append(clip)
else:
raise ValueError(f"Unknown strategy: {strategy}")
return clips
def load_video_clips_streaming(directory: str | Path,
num_frames: int = 16,
max_videos: int | None = None,
clip_strategy: str
| ClipSamplingStrategy = 'beginning',
frame_stride: int = 1,
num_clips_per_video: int = 1,
video_extensions: list[str] | None = None,
support_frame_dirs: bool = True,
target_size: tuple[int, int] | None = (224, 224),
verbose: bool = True) -> Iterator[torch.Tensor]:
"""
This generator yields clips one-by-one instead of loading all videos into RAM.
Perfect for large datasets where memory is limited.
Args:
directory: Path to directory with videos
num_frames: Frames per clip
max_videos: Max videos to load
clip_strategy: 'beginning', 'random', 'uniform', etc.
frame_stride: Frame skip (1=all, 2=every 2nd, 8=every 8th)
num_clips_per_video: Number of clips per video
video_extensions: Video file extensions
support_frame_dirs: Also load frame directories
target_size: Resize clips to (H, W). If None, keep original size.
verbose: Show progress
Yields:
clip: [T, C, H, W] individual clips
Example:
>>> for clip in load_video_clips_streaming('data/videos/', num_frames=16):
>>> features = model.extract_features(clip.unsqueeze(0))
>>> # Process one clip at a time - low memory usage!
"""
if video_extensions is None:
video_extensions = ['.mp4', '.avi', '.mov', '.mkv']
directory = Path(directory)
if not directory.exists():
raise FileNotFoundError(f"Directory not found: {directory}")
# Find video paths
video_paths: list[Path] = []
# Find video files
for ext in video_extensions:
video_paths.extend(directory.glob(f"**/*{ext}"))
# Find frame directories if enabled
if support_frame_dirs:
for subdir in directory.iterdir():
if subdir.is_dir():
# Check if it contains frames
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp']
for ext in image_extensions:
if list(subdir.glob(f"*{ext}")):
video_paths.append(subdir)
break
if len(video_paths) == 0:
raise ValueError(f"No videos found in {directory}")
video_paths = sorted(video_paths)
if max_videos is not None:
video_paths = video_paths[:max_videos]
if verbose:
print(f"Found {len(video_paths)} videos in {directory}")
if num_clips_per_video > 1:
print(f"Extracting {num_clips_per_video} clips per video...")
if frame_stride > 1:
print(f"Subsampling frames with stride {frame_stride}...")
if target_size:
print(f"Resizing clips to {target_size}...")
# Track statistics
failed_count = 0
total_clips = 0
iterator = tqdm(video_paths, desc="Loading videos") if verbose else video_paths
for video_path in iterator:
try:
# Load full video
video = load_video_auto(video_path, num_frames=None, sample_strategy='uniform')
# Sample clips from video
clips = sample_clips_from_video(video,
num_frames_per_clip=num_frames,
num_clips=num_clips_per_video,
strategy=clip_strategy,
frame_stride=frame_stride)
if target_size is not None:
resized_clips = []
for clip in clips:
T, C, H, W = clip.shape
if target_size != (H, W):
# Resize to target size
clip = clip.contiguous() # Fix non-contiguous tensors first
clip_flat = clip.view(T * C, H, W).unsqueeze(0) # [1, T*C, H, W]
clip_resized = torch.nn.functional.interpolate(clip_flat,
size=target_size,
mode='bilinear',
align_corners=False)
clip = clip_resized.squeeze(0).view(T, C, target_size[0],
target_size[1]) # Back to [T, C, H, W]
resized_clips.append(clip)
clips = resized_clips
# Yield clips one by one
for clip in clips:
yield clip
total_clips += 1
# Free memory
del video, clips
except Exception as e:
failed_count += 1
if verbose:
print(f"\nWarning: Failed to load {video_path}: {e}")
continue
# Validate
if total_clips == 0:
raise RuntimeError(f"Failed to load any videos from {directory}")
failure_rate = failed_count / len(video_paths)
if failure_rate > 0.1: # More than 10% failed
print(f"\nWARNING: {failure_rate:.1%} of videos failed to load ({failed_count}/{len(video_paths)})")
if verbose:
print(f"\nSuccessfully loaded {total_clips} clips from {len(video_paths) - failed_count} videos")
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# 1. Install missing dependency
uv pip install -q opencv-python-headless transformers huggingface_hub
# 2. Run FVD script
python benchmarks/fvd/run_fvd.py
-4
View File
@@ -1,4 +0,0 @@
#!/bin/bash
# 1. Install missing dependency
uv pip install -q opencv-python-headless
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env bash
# FastVideo (trackwan_bidir) env — CORRECTED build. CUDA 12.9 container, aarch64 + GB200 (sm_100a).
# Ordering fix: install repo-pinned torch (2.11.0 cu128) FIRST, build the local Blackwell kernel
# against it, then core deps (relaxed kernel pin so the local 0.3.0 satisfies), then FA4.
set +e
say(){ echo; echo "==================== $* ===================="; }
export DEBIAN_FRONTEND=noninteractive
export CUDA_HOME=${CUDA_HOME:-/usr/local/cuda}
export PATH=$CUDA_HOME/bin:/mnt/lustre/vlm-s4duan/bin:$PATH
export LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH
export UV_CACHE_DIR=/mnt/lustre/vlm-s4duan/.uv/cache
export UV_PYTHON_INSTALL_DIR=/mnt/lustre/vlm-s4duan/.uv/python
export TORCH_CUDA_ARCH_LIST=10.0a
export MAX_JOBS=${MAX_JOBS:-64}
cd /mnt/lustre/vlm-s4duan/FastVideo || { echo NO_REPO; exit 1; }
say "ENSURE toolchain"
if ! command -v g++ >/dev/null || ! command -v cmake >/dev/null || ! command -v ninja >/dev/null; then
apt-get update -y >/tmp/apt.log 2>&1 && apt-get install -y --no-install-recommends g++ gcc make cmake ninja-build git ca-certificates >>/tmp/apt.log 2>&1 && echo "apt ok" || { echo APT_FAIL; tail -20 /tmp/apt.log; }
fi
nvcc --version | tail -2
say "FRESH venv (python 3.12 on Lustre)"
rm -rf .venv
uv venv --python 3.12 --seed .venv || exit 1
source .venv/bin/activate
python --version
say "STAGE 1: torch 2.11.0 cu128 (repo pin, Blackwell-capable)"
uv pip install --torch-backend=cu128 torch==2.11.0 torchvision torchaudio 2>&1 | tail -15
python -c "import torch;print('TORCH',torch.__version__,torch.version.cuda,'avail',torch.cuda.is_available())" || echo TORCH_FAIL
say "STAGE 2: submodules + build local fastvideo-kernel (sm_100a) against torch 2.11"
git submodule update --init --recursive fastvideo-kernel/include/cutlass fastvideo-kernel/include/tk 2>&1 | tail -5
( cd fastvideo-kernel && TORCH_CUDA_ARCH_LIST=10.0a bash ./build.sh ) 2>&1 | tail -20
echo "KERNEL_EXIT=$?"
python -c "import fastvideo_kernel;print('kernel import OK')" || echo KERNEL_IMPORT_FAIL
say "STAGE 3: core deps uv pip install -e .[dev] (local kernel already satisfies relaxed pin)"
uv pip install -e ".[dev]" 2>&1 | tail -40
echo "CORE_EXIT=$?"
python -c "import fastvideo;print('fastvideo import OK')" || echo FASTVIDEO_IMPORT_FAIL
say "STAGE 4: FA4 — OFFICIAL Dao-AILab flash_attn/cute (NOT the XOR-op fork; fork is stale/broken)"
# Package name is flash-attn-4; pass the bare git URL (name-qualified spec is rejected).
# --prerelease allow lets it pull the matching nvidia-cutlass-dsl==4.6.0.dev0 + quack>=0.5.3.
uv pip install --prerelease allow \
"git+https://github.com/Dao-AILab/flash-attention.git#subdirectory=flash_attn/cute" 2>&1 | tail -25
python -c "from flash_attn.cute.interface import _flash_attn_fwd,_flash_attn_bwd; print('FA4_CUTE_OK')" 2>&1
say "VERIFY"
python - <<'PY'
import importlib, torch
print("torch", torch.__version__, torch.version.cuda, "cuda_avail", torch.cuda.is_available())
if torch.cuda.is_available():
print("device0", torch.cuda.get_device_name(0), "cc", torch.cuda.get_device_capability(0))
x = torch.randn(2048, 2048, device="cuda", dtype=torch.bfloat16)
y = (x @ x); torch.cuda.synchronize()
print("bf16 matmul on GPU OK", tuple(y.shape))
for m in ["fastvideo", "fastvideo_kernel", "flash_attn"]:
try: importlib.import_module(m); print("import OK:", m)
except Exception as e: print("import FAIL:", m, repr(e)[:180])
try:
from flash_attn.cute.interface import _flash_attn_fwd; print("FA4 flash_attn.cute.interface OK")
except Exception as e:
print("FA4 cute interface:", repr(e)[:180])
PY
say "DONE build_env.sh (v2)"
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set +e
export HF_HOME=/mnt/lustre/vlm-s4duan/.hf
export TOKENIZERS_PARALLELISM=false
cd /mnt/lustre/vlm-s4duan/FastVideo || exit 1
source .venv/bin/activate
BASE=/mnt/lustre/vlm-s4duan/models/Wan2.1-Fun-1.3B-InP-Diffusers
OUT=/mnt/lustre/vlm-s4duan/models/trackwan_1.3b_init
mkdir -p /mnt/lustre/vlm-s4duan/models
echo "==================== download Fun-InP diffusers base ===================="
python - <<PY
from huggingface_hub import snapshot_download
p = snapshot_download("weizhou03/Wan2.1-Fun-1.3B-InP-Diffusers", local_dir="$BASE")
print("downloaded to", p)
PY
echo "DL_EXIT=$?"
echo "==================== base transformer sanity (I2V => in_channels 36) ===================="
python - <<PY
import json
c = json.load(open("$BASE/transformer/config.json"))
print("in_channels:", c.get("in_channels"), "image_dim:", c.get("image_dim"), "num_layers:", c.get("num_layers"), "hidden:", c.get("hidden_size") or c.get("dim"))
PY
echo "==================== convert_trackwan_init.py ===================="
python data_pipeline/convert_trackwan_init.py --base "$BASE" --out "$OUT"
echo "CONVERT_EXIT=$?"
echo "==================== trackwan init result ===================="
ls -la "$OUT" 2>&1 | head -25
python - <<PY
import json
c = json.load(open("$OUT/transformer/config.json"))
print("trackwan in_channels:", c.get("in_channels"), "| has track_config:", "track_config" in c)
PY
echo "==================== DONE build_trackwan_init ===================="
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
# Phase 0, step 0 — carve a SMALL overfit dataset out of the 720p openvid parquets.
# Symlinks a few data_chunk parquets into a dedicated dir; the map-style loader walks it for
# *.parquet, so a couple of chunks (~32 clips each) is a good overfit set. Non-destructive
# (symlinks only). Re-run to rebuild; it clears the stale map_style_cache.
set -uo pipefail
SRC_ROOT=${SRC_ROOT:-/home/hal-shared/motionstream/data/openvid-wantrack-parquets}
SRC_SHARD=${SRC_SHARD:-shard000}
N_CHUNKS=${N_CHUNKS:-2} # ~32 clips/chunk -> ~64 clips
OUT=${OUT:-/home/hal-kevin/data/motion-stream-test/overfit_subset_720p/combined_parquet_dataset}
src_worker="$SRC_ROOT/$SRC_SHARD/combined_parquet_dataset/worker_0"
[ -d "$src_worker" ] || { echo "[subset] source not found: $src_worker" >&2; exit 1; }
dst_worker="$OUT/worker_0"
rm -rf "$OUT" # drop old subset + its map_style_cache
mkdir -p "$dst_worker"
n=0
for f in $(ls "$src_worker"/data_chunk_*.parquet | sort -V | head -n "$N_CHUNKS"); do
ln -s "$(readlink -f "$f")" "$dst_worker/$(basename "$f")"
n=$((n + 1))
done
echo "[subset] linked $n parquet chunk(s) from $SRC_SHARD -> $OUT"
python - "$OUT" <<'PY'
import glob, sys, pyarrow.parquet as pq
fs = glob.glob(f"{sys.argv[1]}/**/*.parquet", recursive=True)
rows = sum(pq.ParquetFile(f).metadata.num_rows for f in fs)
print(f"[subset] {len(fs)} file(s), {rows} clips total")
PY
echo "[subset] point the overfit config data_path at: $OUT"
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Phase 0, step 1 — build the d64 + bias 14B WanTrack init for the overfit.
# CPU only, needs ~62GB RAM (loads the 14B base) -> run on a COMPUTE node, not the login node.
# pretrained channels are preserved; the added track slot is ZERO-init (--pe-init zero, matching
# upstream's trackwan_14b_i2v_d64_zero_init_bias) and the track_encoder gets default init WITH bias
# (--use-bias-defaults), matching TRACKWAN_TRACK_BIAS=1 at train time.
set -uo pipefail
cd ~/FastVideo
BASE=${BASE:-/home/hal-kevin/models/Wan2.1-I2V-14B-720P-Diffusers}
OUT=${OUT:-/home/hal-kevin/models/trackwan_14b_i2v_d64_bias_init}
ID_DIM=${ID_DIM:-64}
PE_INIT=${PE_INIT:-zero}
python data_pipeline/convert_trackwan_init_v2.py \
--base "$BASE" \
--out "$OUT" \
--id-dim "$ID_DIM" \
--pe-init "$PE_INIT" \
--use-bias-defaults
echo "[init] built $OUT (id_dim=$ID_DIM, pe-init=$PE_INIT, bias=on)"
echo "[init] expect: 'added 4 track_encoder tensors' (2 weights + 2 bias)"
python -c "
from safetensors import safe_open
ks=[k for k in safe_open('$OUT/transformer/diffusion_pytorch_model.safetensors','pt').keys() if 'track_encoder' in k]
import json; c=json.load(open('$OUT/transformer/config.json'))
print('[init] in_channels', c['in_channels'], '| track_config.id_dim', c['track_config']['id_dim'])
print('[init] track_encoder keys:', sorted(ks))
"
+56
View File
@@ -0,0 +1,56 @@
#!/bin/bash
# Phase 0, step 2 — overfit the track pathway. 14B/720p, d64+bias, sparse, HEAD TRAINABLE.
# Run the SAME command on BOTH racks (2x4 GB200). MASTER_ADDR = rack0 host on both; NODE_RANK
# differs (0 on rack0, 1 on rack1). STAGE=A uses fixed track IDs (lock onto the pattern), STAGE=B
# uses random IDs; run A first, then B (B resumes A's checkpoint via resume_from_checkpoint: latest).
#
# rack0: MASTER_ADDR=hpc-rack-1-6 NODE_RANK=0 STAGE=A bash data_pipeline/720_stage_1/02_run_overfit.sh
# rack1: MASTER_ADDR=hpc-rack-1-6 NODE_RANK=1 STAGE=A bash data_pipeline/720_stage_1/02_run_overfit.sh
# Extra args pass through to the trainer, e.g. ... STAGE=B bash 02_run_overfit.sh --training.loop.max_train_steps 1600
set -uo pipefail
cd ~/FastVideo
: "${MASTER_ADDR:?set MASTER_ADDR to rack-0 hostname (reachable from both racks)}"
: "${NODE_RANK:?set NODE_RANK: 0 on the master rack, 1 on the other}"
STAGE=${STAGE:-A}
MASTER_PORT=${MASTER_PORT:-29502}
CFG=data_pipeline/720_stage_1/finetune_wantrack_overfit_14b_720p_d64_bias.yaml
# Stage A = deterministic track IDs (fixed sampling); Stage B = random IDs.
# B writes to its OWN output dir (keeps A's dir pristine, A/B checkpoints separated) — mirrors
# upstream chain_stepAB.sh + run_stepB_seed.sh. Seed B's dir once with A's final checkpoint via
# 02b_seed_stageB.sh BEFORE launching B, so resume_from_checkpoint: latest picks up A's weights.
OUT_A=/home/hal-kevin/data/motion-stream-test/overfit_14b_720p_d64_bias_out
OUT_B=/home/hal-kevin/data/motion-stream-test/overfit_14b_720p_d64_bias_stageB_out
case "$STAGE" in
A) FIXED=1; OUT_DIR=$OUT_A ;;
B) FIXED=0; OUT_DIR=$OUT_B ;;
*) echo "STAGE must be A or B (got '$STAGE')" >&2; exit 1 ;;
esac
if [ "$STAGE" = B ] && [ ! -d "$OUT_B" ]; then
echo "[overfit] ERROR: STAGE=B but $OUT_B does not exist." >&2
echo " Seed it first (run ONCE): bash data_pipeline/720_stage_1/02b_seed_stageB.sh" >&2
exit 1
fi
# Pin NCCL to the four active 400G InfiniBand HCAs (keep off the 200G Ethernet port).
export NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_3,mlx5_4
# export NCCL_DEBUG=INFO # uncomment on the FIRST launch to confirm NET/IB, then re-comment.
FASTVIDEO_FA4=1 \
TRACKWAN_TRACK_BIAS=1 WANTRACK_FREEZE_HEAD=0 \
WANTRACK_AUG=1 WANTRACK_SPARSE=1 WANTRACK_EXTRA_RANDOM=20 \
WANTRACK_PMASK=0 WANTRACK_MASK_CHUNK=0 \
WANTRACK_IMAGE_COND=0 \
WANTRACK_FIXED_SAMPLE=${FIXED} \
torchrun \
--nnodes=2 --nproc_per_node=4 --node_rank=${NODE_RANK} \
--rdzv_id=overfit_14b_720p --rdzv_backend=c10d --rdzv_endpoint=${MASTER_ADDR}:${MASTER_PORT} \
--log-dir ~/FastVideo/torchrun_logs \
-m fastvideo.train.entrypoint.train \
--config ${CFG} \
--training.checkpoint.output_dir ${OUT_DIR} \
--training.tracker.run_name overfit-14b-720p-d64-bias-stage${STAGE} \
--training.checkpoint.resume_from_checkpoint latest \
"$@" \
2>&1 | tee -a data_pipeline/720_stage_1/overfit_node${NODE_RANK}_stage${STAGE}.log
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# Seed Stage B's output dir with Stage A's FINAL checkpoint, so B runs in its own dir while its
# config still uses resume_from_checkpoint: latest (which is what makes crash-restarts safe).
# Mirrors upstream examples/train/run_stepB_seed.sh.
#
# Run this ONCE (not per-rack) on the shared filesystem, AFTER Stage A finishes, BEFORE launching
# Stage B. Hardlinked (cp -al), not copied: a 14B training-state checkpoint is 100s of GB, and both
# dirs are on the same filesystem (/home), so hardlinks are instant and use no extra space.
#
# bash data_pipeline/720_stage_1/02b_seed_stageB.sh
# # then, on BOTH racks:
# MASTER_ADDR=hpc-rack-1-6 NODE_RANK=0 STAGE=B bash data_pipeline/720_stage_1/02_run_overfit.sh \
# --training.loop.max_train_steps 3000
set -uo pipefail
OUT_A=${OUT_A:-/home/hal-kevin/data/motion-stream-test/overfit_14b_720p_d64_bias_out}
OUT_B=${OUT_B:-/home/hal-kevin/data/motion-stream-test/overfit_14b_720p_d64_bias_stageB_out}
# Pick A's latest COMPLETE checkpoint (dcp/.metadata present) — refuse to seed off a partial save.
STEP=${STEP:-}
if [ -z "$STEP" ]; then
for d in $(for c in "$OUT_A"/checkpoint-*; do n=${c##*checkpoint-}; echo "$n"; done | sort -rn); do
[ -f "$OUT_A/checkpoint-$d/dcp/.metadata" ] && { STEP=$d; break; }
done
fi
[ -n "$STEP" ] || { echo "[B-seed] no complete checkpoint found under $OUT_A" >&2; exit 1; }
SRC="$OUT_A/checkpoint-$STEP"
DST="$OUT_B/checkpoint-$STEP"
[ -f "$SRC/dcp/.metadata" ] || { echo "[B-seed] $SRC incomplete (no dcp/.metadata) — refusing" >&2; exit 1; }
mkdir -p "$OUT_B"
if [ -e "$DST" ]; then
echo "[B-seed] $DST already exists — leaving it alone"
else
echo "[B-seed] hardlinking $SRC -> $DST"
cp -al "$SRC" "$DST"
fi
echo "[B-seed] Stage B dir now seeded at step $STEP:"
for c in "$OUT_B"/checkpoint-*; do echo " $c"; done
echo "[B-seed] Launch B with --training.loop.max_train_steps > $STEP (e.g. $((STEP + 1000)))."
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Phase 0, step 3 — export the overfit DCP checkpoint to a diffusers model dir.
# The Phase-1 merge (convert_trackwan_init_v2.py --track-src/--pe-src) reads this diffusers dir.
# Only 1 GPU needed (DCP reshards automatically). Run on a compute node.
set -uo pipefail
cd ~/FastVideo
# --checkpoint accepts an output_dir (auto-picks the latest checkpoint-<step>), a specific
# checkpoint-<step> dir, or its dcp/ subdir.
# Default = the Stage-B dir (the FINAL overfit; B refines A and is what the merge lifts). Override
# CKPT to the Stage-A dir only if you deliberately want to export A.
CKPT=${CKPT:-/home/hal-kevin/data/motion-stream-test/overfit_14b_720p_d64_bias_stageB_out}
OUT=${OUT:-/home/hal-kevin/models/overfit14b_export}
CFG=${CFG:-data_pipeline/720_stage_1/finetune_wantrack_overfit_14b_720p_d64_bias.yaml}
# TRACKWAN_TRACK_BIAS=1 MUST match training: it toggles bias on track_encoder.{proj,temporal_conv}
# at build time (track_encoder.py:73). The overfit trained with bias=1, so the checkpoint carries
# those bias params; building bias-less here fails to load them ("track_encoder.proj.bias not found").
TRACKWAN_TRACK_BIAS=1 \
python -m fastvideo.train.entrypoint.dcp_to_diffusers \
--checkpoint "$CKPT" \
--output-dir "$OUT" \
--config "$CFG" \
--role student \
--overwrite \
|| { echo "[export] FAILED (see traceback above) -- nothing written" >&2; exit 1; }
# Guard against a silent partial write (the entrypoint can exit 0 yet write no weights).
ls "$OUT"/transformer/*.safetensors >/dev/null 2>&1 \
|| { echo "[export] ERROR: no transformer/*.safetensors under $OUT -- export did not complete" >&2; exit 1; }
echo "[export] wrote diffusers model -> $OUT"
echo "[export] next (Phase 1 merge):"
echo " python data_pipeline/convert_trackwan_init_v2.py \\"
echo " --base /home/hal-kevin/models/Wan2.1-I2V-14B-720P-Diffusers \\"
echo " --out /home/hal-kevin/models/trackwan_14b_i2v_d64_merged_from_overfit_bias \\"
echo " --id-dim 64 --pe-init random \\"
echo " --track-src $OUT/transformer --pe-src $OUT/transformer"
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
# Step 04 — MERGE. Graft the overfit's co-adapted track pathway (track_encoder + patch-embed track
# slot [36:52]) onto a PRISTINE 14B base, discarding the overfit's base degradation. CPU only,
# ~62GB RAM -> run on a compute node. Needs 03_export.sh to have produced the overfit diffusers dir.
set -uo pipefail
cd ~/FastVideo
BASE=${BASE:-/home/hal-kevin/models/Wan2.1-I2V-14B-720P-Diffusers}
SRC=${SRC:-/home/hal-kevin/models/overfit14b_export} # produced by 03_export.sh
OUT=${OUT:-/home/hal-kevin/models/trackwan_14b_i2v_d64_merged_from_overfit_bias}
ID_DIM=${ID_DIM:-64}
[ -d "$SRC/transformer" ] || { echo "[merge] $SRC/transformer not found — run 03_export.sh first" >&2; exit 1; }
# --track-src + --pe-src from the SAME export = the merge: encoder AND its co-adapted track slot
# lifted together (bias copied through); everything else comes from the pristine --base.
python data_pipeline/convert_trackwan_init_v2.py \
--base "$BASE" \
--out "$OUT" \
--id-dim "$ID_DIM" --pe-init random \
--track-src "$SRC/transformer" \
--pe-src "$SRC/transformer"
echo "[merge] built merged init -> $OUT"
python -c "
import json; c=json.load(open('$OUT/transformer/config.json'))
print('[merge] in_channels', c['in_channels'], '| id_dim', c['track_config']['id_dim'])
"
echo "[merge] next: 05_run_openvid_stage1.sh (init_from defaults to this dir)"
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# Step 05 — OpenVid Stage 1 (the big run, ~10 days). 14B/720p, merged init, HEAD FROZEN, sparse.
# Run the SAME command on BOTH racks. MASTER_ADDR = rack0 host on both; NODE_RANK 0 on rack0, 1 on rack1.
# rack0: MASTER_ADDR=hpc-rack-1-6 NODE_RANK=0 bash data_pipeline/720_stage_1/05_run_openvid_stage1.sh
# rack1: MASTER_ADDR=hpc-rack-1-6 NODE_RANK=1 bash data_pipeline/720_stage_1/05_run_openvid_stage1.sh
# resume_from_checkpoint: latest -> relaunch both to continue after any interruption.
set -uo pipefail
cd ~/FastVideo
: "${MASTER_ADDR:?set MASTER_ADDR to rack-0 hostname (reachable from both racks)}"
: "${NODE_RANK:?set NODE_RANK: 0 on the master rack, 1 on the other}"
MASTER_PORT=${MASTER_PORT:-29503}
NNODES=${NNODES:-8} # 8 nodes x 4 GB200 = 32 GPUs (matches num_gpus:32 / replicate 8 in the config)
GPUS_PER_NODE=${GPUS_PER_NODE:-4}
CFG=data_pipeline/720_stage_1/finetune_wantrack_openvid_stage1_14b_720p_d64_bias.yaml
export NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_3,mlx5_4
# export NCCL_DEBUG=INFO # uncomment on the FIRST launch to confirm NET/IB, then re-comment.
# Stage 1: sparse conditioning, HEAD FROZEN (train the DiT to use the merged track pathway), no masking.
# Every WANTRACK_/TRACKWAN_ knob is set EXPLICITLY (no reliance on code defaults) to match upstream
# stage-1 (D) and to avoid inheriting the overfit launcher's opposite settings (IMAGE_COND=0, FIXED_SAMPLE=1).
FASTVIDEO_FA4=1 \
TRACKWAN_TRACK_BIAS=1 \
WANTRACK_FREEZE_HEAD=1 \
WANTRACK_IMAGE_COND=1 \
WANTRACK_AUG=1 \
WANTRACK_SPARSE=1 WANTRACK_EXTRA_RANDOM=20 WANTRACK_EXTRA_MODE=random \
WANTRACK_FIXED_SAMPLE=0 \
WANTRACK_PMASK=0 WANTRACK_MASK_CHUNK=0 \
WANTRACK_TRACK_DROP=0 WANTRACK_MOTION_DROP=0 WANTRACK_TEXT_DROP=0 \
torchrun \
--nnodes=${NNODES} --nproc_per_node=${GPUS_PER_NODE} --node_rank=${NODE_RANK} \
--rdzv_id=openvid_stage1_14b_720p --rdzv_backend=c10d --rdzv_endpoint=${MASTER_ADDR}:${MASTER_PORT} \
--log-dir ~/FastVideo/torchrun_logs \
-m fastvideo.train.entrypoint.train \
--config ${CFG} \
--training.distributed.num_gpus $((NNODES * GPUS_PER_NODE)) \
--training.checkpoint.resume_from_checkpoint latest \
"$@" \
2>&1 | tee -a data_pipeline/720_stage_1/openvid_stage1_node${NODE_RANK}.log
+66
View File
@@ -0,0 +1,66 @@
#!/bin/bash
# Step 05 (2-RACK variant) — OpenVid Stage 1 on 2 racks (2x4 = 8 GB200) instead of 8 nodes.
#
# SAME effective global batch as the 8-node 05: grad_accum is bumped 4x (4 -> 16) to compensate for
# 4x fewer GPUs, so the optimization is equivalent (see note below) — only the wall-clock is ~4x
# (~48 days vs ~12). Run the SAME command on BOTH racks; MASTER_ADDR = rack0 host on both,
# NODE_RANK 0 on rack0, 1 on rack1.
# rack0: MASTER_ADDR=hpc-rack-1-6 NODE_RANK=0 bash data_pipeline/720_stage_1/05b_run_openvid_stage1_2rack.sh
# rack1: MASTER_ADDR=hpc-rack-1-6 NODE_RANK=1 bash data_pipeline/720_stage_1/05b_run_openvid_stage1_2rack.sh
# resume_from_checkpoint: latest -> relaunch both to continue after any interruption.
set -uo pipefail
cd ~/FastVideo
: "${MASTER_ADDR:?set MASTER_ADDR to rack-0 hostname (reachable from both racks)}"
: "${NODE_RANK:?set NODE_RANK: 0 on the master rack, 1 on the other}"
MASTER_PORT=${MASTER_PORT:-29503}
NNODES=${NNODES:-2} # 2 racks x 4 GB200 = 8 GPUs
GPUS_PER_NODE=${GPUS_PER_NODE:-4}
CFG=data_pipeline/720_stage_1/finetune_wantrack_openvid_stage1_14b_720p_d64_bias.yaml
# bf16 parquets (complete 259k set, ~4.7TB). Loader honors the per-field _dtype; same clips/order
# as fp32 (so val_sample_indices are unchanged). Override to the fp32 set if ever needed:
# DATA_PATH=/home/hal-shared/motionstream/data/openvid-wantrack-parquets
DATA_PATH=${DATA_PATH:-/home/hal-shared/motionstream/data/openvid-wantrack-parquets-bf16}
# --- batch math: hold the effective global batch EQUAL to the 8-node 05 ------------------
# 8-node config: replicate 8 x shard 4 = 32 GPUs, grad_accum 4.
# 2 racks: replicate 2 x shard 4 = 8 GPUs, grad_accum 16.
# 4x fewer GPUs x 4x grad_accum = same effective batch under EITHER counting convention
# (num_gpus- or replicate_dim-based both scale by 4). Model still shards across 4 GPUs
# (shard_dim 4) as in the 8-node run, so per-GPU memory is unchanged (no OOM risk from this).
HSDP_REPLICATE=${HSDP_REPLICATE:-2}
HSDP_SHARD=${HSDP_SHARD:-4}
GRAD_ACCUM=${GRAD_ACCUM:-16}
export NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_3,mlx5_4
# export NCCL_DEBUG=INFO # uncomment on the FIRST launch to confirm NET/IB, then re-comment.
# Stage 1 env — identical to 05 (every knob explicit): sparse, HEAD FROZEN, CLIP on, no masking.
FASTVIDEO_FA4=1 \
TRACKWAN_TRACK_BIAS=1 \
WANTRACK_FREEZE_HEAD=1 \
WANTRACK_IMAGE_COND=1 \
WANTRACK_AUG=1 \
WANTRACK_SPARSE=1 WANTRACK_EXTRA_RANDOM=20 WANTRACK_EXTRA_MODE=random \
WANTRACK_FIXED_SAMPLE=0 \
WANTRACK_PMASK=0 WANTRACK_MASK_CHUNK=0 \
WANTRACK_TRACK_DROP=0 WANTRACK_MOTION_DROP=0 WANTRACK_TEXT_DROP=0 \
torchrun \
--nnodes=${NNODES} --nproc_per_node=${GPUS_PER_NODE} --node_rank=${NODE_RANK} \
--rdzv_id=openvid_stage1_14b_720p_2rack --rdzv_backend=c10d --rdzv_endpoint=${MASTER_ADDR}:${MASTER_PORT} \
--log-dir ~/FastVideo/torchrun_logs \
-m fastvideo.train.entrypoint.train \
--config ${CFG} \
--training.data.data_path ${DATA_PATH} \
--training.distributed.num_gpus $((NNODES * GPUS_PER_NODE)) \
--training.distributed.hsdp_replicate_dim ${HSDP_REPLICATE} \
--training.distributed.hsdp_shard_dim ${HSDP_SHARD} \
--training.loop.gradient_accumulation_steps ${GRAD_ACCUM} \
--training.checkpoint.resume_from_checkpoint latest \
--training.checkpoint.training_state_checkpointing_steps 20 \
--training.checkpoint.checkpoints_total_limit 50 \
--callbacks.track_validation.validate_at_start false \
--callbacks.track_validation.every_steps 100 \
--callbacks.track_validation.val_sample_indices "[1660, 1888]" \
"$@" \
2>&1 | tee -a data_pipeline/720_stage_1/openvid_stage1_2rack_node${NODE_RANK}.log
+20
View File
@@ -0,0 +1,20 @@
#!/bin/bash
# Step 06 — export the OpenVid Stage-1 DCP checkpoint to a diffusers dir. Stage 2 (07) inits its
# WEIGHTS from this export (init_from, fresh optimizer/step), so it must be a diffusers model dir,
# not a DCP resume. Only 1 GPU needed. Run on a compute node.
set -uo pipefail
cd ~/FastVideo
CKPT=${CKPT:-/home/hal-kevin/data/motion-stream-test/openvid_stage1_14b_720p_out} # auto-picks latest
OUT=${OUT:-/home/hal-kevin/models/openvid_stage1_14b_export}
CFG=${CFG:-data_pipeline/720_stage_1/finetune_wantrack_openvid_stage1_14b_720p_d64_bias.yaml}
python -m fastvideo.train.entrypoint.dcp_to_diffusers \
--checkpoint "$CKPT" \
--output-dir "$OUT" \
--config "$CFG" \
--role student \
--overwrite
echo "[export] stage-1 teacher -> $OUT"
echo "[export] next: 07_run_synth_stage2.sh (init_from defaults to this dir)"
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
# Step 07 — Stage 2 (robustness, ~1 day). 14B/720p, inits from exported Stage-1 teacher, HEAD FROZEN,
# sparse + heavy masking/dropout. Produces the FINAL bidir teacher. Run on BOTH racks (same command,
# NODE_RANK 0/1, MASTER_ADDR = rack0).
# rack0: MASTER_ADDR=hpc-rack-1-6 NODE_RANK=0 bash data_pipeline/720_stage_1/07_run_synth_stage2.sh
# rack1: MASTER_ADDR=hpc-rack-1-6 NODE_RANK=1 bash data_pipeline/720_stage_1/07_run_synth_stage2.sh
set -uo pipefail
cd ~/FastVideo
: "${MASTER_ADDR:?set MASTER_ADDR to rack-0 hostname (reachable from both racks)}"
: "${NODE_RANK:?set NODE_RANK: 0 on the master rack, 1 on the other}"
MASTER_PORT=${MASTER_PORT:-29504}
NNODES=${NNODES:-8} # 8 nodes x 4 GB200 = 32 GPUs (matches num_gpus:32 / replicate 8 in the config)
GPUS_PER_NODE=${GPUS_PER_NODE:-4}
CFG=data_pipeline/720_stage_1/finetune_wantrack_synth_stage2_14b_720p_d64_bias.yaml
export NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_3,mlx5_4
# export NCCL_DEBUG=INFO # first launch only.
# Stage 2: sparse + robustness masking/dropout, HEAD still FROZEN.
# Every WANTRACK_/TRACKWAN_ knob is set EXPLICITLY (no code defaults) to match upstream stage-2 (E):
# same as stage-1 but with masking (PMASK=0.2/CHUNK=8) and dropout (TRACK=0.5, MOTION=0.3) ON.
FASTVIDEO_FA4=1 \
TRACKWAN_TRACK_BIAS=1 \
WANTRACK_FREEZE_HEAD=1 \
WANTRACK_IMAGE_COND=1 \
WANTRACK_AUG=1 \
WANTRACK_SPARSE=1 WANTRACK_EXTRA_RANDOM=20 WANTRACK_EXTRA_MODE=random \
WANTRACK_FIXED_SAMPLE=0 \
WANTRACK_PMASK=0.2 WANTRACK_MASK_CHUNK=8 \
WANTRACK_TRACK_DROP=0.5 WANTRACK_MOTION_DROP=0.3 WANTRACK_TEXT_DROP=0 \
torchrun \
--nnodes=${NNODES} --nproc_per_node=${GPUS_PER_NODE} --node_rank=${NODE_RANK} \
--rdzv_id=synth_stage2_14b_720p --rdzv_backend=c10d --rdzv_endpoint=${MASTER_ADDR}:${MASTER_PORT} \
--log-dir ~/FastVideo/torchrun_logs \
-m fastvideo.train.entrypoint.train \
--config ${CFG} \
--training.distributed.num_gpus $((NNODES * GPUS_PER_NODE)) \
--training.checkpoint.resume_from_checkpoint latest \
"$@" \
2>&1 | tee -a data_pipeline/720_stage_1/synth_stage2_node${NODE_RANK}.log
+83
View File
@@ -0,0 +1,83 @@
# Stage 1 — Bidirectional 14B/720p track teacher (full pipeline)
The complete bidirectional teacher: overfit the track pathway → merge it into a pristine 14B
base → OpenVid stage-1 (frozen head) → synth stage-2 (robustness). The final teacher then seeds
the causal (Self-Forcing) student. Full rationale: `../notes/trackwan_14b_720p_teacher_merge_plan.md`.
Recipe: **d64 + bias, sparse conditioning, flow_shift 6, 720p.** Hardware: 2×4 GB200, 400G IB,
manual 2-node `torchrun` (rack0 = `hpc-rack-1-6` = NODE_RANK 0; rack1 = `hpc-rack-1-8` = NODE_RANK 1;
`MASTER_ADDR` = rack0 on both).
## Files / run order
| Step | Script | What | Where |
|---|---|---|---|
| 0 | `00_make_overfit_subset.sh` | carve a small overfit set (symlinks ~2 parquet chunks) | login/compute |
| 1 | `01_build_init.sh` | build `trackwan_14b_i2v_d64_bias_init` | compute (CPU, ~62GB RAM) |
| 2 | `02_run_overfit.sh` | overfit track pathway, head trainable (Stage A→B) | **both racks** |
| 3 | `03_export.sh` | overfit DCP → diffusers | compute (1 GPU) |
| 4 | `04_merge.sh` | graft pathway into pristine 14B base → merged init | compute (CPU, ~62GB) |
| 5 | `05_run_openvid_stage1.sh` | OpenVid stage-1, **head frozen** (~10 days) | **both racks** |
| 6 | `06_export_stage1.sh` | stage-1 DCP → diffusers | compute (1 GPU) |
| 7 | `07_run_synth_stage2.sh` | stage-2 robustness, head frozen (~1 day) → **final teacher** | **both racks** |
Configs (referenced by the scripts): `finetune_wantrack_overfit_14b_720p_d64_bias.yaml`,
`finetune_wantrack_openvid_stage1_14b_720p_d64_bias.yaml`,
`finetune_wantrack_synth_stage2_14b_720p_d64_bias.yaml`.
## Commands
```bash
# 0-1: data + init
bash data_pipeline/720_stage_1/00_make_overfit_subset.sh
bash data_pipeline/720_stage_1/01_build_init.sh # compute node
# 2: overfit — both racks, Stage A (fixed IDs) then Stage B (random IDs, resumes A)
MASTER_ADDR=hpc-rack-1-6 NODE_RANK=0 STAGE=A bash data_pipeline/720_stage_1/02_run_overfit.sh # rack0
MASTER_ADDR=hpc-rack-1-6 NODE_RANK=1 STAGE=A bash data_pipeline/720_stage_1/02_run_overfit.sh # rack1
# ...when track-following is clear in validation, stop and run Stage B (bump steps as needed):
MASTER_ADDR=hpc-rack-1-6 NODE_RANK=0 STAGE=B bash data_pipeline/720_stage_1/02_run_overfit.sh --training.loop.max_train_steps 1600
MASTER_ADDR=hpc-rack-1-6 NODE_RANK=1 STAGE=B bash data_pipeline/720_stage_1/02_run_overfit.sh --training.loop.max_train_steps 1600
# 3-4: export + merge
bash data_pipeline/720_stage_1/03_export.sh # compute node
bash data_pipeline/720_stage_1/04_merge.sh # compute node
# 5: OpenVid stage-1 — both racks (~10 days; resume by relaunching)
MASTER_ADDR=hpc-rack-1-6 NODE_RANK=0 bash data_pipeline/720_stage_1/05_run_openvid_stage1.sh # rack0
MASTER_ADDR=hpc-rack-1-6 NODE_RANK=1 bash data_pipeline/720_stage_1/05_run_openvid_stage1.sh # rack1
# 6-7: export stage-1 + synth stage-2 -> final teacher
bash data_pipeline/720_stage_1/06_export_stage1.sh # compute node
MASTER_ADDR=hpc-rack-1-6 NODE_RANK=0 bash data_pipeline/720_stage_1/07_run_synth_stage2.sh # rack0
MASTER_ADDR=hpc-rack-1-6 NODE_RANK=1 bash data_pipeline/720_stage_1/07_run_synth_stage2.sh # rack1
```
## Notes & decisions baked in
- **Topology / batch:** stage-1 and stage-2 target the **upstream recipe on 8 nodes × 4 GB200 = 32
GPUs** — `num_gpus 32, hsdp_replicate_dim 8, hsdp_shard_dim 4, grad_accum 4 → global bs 128`.
Stage-1 = 4,800 steps = 2.4 epochs over 259k ≈ **~12 days**; stage-2 = 600 steps ≈ **~1.5 days**.
(On only 8 GPUs / 2 nodes this recipe is ~49 days — if you drop back, set `NNODES=2`,
`hsdp_replicate_dim 2`, `grad_accum 2` → global bs 16, and use ~8000 stage-1 steps for a ~10-day
half-epoch.) The overfit (steps 0–3) stays on **2 nodes** — it's ~64 clips, so 32 GPUs / bs 128
would exceed the dataset; leave it at `NNODES=2` there.
- **Launching the 8-node stages (05, 07):** the scripts take `NNODES` (default 8) and pass a matching
`--training.distributed.num_gpus`. But manually running one command on each of 8 nodes (NODE_RANK
0…7) is impractical — **use a SLURM launcher** (`srun` spans all nodes with `--node-rank=$SLURM_PROCID`,
as in `examples/train/run_slurm.sh` / `run_wan14b_held.sh`). The overfit (02) is fine to launch
manually on 2 nodes.
- **Env knobs** live in the launch scripts: overfit = `FREEZE_HEAD=0`; stage-1/2 = `FREEZE_HEAD=1`;
stage-2 adds `TRACK_DROP=0.5 MOTION_DROP=0.3 PMASK=0.2 MASK_CHUNK=8`. `TRACKWAN_TRACK_BIAS=1` and
`WANTRACK_SPARSE=1 EXTRA_RANDOM=20` throughout.
- **Stage-2 data** defaults to the openvid parquets + masking (you have no synth generated). Swap
`data_path` to a synth set if you build one; the robustness comes from the masking either way.
- **`dit_precision`:** fp32 master (default) for the real teacher stages; upstream uses bf16 — flip
only if memory-blocked.
- **Monitor track-following, not loss** — the `track_validation` with-track vs no-track/adversarial
deltas. Gate the overfit (step 2) on these before merging, and watch stage-1's first validation
(step 2000) before committing the full ~10 days. Check efficiency with `python scripts/mfu_estimate.py`.
- **Not runnable end-to-end yet:** steps 4–7 chain via default paths (04 reads 03's export, 05 reads
04's merge, etc.), so they're correct now but only *run* once the prior step's output exists.
- **Two things to smoke-test once:** `dcp_to_diffusers` on a real checkpoint (steps 3 & 6), and that
the overfit actually learns track-following on ~64 clips (if weak: `N_CHUNKS=4` in step 0, or more steps).
@@ -0,0 +1,86 @@
# Step 05 config — OpenVid Stage 1. 14B/720p, merged d64+bias init, HEAD FROZEN, sparse.
# The big run: the DiT learns to USE the frozen (merged) track pathway on the full 720p openvid set.
#
# BATCH: upstream recipe = global bs 128 on 32 GPUs (8 nodes x 4). This config targets that: 4800
# steps x 128 = 614k samples = 2.4 epochs over 259k, ~12 days on 32 GPUs. (On only 8 GPUs, bs 128
# would be grad_accum 16 = ~49 days; if you drop back to 8 nodes/GPUs, use grad_accum 2 = global
# bs 16 and ~8000 steps for a ~10-day half-epoch instead.)
models:
student:
_target_: fastvideo.train.models.wantrack.wantrack.WanTrackModel
init_from: /home/hal-kevin/models/trackwan_14b_i2v_d64_merged_from_overfit_bias # from 04_merge.sh
trainable: true
method:
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
training:
model_path: /home/hal-kevin/models/trackwan_14b_i2v_d64_merged_from_overfit_bias
# dit_precision: fp32 master (default) for the real teacher (standard MFU / stable updates at lr 1e-5).
# upstream sets bf16; flip only if memory-blocked (see notes/trackwan_14b_720p_teacher_merge_plan.md).
distributed:
num_gpus: 32
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 8 # across the 8 nodes
hsdp_shard_dim: 4 # within each node (over NVLink)
data:
data_path: /home/hal-shared/motionstream/data/openvid-wantrack-parquets # full 259k, 720p
dataloader_num_workers: 2
train_batch_size: 1
training_cfg_rate: 0.0
seed: 1000
num_latent_t: 31
num_height: 720
num_width: 1280
num_frames: 121
optimizer:
learning_rate: 1.0e-5 # MotionStream teacher stage-1
betas: [0.9, 0.999]
weight_decay: 0.01
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 4800 # upstream stage-1: 2.4 epochs over 259k at bs128; ~12 days on 32 GPUs; resumable
gradient_accumulation_steps: 4 # 1 x 32 GPU x 4 = global batch 128
checkpoint:
output_dir: /home/hal-kevin/data/motion-stream-test/openvid_stage1_14b_720p_out
training_state_checkpointing_steps: 200
checkpoints_total_limit: 5
resume_from_checkpoint: latest
tracker:
trackers: [wandb]
project_name: wantrack-bidir
run_name: openvid-stage1-14b-720p-d64-bias
entity: s4duan-uc-san-diego
model:
enable_gradient_checkpointing_type: full
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
track_validation:
_target_: fastvideo.train.callbacks.track_validation.TrackValidationCallback
every_steps: 2000 # 14B/720p validation is a multi-hour job; keep it rare
num_val_samples: 1
val_sample_indices: [28]
num_inference_steps: 20
guidance_scale: 3.0
motion_guidance_scale: 1.5
fps: 24
grid_stride: 3
tail: 12
include_heldout: false
validate_at_start: false
seed: 1000
pipeline:
flow_shift: 6
@@ -0,0 +1,85 @@
# Phase 0 overfit — 14B/720p, d64 + bias, sparse conditioning, HEAD TRAINABLE.
# Goal: co-adapt the track pathway (track_encoder + patch-embed track slot) on a small subset so
# the model follows tracks. The base is DISCARDED by the Phase-1 merge (only the track pathway is
# lifted), so dit_precision: bf16 here is fine + faster. Sparse/bias/freeze knobs come from the
# launch script (02_run_overfit.sh), not this file.
models:
student:
_target_: fastvideo.train.models.wantrack.wantrack.WanTrackModel
init_from: /home/hal-kevin/models/trackwan_14b_i2v_d64_bias_init
trainable: true
method:
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
training:
model_path: /home/hal-kevin/models/trackwan_14b_i2v_d64_bias_init
dit_precision: bf16 # overfit base is thrown away by the merge; bf16 for speed
distributed:
num_gpus: 8
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 2 # across the 2 racks
hsdp_shard_dim: 4 # within each rack
data:
data_path: /home/hal-kevin/data/motion-stream-synth/preprocessed_i2v_track/combined_parquet_dataset
dataloader_num_workers: 2
train_batch_size: 1
training_cfg_rate: 0.0
seed: 1000
num_latent_t: 31
num_height: 720
num_width: 1280
num_frames: 121
optimizer:
learning_rate: 1.0e-4 # upstream overfit LR (synth_sparse_*_14b), constant
betas: [0.9, 0.999]
weight_decay: 0.01
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 2000 # upstream overfit steps (per stage); stop earlier if tracks lock in
gradient_accumulation_steps: 1 # 1 x 8 GPU x 1 = global batch 8 (fine on a tiny subset)
checkpoint:
output_dir: /home/hal-kevin/data/motion-stream-test/overfit_14b_720p_d64_bias_out
training_state_checkpointing_steps: 100
checkpoints_total_limit: 30 # keep effectively all (20 ckpts @ every-100 for 2000 steps) so you can export the BEST, not just the latest
resume_from_checkpoint: latest
tracker:
trackers: [wandb]
project_name: wantrack-bidir
run_name: overfit-14b-720p-d64-bias
entity: s4duan-uc-san-diego
model:
enable_gradient_checkpointing_type: full
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
track_validation:
_target_: fastvideo.train.callbacks.track_validation.TrackValidationCallback
# Validate on the overfit clips themselves — you want to SEE track-following emerge. The
# with-track vs no-track/adversarial deltas are the signal to stop the overfit.
every_steps: 200
num_val_samples: 2
val_sample_indices: [0, 10]
num_inference_steps: 20
guidance_scale: 3.0
motion_guidance_scale: 1.5
fps: 24
grid_stride: 3
tail: 12
include_heldout: false
validate_at_start: false
seed: 1000
pipeline:
flow_shift: 6
@@ -0,0 +1,84 @@
# Step 07 config — Stage 2 (robustness). 14B/720p, inits from the exported Stage-1 teacher, HEAD
# still FROZEN, sparse + heavy masking/dropout. Short finetune (lr 1e-6) -> the FINAL teacher.
#
# DATA: upstream uses a synthetic set here for controlled motion. You don't have synth generated,
# so this defaults to the SAME 720p openvid parquets + the stage-2 masking env (the robustness comes
# from the masking, applied on whatever data). Swap data_path to a synth set if you generate one
# (data_pipeline/gen_synth_worker.py). Masking knobs are set in 07_run_synth_stage2.sh, not here.
models:
student:
_target_: fastvideo.train.models.wantrack.wantrack.WanTrackModel
init_from: /home/hal-kevin/models/openvid_stage1_14b_export # from 06_export_stage1.sh
trainable: true
method:
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
training:
model_path: /home/hal-kevin/models/openvid_stage1_14b_export
distributed:
num_gpus: 32
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 8 # across the 8 nodes
hsdp_shard_dim: 4 # within each node
data:
data_path: /home/hal-shared/motionstream/data/openvid-wantrack-parquets # swap to synth if available
dataloader_num_workers: 2
train_batch_size: 1
training_cfg_rate: 0.0
seed: 1000
num_latent_t: 31
num_height: 720
num_width: 1280
num_frames: 121
optimizer:
learning_rate: 1.0e-6 # low LR, short robustness finetune
betas: [0.9, 0.999]
weight_decay: 0.01
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 600 # upstream stage-2; ~1.5 days on 32 GPUs at global bs 128
gradient_accumulation_steps: 4 # 1 x 32 GPU x 4 = global batch 128
checkpoint:
output_dir: /home/hal-kevin/data/motion-stream-test/synth_stage2_14b_720p_out
training_state_checkpointing_steps: 100
checkpoints_total_limit: 3
resume_from_checkpoint: latest
tracker:
trackers: [wandb]
project_name: wantrack-bidir
run_name: synth-stage2-14b-720p-d64-bias
entity: s4duan-uc-san-diego
model:
enable_gradient_checkpointing_type: full
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
track_validation:
_target_: fastvideo.train.callbacks.track_validation.TrackValidationCallback
every_steps: 300
num_val_samples: 1
val_sample_indices: [28]
num_inference_steps: 20
guidance_scale: 3.0
motion_guidance_scale: 1.5
fps: 24
grid_stride: 3
tail: 12
include_heldout: false
validate_at_start: false
seed: 1000
pipeline:
flow_shift: 6
+111
View File
@@ -0,0 +1,111 @@
# SPDX-License-Identifier: Apache-2.0
"""Fill in real captions on a videos2caption.json built from OpenVid clips.
The OpenVid-WanTrack shards ship mp4s only -- no metadata -- so a manifest built by
scanning the clip directory has empty ``cap`` fields. Stage 5 would then encode empty
strings through T5, giving every clip an identical null text embedding: text conditioning
(and the joint text+motion CFG that depends on a meaningful conditional/null contrast)
would be silently dead.
OpenVid-1M's caption CSV keys on exactly the same filenames (``---_iRTHryQ_13_0to241.mp4``),
so the join is 1:1 on basename -- no id parsing required.
python data_pipeline/add_captions.py --manifest <root>/videos2caption.json
The CSV is downloaded once from the Hub (~300 MB) and cached; pass --captions-csv to use
a local copy. Clips with no caption keep "" and are counted, never silently dropped.
"""
from __future__ import annotations
import argparse
import csv
import json
import sys
from pathlib import Path
CAPTION_REPO = "nkp37/OpenVid-1M"
CAPTION_FILES = ["data/train/OpenVid-1M.csv", "data/train/OpenVidHD.csv"]
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--manifest", type=Path, required=True, help="videos2caption.json to patch in place.")
p.add_argument("--captions-csv", type=Path, nargs="*", default=None,
help="Local caption CSV(s). Default: download+cache from the Hub.")
p.add_argument("--cache-dir", type=Path, default=Path.home() / ".cache/openvid_captions",
help="Where downloaded caption CSVs are cached.")
p.add_argument("--min-coverage", type=float, default=0.9,
help="Fail if fewer than this fraction of clips get a caption (0 = never fail).")
p.add_argument("--dry-run", action="store_true", help="Report coverage without writing.")
return p.parse_args()
def caption_paths(args: argparse.Namespace) -> list[Path]:
if args.captions_csv:
return list(args.captions_csv)
from huggingface_hub import hf_hub_download
args.cache_dir.mkdir(parents=True, exist_ok=True)
out = []
for f in CAPTION_FILES:
try:
out.append(Path(hf_hub_download(CAPTION_REPO, f, repo_type="dataset",
cache_dir=str(args.cache_dir))))
except Exception as e: # noqa: BLE001
print(f"[caption] warn: could not fetch {f} ({e})", flush=True)
if not out:
sys.exit("[caption] ERROR: no caption CSV available")
return out
def load_captions(paths: list[Path]) -> dict[str, str]:
"""basename -> caption. Later files do not overwrite earlier hits."""
caps: dict[str, str] = {}
csv.field_size_limit(10 * 1024 * 1024) # captions can be long
for p in paths:
n0 = len(caps)
with p.open(newline="", encoding="utf-8", errors="replace") as fh:
for row in csv.DictReader(fh):
key, cap = row.get("video"), row.get("caption")
if key and cap and key not in caps:
caps[key] = cap.strip()
print(f"[caption] {p.name}: +{len(caps) - n0} captions (total {len(caps)})", flush=True)
return caps
def main() -> None:
args = parse_args()
items = json.loads(args.manifest.read_text())
caps = load_captions(caption_paths(args))
hit = 0
missing: list[str] = []
for it in items:
name = Path(it.get("path", "")).name
cap = caps.get(name)
if cap:
it["cap"] = [cap]
hit += 1
else:
it.setdefault("cap", [""])
missing.append(name)
cov = hit / max(len(items), 1)
print(f"[caption] matched {hit}/{len(items)} clips ({cov*100:.1f}%)", flush=True)
if missing:
print(f"[caption] first few unmatched: {missing[:3]}", flush=True)
if cov < args.min_coverage:
sys.exit(f"[caption] ERROR: coverage {cov*100:.1f}% < required {args.min_coverage*100:.0f}%. "
"Refusing to write -- training on empty captions silently breaks text conditioning.")
if args.dry_run:
print("[caption] dry run, manifest not written", flush=True)
return
tmp = args.manifest.with_suffix(".json.tmp")
tmp.write_text(json.dumps(items, indent=2))
tmp.replace(args.manifest)
print(f"[caption] wrote {args.manifest}", flush=True)
if __name__ == "__main__":
main()
+148
View File
@@ -0,0 +1,148 @@
#!/bin/bash
# Benchmark Stage 3 (extract_tracks) + Stage 4 (segment_tracks) end-to-end on the
# motion-stream-test source videos, writing ALL outputs to a separate bench dir so
# the real dataset is never touched.
#
# Usage:
# bash data_pipeline/benchmark_tracks.sh # 50 videos, 4 GPUs
# GPUS=0 bash data_pipeline/benchmark_tracks.sh # single GPU
# LIMIT=5 bash data_pipeline/benchmark_tracks.sh # smoke run
# VIZ=1 bash data_pipeline/benchmark_tracks.sh # include viz mp4 rendering
# FUSED=1 bash data_pipeline/benchmark_tracks.sh # fused stage 3+4 (extract --segment)
#
# Results append to $OUT_DIR/benchmark_results.txt (tagged with git commit); each
# entry records gpus/videos/viz/fused so runs stay comparable.
set -euo pipefail
SRC_VIDEOS=${SRC_VIDEOS:-/home/hal-kevin/data/motion-stream-test/videos}
SRC_MANIFEST=${SRC_MANIFEST:-/home/hal-kevin/data/motion-stream-test/videos2caption.json}
OUT_DIR=${OUT_DIR:-/home/hal-kevin/data/motion-stream-qtest}
GPUS=${GPUS:-0,1,2,3}
LIMIT=${LIMIT:-}
VIZ=${VIZ:-0} # 0 = lean run (production-like for large-scale), 1 = render overlay mp4s
FUSED=${FUSED:-0} # 1 = single fused pass (extract_tracks --segment); stage 4 not run
AMP=${AMP:-0} # 1 = bf16 autocast for CoTracker (--amp)
COMPILE=${COMPILE:-0} # 1 = torch.compile the main CoTracker pass (--compile)
OVERRIDE_EVERY=${OVERRIDE_EVERY:-3} # --vis-override-every (2 rides the entry-sweep mask cache in fused mode)
cd "$(dirname "$0")/.."
IFS=',' read -ra GPU_ARR <<< "$GPUS"
WORLD_SIZE=${#GPU_ARR[@]}
MANIFEST=bench_manifest.json
LOG="$OUT_DIR/benchmark.log"
RESULTS="$OUT_DIR/benchmark_results.txt"
LIMIT_ARGS=()
[[ -n "$LIMIT" ]] && LIMIT_ARGS=(--limit "$LIMIT")
VIZ_ARGS=()
[[ "$VIZ" == "1" ]] && VIZ_ARGS=(--viz --viz-dir "$OUT_DIR/bench_viz")
FUSED_ARGS=()
if [[ "$FUSED" == "1" ]]; then
FUSED_ARGS=(--segment --vis-override-every "$OVERRIDE_EVERY")
[[ "$VIZ" == "1" ]] && FUSED_ARGS+=("${VIZ_ARGS[@]}")
fi
SPEED_ARGS=()
[[ "$AMP" == "1" ]] && SPEED_ARGS+=(--amp)
if [[ "$COMPILE" == "1" ]]; then
SPEED_ARGS+=(--compile)
# Persist compile artifacts in $HOME (default /tmp/torchinductor_* is wiped by
# reboots/tmp-cleaners), so repeat runs start warm instead of recompiling.
export TORCHINDUCTOR_CACHE_DIR=${TORCHINDUCTOR_CACHE_DIR:-$HOME/.cache/torchinductor}
export TRITON_CACHE_DIR=${TRITON_CACHE_DIR:-$HOME/.cache/triton}
fi
# --- setup: symlink source videos, copy manifest without points_path -------------
mkdir -p "$OUT_DIR"
ln -sfn "$SRC_VIDEOS" "$OUT_DIR/bench_videos"
python - "$SRC_MANIFEST" "$OUT_DIR/$MANIFEST" <<'PY'
import json, sys
items = json.load(open(sys.argv[1]))
for it in items:
it.pop("points_path", None) # stage 3 re-patches these to the bench tracks dir
json.dump(items, open(sys.argv[2], "w"), indent=2)
print(f"[bench] manifest: {len(items)} items -> {sys.argv[2]}")
PY
rm -rf "$OUT_DIR/bench_tracks"
[[ "$VIZ" == "1" ]] && rm -rf "$OUT_DIR/bench_viz"
: > "$LOG"
N_VIDEOS=$(ls "$OUT_DIR"/bench_videos/*.mp4 | wc -l)
[[ -n "$LIMIT" ]] && N_VIDEOS=$LIMIT
COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo unknown)$(git diff --quiet 2>/dev/null || echo -dirty)
echo "[bench] commit=$COMMIT gpus=$GPUS videos=$N_VIDEOS log=$LOG"
wait_workers() { # wait_workers <stage-name> <pid...>
local stage=$1 fail=0 pid
shift
for pid in "$@"; do
wait "$pid" || fail=$((fail + 1))
done
[[ $fail -gt 0 ]] && echo "[bench] WARNING: $fail $stage worker(s) failed — check $LOG"
return 0
}
# --- stage 3: extract tracks ------------------------------------------------------
t0=$(date +%s)
PIDS=()
for i in "${!GPU_ARR[@]}"; do
CUDA_VISIBLE_DEVICES=${GPU_ARR[$i]} python -u data_pipeline/extract_tracks.py \
--data-dir "$OUT_DIR" \
--videos-subdir bench_videos \
--out-subdir bench_tracks \
--manifest "$MANIFEST" \
--grid-size 50 \
--device cuda \
--detect-entries \
--sam-conf 0.75 --sam-iou 0.9 --sam-imgsz 1024 \
--entry-sample-every 2 --entry-min-area 0.001 --entry-new-area 0.5 \
"${FUSED_ARGS[@]}" \
"${SPEED_ARGS[@]}" \
--force \
--rank "$i" --world-size "$WORLD_SIZE" \
"${LIMIT_ARGS[@]}" \
>> "$LOG" 2>&1 &
PIDS+=($!)
done
wait_workers "stage-3" "${PIDS[@]}"
t1=$(date +%s)
S3=$((t1 - t0)); [[ $S3 -eq 0 ]] && S3=1
N_NPZ=$(ls "$OUT_DIR"/bench_tracks/*.npz 2>/dev/null | wc -l || true)
echo "[bench] stage 3: ${S3}s for $N_NPZ npz"
[[ "$N_NPZ" -eq "$N_VIDEOS" ]] || echo "[bench] WARNING: expected $N_VIDEOS npz — check $LOG"
# --- stage 4: segment tracks (skipped when FUSED=1: stage 3 already segmented) -----
S4=0
if [[ "$FUSED" != "1" ]]; then
t2=$(date +%s)
PIDS=()
for i in "${!GPU_ARR[@]}"; do
CUDA_VISIBLE_DEVICES=${GPU_ARR[$i]} python -u data_pipeline/segment_tracks.py \
--data-dir "$OUT_DIR" \
--videos-subdir bench_videos \
--manifest "$MANIFEST" \
--conf 0.75 --iou 0.9 --imgsz 1024 \
--vis-override-every "$OVERRIDE_EVERY" \
"${VIZ_ARGS[@]}" \
--force \
--rank "$i" --world-size "$WORLD_SIZE" \
"${LIMIT_ARGS[@]}" \
>> "$LOG" 2>&1 &
PIDS+=($!)
done
wait_workers "stage-4" "${PIDS[@]}"
t3=$(date +%s)
S4=$((t3 - t2)); [[ $S4 -eq 0 ]] && S4=1
echo "[bench] stage 4: ${S4}s"
fi
# --- summary ----------------------------------------------------------------------
{
echo "=== $(date -u '+%Y-%m-%d %H:%M:%S') UTC commit=$COMMIT gpus=$GPUS videos=$N_VIDEOS viz=$VIZ fused=$FUSED amp=$AMP compile=$COMPILE override=$OVERRIDE_EVERY ==="
awk -v s3="$S3" -v s4="$S4" -v w="$WORLD_SIZE" -v n="$N_VIDEOS" -v fused="$FUSED" 'BEGIN {
label = (fused == "1") ? "stage 3+4 (fused):" : "stage 3 (extract):"
printf "%s %5ds total %6.1fs/video/worker %5.1f videos/min\n", label, s3, s3*w/n, 60*n/s3
if (fused != "1")
printf "stage 4 (segment): %5ds total %6.1fs/video/worker %5.1f videos/min\n", s4, s4*w/n, 60*n/s4
printf "end-to-end: %5ds total\n", s3+s4
}'
} | tee -a "$RESULTS"
echo "[bench] results appended to $RESULTS"
+53
View File
@@ -0,0 +1,53 @@
# SPDX-License-Identifier: Apache-2.0
"""Build videos2caption.json + merge.txt for the final track dataset.
Pairs each 121f/720p clip (--clips-dir) with its CoTracker npz (--tracks-dir) and
caption (--captions JSON {video.mp4: caption}). Output is exactly the manifest
`fastvideo/pipelines/preprocess/v1_preprocess.py --preprocess_task i2v_track` consumes
(same format used for the overfit run), so the dataset is training-ready.
"""
from __future__ import annotations
import argparse, json
from pathlib import Path
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--clips-dir", required=True)
ap.add_argument("--tracks-dir", required=True)
ap.add_argument("--captions", required=True, help="JSON {video.mp4: caption}")
ap.add_argument("--out-dir", required=True)
ap.add_argument("--fps", type=int, default=24)
ap.add_argument("--num-frames", type=int, default=121)
ap.add_argument("--height", type=int, default=720)
ap.add_argument("--width", type=int, default=1280)
a = ap.parse_args()
caps = json.load(open(a.captions))
tracks = {p.stem: p for p in Path(a.tracks_dir).glob("*.npz")}
items, missing_track, missing_cap = [], 0, 0
for clip in sorted(Path(a.clips_dir).glob("*.mp4")):
tr = tracks.get(clip.stem)
if tr is None:
missing_track += 1; continue
cap = caps.get(clip.name, "")
if not cap:
missing_cap += 1
items.append({
"path": clip.name,
"cap": [cap],
"points_path": str(tr.resolve()),
"fps": float(a.fps),
"num_frames": a.num_frames,
"resolution": {"width": a.width, "height": a.height},
})
outd = Path(a.out_dir); outd.mkdir(parents=True, exist_ok=True)
(outd / "videos2caption.json").write_text(json.dumps(items, indent=2))
(outd / "merge.txt").write_text(f"{a.clips_dir},{outd / 'videos2caption.json'}")
print(f"[manifest] {len(items)} clips paired (clip+track+caption); "
f"skipped {missing_track} clips w/o track, {missing_cap} w/o caption")
print(f"[manifest] wrote {outd}/videos2caption.json + merge.txt")
if __name__ == "__main__":
main()
+241
View File
@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""CFG-ablation eval on a trackwan checkpoint.
Reproduces ``fastvideo/train/callbacks/track_validation.py::_sample`` denoise
formula EXACTLY, but with configurable ``w_text`` and ``w_motion`` so we can
isolate which CFG branch is causing artifacts.
Runs each val sample under N configs; uploads all videos to one wandb run for
side-by-side visual comparison.
Usage (single-GPU, on any held alloc):
python data_pipeline/cfg_ablation.py \
--model-dir /mnt/lustre/vlm-s4duan/exports/synth_stage2_paperLR_ckpt400 \
--yaml examples/train/scenario/worldmodel/finetune_wantrack_synth_stage2_paperLR.yaml \
--val-parquet /mnt/lustre/vlm-s4duan/val_examples_mixed/combined_parquet_dataset \
--wandb-run-name cfg_abl_paperLR_ckpt400
"""
from __future__ import annotations
import argparse
import glob
import os
import sys
import time
from pathlib import Path
import imageio.v2 as imageio
import numpy as np
import pyarrow.parquet as pq
import torch
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
import trackwan_infer as twi # noqa: E402
from fastvideo.forward_context import set_forward_context # noqa: E402
from fastvideo.models.schedulers.scheduling_flow_match_euler_discrete import ( # noqa: E402
FlowMatchEulerDiscreteScheduler,
)
def load_val_samples(parquet_dir: str, text_len: int) -> list:
from fastvideo.dataset.dataloader.schema import pyarrow_schema_i2v_track
from fastvideo.dataset.utils import collate_rows_from_parquet_schema
fs = sorted(glob.glob(os.path.join(parquet_dir, "**", "*.parquet"), recursive=True))
if not fs:
raise FileNotFoundError(parquet_dir)
rows = []
for f in fs:
rows.extend(pq.read_table(f).to_pylist())
batch = collate_rows_from_parquet_schema(rows, pyarrow_schema_i2v_track,
text_padding_length=int(text_len), cfg_rate=0.0)
infos = batch.get("info_list") or [{} for _ in rows]
return [{
"id": str(infos[i].get("id", f"clip{i:03d}")) if i < len(infos) else f"clip{i:03d}",
"text_embedding": batch["text_embedding"][i:i + 1].clone(),
"text_attention_mask": batch["text_attention_mask"][i:i + 1].clone(),
"first_frame_latent": batch["first_frame_latent"][i:i + 1].clone(),
"clip_feature": batch["clip_feature"][i:i + 1].clone(),
"vae_latent": batch["vae_latent"][i:i + 1].clone(),
"track_points": batch["track_points"][i:i + 1].clone(),
"track_visibility": batch["track_visibility"][i:i + 1].clone(),
"object_ids": batch["object_ids"][i:i + 1].clone() if "object_ids" in batch else None,
"track_weights": batch["track_weights"][i:i + 1].clone() if "track_weights" in batch else None,
"caption": str(infos[i].get("caption", "") if i < len(infos) else ""),
} for i in range(len(rows))]
@torch.no_grad()
def generate_with_cfg(model, sample: dict, *, w_text: float, w_motion: float,
num_steps: int, seed: int,
null_text: torch.Tensor | None = None,
null_text_mask: torch.Tensor | None = None,
no_track: bool = False) -> np.ndarray:
"""Reproduce track_validation._sample denoise EXACTLY, w/ configurable CFG.
If ``null_text`` is provided, use it as the ∅ text (canonical Wan/T5 convention
= UMT5-encoded empty string). Otherwise fall back to zeros_like(txt).
If ``no_track`` is True, pass tp=None/tv=None to every forward (matches the
val callback's `track_val/no_track` panel).
"""
device = model.device
dtype = torch.bfloat16
ff = sample["first_frame_latent"].to(device, dtype)
txt = sample["text_embedding"].to(device, dtype)
mask = sample["text_attention_mask"].to(device, dtype)
clip = sample["clip_feature"].to(device, dtype)
if no_track:
tp = None
tv = None
else:
# CRITICAL: sparse-sample tracks the same way training + track_validation do.
# Feeding the raw 2500 tracks is out-of-distribution and produces melted output.
tp_raw = sample["track_points"].float()
tv_raw = sample["track_visibility"].float()
oid = sample.get("object_ids")
tw = sample.get("track_weights")
aug_gen = torch.Generator(device=device).manual_seed(int(seed))
tp_s, tv_s = model._augment_tracks(tp_raw, tv_raw, aug_gen, object_ids=oid, track_weights=tw)
if tp_s is None or tv_s is None: # motion_drop fired -> fall back to raw
tp_s, tv_s = tp_raw, tv_raw
tp = tp_s.to(device, dtype)
tv = tv_s.to(device, dtype)
cond20 = model._build_i2v_cond_concat(ff)
_, _, T, H, W = ff.shape
gen = torch.Generator(device="cpu").manual_seed(int(seed))
latents = torch.randn((1, 16, T, H, W), generator=gen, dtype=torch.float32).to(device)
sched = FlowMatchEulerDiscreteScheduler(shift=float(model.timestep_shift))
sched.set_timesteps(int(num_steps), device=device)
do_text_cfg = (w_text != 1.0)
do_motion_cfg = (w_motion != 1.0) and not no_track # no tracks -> motion CFG is a no-op
def _fwd(text_e, tp_e, tv_e, mi, ts_):
with torch.autocast(device.type, dtype=dtype), \
set_forward_context(current_timestep=ts_, attn_metadata=None):
return model.transformer(hidden_states=mi, encoder_hidden_states=text_e,
encoder_attention_mask=mask, timestep=ts_,
encoder_hidden_states_image=clip,
track_points=tp_e, track_visibility=tv_e,
return_dict=False)
if do_text_cfg:
if null_text is not None:
# Broadcast the [seq_len, dim] null embedding to match txt shape [B, seq_len, dim]
nl = null_text.to(device, dtype)
if nl.dim() == 2:
nl = nl.unsqueeze(0).expand_as(txt).contiguous()
txt_null = nl
else:
txt_null = torch.zeros_like(txt)
else:
txt_null = None
for tt in sched.timesteps:
mi = torch.cat([latents.to(dtype), cond20], dim=1)
ts_ = tt.reshape(1).to(device, dtype)
v_full = _fwd(txt, tp, tv, mi, ts_)
if not do_text_cfg and not do_motion_cfg:
v = v_full
elif do_text_cfg and not do_motion_cfg:
v_no_text = _fwd(txt_null, tp, tv, mi, ts_)
v = v_no_text + w_text * (v_full - v_no_text)
elif do_motion_cfg and not do_text_cfg:
v_no_motion = _fwd(txt, None, None, mi, ts_)
v = v_no_motion + w_motion * (v_full - v_no_motion)
else:
# joint text+motion CFG (matches track_validation._sample)
v_no_text = _fwd(txt_null, tp, tv, mi, ts_)
v_no_motion = _fwd(txt, None, None, mi, ts_)
alpha = w_text / max(w_text + w_motion, 1e-6)
v_base = alpha * v_no_text + (1.0 - alpha) * v_no_motion
v = v_base + w_text * (v_full - v_no_text) + w_motion * (v_full - v_no_motion)
latents = sched.step(v.float(), tt, latents.float(), return_dict=False)[0]
return twi.decode_to_pixels(model, latents)
def main() -> None:
p = argparse.ArgumentParser()
p.add_argument("--model-dir", required=True)
p.add_argument("--yaml", required=True)
p.add_argument("--val-parquet", required=True)
p.add_argument("--wandb-project", default="wantrack-bidir")
p.add_argument("--wandb-run-name", required=True)
p.add_argument("--out-dir", default="/mnt/lustre/vlm-s4duan/cfg_ablation")
p.add_argument("--steps", type=int, default=30)
p.add_argument("--seed", type=int, default=1000)
p.add_argument("--fps", type=int, default=24)
p.add_argument("--configs", default="3.0,1.5;1.0,1.5;1.0,1.0",
help="';'-sep list of 'w_text,w_motion' pairs")
p.add_argument("--null-text-pt",
help="Path to precomputed UMT5(``'') embedding .pt file. If given, this is "
"used as the ∅ text branch instead of zeros_like(txt).")
p.add_argument("--no-track", action="store_true",
help="Force track_points=None throughout denoise. Reproduces the val "
"callback's `track_val/no_track` panel.")
args = p.parse_args()
null_text = None
null_text_mask = None
if args.null_text_pt:
blob = torch.load(args.null_text_pt, map_location="cpu")
null_text = blob["embedding"]
null_text_mask = blob.get("attention_mask")
print(f"[abl] loaded null-text embedding: shape={list(null_text.shape)} "
f"norm={null_text.norm().item():.3f}", flush=True)
import wandb
Path(args.out_dir).mkdir(parents=True, exist_ok=True)
print(f"[abl] loading model {args.model_dir}", flush=True)
model, tc = twi.load_trackwan(args.model_dir, args.yaml)
text_len = int(getattr(tc.data, "text_padding_length", 256))
print(f"[abl] loading val samples from {args.val_parquet}", flush=True)
samples = load_val_samples(args.val_parquet, text_len)
print(f"[abl] {len(samples)} val samples", flush=True)
configs = [tuple(float(x) for x in pair.split(","))
for pair in args.configs.split(";")]
print(f"[abl] configs: {configs}", flush=True)
run = wandb.init(project=args.wandb_project, name=args.wandb_run_name,
config={"model_dir": args.model_dir, "steps": args.steps,
"configs": [list(c) for c in configs]})
for i, s in enumerate(samples):
sid = s["id"] or f"clip{i:03d}"
print(f"[abl] === sample {i}: {sid} ===", flush=True)
wandb_log = {"sample": i, "caption": s["caption"][:120]}
for wt, wm in configs:
t0 = time.time()
frames = generate_with_cfg(model, s, w_text=wt, w_motion=wm,
num_steps=args.steps, seed=args.seed + i,
null_text=null_text, null_text_mask=null_text_mask,
no_track=args.no_track)
dt = time.time() - t0
tag = f"wt{wt}_wm{wm}".replace(".", "p")
fn = Path(args.out_dir) / f"{sid}_{tag}.mp4"
imageio.mimsave(str(fn), frames, fps=args.fps, macro_block_size=1)
wandb_log[f"gen_{tag}"] = wandb.Video(str(fn), fps=args.fps, format="mp4")
print(f"[abl] {tag}: {dt:.1f}s -> {fn.name}", flush=True)
wandb.log(wandb_log)
# also render GT reference once
for i, s in enumerate(samples):
try:
ref = twi.decode_reference(model, s["vae_latent"].to(model.device, torch.bfloat16))
fn = Path(args.out_dir) / f"{s['id']}_gt.mp4"
imageio.mimsave(str(fn), ref, fps=args.fps, macro_block_size=1)
wandb.log({"sample": i, "reference_gt": wandb.Video(str(fn), fps=args.fps, format="mp4")})
except Exception as e:
print(f"[abl] gt render failed for sample {i}: {e}", flush=True)
wandb.finish()
print("[abl] DONE", flush=True)
if __name__ == "__main__":
main()
+94
View File
@@ -0,0 +1,94 @@
# SPDX-License-Identifier: Apache-2.0
"""Report the WanTrack "gate" — patch_embedding.weight[:, 36:] — from a DCP checkpoint.
The overfit stages (A/B) exist to grow this slot from EXACTLY ZERO up to roughly the scale the
successful 1.3B merged init reached (std ~0.011), co-adapting it with track_encoder. If it is
still ~0 there is no bootstrap happening and step C would merge nothing; if it explodes past the
pretrained channel scale (~0.0124) the track pathway is overwhelming the image prior.
Loads ONLY the tensors of interest (DCP resharding is per-tensor), so it costs ~MBs, not the
~200GB a full checkpoint load would.
Usage: python data_pipeline/check_gate_growth.py <checkpoint-dir> [more dirs...]
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import torch
import torch.distributed.checkpoint as dcp
from torch.distributed.checkpoint import FileSystemReader
BASE_IN = 36
# NOTE: the live module nests the conv, so a TRAINED checkpoint stores
# 'patch_embedding.proj.weight', while the converter writes 'patch_embedding.weight' into the
# init safetensors. Try both — silently matching neither is how the earlier LWLR experiment
# ended up measuring nothing.
PE_KEYS = ["patch_embedding.proj.weight", "patch_embedding.weight"]
KEYS = PE_KEYS + [
"track_encoder.proj.weight",
"track_encoder.temporal_conv.weight",
]
def _find(md_keys, want: str) -> str | None:
"""DCP nests model tensors under a role prefix (e.g. 'student.'); match on the suffix."""
exact = [k for k in md_keys if k == want]
if exact:
return exact[0]
cands = [k for k in md_keys if k.endswith(want)]
return cands[0] if cands else None
def report(ckpt: Path) -> None:
dcp_dir = ckpt / "dcp" if (ckpt / "dcp").is_dir() else ckpt
reader = FileSystemReader(str(dcp_dir))
md = reader.read_metadata()
planned = md.state_dict_metadata
sd = {}
resolved = {}
for want in KEYS:
k = _find(list(planned.keys()), want)
if k is None:
continue
meta = planned[k]
sd[k] = torch.empty(tuple(meta.size), dtype=meta.properties.dtype)
resolved[want] = k
if not sd:
print(f"{ckpt.name}: none of {KEYS} found in checkpoint metadata")
return
dcp.load(sd, checkpoint_id=str(dcp_dir))
out = [f"{ckpt.name:>18s}"]
pe_k = next((resolved[k] for k in PE_KEYS if k in resolved), None)
if pe_k is None:
out.append("!! patch_embedding NOT FOUND — cannot report gate")
else:
pe = sd[pe_k].float()
out.append(f"gate[:, {BASE_IN}:] std={pe[:, BASE_IN:].std():.6f}")
out.append(f"pretrained std={pe[:, :BASE_IN].std():.6f}")
for w in ("track_encoder.proj.weight", "track_encoder.temporal_conv.weight"):
if w in resolved:
out.append(f"{w.split('.')[1]}_norm={sd[resolved[w]].float().norm():.4f}")
print(" ".join(out))
def main() -> None:
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
for key, default in [("RANK", "0"), ("LOCAL_RANK", "0"), ("WORLD_SIZE", "1"),
("MASTER_ADDR", "127.0.0.1"), ("MASTER_PORT", "29555")]:
os.environ.setdefault(key, default)
print(f"reference: 1.3B merged-from-overfit gate std = 0.011418 (grown from 0.000000)")
for p in sys.argv[1:]:
try:
report(Path(p))
except Exception as e: # keep going across checkpoints
print(f"{Path(p).name}: ERROR {type(e).__name__}: {e}")
if __name__ == "__main__":
main()

Some files were not shown because too many files have changed in this diff Show More