Cccccz commited on
Commit
f70ac4f
·
verified ·
1 Parent(s): f87692b

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +70 -0
  2. assets/lb_actions/a_d_125f/intrinsics.npy +3 -0
  3. assets/lb_actions/a_d_125f/poses.npy +3 -0
  4. assets/lb_actions/left_right_125f/intrinsics.npy +3 -0
  5. assets/lb_actions/left_right_125f/poses.npy +3 -0
  6. assets/lb_actions/up_down_125f/intrinsics.npy +3 -0
  7. assets/lb_actions/up_down_125f/poses.npy +3 -0
  8. assets/lb_actions/w_s_125f/intrinsics.npy +3 -0
  9. assets/lb_actions/w_s_125f/poses.npy +3 -0
  10. assets/lb_actions/w_s_61f/intrinsics.npy +3 -0
  11. assets/lb_actions/w_s_61f/poses.npy +3 -0
  12. cachelib/selective.py +148 -0
  13. eval/aggregate.py +331 -0
  14. eval/build_mapping.py +139 -0
  15. eval/generate_eval.py +259 -0
  16. eval/generate_predictor_eval.py +532 -0
  17. eval/pixel_metrics.py +85 -0
  18. eval/retime_eval.py +188 -0
  19. eval/retime_until_clean.sh +96 -0
  20. eval/run_all_phases.sh +23 -0
  21. eval/run_generation.sh +33 -0
  22. eval/run_predictor_eval.sh +39 -0
  23. eval/run_predictor_eval_s2.sh +59 -0
  24. eval/run_predictor_retime_s2.sh +21 -0
  25. eval/run_predictor_vbench_phase.sh +13 -0
  26. eval/run_retime.sh +42 -0
  27. eval/run_vbench.py +163 -0
  28. eval/run_vbench_all.sh +45 -0
  29. eval/setup_vbench_models.py +29 -0
  30. eval/strategies.py +82 -0
  31. eval/test_protocol.py +172 -0
  32. eval_out/generated_videos/cf_ffff/overall_consistency/000.mp4 +3 -0
  33. eval_out/generated_videos/cf_ffff/overall_consistency/001.mp4 +3 -0
  34. eval_out/generated_videos/cf_ffff/overall_consistency/002.mp4 +3 -0
  35. eval_out/generated_videos/cf_ffff/overall_consistency/003.mp4 +3 -0
  36. eval_out/generated_videos/cf_ffff/overall_consistency/004.mp4 +3 -0
  37. eval_out/generated_videos/cf_ffff/overall_consistency/005.mp4 +3 -0
  38. eval_out/generated_videos/cf_ffff/overall_consistency/006.mp4 +3 -0
  39. eval_out/generated_videos/cf_ffff/overall_consistency/007.mp4 +3 -0
  40. eval_out/generated_videos/cf_ffff/overall_consistency/008.mp4 +3 -0
  41. eval_out/generated_videos/cf_ffff/overall_consistency/009.mp4 +3 -0
  42. eval_out/generated_videos/cf_ffff/overall_consistency/010.mp4 +3 -0
  43. eval_out/generated_videos/cf_ffff/overall_consistency/011.mp4 +3 -0
  44. eval_out/generated_videos/cf_ffff/overall_consistency/012.mp4 +3 -0
  45. eval_out/generated_videos/cf_ffff/overall_consistency/013.mp4 +3 -0
  46. eval_out/generated_videos/cf_ffff/overall_consistency/014.mp4 +3 -0
  47. eval_out/generated_videos/cf_ffff/overall_consistency/015.mp4 +3 -0
  48. eval_out/generated_videos/cf_ffff/overall_consistency/016.mp4 +3 -0
  49. eval_out/generated_videos/cf_ffff/overall_consistency/017.mp4 +3 -0
  50. eval_out/generated_videos/cf_ffff/overall_consistency/019.mp4 +3 -0
.gitattributes CHANGED
@@ -33,3 +33,73 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ eval_out/generated_videos/cf_ffff/overall_consistency/041.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ eval_out/generated_videos/cf_ffff/overall_consistency/044.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ eval_out/generated_videos/cf_ffff/overall_consistency/078.mp4 filter=lfs diff=lfs merge=lfs -text
39
+ eval_out/generated_videos/cf_ffff/overall_consistency/064.mp4 filter=lfs diff=lfs merge=lfs -text
40
+ eval_out/generated_videos/cf_ffff/overall_consistency/000.mp4 filter=lfs diff=lfs merge=lfs -text
41
+ eval_out/generated_videos/cf_ffff/overall_consistency/049.mp4 filter=lfs diff=lfs merge=lfs -text
42
+ eval_out/generated_videos/cf_ffff/overall_consistency/005.mp4 filter=lfs diff=lfs merge=lfs -text
43
+ eval_out/generated_videos/cf_ffff/overall_consistency/007.mp4 filter=lfs diff=lfs merge=lfs -text
44
+ eval_out/generated_videos/cf_ffff/overall_consistency/001.mp4 filter=lfs diff=lfs merge=lfs -text
45
+ eval_out/generated_videos/cf_ffff/overall_consistency/002.mp4 filter=lfs diff=lfs merge=lfs -text
46
+ eval_out/generated_videos/cf_ffff/overall_consistency/003.mp4 filter=lfs diff=lfs merge=lfs -text
47
+ eval_out/generated_videos/cf_ffff/overall_consistency/010.mp4 filter=lfs diff=lfs merge=lfs -text
48
+ eval_out/generated_videos/cf_ffff/overall_consistency/012.mp4 filter=lfs diff=lfs merge=lfs -text
49
+ eval_out/generated_videos/cf_ffff/overall_consistency/083.mp4 filter=lfs diff=lfs merge=lfs -text
50
+ eval_out/generated_videos/cf_ffff/overall_consistency/014.mp4 filter=lfs diff=lfs merge=lfs -text
51
+ eval_out/generated_videos/cf_ffff/overall_consistency/013.mp4 filter=lfs diff=lfs merge=lfs -text
52
+ eval_out/generated_videos/cf_ffff/overall_consistency/009.mp4 filter=lfs diff=lfs merge=lfs -text
53
+ eval_out/generated_videos/cf_ffff/overall_consistency/015.mp4 filter=lfs diff=lfs merge=lfs -text
54
+ eval_out/generated_videos/cf_ffff/overall_consistency/047.mp4 filter=lfs diff=lfs merge=lfs -text
55
+ eval_out/generated_videos/cf_ffff/overall_consistency/016.mp4 filter=lfs diff=lfs merge=lfs -text
56
+ eval_out/generated_videos/cf_ffff/overall_consistency/011.mp4 filter=lfs diff=lfs merge=lfs -text
57
+ eval_out/generated_videos/cf_ffff/overall_consistency/021.mp4 filter=lfs diff=lfs merge=lfs -text
58
+ eval_out/generated_videos/cf_ffff/overall_consistency/004.mp4 filter=lfs diff=lfs merge=lfs -text
59
+ eval_out/generated_videos/cf_ffff/overall_consistency/062.mp4 filter=lfs diff=lfs merge=lfs -text
60
+ eval_out/generated_videos/cf_ffff/overall_consistency/008.mp4 filter=lfs diff=lfs merge=lfs -text
61
+ eval_out/generated_videos/cf_ffff/overall_consistency/019.mp4 filter=lfs diff=lfs merge=lfs -text
62
+ eval_out/generated_videos/cf_ffff/overall_consistency/023.mp4 filter=lfs diff=lfs merge=lfs -text
63
+ eval_out/generated_videos/cf_ffff/overall_consistency/061.mp4 filter=lfs diff=lfs merge=lfs -text
64
+ eval_out/generated_videos/cf_ffff/overall_consistency/022.mp4 filter=lfs diff=lfs merge=lfs -text
65
+ eval_out/generated_videos/cf_ffff/overall_consistency/028.mp4 filter=lfs diff=lfs merge=lfs -text
66
+ eval_out/generated_videos/cf_ffff/overall_consistency/027.mp4 filter=lfs diff=lfs merge=lfs -text
67
+ eval_out/generated_videos/cf_ffff/overall_consistency/017.mp4 filter=lfs diff=lfs merge=lfs -text
68
+ eval_out/generated_videos/cf_ffff/overall_consistency/054.mp4 filter=lfs diff=lfs merge=lfs -text
69
+ eval_out/generated_videos/cf_ffff/overall_consistency/034.mp4 filter=lfs diff=lfs merge=lfs -text
70
+ eval_out/generated_videos/cf_ffff/overall_consistency/033.mp4 filter=lfs diff=lfs merge=lfs -text
71
+ eval_out/generated_videos/cf_ffff/overall_consistency/032.mp4 filter=lfs diff=lfs merge=lfs -text
72
+ eval_out/generated_videos/cf_ffff/overall_consistency/092.mp4 filter=lfs diff=lfs merge=lfs -text
73
+ eval_out/generated_videos/cf_ffff/overall_consistency/037.mp4 filter=lfs diff=lfs merge=lfs -text
74
+ eval_out/generated_videos/cf_ffff/overall_consistency/026.mp4 filter=lfs diff=lfs merge=lfs -text
75
+ eval_out/generated_videos/cf_ffff/overall_consistency/035.mp4 filter=lfs diff=lfs merge=lfs -text
76
+ eval_out/generated_videos/cf_ffff/overall_consistency/040.mp4 filter=lfs diff=lfs merge=lfs -text
77
+ eval_out/generated_videos/cf_ffff/overall_consistency/091.mp4 filter=lfs diff=lfs merge=lfs -text
78
+ eval_out/generated_videos/cf_ffff/overall_consistency/020.mp4 filter=lfs diff=lfs merge=lfs -text
79
+ eval_out/generated_videos/cf_ffff/overall_consistency/039.mp4 filter=lfs diff=lfs merge=lfs -text
80
+ eval_out/generated_videos/cf_ffff/overall_consistency/042.mp4 filter=lfs diff=lfs merge=lfs -text
81
+ eval_out/generated_videos/cf_ffff/overall_consistency/006.mp4 filter=lfs diff=lfs merge=lfs -text
82
+ eval_out/generated_videos/cf_ffff/overall_consistency/024.mp4 filter=lfs diff=lfs merge=lfs -text
83
+ eval_out/generated_videos/cf_ffff/overall_consistency/050.mp4 filter=lfs diff=lfs merge=lfs -text
84
+ eval_out/generated_videos/cf_ffff/overall_consistency/048.mp4 filter=lfs diff=lfs merge=lfs -text
85
+ eval_out/generated_videos/cf_ffff/overall_consistency/059.mp4 filter=lfs diff=lfs merge=lfs -text
86
+ eval_out/generated_videos/cf_ffff/overall_consistency/060.mp4 filter=lfs diff=lfs merge=lfs -text
87
+ eval_out/generated_videos/cf_ffff/overall_consistency/057.mp4 filter=lfs diff=lfs merge=lfs -text
88
+ eval_out/generated_videos/cf_ffff/overall_consistency/088.mp4 filter=lfs diff=lfs merge=lfs -text
89
+ eval_out/generated_videos/cf_ffff/overall_consistency/051.mp4 filter=lfs diff=lfs merge=lfs -text
90
+ eval_out/generated_videos/cf_ffff/overall_consistency/053.mp4 filter=lfs diff=lfs merge=lfs -text
91
+ eval_out/generated_videos/cf_ffff/overall_consistency/090.mp4 filter=lfs diff=lfs merge=lfs -text
92
+ eval_out/generated_videos/cf_ffff/overall_consistency/052.mp4 filter=lfs diff=lfs merge=lfs -text
93
+ eval_out/generated_videos/cf_ffff/overall_consistency/058.mp4 filter=lfs diff=lfs merge=lfs -text
94
+ eval_out/generated_videos/cf_ffff/overall_consistency/055.mp4 filter=lfs diff=lfs merge=lfs -text
95
+ eval_out/generated_videos/cf_ffff/overall_consistency/056.mp4 filter=lfs diff=lfs merge=lfs -text
96
+ eval_out/generated_videos/cf_ffff/overall_consistency/089.mp4 filter=lfs diff=lfs merge=lfs -text
97
+ eval_out/generated_videos/cf_ffff/overall_consistency/079.mp4 filter=lfs diff=lfs merge=lfs -text
98
+ eval_out/generated_videos/cf_ffff/overall_consistency/063.mp4 filter=lfs diff=lfs merge=lfs -text
99
+ eval_out/generated_videos/cf_ffff/overall_consistency/030.mp4 filter=lfs diff=lfs merge=lfs -text
100
+ eval_out/generated_videos/cf_ffff/overall_consistency/068.mp4 filter=lfs diff=lfs merge=lfs -text
101
+ eval_out/generated_videos/cf_ffff/overall_consistency/066.mp4 filter=lfs diff=lfs merge=lfs -text
102
+ eval_out/generated_videos/cf_ffff/overall_consistency/065.mp4 filter=lfs diff=lfs merge=lfs -text
103
+ eval_out/generated_videos/cf_ffff/overall_consistency/070.mp4 filter=lfs diff=lfs merge=lfs -text
104
+ eval_out/generated_videos/cf_ffff/overall_consistency/067.mp4 filter=lfs diff=lfs merge=lfs -text
105
+ eval_out/generated_videos/cf_ffff/overall_consistency/072.mp4 filter=lfs diff=lfs merge=lfs -text
assets/lb_actions/a_d_125f/intrinsics.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a526a861f16b3bdd64d12b1946d2438925b0f68d88822599fc1d6b14a324d905
3
+ size 2128
assets/lb_actions/a_d_125f/poses.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f4cebbc48954133ce27c8adc7c71327f3ab6e6b8f1d735441dc6931bb931b5a9
3
+ size 8128
assets/lb_actions/left_right_125f/intrinsics.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a526a861f16b3bdd64d12b1946d2438925b0f68d88822599fc1d6b14a324d905
3
+ size 2128
assets/lb_actions/left_right_125f/poses.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:03161c72fae5167c67aed4e3fa61cddb4dee23f008712d1ebc6a73f4848c631f
3
+ size 8128
assets/lb_actions/up_down_125f/intrinsics.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a526a861f16b3bdd64d12b1946d2438925b0f68d88822599fc1d6b14a324d905
3
+ size 2128
assets/lb_actions/up_down_125f/poses.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f9b64db0d0cea64c0b9b587f065bd6c2d0d76a1512f0ba128dcb4b41b75c04c0
3
+ size 8128
assets/lb_actions/w_s_125f/intrinsics.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a526a861f16b3bdd64d12b1946d2438925b0f68d88822599fc1d6b14a324d905
3
+ size 2128
assets/lb_actions/w_s_125f/poses.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f0624f5ca69206ecd7045c2f2b863b8e80bb8b5792d6b09f67370dab41d0dbb5
3
+ size 8128
assets/lb_actions/w_s_61f/intrinsics.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d5b972022c390258bd820986b146a9cfb86654b84be31ef161cd5c51b0ff746b
3
+ size 1104
assets/lb_actions/w_s_61f/poses.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:423844e4fd3f7900c8e07acf72544399c9ec1efe2f0ffe2243e0c66a64c56be3
3
+ size 4032
cachelib/selective.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Selective-token forward for the block-causal Wan DiT.
2
+
3
+ MotionCache (token granularity) and FlowCache (frame-group granularity) both need
4
+ to run the transformer stack on a *subset* of the current chunk's tokens while the
5
+ remaining tokens keep the activations cached from the previous computed step.
6
+
7
+ For a KV-cached causal model that means, per self-attention layer:
8
+ * compute q/k/v only for the selected rows,
9
+ * scatter the fresh k/v into the slots the current chunk owns in the KV cache,
10
+ leaving the unselected slots holding the previous step's k/v,
11
+ * attend the selected queries against the *full* cache (history + current chunk).
12
+
13
+ This mirrors ``forward_selective`` in
14
+ ``MotionCache4SkyReels-V2/skyreels_v2_infer/modules/transformer_motion.py``,
15
+ adapted from SkyReels' persistent ``k_cache/v_cache`` buffers to Self-Forcing's
16
+ rolling ``kv_cache[block]`` dict.
17
+ """
18
+
19
+ import math
20
+
21
+ import torch
22
+
23
+ from wan.modules.attention import attention
24
+
25
+
26
+ def build_causal_freqs(grid_sizes, freqs, start_frame):
27
+ """Return the per-token RoPE multipliers [seq_len, 1, c] for one chunk.
28
+
29
+ Same construction as ``causal_rope_apply`` in ``wan/modules/causal_model.py``,
30
+ factored out so it can be indexed by a token subset and reused across layers.
31
+ """
32
+ c = freqs.size(1)
33
+ parts = freqs.split([c - 2 * (c // 3), c // 3, c // 3], dim=1)
34
+ f, h, w = grid_sizes[0].tolist()
35
+ seq_len = f * h * w
36
+ freqs_i = torch.cat(
37
+ [
38
+ parts[0][start_frame:start_frame + f].view(f, 1, 1, -1).expand(f, h, w, -1),
39
+ parts[1][:h].view(1, h, 1, -1).expand(f, h, w, -1),
40
+ parts[2][:w].view(1, 1, w, -1).expand(f, h, w, -1),
41
+ ],
42
+ dim=-1,
43
+ ).reshape(seq_len, 1, -1)
44
+ return freqs_i
45
+
46
+
47
+ def rope_apply_rows(x, freqs_rows):
48
+ """Apply precomputed RoPE multipliers to x [B, S, n, d] given freqs [S, 1, d/2]."""
49
+ b, s, n, _ = x.shape
50
+ x_c = torch.view_as_complex(x.to(torch.float64).reshape(b, s, n, -1, 2))
51
+ out = torch.view_as_real(x_c * freqs_rows.unsqueeze(0)).flatten(3)
52
+ return out.type_as(x)
53
+
54
+
55
+ def self_attn_selective(attn, x_sel, sel_idx, freqs_rows, kv_cache,
56
+ current_start, block_seq_len):
57
+ """Self-attention over a token subset, refreshing only those KV slots.
58
+
59
+ Args:
60
+ attn: a ``CausalWanSelfAttention`` module.
61
+ x_sel: [B, n_sel, C] modulated hidden states for the selected tokens.
62
+ sel_idx: LongTensor [n_sel], token offsets inside the current chunk.
63
+ freqs_rows: [block_seq_len, 1, d/2] RoPE multipliers for the whole chunk.
64
+ kv_cache: this layer's ``{"k", "v", "global_end_index", "local_end_index"}``.
65
+ current_start: token offset of the chunk in global sequence coordinates.
66
+ block_seq_len: number of tokens the chunk owns (all frames of the chunk).
67
+ """
68
+ b, n_sel = x_sel.shape[:2]
69
+ n, d = attn.num_heads, attn.head_dim
70
+
71
+ q = attn.norm_q(attn.q(x_sel)).view(b, n_sel, n, d)
72
+ k = attn.norm_k(attn.k(x_sel)).view(b, n_sel, n, d)
73
+ v = attn.v(x_sel).view(b, n_sel, n, d)
74
+
75
+ rows = freqs_rows[sel_idx]
76
+ roped_q = rope_apply_rows(q, rows)
77
+ roped_k = rope_apply_rows(k, rows).type_as(v)
78
+
79
+ # Index arithmetic must use the *full* chunk length: the chunk still occupies
80
+ # the same KV slots, we are only refreshing some of them.
81
+ current_end = current_start + block_seq_len
82
+ local_end_index = (kv_cache["local_end_index"].item() + current_end
83
+ - kv_cache["global_end_index"].item())
84
+ local_start_index = local_end_index - block_seq_len
85
+
86
+ slots = local_start_index + sel_idx
87
+ kv_cache["k"].index_copy_(1, slots, roped_k)
88
+ kv_cache["v"].index_copy_(1, slots, v)
89
+
90
+ lo = max(0, local_end_index - attn.max_attention_size)
91
+ out = attention(roped_q, kv_cache["k"][:, lo:local_end_index],
92
+ kv_cache["v"][:, lo:local_end_index])
93
+
94
+ kv_cache["global_end_index"].fill_(current_end)
95
+ kv_cache["local_end_index"].fill_(local_end_index)
96
+
97
+ return attn.o(out.flatten(2))
98
+
99
+
100
+ def block_forward_selective(block, x_sel, sel_idx, frame_of_sel, e0, freqs_rows,
101
+ context, kv_cache, crossattn_cache,
102
+ current_start, block_seq_len):
103
+ """One ``CausalWanAttentionBlock`` evaluated on a token subset.
104
+
105
+ ``frame_of_sel`` maps each selected token to its frame index inside the chunk,
106
+ so the per-frame AdaLN modulation can be gathered for the selected rows only.
107
+ """
108
+ # e0: [B, F, 6, C] -> six tensors of [B, F, 1, C]; gather per selected token.
109
+ e = (block.modulation.unsqueeze(1) + e0).chunk(6, dim=2)
110
+ e = [t.squeeze(2)[:, frame_of_sel] for t in e] # each [B, n_sel, C]
111
+
112
+ y = self_attn_selective(
113
+ block.self_attn,
114
+ block.norm1(x_sel) * (1 + e[1]) + e[0],
115
+ sel_idx, freqs_rows, kv_cache, current_start, block_seq_len,
116
+ )
117
+ x_sel = x_sel + y * e[2]
118
+
119
+ x_sel = x_sel + block.cross_attn(block.norm3(x_sel), context, None,
120
+ crossattn_cache=crossattn_cache)
121
+ y = block.ffn(block.norm2(x_sel) * (1 + e[4]) + e[3])
122
+ x_sel = x_sel + y * e[5]
123
+ return x_sel
124
+
125
+
126
+ def run_blocks_selective(model, x, sel_idx, e0, kwargs, kv_cache, crossattn_cache,
127
+ current_start, cache_start, grid_sizes):
128
+ """Run all transformer blocks on ``x[:, sel_idx]`` and return the updated rows.
129
+
130
+ Returns a tensor [B, n_sel, C] holding the new hidden states for the selected
131
+ tokens (the caller merges them into the cached full-length tensor).
132
+ """
133
+ f, h, w = grid_sizes[0].tolist()
134
+ frame_seqlen = h * w
135
+ block_seq_len = f * frame_seqlen
136
+ frame_of_sel = torch.div(sel_idx, frame_seqlen, rounding_mode="floor")
137
+
138
+ freqs_rows = build_causal_freqs(grid_sizes, kwargs["freqs"],
139
+ current_start // frame_seqlen)
140
+
141
+ x_sel = x[:, sel_idx]
142
+ for i, block in enumerate(model.blocks):
143
+ x_sel = block_forward_selective(
144
+ block, x_sel, sel_idx, frame_of_sel, e0, freqs_rows,
145
+ kwargs["context"], kv_cache[i], crossattn_cache[i],
146
+ current_start, block_seq_len,
147
+ )
148
+ return x_sel
eval/aggregate.py ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Final summary for the Self-Forcing Extended-251 Full Evaluation.
3
+
4
+ Implements protocol sections 7 (normalize + aggregate), 8.3 (pixel aggregation),
5
+ 9.4 (matched-FFFF speedup) and 10 (output tables), plus the section 11/15
6
+ completeness gate: a strategy that is not a full 251 is never summarised.
7
+
8
+ python eval/aggregate.py --out-root eval_out
9
+ """
10
+
11
+ import argparse
12
+ import re
13
+ import csv
14
+ import glob
15
+ import hashlib
16
+ import json
17
+ import math
18
+ import os
19
+ import platform
20
+ import sys
21
+ from collections import defaultdict
22
+
23
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
24
+ sys.path.insert(0, ROOT)
25
+
26
+ MAPPING = os.path.join(ROOT, "assets/vbench8_extended_subset_mapping.json")
27
+
28
+ DIMENSIONS = ["subject_consistency", "background_consistency", "motion_smoothness",
29
+ "dynamic_degree", "aesthetic_quality", "imaging_quality", "scene",
30
+ "overall_consistency"]
31
+
32
+ # Protocol section 7.1 -- fixed empirical ranges, never re-estimated from the run.
33
+ NORMALIZE_RANGE = {
34
+ "subject_consistency": (0.1462, 1.0),
35
+ "background_consistency": (0.2615, 1.0),
36
+ "motion_smoothness": (0.7060, 0.9975),
37
+ "dynamic_degree": (0.0, 1.0),
38
+ "aesthetic_quality": (0.0, 1.0),
39
+ "imaging_quality": (0.0, 1.0),
40
+ "scene": (0.0, 0.8222),
41
+ "overall_consistency": (0.0, 0.3640),
42
+ }
43
+
44
+ REFERENCE_OF = {"sf": "sf_ffff", "cf": "cf_ffff", "cfa": "cfa_ffff"}
45
+
46
+
47
+ def normalize(raw):
48
+ out = {}
49
+ for d, v in raw.items():
50
+ lo, hi = NORMALIZE_RANGE[d]
51
+ out[d] = (v - lo) / (hi - lo) # protocol does not clip
52
+ return out
53
+
54
+
55
+ def quality_score(n):
56
+ return (n["subject_consistency"] + n["background_consistency"]
57
+ + n["motion_smoothness"] + 0.5 * n["dynamic_degree"]
58
+ + n["aesthetic_quality"] + n["imaging_quality"]) / 5.5
59
+
60
+
61
+ def semantic_score(n):
62
+ return (n["scene"] + n["overall_consistency"]) / 2.0
63
+
64
+
65
+ def selected_score(q, s):
66
+ return (4.0 * q + s) / 5.0
67
+
68
+
69
+ def sha256(path):
70
+ h = hashlib.sha256()
71
+ with open(path, "rb") as f:
72
+ for chunk in iter(lambda: f.read(1 << 20), b""):
73
+ h.update(chunk)
74
+ return h.hexdigest()
75
+
76
+
77
+ def load_per_prompt(out_root, strategy):
78
+ recs = []
79
+ for p in sorted(glob.glob(os.path.join(out_root, "per_prompt", strategy, "*.json"))):
80
+ with open(p) as f:
81
+ r = json.load(f)
82
+ if r.get("status") == "complete":
83
+ recs.append(r)
84
+ return recs
85
+
86
+
87
+ def main():
88
+ ap = argparse.ArgumentParser()
89
+ ap.add_argument("--out-root", default="eval_out")
90
+ ap.add_argument("--expect", type=int, default=251)
91
+ ap.add_argument("--allow-incomplete", action="store_true",
92
+ help="Report partial strategies instead of refusing (diagnostics only)")
93
+ ap.add_argument("--latency-subsets", default=os.path.join(ROOT, "results/latency_subsets.json"),
94
+ help="JSON {strategy_regex: {shard, num_shards, note}}: time these "
95
+ "strategies on one prompt shard only (the one that ran on an "
96
+ "uncontended GPU); quality still uses every record")
97
+ ap.add_argument("--only", default=None,
98
+ help="Regex: summarise only strategies whose name matches (other "
99
+ "strategies in per_prompt/ are ignored, e.g. while they are "
100
+ "still being generated)")
101
+ args = ap.parse_args()
102
+
103
+ out_root = (args.out_root if os.path.isabs(args.out_root)
104
+ else os.path.join(ROOT, args.out_root))
105
+
106
+ with open(MAPPING) as f:
107
+ mapping = json.load(f)
108
+
109
+ strategies = sorted(os.path.basename(p) for p in
110
+ glob.glob(os.path.join(out_root, "per_prompt", "*")))
111
+ if args.only:
112
+ strategies = [s for s in strategies if re.search(args.only, s)]
113
+ if not strategies:
114
+ print("no per-prompt records found")
115
+ return 1
116
+
117
+ per_strategy, problems = {}, []
118
+ for s in strategies:
119
+ recs = load_per_prompt(out_root, s)
120
+ if len(recs) != args.expect:
121
+ problems.append(f"{s}: {len(recs)} complete records, expected {args.expect}")
122
+ per_strategy[s] = recs
123
+
124
+ if problems and not args.allow_incomplete:
125
+ print("REFUSING TO SUMMARISE -- incomplete strategies (protocol 11.9):")
126
+ for p in problems:
127
+ print(" -", p)
128
+ return 1
129
+
130
+ # --- latency, matched to each base model's own FFFF -----------------------
131
+ mean_latency, mean_context, timed_records = {}, {}, {}
132
+ for s, recs in per_strategy.items():
133
+ # Records flagged latency_excluded keep their pixel metrics but carry no
134
+ # usable timing (see their latency_note); they stay out of the means.
135
+ lat = [r for r in recs if not r.get("latency_excluded")]
136
+ if not lat:
137
+ continue
138
+ timed_records[s] = lat
139
+ mean_latency[s] = sum(r["policy_latency_ms"] for r in lat) / len(lat)
140
+ mean_context[s] = sum(r["excluded_context_kv_latency_ms"] for r in lat) / len(lat)
141
+
142
+ # Strategies generated while another job shared the GPU are timed only on the
143
+ # prompt shard that ran alone (sharding is rows[shard::num_shards] over the
144
+ # mapping order, see generate_eval.py); their FFFF reference is restricted to
145
+ # the same prompts so the ratio stays paired.
146
+ subsets = {}
147
+ if args.latency_subsets and os.path.exists(args.latency_subsets):
148
+ with open(args.latency_subsets) as f:
149
+ subsets = json.load(f)
150
+ with open(os.path.join(ROOT, "assets/vbench8_extended_subset_mapping.json")) as f:
151
+ mapping_rows = json.load(f)["rows"]
152
+ subset_of = {}
153
+ for s in per_strategy:
154
+ for pat, spec in subsets.items():
155
+ if re.search(pat, s):
156
+ keep = {r["global_index"] for r in mapping_rows[spec["shard"]::spec["num_shards"]]}
157
+ subset_of[s] = (keep, spec)
158
+ break
159
+
160
+ rows = []
161
+ for s, recs in per_strategy.items():
162
+ if not recs:
163
+ continue
164
+ ref = recs[0].get("reference_strategy") or REFERENCE_OF[s.split("_")[0]]
165
+ if ref not in mean_latency:
166
+ problems.append(f"{s}: reference {ref} missing")
167
+ continue
168
+
169
+ # Ratio of means (protocol 9.4). Equal sample counts make this the same
170
+ # as the ratio of sums; a mean of per-prompt percentages is not used.
171
+ # A strategy whose records carry the FFFF latency measured in the same
172
+ # process (retime_eval.py) is compared against that, not against the
173
+ # FFFF run's own records: contention differs between runs, and only a
174
+ # same-run pairing is "matched FFFF" in the protocol's sense.
175
+ timed = timed_records[s]
176
+ matched = [r.get("matched_ffff_policy_latency_ms") for r in timed]
177
+ strat_latency = mean_latency[s]
178
+ latency_n = len(timed)
179
+ if s in subset_of:
180
+ keep, spec = subset_of[s]
181
+ sub = [r for r in recs if r["global_index"] in keep]
182
+ ref_sub = [r for r in per_strategy[ref] if r["global_index"] in keep]
183
+ assert sub and ref_sub, f"{s}: empty latency subset"
184
+ strat_latency = sum(r["policy_latency_ms"] for r in sub) / len(sub)
185
+ ref_latency = sum(r["policy_latency_ms"] for r in ref_sub) / len(ref_sub)
186
+ latency_n = len(sub)
187
+ latency_source = (f"generation run, shard {spec['shard']}/{spec['num_shards']} only "
188
+ f"({len(sub)} prompts; {spec.get('note', '')})")
189
+ elif all(m is not None for m in matched):
190
+ ref_latency = sum(matched) / len(matched)
191
+ latency_source = "paired retime (same process as FFFF)"
192
+ elif all(r.get("latency_retimed") for r in recs):
193
+ ref_latency = mean_latency[ref]
194
+ latency_source = ("strategy retime: all records from one denoise-only pass on an idle GPU "
195
+ "(FFFF from its own records)")
196
+ elif any(r.get("latency_retimed") for r in recs):
197
+ n_re = sum(1 for r in recs if r.get("latency_retimed"))
198
+ ref_latency = mean_latency[ref]
199
+ latency_source = (f"generation run; {n_re}/{len(recs)} records re-timed "
200
+ "(strategy-only denoise rerun on an idle GPU; FFFF from its own records)")
201
+ else:
202
+ ref_latency = mean_latency[ref]
203
+ latency_source = "generation run (FFFF from its own records)"
204
+ if len(timed) < len(recs):
205
+ latency_source += (f"; {len(timed)}/{len(recs)} prompts with intact timing "
206
+ "(latency_excluded records dropped)")
207
+ speedup_pct = 100.0 * (1.0 - strat_latency / ref_latency)
208
+
209
+ # PSNR from the mean of per-prompt mean MSE, then converted once (8.3).
210
+ mses = [r["pixel_metrics_vs_ffff"]["mean_mse"] for r in recs]
211
+ mean_mse = sum(mses) / len(mses)
212
+ psnr = -10.0 * math.log10(max(mean_mse, 1e-12))
213
+ ssim = sum(r["pixel_metrics_vs_ffff"]["ssim"] for r in recs) / len(recs)
214
+ lpips = sum(r["pixel_metrics_vs_ffff"]["lpips"] for r in recs) / len(recs)
215
+ frames = {r["pixel_metrics_vs_ffff"]["num_frames"] for r in recs}
216
+ if frames != {81}:
217
+ problems.append(f"{s}: pixel metrics used frame counts {sorted(frames)}, expected 81")
218
+
219
+ row = {
220
+ "strategy": s,
221
+ "base_model": recs[0]["base_model"],
222
+ "method": recs[0]["method"],
223
+ "target_speedup": recs[0]["target_speedup"],
224
+ "schedule": recs[0].get("schedule"),
225
+ "num_inference_steps": recs[0].get("num_inference_steps"),
226
+ "num_videos": len(recs),
227
+ "mp4_reference_records": sum(1 for r in recs if r.get("reference_source") == "ffff_mp4"),
228
+ "policy_latency_ms": strat_latency,
229
+ "latency_num_videos": latency_n,
230
+ "speedup_percent_vs_ffff": 0.0 if s == ref else speedup_pct,
231
+ "latency_ratio_vs_ffff": strat_latency / ref_latency,
232
+ "matched_ffff_policy_latency_ms": ref_latency,
233
+ "latency_source": latency_source,
234
+ "excluded_context_kv_latency_ms": mean_context[s],
235
+ "psnr": psnr,
236
+ "ssim": ssim,
237
+ "lpips": lpips,
238
+ "mean_compute_equivalent_forwards": (
239
+ sum(r["cache_diagnostics"]["compute_equivalent_forwards"] for r in recs)
240
+ / len(recs)),
241
+ }
242
+
243
+ score_path = os.path.join(out_root, "vbench", "scores", f"{s}.json")
244
+ if os.path.exists(score_path):
245
+ with open(score_path) as f:
246
+ sc = json.load(f)
247
+ raw = {d: sc["raw"][d] for d in DIMENSIONS if d in sc["raw"]}
248
+ if len(raw) == len(DIMENSIONS):
249
+ bad = {d: v for d, v in raw.items() if not (0.0 <= v <= 1.0)}
250
+ if bad:
251
+ problems.append(f"{s}: raw scores outside [0,1]: {bad}")
252
+ n = normalize(raw)
253
+ q, sem = quality_score(n), semantic_score(n)
254
+ sel = selected_score(q, sem)
255
+ row.update({f"raw_{d}": raw[d] for d in DIMENSIONS})
256
+ row.update({f"normalized_{d}": n[d] for d in DIMENSIONS})
257
+ row.update({"quality_score": q, "semantic_score": sem,
258
+ "selected_vbench_score": sel,
259
+ "selected_vbench_percent": sel * 100.0})
260
+ else:
261
+ problems.append(f"{s}: only {len(raw)}/8 VBench dimensions")
262
+ else:
263
+ problems.append(f"{s}: no VBench scores yet")
264
+ rows.append(row)
265
+
266
+ rows.sort(key=lambda r: (r["base_model"], r["method"], r["target_speedup"]))
267
+
268
+ summary = {
269
+ "protocol": "Self-Forcing Extended-251 Full Evaluation",
270
+ "vbench_long": False,
271
+ "num_strategies": len(rows),
272
+ "prompts_per_strategy": args.expect,
273
+ "seed": 0,
274
+ "video_spec": {"frames": 81, "height": 480, "width": 832, "fps": 16},
275
+ "precision": "bfloat16",
276
+ "normalize_range": NORMALIZE_RANGE,
277
+ "aggregation": {
278
+ "quality": "(n_sc + n_bc + n_ms + 0.5*n_dd + n_aq + n_iq) / 5.5",
279
+ "semantic": "(n_scene + n_oc) / 2",
280
+ "selected": "(4*quality + semantic) / 5",
281
+ "psnr": "-10*log10(mean(per_prompt_mean_mse)), clamp 1e-12",
282
+ "speedup": "100 * (1 - mean(strategy) / mean(matched_ffff))",
283
+ },
284
+ "latency_includes": "all denoise policy GPU compute incl. cache control modules",
285
+ "latency_excludes": "context/KV-cache DiT, text encoder, VAE decode, metrics, I/O",
286
+ "sources": mapping["sources"],
287
+ "mapping": {"path": MAPPING, "sha256": sha256(MAPPING)},
288
+ "environment": {
289
+ "generation_python": "/local/zoubin/cz/envs/self_forcing/bin/python",
290
+ "vbench_python": "/local/zoubin/cz/envs/vbench_eval/bin/python",
291
+ "vbench_source": "/local/zoubin/cz/projects/VBench",
292
+ "vbench_cache_dir": "/local/zoubin/cz/.cache/vbench",
293
+ "platform": platform.platform(),
294
+ },
295
+ "problems": problems,
296
+ "rows": rows,
297
+ }
298
+
299
+ os.makedirs(os.path.join(out_root, "summaries"), exist_ok=True)
300
+ jpath = os.path.join(out_root, "summaries", "final_summary.json")
301
+ with open(jpath, "w") as f:
302
+ json.dump(summary, f, indent=2)
303
+
304
+ cpath = os.path.join(out_root, "summaries", "final_summary.csv")
305
+ if rows:
306
+ keys = sorted({k for r in rows for k in r},
307
+ key=lambda k: (k not in ("strategy", "base_model", "method"), k))
308
+ with open(cpath, "w", newline="") as f:
309
+ w = csv.DictWriter(f, fieldnames=keys)
310
+ w.writeheader()
311
+ w.writerows(rows)
312
+
313
+ hdr = (f"{'strategy':24s} {'videos':>6s} {'lat_ms':>9s} {'speedup%':>9s} "
314
+ f"{'psnr':>7s} {'ssim':>7s} {'lpips':>7s} {'sel_vbench%':>12s}")
315
+ print(hdr)
316
+ print("-" * len(hdr))
317
+ for r in rows:
318
+ sel = r.get("selected_vbench_percent")
319
+ print(f"{r['strategy']:24s} {r['num_videos']:6d} {r['policy_latency_ms']:9.1f} "
320
+ f"{r['speedup_percent_vs_ffff']:9.2f} {r['psnr']:7.2f} {r['ssim']:7.4f} "
321
+ f"{r['lpips']:7.4f} " + (f"{sel:12.3f}" if sel is not None else f"{'-':>12s}"))
322
+ if problems:
323
+ print("\nnotes:")
324
+ for p in problems:
325
+ print(" -", p)
326
+ print(f"\nwrote {jpath}\nwrote {cpath}")
327
+ return 0
328
+
329
+
330
+ if __name__ == "__main__":
331
+ sys.exit(main())
eval/build_mapping.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Build and validate assets/vbench8_extended_subset_mapping.json (Extended-251).
3
+
4
+ Protocol section 4: take the subject_consistency (72), overall_consistency (93)
5
+ and scene (86) suites out of the canonical 946-prompt VBench order, and carry the
6
+ Self-Forcing rewritten extended prompt for each.
7
+
8
+ Selection is strictly by index into VBench_full_info.json -- the short prompt list
9
+ contains two duplicate texts, so text matching is not safe, and the protocol
10
+ forbids it anyway.
11
+
12
+ python eval/build_mapping.py --out assets/vbench8_extended_subset_mapping.json
13
+ """
14
+
15
+ import argparse
16
+ import hashlib
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
+
23
+ SUITES = ["subject_consistency", "overall_consistency", "scene"]
24
+ EXPECTED = {"subject_consistency": 72, "overall_consistency": 93, "scene": 86}
25
+
26
+ DEFAULT_FULL_INFO = "/local/zoubin/cz/projects/VBench/vbench/VBench_full_info.json"
27
+ DEFAULT_PROMPT_DIR = "/local/zoubin/cz/projects/Self-Forcing/prompts/vbench"
28
+
29
+
30
+ def sha256(path):
31
+ h = hashlib.sha256()
32
+ with open(path, "rb") as f:
33
+ for chunk in iter(lambda: f.read(1 << 20), b""):
34
+ h.update(chunk)
35
+ return h.hexdigest()
36
+
37
+
38
+ def main():
39
+ ap = argparse.ArgumentParser()
40
+ ap.add_argument("--full-info", default=DEFAULT_FULL_INFO)
41
+ ap.add_argument("--prompt-dir", default=DEFAULT_PROMPT_DIR)
42
+ ap.add_argument("--out", default=os.path.join(ROOT, "assets/vbench8_extended_subset_mapping.json"))
43
+ args = ap.parse_args()
44
+
45
+ short_path = os.path.join(args.prompt_dir, "all_dimension.txt")
46
+ ext_path = os.path.join(args.prompt_dir, "all_dimension_extended.txt")
47
+
48
+ with open(args.full_info) as f:
49
+ info = json.load(f)
50
+ with open(short_path, encoding="utf-8") as f:
51
+ short = [l.rstrip("\n") for l in f]
52
+ with open(ext_path, encoding="utf-8") as f:
53
+ ext = [l.rstrip("\n") for l in f]
54
+
55
+ errors = []
56
+ if len(short) != 946:
57
+ errors.append(f"short prompts == {len(short)}, expected 946")
58
+ if len(ext) != 946:
59
+ errors.append(f"extended prompts == {len(ext)}, expected 946")
60
+ if len(info) != 946:
61
+ errors.append(f"VBench_full_info rows == {len(info)}, expected 946")
62
+ for i, (row, s) in enumerate(zip(info, short)):
63
+ if row["prompt_en"].strip() != s.strip():
64
+ errors.append(f"short prompt order differs from VBench_full_info at line {i}")
65
+ break
66
+ for i, e in enumerate(ext):
67
+ if not e.strip():
68
+ errors.append(f"empty extended prompt at line {i}")
69
+ break
70
+
71
+ rows = []
72
+ for suite in SUITES:
73
+ suite_index = 0
74
+ for gi, row in enumerate(info):
75
+ if suite not in row["dimension"]:
76
+ continue
77
+ entry = {
78
+ "global_index": gi,
79
+ "prompt_suite": suite,
80
+ "suite_index": suite_index,
81
+ "original_prompt": short[gi],
82
+ "extended_prompt": ext[gi],
83
+ "official_dimensions": list(row["dimension"]),
84
+ }
85
+ # Standard VBench 0.1.5 needs the official scene keywords to score `scene`.
86
+ for key in ("auxiliary_info",):
87
+ if key in row:
88
+ entry[key] = row[key]
89
+ rows.append(entry)
90
+ suite_index += 1
91
+
92
+ counts = {s: sum(1 for r in rows if r["prompt_suite"] == s) for s in SUITES}
93
+ for s, n in EXPECTED.items():
94
+ if counts.get(s) != n:
95
+ errors.append(f"{s} == {counts.get(s)}, expected {n}")
96
+ if len(rows) != 251:
97
+ errors.append(f"mapping rows == {len(rows)}, expected 251")
98
+ if len({r["global_index"] for r in rows}) != len(rows):
99
+ errors.append("duplicate global_index")
100
+ for s in SUITES:
101
+ idx = [r["suite_index"] for r in rows if r["prompt_suite"] == s]
102
+ if idx != list(range(len(idx))):
103
+ errors.append(f"{s} suite_index not contiguous")
104
+
105
+ if errors:
106
+ print("MAPPING VALIDATION FAILED:")
107
+ for e in errors:
108
+ print(" -", e)
109
+ return 1
110
+
111
+ payload = {
112
+ "protocol": "Self-Forcing Extended-251 Full Evaluation",
113
+ "num_rows": len(rows),
114
+ "counts": counts,
115
+ "sources": {
116
+ "vbench_full_info": {"path": args.full_info, "sha256": sha256(args.full_info)},
117
+ "short_prompts": {"path": short_path, "sha256": sha256(short_path)},
118
+ "extended_prompts": {"path": ext_path, "sha256": sha256(ext_path)},
119
+ },
120
+ "rows": rows,
121
+ }
122
+ os.makedirs(os.path.dirname(args.out), exist_ok=True)
123
+ with open(args.out, "w") as f:
124
+ json.dump(payload, f, indent=2)
125
+
126
+ print("mapping OK")
127
+ print(f" rows {len(rows)}")
128
+ for s in SUITES:
129
+ print(f" {s:22s} {counts[s]}")
130
+ for k, v in payload["sources"].items():
131
+ print(f" sha256 {k:18s} {v['sha256'][:16]}...")
132
+ print(f" scene rows with auxiliary_info: "
133
+ f"{sum(1 for r in rows if r['prompt_suite'] == 'scene' and 'auxiliary_info' in r)}")
134
+ print(f"wrote {args.out}")
135
+ return 0
136
+
137
+
138
+ if __name__ == "__main__":
139
+ sys.exit(main())
eval/generate_eval.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Generate the Extended-251 videos for every strategy of one base model.
3
+
4
+ Work is sharded by prompt, not by strategy, because the protocol requires each
5
+ strategy's clip to be paired with the FFFF clip for the *same* prompt and seed
6
+ (section 8.1). A shard therefore generates FFFF first, keeps its decoded frames,
7
+ and immediately scores every cache strategy against them -- no 25 GB of reference
8
+ frames on disk, and the pairing cannot get mismatched.
9
+
10
+ CUDA_VISIBLE_DEVICES=0 python eval/generate_eval.py \
11
+ --base self_forcing --shard 0 --num-shards 4 --out-root eval_out
12
+
13
+ Re-running skips prompts whose per-strategy record is already complete.
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import sys
20
+ import time
21
+
22
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
+ sys.path.insert(0, ROOT)
24
+
25
+ from eval.strategies import load_strategies # noqa: E402
26
+ from harness import denoising_steps_for, BASES, enter_base, load_pipeline, use_denoising_steps # noqa: E402
27
+
28
+ MAPPING = os.path.join(ROOT, "assets/vbench8_extended_subset_mapping.json")
29
+
30
+
31
+ def parse_args():
32
+ p = argparse.ArgumentParser()
33
+ p.add_argument("--base", choices=sorted(BASES), required=True)
34
+ p.add_argument("--shard", type=int, default=0)
35
+ p.add_argument("--num-shards", type=int, default=1)
36
+ p.add_argument("--out-root", default="eval_out")
37
+ p.add_argument("--seed", type=int, default=0)
38
+ p.add_argument("--num-latent-frames", type=int, default=21)
39
+ p.add_argument("--fps", type=int, default=16)
40
+ p.add_argument("--limit", type=int, default=None, help="Smoke-test: first N prompts")
41
+ p.add_argument("--only-strategy", default=None, help="Smoke-test: one strategy name")
42
+ p.add_argument("--strategies", default=None,
43
+ help="Comma-separated subset to generate (the FFFF reference is always kept)")
44
+ p.add_argument("--overwrite", action="store_true")
45
+ p.add_argument("--regenerate-ffff", action="store_true",
46
+ help="Regenerate the FFFF reference instead of reading its MP4")
47
+ return p.parse_args()
48
+
49
+
50
+ def main():
51
+ args = parse_args()
52
+ out_root = (args.out_root if os.path.isabs(args.out_root)
53
+ else os.path.join(ROOT, args.out_root))
54
+
55
+ with open(MAPPING) as f:
56
+ mapping = json.load(f)
57
+ rows = mapping["rows"]
58
+ if args.limit:
59
+ rows = rows[:args.limit]
60
+ shard_rows = rows[args.shard::args.num_shards]
61
+
62
+ strategies = load_strategies(args.base)
63
+ if args.only_strategy:
64
+ keep = {strategies[0]["name"], args.only_strategy}
65
+ strategies = [s for s in strategies if s["name"] in keep]
66
+ if args.strategies:
67
+ keep = set(args.strategies.split(","))
68
+ strategies = [s for s in strategies if s["is_reference"] or s["name"] in keep]
69
+ ref_name = strategies[0]["name"]
70
+ assert strategies[0]["is_reference"], "first strategy must be FFFF"
71
+
72
+ print(f"base={args.base} shard {args.shard}/{args.num_shards}: "
73
+ f"{len(shard_rows)} prompts x {len(strategies)} strategies", flush=True)
74
+
75
+ base = enter_base(args.base)
76
+
77
+ import torch
78
+ from einops import rearrange
79
+ from torchvision.io import read_video, write_video
80
+ from utils.misc import set_seed
81
+
82
+ from cachelib import (CacheController, build_method, cached_inference, install,
83
+ parse_schedule)
84
+ from eval.pixel_metrics import PixelMetrics
85
+
86
+ torch.set_grad_enabled(False)
87
+ device = torch.device("cuda")
88
+ pipeline = load_pipeline(base)
89
+ num_steps = len(pipeline.denoising_step_list)
90
+ metrics = PixelMetrics(device)
91
+
92
+ def record_path(strategy, row):
93
+ d = os.path.join(out_root, "per_prompt", strategy)
94
+ os.makedirs(d, exist_ok=True)
95
+ return os.path.join(d, f"{row['prompt_suite']}_{row['suite_index']:03d}.json")
96
+
97
+ def video_path(strategy, row):
98
+ d = os.path.join(out_root, "generated_videos", strategy, row["prompt_suite"])
99
+ os.makedirs(d, exist_ok=True)
100
+ return os.path.join(d, f"{row['suite_index']:03d}.mp4")
101
+
102
+ def ref_cache_path(row):
103
+ d = os.path.join(out_root, "ref_cache", args.base)
104
+ os.makedirs(d, exist_ok=True)
105
+ return os.path.join(d, f"{row['prompt_suite']}_{row['suite_index']:03d}.pt")
106
+
107
+ def is_done(strategy, row):
108
+ p = record_path(strategy, row)
109
+ if args.overwrite or not os.path.exists(p):
110
+ return False
111
+ try:
112
+ with open(p) as f:
113
+ rec = json.load(f)
114
+ return rec.get("status") == "complete" and os.path.exists(rec.get("video", ""))
115
+ except Exception:
116
+ return False
117
+
118
+ def write_json(path, payload):
119
+ tmp = path + ".tmp"
120
+ with open(tmp, "w") as f:
121
+ json.dump(payload, f, indent=2)
122
+ os.replace(tmp, path) # atomic: a partial file is never seen as complete
123
+
124
+ def run_one(strategy, row):
125
+ kw = dict(indicator=strategy["indicator"], coefficients=strategy["coefficients"])
126
+ if strategy["method"] != "none" and strategy.get("param"):
127
+ kw[strategy["param"]] = strategy["value"]
128
+ n_steps = strategy.get("num_inference_steps") or num_steps
129
+ original_steps = use_denoising_steps(pipeline, n_steps)
130
+ sched = strategy.get("schedule") or ("F" * n_steps)
131
+ first = strategy.get("first_chunk_schedule")
132
+ # Chunk 0 may have a longer schedule than the rest (e.g. FFFF over naive 2-step).
133
+ first_steps = (denoising_steps_for(original_steps, len(first))
134
+ if first and len(first) != n_steps else None)
135
+ ctrl = CacheController(
136
+ build_method(strategy["method"], **kw), num_steps=n_steps,
137
+ forced_steps=parse_schedule(sched, n_steps),
138
+ first_chunk_forced_steps=(parse_schedule(first, len(first)) if first else None))
139
+ install(pipeline.generator.model, ctrl)
140
+
141
+ # Identical noise for every strategy: same seed, same draw order, same shape.
142
+ set_seed(args.seed)
143
+ noise = torch.randn([1, args.num_latent_frames, 16, 60, 104],
144
+ device=device, dtype=torch.bfloat16)
145
+ wall0 = time.time()
146
+ video, latents, timing = cached_inference(
147
+ pipeline, ctrl, noise, [row["extended_prompt"]], decode=True,
148
+ first_chunk_steps=first_steps, sampler=strategy.get("sampler", "renoise"))
149
+ wall = time.time() - wall0
150
+ pipeline.vae.model.clear_cache()
151
+ pipeline.denoising_step_list = original_steps
152
+ return video, timing, ctrl.summary(), wall
153
+
154
+ # Warm up every strategy once on a throwaway prompt and discard the result.
155
+ # Without this the first recorded prompt carries one-off costs -- notably
156
+ # TaylorSeer's torch.compile of the forecast, which made its first video look
157
+ # slower than FFFF.
158
+ if shard_rows:
159
+ print("warm-up pass (not recorded)", flush=True)
160
+ for strategy in strategies:
161
+ run_one(strategy, shard_rows[0])
162
+ torch.cuda.empty_cache()
163
+
164
+ t_start = time.time()
165
+ for n, row in enumerate(shard_rows):
166
+ todo = [s for s in strategies if not is_done(s["name"], row)]
167
+ if not todo:
168
+ continue
169
+ # The reference is needed for pixel metrics even when only some strategies
170
+ # are outstanding.
171
+ need_ref = any(not s["is_reference"] for s in todo)
172
+ ref_frames = None
173
+ # The FFFF reference frames (pre-MP4, fp16) are cached on disk after their
174
+ # first generation, so later strategy runs do not regenerate FFFF at all.
175
+ rc = ref_cache_path(row)
176
+ ref_from_mp4 = False
177
+ if need_ref and os.path.exists(rc):
178
+ ref_frames = torch.load(rc, map_location=device)
179
+ need_ref = False
180
+ elif need_ref and os.path.exists(video_path(ref_name, row)) and not args.regenerate_ffff:
181
+ # Reference from the FFFF run's MP4 (H.264, ~40+ dB above the 12-20 dB
182
+ # strategy PSNRs, so the compression adds <1 % to the MSE); never
183
+ # regenerate FFFF just to compare against it.
184
+ vid, _, _ = read_video(video_path(ref_name, row), pts_unit="sec", output_format="TCHW")
185
+ ref_frames = (vid.to(device, torch.float16) / 255.0)
186
+ ref_from_mp4 = True
187
+ need_ref = False
188
+
189
+ for strategy in strategies:
190
+ if strategy["is_reference"]:
191
+ if strategy not in todo and not need_ref:
192
+ continue
193
+ elif strategy not in todo:
194
+ continue
195
+
196
+ video, timing, summary, wall = run_one(strategy, row)
197
+ frames = video[0] # [T, C, H, W] in [0, 1]
198
+
199
+ if strategy["is_reference"]:
200
+ ref_frames = frames.detach().to(torch.float16)
201
+ if not os.path.exists(rc):
202
+ torch.save(ref_frames.cpu(), rc + ".tmp")
203
+ os.replace(rc + ".tmp", rc)
204
+ pix = PixelMetrics.identity(frames.shape[0])
205
+ else:
206
+ pix = metrics.compute(frames, ref_frames)
207
+
208
+ if strategy in todo:
209
+ vp = video_path(strategy["name"], row)
210
+ arr = (255.0 * rearrange(frames, "t c h w -> t h w c")).clamp(0, 255)
211
+ write_video(vp, arr.to(torch.uint8).cpu(), fps=args.fps)
212
+
213
+ write_json(record_path(strategy["name"], row), {
214
+ "status": "complete",
215
+ "protocol": "Self-Forcing Extended-251 Full Evaluation",
216
+ "strategy": strategy["name"],
217
+ "base_model": args.base,
218
+ "method": strategy["method"],
219
+ "param": strategy["param"],
220
+ "param_value": strategy["value"],
221
+ "target_speedup": strategy["target"],
222
+ "schedule": strategy.get("schedule") or ("F" * (strategy.get("num_inference_steps") or 4)),
223
+ "num_inference_steps": strategy.get("num_inference_steps") or 4,
224
+ "first_chunk_schedule": strategy.get("first_chunk_schedule"),
225
+ "sampler": strategy.get("sampler", "renoise"),
226
+ "global_index": row["global_index"],
227
+ "prompt_suite": row["prompt_suite"],
228
+ "suite_index": row["suite_index"],
229
+ "prompt": row["extended_prompt"],
230
+ "original_prompt": row["original_prompt"],
231
+ "seed": args.seed,
232
+ "video": vp,
233
+ "num_frames": int(frames.shape[0]),
234
+ "height": int(frames.shape[2]),
235
+ "width": int(frames.shape[3]),
236
+ "fps": args.fps,
237
+ "policy_latency_ms": timing["denoise_dit_ms"],
238
+ "excluded_context_kv_latency_ms": timing["context_kv_dit_ms"],
239
+ "wall_generation_s": wall,
240
+ "reference_strategy": ref_name,
241
+ "pixel_metrics_vs_ffff": pix,
242
+ "reference_source": "ffff_mp4" if ref_from_mp4 else "ffff_frames",
243
+ "cache_diagnostics": dict(summary),
244
+ })
245
+ del frames, video
246
+
247
+ ref_frames = None
248
+ done = n + 1
249
+ rate = (time.time() - t_start) / done
250
+ print(f"[{args.base} shard {args.shard}] {done}/{len(shard_rows)} prompts "
251
+ f"({row['prompt_suite']}/{row['suite_index']:03d}) "
252
+ f"{rate:.1f}s/prompt eta {(len(shard_rows) - done) * rate / 60:.0f} min",
253
+ flush=True)
254
+
255
+ print(f"shard {args.shard} done in {(time.time() - t_start) / 60:.1f} min", flush=True)
256
+
257
+
258
+ if __name__ == "__main__":
259
+ main()
eval/generate_predictor_eval.py ADDED
@@ -0,0 +1,532 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Extended-251 videos for the Causal-Forcing-a single-block Predictors.
3
+
4
+ Same protocol as generate_eval.py: work is sharded by prompt, FFFF is generated
5
+ first in the same process and kept as the pixel-metric reference, every strategy
6
+ sees the identical noise draw, and the per-prompt record has the layout
7
+ aggregate.py / run_vbench.py expect.
8
+
9
+ Strategies (prefix ``cfa`` = Causal-Forcing-a stack):
10
+
11
+ cfa_ffff reference, 28 full DiT forwards
12
+ cfa_<model>_fpff chunks 1..6: F P F F (22 full + 6 Predictor)
13
+ cfa_<model>_fppf chunks 1..6: F P P F (16 full + 12 Predictor)
14
+ cfa_<model>_fppp chunks 1..6: F P P P (10 full + 18 Predictor)
15
+
16
+ where <model> is one of atc_chunk, atc_last_frame, disca (Stage-1 Layer-17
17
+ Predictors) or atc_chunk_s2 (Stage-2 DMD EMA). Chunk 0 is always fully denoised.
18
+ ``<model>@<step>`` selects another checkpoint of that run and names the strategy
19
+ cfa_<model>_s<step>..., e.g. ``disca@500`` -> cfa_disca_s500_fppf; a bare model
20
+ uses --checkpoint-step (default 2000) and the unsuffixed name.
21
+
22
+ cfa_naive2step chunks 1..: the 2-step subset of the grid (t = 1000 / 250,
23
+ the same subset as cf_naive2step_c0FFFF), no Predictor
24
+
25
+ CUDA_VISIBLE_DEVICES=3 python eval/generate_predictor_eval.py --shard 0 --num-shards 4
26
+ """
27
+
28
+ import argparse
29
+ import json
30
+ import os
31
+ import sys
32
+ import time
33
+
34
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
35
+ REPO = os.environ.get("CFA_REPO", "/local/zoubin/cz/projects/Causal-Forcing-a")
36
+ MAPPING = os.path.join(ROOT, "assets/vbench8_extended_subset_mapping.json")
37
+
38
+ BASE = {
39
+ "repo": REPO,
40
+ "config": "configs/causal_forcing_dmd_chunkwise.yaml",
41
+ "checkpoint": "checkpoints/chunkwise/causal_forcing.pt",
42
+ }
43
+ MODELS = {
44
+ "atc_chunk": "output/layer17_stage1_atc_chunk_1000p_21f_seed0_4gpu_b16_acc1_2000steps",
45
+ "atc_last_frame": "output/layer17_stage1_atc_last_frame_1000p_21f_seed0_4gpu_b16_acc1_2000steps",
46
+ "disca": "output/layer17_stage1_disca_1000p_21f_seed0_4gpu_b16_acc1_2000steps",
47
+ # Stage-2 random-exit DMD (LoRA critic), EMA weights (a (run dir, file name) pair;
48
+ # a plain directory gets checkpoint_step_<N>/predictor.safetensors).
49
+ "atc_chunk_s2": ("output/layer17_stage2_dmd_atc_chunk_lora128_4gpu_b2_2000steps", "predictor_ema.safetensors"),
50
+ }
51
+ NAIVE = {"naive2step": [0, 3]} # pseudo-models: reduced step grid, no Predictor
52
+
53
+
54
+ def checkpoint_weights(model, step):
55
+ """Weights file of ``model`` at training step ``step`` (Stage-1 runs zero-pad the
56
+ checkpoint directory, Stage-2 does not)."""
57
+ spec = MODELS[model]
58
+ run_dir, fname = spec if isinstance(spec, tuple) else (spec, "predictor.safetensors")
59
+ for d in (f"checkpoint_step_{step:04d}", f"checkpoint_step_{step}"):
60
+ w = os.path.join(REPO, run_dir, d, fname)
61
+ if os.path.exists(w):
62
+ return w
63
+ raise FileNotFoundError(f"{model} step {step}: no checkpoint under {os.path.join(REPO, run_dir)}")
64
+ PATTERNS = {"FPFF": {1}, "FPPF": {1, 2}, "FPPP": {1, 2, 3}}
65
+ # Reduced schedules: which step indices of the 4-step grid a chunk (after chunk 0)
66
+ # actually visits. FPF keeps steps 0 and 3 (t = 1000 / 250) as Full and runs one
67
+ # Predictor step at step 1 (t = 750, anchored on step 0 as in training), skipping
68
+ # step 2 entirely: 3 denoising positions per chunk, 2 Full + 1 Predictor.
69
+ RUN_STEPS = {"FPF": [0, 1, 3]}
70
+ PATTERNS["FPF"] = {1}
71
+ NUM_CHUNKS, FRAMES_PER_CHUNK, NUM_STEPS = 7, 3, 4
72
+ # A Predictor step runs 1 of the 30 Teacher blocks (plus a small fusion MLP);
73
+ # its compute-equivalent cost is booked as 1/30 of a full forward.
74
+ PREDICTOR_COMPUTE_EQUIV = 1.0 / 30.0
75
+
76
+
77
+ def parse_args():
78
+ p = argparse.ArgumentParser()
79
+ p.add_argument("--shard", type=int, default=0)
80
+ p.add_argument("--num-shards", type=int, default=1)
81
+ p.add_argument("--out-root", default="eval_out")
82
+ p.add_argument("--seed", type=int, default=0)
83
+ p.add_argument("--fps", type=int, default=16)
84
+ p.add_argument("--models", default=",".join(MODELS))
85
+ p.add_argument("--patterns", default="FPPF,FPPP")
86
+ p.add_argument("--checkpoint-step", type=int, default=2000)
87
+ p.add_argument("--num-chunks", type=int, default=NUM_CHUNKS,
88
+ help="Video length in 3-latent-frame chunks (7 = the 81-frame protocol; longer "
89
+ "videos use a rolling 21-frame KV window like Self-Forcing's long-video mode)")
90
+ p.add_argument("--ref-tag", default="",
91
+ help="Inserted into the reference name, e.g. _14c -> cfa_14c_ffff (a separate FFFF "
92
+ "reference for a different video length)")
93
+ p.add_argument("--tag", default="",
94
+ help="Inserted into strategy names after the model, e.g. _s1000 -> "
95
+ "cfa_disca_s1000_fppf, so another checkpoint does not overwrite the default rows")
96
+ p.add_argument("--regenerate-ffff", action="store_true",
97
+ help="Regenerate the FFFF reference instead of reading cfa_ffff's MP4")
98
+ p.add_argument("--limit", type=int, default=None, help="Smoke-test: first N prompts")
99
+ p.add_argument("--overwrite", action="store_true")
100
+ p.add_argument("--retime-only", action="store_true",
101
+ help="Do not regenerate videos/metrics; re-time FFFF and each strategy in the same "
102
+ "process per prompt and update policy_latency_ms / matched_ffff_policy_latency_ms.")
103
+ p.add_argument("--retime-max-ffff-ms", type=float, default=3500.0,
104
+ help="With --retime-only, records whose paired FFFF exceeds this are re-timed again "
105
+ "(up to --retime-passes passes); clean records are left alone.")
106
+ p.add_argument("--retime-passes", type=int, default=3)
107
+ p.add_argument("--retime-strategy-only", action="store_true",
108
+ help="Re-time only the strategies (no FFFF run): rerun each selected record's "
109
+ "rollout on an idle GPU and replace policy_latency_ms; the generation-run "
110
+ "value is kept under generation_run_latency.")
111
+ p.add_argument("--written-before", type=float, default=None,
112
+ help="With --retime-strategy-only: only records whose file mtime is before this "
113
+ "epoch second (the window in which another job shared the GPU)")
114
+ return p.parse_args()
115
+
116
+
117
+ def build_strategies(args):
118
+ out = [{"name": f"cfa{args.ref_tag}_ffff", "model": None, "pattern": "FFFF", "is_reference": True}]
119
+ for token in args.models.split(","):
120
+ if token in NAIVE:
121
+ out.append({"name": f"cfa_{token}{args.tag}", "model": None, "pattern": token.upper(),
122
+ "naive": token, "is_reference": False})
123
+ continue
124
+ model, _, step = token.partition("@")
125
+ step = int(step) if step else args.checkpoint_step
126
+ key = f"{model}@{step}" # one loaded Predictor per (model, step)
127
+ label = f"{model}_s{step}" if "@" in token else model
128
+ for pattern in args.patterns.split(","):
129
+ out.append({"name": f"cfa_{label}{args.tag}_{pattern.lower()}", "model": key,
130
+ "base_model": model, "checkpoint_step": step,
131
+ "pattern": pattern.upper(), "is_reference": False})
132
+ return out
133
+
134
+
135
+ class FinalHiddenCapture:
136
+ """Grab the Teacher's final hidden (input of its output head) on a Full step."""
137
+
138
+ def __init__(self, teacher):
139
+ self.enabled, self.value = False, None
140
+ self.handle = teacher.head.register_forward_pre_hook(self._hook)
141
+
142
+ def _hook(self, _module, inputs):
143
+ if self.enabled:
144
+ self.value = inputs[0].detach()
145
+
146
+ def start(self):
147
+ self.value, self.enabled = None, True
148
+
149
+ def finish(self):
150
+ self.enabled = False
151
+ if self.value is None:
152
+ raise RuntimeError("Teacher final hidden was not captured")
153
+ value, self.value = self.value, None
154
+ return value
155
+
156
+
157
+ def main():
158
+ args = parse_args()
159
+ out_root = args.out_root if os.path.isabs(args.out_root) else os.path.join(ROOT, args.out_root)
160
+ with open(MAPPING) as f:
161
+ rows = json.load(f)["rows"]
162
+ if args.limit:
163
+ rows = rows[:args.limit]
164
+ shard_rows = rows[args.shard::args.num_shards]
165
+ strategies = build_strategies(args)
166
+ ref_name = strategies[0]["name"]
167
+ print(f"cfa shard {args.shard}/{args.num_shards}: {len(shard_rows)} prompts x "
168
+ f"{len(strategies)} strategies", flush=True)
169
+
170
+ os.chdir(REPO)
171
+ sys.path.insert(0, ROOT)
172
+ sys.path.insert(0, REPO) # pipeline/, utils/, predictor_training/ from Causal-Forcing-a
173
+
174
+ import torch
175
+ from einops import rearrange
176
+ from safetensors.torch import load_file
177
+ from torchvision.io import read_video, write_video
178
+
179
+ from eval.pixel_metrics import PixelMetrics
180
+ from harness import load_pipeline
181
+ from predictor_training.metadata import build_single_block_predictor, read_predictor_config
182
+ from predictor_training.online import online_predictor_step as _online_predictor_step
183
+ from predictor_training.offline_data import TOKENS_PER_CHUNK
184
+
185
+ def online_predictor_step(*, history_cache, chunk, **kw):
186
+ """Stage-1 contract: the Predictor sees the clean K/V of the *earlier* chunks.
187
+ With the rolling 21-frame window (long videos) the cache holds the last six
188
+ clean chunks plus the current noisy one, so the history is everything before
189
+ the current chunk's tokens rather than ``chunk * TOKENS_PER_CHUNK`` (which
190
+ the stock helper asserts and which exceeds the window from chunk 7 on).
191
+ For chunk <= 6 the two are identical."""
192
+ if chunk * TOKENS_PER_CHUNK <= int(history_cache["local_end_index"].item()):
193
+ return _online_predictor_step(history_cache=history_cache, chunk=chunk, **kw)
194
+ end = int(history_cache["local_end_index"].item()) - TOKENS_PER_CHUNK
195
+ window = {"k": history_cache["k"][:, :end], "v": history_cache["v"][:, :end],
196
+ "local_end_index": torch.tensor([end], device=history_cache["k"].device)}
197
+ # left-pad to the absolute length the stock helper slices; the attention
198
+ # window (max_attention_size = 21 frames) never reaches the padding
199
+ pad = chunk * TOKENS_PER_CHUNK - end
200
+ if pad > 0:
201
+ z = history_cache["k"].new_zeros(history_cache["k"].shape[0], pad, *history_cache["k"].shape[2:])
202
+ window = {"k": torch.cat([z, window["k"]], 1), "v": torch.cat([z, window["v"]], 1),
203
+ "local_end_index": torch.tensor([chunk * TOKENS_PER_CHUNK], device=z.device)}
204
+ return _online_predictor_step(history_cache=window, chunk=chunk, **kw)
205
+ from utils.misc import set_seed
206
+
207
+ torch.set_grad_enabled(False)
208
+ device = torch.device("cuda")
209
+ pipeline = load_pipeline(BASE)
210
+ n_chunks = args.num_chunks
211
+ if n_chunks * FRAMES_PER_CHUNK > 21:
212
+ # Longer than the 21-latent-frame training context: roll the KV cache over a
213
+ # 21-frame local window (the model's long-video mechanism; RoPE is relative
214
+ # so positions inside the window stay in the trained range).
215
+ model = pipeline.generator.model
216
+ for m in [model] + [b for b in model.blocks] + [b.self_attn for b in model.blocks]:
217
+ m.local_attn_size = 21
218
+ if hasattr(m, "max_attention_size"):
219
+ m.max_attention_size = 21 * pipeline.frame_seq_length
220
+ pipeline.local_attn_size = 21
221
+ pipeline.kv_cache1 = None
222
+ print(f"long video: {n_chunks} chunks = {n_chunks * FRAMES_PER_CHUNK} latent frames, "
223
+ f"rolling KV window 21 frames", flush=True)
224
+ teacher = pipeline.generator.model
225
+ capture = FinalHiddenCapture(teacher)
226
+ metrics = PixelMetrics(device)
227
+ timesteps = pipeline.denoising_step_list.to(device)
228
+ assert len(timesteps) == NUM_STEPS and pipeline.num_frame_per_block == FRAMES_PER_CHUNK
229
+
230
+ predictors, predictor_configs = {}, {}
231
+ for model in {s["model"] for s in strategies if s["model"]}:
232
+ base, step = model.split("@")
233
+ weights = checkpoint_weights(base, int(step))
234
+ config = read_predictor_config(weights)
235
+ predictor = build_single_block_predictor(teacher, config, gradient_checkpointing=False)
236
+ predictor.load_state_dict(load_file(weights, device="cpu"), strict=True)
237
+ predictors[model] = predictor.to(device).eval().requires_grad_(False)
238
+ predictor_configs[model] = {**config, "weights": weights}
239
+ print(f"loaded {model}: {config}", flush=True)
240
+
241
+ def reset_caches():
242
+ if pipeline.kv_cache1 is None:
243
+ pipeline._initialize_kv_cache(1, torch.bfloat16, device)
244
+ pipeline._initialize_crossattn_cache(1, torch.bfloat16, device)
245
+ for cache in pipeline.kv_cache1:
246
+ cache["global_end_index"] = torch.tensor([0], dtype=torch.long, device=device)
247
+ cache["local_end_index"] = torch.tensor([0], dtype=torch.long, device=device)
248
+ for cache in pipeline.crossattn_cache:
249
+ cache["is_init"] = False
250
+
251
+ def rollout(conditional, noise, predictor=None, predictor_steps=frozenset(), run_steps=None):
252
+ """Mirror of cachelib.runner.cached_inference with Predictor steps spliced in."""
253
+ reset_caches()
254
+ output = torch.zeros_like(noise)
255
+ denoise_events, context_events = [], []
256
+ full_calls = predictor_calls = 0
257
+ previous_hidden = None
258
+ for chunk in range(n_chunks):
259
+ start_frame = chunk * FRAMES_PER_CHUNK
260
+ token_start = start_frame * pipeline.frame_seq_length
261
+ noisy_input = noise[:, start_frame:start_frame + FRAMES_PER_CHUNK]
262
+ current_hidden = [None] * NUM_STEPS
263
+ # chunk 0 always runs the full grid; later chunks may visit a subset
264
+ steps = list(range(NUM_STEPS)) if (run_steps is None or chunk == 0) else list(run_steps)
265
+ for si, step in enumerate(steps):
266
+ timestep = torch.ones([1, FRAMES_PER_CHUNK], device=device, dtype=torch.int64) * timesteps[step]
267
+ start_ev = torch.cuda.Event(enable_timing=True)
268
+ end_ev = torch.cuda.Event(enable_timing=True)
269
+ start_ev.record()
270
+ if predictor is not None and chunk > 0 and step in predictor_steps:
271
+ hidden, flow = online_predictor_step(
272
+ predictor=predictor, teacher=teacher, noisy_input=noisy_input,
273
+ timestep=timestep, anchor_timestep=torch.ones_like(timestep) * timesteps[step - 1],
274
+ anchor_hidden=current_hidden[step - 1], previous_hidden=previous_hidden[step],
275
+ history_cache=pipeline.kv_cache1[predictor.source_layer],
276
+ cross_cache=pipeline.crossattn_cache[predictor.source_layer], chunk=chunk)
277
+ denoised_pred = pipeline.generator._convert_flow_pred_to_x0(
278
+ flow_pred=flow.flatten(0, 1), xt=noisy_input.flatten(0, 1),
279
+ timestep=timestep.flatten(0, 1)).unflatten(0, flow.shape[:2])
280
+ current_hidden[step] = hidden
281
+ predictor_calls += 1
282
+ else:
283
+ capture.start()
284
+ _, denoised_pred = pipeline.generator(
285
+ noisy_image_or_video=noisy_input, conditional_dict=conditional,
286
+ timestep=timestep, kv_cache=pipeline.kv_cache1,
287
+ crossattn_cache=pipeline.crossattn_cache, current_start=token_start)
288
+ current_hidden[step] = capture.finish()
289
+ full_calls += 1
290
+ end_ev.record()
291
+ denoise_events.append((start_ev, end_ev))
292
+ if si < len(steps) - 1:
293
+ # Same RNG draw order as the protocol runner, so every strategy
294
+ # re-noises with identical samples.
295
+ noisy_input = pipeline.scheduler.add_noise(
296
+ denoised_pred.flatten(0, 1), torch.randn_like(denoised_pred.flatten(0, 1)),
297
+ timesteps[steps[si + 1]] * torch.ones([FRAMES_PER_CHUNK], device=device, dtype=torch.long),
298
+ ).unflatten(0, denoised_pred.shape[:2])
299
+ output[:, start_frame:start_frame + FRAMES_PER_CHUNK] = denoised_pred
300
+ # Clean-context KV refresh: full DiT, timed separately, never added in.
301
+ ctx_start = torch.cuda.Event(enable_timing=True)
302
+ ctx_end = torch.cuda.Event(enable_timing=True)
303
+ ctx_start.record()
304
+ pipeline.generator(
305
+ noisy_image_or_video=denoised_pred, conditional_dict=conditional,
306
+ timestep=torch.ones_like(timestep) * pipeline.args.context_noise,
307
+ kv_cache=pipeline.kv_cache1, crossattn_cache=pipeline.crossattn_cache,
308
+ current_start=token_start)
309
+ ctx_end.record()
310
+ context_events.append((ctx_start, ctx_end))
311
+ previous_hidden = current_hidden
312
+ torch.cuda.synchronize()
313
+ timing = {
314
+ "denoise_dit_ms": sum(s.elapsed_time(e) for s, e in denoise_events),
315
+ "context_kv_dit_ms": sum(s.elapsed_time(e) for s, e in context_events),
316
+ "num_denoise_forwards": len(denoise_events),
317
+ }
318
+ video = pipeline.vae.decode_to_pixel(output, use_cache=False)
319
+ video = (video * 0.5 + 0.5).clamp(0, 1)
320
+ pipeline.vae.model.clear_cache()
321
+ return video, timing, {"full_forwards": full_calls, "predictor_forwards": predictor_calls}
322
+
323
+ def run_one(strategy, row):
324
+ set_seed(args.seed)
325
+ noise = torch.randn([1, n_chunks * FRAMES_PER_CHUNK, 16, 60, 104], device=device, dtype=torch.bfloat16)
326
+ conditional = pipeline.text_encoder(text_prompts=[row["extended_prompt"]])
327
+ wall0 = time.time()
328
+ if strategy["is_reference"]:
329
+ video, timing, counts = rollout(conditional, noise)
330
+ elif strategy.get("naive"):
331
+ video, timing, counts = rollout(conditional, noise, None, frozenset(),
332
+ NAIVE[strategy["naive"]])
333
+ else:
334
+ video, timing, counts = rollout(conditional, noise, predictors[strategy["model"]],
335
+ PATTERNS[strategy["pattern"]],
336
+ RUN_STEPS.get(strategy["pattern"]))
337
+ return video, timing, counts, time.time() - wall0
338
+
339
+ def record_path(strategy, row):
340
+ d = os.path.join(out_root, "per_prompt", strategy)
341
+ os.makedirs(d, exist_ok=True)
342
+ return os.path.join(d, f"{row['prompt_suite']}_{row['suite_index']:03d}.json")
343
+
344
+ def video_path(strategy, row):
345
+ d = os.path.join(out_root, "generated_videos", strategy, row["prompt_suite"])
346
+ os.makedirs(d, exist_ok=True)
347
+ return os.path.join(d, f"{row['suite_index']:03d}.mp4")
348
+
349
+ def is_done(strategy, row):
350
+ p = record_path(strategy, row)
351
+ if args.overwrite or not os.path.exists(p):
352
+ return False
353
+ try:
354
+ with open(p) as f:
355
+ rec = json.load(f)
356
+ return rec.get("status") == "complete" and os.path.exists(rec.get("video", ""))
357
+ except Exception:
358
+ return False
359
+
360
+ def write_json(path, payload):
361
+ tmp = path + ".tmp"
362
+ with open(tmp, "w") as f:
363
+ json.dump(payload, f, indent=2)
364
+ os.replace(tmp, path)
365
+
366
+ if shard_rows:
367
+ print("warm-up pass (not recorded)", flush=True)
368
+ for strategy in strategies:
369
+ run_one(strategy, shard_rows[0])
370
+ torch.cuda.empty_cache()
371
+
372
+ if args.retime_strategy_only:
373
+ targets = [s for s in strategies if not s["is_reference"]]
374
+ n_done = 0
375
+ for n, row in enumerate(shard_rows):
376
+ for strategy in targets:
377
+ rp = record_path(strategy["name"], row)
378
+ if not os.path.exists(rp):
379
+ continue
380
+ if args.written_before and os.path.getmtime(rp) >= args.written_before:
381
+ continue
382
+ rec = json.load(open(rp))
383
+ if rec.get("status") != "complete" or "generation_run_latency" in rec:
384
+ continue
385
+ _, timing, counts, _ = run_one(strategy, row)
386
+ assert counts["full_forwards"] == rec["cache_diagnostics"]["full_forwards"], rp
387
+ rec["generation_run_latency"] = {
388
+ "policy_latency_ms": rec["policy_latency_ms"],
389
+ "excluded_context_kv_latency_ms": rec["excluded_context_kv_latency_ms"]}
390
+ rec["policy_latency_ms"] = timing["denoise_dit_ms"]
391
+ rec["excluded_context_kv_latency_ms"] = timing["context_kv_dit_ms"]
392
+ rec["latency_retimed"] = "strategy-only rerun on an idle GPU (FFFF not re-run)"
393
+ write_json(rp, rec)
394
+ n_done += 1
395
+ if (n + 1) % 20 == 0:
396
+ print(f"[strategy retime shard {args.shard}] {n + 1}/{len(shard_rows)}", flush=True)
397
+ print(f"shard {args.shard}: {n_done} records re-timed", flush=True)
398
+ return
399
+
400
+ if args.retime_only:
401
+ # Protocol "matched FFFF": pair every strategy with an FFFF run of the same
402
+ # prompt in the same process, seconds apart, so contention cancels in the
403
+ # ratio. Only records whose pairing is missing or dirty are touched.
404
+ targets = [s for s in strategies if not s["is_reference"]]
405
+ for pass_index in range(args.retime_passes):
406
+ dirty = 0
407
+ for n, row in enumerate(shard_rows):
408
+ todo = []
409
+ for strategy in targets:
410
+ rp = record_path(strategy["name"], row)
411
+ if not os.path.exists(rp):
412
+ continue
413
+ rec = json.load(open(rp))
414
+ m = rec.get("matched_ffff_policy_latency_ms")
415
+ if m is None or m > args.retime_max_ffff_ms:
416
+ todo.append((strategy, rp, rec))
417
+ if not todo:
418
+ continue
419
+ dirty += len(todo)
420
+ _, ref_timing, _, _ = run_one(strategies[0], row)
421
+ for strategy, rp, rec in todo:
422
+ _, timing, _, _ = run_one(strategy, row)
423
+ rec["policy_latency_ms"] = timing["denoise_dit_ms"]
424
+ rec["excluded_context_kv_latency_ms"] = timing["context_kv_dit_ms"]
425
+ rec["matched_ffff_policy_latency_ms"] = ref_timing["denoise_dit_ms"]
426
+ rec["matched_ffff_context_kv_latency_ms"] = ref_timing["context_kv_dit_ms"]
427
+ rec["latency_source"] = "paired retime (same process as FFFF)"
428
+ write_json(rp, rec)
429
+ if (n + 1) % 10 == 0:
430
+ print(f"[retime pass {pass_index} shard {args.shard}] {n + 1}/{len(shard_rows)} "
431
+ f"ffff={ref_timing['denoise_dit_ms']:.0f}ms", flush=True)
432
+ print(f"[retime pass {pass_index} shard {args.shard}] re-timed {dirty} records", flush=True)
433
+ if dirty == 0:
434
+ break
435
+ return
436
+
437
+ t_start = time.time()
438
+ for n, row in enumerate(shard_rows):
439
+ todo = [s for s in strategies if not is_done(s["name"], row)]
440
+ if not todo:
441
+ continue
442
+ need_ref = any(not s["is_reference"] for s in todo)
443
+ ref_frames = None
444
+ ref_timing = None
445
+ ref_from_mp4 = False
446
+ if need_ref and os.path.exists(video_path(ref_name, row)) and not args.regenerate_ffff:
447
+ # Reference from the cfa_ffff run's MP4 (H.264, 40+ dB above the strategy
448
+ # PSNRs); FFFF is not regenerated, so there is no same-process FFFF timing
449
+ # and the speedup is taken against cfa_ffff's own records (aggregate.py).
450
+ vid, _, _ = read_video(video_path(ref_name, row), pts_unit="sec", output_format="TCHW")
451
+ ref_frames = vid.to(device, torch.float16) / 255.0
452
+ ref_from_mp4 = True
453
+ need_ref = False
454
+ for strategy in strategies:
455
+ if strategy["is_reference"]:
456
+ if strategy not in todo and not need_ref:
457
+ continue
458
+ elif strategy not in todo:
459
+ continue
460
+ video, timing, counts, wall = run_one(strategy, row)
461
+ frames = video[0]
462
+ if strategy["is_reference"]:
463
+ ref_frames = frames.detach().to(torch.float16)
464
+ ref_timing = timing
465
+ pix = PixelMetrics.identity(frames.shape[0])
466
+ else:
467
+ pix = metrics.compute(frames, ref_frames)
468
+ if strategy in todo:
469
+ vp = video_path(strategy["name"], row)
470
+ arr = (255.0 * rearrange(frames, "t c h w -> t h w c")).clamp(0, 255)
471
+ write_video(vp, arr.to(torch.uint8).cpu(), fps=args.fps)
472
+ compute_equiv = counts["full_forwards"] + PREDICTOR_COMPUTE_EQUIV * counts["predictor_forwards"]
473
+ model = strategy["model"]
474
+ naive = strategy.get("naive")
475
+ schedule = "F" * len(NAIVE[naive]) if naive else strategy["pattern"]
476
+ write_json(record_path(strategy["name"], row), {
477
+ "status": "complete",
478
+ "protocol": "Self-Forcing Extended-251 Full Evaluation",
479
+ "strategy": strategy["name"],
480
+ "base_model": "causal_forcing_a",
481
+ "method": "none" if model is None else f"predictor_{strategy['base_model']}",
482
+ "param": None if model is None else "pattern",
483
+ "param_value": None if model is None else strategy["pattern"],
484
+ "target_speedup": (n_chunks * NUM_STEPS) / compute_equiv,
485
+ "schedule": schedule,
486
+ "num_inference_steps": len(NAIVE[naive]) if naive else NUM_STEPS,
487
+ "first_chunk_schedule": "FFFF" if (naive or model) else None,
488
+ "predictor_config": None if model is None else predictor_configs[model],
489
+ "global_index": row["global_index"],
490
+ "prompt_suite": row["prompt_suite"],
491
+ "suite_index": row["suite_index"],
492
+ "prompt": row["extended_prompt"],
493
+ "original_prompt": row["original_prompt"],
494
+ "seed": args.seed,
495
+ "video": vp,
496
+ "num_frames": int(frames.shape[0]),
497
+ "height": int(frames.shape[2]),
498
+ "width": int(frames.shape[3]),
499
+ "fps": args.fps,
500
+ "policy_latency_ms": timing["denoise_dit_ms"],
501
+ "excluded_context_kv_latency_ms": timing["context_kv_dit_ms"],
502
+ # Same-process FFFF timing of this prompt (protocol "matched FFFF").
503
+ "matched_ffff_policy_latency_ms": ref_timing["denoise_dit_ms"] if ref_timing else None,
504
+ "matched_ffff_context_kv_latency_ms": ref_timing["context_kv_dit_ms"] if ref_timing else None,
505
+ "checkpoint_step": strategy.get("checkpoint_step"),
506
+ "num_chunks": n_chunks,
507
+ "wall_generation_s": wall,
508
+ "reference_strategy": ref_name,
509
+ "pixel_metrics_vs_ffff": pix,
510
+ "reference_source": "ffff_mp4" if ref_from_mp4 else "ffff_frames",
511
+ "cache_diagnostics": {
512
+ "denoise_forwards": timing["num_denoise_forwards"],
513
+ "full_forwards": counts["full_forwards"],
514
+ "predictor_forwards": counts["predictor_forwards"],
515
+ "predictor_compute_equivalent": PREDICTOR_COMPUTE_EQUIV,
516
+ "compute_equivalent_forwards": compute_equiv,
517
+ "middle_steps": n_chunks * 2,
518
+ "middle_compute_equivalent": compute_equiv - n_chunks * 2.0,
519
+ },
520
+ })
521
+ del frames, video
522
+ ref_frames = None
523
+ done = n + 1
524
+ rate = (time.time() - t_start) / done
525
+ print(f"[cfa shard {args.shard}] {done}/{len(shard_rows)} prompts "
526
+ f"({row['prompt_suite']}/{row['suite_index']:03d}) {rate:.1f}s/prompt "
527
+ f"eta {(len(shard_rows) - done) * rate / 60:.0f} min", flush=True)
528
+ print(f"shard {args.shard} done in {(time.time() - t_start) / 60:.1f} min", flush=True)
529
+
530
+
531
+ if __name__ == "__main__":
532
+ main()
eval/pixel_metrics.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PSNR / SSIM / LPIPS between a strategy's frames and its matched FFFF frames.
2
+
3
+ Computed on the decoded RGB tensors in [0, 1], before MP4 encoding, as the
4
+ protocol requires (section 8.1) -- video compression noise would otherwise leak
5
+ into the numbers.
6
+
7
+ SSIM is implemented here rather than taken from skimage: skimage needs ~30 ms per
8
+ 480x832 frame on CPU, and this evaluation compares ~500k frames.
9
+ """
10
+
11
+ import torch
12
+ import torch.nn.functional as F
13
+
14
+
15
+ def _gaussian_window(size=11, sigma=1.5, device=None, dtype=torch.float32):
16
+ coords = torch.arange(size, device=device, dtype=dtype) - (size - 1) / 2.0
17
+ g = torch.exp(-(coords ** 2) / (2 * sigma ** 2))
18
+ g = g / g.sum()
19
+ return g[:, None] @ g[None, :]
20
+
21
+
22
+ class PixelMetrics:
23
+ """Frame-wise metrics against a reference clip. Call once per (strategy, prompt)."""
24
+
25
+ def __init__(self, device, lpips_net="alex", ssim_window=11, ssim_sigma=1.5):
26
+ self.device = device
27
+ self.window = _gaussian_window(ssim_window, ssim_sigma, device=device)[None, None]
28
+ self.window_size = ssim_window
29
+ import lpips
30
+ self.lpips = lpips.LPIPS(net=lpips_net).to(device).eval()
31
+ for p in self.lpips.parameters():
32
+ p.requires_grad_(False)
33
+
34
+ def ssim(self, x, y):
35
+ """x, y: [T, C, H, W] in [0, 1]. Returns the mean over frames and channels."""
36
+ t, c, h, w = x.shape
37
+ win = self.window.expand(c, 1, self.window_size, self.window_size)
38
+ pad = self.window_size // 2
39
+ mu_x = F.conv2d(x, win, padding=pad, groups=c)
40
+ mu_y = F.conv2d(y, win, padding=pad, groups=c)
41
+ mu_x2, mu_y2, mu_xy = mu_x * mu_x, mu_y * mu_y, mu_x * mu_y
42
+ sig_x = F.conv2d(x * x, win, padding=pad, groups=c) - mu_x2
43
+ sig_y = F.conv2d(y * y, win, padding=pad, groups=c) - mu_y2
44
+ sig_xy = F.conv2d(x * y, win, padding=pad, groups=c) - mu_xy
45
+ c1, c2 = 0.01 ** 2, 0.03 ** 2
46
+ s = ((2 * mu_xy + c1) * (2 * sig_xy + c2)) / ((mu_x2 + mu_y2 + c1) * (sig_x + sig_y + c2))
47
+ # Drop the padded border, as skimage does with its default crop.
48
+ s = s[..., pad:-pad, pad:-pad]
49
+ return s.mean(dim=(1, 2, 3)) # per frame
50
+
51
+ @torch.no_grad()
52
+ def compute(self, frames, ref, chunk=16):
53
+ """frames, ref: [T, C, H, W] float in [0, 1] on any device.
54
+
55
+ Returns per-prompt mean MSE (for the protocol's ratio-of-means PSNR),
56
+ plus mean SSIM and mean LPIPS over frames.
57
+ """
58
+ frames = frames.to(self.device, torch.float32)
59
+ ref = ref.to(self.device, torch.float32)
60
+ assert frames.shape == ref.shape, (frames.shape, ref.shape)
61
+
62
+ mse_terms, ssim_terms, lpips_terms = [], [], []
63
+ for i in range(0, frames.shape[0], chunk):
64
+ a, b = frames[i:i + chunk], ref[i:i + chunk]
65
+ mse_terms.append(((a - b) ** 2).mean(dim=(1, 2, 3)))
66
+ ssim_terms.append(self.ssim(a, b))
67
+ lpips_terms.append(self.lpips(a * 2 - 1, b * 2 - 1).flatten())
68
+
69
+ mse = torch.cat(mse_terms)
70
+ ssim = torch.cat(ssim_terms)
71
+ lp = torch.cat(lpips_terms)
72
+ mean_mse = mse.mean().item()
73
+ return {
74
+ "mean_mse": mean_mse,
75
+ "psnr": -10.0 * torch.log10(torch.tensor(max(mean_mse, 1e-12))).item(),
76
+ "ssim": ssim.mean().item(),
77
+ "lpips": lp.mean().item(),
78
+ "num_frames": int(frames.shape[0]),
79
+ }
80
+
81
+ @staticmethod
82
+ def identity(num_frames):
83
+ """Fixed self-comparison values the protocol pins for FFFF vs itself."""
84
+ return {"mean_mse": 1e-12, "psnr": 120.0, "ssim": 1.0, "lpips": 0.0,
85
+ "num_frames": int(num_frames)}
eval/retime_eval.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Paired re-timing of evaluated strategies against FFFF, prompt by prompt.
3
+
4
+ ``generate_eval.py`` records each strategy's denoise latency, but when it is
5
+ re-run for *additional* strategies the FFFF reference is regenerated only for
6
+ the pixel metrics and its latency is not recorded, so the new records would be
7
+ compared against FFFF timings from a different run under different contention.
8
+ The protocol's speedup is "matched FFFF" (section 9.4): same prompt, same seed,
9
+ same conditions. This pass restores that: for every prompt it runs FFFF and
10
+ then each requested strategy back to back (denoise only, no decode, no metrics)
11
+ and writes the strategy's latency together with the FFFF latency measured
12
+ seconds earlier into the strategy's per-prompt record.
13
+
14
+ CUDA_VISIBLE_DEVICES=0 python eval/retime_eval.py --base self_forcing \
15
+ --strategies sf_taylorseer_FFxx_x1.3,sf_teacache_Fxxx_x3 \
16
+ --shard 0 --num-shards 4 --out-root eval_out
17
+
18
+ Records already carrying ``latency_source`` are skipped, so the pass resumes.
19
+ """
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import sys
25
+ import time
26
+
27
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
28
+ sys.path.insert(0, ROOT)
29
+
30
+ from eval.strategies import load_strategies # noqa: E402
31
+ from harness import BASES, enter_base, load_pipeline # noqa: E402
32
+
33
+ MAPPING = os.path.join(ROOT, "assets/vbench8_extended_subset_mapping.json")
34
+ SOURCE = "retime_eval: paired with FFFF in the same process, denoise only"
35
+
36
+
37
+ def parse_args():
38
+ p = argparse.ArgumentParser()
39
+ p.add_argument("--base", choices=sorted(BASES), required=True)
40
+ p.add_argument("--strategies", required=True,
41
+ help="Comma-separated strategy names, or 'all' for every "
42
+ "non-reference strategy of the base")
43
+ p.add_argument("--shard", type=int, default=0)
44
+ p.add_argument("--num-shards", type=int, default=1)
45
+ p.add_argument("--out-root", default="eval_out")
46
+ p.add_argument("--seed", type=int, default=0)
47
+ p.add_argument("--num-latent-frames", type=int, default=21)
48
+ p.add_argument("--limit", type=int, default=None)
49
+ p.add_argument("--force", action="store_true", help="Re-time records already re-timed")
50
+ p.add_argument("--redo-ratio-bounds", default=None,
51
+ help="lo,hi: also re-time records whose measured/compute speedup "
52
+ "ratio falls outside this band -- a spike that hit the strategy "
53
+ "half of the pair rather than the FFFF half")
54
+ p.add_argument("--redo-above-ms", type=float, default=None,
55
+ help="Also re-time records whose matched FFFF latency exceeds this: "
56
+ "a neighbour's job hit that pair, and the pair is only fair "
57
+ "when both halves ran under the same conditions")
58
+ return p.parse_args()
59
+
60
+
61
+ def main():
62
+ args = parse_args()
63
+ out_root = (args.out_root if os.path.isabs(args.out_root)
64
+ else os.path.join(ROOT, args.out_root))
65
+
66
+ with open(MAPPING) as f:
67
+ rows = json.load(f)["rows"]
68
+ if args.limit:
69
+ rows = rows[:args.limit]
70
+ shard_rows = rows[args.shard::args.num_shards]
71
+
72
+ all_strategies = load_strategies(args.base)
73
+ reference = all_strategies[0]
74
+ assert reference["is_reference"]
75
+ if args.strategies == "all":
76
+ wanted = [s for s in all_strategies if not s["is_reference"]]
77
+ else:
78
+ names = set(args.strategies.split(","))
79
+ wanted = [s for s in all_strategies if s["name"] in names]
80
+ missing = names - {s["name"] for s in wanted}
81
+ if missing:
82
+ sys.exit(f"unknown strategies for {args.base}: {sorted(missing)}")
83
+
84
+ print(f"base={args.base} shard {args.shard}/{args.num_shards}: "
85
+ f"{len(shard_rows)} prompts x ({reference['name']} + {len(wanted)} strategies)",
86
+ flush=True)
87
+
88
+ base = enter_base(args.base)
89
+
90
+ import torch
91
+ from utils.misc import set_seed
92
+
93
+ from cachelib import (CacheController, build_method, cached_inference, install,
94
+ parse_schedule)
95
+
96
+ torch.set_grad_enabled(False)
97
+ device = torch.device("cuda")
98
+ pipeline = load_pipeline(base)
99
+ num_steps = len(pipeline.denoising_step_list)
100
+
101
+ def record_path(strategy, row):
102
+ return os.path.join(out_root, "per_prompt", strategy,
103
+ f"{row['prompt_suite']}_{row['suite_index']:03d}.json")
104
+
105
+ def time_one(strategy, row):
106
+ kw = dict(indicator=strategy["indicator"], coefficients=strategy["coefficients"])
107
+ if strategy["method"] != "none":
108
+ kw[strategy["param"]] = strategy["value"]
109
+ first = strategy.get("first_chunk_schedule")
110
+ ctrl = CacheController(
111
+ build_method(strategy["method"], **kw), num_steps=num_steps,
112
+ forced_steps=parse_schedule(strategy.get("schedule", "FxxF"), num_steps),
113
+ first_chunk_forced_steps=(parse_schedule(first, num_steps) if first else None))
114
+ install(pipeline.generator.model, ctrl)
115
+ set_seed(args.seed)
116
+ noise = torch.randn([1, args.num_latent_frames, 16, 60, 104],
117
+ device=device, dtype=torch.bfloat16)
118
+ _, _, timing = cached_inference(pipeline, ctrl, noise, [row["extended_prompt"]],
119
+ decode=False)
120
+ return timing, ctrl.summary()
121
+
122
+ def write_json(path, payload):
123
+ tmp = path + ".tmp"
124
+ with open(tmp, "w") as f:
125
+ json.dump(payload, f, indent=2)
126
+ os.replace(tmp, path)
127
+
128
+ def needs(strategy, row):
129
+ p = record_path(strategy["name"], row)
130
+ if not os.path.exists(p):
131
+ return False # nothing to attach the timing to; generation must run first
132
+ with open(p) as f:
133
+ rec = json.load(f)
134
+ if rec.get("status") != "complete":
135
+ return False
136
+ if args.force or "latency_source" not in rec:
137
+ return True
138
+ if (args.redo_above_ms is not None
139
+ and rec.get("matched_ffff_policy_latency_ms", 0) > args.redo_above_ms):
140
+ return True
141
+ if args.redo_ratio_bounds:
142
+ lo, hi = (float(v) for v in args.redo_ratio_bounds.split(","))
143
+ d = rec["cache_diagnostics"]
144
+ compute_x = d["denoise_forwards"] / max(d["compute_equivalent_forwards"], 1e-6)
145
+ measured_x = rec["matched_ffff_policy_latency_ms"] / rec["policy_latency_ms"]
146
+ if not (lo <= measured_x / compute_x <= hi):
147
+ return True
148
+ return False
149
+
150
+ if shard_rows:
151
+ print("warm-up pass (not recorded)", flush=True)
152
+ for s in [reference] + wanted:
153
+ time_one(s, shard_rows[0])
154
+ torch.cuda.empty_cache()
155
+
156
+ t0 = time.time()
157
+ for n, row in enumerate(shard_rows):
158
+ todo = [s for s in wanted if needs(s, row)]
159
+ if not todo:
160
+ continue
161
+ ref_timing, _ = time_one(reference, row)
162
+ for s in todo:
163
+ timing, summary = time_one(s, row)
164
+ p = record_path(s["name"], row)
165
+ with open(p) as f:
166
+ rec = json.load(f)
167
+ rec["policy_latency_ms_generation_run"] = rec.get("policy_latency_ms")
168
+ rec["policy_latency_ms"] = timing["denoise_dit_ms"]
169
+ rec["excluded_context_kv_latency_ms"] = timing["context_kv_dit_ms"]
170
+ rec["matched_ffff_policy_latency_ms"] = ref_timing["denoise_dit_ms"]
171
+ rec["latency_source"] = SOURCE
172
+ # The compute fraction is deterministic; recording it again guards
173
+ # against a mismatch between the two runs.
174
+ rec["cache_diagnostics"]["compute_equivalent_forwards_retime"] = summary.get(
175
+ "compute_equivalent_forwards")
176
+ write_json(p, rec)
177
+ done = n + 1
178
+ rate = (time.time() - t0) / done
179
+ print(f"[{args.base} retime shard {args.shard}] {done}/{len(shard_rows)} "
180
+ f"({row['prompt_suite']}/{row['suite_index']:03d}) ffff={ref_timing['denoise_dit_ms']:.0f}ms "
181
+ f"{rate:.1f}s/prompt eta {(len(shard_rows) - done) * rate / 60:.0f} min",
182
+ flush=True)
183
+
184
+ print(f"retime shard {args.shard} done in {(time.time() - t0) / 60:.1f} min", flush=True)
185
+
186
+
187
+ if __name__ == "__main__":
188
+ main()
eval/retime_until_clean.sh ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Self-healing driver for the paired re-timing on a shared node.
3
+ #
4
+ # Neighbours' jobs both inflate timings and, twice now, took our processes down
5
+ # with them. So: loop. Each pass counts the records that still need a fair
6
+ # pairing (never timed, FFFF half contended, or measured/compute ratio outside a
7
+ # sane band), launches one shard per *currently free* GPU among $GPUS, waits, and
8
+ # repeats until nothing is left or MAX_PASSES is hit. Then aggregate.
9
+ set -u
10
+ C=/local/zoubin/cz/projects/comparison
11
+ PY=/local/zoubin/cz/envs/self_forcing/bin/python
12
+ LOG="$1"
13
+ GPUS=(${GPUS:-4 5 6 7})
14
+ THRESH_MS=${THRESH_MS:-3500}
15
+ RATIO=${RATIO:-0.5,1.1}
16
+ MAX_PASSES=${MAX_PASSES:-12}
17
+ MIN_FREE=${MIN_FREE:-2} # do not start a pass on fewer free GPUs than this
18
+ WAIT=${WAIT:-120} # seconds between scans while waiting for GPUs
19
+ NEW_SF=sf_teacache_Fxxx_x3,sf_flowcache_Fxxx_x3,sf_taylorseer_FFxx_x1.3,sf_taylorseer_FFxx_x1.6,sf_taylorseer_FFxx_x2,sf_motioncache_Fxxx_x3
20
+ NEW_CF=cf_teacache_Fxxx_x3,cf_flowcache_Fxxx_x3,cf_taylorseer_FFxx_x1.3,cf_taylorseer_FFxx_x1.6,cf_taylorseer_FFxx_x2,cf_motioncache_Fxxx_x3
21
+
22
+ export SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt
23
+ export HF_HOME=/local/zoubin/cz/.cache/huggingface HF_HUB_CACHE=/local/zoubin/cz/.cache/huggingface/hub
24
+ export TRANSFORMERS_CACHE=/local/zoubin/cz/.cache/huggingface/hub TORCH_HOME=/local/zoubin/cz/.cache/torch
25
+ export XDG_CACHE_HOME=/local/zoubin/cz/.cache
26
+ cd "$C"
27
+
28
+ pending() { # how many records of $1's strategies still need work
29
+ $PY - "$1" "$THRESH_MS" "$RATIO" <<'PYEOF'
30
+ import glob, json, sys
31
+ strats, thresh, ratio = sys.argv[1].split(","), float(sys.argv[2]), [float(v) for v in sys.argv[3].split(",")]
32
+ n = 0
33
+ for s in strats:
34
+ for p in glob.glob(f"eval_out/per_prompt/{s}/*.json"):
35
+ r = json.load(open(p))
36
+ if "latency_source" not in r or r["matched_ffff_policy_latency_ms"] > thresh:
37
+ n += 1; continue
38
+ d = r["cache_diagnostics"]
39
+ cx = d["denoise_forwards"] / max(d["compute_equivalent_forwards"], 1e-6)
40
+ mx = r["matched_ffff_policy_latency_ms"] / r["policy_latency_ms"]
41
+ if not (ratio[0] <= mx / cx <= ratio[1]):
42
+ n += 1
43
+ print(n)
44
+ PYEOF
45
+ }
46
+
47
+ free_gpus() { # members of GPUS that are actually quiet: little memory AND no compute
48
+ # Memory alone is not enough -- a VBench scorer holds 2 GB and saturates the SMs,
49
+ # and that alone inflated a whole pass by 25-80%. Sample utilisation 3x over 3 s.
50
+ for g in "${GPUS[@]}"; do
51
+ used=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits -i "$g" 2>/dev/null | tr -d ' ')
52
+ [ -z "$used" ] || [ "$used" -ge 3000 ] && continue
53
+ peak=0
54
+ for _ in 1 2 3; do
55
+ u=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits -i "$g" 2>/dev/null | tr -d ' ')
56
+ [ -n "$u" ] && [ "$u" -gt "$peak" ] && peak=$u
57
+ sleep 1
58
+ done
59
+ [ "$peak" -lt 10 ] && echo "$g"
60
+ done
61
+ }
62
+
63
+ # Only GPUs nobody else is using are ever touched; waiting never counts as a pass.
64
+ pass=0
65
+ while [ "$pass" -lt "$MAX_PASSES" ]; do
66
+ n_sf=$(pending "$NEW_SF"); n_cf=$(pending "$NEW_CF")
67
+ echo "=== scan ($(date +%H:%M:%S)): pending sf=$n_sf cf=$n_cf, passes so far $pass ==="
68
+ [ "$n_sf" -eq 0 ] && [ "$n_cf" -eq 0 ] && break
69
+ mapfile -t FREE < <(free_gpus)
70
+ if [ "${#FREE[@]}" -lt "$MIN_FREE" ]; then
71
+ echo "only ${#FREE[@]} free GPU(s) among ${GPUS[*]} (need $MIN_FREE); waiting ${WAIT}s"
72
+ sleep "$WAIT"; continue
73
+ fi
74
+ pass=$((pass+1))
75
+ echo "using GPUs: ${FREE[*]}"
76
+ for BASE in self_forcing causal_forcing; do
77
+ if [ "$BASE" = self_forcing ]; then S=$NEW_SF; n=$n_sf; else S=$NEW_CF; n=$n_cf; fi
78
+ [ "$n" -eq 0 ] && continue
79
+ pids=()
80
+ for i in "${!FREE[@]}"; do
81
+ CUDA_VISIBLE_DEVICES=${FREE[$i]} $PY eval/retime_eval.py --base "$BASE" --strategies "$S" \
82
+ --shard "$i" --num-shards "${#FREE[@]}" --out-root eval_out \
83
+ --redo-above-ms "$THRESH_MS" --redo-ratio-bounds "$RATIO" \
84
+ > "$LOG/retime_${BASE}_pass${pass}_$i.log" 2>&1 &
85
+ pids+=($!)
86
+ done
87
+ for p in "${pids[@]}"; do wait "$p"; done
88
+ echo " $BASE pass $pass finished ($(date +%H:%M:%S))"
89
+ done
90
+ done
91
+ echo "=== final pending: sf=$(pending "$NEW_SF") cf=$(pending "$NEW_CF") ==="
92
+ echo "=== aggregating ($(date +%H:%M:%S)) ==="
93
+ # --only: the predictor evaluation shares eval_out/ and may still be incomplete.
94
+ $PY eval/aggregate.py --out-root "$C/eval_out" --only '^(sf|cf)_' 2>&1 | tee "$LOG/aggregate.log"
95
+ python3 experiment_md.py 2>&1 | tee "$LOG/experiment_md.log"
96
+ echo "ALL RETIME DONE $(date +%H:%M:%S)"
eval/run_all_phases.sh ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Waits for the running generation phase, then scores and aggregates.
3
+ set -u
4
+ C=/local/zoubin/cz/projects/comparison
5
+ LOG="$1"
6
+
7
+ echo "waiting for generation to finish ..."
8
+ while ! grep -q "ALL GENERATION DONE" "$LOG/generation_driver.log" 2>/dev/null; do
9
+ if ! pgrep -f "[g]enerate_eval.py" > /dev/null 2>&1 \
10
+ && ! pgrep -f "[r]un_generation.sh" > /dev/null 2>&1; then
11
+ echo "generation processes gone without the done marker; stopping."
12
+ exit 1
13
+ fi
14
+ sleep 60
15
+ done
16
+ echo "generation complete $(date +%H:%M:%S)"
17
+
18
+ "$C/eval/run_vbench_all.sh" "$LOG" 2>&1 | tee "$LOG/vbench_driver.log"
19
+
20
+ echo "=== aggregating $(date +%H:%M:%S) ==="
21
+ /local/zoubin/cz/envs/self_forcing/bin/python "$C/eval/aggregate.py" \
22
+ --out-root "$C/eval_out" 2>&1 | tee "$LOG/aggregate.log"
23
+ echo "ALL PHASES DONE $(date +%H:%M:%S)"
eval/run_generation.sh ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Phase 1: generate the Extended-251 videos for every strategy, one base at a time,
3
+ # sharded by prompt across the GPUs in $GPUS (default 0-3). Generation runs in the base self_forcing env --
4
+ # the same interpreter and package set the speed study used -- so the eval videos
5
+ # are produced by exactly the model stack that was benchmarked.
6
+ set -u
7
+ C=/local/zoubin/cz/projects/comparison
8
+ PY=/local/zoubin/cz/envs/self_forcing/bin/python
9
+ LOG="$1"; shift
10
+ NSHARDS=${NSHARDS:-4}
11
+ GPUS=(${GPUS:-0 1 2 3})
12
+
13
+ export SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt
14
+ export TORCH_HOME=/local/zoubin/cz/.cache/torch
15
+ export HF_HOME=/local/zoubin/cz/.cache/huggingface
16
+ export HF_HUB_CACHE=/local/zoubin/cz/.cache/huggingface/hub
17
+ export TRANSFORMERS_CACHE=/local/zoubin/cz/.cache/huggingface/hub
18
+ export XDG_CACHE_HOME=/local/zoubin/cz/.cache
19
+
20
+ cd "$C"
21
+ for BASE in ${BASES:-self_forcing causal_forcing}; do
22
+ echo "=== generation: $BASE ($(date +%H:%M:%S)) ==="
23
+ pids=()
24
+ for i in $(seq 0 $((NSHARDS-1))); do
25
+ CUDA_VISIBLE_DEVICES=${GPUS[$i]} nohup $PY eval/generate_eval.py \
26
+ --base "$BASE" --shard $i --num-shards $NSHARDS --out-root eval_out \
27
+ > "$LOG/gen_${BASE}_$i.log" 2>&1 &
28
+ pids+=($!)
29
+ done
30
+ for p in "${pids[@]}"; do wait "$p"; done
31
+ echo "=== generation done: $BASE ($(date +%H:%M:%S)) ==="
32
+ done
33
+ echo "ALL GENERATION DONE"
eval/run_predictor_eval.sh ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Extended-251 evaluation of the Causal-Forcing-a Predictors (cfa_* strategies):
3
+ # phase 1 generation sharded across $GPUS, phase 2 VBench on the not-yet-scored
4
+ # strategies (run_vbench_all.sh skips scored ones), phase 3 aggregate.
5
+ # GPUS="0 1 2 3" eval/run_predictor_eval.sh logs_cfa
6
+ set -u
7
+ C=/local/zoubin/cz/projects/comparison
8
+ PY=/local/zoubin/cz/envs/self_forcing/bin/python
9
+ LOG="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"; shift
10
+ GPUS=(${GPUS:-0 1 2 3})
11
+ NSHARDS=${#GPUS[@]}
12
+ mkdir -p "$LOG"
13
+
14
+ export SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt
15
+ export TORCH_HOME=/local/zoubin/cz/.cache/torch
16
+ export HF_HOME=/local/zoubin/cz/.cache/huggingface
17
+ export HF_HUB_CACHE=/local/zoubin/cz/.cache/huggingface/hub
18
+ export TRANSFORMERS_CACHE=/local/zoubin/cz/.cache/huggingface/hub
19
+ export XDG_CACHE_HOME=/local/zoubin/cz/.cache
20
+
21
+ cd "$C"
22
+ echo "=== cfa generation ($(date +%H:%M:%S)) gpus=${GPUS[*]} ==="
23
+ pids=()
24
+ for i in $(seq 0 $((NSHARDS-1))); do
25
+ CUDA_VISIBLE_DEVICES=${GPUS[$i]} $PY eval/generate_predictor_eval.py \
26
+ --shard $i --num-shards $NSHARDS --out-root eval_out "$@" \
27
+ > "$LOG/gen_cfa_$i.log" 2>&1 &
28
+ pids+=($!)
29
+ done
30
+ status=0
31
+ for p in "${pids[@]}"; do wait "$p" || status=1; done
32
+ echo "=== cfa generation done ($(date +%H:%M:%S)) status=$status ==="
33
+ [[ $status == 0 ]] || { echo "generation failed; not scoring"; exit 1; }
34
+
35
+ GPUS="${GPUS[*]}" NGPU=$NSHARDS bash "$C/eval/run_vbench_all.sh" "$LOG" 2>&1 | tee "$LOG/vbench_driver.log"
36
+
37
+ echo "=== aggregating ($(date +%H:%M:%S)) ==="
38
+ $PY "$C/eval/aggregate.py" --out-root "$C/eval_out" 2>&1 | tee "$LOG/aggregate.log"
39
+ echo "ALL CFA PHASES DONE $(date +%H:%M:%S)"
eval/run_predictor_eval_s2.sh ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Extended-251 evaluation of the Stage-2 EMA Predictor: generation sharded over
3
+ # $GPUS, VBench only for the strategies generated here, then aggregate.
4
+ # GPUS="0 1 2" bash eval/run_predictor_eval_s2.sh logs_cfa_s2
5
+ set -u
6
+ C=/local/zoubin/cz/projects/comparison
7
+ V=/local/zoubin/cz/projects/VBench
8
+ PY=/local/zoubin/cz/envs/self_forcing/bin/python
9
+ VPY=/local/zoubin/cz/envs/vbench_eval/bin/python
10
+ LOG="$(mkdir -p "$1" && cd "$1" && pwd)"
11
+ MODELS="${MODELS:-atc_chunk_s2}"
12
+ PATTERNS="${PATTERNS:-FPFF,FPPF,FPPP}"
13
+ GPUS=(${GPUS:-0 1 2})
14
+ NSHARDS=${#GPUS[@]}
15
+
16
+ export SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt
17
+ export TORCH_HOME=/local/zoubin/cz/.cache/torch
18
+ export HF_HOME=/local/zoubin/cz/.cache/huggingface
19
+ export HF_HUB_CACHE=/local/zoubin/cz/.cache/huggingface/hub
20
+ export TRANSFORMERS_CACHE=/local/zoubin/cz/.cache/huggingface/hub
21
+ export XDG_CACHE_HOME=/local/zoubin/cz/.cache
22
+ export VBENCH_CACHE_DIR=/local/zoubin/cz/.cache/vbench
23
+
24
+ cd "$C"
25
+ echo "=== generation ($(date +%H:%M:%S)) gpus=${GPUS[*]} models=$MODELS patterns=$PATTERNS ==="
26
+ pids=()
27
+ for i in $(seq 0 $((NSHARDS-1))); do
28
+ CUDA_VISIBLE_DEVICES=${GPUS[$i]} $PY eval/generate_predictor_eval.py \
29
+ --shard $i --num-shards $NSHARDS --out-root eval_out --models "$MODELS" --patterns "$PATTERNS" \
30
+ > "$LOG/gen_$i.log" 2>&1 &
31
+ pids+=($!)
32
+ done
33
+ status=0; for p in "${pids[@]}"; do wait "$p" || status=1; done
34
+ echo "=== generation done ($(date +%H:%M:%S)) status=$status ==="
35
+ [[ $status == 0 ]] || { echo "generation failed; not scoring"; exit 1; }
36
+
37
+ strats=()
38
+ for m in ${MODELS//,/ }; do for pat in ${PATTERNS//,/ }; do strats+=("cfa_${m}_$(echo "$pat" | tr 'A-Z' 'a-z')"); done; done
39
+ echo "=== vbench ($(date +%H:%M:%S)) strategies=${strats[*]} ==="
40
+ export PYTHONPATH="$V:$C"
41
+ cd "$V"
42
+ pids=()
43
+ for g in $(seq 0 $((NSHARDS-1))); do
44
+ (
45
+ for i in $(seq $g $NSHARDS $(( ${#strats[@]} - 1 ))); do
46
+ s="${strats[$i]}"
47
+ [[ -f "$C/eval_out/vbench/scores/$s.json" ]] && { echo "skip $s"; continue; }
48
+ echo "[gpu ${GPUS[$g]}] $s $(date +%H:%M:%S)"
49
+ CUDA_VISIBLE_DEVICES=${GPUS[$g]} $VPY "$C/eval/run_vbench.py" --strategy "$s" --out-root "$C/eval_out" \
50
+ > "$LOG/vbench_$s.log" 2>&1 || echo "[gpu ${GPUS[$g]}] FAILED $s"
51
+ done
52
+ ) &
53
+ pids+=($!)
54
+ done
55
+ for p in "${pids[@]}"; do wait "$p"; done
56
+ echo "=== aggregating ($(date +%H:%M:%S)) ==="
57
+ cd "$C"
58
+ $PY eval/aggregate.py --out-root "$C/eval_out" --allow-incomplete > "$LOG/aggregate.log" 2>&1
59
+ echo "ALL S2 PHASES DONE $(date +%H:%M:%S)"
eval/run_predictor_retime_s2.sh ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Paired re-timing of the Stage-2 predictor strategies against FFFF (same process).
3
+ set -u
4
+ C=/local/zoubin/cz/projects/comparison
5
+ PY=/local/zoubin/cz/envs/self_forcing/bin/python
6
+ LOG="$(mkdir -p "$1" && cd "$1" && pwd)"
7
+ GPUS=(${GPUS:-0 1 2}); NSHARDS=${#GPUS[@]}
8
+ export SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt TORCH_HOME=/local/zoubin/cz/.cache/torch HF_HOME=/local/zoubin/cz/.cache/huggingface HF_HUB_CACHE=/local/zoubin/cz/.cache/huggingface/hub XDG_CACHE_HOME=/local/zoubin/cz/.cache
9
+ cd "$C"
10
+ echo "=== retime ($(date +%H:%M:%S)) gpus=${GPUS[*]} ==="
11
+ pids=()
12
+ for i in $(seq 0 $((NSHARDS-1))); do
13
+ CUDA_VISIBLE_DEVICES=${GPUS[$i]} $PY eval/generate_predictor_eval.py --shard $i --num-shards $NSHARDS --out-root eval_out \
14
+ --models "${MODELS:-atc_chunk_s2}" --patterns "${PATTERNS:-FPFF,FPPF,FPPP}" --retime-only > "$LOG/retime_$i.log" 2>&1 &
15
+ pids+=($!)
16
+ done
17
+ status=0; for p in "${pids[@]}"; do wait "$p" || status=1; done
18
+ echo "=== retime done ($(date +%H:%M:%S)) status=$status ==="
19
+ $PY eval/aggregate.py --out-root "$C/eval_out" --allow-incomplete > "$LOG/aggregate_retime.log" 2>&1
20
+ $PY experiment_md.py > "$LOG/experiment_md.log" 2>&1
21
+ echo "RETIME DONE $(date +%H:%M:%S)"
eval/run_predictor_vbench_phase.sh ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # VBench + aggregate only (generation already complete).
3
+ set -u
4
+ C=/local/zoubin/cz/projects/comparison
5
+ PY=/local/zoubin/cz/envs/self_forcing/bin/python
6
+ LOG="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
7
+ GPUS="${GPUS:-0 1 2 3}"
8
+ read -r -a gpu_list <<< "$GPUS"
9
+ cd "$C"
10
+ GPUS="$GPUS" NGPU=${#gpu_list[@]} bash "$C/eval/run_vbench_all.sh" "$LOG" 2>&1 | tee "$LOG/vbench_driver.log"
11
+ echo "=== aggregating ($(date +%H:%M:%S)) ==="
12
+ $PY "$C/eval/aggregate.py" --out-root "$C/eval_out" 2>&1 | tee "$LOG/aggregate.log"
13
+ echo "ALL CFA PHASES DONE $(date +%H:%M:%S)"
eval/run_retime.sh ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Phase 3: paired re-timing of strategies added after the first generation run
3
+ # (their FFFF reference was regenerated for the pixel metrics but not timed).
4
+ # Runs after VBench so the GPUs carry nothing else. Usage:
5
+ # eval/run_retime.sh <logdir> <comma-separated strategy names, per base via
6
+ # STRATS_SF / STRATS_CF env, or "all">
7
+ set -u
8
+ # DISABLED (Sep 6): the user asked for no more FFFF re-timing passes. Speedups
9
+ # are now the ratio of mean latencies against the FFFF strategy's own records;
10
+ # where those come from a different run the absolute ms are not comparable and
11
+ # the table says so. Kept for reference only.
12
+ echo "eval/run_retime.sh is disabled; re-timing is no longer part of the pipeline." >&2
13
+ exit 1
14
+ C=/local/zoubin/cz/projects/comparison
15
+ PY=/local/zoubin/cz/envs/self_forcing/bin/python
16
+ LOG="$1"; shift
17
+ NSHARDS=${NSHARDS:-4}
18
+ GPUS=(${GPUS:-0 1 2 3})
19
+ EXTRA=${EXTRA:-}
20
+
21
+ export SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt
22
+ export HF_HOME=/local/zoubin/cz/.cache/huggingface
23
+ export HF_HUB_CACHE=/local/zoubin/cz/.cache/huggingface/hub
24
+ export TRANSFORMERS_CACHE=/local/zoubin/cz/.cache/huggingface/hub
25
+ export TORCH_HOME=/local/zoubin/cz/.cache/torch
26
+ export XDG_CACHE_HOME=/local/zoubin/cz/.cache
27
+
28
+ cd "$C"
29
+ for BASE in self_forcing causal_forcing; do
30
+ if [ "$BASE" = self_forcing ]; then S="${STRATS_SF:-all}"; else S="${STRATS_CF:-all}"; fi
31
+ echo "=== retime: $BASE [$S] ($(date +%H:%M:%S)) ==="
32
+ pids=()
33
+ for i in $(seq 0 $((NSHARDS-1))); do
34
+ CUDA_VISIBLE_DEVICES=${GPUS[$i]} nohup $PY eval/retime_eval.py \
35
+ --base "$BASE" --strategies "$S" --shard $i --num-shards $NSHARDS \
36
+ --out-root eval_out $EXTRA > "$LOG/retime_${BASE}_$i.log" 2>&1 &
37
+ pids+=($!)
38
+ done
39
+ for p in "${pids[@]}"; do wait "$p"; done
40
+ echo "=== retime done: $BASE ($(date +%H:%M:%S)) ==="
41
+ done
42
+ echo "ALL RETIME DONE"
eval/run_vbench.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Score one strategy on the eight protocol VBench dimensions.
3
+
4
+ Standard VBench 0.1.5 metric code is used unmodified. What is built here is only
5
+ the evaluation metadata file, because the protocol needs two things the stock
6
+ `vbench_standard` mode cannot express:
7
+
8
+ * `prompt_en` must be the *extended* prompt actually fed to the generator
9
+ (section 6.1), not the short canonical one;
10
+ * `scene` must keep the canonical `auxiliary_info` keywords.
11
+
12
+ The file has exactly the shape `build_full_info_json` produces, and each
13
+ `compute_<dimension>` selects the rows that list it, so per-suite scoping
14
+ (72 / 93 / 86 videos) falls out of the mapping rather than being hard-coded.
15
+
16
+ python eval/run_vbench.py --strategy sf_ffff --out-root eval_out
17
+ """
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import sys
23
+ import time
24
+
25
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
26
+ sys.path.insert(0, ROOT)
27
+
28
+ MAPPING = os.path.join(ROOT, "assets/vbench8_extended_subset_mapping.json")
29
+
30
+ SUITE_DIMENSIONS = {
31
+ "subject_consistency": ["subject_consistency", "motion_smoothness", "dynamic_degree"],
32
+ "overall_consistency": ["overall_consistency", "aesthetic_quality", "imaging_quality"],
33
+ "scene": ["scene", "background_consistency"],
34
+ }
35
+ DIMENSIONS = [d for dims in SUITE_DIMENSIONS.values() for d in dims]
36
+
37
+
38
+ def parse_args():
39
+ p = argparse.ArgumentParser()
40
+ p.add_argument("--strategy", required=True)
41
+ p.add_argument("--out-root", default="eval_out")
42
+ p.add_argument("--dimensions", default=None,
43
+ help="Comma-separated subset (default: all eight)")
44
+ p.add_argument("--allow-partial", action="store_true",
45
+ help="Score even if fewer than 251 videos exist (smoke tests only)")
46
+ p.add_argument("--overwrite", action="store_true",
47
+ help="Recompute dimensions whose raw result file already exists")
48
+ p.add_argument("--threads", type=int, default=None,
49
+ help="torch/OpenMP CPU threads for this process (scoring is CPU-bound; "
50
+ "several scorers on one node oversubscribe the cores)")
51
+ return p.parse_args()
52
+
53
+
54
+ def main():
55
+ args = parse_args()
56
+ out_root = (args.out_root if os.path.isabs(args.out_root)
57
+ else os.path.join(ROOT, args.out_root))
58
+ dims = args.dimensions.split(",") if args.dimensions else list(DIMENSIONS)
59
+
60
+ with open(MAPPING) as f:
61
+ mapping = json.load(f)
62
+
63
+ video_root = os.path.join(out_root, "generated_videos", args.strategy)
64
+ rows, missing = [], []
65
+ for r in mapping["rows"]:
66
+ vp = os.path.join(video_root, r["prompt_suite"], f"{r['suite_index']:03d}.mp4")
67
+ if not os.path.exists(vp):
68
+ missing.append(vp)
69
+ continue
70
+ entry = {
71
+ # The extended prompt is what the video was generated from, so it is
72
+ # what overall_consistency and scene must be scored against.
73
+ "prompt_en": r["extended_prompt"],
74
+ "dimension": [d for d in SUITE_DIMENSIONS[r["prompt_suite"]]],
75
+ "video_list": [vp],
76
+ "global_index": r["global_index"],
77
+ "prompt_suite": r["prompt_suite"],
78
+ "suite_index": r["suite_index"],
79
+ }
80
+ if "auxiliary_info" in r:
81
+ entry["auxiliary_info"] = r["auxiliary_info"]
82
+ rows.append(entry)
83
+
84
+ if missing and not args.allow_partial:
85
+ print(f"ERROR: {len(missing)} of {len(mapping['rows'])} videos missing for "
86
+ f"{args.strategy}; refusing to score an incomplete strategy.")
87
+ for m in missing[:5]:
88
+ print(" ", m)
89
+ return 1
90
+ print(f"{args.strategy}: {len(rows)} videos, dimensions={dims}", flush=True)
91
+
92
+ scores_dir = os.path.join(out_root, "vbench", "raw_results", args.strategy)
93
+ os.makedirs(scores_dir, exist_ok=True)
94
+ info_path = os.path.join(scores_dir, "full_info.json")
95
+ # Atomic: several per-dimension processes may start on the same strategy.
96
+ with open(info_path + f".{os.getpid()}", "w") as f:
97
+ json.dump(rows, f, indent=2)
98
+ os.replace(info_path + f".{os.getpid()}", info_path)
99
+
100
+ import importlib
101
+
102
+ import torch
103
+ if args.threads:
104
+ torch.set_num_threads(args.threads)
105
+ from vbench.utils import init_submodules
106
+
107
+ device = torch.device("cuda")
108
+
109
+ def raw_path(d):
110
+ return os.path.join(scores_dir, f"{d}.json")
111
+
112
+ # A dimension already scored (its raw file exists) is reused, so dimensions can
113
+ # be spread over several processes and a killed run resumes where it stopped.
114
+ todo = [d for d in dims if args.overwrite or not os.path.exists(raw_path(d))]
115
+ submodules = init_submodules(todo, local=True, read_frame=False) if todo else {}
116
+
117
+ for d in todo:
118
+ t0 = time.time()
119
+ module = importlib.import_module(f"vbench.{d}")
120
+ fn = getattr(module, f"compute_{d}")
121
+ score, per_video = fn(info_path, device, submodules[d])
122
+ tmp = raw_path(d) + ".tmp"
123
+ with open(tmp, "w") as f:
124
+ json.dump({"dimension": d, "score": score, "video_results": per_video,
125
+ "seconds": time.time() - t0}, f, indent=2)
126
+ os.replace(tmp, raw_path(d))
127
+ print(f" {d:24s} {score:.6f} ({len(per_video)} videos, "
128
+ f"{time.time() - t0:.0f}s)", flush=True)
129
+ torch.cuda.empty_cache()
130
+
131
+ # The strategy's score file is written only once all eight dimensions exist.
132
+ missing_dims = [d for d in DIMENSIONS if not os.path.exists(raw_path(d))]
133
+ if missing_dims:
134
+ print(f"{args.strategy}: {len(DIMENSIONS) - len(missing_dims)}/{len(DIMENSIONS)} "
135
+ f"dimensions scored; missing {missing_dims}", flush=True)
136
+ return 0
137
+ results = {}
138
+ for d in DIMENSIONS:
139
+ with open(raw_path(d)) as f:
140
+ raw = json.load(f)
141
+ results[d] = {"score": raw["score"], "num_videos": len(raw["video_results"]),
142
+ "seconds": raw.get("seconds")}
143
+
144
+ out_path = os.path.join(out_root, "vbench", "scores", f"{args.strategy}.json")
145
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
146
+ payload = {
147
+ "strategy": args.strategy,
148
+ "vbench_long": False,
149
+ "num_videos": len(rows),
150
+ "raw": {d: results[d]["score"] for d in DIMENSIONS},
151
+ "detail": results,
152
+ "full_info": info_path,
153
+ }
154
+ tmp = out_path + ".tmp"
155
+ with open(tmp, "w") as f:
156
+ json.dump(payload, f, indent=2)
157
+ os.replace(tmp, out_path)
158
+ print(f"wrote {out_path}", flush=True)
159
+ return 0
160
+
161
+
162
+ if __name__ == "__main__":
163
+ sys.exit(main())
eval/run_vbench_all.sh ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Phase 2: score every strategy on the eight protocol dimensions.
3
+ # VBench runs in the vbench_eval venv (torchao 0.7.0 / transformers 4.46.3 /
4
+ # fairscale), because the base env's torchao 0.17.0 is unimportable under torch
5
+ # 2.5.1 and takes transformers' BertModel -- and therefore Tag2Text/scene -- with
6
+ # it. Generation deliberately stays in the base env.
7
+ set -u
8
+ C=/local/zoubin/cz/projects/comparison
9
+ V=/local/zoubin/cz/projects/VBench
10
+ PY=/local/zoubin/cz/envs/vbench_eval/bin/python
11
+ LOG="$1"; shift
12
+ NGPU=${NGPU:-4}
13
+ GPUS=(${GPUS:-0 1 2 3})
14
+
15
+ export PYTHONPATH="$V:$C"
16
+ export VBENCH_CACHE_DIR=/local/zoubin/cz/.cache/vbench
17
+ export HF_HOME=/local/zoubin/cz/.cache/huggingface
18
+ export HF_HUB_CACHE=/local/zoubin/cz/.cache/huggingface/hub
19
+ export TRANSFORMERS_CACHE=/local/zoubin/cz/.cache/huggingface/hub
20
+ export XDG_CACHE_HOME=/local/zoubin/cz/.cache
21
+ export SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt
22
+ export TORCH_HOME=/local/zoubin/cz/.cache/torch
23
+
24
+ mapfile -t STRATS < <(ls "$C/eval_out/per_prompt")
25
+ echo "scoring ${#STRATS[@]} strategies across $NGPU GPUs"
26
+
27
+ cd "$V"
28
+ pids=()
29
+ for g in $(seq 0 $((NGPU-1))); do
30
+ (
31
+ for i in $(seq $g $NGPU $(( ${#STRATS[@]} - 1 ))); do
32
+ s="${STRATS[$i]}"
33
+ if [ -f "$C/eval_out/vbench/scores/$s.json" ]; then
34
+ echo "skip $s (already scored)"; continue
35
+ fi
36
+ echo "[gpu $g] $s $(date +%H:%M:%S)"
37
+ CUDA_VISIBLE_DEVICES=${GPUS[$g]} $PY "$C/eval/run_vbench.py" \
38
+ --strategy "$s" --out-root "$C/eval_out" > "$LOG/vbench_$s.log" 2>&1 \
39
+ || echo "[gpu $g] FAILED $s"
40
+ done
41
+ ) &
42
+ pids+=($!)
43
+ done
44
+ for p in "${pids[@]}"; do wait "$p"; done
45
+ echo "ALL VBENCH DONE"
eval/setup_vbench_models.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Download the VBench weights the eight protocol dimensions need.
3
+
4
+ Everything lands in $VBENCH_CACHE_DIR so the run does not depend on whatever a
5
+ user's ~/.cache happens to hold. `local=True` is used wherever VBench supports
6
+ it, so CLIP and DINO come from the cache directory too rather than from the clip
7
+ package's own download path.
8
+ """
9
+ import os
10
+ import sys
11
+
12
+ DIMS = ["subject_consistency", "background_consistency", "motion_smoothness",
13
+ "dynamic_degree", "aesthetic_quality", "imaging_quality", "scene",
14
+ "overall_consistency"]
15
+
16
+ def main():
17
+ from vbench.utils import init_submodules, CACHE_DIR
18
+ print("CACHE_DIR =", CACHE_DIR, flush=True)
19
+ for d in DIMS:
20
+ print(f"--- {d} ---", flush=True)
21
+ try:
22
+ got = init_submodules([d], local=True, read_frame=False)
23
+ print(f" ok: {got.get(d)}", flush=True)
24
+ except Exception as e:
25
+ print(f" FAILED: {type(e).__name__}: {e}", flush=True)
26
+ print("done", flush=True)
27
+
28
+ if __name__ == "__main__":
29
+ sys.exit(main())
eval/strategies.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The strategy list for the Extended-251 evaluation.
2
+
3
+ FFFF (every denoising step full) is the reference for each base model; the cache
4
+ strategies are the operating points settled by finalize.py / retime.py, read back
5
+ from results/ so the evaluation cannot silently drift from the speed study.
6
+ """
7
+
8
+ import glob
9
+ import json
10
+ import os
11
+
12
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13
+
14
+ METHOD_ORDER = ["teacache", "flowcache", "taylorseer", "motioncache"]
15
+ # FlowCache is frozen: finished operating points stay in the tables, no new ones
16
+ # are added. ``final_*_v2.json`` files (the measured-speedup targeted points)
17
+ # therefore never contain it.
18
+ DROPPED_FOR_NEW = {"flowcache"}
19
+ BASES = ["self_forcing", "causal_forcing"]
20
+ SHORT = {"self_forcing": "sf", "causal_forcing": "cf"}
21
+
22
+
23
+ def load_strategies(base):
24
+ """[{name, base, method, param, value, target, coefficients, indicator}, ...]
25
+
26
+ The first entry is always FFFF.
27
+ """
28
+ coeff_path = os.path.join(ROOT, f"results/coeff_{base}.json")
29
+ coefficients = None
30
+ if os.path.exists(coeff_path):
31
+ with open(coeff_path) as f:
32
+ coefficients = json.load(f)["coefficients"]
33
+
34
+ out = [{
35
+ "name": f"{SHORT[base]}_ffff",
36
+ "base": base, "method": "none", "param": None, "value": None,
37
+ "target": 1.0, "coefficients": None, "indicator": "modulated_input",
38
+ "schedule": "FxxF", "is_reference": True, "sampler": "renoise",
39
+ }]
40
+
41
+ rows = []
42
+ for p in sorted(glob.glob(os.path.join(ROOT, f"results/final_{base}*.json"))):
43
+ with open(p) as f:
44
+ rows += json.load(f)["rows"]
45
+ rows.sort(key=lambda r: (METHOD_ORDER.index(r["method"])
46
+ if r["method"] in METHOD_ORDER else 9,
47
+ r.get("schedule", "FxxF") != "FxxF",
48
+ r["target_speedup"]))
49
+ for r in rows:
50
+ # The original FxxF study keeps its unsuffixed names; other schedules
51
+ # carry the schedule so they never collide with those records.
52
+ sched = r.get("schedule", "FxxF")
53
+ tag = "" if sched.upper() == "FXXF" else f"_{sched}"
54
+ out.append({
55
+ "name": r.get("name") or f"{SHORT[base]}_{r['method']}{tag}_x{r['target_speedup']:g}",
56
+ "schedule": sched,
57
+ "num_inference_steps": r.get("num_inference_steps"),
58
+ "first_chunk_schedule": r.get("first_chunk_schedule"),
59
+ "sampler": r.get("sampler", "renoise"),
60
+ "base": base,
61
+ "method": r["method"],
62
+ "param": r["param"],
63
+ "value": r["value"],
64
+ "target": r["target_speedup"],
65
+ "coefficients": coefficients,
66
+ "indicator": "modulated_input",
67
+ "is_reference": False,
68
+ "compute_equivalent_forwards": r.get("compute_equivalent_forwards"),
69
+ })
70
+ return out
71
+
72
+
73
+ def all_strategies():
74
+ return {b: load_strategies(b) for b in BASES}
75
+
76
+
77
+ if __name__ == "__main__":
78
+ for b, s in all_strategies().items():
79
+ print(f"{b}: {len(s)} strategies")
80
+ for x in s:
81
+ v = "-" if x["value"] is None else f"{x['param']}={x['value']:.6g}"
82
+ print(f" {x['name']:28s} {v}")
eval/test_protocol.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Unit tests for the evaluation protocol's section 12 checklist.
3
+
4
+ python eval/test_protocol.py
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ import tempfile
11
+
12
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13
+ sys.path.insert(0, ROOT)
14
+
15
+ from eval import aggregate as agg # noqa: E402
16
+
17
+ MAPPING = os.path.join(ROOT, "assets/vbench8_extended_subset_mapping.json")
18
+ FULL_INFO = "/local/zoubin/cz/projects/VBench/vbench/VBench_full_info.json"
19
+ PROMPT_DIR = "/local/zoubin/cz/projects/Self-Forcing/prompts/vbench"
20
+
21
+ FAILURES = []
22
+
23
+
24
+ def check(name, cond, detail=""):
25
+ print(f" {'PASS' if cond else 'FAIL'} {name}" + (f" {detail}" if detail else ""))
26
+ if not cond:
27
+ FAILURES.append(name)
28
+
29
+
30
+ def test_mapping():
31
+ print("946 -> 251 mapping")
32
+ with open(MAPPING) as f:
33
+ m = json.load(f)
34
+ rows = m["rows"]
35
+ check("mapping has 251 rows", len(rows) == 251, f"got {len(rows)}")
36
+ counts = {s: sum(1 for r in rows if r["prompt_suite"] == s)
37
+ for s in ("subject_consistency", "overall_consistency", "scene")}
38
+ check("suite counts 72/93/86",
39
+ counts == {"subject_consistency": 72, "overall_consistency": 93, "scene": 86},
40
+ str(counts))
41
+ check("global_index unique",
42
+ len({r["global_index"] for r in rows}) == len(rows))
43
+ for s in counts:
44
+ idx = [r["suite_index"] for r in rows if r["prompt_suite"] == s]
45
+ check(f"{s} suite_index contiguous", idx == list(range(len(idx))))
46
+ check("every scene row keeps auxiliary_info",
47
+ all("auxiliary_info" in r for r in rows if r["prompt_suite"] == "scene"))
48
+ check("extended prompts non-empty",
49
+ all(r["extended_prompt"].strip() for r in rows))
50
+
51
+
52
+ def test_canonical_order():
53
+ print("canonical prompt order is enforced")
54
+ with open(FULL_INFO) as f:
55
+ info = json.load(f)
56
+ with open(os.path.join(PROMPT_DIR, "all_dimension.txt"), encoding="utf-8") as f:
57
+ short = [l.rstrip("\n") for l in f]
58
+ check("946 short prompts", len(short) == 946, f"got {len(short)}")
59
+ check("short prompt order == VBench_full_info",
60
+ all(a["prompt_en"].strip() == b.strip() for a, b in zip(info, short)))
61
+
62
+ # A mismatched prompt file must be refused, not silently fuzzy-matched.
63
+ with open(MAPPING) as f:
64
+ rows = json.load(f)["rows"]
65
+ with open(os.path.join(PROMPT_DIR, "all_dimension_extended.txt"), encoding="utf-8") as f:
66
+ ext = [l.rstrip("\n") for l in f]
67
+ check("mapping extended prompt matches its global_index",
68
+ all(r["extended_prompt"] == ext[r["global_index"]] for r in rows))
69
+ check("mapping original prompt matches its global_index",
70
+ all(r["original_prompt"] == short[r["global_index"]] for r in rows))
71
+
72
+
73
+ def test_normalize_and_aggregate():
74
+ print("normalize + Quality/Semantic/Selected aggregation")
75
+ lo_raw = {d: agg.NORMALIZE_RANGE[d][0] for d in agg.DIMENSIONS}
76
+ hi_raw = {d: agg.NORMALIZE_RANGE[d][1] for d in agg.DIMENSIONS}
77
+ check("normalize maps min -> 0",
78
+ all(abs(v) < 1e-12 for v in agg.normalize(lo_raw).values()))
79
+ check("normalize maps max -> 1",
80
+ all(abs(v - 1.0) < 1e-12 for v in agg.normalize(hi_raw).values()))
81
+
82
+ n = agg.normalize(hi_raw)
83
+ q, s = agg.quality_score(n), agg.semantic_score(n)
84
+ check("quality of all-ones == 1", abs(q - 1.0) < 1e-12, f"got {q}")
85
+ check("semantic of all-ones == 1", abs(s - 1.0) < 1e-12, f"got {s}")
86
+ check("selected of all-ones == 1", abs(agg.selected_score(q, s) - 1.0) < 1e-12)
87
+
88
+ # dynamic_degree carries weight 0.5, everything else in Quality carries 1.0.
89
+ n0 = {d: 0.0 for d in agg.DIMENSIONS}
90
+ n_dd = dict(n0, dynamic_degree=1.0)
91
+ check("dynamic_degree weight is 0.5/5.5",
92
+ abs(agg.quality_score(n_dd) - 0.5 / 5.5) < 1e-12)
93
+ n_sc = dict(n0, subject_consistency=1.0)
94
+ check("other quality dims weight 1/5.5",
95
+ abs(agg.quality_score(n_sc) - 1.0 / 5.5) < 1e-12)
96
+ check("selected weights Quality:Semantic 4:1",
97
+ abs(agg.selected_score(1.0, 0.0) - 0.8) < 1e-12
98
+ and abs(agg.selected_score(0.0, 1.0) - 0.2) < 1e-12)
99
+ check("selected is not the plain mean of 8 normalized dims",
100
+ abs(agg.selected_score(agg.quality_score(n_dd), agg.semantic_score(n_dd))
101
+ - sum(n_dd.values()) / 8.0) > 1e-6)
102
+
103
+
104
+ def _rec(strategy, idx, lat, ctx, mse, frames=81):
105
+ return {
106
+ "status": "complete", "strategy": strategy, "base_model": "self_forcing",
107
+ "method": "none" if strategy.endswith("ffff") else "teacache",
108
+ "target_speedup": 1.0, "global_index": idx, "prompt_suite": "scene",
109
+ "suite_index": idx, "policy_latency_ms": lat,
110
+ "excluded_context_kv_latency_ms": ctx,
111
+ "pixel_metrics_vs_ffff": {"mean_mse": mse, "psnr": 0.0, "ssim": 1.0,
112
+ "lpips": 0.0, "num_frames": frames},
113
+ "cache_diagnostics": {"compute_equivalent_forwards": 28.0},
114
+ }
115
+
116
+
117
+ def test_latency_and_completeness():
118
+ print("latency accounting and the completeness gate")
119
+ import math
120
+
121
+ # ratio of means, not the mean of per-prompt ratios
122
+ ffff = [_rec("sf_ffff", i, lat, 800.0, 1e-12) for i, lat in enumerate([100.0, 300.0])]
123
+ cand = [_rec("sf_x", i, lat, 800.0, 1e-4) for i, lat in enumerate([50.0, 250.0])]
124
+ ratio_of_means = 100.0 * (1 - (sum(r["policy_latency_ms"] for r in cand) / 2)
125
+ / (sum(r["policy_latency_ms"] for r in ffff) / 2))
126
+ mean_of_ratios = 100.0 * (1 - ((50.0 / 100.0) + (250.0 / 300.0)) / 2)
127
+ check("speedup uses ratio of means", abs(ratio_of_means - 25.0) < 1e-9,
128
+ f"{ratio_of_means:.3f}%")
129
+ check("ratio of means differs from mean of ratios",
130
+ abs(ratio_of_means - mean_of_ratios) > 1.0)
131
+
132
+ check("context/KV DiT is a separate field, never inside policy latency",
133
+ all("excluded_context_kv_latency_ms" in r
134
+ and r["policy_latency_ms"] != r["policy_latency_ms"]
135
+ + r["excluded_context_kv_latency_ms"] for r in cand))
136
+
137
+ mse = sum(r["pixel_metrics_vs_ffff"]["mean_mse"] for r in cand) / len(cand)
138
+ check("PSNR aggregates MSE first, then converts once",
139
+ abs(-10.0 * math.log10(max(mse, 1e-12)) - 40.0) < 1e-9)
140
+ check("pixel metrics span all 81 frames",
141
+ {r["pixel_metrics_vs_ffff"]["num_frames"] for r in cand} == {81})
142
+
143
+ with tempfile.TemporaryDirectory() as td:
144
+ for name, recs in (("sf_ffff", ffff), ("sf_x", cand)):
145
+ d = os.path.join(td, "per_prompt", name)
146
+ os.makedirs(d)
147
+ for r in recs:
148
+ with open(os.path.join(d, f"scene_{r['suite_index']:03d}.json"), "w") as f:
149
+ json.dump(r, f)
150
+ argv = sys.argv
151
+ sys.argv = ["aggregate", "--out-root", td, "--expect", "251"]
152
+ try:
153
+ rc = agg.main()
154
+ finally:
155
+ sys.argv = argv
156
+ check("aggregation refuses a strategy that is not 251", rc == 1, f"rc={rc}")
157
+
158
+
159
+ def main():
160
+ for fn in (test_mapping, test_canonical_order, test_normalize_and_aggregate,
161
+ test_latency_and_completeness):
162
+ fn()
163
+ print()
164
+ if FAILURES:
165
+ print(f"{len(FAILURES)} FAILED: {FAILURES}")
166
+ return 1
167
+ print("all protocol unit tests passed")
168
+ return 0
169
+
170
+
171
+ if __name__ == "__main__":
172
+ sys.exit(main())
eval_out/generated_videos/cf_ffff/overall_consistency/000.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d3b9313a8d35ed942c8056953a8a24e8ad5c2eb6d51b84dbbb7263b3a135d9ee
3
+ size 718017
eval_out/generated_videos/cf_ffff/overall_consistency/001.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:97434fa492a782f473db53f5325943455e38f44c2ec0b765a9e94facc1b41f64
3
+ size 678798
eval_out/generated_videos/cf_ffff/overall_consistency/002.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8cc8240a9fc4555de544e0131304dd301803dab2a1867f8efe73d36a6686ed27
3
+ size 665068
eval_out/generated_videos/cf_ffff/overall_consistency/003.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dfeeb51a34c8db083f840497bc64bd6850f361e57b03435d436c0625cf1aa614
3
+ size 652636
eval_out/generated_videos/cf_ffff/overall_consistency/004.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1bcf14ef30751d4cc7ca646615758207443e7848e99c61e91a62566a17301b3a
3
+ size 624330
eval_out/generated_videos/cf_ffff/overall_consistency/005.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5a003819fae2c958584f41480ae48397ed99cb2afcb0bee20a30aa8359adf985
3
+ size 619500
eval_out/generated_videos/cf_ffff/overall_consistency/006.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:510c4b85f51b3605bdb24602644f27c2865ad8cfb7d0f6d105fc1c2fe5027bbf
3
+ size 667321
eval_out/generated_videos/cf_ffff/overall_consistency/007.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cb5b16a9bdf0ba7990fcf6ad75f18a2a0dc8142ca10f2276716c8136ace79e3c
3
+ size 692210
eval_out/generated_videos/cf_ffff/overall_consistency/008.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:93f6f41136b394e1e642a060d2783281692481f08e725a4933e872cf321be58d
3
+ size 538371
eval_out/generated_videos/cf_ffff/overall_consistency/009.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5669c892dd2fdbce0fcfb2ee2f0221645ad620c72562b1bfd3ad1d8a3ad40202
3
+ size 649393
eval_out/generated_videos/cf_ffff/overall_consistency/010.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d05a3ee90af4a46d5cef630591125ad542ec7695b9c25f5279572b9d536d6568
3
+ size 640856
eval_out/generated_videos/cf_ffff/overall_consistency/011.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9aceedf023cd61aac971d5933f802ad3e75770296b10888a2bd5d8fb2294f211
3
+ size 673335
eval_out/generated_videos/cf_ffff/overall_consistency/012.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1742fc927aee1bea4a58cfb703a771181f5558ac95eec019bf82bbb60684ebe6
3
+ size 643474
eval_out/generated_videos/cf_ffff/overall_consistency/013.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8dc175733a2dd86a2a86a5cd181f2990003b1532149aaec82623d841fa710276
3
+ size 697151
eval_out/generated_videos/cf_ffff/overall_consistency/014.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:53b34417aa633a8a16f67d5569cd9acdaae30016bca4b0a12e89297af99a09ab
3
+ size 564467
eval_out/generated_videos/cf_ffff/overall_consistency/015.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5a0bbec98e10b7de4a5edb3ba14d7318683afef3c89587c44f66468c9bbfd705
3
+ size 607298
eval_out/generated_videos/cf_ffff/overall_consistency/016.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d9feefa47ae677af86818c9e3dee84155fe4cc7374a425c9a6d828195fb72361
3
+ size 693258
eval_out/generated_videos/cf_ffff/overall_consistency/017.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c1f29a4bb28547296142bcde8935031005095a0d0fd35f7d46c9c4f0c7ef77f4
3
+ size 647002
eval_out/generated_videos/cf_ffff/overall_consistency/019.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:938b5f61acfd80ca568e800a618032bcb090ed37130378b32e51284d41979519
3
+ size 635121