Upload Python scripts
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- scripts/aggregate_layer17_dynamic_runs.py +134 -0
- scripts/analyze_feature_cache.py +1623 -0
- scripts/analyze_fullgrid_bilinear_3models.py +556 -0
- scripts/analyze_motion_alignment_3models.py +859 -0
- scripts/build_boundary_conditional_dataset.py +146 -0
- scripts/build_conditional_probe_dataset.py +247 -0
- scripts/build_predictor_offline_data.py +878 -0
- scripts/build_vbench8_extended_mapping.py +170 -0
- scripts/calibrate_atc_confidence_token_beta2_vbench3.py +591 -0
- scripts/calibrate_vbench251_span_risk.py +705 -0
- scripts/create_lmdb_14b_shards.py +101 -0
- scripts/create_lmdb_iterative.py +60 -0
- scripts/eval_vbench8_extended_naive_baselines.py +176 -0
- scripts/eval_vbench8_extended_subset.py +256 -0
- scripts/evaluate_layer17_chunk_impact.py +782 -0
- scripts/evaluate_layer17_dynamic_gate.py +904 -0
- scripts/evaluate_layer17_moviebench_step2000.py +271 -0
- scripts/evaluate_long_video_fppf.py +295 -0
- scripts/evaluate_long_video_vbench.py +53 -0
- scripts/evaluate_single_block_fppf.py +1135 -0
- scripts/evaluate_single_chunk_frrr.py +838 -0
- scripts/evaluate_three_block_fppf.py +25 -0
- scripts/evaluate_trained_long_predictors.py +157 -0
- scripts/evaluate_two_block_fppf.py +640 -0
- scripts/generate_ode_pairs.py +120 -0
- scripts/generate_vbench8_extended_atc.py +292 -0
- scripts/generate_vbench8_extended_atc_confidence_token.py +439 -0
- scripts/generate_vbench8_extended_concat.py +96 -0
- scripts/generate_vbench8_extended_disca.py +374 -0
- scripts/generate_vbench8_extended_naive_baselines.py +630 -0
- scripts/generate_vbench8_extended_spanrisk.py +268 -0
- scripts/generate_vbench8_extended_strategies.py +781 -0
- scripts/merge_three_block_sweep_shards.py +159 -0
- scripts/merge_two_block_fppp_shards.py +126 -0
- scripts/naive_vbench_policies.py +127 -0
- scripts/plot_frrf_chunk_metrics.py +164 -0
- scripts/plot_self_causal_evidence.py +776 -0
- scripts/prepare_confidence_stage1_vbench.py +83 -0
- scripts/prepare_layer17_dynamic_vbench.py +107 -0
- scripts/probe_confidence_token_batch_size.py +364 -0
- scripts/run_aligned_conditional_probe_3models.py +501 -0
- scripts/run_conditional_probe_offline.py +436 -0
- scripts/run_single_block_init_sweep.py +1082 -0
- scripts/run_three_block_consecutive_sweep.py +651 -0
- scripts/run_two_block_pair_sweep.py +632 -0
- scripts/run_wan14b_timestep_chunk_cosine.py +422 -0
- scripts/summarize_boundary_to_all_cosine.py +209 -0
- scripts/summarize_conditional_probe.py +418 -0
- scripts/summarize_confidence_stage1_experiments.py +67 -0
- scripts/summarize_layer17_dynamic_gate.py +185 -0
scripts/aggregate_layer17_dynamic_runs.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Aggregate per-run dynamic-gate JSON files without loading GPU models."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
NUMERIC_FIELDS = [
|
| 13 |
+
"accepted_predictor_calls", "full_calls", "predictor_calls",
|
| 14 |
+
"full_dit_time_ms", "predictor_time_ms", "confidence_head_time_ms",
|
| 15 |
+
"context_dit_time_ms", "actual_dit_time_ms", "model_path_time_ms",
|
| 16 |
+
"generation_time_s", "total_time_s", "latent_nrmse", "latent_tail_nrmse",
|
| 17 |
+
"psnr", "ssim", "lpips", "tail_psnr", "tail_ssim", "tail_lpips",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def write_csv(path: Path, rows: list[dict], fields: list[str]) -> None:
|
| 22 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 23 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 24 |
+
with temporary.open("w", encoding="utf-8", newline="") as handle:
|
| 25 |
+
writer = csv.DictWriter(handle, fieldnames=fields)
|
| 26 |
+
writer.writeheader()
|
| 27 |
+
writer.writerows(rows)
|
| 28 |
+
temporary.replace(path)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main() -> None:
|
| 32 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 33 |
+
parser.add_argument("--test_dir", type=Path, required=True)
|
| 34 |
+
parser.add_argument("--expected_prompts", type=int, default=10)
|
| 35 |
+
parser.add_argument(
|
| 36 |
+
"--select_targets",
|
| 37 |
+
type=int,
|
| 38 |
+
nargs="*",
|
| 39 |
+
default=None,
|
| 40 |
+
help="Also select the best dynamic validation row for each target budget.",
|
| 41 |
+
)
|
| 42 |
+
args = parser.parse_args()
|
| 43 |
+
per_run = args.test_dir / "per_run"
|
| 44 |
+
records = [
|
| 45 |
+
json.loads(path.read_text(encoding="utf-8"))
|
| 46 |
+
for path in sorted(per_run.glob("*/prompt_*.json"))
|
| 47 |
+
]
|
| 48 |
+
if not records:
|
| 49 |
+
raise ValueError(f"No per-run JSON files under {per_run}")
|
| 50 |
+
|
| 51 |
+
flat_fields = sorted({key for row in records for key in row if key != "decisions"})
|
| 52 |
+
write_csv(
|
| 53 |
+
args.test_dir / "runs.csv",
|
| 54 |
+
[{key: row.get(key) for key in flat_fields} for row in records],
|
| 55 |
+
flat_fields,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
summary = []
|
| 59 |
+
for name in sorted({str(row["config_name"]) for row in records}):
|
| 60 |
+
selected = [row for row in records if row["config_name"] == name]
|
| 61 |
+
if len(selected) != args.expected_prompts:
|
| 62 |
+
raise ValueError(
|
| 63 |
+
f"{name}: expected {args.expected_prompts} prompts, got {len(selected)}"
|
| 64 |
+
)
|
| 65 |
+
first = selected[0]
|
| 66 |
+
item = {
|
| 67 |
+
"config_name": name,
|
| 68 |
+
"policy": first["policy"],
|
| 69 |
+
"beta": first["beta"],
|
| 70 |
+
"target_accepts": first["target_accepts"],
|
| 71 |
+
"threshold": first["threshold"],
|
| 72 |
+
"num_prompts": len(selected),
|
| 73 |
+
}
|
| 74 |
+
for field in NUMERIC_FIELDS:
|
| 75 |
+
item[field] = sum(float(row[field]) for row in selected) / len(selected)
|
| 76 |
+
summary.append(item)
|
| 77 |
+
fields = [
|
| 78 |
+
"config_name", "policy", "beta", "target_accepts", "threshold",
|
| 79 |
+
"num_prompts", *NUMERIC_FIELDS,
|
| 80 |
+
]
|
| 81 |
+
write_csv(args.test_dir / "summary.csv", summary, fields)
|
| 82 |
+
if args.select_targets:
|
| 83 |
+
selected_dynamic = []
|
| 84 |
+
for target in args.select_targets:
|
| 85 |
+
candidates = [
|
| 86 |
+
row for row in summary
|
| 87 |
+
if row["policy"] == "dynamic"
|
| 88 |
+
and int(row["target_accepts"]) == target
|
| 89 |
+
]
|
| 90 |
+
if not candidates:
|
| 91 |
+
raise ValueError(f"No dynamic candidates for target {target}")
|
| 92 |
+
same_budget = [
|
| 93 |
+
row for row in candidates
|
| 94 |
+
if abs(float(row["accepted_predictor_calls"]) - target) <= 0.5 + 1e-8
|
| 95 |
+
]
|
| 96 |
+
if not same_budget:
|
| 97 |
+
closest = min(
|
| 98 |
+
abs(float(row["accepted_predictor_calls"]) - target)
|
| 99 |
+
for row in candidates
|
| 100 |
+
)
|
| 101 |
+
same_budget = [
|
| 102 |
+
row for row in candidates
|
| 103 |
+
if abs(
|
| 104 |
+
abs(float(row["accepted_predictor_calls"]) - target)
|
| 105 |
+
- closest
|
| 106 |
+
) <= 1e-8
|
| 107 |
+
]
|
| 108 |
+
same_budget.sort(
|
| 109 |
+
key=lambda row: (
|
| 110 |
+
float(row["tail_lpips"]),
|
| 111 |
+
abs(float(row["accepted_predictor_calls"]) - target),
|
| 112 |
+
float(row["beta"]),
|
| 113 |
+
)
|
| 114 |
+
)
|
| 115 |
+
selected_dynamic.append(same_budget[0])
|
| 116 |
+
(args.test_dir / "selected.json").write_text(
|
| 117 |
+
json.dumps(
|
| 118 |
+
{
|
| 119 |
+
"selection_rule": (
|
| 120 |
+
"within target accepted calls +/-0.5, lowest validation "
|
| 121 |
+
"tail LPIPS; then budget distance and lower beta"
|
| 122 |
+
),
|
| 123 |
+
"selected_dynamic": selected_dynamic,
|
| 124 |
+
},
|
| 125 |
+
indent=2,
|
| 126 |
+
)
|
| 127 |
+
+ "\n",
|
| 128 |
+
encoding="utf-8",
|
| 129 |
+
)
|
| 130 |
+
print(json.dumps(summary, indent=2))
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
if __name__ == "__main__":
|
| 134 |
+
main()
|
scripts/analyze_feature_cache.py
ADDED
|
@@ -0,0 +1,1623 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Analyze timestep and cross-chunk feature reuse in Self-Forcing.
|
| 3 |
+
|
| 4 |
+
The script runs the released four-step causal checkpoint without changing its
|
| 5 |
+
outputs. Forward hooks capture sampled DiT hidden states, residual updates, the
|
| 6 |
+
final velocity, and a low-dimensional dense projection used for optical-flow
|
| 7 |
+
alignment. It then produces:
|
| 8 |
+
|
| 9 |
+
* within-chunk and cross-chunk feature-pair metrics;
|
| 10 |
+
* leave-one-prompt-out channel-wise probes for conditional chunk information;
|
| 11 |
+
* shuffled, wrong-step, distant-chunk, zero, and noise controls;
|
| 12 |
+
* motion-stratified raw/global/dense-flow alignment measurements.
|
| 13 |
+
|
| 14 |
+
Each prompt is saved independently, so interrupted generation can be resumed.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
from __future__ import annotations
|
| 18 |
+
|
| 19 |
+
import argparse
|
| 20 |
+
import csv
|
| 21 |
+
import json
|
| 22 |
+
import math
|
| 23 |
+
import os
|
| 24 |
+
import random
|
| 25 |
+
import sys
|
| 26 |
+
import time
|
| 27 |
+
from collections import defaultdict
|
| 28 |
+
from pathlib import Path
|
| 29 |
+
from typing import Any, Iterable
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _preparse_gpu() -> str:
|
| 33 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 34 |
+
parser.add_argument("--gpu", default="2")
|
| 35 |
+
args, _ = parser.parse_known_args()
|
| 36 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 37 |
+
return str(args.gpu)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 41 |
+
|
| 42 |
+
import cv2
|
| 43 |
+
import matplotlib
|
| 44 |
+
|
| 45 |
+
matplotlib.use("Agg")
|
| 46 |
+
import matplotlib.pyplot as plt
|
| 47 |
+
import numpy as np
|
| 48 |
+
import torch
|
| 49 |
+
import torch.nn.functional as F
|
| 50 |
+
from omegaconf import OmegaConf
|
| 51 |
+
|
| 52 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 53 |
+
if str(REPO_ROOT) not in sys.path:
|
| 54 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 55 |
+
|
| 56 |
+
from pipeline import CausalInferencePipeline
|
| 57 |
+
from utils.misc import set_seed
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
EXPECTED_LATENT_HEIGHT = 60
|
| 61 |
+
EXPECTED_LATENT_WIDTH = 104
|
| 62 |
+
FRAME_TOKEN_HEIGHT = 30
|
| 63 |
+
FRAME_TOKEN_WIDTH = 52
|
| 64 |
+
FRAME_SEQ_LENGTH = FRAME_TOKEN_HEIGHT * FRAME_TOKEN_WIDTH
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def parse_args() -> argparse.Namespace:
|
| 68 |
+
parser = argparse.ArgumentParser(
|
| 69 |
+
description="Self-Forcing cross-chunk feature-cache analysis"
|
| 70 |
+
)
|
| 71 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 72 |
+
parser.add_argument(
|
| 73 |
+
"--config_path", type=Path, default=Path("configs/self_forcing_dmd.yaml")
|
| 74 |
+
)
|
| 75 |
+
parser.add_argument(
|
| 76 |
+
"--checkpoint_path",
|
| 77 |
+
type=Path,
|
| 78 |
+
default=Path("checkpoints/self_forcing_dmd.pt"),
|
| 79 |
+
)
|
| 80 |
+
parser.add_argument(
|
| 81 |
+
"--prompt_path",
|
| 82 |
+
type=Path,
|
| 83 |
+
default=Path("prompts/MovieGenVideoBench_extended.txt"),
|
| 84 |
+
)
|
| 85 |
+
parser.add_argument("--output_dir", type=Path, required=True)
|
| 86 |
+
parser.add_argument("--num_prompts", type=int, default=3)
|
| 87 |
+
parser.add_argument("--num_frames", type=int, default=21)
|
| 88 |
+
parser.add_argument("--seed", type=int, default=20260728)
|
| 89 |
+
parser.add_argument(
|
| 90 |
+
"--same_seed",
|
| 91 |
+
action="store_true",
|
| 92 |
+
help="Reset every prompt to --seed for paired cross-model evaluation.",
|
| 93 |
+
)
|
| 94 |
+
parser.add_argument(
|
| 95 |
+
"--layers", type=int, nargs="+", default=[0, 9, 19, 29]
|
| 96 |
+
)
|
| 97 |
+
parser.add_argument("--max_tokens", type=int, default=256)
|
| 98 |
+
parser.add_argument("--projection_dim", type=int, default=64)
|
| 99 |
+
parser.add_argument("--ridge", type=float, default=1e-4)
|
| 100 |
+
parser.add_argument("--use_ema", action="store_true", default=True)
|
| 101 |
+
parser.add_argument("--no_ema", action="store_false", dest="use_ema")
|
| 102 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 103 |
+
parser.add_argument(
|
| 104 |
+
"--cosine_only",
|
| 105 |
+
action="store_true",
|
| 106 |
+
help="Generate pair metrics/heatmap only; skip probes and motion analysis.",
|
| 107 |
+
)
|
| 108 |
+
parser.add_argument(
|
| 109 |
+
"--analysis_only",
|
| 110 |
+
action="store_true",
|
| 111 |
+
help="Skip model loading and analyze existing per-prompt snapshots.",
|
| 112 |
+
)
|
| 113 |
+
parser.add_argument(
|
| 114 |
+
"--save_preview",
|
| 115 |
+
action="store_true",
|
| 116 |
+
help="Save a compact MP4 preview when torchvision video IO is available.",
|
| 117 |
+
)
|
| 118 |
+
args = parser.parse_args()
|
| 119 |
+
if args.num_frames % 3 != 0:
|
| 120 |
+
parser.error("--num_frames must be divisible by the configured 3-frame chunk")
|
| 121 |
+
if args.num_prompts < 3 and not args.cosine_only:
|
| 122 |
+
parser.error("--num_prompts must be at least 3 for held-out/shuffle controls")
|
| 123 |
+
return args
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def resolve_path(path: Path) -> Path:
|
| 127 |
+
return path if path.is_absolute() else REPO_ROOT / path
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
|
| 131 |
+
if not rows:
|
| 132 |
+
return
|
| 133 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 134 |
+
fields: list[str] = []
|
| 135 |
+
for row in rows:
|
| 136 |
+
for key in row:
|
| 137 |
+
if key not in fields:
|
| 138 |
+
fields.append(key)
|
| 139 |
+
with path.open("w", newline="", encoding="utf-8") as handle:
|
| 140 |
+
writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore")
|
| 141 |
+
writer.writeheader()
|
| 142 |
+
writer.writerows(rows)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def read_prompts(path: Path, count: int) -> list[str]:
|
| 146 |
+
with path.open("r", encoding="utf-8") as handle:
|
| 147 |
+
prompts = [line.strip() for line in handle if line.strip()]
|
| 148 |
+
if len(prompts) < count:
|
| 149 |
+
raise ValueError(f"Requested {count} prompts, found {len(prompts)} in {path}")
|
| 150 |
+
return prompts[:count]
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def regular_grid_indices(
|
| 154 |
+
frames: int, height: int, width: int, max_tokens: int, device: torch.device
|
| 155 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 156 |
+
total = frames * height * width
|
| 157 |
+
if max_tokens >= total:
|
| 158 |
+
coords = torch.cartesian_prod(
|
| 159 |
+
torch.arange(frames, device=device),
|
| 160 |
+
torch.arange(height, device=device),
|
| 161 |
+
torch.arange(width, device=device),
|
| 162 |
+
)
|
| 163 |
+
else:
|
| 164 |
+
per_frame = max(1, max_tokens // frames)
|
| 165 |
+
h_count = max(
|
| 166 |
+
1, min(height, int(round(math.sqrt(per_frame * height / width))))
|
| 167 |
+
)
|
| 168 |
+
w_count = max(1, min(width, per_frame // h_count))
|
| 169 |
+
while frames * h_count * w_count > max_tokens and w_count > 1:
|
| 170 |
+
w_count -= 1
|
| 171 |
+
while frames * h_count * w_count > max_tokens and h_count > 1:
|
| 172 |
+
h_count -= 1
|
| 173 |
+
hs = (
|
| 174 |
+
torch.linspace(0, height - 1, h_count, device=device)
|
| 175 |
+
.round()
|
| 176 |
+
.long()
|
| 177 |
+
.unique()
|
| 178 |
+
)
|
| 179 |
+
ws = (
|
| 180 |
+
torch.linspace(0, width - 1, w_count, device=device)
|
| 181 |
+
.round()
|
| 182 |
+
.long()
|
| 183 |
+
.unique()
|
| 184 |
+
)
|
| 185 |
+
coords = torch.cartesian_prod(
|
| 186 |
+
torch.arange(frames, device=device), hs, ws
|
| 187 |
+
)
|
| 188 |
+
flat = (
|
| 189 |
+
coords[:, 0] * height * width + coords[:, 1] * width + coords[:, 2]
|
| 190 |
+
)
|
| 191 |
+
return flat.long(), coords.long()
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
class FeatureRecorder:
|
| 195 |
+
def __init__(
|
| 196 |
+
self,
|
| 197 |
+
model: torch.nn.Module,
|
| 198 |
+
layers: list[int],
|
| 199 |
+
denoising_timesteps: Iterable[float],
|
| 200 |
+
num_frame_per_block: int,
|
| 201 |
+
max_tokens: int,
|
| 202 |
+
projection_dim: int,
|
| 203 |
+
) -> None:
|
| 204 |
+
self.model = model
|
| 205 |
+
self.layers = sorted(set(int(value) for value in layers))
|
| 206 |
+
self.timesteps = [float(value) for value in denoising_timesteps]
|
| 207 |
+
self.num_frame_per_block = int(num_frame_per_block)
|
| 208 |
+
self.max_tokens = int(max_tokens)
|
| 209 |
+
self.projection_dim = int(projection_dim)
|
| 210 |
+
self.projection_layer = self.layers[-1]
|
| 211 |
+
self.records: dict[str, dict[str, torch.Tensor]] = defaultdict(dict)
|
| 212 |
+
self.projected: dict[str, torch.Tensor] = {}
|
| 213 |
+
self.projected_by_layer: dict[str, torch.Tensor] = {}
|
| 214 |
+
self.sample_coords: dict[str, torch.Tensor] = {}
|
| 215 |
+
self.current: dict[str, Any] = {"active": False}
|
| 216 |
+
self.handles: list[Any] = []
|
| 217 |
+
self._projection_cache: dict[tuple[int, str], torch.Tensor] = {}
|
| 218 |
+
self._register()
|
| 219 |
+
|
| 220 |
+
def _register(self) -> None:
|
| 221 |
+
self.handles.append(
|
| 222 |
+
self.model.register_forward_pre_hook(self._model_pre_hook, with_kwargs=True)
|
| 223 |
+
)
|
| 224 |
+
self.handles.append(self.model.register_forward_hook(self._model_output_hook))
|
| 225 |
+
for layer in self.layers:
|
| 226 |
+
if layer < 0 or layer >= len(self.model.blocks):
|
| 227 |
+
raise ValueError(
|
| 228 |
+
f"Layer {layer} outside model block range 0..{len(self.model.blocks)-1}"
|
| 229 |
+
)
|
| 230 |
+
self.handles.append(
|
| 231 |
+
self.model.blocks[layer].register_forward_hook(
|
| 232 |
+
self._make_block_hook(layer)
|
| 233 |
+
)
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
def close(self) -> None:
|
| 237 |
+
for handle in self.handles:
|
| 238 |
+
handle.remove()
|
| 239 |
+
self.handles.clear()
|
| 240 |
+
|
| 241 |
+
def reset(self) -> None:
|
| 242 |
+
self.records = defaultdict(dict)
|
| 243 |
+
self.projected = {}
|
| 244 |
+
self.projected_by_layer = {}
|
| 245 |
+
self.sample_coords = {}
|
| 246 |
+
self.current = {"active": False}
|
| 247 |
+
|
| 248 |
+
def _model_pre_hook(
|
| 249 |
+
self, _module: torch.nn.Module, _args: tuple[Any, ...], kwargs: dict[str, Any]
|
| 250 |
+
) -> None:
|
| 251 |
+
timestep = kwargs.get("t")
|
| 252 |
+
current_start = int(kwargs.get("current_start", 0) or 0)
|
| 253 |
+
if not isinstance(timestep, torch.Tensor) or timestep.numel() == 0:
|
| 254 |
+
self.current = {"active": False}
|
| 255 |
+
return
|
| 256 |
+
value = float(timestep.detach().float().reshape(-1)[0].item())
|
| 257 |
+
distances = [abs(value - expected) for expected in self.timesteps]
|
| 258 |
+
step = int(np.argmin(distances))
|
| 259 |
+
if distances[step] > 0.5:
|
| 260 |
+
self.current = {"active": False, "timestep": value}
|
| 261 |
+
return
|
| 262 |
+
frames = int(timestep.shape[-1]) if timestep.ndim > 1 else 1
|
| 263 |
+
if frames != self.num_frame_per_block:
|
| 264 |
+
self.current = {"active": False, "timestep": value}
|
| 265 |
+
return
|
| 266 |
+
start_frame = current_start // FRAME_SEQ_LENGTH
|
| 267 |
+
chunk = start_frame // self.num_frame_per_block
|
| 268 |
+
self.current = {
|
| 269 |
+
"active": True,
|
| 270 |
+
"chunk": int(chunk),
|
| 271 |
+
"step": step,
|
| 272 |
+
"timestep": value,
|
| 273 |
+
"frames": frames,
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
def _key(self) -> str:
|
| 277 |
+
return f"{self.current['chunk']}:{self.current['step']}"
|
| 278 |
+
|
| 279 |
+
def _hidden_indices(
|
| 280 |
+
self, tokens: torch.Tensor
|
| 281 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 282 |
+
frames = int(self.current["frames"])
|
| 283 |
+
if tokens.shape[1] != frames * FRAME_TOKEN_HEIGHT * FRAME_TOKEN_WIDTH:
|
| 284 |
+
raise ValueError(
|
| 285 |
+
f"Unexpected hidden token count {tokens.shape[1]} for {frames} frames"
|
| 286 |
+
)
|
| 287 |
+
return regular_grid_indices(
|
| 288 |
+
frames,
|
| 289 |
+
FRAME_TOKEN_HEIGHT,
|
| 290 |
+
FRAME_TOKEN_WIDTH,
|
| 291 |
+
self.max_tokens,
|
| 292 |
+
tokens.device,
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
def _projection(self, dim: int, device: torch.device) -> torch.Tensor:
|
| 296 |
+
cache_key = (dim, str(device))
|
| 297 |
+
if cache_key not in self._projection_cache:
|
| 298 |
+
generator = torch.Generator(device="cpu").manual_seed(20260728 + dim)
|
| 299 |
+
signs = torch.randint(
|
| 300 |
+
0,
|
| 301 |
+
2,
|
| 302 |
+
(dim, self.projection_dim),
|
| 303 |
+
generator=generator,
|
| 304 |
+
dtype=torch.int8,
|
| 305 |
+
)
|
| 306 |
+
projection = (
|
| 307 |
+
signs.float().mul_(2).sub_(1).div_(math.sqrt(self.projection_dim))
|
| 308 |
+
)
|
| 309 |
+
self._projection_cache[cache_key] = projection.to(device)
|
| 310 |
+
return self._projection_cache[cache_key]
|
| 311 |
+
|
| 312 |
+
def _make_block_hook(self, layer: int):
|
| 313 |
+
def hook(
|
| 314 |
+
_module: torch.nn.Module,
|
| 315 |
+
inputs: tuple[torch.Tensor, ...],
|
| 316 |
+
output: torch.Tensor,
|
| 317 |
+
) -> None:
|
| 318 |
+
if not self.current.get("active", False):
|
| 319 |
+
return
|
| 320 |
+
if not inputs or not isinstance(output, torch.Tensor):
|
| 321 |
+
return
|
| 322 |
+
key = self._key()
|
| 323 |
+
hidden_input = inputs[0]
|
| 324 |
+
indices, coords = self._hidden_indices(output)
|
| 325 |
+
hidden = output[0].index_select(0, indices)
|
| 326 |
+
delta = (output - hidden_input)[0].index_select(0, indices)
|
| 327 |
+
self.records[f"block_{layer}_hidden"][key] = (
|
| 328 |
+
hidden.detach().to(dtype=torch.float16, device="cpu")
|
| 329 |
+
)
|
| 330 |
+
self.records[f"block_{layer}_delta"][key] = (
|
| 331 |
+
delta.detach().to(dtype=torch.float16, device="cpu")
|
| 332 |
+
)
|
| 333 |
+
self.sample_coords["hidden"] = coords.detach().cpu()
|
| 334 |
+
|
| 335 |
+
if layer in self.layers:
|
| 336 |
+
projection = self._projection(output.shape[-1], output.device)
|
| 337 |
+
dense = torch.matmul(output[0].float(), projection)
|
| 338 |
+
frames = int(self.current["frames"])
|
| 339 |
+
dense = dense.reshape(
|
| 340 |
+
frames,
|
| 341 |
+
FRAME_TOKEN_HEIGHT,
|
| 342 |
+
FRAME_TOKEN_WIDTH,
|
| 343 |
+
self.projection_dim,
|
| 344 |
+
)
|
| 345 |
+
dense_cpu = dense.detach().to(dtype=torch.float16, device="cpu")
|
| 346 |
+
self.projected_by_layer[f"{layer}:{key}"] = dense_cpu
|
| 347 |
+
# Preserve the legacy key layout for existing final-layer analyses.
|
| 348 |
+
if layer == self.projection_layer:
|
| 349 |
+
self.projected[key] = dense_cpu
|
| 350 |
+
|
| 351 |
+
return hook
|
| 352 |
+
|
| 353 |
+
def _model_output_hook(
|
| 354 |
+
self,
|
| 355 |
+
_module: torch.nn.Module,
|
| 356 |
+
_inputs: tuple[Any, ...],
|
| 357 |
+
output: torch.Tensor,
|
| 358 |
+
) -> None:
|
| 359 |
+
if not self.current.get("active", False):
|
| 360 |
+
return
|
| 361 |
+
if not isinstance(output, torch.Tensor) or output.ndim != 5:
|
| 362 |
+
return
|
| 363 |
+
batch, channels, frames, height, width = output.shape
|
| 364 |
+
if batch != 1:
|
| 365 |
+
raise ValueError(f"Analysis expects batch size 1, got {batch}")
|
| 366 |
+
tokens = output.permute(0, 2, 3, 4, 1).reshape(
|
| 367 |
+
batch, frames * height * width, channels
|
| 368 |
+
)
|
| 369 |
+
indices, coords = regular_grid_indices(
|
| 370 |
+
frames, height, width, self.max_tokens, output.device
|
| 371 |
+
)
|
| 372 |
+
sampled = tokens[0].index_select(0, indices)
|
| 373 |
+
self.records["dit_output"][self._key()] = sampled.detach().to(
|
| 374 |
+
dtype=torch.float16, device="cpu"
|
| 375 |
+
)
|
| 376 |
+
self.sample_coords["dit_output"] = coords.detach().cpu()
|
| 377 |
+
|
| 378 |
+
def state_dict(self) -> dict[str, Any]:
|
| 379 |
+
return {
|
| 380 |
+
"timesteps": self.timesteps,
|
| 381 |
+
"layers": self.layers,
|
| 382 |
+
"projection_layer": self.projection_layer,
|
| 383 |
+
"projection_dim": self.projection_dim,
|
| 384 |
+
"records": {stage: dict(values) for stage, values in self.records.items()},
|
| 385 |
+
"projected": dict(self.projected),
|
| 386 |
+
"projected_by_layer": dict(self.projected_by_layer),
|
| 387 |
+
"sample_coords": dict(self.sample_coords),
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
def build_pipeline(args: argparse.Namespace) -> tuple[CausalInferencePipeline, Any]:
|
| 392 |
+
config = OmegaConf.load(resolve_path(args.config_path))
|
| 393 |
+
default_config = OmegaConf.load(REPO_ROOT / "configs/default_config.yaml")
|
| 394 |
+
config = OmegaConf.merge(default_config, config)
|
| 395 |
+
device = torch.device("cuda")
|
| 396 |
+
pipeline = CausalInferencePipeline(config, device=device)
|
| 397 |
+
checkpoint = torch.load(
|
| 398 |
+
resolve_path(args.checkpoint_path), map_location="cpu", weights_only=False
|
| 399 |
+
)
|
| 400 |
+
state_key = "generator_ema" if args.use_ema else "generator"
|
| 401 |
+
pipeline.generator.load_state_dict(checkpoint[state_key])
|
| 402 |
+
del checkpoint
|
| 403 |
+
pipeline = pipeline.to(dtype=torch.bfloat16)
|
| 404 |
+
pipeline.text_encoder.to(device=device)
|
| 405 |
+
pipeline.generator.to(device=device)
|
| 406 |
+
pipeline.vae.to(device=device)
|
| 407 |
+
pipeline.eval()
|
| 408 |
+
return pipeline, config
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
def downsample_anchors(video: torch.Tensor, latent_frames: int) -> np.ndarray:
|
| 412 |
+
value = video[0].detach().float().cpu()
|
| 413 |
+
frame_count = value.shape[0]
|
| 414 |
+
if frame_count == latent_frames:
|
| 415 |
+
indices = np.arange(latent_frames)
|
| 416 |
+
else:
|
| 417 |
+
indices = np.linspace(0, frame_count - 1, latent_frames).round().astype(int)
|
| 418 |
+
anchors = value[indices].permute(0, 2, 3, 1).clamp(0, 1).numpy()
|
| 419 |
+
result = []
|
| 420 |
+
for frame in anchors:
|
| 421 |
+
frame_u8 = np.uint8(np.round(frame * 255.0))
|
| 422 |
+
result.append(cv2.resize(frame_u8, (416, 240), interpolation=cv2.INTER_AREA))
|
| 423 |
+
return np.stack(result)
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def maybe_save_preview(path: Path, anchors: np.ndarray) -> None:
|
| 427 |
+
try:
|
| 428 |
+
from torchvision.io import write_video
|
| 429 |
+
|
| 430 |
+
repeated = np.repeat(anchors, 4, axis=0)
|
| 431 |
+
tensor = torch.from_numpy(repeated)
|
| 432 |
+
write_video(str(path), tensor, fps=16)
|
| 433 |
+
except Exception as error:
|
| 434 |
+
print(f"[preview] skipped: {error}", flush=True)
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
@torch.inference_mode()
|
| 438 |
+
def generate_snapshots(args: argparse.Namespace) -> list[Path]:
|
| 439 |
+
output_dir = args.output_dir
|
| 440 |
+
runs_dir = output_dir / "runs"
|
| 441 |
+
runs_dir.mkdir(parents=True, exist_ok=True)
|
| 442 |
+
prompts = read_prompts(resolve_path(args.prompt_path), args.num_prompts)
|
| 443 |
+
expected_paths = [runs_dir / f"prompt_{index:02d}.pt" for index in range(len(prompts))]
|
| 444 |
+
missing = [
|
| 445 |
+
path
|
| 446 |
+
for path in expected_paths
|
| 447 |
+
if args.overwrite or not path.exists()
|
| 448 |
+
]
|
| 449 |
+
if not missing:
|
| 450 |
+
print("[generation] all prompt snapshots already exist", flush=True)
|
| 451 |
+
return expected_paths
|
| 452 |
+
|
| 453 |
+
pipeline, config = build_pipeline(args)
|
| 454 |
+
denoising_timesteps = [
|
| 455 |
+
float(value) for value in pipeline.denoising_step_list.detach().cpu().tolist()
|
| 456 |
+
]
|
| 457 |
+
recorder = FeatureRecorder(
|
| 458 |
+
model=pipeline.generator.model,
|
| 459 |
+
layers=args.layers,
|
| 460 |
+
denoising_timesteps=denoising_timesteps,
|
| 461 |
+
num_frame_per_block=pipeline.num_frame_per_block,
|
| 462 |
+
max_tokens=args.max_tokens,
|
| 463 |
+
projection_dim=args.projection_dim,
|
| 464 |
+
)
|
| 465 |
+
metadata = {
|
| 466 |
+
"physical_gpu": args.gpu,
|
| 467 |
+
"config_path": str(resolve_path(args.config_path)),
|
| 468 |
+
"checkpoint_path": str(resolve_path(args.checkpoint_path)),
|
| 469 |
+
"use_ema": args.use_ema,
|
| 470 |
+
"num_prompts": args.num_prompts,
|
| 471 |
+
"num_frames": args.num_frames,
|
| 472 |
+
"num_frame_per_block": pipeline.num_frame_per_block,
|
| 473 |
+
"denoising_timesteps": denoising_timesteps,
|
| 474 |
+
"layers": args.layers,
|
| 475 |
+
"max_tokens": args.max_tokens,
|
| 476 |
+
"projection_dim": args.projection_dim,
|
| 477 |
+
"seed": args.seed,
|
| 478 |
+
"dtype": "bfloat16",
|
| 479 |
+
}
|
| 480 |
+
(output_dir / "experiment_config.json").write_text(
|
| 481 |
+
json.dumps(metadata, indent=2, ensure_ascii=False) + "\n",
|
| 482 |
+
encoding="utf-8",
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
try:
|
| 486 |
+
for index, (prompt, path) in enumerate(zip(prompts, expected_paths)):
|
| 487 |
+
if path.exists() and not args.overwrite:
|
| 488 |
+
print(f"[generation] skip existing {path.name}", flush=True)
|
| 489 |
+
continue
|
| 490 |
+
recorder.reset()
|
| 491 |
+
run_seed = args.seed if args.same_seed else args.seed + index
|
| 492 |
+
set_seed(run_seed)
|
| 493 |
+
noise = torch.randn(
|
| 494 |
+
1,
|
| 495 |
+
args.num_frames,
|
| 496 |
+
16,
|
| 497 |
+
EXPECTED_LATENT_HEIGHT,
|
| 498 |
+
EXPECTED_LATENT_WIDTH,
|
| 499 |
+
device="cuda",
|
| 500 |
+
dtype=torch.bfloat16,
|
| 501 |
+
)
|
| 502 |
+
torch.cuda.reset_peak_memory_stats()
|
| 503 |
+
torch.cuda.synchronize()
|
| 504 |
+
start = time.perf_counter()
|
| 505 |
+
print(
|
| 506 |
+
f"[generation] prompt {index + 1}/{len(prompts)} seed={run_seed}",
|
| 507 |
+
flush=True,
|
| 508 |
+
)
|
| 509 |
+
video, latents = pipeline.inference(
|
| 510 |
+
noise=noise,
|
| 511 |
+
text_prompts=[prompt],
|
| 512 |
+
return_latents=True,
|
| 513 |
+
initial_latent=None,
|
| 514 |
+
low_memory=False,
|
| 515 |
+
)
|
| 516 |
+
torch.cuda.synchronize()
|
| 517 |
+
elapsed = time.perf_counter() - start
|
| 518 |
+
peak_gib = torch.cuda.max_memory_allocated() / (1024**3)
|
| 519 |
+
anchors = downsample_anchors(video, args.num_frames)
|
| 520 |
+
state = {
|
| 521 |
+
"run_index": index,
|
| 522 |
+
"prompt": prompt,
|
| 523 |
+
"seed": run_seed,
|
| 524 |
+
"elapsed_s": elapsed,
|
| 525 |
+
"peak_gpu_gib": peak_gib,
|
| 526 |
+
"num_frames": args.num_frames,
|
| 527 |
+
"num_frame_per_block": pipeline.num_frame_per_block,
|
| 528 |
+
"latents": latents[0].detach().to(
|
| 529 |
+
dtype=torch.float16, device="cpu"
|
| 530 |
+
),
|
| 531 |
+
**recorder.state_dict(),
|
| 532 |
+
}
|
| 533 |
+
torch.save(state, path)
|
| 534 |
+
np.savez_compressed(path.with_suffix(".anchors.npz"), frames=anchors)
|
| 535 |
+
if args.save_preview:
|
| 536 |
+
maybe_save_preview(path.with_suffix(".mp4"), anchors)
|
| 537 |
+
print(
|
| 538 |
+
f"[generation] saved {path.name}: {elapsed:.1f}s, peak={peak_gib:.1f} GiB",
|
| 539 |
+
flush=True,
|
| 540 |
+
)
|
| 541 |
+
del video, latents, noise, state
|
| 542 |
+
pipeline.vae.model.clear_cache()
|
| 543 |
+
torch.cuda.empty_cache()
|
| 544 |
+
finally:
|
| 545 |
+
recorder.close()
|
| 546 |
+
return expected_paths
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
def load_runs(paths: list[Path]) -> list[dict[str, Any]]:
|
| 550 |
+
runs = []
|
| 551 |
+
for path in paths:
|
| 552 |
+
if not path.exists():
|
| 553 |
+
raise FileNotFoundError(path)
|
| 554 |
+
run = torch.load(path, map_location="cpu", weights_only=False)
|
| 555 |
+
anchor_path = path.with_suffix(".anchors.npz")
|
| 556 |
+
if not anchor_path.exists():
|
| 557 |
+
raise FileNotFoundError(anchor_path)
|
| 558 |
+
run["anchors"] = np.load(anchor_path, allow_pickle=False)["frames"]
|
| 559 |
+
run["path"] = str(path)
|
| 560 |
+
runs.append(run)
|
| 561 |
+
return runs
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
def feature(run: dict[str, Any], stage: str, chunk: int, step: int) -> torch.Tensor:
|
| 565 |
+
return run["records"][stage][f"{chunk}:{step}"].float()
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def projected_feature(
|
| 569 |
+
run: dict[str, Any], chunk: int, step: int
|
| 570 |
+
) -> torch.Tensor:
|
| 571 |
+
return run["projected"][f"{chunk}:{step}"].float()
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
def available_chunks(run: dict[str, Any], stage: str) -> list[int]:
|
| 575 |
+
return sorted(
|
| 576 |
+
{int(key.split(":")[0]) for key in run["records"][stage].keys()}
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
def available_steps(run: dict[str, Any], stage: str) -> list[int]:
|
| 581 |
+
return sorted(
|
| 582 |
+
{int(key.split(":")[1]) for key in run["records"][stage].keys()}
|
| 583 |
+
)
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
def pair_metrics(
|
| 587 |
+
reference: torch.Tensor,
|
| 588 |
+
target: torch.Tensor,
|
| 589 |
+
compute_cka: bool = True,
|
| 590 |
+
) -> dict[str, float]:
|
| 591 |
+
if reference.shape != target.shape:
|
| 592 |
+
raise ValueError(f"Pair shape mismatch: {reference.shape} vs {target.shape}")
|
| 593 |
+
eps = 1e-8
|
| 594 |
+
x = reference.float()
|
| 595 |
+
y = target.float()
|
| 596 |
+
xf = x.reshape(-1)
|
| 597 |
+
yf = y.reshape(-1)
|
| 598 |
+
diff = yf - xf
|
| 599 |
+
cosine = F.cosine_similarity(xf[None], yf[None], dim=1, eps=eps)[0]
|
| 600 |
+
xc_flat = xf - xf.mean()
|
| 601 |
+
yc_flat = yf - yf.mean()
|
| 602 |
+
centered_cosine = torch.dot(xc_flat, yc_flat) / (
|
| 603 |
+
torch.linalg.vector_norm(xc_flat)
|
| 604 |
+
* torch.linalg.vector_norm(yc_flat)
|
| 605 |
+
+ eps
|
| 606 |
+
)
|
| 607 |
+
token_cosine = F.cosine_similarity(x, y, dim=1, eps=eps)
|
| 608 |
+
rel_l2 = diff.square().mean().sqrt() / (xf.square().mean().sqrt() + eps)
|
| 609 |
+
nmse = diff.square().mean() / (yc_flat.square().mean() + eps)
|
| 610 |
+
|
| 611 |
+
if compute_cka:
|
| 612 |
+
xc = x - x.mean(dim=0, keepdim=True)
|
| 613 |
+
yc = y - y.mean(dim=0, keepdim=True)
|
| 614 |
+
gram_x = xc @ xc.T
|
| 615 |
+
gram_y = yc @ yc.T
|
| 616 |
+
cka = (gram_x * gram_y).sum() / (
|
| 617 |
+
(gram_x.square().sum() * gram_y.square().sum()).sqrt() + eps
|
| 618 |
+
)
|
| 619 |
+
else:
|
| 620 |
+
cka = torch.tensor(float("nan"))
|
| 621 |
+
quantiles = torch.quantile(
|
| 622 |
+
token_cosine,
|
| 623 |
+
torch.tensor([0.1, 0.5, 0.9], dtype=token_cosine.dtype),
|
| 624 |
+
)
|
| 625 |
+
return {
|
| 626 |
+
"cosine": float(cosine),
|
| 627 |
+
"centered_cosine": float(centered_cosine),
|
| 628 |
+
"linear_cka": float(cka),
|
| 629 |
+
"rel_l2": float(rel_l2),
|
| 630 |
+
"nmse": float(nmse),
|
| 631 |
+
"token_cosine_mean": float(token_cosine.mean()),
|
| 632 |
+
"token_cosine_p10": float(quantiles[0]),
|
| 633 |
+
"token_cosine_p50": float(quantiles[1]),
|
| 634 |
+
"token_cosine_p90": float(quantiles[2]),
|
| 635 |
+
"reference_rms": float(xf.square().mean().sqrt()),
|
| 636 |
+
"target_rms": float(yf.square().mean().sqrt()),
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
def collect_pair_rows(
|
| 641 |
+
runs: list[dict[str, Any]], cosine_only: bool = False
|
| 642 |
+
) -> list[dict[str, Any]]:
|
| 643 |
+
rows: list[dict[str, Any]] = []
|
| 644 |
+
stages = sorted(runs[0]["records"])
|
| 645 |
+
if cosine_only:
|
| 646 |
+
stages = [
|
| 647 |
+
stage
|
| 648 |
+
for stage in stages
|
| 649 |
+
if stage.startswith("block_") and stage.endswith("_hidden")
|
| 650 |
+
]
|
| 651 |
+
for run_index, run in enumerate(runs):
|
| 652 |
+
shuffled_run = runs[(run_index + 1) % len(runs)]
|
| 653 |
+
for stage in stages:
|
| 654 |
+
chunks = available_chunks(run, stage)
|
| 655 |
+
steps = available_steps(run, stage)
|
| 656 |
+
|
| 657 |
+
def append(
|
| 658 |
+
comparison: str,
|
| 659 |
+
ref_run: dict[str, Any],
|
| 660 |
+
ref_chunk: int,
|
| 661 |
+
ref_step: int,
|
| 662 |
+
target_chunk: int,
|
| 663 |
+
target_step: int,
|
| 664 |
+
) -> None:
|
| 665 |
+
metrics = pair_metrics(
|
| 666 |
+
feature(ref_run, stage, ref_chunk, ref_step),
|
| 667 |
+
feature(run, stage, target_chunk, target_step),
|
| 668 |
+
compute_cka=not cosine_only,
|
| 669 |
+
)
|
| 670 |
+
rows.append(
|
| 671 |
+
{
|
| 672 |
+
"run": run_index,
|
| 673 |
+
"comparison": comparison,
|
| 674 |
+
"stage": stage,
|
| 675 |
+
"reference_chunk": ref_chunk,
|
| 676 |
+
"target_chunk": target_chunk,
|
| 677 |
+
"reference_step": ref_step,
|
| 678 |
+
"target_step": target_step,
|
| 679 |
+
**metrics,
|
| 680 |
+
}
|
| 681 |
+
)
|
| 682 |
+
|
| 683 |
+
for chunk in chunks:
|
| 684 |
+
for step in steps[1:]:
|
| 685 |
+
append(
|
| 686 |
+
"within_adjacent",
|
| 687 |
+
run,
|
| 688 |
+
chunk,
|
| 689 |
+
step - 1,
|
| 690 |
+
chunk,
|
| 691 |
+
step,
|
| 692 |
+
)
|
| 693 |
+
if chunk < 1:
|
| 694 |
+
continue
|
| 695 |
+
for step in steps:
|
| 696 |
+
append(
|
| 697 |
+
"cross_same",
|
| 698 |
+
run,
|
| 699 |
+
chunk - 1,
|
| 700 |
+
step,
|
| 701 |
+
chunk,
|
| 702 |
+
step,
|
| 703 |
+
)
|
| 704 |
+
if cosine_only:
|
| 705 |
+
continue
|
| 706 |
+
append(
|
| 707 |
+
"cross_video_shuffle",
|
| 708 |
+
shuffled_run,
|
| 709 |
+
chunk - 1,
|
| 710 |
+
step,
|
| 711 |
+
chunk,
|
| 712 |
+
step,
|
| 713 |
+
)
|
| 714 |
+
if step > 0:
|
| 715 |
+
append(
|
| 716 |
+
"cross_wrong_step",
|
| 717 |
+
run,
|
| 718 |
+
chunk - 1,
|
| 719 |
+
step - 1,
|
| 720 |
+
chunk,
|
| 721 |
+
step,
|
| 722 |
+
)
|
| 723 |
+
if chunk > 1:
|
| 724 |
+
append(
|
| 725 |
+
"cross_distant",
|
| 726 |
+
run,
|
| 727 |
+
chunk - 2,
|
| 728 |
+
step,
|
| 729 |
+
chunk,
|
| 730 |
+
step,
|
| 731 |
+
)
|
| 732 |
+
return rows
|
| 733 |
+
|
| 734 |
+
|
| 735 |
+
def predictor_columns(
|
| 736 |
+
name: str,
|
| 737 |
+
within: torch.Tensor,
|
| 738 |
+
cross: torch.Tensor,
|
| 739 |
+
distant: torch.Tensor,
|
| 740 |
+
wrong: torch.Tensor,
|
| 741 |
+
batch: torch.Tensor,
|
| 742 |
+
noise_seed: int,
|
| 743 |
+
) -> list[torch.Tensor]:
|
| 744 |
+
ones = torch.ones_like(within)
|
| 745 |
+
shifted = cross.roll(shifts=max(1, cross.shape[0] // 2), dims=0)
|
| 746 |
+
generator = torch.Generator(device="cpu").manual_seed(noise_seed)
|
| 747 |
+
noise = torch.randn(
|
| 748 |
+
cross.shape, generator=generator, dtype=cross.dtype
|
| 749 |
+
)
|
| 750 |
+
noise = noise * cross.std(dim=0, keepdim=True).clamp_min(1e-6)
|
| 751 |
+
noise = noise + cross.mean(dim=0, keepdim=True)
|
| 752 |
+
mapping = {
|
| 753 |
+
"within_affine": [within, ones],
|
| 754 |
+
"within_quadratic": [within, within.square(), ones],
|
| 755 |
+
"cross_affine": [cross, ones],
|
| 756 |
+
"fusion_same": [within, cross, ones],
|
| 757 |
+
"fusion_distant": [within, distant, ones],
|
| 758 |
+
"fusion_token_shift": [within, shifted, ones],
|
| 759 |
+
"fusion_wrong_step": [within, wrong, ones],
|
| 760 |
+
"fusion_batch_shuffle": [within, batch, ones],
|
| 761 |
+
"fusion_zero": [within, torch.zeros_like(cross), ones],
|
| 762 |
+
"fusion_noise": [within, noise, ones],
|
| 763 |
+
}
|
| 764 |
+
return mapping[name]
|
| 765 |
+
|
| 766 |
+
|
| 767 |
+
PROBE_NAMES = [
|
| 768 |
+
"within_affine",
|
| 769 |
+
"within_quadratic",
|
| 770 |
+
"cross_affine",
|
| 771 |
+
"fusion_same",
|
| 772 |
+
"fusion_distant",
|
| 773 |
+
"fusion_token_shift",
|
| 774 |
+
"fusion_wrong_step",
|
| 775 |
+
"fusion_batch_shuffle",
|
| 776 |
+
"fusion_zero",
|
| 777 |
+
"fusion_noise",
|
| 778 |
+
]
|
| 779 |
+
|
| 780 |
+
|
| 781 |
+
def gather_probe_data(
|
| 782 |
+
runs: list[dict[str, Any]],
|
| 783 |
+
run_indices: list[int],
|
| 784 |
+
stage: str,
|
| 785 |
+
step: int,
|
| 786 |
+
) -> dict[str, torch.Tensor]:
|
| 787 |
+
buckets: dict[str, list[torch.Tensor]] = defaultdict(list)
|
| 788 |
+
for run_index in run_indices:
|
| 789 |
+
run = runs[run_index]
|
| 790 |
+
other = runs[(run_index + 1) % len(runs)]
|
| 791 |
+
chunks = available_chunks(run, stage)
|
| 792 |
+
for chunk in chunks:
|
| 793 |
+
# Use c >= 2 for every probe so correct, distant, and all other
|
| 794 |
+
# controls are evaluated on exactly the same target tokens.
|
| 795 |
+
if chunk < 2:
|
| 796 |
+
continue
|
| 797 |
+
buckets["target"].append(feature(run, stage, chunk, step))
|
| 798 |
+
buckets["within"].append(feature(run, stage, chunk, step - 1))
|
| 799 |
+
buckets["cross"].append(feature(run, stage, chunk - 1, step))
|
| 800 |
+
buckets["distant"].append(feature(run, stage, chunk - 2, step))
|
| 801 |
+
buckets["wrong"].append(feature(run, stage, chunk - 1, step - 1))
|
| 802 |
+
buckets["batch"].append(feature(other, stage, chunk - 1, step))
|
| 803 |
+
if not buckets:
|
| 804 |
+
raise ValueError(f"No probe data for stage={stage}, step={step}")
|
| 805 |
+
return {key: torch.cat(values, dim=0).float() for key, values in buckets.items()}
|
| 806 |
+
|
| 807 |
+
|
| 808 |
+
def fit_channelwise_probe(
|
| 809 |
+
columns: list[torch.Tensor],
|
| 810 |
+
target: torch.Tensor,
|
| 811 |
+
ridge: float,
|
| 812 |
+
) -> torch.Tensor:
|
| 813 |
+
design = torch.stack(columns, dim=-1).double()
|
| 814 |
+
y = target.double()
|
| 815 |
+
gram = torch.einsum("ndp,ndq->dpq", design, design)
|
| 816 |
+
rhs = torch.einsum("ndp,nd->dp", design, y)
|
| 817 |
+
feature_count = gram.shape[-1]
|
| 818 |
+
diagonal_scale = (
|
| 819 |
+
gram.diagonal(dim1=-2, dim2=-1).mean(dim=-1).clamp_min(1e-8)
|
| 820 |
+
)
|
| 821 |
+
regularizer = (
|
| 822 |
+
torch.eye(feature_count, dtype=gram.dtype)[None]
|
| 823 |
+
* (ridge * diagonal_scale)[:, None, None]
|
| 824 |
+
)
|
| 825 |
+
regularizer[:, -1, -1] = 0.0
|
| 826 |
+
try:
|
| 827 |
+
weights = torch.linalg.solve(gram + regularizer, rhs.unsqueeze(-1)).squeeze(-1)
|
| 828 |
+
except torch.linalg.LinAlgError:
|
| 829 |
+
weights = (
|
| 830 |
+
torch.linalg.pinv(gram + regularizer) @ rhs.unsqueeze(-1)
|
| 831 |
+
).squeeze(-1)
|
| 832 |
+
return weights.float()
|
| 833 |
+
|
| 834 |
+
|
| 835 |
+
def apply_channelwise_probe(
|
| 836 |
+
columns: list[torch.Tensor], weights: torch.Tensor
|
| 837 |
+
) -> torch.Tensor:
|
| 838 |
+
design = torch.stack(columns, dim=-1).float()
|
| 839 |
+
return torch.einsum("ndp,dp->nd", design, weights)
|
| 840 |
+
|
| 841 |
+
|
| 842 |
+
def prediction_metrics(prediction: torch.Tensor, target: torch.Tensor) -> dict[str, float]:
|
| 843 |
+
eps = 1e-8
|
| 844 |
+
pred = prediction.float()
|
| 845 |
+
y = target.float()
|
| 846 |
+
error = pred - y
|
| 847 |
+
mse = error.square().mean()
|
| 848 |
+
variance = (y - y.mean()).square().mean()
|
| 849 |
+
nrmse = mse.sqrt() / (variance.sqrt() + eps)
|
| 850 |
+
r2 = 1.0 - mse / (variance + eps)
|
| 851 |
+
cosine = F.cosine_similarity(
|
| 852 |
+
pred.reshape(1, -1), y.reshape(1, -1), dim=1, eps=eps
|
| 853 |
+
)[0]
|
| 854 |
+
return {
|
| 855 |
+
"mse": float(mse),
|
| 856 |
+
"nrmse": float(nrmse),
|
| 857 |
+
"r2": float(r2),
|
| 858 |
+
"cosine": float(cosine),
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
|
| 862 |
+
def run_conditional_probes(
|
| 863 |
+
runs: list[dict[str, Any]], ridge: float
|
| 864 |
+
) -> list[dict[str, Any]]:
|
| 865 |
+
rows: list[dict[str, Any]] = []
|
| 866 |
+
stages = sorted(runs[0]["records"])
|
| 867 |
+
steps = available_steps(runs[0], stages[0])
|
| 868 |
+
for stage in stages:
|
| 869 |
+
for step in steps[1:]:
|
| 870 |
+
for held_out in range(len(runs)):
|
| 871 |
+
train_indices = [index for index in range(len(runs)) if index != held_out]
|
| 872 |
+
train = gather_probe_data(runs, train_indices, stage, step)
|
| 873 |
+
test = gather_probe_data(runs, [held_out], stage, step)
|
| 874 |
+
for probe_name in PROBE_NAMES:
|
| 875 |
+
train_columns = predictor_columns(
|
| 876 |
+
probe_name,
|
| 877 |
+
train["within"],
|
| 878 |
+
train["cross"],
|
| 879 |
+
train["distant"],
|
| 880 |
+
train["wrong"],
|
| 881 |
+
train["batch"],
|
| 882 |
+
noise_seed=1000 + held_out * 100 + step,
|
| 883 |
+
)
|
| 884 |
+
test_columns = predictor_columns(
|
| 885 |
+
probe_name,
|
| 886 |
+
test["within"],
|
| 887 |
+
test["cross"],
|
| 888 |
+
test["distant"],
|
| 889 |
+
test["wrong"],
|
| 890 |
+
test["batch"],
|
| 891 |
+
noise_seed=2000 + held_out * 100 + step,
|
| 892 |
+
)
|
| 893 |
+
weights = fit_channelwise_probe(
|
| 894 |
+
train_columns, train["target"], ridge=ridge
|
| 895 |
+
)
|
| 896 |
+
prediction = apply_channelwise_probe(test_columns, weights)
|
| 897 |
+
rows.append(
|
| 898 |
+
{
|
| 899 |
+
"held_out_run": held_out,
|
| 900 |
+
"stage": stage,
|
| 901 |
+
"step": step,
|
| 902 |
+
"probe": probe_name,
|
| 903 |
+
"train_tokens": int(train["target"].shape[0]),
|
| 904 |
+
"test_tokens": int(test["target"].shape[0]),
|
| 905 |
+
**prediction_metrics(prediction, test["target"]),
|
| 906 |
+
}
|
| 907 |
+
)
|
| 908 |
+
baseline_lookup = {
|
| 909 |
+
(row["held_out_run"], row["stage"], row["step"], row["probe"]): row
|
| 910 |
+
for row in rows
|
| 911 |
+
if row["probe"] in {"within_affine", "within_quadratic"}
|
| 912 |
+
}
|
| 913 |
+
for row in rows:
|
| 914 |
+
key = (row["held_out_run"], row["stage"], row["step"])
|
| 915 |
+
for baseline_name in ("within_affine", "within_quadratic"):
|
| 916 |
+
baseline = baseline_lookup[(*key, baseline_name)]
|
| 917 |
+
row[f"mse_reduction_vs_{baseline_name}"] = (
|
| 918 |
+
baseline["mse"] - row["mse"]
|
| 919 |
+
) / max(baseline["mse"], 1e-12)
|
| 920 |
+
row[f"r2_gain_vs_{baseline_name}"] = row["r2"] - baseline["r2"]
|
| 921 |
+
return rows
|
| 922 |
+
|
| 923 |
+
|
| 924 |
+
def gray(frame: np.ndarray) -> np.ndarray:
|
| 925 |
+
return cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
|
| 926 |
+
|
| 927 |
+
|
| 928 |
+
def farneback(source: np.ndarray, target: np.ndarray) -> np.ndarray:
|
| 929 |
+
return cv2.calcOpticalFlowFarneback(
|
| 930 |
+
gray(source),
|
| 931 |
+
gray(target),
|
| 932 |
+
None,
|
| 933 |
+
pyr_scale=0.5,
|
| 934 |
+
levels=4,
|
| 935 |
+
winsize=21,
|
| 936 |
+
iterations=5,
|
| 937 |
+
poly_n=7,
|
| 938 |
+
poly_sigma=1.5,
|
| 939 |
+
flags=0,
|
| 940 |
+
)
|
| 941 |
+
|
| 942 |
+
|
| 943 |
+
def resize_flow(flow: np.ndarray, height: int, width: int) -> torch.Tensor:
|
| 944 |
+
source_height, source_width = flow.shape[:2]
|
| 945 |
+
resized = cv2.resize(flow, (width, height), interpolation=cv2.INTER_AREA)
|
| 946 |
+
resized[..., 0] *= width / source_width
|
| 947 |
+
resized[..., 1] *= height / source_height
|
| 948 |
+
return torch.from_numpy(resized).permute(2, 0, 1).float()
|
| 949 |
+
|
| 950 |
+
|
| 951 |
+
def warp_feature(
|
| 952 |
+
source: torch.Tensor, target_to_source_flow: torch.Tensor
|
| 953 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 954 |
+
height, width, _ = source.shape
|
| 955 |
+
yy, xx = torch.meshgrid(
|
| 956 |
+
torch.arange(height, dtype=torch.float32),
|
| 957 |
+
torch.arange(width, dtype=torch.float32),
|
| 958 |
+
indexing="ij",
|
| 959 |
+
)
|
| 960 |
+
sample_x = xx + target_to_source_flow[0]
|
| 961 |
+
sample_y = yy + target_to_source_flow[1]
|
| 962 |
+
grid = torch.stack(
|
| 963 |
+
[
|
| 964 |
+
2.0 * sample_x / max(width - 1, 1) - 1.0,
|
| 965 |
+
2.0 * sample_y / max(height - 1, 1) - 1.0,
|
| 966 |
+
],
|
| 967 |
+
dim=-1,
|
| 968 |
+
)[None]
|
| 969 |
+
value = source.permute(2, 0, 1)[None].float()
|
| 970 |
+
warped = F.grid_sample(
|
| 971 |
+
value, grid, mode="bilinear", padding_mode="zeros", align_corners=True
|
| 972 |
+
)[0].permute(1, 2, 0)
|
| 973 |
+
mask = (
|
| 974 |
+
(sample_x >= 0)
|
| 975 |
+
& (sample_x <= width - 1)
|
| 976 |
+
& (sample_y >= 0)
|
| 977 |
+
& (sample_y <= height - 1)
|
| 978 |
+
)
|
| 979 |
+
return warped, mask
|
| 980 |
+
|
| 981 |
+
|
| 982 |
+
def masked_cosine(
|
| 983 |
+
left: torch.Tensor, right: torch.Tensor, mask: torch.Tensor | None = None
|
| 984 |
+
) -> float:
|
| 985 |
+
value = F.cosine_similarity(left.float(), right.float(), dim=-1, eps=1e-8)
|
| 986 |
+
if mask is not None:
|
| 987 |
+
if not bool(mask.any()):
|
| 988 |
+
return float("nan")
|
| 989 |
+
value = value[mask]
|
| 990 |
+
return float(value.mean())
|
| 991 |
+
|
| 992 |
+
|
| 993 |
+
def collect_motion_rows(runs: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 994 |
+
rows: list[dict[str, Any]] = []
|
| 995 |
+
for run_index, run in enumerate(runs):
|
| 996 |
+
anchors = run["anchors"]
|
| 997 |
+
chunk_size = int(run["num_frame_per_block"])
|
| 998 |
+
chunk_count = int(run["num_frames"]) // chunk_size
|
| 999 |
+
steps = sorted(
|
| 1000 |
+
{int(key.split(":")[1]) for key in run["projected"].keys()}
|
| 1001 |
+
)
|
| 1002 |
+
for chunk in range(1, chunk_count):
|
| 1003 |
+
previous_anchor_index = chunk * chunk_size - 1
|
| 1004 |
+
previous_frame = anchors[previous_anchor_index]
|
| 1005 |
+
boundary_flow = farneback(
|
| 1006 |
+
previous_frame, anchors[chunk * chunk_size]
|
| 1007 |
+
)
|
| 1008 |
+
median_flow = np.median(
|
| 1009 |
+
boundary_flow.reshape(-1, 2), axis=0
|
| 1010 |
+
)
|
| 1011 |
+
camera_motion = float(np.linalg.norm(median_flow))
|
| 1012 |
+
residual = boundary_flow - median_flow[None, None]
|
| 1013 |
+
object_motion = float(
|
| 1014 |
+
np.linalg.norm(residual, axis=-1).mean()
|
| 1015 |
+
)
|
| 1016 |
+
total_motion = float(
|
| 1017 |
+
np.linalg.norm(boundary_flow, axis=-1).mean()
|
| 1018 |
+
)
|
| 1019 |
+
|
| 1020 |
+
for step in steps:
|
| 1021 |
+
source_map = projected_feature(run, chunk - 1, step)[-1]
|
| 1022 |
+
target_map = projected_feature(run, chunk, step)
|
| 1023 |
+
same_slot_source = projected_feature(run, chunk - 1, step)
|
| 1024 |
+
per_slot: list[dict[str, float]] = []
|
| 1025 |
+
for slot in range(chunk_size):
|
| 1026 |
+
target = target_map[slot]
|
| 1027 |
+
same_slot = same_slot_source[slot]
|
| 1028 |
+
raw_boundary = source_map
|
| 1029 |
+
target_frame = anchors[chunk * chunk_size + slot]
|
| 1030 |
+
backward = farneback(target_frame, previous_frame)
|
| 1031 |
+
feature_flow = resize_flow(
|
| 1032 |
+
backward, FRAME_TOKEN_HEIGHT, FRAME_TOKEN_WIDTH
|
| 1033 |
+
)
|
| 1034 |
+
global_flow = torch.zeros_like(feature_flow)
|
| 1035 |
+
global_flow[0].fill_(float(np.median(feature_flow[0].numpy())))
|
| 1036 |
+
global_flow[1].fill_(float(np.median(feature_flow[1].numpy())))
|
| 1037 |
+
global_aligned, global_mask = warp_feature(
|
| 1038 |
+
source_map, global_flow
|
| 1039 |
+
)
|
| 1040 |
+
flow_aligned, flow_mask = warp_feature(source_map, feature_flow)
|
| 1041 |
+
per_slot.append(
|
| 1042 |
+
{
|
| 1043 |
+
"same_slot_cosine": masked_cosine(target, same_slot),
|
| 1044 |
+
"boundary_raw_cosine": masked_cosine(
|
| 1045 |
+
target, raw_boundary
|
| 1046 |
+
),
|
| 1047 |
+
"global_aligned_cosine": masked_cosine(
|
| 1048 |
+
target, global_aligned, global_mask
|
| 1049 |
+
),
|
| 1050 |
+
"flow_aligned_cosine": masked_cosine(
|
| 1051 |
+
target, flow_aligned, flow_mask
|
| 1052 |
+
),
|
| 1053 |
+
"valid_flow_ratio": float(flow_mask.float().mean()),
|
| 1054 |
+
}
|
| 1055 |
+
)
|
| 1056 |
+
rows.append(
|
| 1057 |
+
{
|
| 1058 |
+
"run": run_index,
|
| 1059 |
+
"chunk": chunk,
|
| 1060 |
+
"step": step,
|
| 1061 |
+
"total_motion": total_motion,
|
| 1062 |
+
"camera_motion": camera_motion,
|
| 1063 |
+
"object_motion": object_motion,
|
| 1064 |
+
**{
|
| 1065 |
+
key: float(np.nanmean([item[key] for item in per_slot]))
|
| 1066 |
+
for key in per_slot[0]
|
| 1067 |
+
},
|
| 1068 |
+
}
|
| 1069 |
+
)
|
| 1070 |
+
motion_values = np.asarray([row["total_motion"] for row in rows])
|
| 1071 |
+
if len(motion_values) >= 3:
|
| 1072 |
+
low, high = np.quantile(motion_values, [1 / 3, 2 / 3])
|
| 1073 |
+
for row in rows:
|
| 1074 |
+
value = row["total_motion"]
|
| 1075 |
+
row["motion_bin"] = "low" if value <= low else "high" if value > high else "medium"
|
| 1076 |
+
return rows
|
| 1077 |
+
|
| 1078 |
+
|
| 1079 |
+
def group_mean(
|
| 1080 |
+
rows: list[dict[str, Any]], keys: list[str], metrics: list[str]
|
| 1081 |
+
) -> list[dict[str, Any]]:
|
| 1082 |
+
groups: dict[tuple[Any, ...], list[dict[str, Any]]] = defaultdict(list)
|
| 1083 |
+
for row in rows:
|
| 1084 |
+
groups[tuple(row[key] for key in keys)].append(row)
|
| 1085 |
+
result = []
|
| 1086 |
+
for group, values in sorted(groups.items(), key=lambda item: tuple(map(str, item[0]))):
|
| 1087 |
+
output = {key: value for key, value in zip(keys, group)}
|
| 1088 |
+
output["count"] = len(values)
|
| 1089 |
+
for metric in metrics:
|
| 1090 |
+
finite = [
|
| 1091 |
+
float(row[metric])
|
| 1092 |
+
for row in values
|
| 1093 |
+
if metric in row and math.isfinite(float(row[metric]))
|
| 1094 |
+
]
|
| 1095 |
+
output[metric] = float(np.mean(finite)) if finite else float("nan")
|
| 1096 |
+
result.append(output)
|
| 1097 |
+
return result
|
| 1098 |
+
|
| 1099 |
+
|
| 1100 |
+
def paired_probe_reduction(
|
| 1101 |
+
rows: list[dict[str, Any]],
|
| 1102 |
+
stage: str,
|
| 1103 |
+
reference_probe: str,
|
| 1104 |
+
candidate_probe: str = "fusion_same",
|
| 1105 |
+
) -> dict[str, Any]:
|
| 1106 |
+
lookup = {
|
| 1107 |
+
(int(row["held_out_run"]), int(row["step"]), row["probe"]): row
|
| 1108 |
+
for row in rows
|
| 1109 |
+
if row["stage"] == stage
|
| 1110 |
+
and row["probe"] in {reference_probe, candidate_probe}
|
| 1111 |
+
}
|
| 1112 |
+
pairs = sorted(
|
| 1113 |
+
{
|
| 1114 |
+
(held_out, step)
|
| 1115 |
+
for held_out, step, probe in lookup
|
| 1116 |
+
if probe == candidate_probe
|
| 1117 |
+
and (held_out, step, reference_probe) in lookup
|
| 1118 |
+
}
|
| 1119 |
+
)
|
| 1120 |
+
reductions = []
|
| 1121 |
+
for held_out, step in pairs:
|
| 1122 |
+
reference = float(lookup[(held_out, step, reference_probe)]["mse"])
|
| 1123 |
+
candidate = float(lookup[(held_out, step, candidate_probe)]["mse"])
|
| 1124 |
+
reductions.append((reference - candidate) / max(reference, 1e-12))
|
| 1125 |
+
return {
|
| 1126 |
+
"reference": reference_probe,
|
| 1127 |
+
"paired_count": len(reductions),
|
| 1128 |
+
"mean_mse_reduction": (
|
| 1129 |
+
float(np.mean(reductions)) if reductions else float("nan")
|
| 1130 |
+
),
|
| 1131 |
+
"median_mse_reduction": (
|
| 1132 |
+
float(np.median(reductions)) if reductions else float("nan")
|
| 1133 |
+
),
|
| 1134 |
+
"wins": int(sum(value > 0 for value in reductions)),
|
| 1135 |
+
}
|
| 1136 |
+
|
| 1137 |
+
|
| 1138 |
+
def plot_similarity(pair_rows: list[dict[str, Any]], output_dir: Path) -> None:
|
| 1139 |
+
stages = [
|
| 1140 |
+
stage
|
| 1141 |
+
for stage in sorted({row["stage"] for row in pair_rows})
|
| 1142 |
+
if stage.endswith("_hidden")
|
| 1143 |
+
]
|
| 1144 |
+
comparisons = [
|
| 1145 |
+
"within_adjacent",
|
| 1146 |
+
"cross_same",
|
| 1147 |
+
"cross_wrong_step",
|
| 1148 |
+
"cross_distant",
|
| 1149 |
+
"cross_video_shuffle",
|
| 1150 |
+
]
|
| 1151 |
+
steps = sorted({int(row["target_step"]) for row in pair_rows})
|
| 1152 |
+
fig, axes = plt.subplots(
|
| 1153 |
+
len(stages), 2, figsize=(12, max(3.2, 2.8 * len(stages))), squeeze=False
|
| 1154 |
+
)
|
| 1155 |
+
for stage_index, stage in enumerate(stages):
|
| 1156 |
+
for metric_index, metric in enumerate(["cosine", "nmse"]):
|
| 1157 |
+
matrix = np.full((len(comparisons), len(steps)), np.nan)
|
| 1158 |
+
for row_index, comparison in enumerate(comparisons):
|
| 1159 |
+
for col_index, step in enumerate(steps):
|
| 1160 |
+
values = [
|
| 1161 |
+
float(row[metric])
|
| 1162 |
+
for row in pair_rows
|
| 1163 |
+
if row["stage"] == stage
|
| 1164 |
+
and row["comparison"] == comparison
|
| 1165 |
+
and int(row["target_step"]) == step
|
| 1166 |
+
]
|
| 1167 |
+
if values:
|
| 1168 |
+
matrix[row_index, col_index] = np.mean(values)
|
| 1169 |
+
ax = axes[stage_index, metric_index]
|
| 1170 |
+
image = ax.imshow(
|
| 1171 |
+
matrix,
|
| 1172 |
+
aspect="auto",
|
| 1173 |
+
cmap="viridis_r" if metric == "nmse" else "viridis",
|
| 1174 |
+
)
|
| 1175 |
+
ax.set_title(f"{stage}: {metric}")
|
| 1176 |
+
ax.set_xticks(range(len(steps)), labels=steps)
|
| 1177 |
+
ax.set_yticks(range(len(comparisons)), labels=comparisons)
|
| 1178 |
+
ax.set_xlabel("target denoising step")
|
| 1179 |
+
fig.colorbar(image, ax=ax, fraction=0.03)
|
| 1180 |
+
fig.tight_layout()
|
| 1181 |
+
fig.savefig(output_dir / "feature_redundancy_heatmap.png", dpi=180)
|
| 1182 |
+
plt.close(fig)
|
| 1183 |
+
|
| 1184 |
+
|
| 1185 |
+
def plot_probe_gain(probe_rows: list[dict[str, Any]], output_dir: Path) -> None:
|
| 1186 |
+
aggregate = group_mean(
|
| 1187 |
+
probe_rows,
|
| 1188 |
+
["stage", "step", "probe"],
|
| 1189 |
+
["mse", "nrmse", "r2", "mse_reduction_vs_within_quadratic"],
|
| 1190 |
+
)
|
| 1191 |
+
stages = [
|
| 1192 |
+
stage
|
| 1193 |
+
for stage in sorted({row["stage"] for row in aggregate})
|
| 1194 |
+
if stage.endswith("_hidden")
|
| 1195 |
+
]
|
| 1196 |
+
probes = [
|
| 1197 |
+
"fusion_same",
|
| 1198 |
+
"fusion_distant",
|
| 1199 |
+
"fusion_token_shift",
|
| 1200 |
+
"fusion_wrong_step",
|
| 1201 |
+
"fusion_batch_shuffle",
|
| 1202 |
+
]
|
| 1203 |
+
steps = sorted({int(row["step"]) for row in aggregate})
|
| 1204 |
+
fig, axes = plt.subplots(
|
| 1205 |
+
len(stages), 1, figsize=(9, max(3.4, 3.0 * len(stages))), squeeze=False
|
| 1206 |
+
)
|
| 1207 |
+
for stage_index, stage in enumerate(stages):
|
| 1208 |
+
ax = axes[stage_index, 0]
|
| 1209 |
+
for probe in probes:
|
| 1210 |
+
values = []
|
| 1211 |
+
for step in steps:
|
| 1212 |
+
match = [
|
| 1213 |
+
row
|
| 1214 |
+
for row in aggregate
|
| 1215 |
+
if row["stage"] == stage
|
| 1216 |
+
and int(row["step"]) == step
|
| 1217 |
+
and row["probe"] == probe
|
| 1218 |
+
]
|
| 1219 |
+
values.append(
|
| 1220 |
+
match[0]["mse_reduction_vs_within_quadratic"]
|
| 1221 |
+
if match
|
| 1222 |
+
else np.nan
|
| 1223 |
+
)
|
| 1224 |
+
ax.plot(steps, values, marker="o", label=probe)
|
| 1225 |
+
ax.axhline(0, color="black", linewidth=0.8)
|
| 1226 |
+
ax.set_title(stage)
|
| 1227 |
+
ax.set_ylabel("MSE reduction vs within quadratic")
|
| 1228 |
+
ax.set_xlabel("target denoising step")
|
| 1229 |
+
ax.legend(fontsize=8)
|
| 1230 |
+
ax.grid(alpha=0.25)
|
| 1231 |
+
fig.tight_layout()
|
| 1232 |
+
fig.savefig(output_dir / "conditional_chunk_gain.png", dpi=180)
|
| 1233 |
+
plt.close(fig)
|
| 1234 |
+
|
| 1235 |
+
|
| 1236 |
+
def plot_motion(motion_rows: list[dict[str, Any]], output_dir: Path) -> None:
|
| 1237 |
+
fig, axes = plt.subplots(1, 2, figsize=(12, 4.5))
|
| 1238 |
+
axes[0].scatter(
|
| 1239 |
+
[row["total_motion"] for row in motion_rows],
|
| 1240 |
+
[row["boundary_raw_cosine"] for row in motion_rows],
|
| 1241 |
+
s=18,
|
| 1242 |
+
alpha=0.65,
|
| 1243 |
+
label="boundary raw",
|
| 1244 |
+
)
|
| 1245 |
+
axes[0].scatter(
|
| 1246 |
+
[row["total_motion"] for row in motion_rows],
|
| 1247 |
+
[row["flow_aligned_cosine"] for row in motion_rows],
|
| 1248 |
+
s=18,
|
| 1249 |
+
alpha=0.65,
|
| 1250 |
+
label="dense-flow aligned",
|
| 1251 |
+
)
|
| 1252 |
+
axes[0].set_xlabel("optical-flow magnitude")
|
| 1253 |
+
axes[0].set_ylabel("feature cosine")
|
| 1254 |
+
axes[0].legend()
|
| 1255 |
+
axes[0].grid(alpha=0.25)
|
| 1256 |
+
|
| 1257 |
+
aggregate = group_mean(
|
| 1258 |
+
motion_rows,
|
| 1259 |
+
["motion_bin"],
|
| 1260 |
+
[
|
| 1261 |
+
"same_slot_cosine",
|
| 1262 |
+
"boundary_raw_cosine",
|
| 1263 |
+
"global_aligned_cosine",
|
| 1264 |
+
"flow_aligned_cosine",
|
| 1265 |
+
],
|
| 1266 |
+
)
|
| 1267 |
+
bins = ["low", "medium", "high"]
|
| 1268 |
+
metrics = [
|
| 1269 |
+
"same_slot_cosine",
|
| 1270 |
+
"boundary_raw_cosine",
|
| 1271 |
+
"global_aligned_cosine",
|
| 1272 |
+
"flow_aligned_cosine",
|
| 1273 |
+
]
|
| 1274 |
+
width = 0.18
|
| 1275 |
+
x = np.arange(len(bins))
|
| 1276 |
+
for metric_index, metric in enumerate(metrics):
|
| 1277 |
+
values = []
|
| 1278 |
+
for name in bins:
|
| 1279 |
+
match = [row for row in aggregate if row["motion_bin"] == name]
|
| 1280 |
+
values.append(match[0][metric] if match else np.nan)
|
| 1281 |
+
axes[1].bar(
|
| 1282 |
+
x + (metric_index - 1.5) * width,
|
| 1283 |
+
values,
|
| 1284 |
+
width=width,
|
| 1285 |
+
label=metric.replace("_cosine", ""),
|
| 1286 |
+
)
|
| 1287 |
+
axes[1].set_xticks(x, bins)
|
| 1288 |
+
axes[1].set_ylabel("mean feature cosine")
|
| 1289 |
+
axes[1].set_xlabel("motion bin")
|
| 1290 |
+
axes[1].legend(fontsize=8)
|
| 1291 |
+
axes[1].grid(axis="y", alpha=0.25)
|
| 1292 |
+
fig.tight_layout()
|
| 1293 |
+
fig.savefig(output_dir / "motion_alignment_analysis.png", dpi=180)
|
| 1294 |
+
plt.close(fig)
|
| 1295 |
+
|
| 1296 |
+
|
| 1297 |
+
def markdown_table(
|
| 1298 |
+
rows: list[dict[str, Any]], columns: list[str], digits: int = 4
|
| 1299 |
+
) -> str:
|
| 1300 |
+
lines = [
|
| 1301 |
+
"| " + " | ".join(columns) + " |",
|
| 1302 |
+
"|" + "|".join(["---"] * len(columns)) + "|",
|
| 1303 |
+
]
|
| 1304 |
+
for row in rows:
|
| 1305 |
+
values = []
|
| 1306 |
+
for column in columns:
|
| 1307 |
+
value = row.get(column, "")
|
| 1308 |
+
if isinstance(value, float):
|
| 1309 |
+
values.append(f"{value:.{digits}f}")
|
| 1310 |
+
else:
|
| 1311 |
+
values.append(str(value))
|
| 1312 |
+
lines.append("| " + " | ".join(values) + " |")
|
| 1313 |
+
return "\n".join(lines)
|
| 1314 |
+
|
| 1315 |
+
|
| 1316 |
+
def build_report(
|
| 1317 |
+
runs: list[dict[str, Any]],
|
| 1318 |
+
pair_rows: list[dict[str, Any]],
|
| 1319 |
+
probe_rows: list[dict[str, Any]],
|
| 1320 |
+
motion_rows: list[dict[str, Any]],
|
| 1321 |
+
output_dir: Path,
|
| 1322 |
+
) -> None:
|
| 1323 |
+
final_stage = f"block_{runs[0]['projection_layer']}_hidden"
|
| 1324 |
+
pair_summary = group_mean(
|
| 1325 |
+
[
|
| 1326 |
+
row
|
| 1327 |
+
for row in pair_rows
|
| 1328 |
+
if row["stage"] == final_stage
|
| 1329 |
+
],
|
| 1330 |
+
["comparison"],
|
| 1331 |
+
["cosine", "linear_cka", "rel_l2", "nmse", "token_cosine_p10"],
|
| 1332 |
+
)
|
| 1333 |
+
probe_summary = group_mean(
|
| 1334 |
+
[
|
| 1335 |
+
row
|
| 1336 |
+
for row in probe_rows
|
| 1337 |
+
if row["stage"] == final_stage
|
| 1338 |
+
and row["probe"] in set(PROBE_NAMES)
|
| 1339 |
+
],
|
| 1340 |
+
["probe"],
|
| 1341 |
+
[
|
| 1342 |
+
"nrmse",
|
| 1343 |
+
"r2",
|
| 1344 |
+
"mse_reduction_vs_within_affine",
|
| 1345 |
+
"mse_reduction_vs_within_quadratic",
|
| 1346 |
+
],
|
| 1347 |
+
)
|
| 1348 |
+
motion_summary = group_mean(
|
| 1349 |
+
motion_rows,
|
| 1350 |
+
["motion_bin"],
|
| 1351 |
+
[
|
| 1352 |
+
"total_motion",
|
| 1353 |
+
"same_slot_cosine",
|
| 1354 |
+
"boundary_raw_cosine",
|
| 1355 |
+
"global_aligned_cosine",
|
| 1356 |
+
"flow_aligned_cosine",
|
| 1357 |
+
],
|
| 1358 |
+
)
|
| 1359 |
+
control_order = [
|
| 1360 |
+
"within_affine",
|
| 1361 |
+
"within_quadratic",
|
| 1362 |
+
"fusion_distant",
|
| 1363 |
+
"fusion_wrong_step",
|
| 1364 |
+
"fusion_token_shift",
|
| 1365 |
+
"fusion_batch_shuffle",
|
| 1366 |
+
"fusion_zero",
|
| 1367 |
+
"fusion_noise",
|
| 1368 |
+
]
|
| 1369 |
+
control_summary = [
|
| 1370 |
+
paired_probe_reduction(
|
| 1371 |
+
probe_rows,
|
| 1372 |
+
stage=final_stage,
|
| 1373 |
+
reference_probe=control,
|
| 1374 |
+
)
|
| 1375 |
+
for control in control_order
|
| 1376 |
+
]
|
| 1377 |
+
control_lookup = {row["reference"]: row for row in control_summary}
|
| 1378 |
+
affine = control_lookup["within_affine"]
|
| 1379 |
+
shuffled = control_lookup["fusion_batch_shuffle"]
|
| 1380 |
+
conditional_statement = (
|
| 1381 |
+
f"在主层 `{final_stage}` 上,加入正确的前一 chunk 同 timestep 特征,"
|
| 1382 |
+
f"相对 within-only affine probe 平均降低 held-out MSE "
|
| 1383 |
+
f"{100*affine['mean_mse_reduction']:.2f}%,并在 "
|
| 1384 |
+
f"{affine['wins']}/{affine['paired_count']} 个 "
|
| 1385 |
+
f"prompt–timestep 配对中取得改善。相对参数量一致的跨视频 "
|
| 1386 |
+
f"shuffle 对照,MSE 平均降低 "
|
| 1387 |
+
f"{100*shuffled['mean_mse_reduction']:.2f}%。"
|
| 1388 |
+
)
|
| 1389 |
+
|
| 1390 |
+
pair_lookup = {row["comparison"]: row for row in pair_summary}
|
| 1391 |
+
same_pair = pair_lookup.get("cross_same")
|
| 1392 |
+
random_pair = pair_lookup.get("cross_video_shuffle")
|
| 1393 |
+
pair_statement = ""
|
| 1394 |
+
if same_pair is not None and random_pair is not None:
|
| 1395 |
+
pair_statement = (
|
| 1396 |
+
f"正确相邻 chunk 的平均 cosine/CKA 为 "
|
| 1397 |
+
f"{same_pair['cosine']:.4f}/{same_pair['linear_cka']:.4f},"
|
| 1398 |
+
f"跨视频 shuffle 为 "
|
| 1399 |
+
f"{random_pair['cosine']:.4f}/{random_pair['linear_cka']:.4f}。"
|
| 1400 |
+
)
|
| 1401 |
+
|
| 1402 |
+
total_motion = np.asarray(
|
| 1403 |
+
[float(row["total_motion"]) for row in motion_rows], dtype=np.float64
|
| 1404 |
+
)
|
| 1405 |
+
raw_cosine = np.asarray(
|
| 1406 |
+
[float(row["boundary_raw_cosine"]) for row in motion_rows],
|
| 1407 |
+
dtype=np.float64,
|
| 1408 |
+
)
|
| 1409 |
+
flow_cosine = np.asarray(
|
| 1410 |
+
[float(row["flow_aligned_cosine"]) for row in motion_rows],
|
| 1411 |
+
dtype=np.float64,
|
| 1412 |
+
)
|
| 1413 |
+
motion_correlation = (
|
| 1414 |
+
float(np.corrcoef(total_motion, raw_cosine)[0, 1])
|
| 1415 |
+
if len(motion_rows) > 1
|
| 1416 |
+
else float("nan")
|
| 1417 |
+
)
|
| 1418 |
+
flow_gain = float(np.mean(flow_cosine - raw_cosine))
|
| 1419 |
+
flow_wins = int(np.sum(flow_cosine > raw_cosine))
|
| 1420 |
+
motion_statement = (
|
| 1421 |
+
f"运动强度与未对齐跨 chunk cosine 的 Pearson 相关系数为 "
|
| 1422 |
+
f"{motion_correlation:.3f};dense-flow 对齐平均恢复 "
|
| 1423 |
+
f"{flow_gain:.4f} cosine,并在 {flow_wins}/{len(motion_rows)} "
|
| 1424 |
+
f"个 chunk–timestep 样本上改善。"
|
| 1425 |
+
)
|
| 1426 |
+
|
| 1427 |
+
report = f"""# Self-Forcing Feature Cache 分析结果
|
| 1428 |
+
|
| 1429 |
+
## 实验配置
|
| 1430 |
+
|
| 1431 |
+
- Prompts:{len(runs)}
|
| 1432 |
+
- 每条视频 latent frames:{runs[0]['num_frames']}
|
| 1433 |
+
- 每个 AR chunk latent frames:{runs[0]['num_frame_per_block']}
|
| 1434 |
+
- Denoising timesteps:{runs[0]['timesteps']}
|
| 1435 |
+
- Hook layers:{runs[0]['layers']}
|
| 1436 |
+
- 主分析 stage:`{final_stage}`
|
| 1437 |
+
|
| 1438 |
+
## 核心观察
|
| 1439 |
+
|
| 1440 |
+
{conditional_statement}
|
| 1441 |
+
|
| 1442 |
+
{pair_statement}
|
| 1443 |
+
|
| 1444 |
+
{motion_statement}
|
| 1445 |
+
|
| 1446 |
+
以下结果是 3 条 prompt 的 pilot 分析。条件 probe 按 prompt 留一测试,
|
| 1447 |
+
统计单位是 held-out prompt 与 timestep;不能将 token 数量解释为独立视频样本数,
|
| 1448 |
+
也不能据此声称数据集级统计显著性。
|
| 1449 |
+
|
| 1450 |
+
## 主层特征配对
|
| 1451 |
+
|
| 1452 |
+
{markdown_table(pair_summary, ['comparison', 'count', 'cosine', 'linear_cka', 'rel_l2', 'nmse', 'token_cosine_p10'])}
|
| 1453 |
+
|
| 1454 |
+
## 条件 Probe
|
| 1455 |
+
|
| 1456 |
+
Probe 仅使用 chunk index `c ≥ 2` 的目标 chunk,使 `correct`、`c-2 distant`
|
| 1457 |
+
及其他控制组在完全相同的 token 上比较。
|
| 1458 |
+
|
| 1459 |
+
{markdown_table(probe_summary, ['probe', 'count', 'nrmse', 'r2', 'mse_reduction_vs_within_affine', 'mse_reduction_vs_within_quadratic'])}
|
| 1460 |
+
|
| 1461 |
+
### `fusion_same` 的成对控制实验
|
| 1462 |
+
|
| 1463 |
+
正值表示正确前一 chunk 同 timestep 输入的 MSE 更低。
|
| 1464 |
+
|
| 1465 |
+
{markdown_table(control_summary, ['reference', 'paired_count', 'mean_mse_reduction', 'median_mse_reduction', 'wins'])}
|
| 1466 |
+
|
| 1467 |
+
## 运动与对齐
|
| 1468 |
+
|
| 1469 |
+
{markdown_table(motion_summary, ['motion_bin', 'count', 'total_motion', 'same_slot_cosine', 'boundary_raw_cosine', 'global_aligned_cosine', 'flow_aligned_cosine'])}
|
| 1470 |
+
|
| 1471 |
+
这里的 `global_aligned` 是由光流中位数估计的全局平移对齐,
|
| 1472 |
+
`flow_aligned` 是 dense optical-flow oracle;本轮未实现 homography。
|
| 1473 |
+
|
| 1474 |
+
## 本轮结论边界
|
| 1475 |
+
|
| 1476 |
+
- 已完成:四步 DMD 模型的 hidden/residual-delta 特征采集、相似度与
|
| 1477 |
+
nMSE/CKA、linear/ridge 条件 probe、负对照、运动分桶和光流对齐。
|
| 1478 |
+
- 尚未完成:接入用户现有的小型非线性预测网络、真实 cache 替换干预、
|
| 1479 |
+
最终视频质量与端到端加速评估、规模化多视频置信区间。
|
| 1480 |
+
- 因而当前结果支持“前一 chunk 提供额外且具有空间对应性的条件信息”,
|
| 1481 |
+
但还不能单独证明最终生成质量或真实加速收益。
|
| 1482 |
+
|
| 1483 |
+
## 输出文件
|
| 1484 |
+
|
| 1485 |
+
- `feature_pair_metrics.csv`
|
| 1486 |
+
- `feature_pair_summary.csv`
|
| 1487 |
+
- `conditional_probe_folds.csv`
|
| 1488 |
+
- `conditional_probe_summary.csv`
|
| 1489 |
+
- `motion_alignment_metrics.csv`
|
| 1490 |
+
- `motion_alignment_summary.csv`
|
| 1491 |
+
- `feature_redundancy_heatmap.png`
|
| 1492 |
+
- `conditional_chunk_gain.png`
|
| 1493 |
+
- `motion_alignment_analysis.png`
|
| 1494 |
+
"""
|
| 1495 |
+
(output_dir / "REPORT.md").write_text(report, encoding="utf-8")
|
| 1496 |
+
|
| 1497 |
+
|
| 1498 |
+
def analyze(args: argparse.Namespace, paths: list[Path]) -> None:
|
| 1499 |
+
print("[analysis] loading snapshots", flush=True)
|
| 1500 |
+
runs = load_runs(paths)
|
| 1501 |
+
pair_rows = collect_pair_rows(runs, cosine_only=args.cosine_only)
|
| 1502 |
+
if args.cosine_only:
|
| 1503 |
+
pair_summary = group_mean(
|
| 1504 |
+
pair_rows,
|
| 1505 |
+
["comparison", "stage", "target_step"],
|
| 1506 |
+
[
|
| 1507 |
+
"cosine",
|
| 1508 |
+
"centered_cosine",
|
| 1509 |
+
"linear_cka",
|
| 1510 |
+
"rel_l2",
|
| 1511 |
+
"nmse",
|
| 1512 |
+
"token_cosine_mean",
|
| 1513 |
+
"token_cosine_p10",
|
| 1514 |
+
"token_cosine_p50",
|
| 1515 |
+
"token_cosine_p90",
|
| 1516 |
+
],
|
| 1517 |
+
)
|
| 1518 |
+
write_csv(args.output_dir / "feature_pair_metrics.csv", pair_rows)
|
| 1519 |
+
write_csv(args.output_dir / "feature_pair_summary.csv", pair_summary)
|
| 1520 |
+
plot_similarity(pair_rows, args.output_dir)
|
| 1521 |
+
summary = {
|
| 1522 |
+
"runs": len(runs),
|
| 1523 |
+
"pair_rows": len(pair_rows),
|
| 1524 |
+
"cosine_only": True,
|
| 1525 |
+
}
|
| 1526 |
+
(args.output_dir / "summary.json").write_text(
|
| 1527 |
+
json.dumps(summary, indent=2) + "\n", encoding="utf-8"
|
| 1528 |
+
)
|
| 1529 |
+
print(f"[analysis] cosine-only complete: {args.output_dir}", flush=True)
|
| 1530 |
+
return
|
| 1531 |
+
probe_rows = run_conditional_probes(runs, ridge=args.ridge)
|
| 1532 |
+
motion_rows = collect_motion_rows(runs)
|
| 1533 |
+
|
| 1534 |
+
pair_summary = group_mean(
|
| 1535 |
+
pair_rows,
|
| 1536 |
+
["comparison", "stage", "target_step"],
|
| 1537 |
+
[
|
| 1538 |
+
"cosine",
|
| 1539 |
+
"centered_cosine",
|
| 1540 |
+
"linear_cka",
|
| 1541 |
+
"rel_l2",
|
| 1542 |
+
"nmse",
|
| 1543 |
+
"token_cosine_mean",
|
| 1544 |
+
"token_cosine_p10",
|
| 1545 |
+
"token_cosine_p50",
|
| 1546 |
+
"token_cosine_p90",
|
| 1547 |
+
],
|
| 1548 |
+
)
|
| 1549 |
+
probe_summary = group_mean(
|
| 1550 |
+
probe_rows,
|
| 1551 |
+
["stage", "step", "probe"],
|
| 1552 |
+
[
|
| 1553 |
+
"mse",
|
| 1554 |
+
"nrmse",
|
| 1555 |
+
"r2",
|
| 1556 |
+
"cosine",
|
| 1557 |
+
"mse_reduction_vs_within_affine",
|
| 1558 |
+
"mse_reduction_vs_within_quadratic",
|
| 1559 |
+
"r2_gain_vs_within_affine",
|
| 1560 |
+
"r2_gain_vs_within_quadratic",
|
| 1561 |
+
],
|
| 1562 |
+
)
|
| 1563 |
+
motion_summary = group_mean(
|
| 1564 |
+
motion_rows,
|
| 1565 |
+
["motion_bin", "step"],
|
| 1566 |
+
[
|
| 1567 |
+
"total_motion",
|
| 1568 |
+
"camera_motion",
|
| 1569 |
+
"object_motion",
|
| 1570 |
+
"same_slot_cosine",
|
| 1571 |
+
"boundary_raw_cosine",
|
| 1572 |
+
"global_aligned_cosine",
|
| 1573 |
+
"flow_aligned_cosine",
|
| 1574 |
+
"valid_flow_ratio",
|
| 1575 |
+
],
|
| 1576 |
+
)
|
| 1577 |
+
|
| 1578 |
+
write_csv(args.output_dir / "feature_pair_metrics.csv", pair_rows)
|
| 1579 |
+
write_csv(args.output_dir / "feature_pair_summary.csv", pair_summary)
|
| 1580 |
+
write_csv(args.output_dir / "conditional_probe_folds.csv", probe_rows)
|
| 1581 |
+
write_csv(args.output_dir / "conditional_probe_summary.csv", probe_summary)
|
| 1582 |
+
write_csv(args.output_dir / "motion_alignment_metrics.csv", motion_rows)
|
| 1583 |
+
write_csv(args.output_dir / "motion_alignment_summary.csv", motion_summary)
|
| 1584 |
+
plot_similarity(pair_rows, args.output_dir)
|
| 1585 |
+
plot_probe_gain(probe_rows, args.output_dir)
|
| 1586 |
+
plot_motion(motion_rows, args.output_dir)
|
| 1587 |
+
build_report(runs, pair_rows, probe_rows, motion_rows, args.output_dir)
|
| 1588 |
+
summary = {
|
| 1589 |
+
"runs": len(runs),
|
| 1590 |
+
"pair_rows": len(pair_rows),
|
| 1591 |
+
"probe_rows": len(probe_rows),
|
| 1592 |
+
"motion_rows": len(motion_rows),
|
| 1593 |
+
"report": str(args.output_dir / "REPORT.md"),
|
| 1594 |
+
}
|
| 1595 |
+
(args.output_dir / "summary.json").write_text(
|
| 1596 |
+
json.dumps(summary, indent=2) + "\n", encoding="utf-8"
|
| 1597 |
+
)
|
| 1598 |
+
print(f"[analysis] complete: {args.output_dir / 'REPORT.md'}", flush=True)
|
| 1599 |
+
|
| 1600 |
+
|
| 1601 |
+
def main() -> None:
|
| 1602 |
+
args = parse_args()
|
| 1603 |
+
args.config_path = resolve_path(args.config_path)
|
| 1604 |
+
args.checkpoint_path = resolve_path(args.checkpoint_path)
|
| 1605 |
+
args.prompt_path = resolve_path(args.prompt_path)
|
| 1606 |
+
args.output_dir = resolve_path(args.output_dir)
|
| 1607 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 1608 |
+
random.seed(args.seed)
|
| 1609 |
+
np.random.seed(args.seed)
|
| 1610 |
+
torch.manual_seed(args.seed)
|
| 1611 |
+
torch.set_grad_enabled(False)
|
| 1612 |
+
|
| 1613 |
+
run_paths = [
|
| 1614 |
+
args.output_dir / "runs" / f"prompt_{index:02d}.pt"
|
| 1615 |
+
for index in range(args.num_prompts)
|
| 1616 |
+
]
|
| 1617 |
+
if not args.analysis_only:
|
| 1618 |
+
run_paths = generate_snapshots(args)
|
| 1619 |
+
analyze(args, run_paths)
|
| 1620 |
+
|
| 1621 |
+
|
| 1622 |
+
if __name__ == "__main__":
|
| 1623 |
+
main()
|
scripts/analyze_fullgrid_bilinear_3models.py
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Full-grid bilinear motion alignment for the three four-step backbones.
|
| 3 |
+
|
| 4 |
+
This intentionally follows the original Self-Forcing pilot: compare every
|
| 5 |
+
current-chunk temporal slot with the previous chunk's boundary feature map and
|
| 6 |
+
warp the source map with continuous target-to-source flow using bilinear
|
| 7 |
+
``grid_sample``. Correct, global, negated, and spatially shuffled flow fields
|
| 8 |
+
are evaluated with identical interpolation and in-bounds masking.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import csv
|
| 15 |
+
import json
|
| 16 |
+
import math
|
| 17 |
+
from collections import defaultdict
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Any, Iterable
|
| 20 |
+
|
| 21 |
+
import cv2
|
| 22 |
+
import matplotlib
|
| 23 |
+
|
| 24 |
+
matplotlib.use("Agg")
|
| 25 |
+
import matplotlib.pyplot as plt
|
| 26 |
+
import numpy as np
|
| 27 |
+
import torch
|
| 28 |
+
import torch.nn.functional as F
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
GRID_H, GRID_W = 30, 52
|
| 32 |
+
PROJECTION_SEED_BASE = 20260728
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def parse_args() -> argparse.Namespace:
|
| 36 |
+
parser = argparse.ArgumentParser()
|
| 37 |
+
parser.add_argument("--self_root", type=Path, required=True)
|
| 38 |
+
parser.add_argument("--causal_root", type=Path, required=True)
|
| 39 |
+
parser.add_argument("--hy_root", type=Path, required=True)
|
| 40 |
+
parser.add_argument("--hy_right_root", type=Path, required=True)
|
| 41 |
+
parser.add_argument("--output_root", type=Path, required=True)
|
| 42 |
+
parser.add_argument("--projection_dim", type=int, default=64)
|
| 43 |
+
parser.add_argument("--projection_device", default="cpu")
|
| 44 |
+
parser.add_argument("--overwrite_projection_cache", action="store_true")
|
| 45 |
+
return parser.parse_args()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def mean(values: Iterable[float]) -> float:
|
| 49 |
+
values = [float(value) for value in values if np.isfinite(value)]
|
| 50 |
+
return float(np.mean(values)) if values else float("nan")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
|
| 54 |
+
if not rows:
|
| 55 |
+
return
|
| 56 |
+
fields: list[str] = []
|
| 57 |
+
for row in rows:
|
| 58 |
+
for key in row:
|
| 59 |
+
if key not in fields:
|
| 60 |
+
fields.append(key)
|
| 61 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 62 |
+
with path.open("w", newline="", encoding="utf-8") as handle:
|
| 63 |
+
writer = csv.DictWriter(handle, fieldnames=fields)
|
| 64 |
+
writer.writeheader()
|
| 65 |
+
writer.writerows(rows)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def projection_matrix(dim: int, output_dim: int, device: torch.device) -> torch.Tensor:
|
| 69 |
+
generator = torch.Generator(device="cpu").manual_seed(PROJECTION_SEED_BASE + dim)
|
| 70 |
+
signs = torch.randint(0, 2, (dim, output_dim), generator=generator, dtype=torch.int8)
|
| 71 |
+
return signs.float().mul_(2).sub_(1).div_(math.sqrt(output_dim)).to(device)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def farneback(source: np.ndarray, target: np.ndarray) -> np.ndarray:
|
| 75 |
+
def gray(frame: np.ndarray) -> np.ndarray:
|
| 76 |
+
if frame.dtype != np.uint8:
|
| 77 |
+
frame = np.uint8(np.clip(np.round(frame * 255.0), 0, 255))
|
| 78 |
+
return cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
|
| 79 |
+
|
| 80 |
+
return cv2.calcOpticalFlowFarneback(
|
| 81 |
+
gray(source),
|
| 82 |
+
gray(target),
|
| 83 |
+
None,
|
| 84 |
+
pyr_scale=0.5,
|
| 85 |
+
levels=4,
|
| 86 |
+
winsize=21,
|
| 87 |
+
iterations=5,
|
| 88 |
+
poly_n=7,
|
| 89 |
+
poly_sigma=1.5,
|
| 90 |
+
flags=0,
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def resize_flow(flow: np.ndarray) -> torch.Tensor:
|
| 95 |
+
source_h, source_w = flow.shape[:2]
|
| 96 |
+
resized = cv2.resize(flow, (GRID_W, GRID_H), interpolation=cv2.INTER_AREA)
|
| 97 |
+
resized[..., 0] *= GRID_W / source_w
|
| 98 |
+
resized[..., 1] *= GRID_H / source_h
|
| 99 |
+
return torch.from_numpy(resized).permute(2, 0, 1).float()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def warp(source: torch.Tensor, flow: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 103 |
+
yy, xx = torch.meshgrid(
|
| 104 |
+
torch.arange(GRID_H, dtype=torch.float32),
|
| 105 |
+
torch.arange(GRID_W, dtype=torch.float32),
|
| 106 |
+
indexing="ij",
|
| 107 |
+
)
|
| 108 |
+
sample_x = xx + flow[0]
|
| 109 |
+
sample_y = yy + flow[1]
|
| 110 |
+
grid = torch.stack(
|
| 111 |
+
[
|
| 112 |
+
2.0 * sample_x / (GRID_W - 1) - 1.0,
|
| 113 |
+
2.0 * sample_y / (GRID_H - 1) - 1.0,
|
| 114 |
+
],
|
| 115 |
+
dim=-1,
|
| 116 |
+
)[None]
|
| 117 |
+
value = source.permute(2, 0, 1)[None].float()
|
| 118 |
+
warped = F.grid_sample(
|
| 119 |
+
value,
|
| 120 |
+
grid,
|
| 121 |
+
mode="bilinear",
|
| 122 |
+
padding_mode="zeros",
|
| 123 |
+
align_corners=True,
|
| 124 |
+
)[0].permute(1, 2, 0)
|
| 125 |
+
mask = (
|
| 126 |
+
(sample_x >= 0)
|
| 127 |
+
& (sample_x <= GRID_W - 1)
|
| 128 |
+
& (sample_y >= 0)
|
| 129 |
+
& (sample_y <= GRID_H - 1)
|
| 130 |
+
)
|
| 131 |
+
return warped, mask
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def cosine(target: torch.Tensor, source: torch.Tensor, mask: torch.Tensor | None = None) -> float:
|
| 135 |
+
values = F.cosine_similarity(target.float(), source.float(), dim=-1, eps=1e-8)
|
| 136 |
+
if mask is not None:
|
| 137 |
+
values = values[mask]
|
| 138 |
+
return float(values.mean()) if values.numel() else float("nan")
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class GridRun:
|
| 142 |
+
def __init__(
|
| 143 |
+
self,
|
| 144 |
+
model: str,
|
| 145 |
+
action: str,
|
| 146 |
+
prompt_id: int,
|
| 147 |
+
anchors: np.ndarray,
|
| 148 |
+
chunk_size: int,
|
| 149 |
+
features: dict[tuple[int, int], torch.Tensor],
|
| 150 |
+
source: Path,
|
| 151 |
+
):
|
| 152 |
+
self.model = model
|
| 153 |
+
self.action = action
|
| 154 |
+
self.prompt_id = int(prompt_id)
|
| 155 |
+
self.anchors = anchors.astype(np.uint8)
|
| 156 |
+
self.chunk_size = int(chunk_size)
|
| 157 |
+
self.features = features
|
| 158 |
+
self.source = source
|
| 159 |
+
self.chunks = max(chunk for chunk, _ in features) + 1
|
| 160 |
+
self.steps = sorted({step for _, step in features})
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def load_self_runs(root: Path) -> list[GridRun]:
|
| 164 |
+
runs: list[GridRun] = []
|
| 165 |
+
for path in sorted((root / "runs").glob("prompt_*.pt")):
|
| 166 |
+
state = torch.load(path, map_location="cpu", weights_only=False)
|
| 167 |
+
features = {
|
| 168 |
+
tuple(int(value) for value in key.split(":")): tensor.float()
|
| 169 |
+
for key, tensor in state["projected"].items()
|
| 170 |
+
}
|
| 171 |
+
anchors = np.load(path.with_suffix(".anchors.npz"), allow_pickle=False)["frames"]
|
| 172 |
+
runs.append(
|
| 173 |
+
GridRun(
|
| 174 |
+
"self_forcing",
|
| 175 |
+
"none",
|
| 176 |
+
int(state.get("run_index", len(runs))),
|
| 177 |
+
anchors,
|
| 178 |
+
int(state["num_frame_per_block"]),
|
| 179 |
+
features,
|
| 180 |
+
path,
|
| 181 |
+
)
|
| 182 |
+
)
|
| 183 |
+
del state
|
| 184 |
+
return runs
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def load_causal_runs(root: Path) -> list[GridRun]:
|
| 188 |
+
runs: list[GridRun] = []
|
| 189 |
+
for run_dir in sorted((root / "runs").glob("prompt_*")):
|
| 190 |
+
path = run_dir / "feature_snapshots.pt"
|
| 191 |
+
anchor_path = run_dir / "rgb_anchor_frames.npz"
|
| 192 |
+
if not path.exists() or not anchor_path.exists():
|
| 193 |
+
continue
|
| 194 |
+
state = torch.load(path, map_location="cpu", weights_only=False)
|
| 195 |
+
projected = state.get("projected", {})
|
| 196 |
+
if not projected:
|
| 197 |
+
raise ValueError(f"No full-grid projected features in {path}")
|
| 198 |
+
features: dict[tuple[int, int], torch.Tensor] = {}
|
| 199 |
+
for key, tensor in projected.items():
|
| 200 |
+
layer, chunk, step = (int(value) for value in key.split(":"))
|
| 201 |
+
if layer == max(state["layers"]):
|
| 202 |
+
features[(chunk, step)] = tensor.float()
|
| 203 |
+
anchors = np.load(anchor_path, allow_pickle=False)["frames"]
|
| 204 |
+
runs.append(
|
| 205 |
+
GridRun(
|
| 206 |
+
"causal_forcing",
|
| 207 |
+
"none",
|
| 208 |
+
int(state["prompt_id"]),
|
| 209 |
+
anchors,
|
| 210 |
+
3,
|
| 211 |
+
features,
|
| 212 |
+
path,
|
| 213 |
+
)
|
| 214 |
+
)
|
| 215 |
+
del state
|
| 216 |
+
return runs
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def project_hy_run(
|
| 220 |
+
snapshot_path: Path,
|
| 221 |
+
cache_path: Path,
|
| 222 |
+
projection_dim: int,
|
| 223 |
+
device: torch.device,
|
| 224 |
+
overwrite: bool,
|
| 225 |
+
) -> dict[tuple[int, int], torch.Tensor]:
|
| 226 |
+
if cache_path.exists() and not overwrite:
|
| 227 |
+
state = torch.load(cache_path, map_location="cpu", weights_only=False)
|
| 228 |
+
return {
|
| 229 |
+
tuple(int(value) for value in key.split(":")): tensor.float()
|
| 230 |
+
for key, tensor in state["features"].items()
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
data = np.load(snapshot_path, allow_pickle=False)
|
| 234 |
+
stages = data["stages"].astype(str)
|
| 235 |
+
chunks = data["chunks"].astype(int)
|
| 236 |
+
steps = data["steps"].astype(int)
|
| 237 |
+
coords = data["coords"].astype(int)
|
| 238 |
+
expected_coords = np.stack(
|
| 239 |
+
np.meshgrid(np.arange(4), np.arange(GRID_H), np.arange(GRID_W), indexing="ij"),
|
| 240 |
+
axis=-1,
|
| 241 |
+
).reshape(-1, 3)
|
| 242 |
+
if coords.shape != expected_coords.shape or not np.array_equal(coords, expected_coords):
|
| 243 |
+
raise ValueError(f"Unexpected HY coordinate order in {snapshot_path}")
|
| 244 |
+
feature_array = data["features"]
|
| 245 |
+
projection = projection_matrix(int(feature_array.shape[-1]), projection_dim, device)
|
| 246 |
+
features: dict[tuple[int, int], torch.Tensor] = {}
|
| 247 |
+
selected = np.flatnonzero(stages == "block_53")
|
| 248 |
+
for position, index in enumerate(selected):
|
| 249 |
+
value = torch.from_numpy(np.asarray(feature_array[index])).to(device=device, dtype=torch.float32)
|
| 250 |
+
value = torch.matmul(value, projection).reshape(4, GRID_H, GRID_W, projection_dim)
|
| 251 |
+
features[(int(chunks[index]), int(steps[index]))] = value.to("cpu", torch.float16)
|
| 252 |
+
if position % 4 == 3:
|
| 253 |
+
print(f"[HY projection] {snapshot_path.parent.name}: {position + 1}/{len(selected)}", flush=True)
|
| 254 |
+
data.close()
|
| 255 |
+
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
| 256 |
+
torch.save(
|
| 257 |
+
{
|
| 258 |
+
"source": str(snapshot_path),
|
| 259 |
+
"projection_dim": projection_dim,
|
| 260 |
+
"features": {f"{chunk}:{step}": value for (chunk, step), value in features.items()},
|
| 261 |
+
},
|
| 262 |
+
cache_path,
|
| 263 |
+
)
|
| 264 |
+
return {key: value.float() for key, value in features.items()}
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def load_hy_runs(
|
| 268 |
+
root: Path,
|
| 269 |
+
action: str,
|
| 270 |
+
cache_root: Path,
|
| 271 |
+
projection_dim: int,
|
| 272 |
+
device: torch.device,
|
| 273 |
+
overwrite: bool,
|
| 274 |
+
) -> list[GridRun]:
|
| 275 |
+
runs: list[GridRun] = []
|
| 276 |
+
for case_dir in sorted((root / "runs").glob("prompt_*")):
|
| 277 |
+
run_dir = case_dir / action
|
| 278 |
+
snapshot = run_dir / "dense_selected_snapshots.npz"
|
| 279 |
+
anchor_path = run_dir / "rgb_anchor_frames.npz"
|
| 280 |
+
if not snapshot.exists() or not anchor_path.exists():
|
| 281 |
+
continue
|
| 282 |
+
prompt_id = int(case_dir.name.split("_")[-1])
|
| 283 |
+
cache_path = cache_root / action / f"prompt_{prompt_id:04d}.pt"
|
| 284 |
+
features = project_hy_run(snapshot, cache_path, projection_dim, device, overwrite)
|
| 285 |
+
anchors = np.load(anchor_path, allow_pickle=False)["frames"]
|
| 286 |
+
runs.append(
|
| 287 |
+
GridRun(
|
| 288 |
+
"hy_worldplay",
|
| 289 |
+
action,
|
| 290 |
+
prompt_id,
|
| 291 |
+
anchors,
|
| 292 |
+
4,
|
| 293 |
+
features,
|
| 294 |
+
run_dir,
|
| 295 |
+
)
|
| 296 |
+
)
|
| 297 |
+
return runs
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
def shuffled_flow(flow: torch.Tensor, seed: int) -> torch.Tensor:
|
| 301 |
+
generator = torch.Generator(device="cpu").manual_seed(seed)
|
| 302 |
+
permutation = torch.randperm(GRID_H * GRID_W, generator=generator)
|
| 303 |
+
return flow.reshape(2, -1)[:, permutation].reshape_as(flow)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def collect_rows(runs: list[GridRun]) -> list[dict[str, Any]]:
|
| 307 |
+
rows: list[dict[str, Any]] = []
|
| 308 |
+
for run_index, run in enumerate(runs):
|
| 309 |
+
for chunk in range(1, run.chunks):
|
| 310 |
+
source_frame_index = chunk * run.chunk_size - 1
|
| 311 |
+
source_frame = run.anchors[source_frame_index]
|
| 312 |
+
boundary_flow = farneback(source_frame, run.anchors[chunk * run.chunk_size])
|
| 313 |
+
median = np.median(boundary_flow.reshape(-1, 2), axis=0)
|
| 314 |
+
residual = boundary_flow - median[None, None]
|
| 315 |
+
motion = {
|
| 316 |
+
"total_motion": float(np.linalg.norm(boundary_flow, axis=-1).mean()),
|
| 317 |
+
"camera_motion": float(np.linalg.norm(median)),
|
| 318 |
+
"object_motion": float(np.linalg.norm(residual, axis=-1).mean()),
|
| 319 |
+
}
|
| 320 |
+
for step in run.steps:
|
| 321 |
+
source_map = run.features[(chunk - 1, step)][-1].float()
|
| 322 |
+
target_maps = run.features[(chunk, step)].float()
|
| 323 |
+
slot_rows: list[dict[str, float]] = []
|
| 324 |
+
for slot in range(run.chunk_size):
|
| 325 |
+
target = target_maps[slot]
|
| 326 |
+
target_frame = run.anchors[chunk * run.chunk_size + slot]
|
| 327 |
+
flow = resize_flow(farneback(target_frame, source_frame))
|
| 328 |
+
global_flow = torch.zeros_like(flow)
|
| 329 |
+
global_flow[0].fill_(float(torch.median(flow[0])))
|
| 330 |
+
global_flow[1].fill_(float(torch.median(flow[1])))
|
| 331 |
+
controls = {
|
| 332 |
+
"global": global_flow,
|
| 333 |
+
"flow": flow,
|
| 334 |
+
"negated": -flow,
|
| 335 |
+
"shuffled": shuffled_flow(
|
| 336 |
+
flow,
|
| 337 |
+
seed=(run.prompt_id + 1) * 100000 + chunk * 1000 + slot * 10 + step,
|
| 338 |
+
),
|
| 339 |
+
}
|
| 340 |
+
values = {"raw_cosine": cosine(target, source_map)}
|
| 341 |
+
valid_ratios = []
|
| 342 |
+
for name, control_flow in controls.items():
|
| 343 |
+
aligned, mask = warp(source_map, control_flow)
|
| 344 |
+
values[f"{name}_aligned_cosine"] = cosine(target, aligned, mask)
|
| 345 |
+
valid_ratios.append(float(mask.float().mean()))
|
| 346 |
+
values["valid_flow_ratio"] = mean(valid_ratios)
|
| 347 |
+
slot_rows.append(values)
|
| 348 |
+
row = {
|
| 349 |
+
"model": run.model,
|
| 350 |
+
"action": run.action,
|
| 351 |
+
"prompt_id": run.prompt_id,
|
| 352 |
+
"chunk": chunk,
|
| 353 |
+
"step": step,
|
| 354 |
+
"source": str(run.source),
|
| 355 |
+
**motion,
|
| 356 |
+
}
|
| 357 |
+
for key in slot_rows[0]:
|
| 358 |
+
row[key] = mean(item[key] for item in slot_rows)
|
| 359 |
+
for name in ("global", "flow", "negated", "shuffled"):
|
| 360 |
+
row[f"{name}_gain"] = row[f"{name}_aligned_cosine"] - row["raw_cosine"]
|
| 361 |
+
row["flow_over_global"] = row["flow_aligned_cosine"] - row["global_aligned_cosine"]
|
| 362 |
+
row["flow_over_shuffled"] = row["flow_aligned_cosine"] - row["shuffled_aligned_cosine"]
|
| 363 |
+
rows.append(row)
|
| 364 |
+
print(f"[analysis] {run.model}/{run.action} prompt {run.prompt_id}: {run_index + 1}/{len(runs)}", flush=True)
|
| 365 |
+
return rows
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def add_motion_bins(rows: list[dict[str, Any]]) -> None:
|
| 369 |
+
groups: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list)
|
| 370 |
+
for row in rows:
|
| 371 |
+
groups[(row["model"], row["action"])].append(row)
|
| 372 |
+
for values in groups.values():
|
| 373 |
+
low, high = np.quantile([row["total_motion"] for row in values], [1 / 3, 2 / 3])
|
| 374 |
+
for row in values:
|
| 375 |
+
row["motion_bin"] = (
|
| 376 |
+
"low" if row["total_motion"] <= low else "high" if row["total_motion"] > high else "medium"
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
|
| 380 |
+
METRICS = [
|
| 381 |
+
"total_motion",
|
| 382 |
+
"camera_motion",
|
| 383 |
+
"object_motion",
|
| 384 |
+
"raw_cosine",
|
| 385 |
+
"global_aligned_cosine",
|
| 386 |
+
"flow_aligned_cosine",
|
| 387 |
+
"negated_aligned_cosine",
|
| 388 |
+
"shuffled_aligned_cosine",
|
| 389 |
+
"global_gain",
|
| 390 |
+
"flow_gain",
|
| 391 |
+
"negated_gain",
|
| 392 |
+
"shuffled_gain",
|
| 393 |
+
"flow_over_global",
|
| 394 |
+
"flow_over_shuffled",
|
| 395 |
+
"valid_flow_ratio",
|
| 396 |
+
]
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def summarize(rows: list[dict[str, Any]], keys: list[str]) -> list[dict[str, Any]]:
|
| 400 |
+
groups: dict[tuple[Any, ...], list[dict[str, Any]]] = defaultdict(list)
|
| 401 |
+
for row in rows:
|
| 402 |
+
groups[tuple(row[key] for key in keys)].append(row)
|
| 403 |
+
output = []
|
| 404 |
+
for group, values in sorted(groups.items(), key=lambda item: tuple(map(str, item[0]))):
|
| 405 |
+
item = {key: value for key, value in zip(keys, group)}
|
| 406 |
+
item["count"] = len(values)
|
| 407 |
+
for metric in METRICS:
|
| 408 |
+
item[metric] = mean(row[metric] for row in values)
|
| 409 |
+
item["flow_win_fraction"] = mean(row["flow_gain"] > 0 for row in values)
|
| 410 |
+
item["flow_beats_shuffled_fraction"] = mean(
|
| 411 |
+
row["flow_aligned_cosine"] > row["shuffled_aligned_cosine"] for row in values
|
| 412 |
+
)
|
| 413 |
+
motion = np.asarray([row["total_motion"] for row in values], dtype=np.float64)
|
| 414 |
+
raw = np.asarray([row["raw_cosine"] for row in values], dtype=np.float64)
|
| 415 |
+
item["motion_raw_pearson"] = (
|
| 416 |
+
float(np.corrcoef(motion, raw)[0, 1]) if len(values) >= 3 and np.std(motion) > 0 else float("nan")
|
| 417 |
+
)
|
| 418 |
+
output.append(item)
|
| 419 |
+
return output
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def plot(rows: list[dict[str, Any]], output: Path) -> None:
|
| 423 |
+
groups = sorted({(row["model"], row["action"]) for row in rows})
|
| 424 |
+
names = [f"{model}\n{action}" for model, action in groups]
|
| 425 |
+
methods = ["raw_cosine", "global_aligned_cosine", "flow_aligned_cosine", "negated_aligned_cosine", "shuffled_aligned_cosine"]
|
| 426 |
+
labels = ["raw", "global", "correct flow", "negated", "shuffled"]
|
| 427 |
+
fig, axes = plt.subplots(1, 2, figsize=(15, 5.5))
|
| 428 |
+
x = np.arange(len(groups))
|
| 429 |
+
width = 0.15
|
| 430 |
+
for index, (metric, label) in enumerate(zip(methods, labels)):
|
| 431 |
+
values = [mean(row[metric] for row in rows if (row["model"], row["action"]) == group) for group in groups]
|
| 432 |
+
axes[0].bar(x + (index - 2) * width, values, width=width, label=label)
|
| 433 |
+
axes[0].set_xticks(x, names)
|
| 434 |
+
axes[0].set_ylabel("cosine")
|
| 435 |
+
axes[0].set_title("Full-grid bilinear alignment")
|
| 436 |
+
axes[0].legend(fontsize=8)
|
| 437 |
+
for group in groups:
|
| 438 |
+
selected = [row for row in rows if (row["model"], row["action"]) == group]
|
| 439 |
+
axes[1].scatter(
|
| 440 |
+
[row["total_motion"] for row in selected],
|
| 441 |
+
[row["flow_gain"] for row in selected],
|
| 442 |
+
s=14,
|
| 443 |
+
alpha=0.5,
|
| 444 |
+
label="/".join(group),
|
| 445 |
+
)
|
| 446 |
+
axes[1].axhline(0, color="black", linewidth=1)
|
| 447 |
+
axes[1].set_xlabel("motion magnitude")
|
| 448 |
+
axes[1].set_ylabel("correct-flow cosine gain")
|
| 449 |
+
axes[1].set_title("Alignment gain vs motion")
|
| 450 |
+
axes[1].legend(fontsize=7)
|
| 451 |
+
fig.tight_layout()
|
| 452 |
+
fig.savefig(output / "fullgrid_bilinear_alignment.png", dpi=180)
|
| 453 |
+
plt.close(fig)
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
def markdown_table(rows: list[dict[str, Any]]) -> str:
|
| 457 |
+
columns = [
|
| 458 |
+
"model",
|
| 459 |
+
"action",
|
| 460 |
+
"motion_bin",
|
| 461 |
+
"count",
|
| 462 |
+
"raw_cosine",
|
| 463 |
+
"global_aligned_cosine",
|
| 464 |
+
"flow_aligned_cosine",
|
| 465 |
+
"negated_aligned_cosine",
|
| 466 |
+
"shuffled_aligned_cosine",
|
| 467 |
+
"flow_gain",
|
| 468 |
+
"flow_over_shuffled",
|
| 469 |
+
"flow_win_fraction",
|
| 470 |
+
]
|
| 471 |
+
lines = ["| " + " | ".join(columns) + " |", "|" + "|".join("---" for _ in columns) + "|"]
|
| 472 |
+
for row in rows:
|
| 473 |
+
cells = []
|
| 474 |
+
for column in columns:
|
| 475 |
+
value = row.get(column, "")
|
| 476 |
+
cells.append(f"{value:.4f}" if isinstance(value, float) and np.isfinite(value) else str(value))
|
| 477 |
+
lines.append("| " + " | ".join(cells) + " |")
|
| 478 |
+
return "\n".join(lines)
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
def main() -> None:
|
| 482 |
+
args = parse_args()
|
| 483 |
+
output = args.output_root.resolve()
|
| 484 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 485 |
+
device = torch.device(args.projection_device)
|
| 486 |
+
rows: list[dict[str, Any]] = []
|
| 487 |
+
|
| 488 |
+
self_runs = load_self_runs(args.self_root.resolve())
|
| 489 |
+
print(f"[load] Self runs: {len(self_runs)}", flush=True)
|
| 490 |
+
rows.extend(collect_rows(self_runs))
|
| 491 |
+
del self_runs
|
| 492 |
+
|
| 493 |
+
causal_runs = load_causal_runs(args.causal_root.resolve())
|
| 494 |
+
print(f"[load] Causal runs: {len(causal_runs)}", flush=True)
|
| 495 |
+
rows.extend(collect_rows(causal_runs))
|
| 496 |
+
del causal_runs
|
| 497 |
+
|
| 498 |
+
cache_root = output / "projected_cache" / "hy_worldplay"
|
| 499 |
+
for action, root in (
|
| 500 |
+
("static", args.hy_root.resolve()),
|
| 501 |
+
("forward", args.hy_root.resolve()),
|
| 502 |
+
("right", args.hy_right_root.resolve()),
|
| 503 |
+
):
|
| 504 |
+
runs = load_hy_runs(
|
| 505 |
+
root,
|
| 506 |
+
action,
|
| 507 |
+
cache_root,
|
| 508 |
+
args.projection_dim,
|
| 509 |
+
device,
|
| 510 |
+
args.overwrite_projection_cache,
|
| 511 |
+
)
|
| 512 |
+
print(f"[load] HY {action} runs: {len(runs)}", flush=True)
|
| 513 |
+
rows.extend(collect_rows(runs))
|
| 514 |
+
del runs
|
| 515 |
+
if device.type == "cuda":
|
| 516 |
+
torch.cuda.empty_cache()
|
| 517 |
+
|
| 518 |
+
add_motion_bins(rows)
|
| 519 |
+
overall = summarize(rows, ["model", "action"])
|
| 520 |
+
bins = summarize(rows, ["model", "action", "motion_bin"])
|
| 521 |
+
chunks = summarize(rows, ["model", "action", "chunk"])
|
| 522 |
+
write_csv(output / "fullgrid_bilinear_metrics.csv", rows)
|
| 523 |
+
write_csv(output / "fullgrid_bilinear_summary.csv", overall)
|
| 524 |
+
write_csv(output / "fullgrid_bilinear_motion_bins.csv", bins)
|
| 525 |
+
write_csv(output / "fullgrid_bilinear_chunks.csv", chunks)
|
| 526 |
+
plot(rows, output)
|
| 527 |
+
metadata = {
|
| 528 |
+
"projection_dim": args.projection_dim,
|
| 529 |
+
"projection_seed_base": PROJECTION_SEED_BASE,
|
| 530 |
+
"grid": [GRID_H, GRID_W],
|
| 531 |
+
"flow": "Farneback target-to-source, resized with vector scaling",
|
| 532 |
+
"warp": "bilinear grid_sample, align_corners=True, in-bounds mask",
|
| 533 |
+
"motion_bins": "tertiles computed independently inside each model/action",
|
| 534 |
+
"run_count": len({(row["model"], row["action"], row["prompt_id"]) for row in rows}),
|
| 535 |
+
"row_count": len(rows),
|
| 536 |
+
}
|
| 537 |
+
(output / "analysis_config.json").write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8")
|
| 538 |
+
report = [
|
| 539 |
+
"# Three-backbone full-grid bilinear alignment",
|
| 540 |
+
"",
|
| 541 |
+
"Primary comparison follows the original Self-Forcing pilot and keeps a complete 30x52 feature grid. Correct flow is evaluated against global, negated, and spatially shuffled flow fields under the same bilinear interpolation.",
|
| 542 |
+
"",
|
| 543 |
+
markdown_table(bins),
|
| 544 |
+
"",
|
| 545 |
+
"A correct-flow gain alone can include interpolation effects. `flow_over_shuffled` and `flow_beats_shuffled_fraction` test whether spatially correct displacement adds value beyond a magnitude-matched interpolating control.",
|
| 546 |
+
"",
|
| 547 |
+
"In the current results, Self-Forcing and Causal-Forcing retain material correct-flow advantages over shuffled flow (0.0040 and 0.0075 cosine overall). HY's static/forward/right advantages are only about 0.0002: its raw-to-warp gain is therefore dominated by bilinear smoothing rather than verified optical-flow correspondence.",
|
| 548 |
+
"",
|
| 549 |
+
"HY has only two target chunk boundaries. Motion buckets are strongly confounded with chunk position and must not be read as a clean low/medium/high causal trend; use the chunk-stratified CSV for diagnosis.",
|
| 550 |
+
]
|
| 551 |
+
(output / "REPORT.md").write_text("\n".join(report) + "\n", encoding="utf-8")
|
| 552 |
+
print(f"[complete] {output}: {len(rows)} rows", flush=True)
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
if __name__ == "__main__":
|
| 556 |
+
main()
|
scripts/analyze_motion_alignment_3models.py
ADDED
|
@@ -0,0 +1,859 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Motion-stratified and correspondence-aware cache analysis for the three AR4 models.
|
| 3 |
+
|
| 4 |
+
The generation jobs deliberately remain project-native. This file is an offline
|
| 5 |
+
consumer of their saved feature snapshots and RGB anchor frames. It keeps the
|
| 6 |
+
feature comparison at token locations (rather than comparing only a pooled
|
| 7 |
+
vector), estimates RAFT flow with a forward/backward consistency mask, and
|
| 8 |
+
reports raw, global-transform, homography, dense-flow, and (for WorldPlay)
|
| 9 |
+
camera-action rotation alignment.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import csv
|
| 16 |
+
import gc
|
| 17 |
+
import json
|
| 18 |
+
import math
|
| 19 |
+
import os
|
| 20 |
+
from collections import defaultdict
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Any, Iterable
|
| 23 |
+
|
| 24 |
+
import cv2
|
| 25 |
+
import matplotlib
|
| 26 |
+
|
| 27 |
+
matplotlib.use("Agg")
|
| 28 |
+
import matplotlib.pyplot as plt
|
| 29 |
+
import numpy as np
|
| 30 |
+
import torch
|
| 31 |
+
import torch.nn.functional as F
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
IMG_W, IMG_H = 416, 240
|
| 35 |
+
ROLE_ORDER = ("early", "middle", "late", "final")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def parse_args() -> argparse.Namespace:
|
| 39 |
+
parser = argparse.ArgumentParser()
|
| 40 |
+
parser.add_argument("--self_root", type=Path, required=True)
|
| 41 |
+
parser.add_argument("--causal_root", type=Path, required=True)
|
| 42 |
+
parser.add_argument("--hy_root", type=Path, required=True)
|
| 43 |
+
parser.add_argument("--hy_right_root", type=Path, default=None)
|
| 44 |
+
parser.add_argument("--output_root", type=Path, required=True)
|
| 45 |
+
parser.add_argument("--flow_device", default="auto")
|
| 46 |
+
parser.add_argument("--flow_backend", choices=("raft", "farneback"), default="raft")
|
| 47 |
+
parser.add_argument("--corr_max_tokens", type=int, default=512)
|
| 48 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 49 |
+
return parser.parse_args()
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def finite(value: Any) -> bool:
|
| 53 |
+
try:
|
| 54 |
+
return math.isfinite(float(value))
|
| 55 |
+
except (TypeError, ValueError):
|
| 56 |
+
return False
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def mean_or_nan(values: Iterable[float]) -> float:
|
| 60 |
+
values = [float(v) for v in values if finite(v)]
|
| 61 |
+
return float(np.mean(values)) if values else float("nan")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
|
| 65 |
+
if not rows:
|
| 66 |
+
return
|
| 67 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 68 |
+
fields: list[str] = []
|
| 69 |
+
for row in rows:
|
| 70 |
+
for key in row:
|
| 71 |
+
if key not in fields:
|
| 72 |
+
fields.append(key)
|
| 73 |
+
with path.open("w", newline="", encoding="utf-8") as handle:
|
| 74 |
+
writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore")
|
| 75 |
+
writer.writeheader()
|
| 76 |
+
writer.writerows(rows)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def json_dump(path: Path, value: Any) -> None:
|
| 80 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 81 |
+
path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _as_numpy(value: Any) -> np.ndarray:
|
| 85 |
+
if isinstance(value, torch.Tensor):
|
| 86 |
+
return value.detach().cpu().numpy()
|
| 87 |
+
return np.asarray(value)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _key_parts(key: str) -> tuple[int, int]:
|
| 91 |
+
left, right = str(key).split(":")[:2]
|
| 92 |
+
return int(left), int(right)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class FeatureRun:
|
| 96 |
+
"""A normalized view of one prompt/action's saved features."""
|
| 97 |
+
|
| 98 |
+
def __init__(
|
| 99 |
+
self,
|
| 100 |
+
model: str,
|
| 101 |
+
action: str,
|
| 102 |
+
prompt_id: int,
|
| 103 |
+
path: Path,
|
| 104 |
+
anchors: np.ndarray,
|
| 105 |
+
chunks: int,
|
| 106 |
+
chunk_size: int,
|
| 107 |
+
stages: dict[tuple[str, int, int], tuple[np.ndarray, np.ndarray]],
|
| 108 |
+
camera: tuple[np.ndarray, np.ndarray] | None = None,
|
| 109 |
+
dense_path: Path | None = None,
|
| 110 |
+
):
|
| 111 |
+
self.model = model
|
| 112 |
+
self.action = action
|
| 113 |
+
self.prompt_id = int(prompt_id)
|
| 114 |
+
self.path = path
|
| 115 |
+
self.anchors = anchors
|
| 116 |
+
self.chunk_size = int(chunk_size)
|
| 117 |
+
self.chunks = int(chunks)
|
| 118 |
+
self.stages = stages
|
| 119 |
+
self.camera = camera
|
| 120 |
+
self.dense_path = dense_path
|
| 121 |
+
self._dense_features = None
|
| 122 |
+
self._dense_coords = None
|
| 123 |
+
self._dense_stages = None
|
| 124 |
+
self._dense_chunks = None
|
| 125 |
+
self._dense_steps = None
|
| 126 |
+
self.grid_h = 30
|
| 127 |
+
self.grid_w = 52
|
| 128 |
+
for _, (_, coords) in stages.items():
|
| 129 |
+
if len(coords):
|
| 130 |
+
self.grid_h = max(self.grid_h, int(np.max(coords[:, 1])) + 1)
|
| 131 |
+
self.grid_w = max(self.grid_w, int(np.max(coords[:, 2])) + 1)
|
| 132 |
+
|
| 133 |
+
@property
|
| 134 |
+
def stage_names(self) -> list[str]:
|
| 135 |
+
return sorted({key[0] for key in self.stages}, key=stage_sort)
|
| 136 |
+
|
| 137 |
+
def get(self, stage: str, chunk: int, step: int):
|
| 138 |
+
# HY's block_53 dense snapshots are large compressed archives. Keep
|
| 139 |
+
# them lazy so that one run is decompressed only while it is being
|
| 140 |
+
# analyzed, instead of retaining ~1.2 GB per prompt/action in RAM.
|
| 141 |
+
if stage == "block_53" and self.dense_path is not None and self.dense_path.exists():
|
| 142 |
+
if self._dense_features is None:
|
| 143 |
+
data = np.load(self.dense_path, allow_pickle=False)
|
| 144 |
+
# NpzFile lazily decompresses an array on every indexing
|
| 145 |
+
# operation. Materialize each member once per run so the
|
| 146 |
+
# 6.2-GB float16 feature tensor is not decompressed once per
|
| 147 |
+
# chunk/timestep query.
|
| 148 |
+
try:
|
| 149 |
+
self._dense_features = np.asarray(data["features"])
|
| 150 |
+
self._dense_coords = np.asarray(data["coords"], dtype=np.int32)
|
| 151 |
+
self._dense_stages = np.asarray(data["stages"]).astype(str)
|
| 152 |
+
self._dense_chunks = np.asarray(data["chunks"], dtype=np.int32)
|
| 153 |
+
self._dense_steps = np.asarray(data["steps"], dtype=np.int32)
|
| 154 |
+
finally:
|
| 155 |
+
data.close()
|
| 156 |
+
stages = self._dense_stages
|
| 157 |
+
chunks = self._dense_chunks
|
| 158 |
+
steps = self._dense_steps
|
| 159 |
+
matches = np.flatnonzero(
|
| 160 |
+
(stages == stage) & (chunks == int(chunk)) & (steps == int(step))
|
| 161 |
+
)
|
| 162 |
+
if len(matches):
|
| 163 |
+
index = int(matches[0])
|
| 164 |
+
return self._dense_features[index].astype(np.float32), self._dense_coords
|
| 165 |
+
return self.stages.get((stage, int(chunk), int(step)))
|
| 166 |
+
|
| 167 |
+
def release(self):
|
| 168 |
+
if self._dense_features is not None:
|
| 169 |
+
self._dense_features = None
|
| 170 |
+
self._dense_coords = None
|
| 171 |
+
self._dense_stages = None
|
| 172 |
+
self._dense_chunks = None
|
| 173 |
+
self._dense_steps = None
|
| 174 |
+
gc.collect()
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def stage_sort(stage: str):
|
| 178 |
+
if stage == "projected":
|
| 179 |
+
return (10000, stage)
|
| 180 |
+
try:
|
| 181 |
+
return (int(stage.split("_")[1]), stage)
|
| 182 |
+
except (IndexError, ValueError):
|
| 183 |
+
return (9000, stage)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def stage_role(run: FeatureRun, stage: str) -> str:
|
| 187 |
+
if stage == "projected":
|
| 188 |
+
return "final"
|
| 189 |
+
block_stages = [name for name in run.stage_names if name.startswith("block_")]
|
| 190 |
+
if stage in block_stages:
|
| 191 |
+
index = block_stages.index(stage)
|
| 192 |
+
return ROLE_ORDER[min(index, len(ROLE_ORDER) - 1)]
|
| 193 |
+
return stage
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def _anchor(path: Path) -> np.ndarray:
|
| 197 |
+
data = np.load(path, allow_pickle=False)
|
| 198 |
+
value = data["frames"]
|
| 199 |
+
if value.ndim != 4:
|
| 200 |
+
raise ValueError(f"Expected [T,H,W,3] anchors at {path}, got {value.shape}")
|
| 201 |
+
return value.astype(np.uint8)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def load_self(root: Path) -> list[FeatureRun]:
|
| 205 |
+
result: list[FeatureRun] = []
|
| 206 |
+
for path in sorted((root / "runs").glob("prompt_*.pt")):
|
| 207 |
+
state = torch.load(path, map_location="cpu", weights_only=False)
|
| 208 |
+
anchor_path = path.with_suffix(".anchors.npz")
|
| 209 |
+
if not anchor_path.exists():
|
| 210 |
+
raise FileNotFoundError(anchor_path)
|
| 211 |
+
coords_hidden = _as_numpy(state["sample_coords"]["hidden"]).astype(np.int32)
|
| 212 |
+
stages: dict[tuple[str, int, int], tuple[np.ndarray, np.ndarray]] = {}
|
| 213 |
+
for stage, values in state["records"].items():
|
| 214 |
+
if not stage.endswith("_hidden"):
|
| 215 |
+
continue
|
| 216 |
+
for key, value in values.items():
|
| 217 |
+
chunk, step = _key_parts(key)
|
| 218 |
+
stages[(stage, chunk, step)] = (_as_numpy(value).astype(np.float32), coords_hidden)
|
| 219 |
+
# The native recorder has a compact full-grid random projection for its
|
| 220 |
+
# last block. Keep it as an additional final-stage representation.
|
| 221 |
+
full_coords = np.stack(
|
| 222 |
+
np.meshgrid(np.arange(3), np.arange(30), np.arange(52), indexing="ij"), axis=-1
|
| 223 |
+
).reshape(-1, 3).astype(np.int32)
|
| 224 |
+
for key, value in state.get("projected", {}).items():
|
| 225 |
+
chunk, step = _key_parts(key)
|
| 226 |
+
array = _as_numpy(value).astype(np.float32).reshape(-1, _as_numpy(value).shape[-1])
|
| 227 |
+
stages[("projected", chunk, step)] = (array, full_coords)
|
| 228 |
+
result.append(
|
| 229 |
+
FeatureRun(
|
| 230 |
+
"self_forcing",
|
| 231 |
+
"none",
|
| 232 |
+
int(state.get("run_index", len(result))),
|
| 233 |
+
path,
|
| 234 |
+
_anchor(anchor_path),
|
| 235 |
+
int(state["num_frames"]) // int(state["num_frame_per_block"]),
|
| 236 |
+
int(state["num_frame_per_block"]),
|
| 237 |
+
stages,
|
| 238 |
+
)
|
| 239 |
+
)
|
| 240 |
+
del state
|
| 241 |
+
if len(result) != 10:
|
| 242 |
+
print(f"[warn] Self-Forcing runs found {len(result)} prompts, expected 10")
|
| 243 |
+
return result
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def _flat_indices_to_coords(indices: np.ndarray, frame_count: int = 3) -> np.ndarray:
|
| 247 |
+
indices = indices.astype(np.int64).reshape(-1)
|
| 248 |
+
frame_size = 30 * 52
|
| 249 |
+
return np.stack(
|
| 250 |
+
[indices // frame_size, (indices % frame_size) // 52, indices % 52], axis=1
|
| 251 |
+
).astype(np.int32)
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def load_causal(root: Path) -> list[FeatureRun]:
|
| 255 |
+
result: list[FeatureRun] = []
|
| 256 |
+
for run_dir in sorted((root / "runs").glob("prompt_*")):
|
| 257 |
+
path = run_dir / "feature_snapshots.pt"
|
| 258 |
+
if not path.exists():
|
| 259 |
+
continue
|
| 260 |
+
state = torch.load(path, map_location="cpu", weights_only=False)
|
| 261 |
+
anchor_path = run_dir / "rgb_anchor_frames.npz"
|
| 262 |
+
if not anchor_path.exists():
|
| 263 |
+
raise FileNotFoundError(anchor_path)
|
| 264 |
+
stages: dict[tuple[str, int, int], tuple[np.ndarray, np.ndarray]] = {}
|
| 265 |
+
index_dict = state.get("feature_indices", {})
|
| 266 |
+
for key, value in state["features"].items():
|
| 267 |
+
layer, chunk, step = (int(v) for v in str(key).split(":"))
|
| 268 |
+
indices = index_dict.get(key)
|
| 269 |
+
if indices is None:
|
| 270 |
+
token_count = 3 * 30 * 52
|
| 271 |
+
indices = np.linspace(0, token_count - 1, int(state["max_tokens"])).round().astype(np.int64)
|
| 272 |
+
coords = _flat_indices_to_coords(_as_numpy(indices))
|
| 273 |
+
stages[(f"block_{layer:02d}", chunk, step)] = (_as_numpy(value).astype(np.float32), coords)
|
| 274 |
+
prompt_id = int(state.get("prompt_id", run_dir.name.split("_")[-1]))
|
| 275 |
+
result.append(
|
| 276 |
+
FeatureRun(
|
| 277 |
+
"causal_forcing",
|
| 278 |
+
"none",
|
| 279 |
+
prompt_id,
|
| 280 |
+
path,
|
| 281 |
+
_anchor(anchor_path),
|
| 282 |
+
int(state["num_chunks"]),
|
| 283 |
+
3,
|
| 284 |
+
stages,
|
| 285 |
+
)
|
| 286 |
+
)
|
| 287 |
+
del state
|
| 288 |
+
if len(result) != 10:
|
| 289 |
+
print(f"[warn] Causal-Forcing runs found {len(result)} prompts, expected 10")
|
| 290 |
+
return result
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def _load_hy_action(root: Path, action: str) -> list[FeatureRun]:
|
| 294 |
+
result: list[FeatureRun] = []
|
| 295 |
+
for case_dir in sorted((root / "runs").glob("prompt_*")):
|
| 296 |
+
run_dir = case_dir / action
|
| 297 |
+
if not run_dir.exists():
|
| 298 |
+
continue
|
| 299 |
+
anchor_path = run_dir / "rgb_anchor_frames.npz"
|
| 300 |
+
dense_path = run_dir / "dense_selected_snapshots.npz"
|
| 301 |
+
sampled_path = run_dir / "final_hidden_snapshots.npz"
|
| 302 |
+
# The sampled archive contains all requested layers and is sufficient
|
| 303 |
+
# for the ordinary token metrics. Keep the expensive dense archive
|
| 304 |
+
# only as a lazy source for HY's final block (block_53).
|
| 305 |
+
snapshot_path = sampled_path if sampled_path.exists() else dense_path
|
| 306 |
+
if not anchor_path.exists() or not snapshot_path.exists():
|
| 307 |
+
print(f"[warn] skip incomplete HY run {run_dir}")
|
| 308 |
+
continue
|
| 309 |
+
data = np.load(snapshot_path, allow_pickle=False)
|
| 310 |
+
features = data["features"].astype(np.float32)
|
| 311 |
+
chunks = data["chunks"].astype(int)
|
| 312 |
+
steps = data["steps"].astype(int)
|
| 313 |
+
stages_np = data["stages"].astype(str)
|
| 314 |
+
coords = data["coords"].astype(np.int32)
|
| 315 |
+
grid_shape = tuple(int(v) for v in data["grid_shape"])
|
| 316 |
+
stages: dict[tuple[str, int, int], tuple[np.ndarray, np.ndarray]] = {}
|
| 317 |
+
for index in range(features.shape[0]):
|
| 318 |
+
stages[(stages_np[index], int(chunks[index]), int(steps[index]))] = (
|
| 319 |
+
features[index],
|
| 320 |
+
coords,
|
| 321 |
+
)
|
| 322 |
+
camera = None
|
| 323 |
+
camera_path = run_dir / "camera_trajectory.npz"
|
| 324 |
+
if camera_path.exists():
|
| 325 |
+
camera_data = np.load(camera_path, allow_pickle=False)
|
| 326 |
+
camera = (camera_data["viewmats"].astype(np.float64), camera_data["intrinsics"].astype(np.float64))
|
| 327 |
+
metadata = run_dir / "run_metadata.json"
|
| 328 |
+
prompt_id = int(case_dir.name.split("_")[-1])
|
| 329 |
+
chunks_count = int(json.loads(metadata.read_text()).get("video_length", 45) if metadata.exists() else 45)
|
| 330 |
+
latent_count = (chunks_count - 1) // 4 + 1
|
| 331 |
+
# Prefer the actual snapshot chunk count when available.
|
| 332 |
+
chunks_count = max(1, int(np.max(chunks)) + 1)
|
| 333 |
+
result.append(
|
| 334 |
+
FeatureRun(
|
| 335 |
+
"hy_worldplay",
|
| 336 |
+
action,
|
| 337 |
+
prompt_id,
|
| 338 |
+
run_dir,
|
| 339 |
+
_anchor(anchor_path),
|
| 340 |
+
chunks_count,
|
| 341 |
+
4,
|
| 342 |
+
stages,
|
| 343 |
+
camera,
|
| 344 |
+
# Keep the cross-model comparison at the same 240-token
|
| 345 |
+
# sampling budget. The dense 6,240-token archive is retained
|
| 346 |
+
# as an optional artifact, but using it for all-pairs matching
|
| 347 |
+
# would make HY's final layer incomparable and unnecessarily
|
| 348 |
+
# expensive (6,240^2 distances per query).
|
| 349 |
+
dense_path=None,
|
| 350 |
+
)
|
| 351 |
+
)
|
| 352 |
+
return result
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
class FlowEstimator:
|
| 356 |
+
def __init__(self, backend: str, device: str):
|
| 357 |
+
self.backend = backend
|
| 358 |
+
self.device = torch.device("cuda" if device == "auto" and torch.cuda.is_available() else (device if device != "auto" else "cpu"))
|
| 359 |
+
self.model = None
|
| 360 |
+
self.transforms = None
|
| 361 |
+
if backend == "raft":
|
| 362 |
+
try:
|
| 363 |
+
from torchvision.models.optical_flow import Raft_Small_Weights, raft_small
|
| 364 |
+
|
| 365 |
+
weights = Raft_Small_Weights.DEFAULT
|
| 366 |
+
self.model = raft_small(weights=weights, progress=True).eval().to(self.device)
|
| 367 |
+
self.transforms = weights.transforms()
|
| 368 |
+
print(f"[flow] RAFT-small on {self.device}")
|
| 369 |
+
except Exception as error:
|
| 370 |
+
print(f"[flow] RAFT unavailable ({error}); using Farneback")
|
| 371 |
+
self.backend = "farneback"
|
| 372 |
+
|
| 373 |
+
@staticmethod
|
| 374 |
+
def _image(frame: np.ndarray) -> np.ndarray:
|
| 375 |
+
if frame.shape[:2] != (IMG_H, IMG_W):
|
| 376 |
+
return cv2.resize(frame, (IMG_W, IMG_H), interpolation=cv2.INTER_AREA)
|
| 377 |
+
return frame
|
| 378 |
+
|
| 379 |
+
def _farneback(self, first: np.ndarray, second: np.ndarray) -> np.ndarray:
|
| 380 |
+
a = cv2.cvtColor(self._image(first), cv2.COLOR_RGB2GRAY)
|
| 381 |
+
b = cv2.cvtColor(self._image(second), cv2.COLOR_RGB2GRAY)
|
| 382 |
+
return cv2.calcOpticalFlowFarneback(a, b, None, 0.5, 3, 21, 5, 7, 1.5, 0)
|
| 383 |
+
|
| 384 |
+
@torch.inference_mode()
|
| 385 |
+
def pair(self, first: np.ndarray, second: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 386 |
+
"""Return first->second flow, reverse flow, and a mask on first pixels."""
|
| 387 |
+
first = self._image(first)
|
| 388 |
+
second = self._image(second)
|
| 389 |
+
if self.backend != "raft" or self.model is None:
|
| 390 |
+
forward = self._farneback(first, second)
|
| 391 |
+
backward = self._farneback(second, first)
|
| 392 |
+
else:
|
| 393 |
+
x = torch.from_numpy(first).permute(2, 0, 1).float().div_(255).unsqueeze(0).to(self.device)
|
| 394 |
+
y = torch.from_numpy(second).permute(2, 0, 1).float().div_(255).unsqueeze(0).to(self.device)
|
| 395 |
+
x, y = self.transforms(x, y)
|
| 396 |
+
forward = self.model(x, y)[-1][0].permute(1, 2, 0).float().cpu().numpy()
|
| 397 |
+
backward = self.model(y, x)[-1][0].permute(1, 2, 0).float().cpu().numpy()
|
| 398 |
+
# The feature query is made at pixels in ``first`` (the current/target
|
| 399 |
+
# frame), so the consistency mask must also be indexed in that frame.
|
| 400 |
+
# A previous implementation used the reverse flow and a mask indexed
|
| 401 |
+
# in ``second``; that silently inverted the alignment direction.
|
| 402 |
+
h, w = forward.shape[:2]
|
| 403 |
+
yy, xx = np.mgrid[0:h, 0:w].astype(np.float32)
|
| 404 |
+
sx = xx + forward[..., 0]
|
| 405 |
+
sy = yy + forward[..., 1]
|
| 406 |
+
sampled_backward = cv2.remap(backward, sx, sy, cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT)
|
| 407 |
+
fb = np.linalg.norm(forward + sampled_backward, axis=-1)
|
| 408 |
+
magnitude = np.linalg.norm(forward, axis=-1)
|
| 409 |
+
in_bounds = (sx >= 0) & (sx < w) & (sy >= 0) & (sy < h)
|
| 410 |
+
# The threshold scales mildly with motion, avoiding an overly strict
|
| 411 |
+
# rejection of the fast camera actions while retaining occlusion masks.
|
| 412 |
+
valid = in_bounds & (fb <= 1.5 + 0.05 * magnitude)
|
| 413 |
+
return forward, backward, valid
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
def flow_at(flow: np.ndarray, xy: np.ndarray) -> np.ndarray:
|
| 417 |
+
h, w = flow.shape[:2]
|
| 418 |
+
x = np.clip(xy[:, 0], 0, w - 1).astype(np.float32)
|
| 419 |
+
y = np.clip(xy[:, 1], 0, h - 1).astype(np.float32)
|
| 420 |
+
return np.stack(
|
| 421 |
+
[cv2.remap(flow[..., dim], x, y, cv2.INTER_LINEAR).reshape(-1) for dim in range(2)], axis=1
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def mask_at(mask: np.ndarray, xy: np.ndarray) -> np.ndarray:
|
| 426 |
+
h, w = mask.shape[:2]
|
| 427 |
+
x = np.clip(xy[:, 0], 0, w - 1).astype(np.float32)
|
| 428 |
+
y = np.clip(xy[:, 1], 0, h - 1).astype(np.float32)
|
| 429 |
+
value = cv2.remap(mask.astype(np.uint8), x, y, cv2.INTER_NEAREST).reshape(-1)
|
| 430 |
+
return value.astype(bool)
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
def fit_transforms(flow: np.ndarray, valid: np.ndarray):
|
| 434 |
+
h, w = flow.shape[:2]
|
| 435 |
+
yy, xx = np.mgrid[0:h:8, 0:w:8].astype(np.float32)
|
| 436 |
+
points = np.stack([xx.reshape(-1), yy.reshape(-1)], axis=1)
|
| 437 |
+
selected = valid[::8, ::8].reshape(-1)
|
| 438 |
+
points = points[selected]
|
| 439 |
+
if len(points) < 6:
|
| 440 |
+
fallback_y, fallback_x = np.mgrid[0:h:4, 0:w:4]
|
| 441 |
+
points = np.stack([fallback_x.reshape(-1), fallback_y.reshape(-1)], axis=1).astype(np.float32)
|
| 442 |
+
selected_flow = flow[::4, ::4].reshape(-1, 2)
|
| 443 |
+
else:
|
| 444 |
+
selected_flow = flow[::8, ::8].reshape(-1, 2)[selected]
|
| 445 |
+
destinations = points + selected_flow
|
| 446 |
+
affine = None
|
| 447 |
+
homography = None
|
| 448 |
+
if len(points) >= 3:
|
| 449 |
+
affine, _ = cv2.estimateAffine2D(points, destinations, method=cv2.RANSAC, ransacReprojThreshold=3.0)
|
| 450 |
+
if len(points) >= 4:
|
| 451 |
+
homography, _ = cv2.findHomography(points, destinations, cv2.RANSAC, 4.0)
|
| 452 |
+
median = np.median(flow[valid] if bool(np.any(valid)) else flow.reshape(-1, 2), axis=0)
|
| 453 |
+
translation = np.asarray([[1.0, 0.0, median[0]], [0.0, 1.0, median[1]]], dtype=np.float32)
|
| 454 |
+
return translation, affine, homography
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
def apply_transform(points: np.ndarray, transform: np.ndarray | None) -> np.ndarray | None:
|
| 458 |
+
if transform is None or len(points) == 0:
|
| 459 |
+
return None
|
| 460 |
+
if transform.shape == (2, 3):
|
| 461 |
+
return points @ transform[:, :2].T + transform[:, 2]
|
| 462 |
+
homogeneous = np.concatenate([points, np.ones((len(points), 1), dtype=np.float32)], axis=1)
|
| 463 |
+
mapped = homogeneous @ transform.T
|
| 464 |
+
return mapped[:, :2] / np.clip(mapped[:, 2:3], 1e-6, None)
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
def nearest_features(source_xy: np.ndarray, source_features: np.ndarray, query_xy: np.ndarray):
|
| 468 |
+
if len(source_xy) == 0 or len(query_xy) == 0:
|
| 469 |
+
return np.empty((0, source_features.shape[-1]), dtype=np.float32), np.zeros(len(query_xy), bool), np.empty(len(query_xy))
|
| 470 |
+
distances = ((query_xy[:, None, :] - source_xy[None, :, :]) ** 2).sum(axis=-1)
|
| 471 |
+
indices = np.argmin(distances, axis=1)
|
| 472 |
+
return source_features[indices], np.ones(len(query_xy), bool), np.sqrt(distances[np.arange(len(indices)), indices])
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
def cosine_mean(left: np.ndarray, right: np.ndarray, mask: np.ndarray | None = None) -> float:
|
| 476 |
+
if len(left) == 0 or len(right) == 0:
|
| 477 |
+
return float("nan")
|
| 478 |
+
value = np.sum(left * right, axis=-1) / (
|
| 479 |
+
np.linalg.norm(left, axis=-1) * np.linalg.norm(right, axis=-1) + 1e-8
|
| 480 |
+
)
|
| 481 |
+
if mask is not None:
|
| 482 |
+
value = value[mask]
|
| 483 |
+
return mean_or_nan(value)
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def select_frame(features: np.ndarray, coords: np.ndarray, frame: int):
|
| 487 |
+
selected = coords[:, 0] == int(frame)
|
| 488 |
+
return features[selected], coords[selected, 1:3][:, ::-1].astype(np.float32)
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
def token_correspondence(target: np.ndarray, target_xy: np.ndarray, source: np.ndarray, source_xy: np.ndarray, max_tokens: int):
|
| 492 |
+
if len(target) == 0 or len(source) == 0:
|
| 493 |
+
return {"top1_cosine": float("nan"), "top5_cosine": float("nan"), "mnn_fraction": float("nan"), "match_distance": float("nan")}
|
| 494 |
+
def thin(array, xy):
|
| 495 |
+
if len(array) <= max_tokens:
|
| 496 |
+
return array, xy
|
| 497 |
+
idx = np.linspace(0, len(array) - 1, max_tokens).round().astype(int)
|
| 498 |
+
return array[idx], xy[idx]
|
| 499 |
+
target, target_xy = thin(target, target_xy)
|
| 500 |
+
source, source_xy = thin(source, source_xy)
|
| 501 |
+
target_n = target / (np.linalg.norm(target, axis=-1, keepdims=True) + 1e-8)
|
| 502 |
+
source_n = source / (np.linalg.norm(source, axis=-1, keepdims=True) + 1e-8)
|
| 503 |
+
similarity = target_n @ source_n.T
|
| 504 |
+
topk = np.sort(similarity, axis=1)[:, -min(5, similarity.shape[1]):]
|
| 505 |
+
best_source = np.argmax(similarity, axis=1)
|
| 506 |
+
best_target = np.argmax(similarity, axis=0)
|
| 507 |
+
target_ids = np.arange(len(best_source))
|
| 508 |
+
mnn = best_target[best_source] == target_ids
|
| 509 |
+
distance = np.linalg.norm(target_xy - source_xy[best_source], axis=-1)
|
| 510 |
+
return {
|
| 511 |
+
"top1_cosine": float(np.mean(topk[:, -1])),
|
| 512 |
+
"top5_cosine": float(np.mean(topk)),
|
| 513 |
+
"mnn_fraction": float(np.mean(mnn)),
|
| 514 |
+
"match_distance": float(np.mean(distance)),
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
def camera_rotation_homography(camera, source_index: int, target_index: int, image_shape=(IMG_H, IMG_W)):
|
| 519 |
+
if camera is None:
|
| 520 |
+
return None
|
| 521 |
+
viewmats, intrinsics = camera
|
| 522 |
+
if source_index >= len(viewmats) or target_index >= len(viewmats):
|
| 523 |
+
return None
|
| 524 |
+
source_k = intrinsics[source_index].copy()
|
| 525 |
+
target_k = intrinsics[target_index].copy()
|
| 526 |
+
h, w = image_shape
|
| 527 |
+
for k in (source_k, target_k):
|
| 528 |
+
k[0, 0] *= w
|
| 529 |
+
k[0, 2] *= w
|
| 530 |
+
k[1, 1] *= h
|
| 531 |
+
k[1, 2] *= h
|
| 532 |
+
source_r = viewmats[source_index][:3, :3]
|
| 533 |
+
target_r = viewmats[target_index][:3, :3]
|
| 534 |
+
try:
|
| 535 |
+
return source_k @ source_r @ target_r.T @ np.linalg.inv(target_k)
|
| 536 |
+
except np.linalg.LinAlgError:
|
| 537 |
+
return None
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def action_motion(camera, source_index: int, target_index: int):
|
| 541 |
+
if camera is None:
|
| 542 |
+
return {"action_translation": float("nan"), "action_rotation_deg": float("nan")}
|
| 543 |
+
viewmats, _ = camera
|
| 544 |
+
if source_index >= len(viewmats) or target_index >= len(viewmats):
|
| 545 |
+
return {"action_translation": float("nan"), "action_rotation_deg": float("nan")}
|
| 546 |
+
source_to_world = np.linalg.inv(viewmats[source_index])
|
| 547 |
+
target_to_world = np.linalg.inv(viewmats[target_index])
|
| 548 |
+
relative_rotation = source_to_world[:3, :3].T @ target_to_world[:3, :3]
|
| 549 |
+
trace = np.clip((np.trace(relative_rotation) - 1.0) / 2.0, -1.0, 1.0)
|
| 550 |
+
return {
|
| 551 |
+
"action_translation": float(np.linalg.norm(target_to_world[:3, 3] - source_to_world[:3, 3])),
|
| 552 |
+
"action_rotation_deg": float(np.degrees(np.arccos(trace))),
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
def token_points(run: FeatureRun, coords: np.ndarray) -> np.ndarray:
|
| 557 |
+
# coords are (frame,y,x) in the latent feature grid; convert to pixel
|
| 558 |
+
# centers in the resized RGB canvas, represented as (x,y).
|
| 559 |
+
return np.stack(
|
| 560 |
+
[
|
| 561 |
+
(coords[:, 2].astype(np.float32) + 0.5) * IMG_W / run.grid_w,
|
| 562 |
+
(coords[:, 1].astype(np.float32) + 0.5) * IMG_H / run.grid_h,
|
| 563 |
+
],
|
| 564 |
+
axis=1,
|
| 565 |
+
)
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def alignment_metrics(run: FeatureRun, stage: str, target_pair, source_pair, target_slot: int, flow: np.ndarray, valid_flow: np.ndarray, transforms, action_h, corr_max_tokens: int):
|
| 569 |
+
target_features, target_coords = target_pair
|
| 570 |
+
source_features, source_coords = source_pair
|
| 571 |
+
target_features, target_xy = select_frame(target_features, target_coords, target_slot)
|
| 572 |
+
source_boundary, source_boundary_xy = select_frame(source_features, source_coords, run.chunk_size - 1)
|
| 573 |
+
source_same, source_same_xy = select_frame(source_features, source_coords, min(target_slot, run.chunk_size - 1))
|
| 574 |
+
if len(target_features) == 0 or len(source_boundary) == 0:
|
| 575 |
+
return None
|
| 576 |
+
target_pixels = token_points(run, np.concatenate([np.full((len(target_xy), 1), target_slot), target_xy[:, ::-1]], axis=1).astype(np.int32))
|
| 577 |
+
source_boundary_pixels = token_points(run, np.concatenate([np.full((len(source_boundary_xy), 1), run.chunk_size - 1), source_boundary_xy[:, ::-1]], axis=1).astype(np.int32))
|
| 578 |
+
source_same_pixels = token_points(run, np.concatenate([np.full((len(source_same_xy), 1), min(target_slot, run.chunk_size - 1)), source_same_xy[:, ::-1]], axis=1).astype(np.int32))
|
| 579 |
+
raw_features, _, raw_dist = nearest_features(source_boundary_pixels, source_boundary, target_pixels)
|
| 580 |
+
same_features, _, _ = nearest_features(source_same_pixels, source_same, target_pixels)
|
| 581 |
+
target_flow = flow_at(flow, target_pixels)
|
| 582 |
+
flow_query = target_pixels + target_flow
|
| 583 |
+
flow_mask = mask_at(valid_flow, target_pixels)
|
| 584 |
+
flow_features, _, flow_dist = nearest_features(source_boundary_pixels, source_boundary, flow_query)
|
| 585 |
+
translation, affine, homography = transforms
|
| 586 |
+
def transformed_metric(transform):
|
| 587 |
+
query = apply_transform(target_pixels, transform)
|
| 588 |
+
if query is None:
|
| 589 |
+
return float("nan"), float("nan")
|
| 590 |
+
values, _, distances = nearest_features(source_boundary_pixels, source_boundary, query)
|
| 591 |
+
in_bounds = (query[:, 0] >= 0) & (query[:, 0] < IMG_W) & (query[:, 1] >= 0) & (query[:, 1] < IMG_H)
|
| 592 |
+
return cosine_mean(target_features, values, in_bounds), mean_or_nan(distances[in_bounds])
|
| 593 |
+
trans_cos, trans_dist = transformed_metric(translation)
|
| 594 |
+
affine_cos, affine_dist = transformed_metric(affine)
|
| 595 |
+
homo_cos, homo_dist = transformed_metric(homography)
|
| 596 |
+
action_cos, action_dist = transformed_metric(action_h)
|
| 597 |
+
correspondence = token_correspondence(target_features, target_pixels, source_boundary, source_boundary_pixels, corr_max_tokens)
|
| 598 |
+
return {
|
| 599 |
+
"same_slot_cosine": cosine_mean(target_features, same_features),
|
| 600 |
+
"boundary_raw_cosine": cosine_mean(target_features, raw_features),
|
| 601 |
+
"translation_aligned_cosine": trans_cos,
|
| 602 |
+
"affine_aligned_cosine": affine_cos,
|
| 603 |
+
"homography_aligned_cosine": homo_cos,
|
| 604 |
+
"flow_aligned_cosine": cosine_mean(target_features, flow_features, flow_mask),
|
| 605 |
+
"action_rotation_cosine": action_cos,
|
| 606 |
+
"raw_match_distance": mean_or_nan(raw_dist),
|
| 607 |
+
"flow_match_distance": mean_or_nan(flow_dist[flow_mask]),
|
| 608 |
+
"translation_match_distance": trans_dist,
|
| 609 |
+
"affine_match_distance": affine_dist,
|
| 610 |
+
"homography_match_distance": homo_dist,
|
| 611 |
+
"action_match_distance": action_dist,
|
| 612 |
+
"valid_flow_ratio": float(np.mean(flow_mask)),
|
| 613 |
+
"occlusion_ratio": float(1.0 - np.mean(flow_mask)),
|
| 614 |
+
**correspondence,
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
|
| 618 |
+
def motion_rows(runs: list[FeatureRun], estimator: FlowEstimator, corr_max_tokens: int) -> tuple[list[dict[str, Any]], list[dict[str, Any]], dict[str, Any]]:
|
| 619 |
+
rows: list[dict[str, Any]] = []
|
| 620 |
+
correspondence_rows: list[dict[str, Any]] = []
|
| 621 |
+
flow_cache: dict[tuple[str, int, int, int], tuple[np.ndarray, np.ndarray, np.ndarray]] = {}
|
| 622 |
+
for run in runs:
|
| 623 |
+
for chunk in range(1, run.chunks):
|
| 624 |
+
source_index = (chunk - 1) * run.chunk_size + run.chunk_size - 1
|
| 625 |
+
if source_index >= len(run.anchors):
|
| 626 |
+
continue
|
| 627 |
+
for slot in range(run.chunk_size):
|
| 628 |
+
target_index = chunk * run.chunk_size + slot
|
| 629 |
+
if target_index >= len(run.anchors):
|
| 630 |
+
continue
|
| 631 |
+
cache_key = (str(run.path), chunk, source_index, target_index)
|
| 632 |
+
if cache_key not in flow_cache:
|
| 633 |
+
# The first return is target->source and is therefore the
|
| 634 |
+
# displacement used to query the previous chunk's tokens.
|
| 635 |
+
forward, _, valid = estimator.pair(run.anchors[target_index], run.anchors[source_index])
|
| 636 |
+
flow_cache[cache_key] = (forward, valid, np.asarray([]))
|
| 637 |
+
flow, valid, _ = flow_cache[cache_key]
|
| 638 |
+
flow_mag = np.linalg.norm(flow, axis=-1)
|
| 639 |
+
median = np.median(flow[valid] if bool(np.any(valid)) else flow.reshape(-1, 2), axis=0)
|
| 640 |
+
residual = flow - median[None, None]
|
| 641 |
+
source_img = cv2.resize(run.anchors[source_index], (IMG_W, IMG_H), interpolation=cv2.INTER_AREA)
|
| 642 |
+
target_img = cv2.resize(run.anchors[target_index], (IMG_W, IMG_H), interpolation=cv2.INTER_AREA)
|
| 643 |
+
scene_cut = float(np.mean(np.abs(source_img.astype(np.float32) - target_img.astype(np.float32))) / 255.0)
|
| 644 |
+
action_motion_values = action_motion(run.camera, source_index, target_index)
|
| 645 |
+
action_h = camera_rotation_homography(run.camera, source_index, target_index)
|
| 646 |
+
transforms = fit_transforms(flow, valid)
|
| 647 |
+
stage_names = [stage for stage in run.stage_names if stage.startswith("block_") or stage == "projected"]
|
| 648 |
+
for stage in stage_names:
|
| 649 |
+
common_steps = sorted(
|
| 650 |
+
set(step for st, ch, step in run.stages if st == stage and ch == chunk)
|
| 651 |
+
& set(step for st, ch, step in run.stages if st == stage and ch == chunk - 1)
|
| 652 |
+
)
|
| 653 |
+
for step in common_steps:
|
| 654 |
+
target_pair = run.get(stage, chunk, step)
|
| 655 |
+
source_pair = run.get(stage, chunk - 1, step)
|
| 656 |
+
metrics = alignment_metrics(run, stage, target_pair, source_pair, slot, flow, valid, transforms, action_h, corr_max_tokens)
|
| 657 |
+
if metrics is None:
|
| 658 |
+
continue
|
| 659 |
+
row = {
|
| 660 |
+
"model": run.model,
|
| 661 |
+
"action": run.action,
|
| 662 |
+
"prompt_id": run.prompt_id,
|
| 663 |
+
"chunk": chunk,
|
| 664 |
+
"target_slot": slot,
|
| 665 |
+
"step": step,
|
| 666 |
+
"stage": stage,
|
| 667 |
+
"role": stage_role(run, stage),
|
| 668 |
+
"source_frame": source_index,
|
| 669 |
+
"target_frame": target_index,
|
| 670 |
+
"total_motion": float(np.mean(flow_mag)),
|
| 671 |
+
"camera_motion": float(np.linalg.norm(median)),
|
| 672 |
+
"object_motion": float(np.mean(np.linalg.norm(residual, axis=-1))),
|
| 673 |
+
"scene_cut": scene_cut,
|
| 674 |
+
**action_motion_values,
|
| 675 |
+
**metrics,
|
| 676 |
+
}
|
| 677 |
+
rows.append(row)
|
| 678 |
+
if row["role"] == "final":
|
| 679 |
+
correspondence_rows.append({
|
| 680 |
+
key: row[key]
|
| 681 |
+
for key in ("model", "action", "prompt_id", "chunk", "target_slot", "step", "stage", "role", "total_motion")
|
| 682 |
+
} | {key: metrics[key] for key in ("top1_cosine", "top5_cosine", "mnn_fraction", "match_distance")})
|
| 683 |
+
run.release()
|
| 684 |
+
values = np.asarray([row["total_motion"] for row in rows if finite(row["total_motion"])], dtype=np.float64)
|
| 685 |
+
if len(values) >= 3:
|
| 686 |
+
low, high = np.quantile(values, [1 / 3, 2 / 3])
|
| 687 |
+
for row in rows:
|
| 688 |
+
row["motion_bin"] = "low" if row["total_motion"] <= low else "high" if row["total_motion"] > high else "medium"
|
| 689 |
+
for row in correspondence_rows:
|
| 690 |
+
matches = [item for item in rows if all(item[k] == row[k] for k in ("model", "action", "prompt_id", "chunk", "target_slot", "step", "stage", "role"))]
|
| 691 |
+
row["motion_bin"] = matches[0].get("motion_bin", "unknown") if matches else "unknown"
|
| 692 |
+
metadata = {
|
| 693 |
+
"flow_backend": estimator.backend,
|
| 694 |
+
"flow_device": str(estimator.device),
|
| 695 |
+
"flow_resolution": [IMG_W, IMG_H],
|
| 696 |
+
"forward_backward_consistency": "valid = in-bounds and FB error <= 1.5 + 0.05*|flow|",
|
| 697 |
+
"motion_bin_edges": [float(low), float(high)] if len(values) >= 3 else [],
|
| 698 |
+
}
|
| 699 |
+
return rows, correspondence_rows, metadata
|
| 700 |
+
|
| 701 |
+
|
| 702 |
+
def group_rows(rows: list[dict[str, Any]], keys: list[str], metrics: list[str]) -> list[dict[str, Any]]:
|
| 703 |
+
groups: dict[tuple[Any, ...], list[dict[str, Any]]] = defaultdict(list)
|
| 704 |
+
for row in rows:
|
| 705 |
+
groups[tuple(row.get(key) for key in keys)].append(row)
|
| 706 |
+
output: list[dict[str, Any]] = []
|
| 707 |
+
for group, values in sorted(groups.items(), key=lambda item: tuple(map(str, item[0]))):
|
| 708 |
+
item = {key: value for key, value in zip(keys, group)}
|
| 709 |
+
item["count"] = len(values)
|
| 710 |
+
for metric in metrics:
|
| 711 |
+
item[metric] = mean_or_nan([row.get(metric) for row in values])
|
| 712 |
+
output.append(item)
|
| 713 |
+
return output
|
| 714 |
+
|
| 715 |
+
|
| 716 |
+
def plot_outputs(rows: list[dict[str, Any]], correspondence: list[dict[str, Any]], output: Path) -> None:
|
| 717 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 718 |
+
if not rows:
|
| 719 |
+
return
|
| 720 |
+
models = sorted({str(row["model"]) for row in rows})
|
| 721 |
+
colors = {model: plt.cm.tab10(index) for index, model in enumerate(models)}
|
| 722 |
+
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
|
| 723 |
+
for model in models:
|
| 724 |
+
selected = [row for row in rows if row["model"] == model]
|
| 725 |
+
axes[0, 0].scatter([row["total_motion"] for row in selected], [row["boundary_raw_cosine"] for row in selected], s=10, alpha=0.45, label=model, color=colors[model])
|
| 726 |
+
axes[0, 0].set(xlabel="flow magnitude", ylabel="raw boundary cosine", title="Motion vs raw cross-chunk similarity")
|
| 727 |
+
axes[0, 0].legend(fontsize=8)
|
| 728 |
+
methods = ["boundary_raw_cosine", "translation_aligned_cosine", "affine_aligned_cosine", "homography_aligned_cosine", "flow_aligned_cosine", "action_rotation_cosine"]
|
| 729 |
+
labels = ["raw", "translation", "affine", "homography", "dense flow", "action rotation"]
|
| 730 |
+
positions = np.arange(len(methods))
|
| 731 |
+
for index, model in enumerate(models):
|
| 732 |
+
values = [mean_or_nan([row.get(method) for row in rows if row["model"] == model]) for method in methods]
|
| 733 |
+
axes[0, 1].plot(positions, values, marker="o", label=model, color=colors[model])
|
| 734 |
+
axes[0, 1].set_xticks(positions, labels, rotation=25, ha="right")
|
| 735 |
+
axes[0, 1].set_ylim(-1, 1)
|
| 736 |
+
axes[0, 1].set_title("Alignment methods")
|
| 737 |
+
axes[0, 1].legend(fontsize=8)
|
| 738 |
+
for index, model in enumerate(models):
|
| 739 |
+
selected = [row for row in rows if row["model"] == model]
|
| 740 |
+
bins = ["low", "medium", "high"]
|
| 741 |
+
raw = [mean_or_nan([row["boundary_raw_cosine"] for row in selected if row.get("motion_bin") == name]) for name in bins]
|
| 742 |
+
flow = [mean_or_nan([row["flow_aligned_cosine"] for row in selected if row.get("motion_bin") == name]) for name in bins]
|
| 743 |
+
axes[1, 0].plot(bins, raw, marker="o", linestyle="--", label=f"{model} raw", color=colors[model], alpha=0.55)
|
| 744 |
+
axes[1, 0].plot(bins, flow, marker="o", label=f"{model} flow", color=colors[model])
|
| 745 |
+
axes[1, 0].set_ylim(-1, 1)
|
| 746 |
+
axes[1, 0].set_title("Motion bins")
|
| 747 |
+
axes[1, 0].set_ylabel("cosine")
|
| 748 |
+
axes[1, 0].legend(fontsize=7)
|
| 749 |
+
if correspondence:
|
| 750 |
+
for model in models:
|
| 751 |
+
selected = [row for row in correspondence if row["model"] == model]
|
| 752 |
+
axes[1, 1].scatter([row["total_motion"] for row in selected], [row["top1_cosine"] for row in selected], s=12, alpha=0.5, label=model, color=colors[model])
|
| 753 |
+
axes[1, 1].set(xlabel="flow magnitude", ylabel="top-1 feature match cosine", title="Token correspondence")
|
| 754 |
+
axes[1, 1].legend(fontsize=8)
|
| 755 |
+
fig.tight_layout()
|
| 756 |
+
fig.savefig(output / "motion_alignment_overview.png", dpi=180)
|
| 757 |
+
plt.close(fig)
|
| 758 |
+
|
| 759 |
+
if correspondence:
|
| 760 |
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4.5))
|
| 761 |
+
for model in models:
|
| 762 |
+
selected = [row for row in correspondence if row["model"] == model]
|
| 763 |
+
for axis, metric, title in zip(axes, ("top1_cosine", "mnn_fraction", "match_distance"), ("Top-1 cosine", "MNN fraction", "Match distance")):
|
| 764 |
+
axis.scatter([row["total_motion"] for row in selected], [row[metric] for row in selected], s=12, alpha=0.5, label=model, color=colors[model])
|
| 765 |
+
axis.set_title(title)
|
| 766 |
+
axis.set_xlabel("flow magnitude")
|
| 767 |
+
axes[0].set_ylabel("value")
|
| 768 |
+
axes[0].legend(fontsize=8)
|
| 769 |
+
fig.tight_layout()
|
| 770 |
+
fig.savefig(output / "token_correspondence_summary.png", dpi=180)
|
| 771 |
+
plt.close(fig)
|
| 772 |
+
|
| 773 |
+
|
| 774 |
+
def markdown_table(rows: list[dict[str, Any]], columns: list[str]) -> str:
|
| 775 |
+
if not rows:
|
| 776 |
+
return "_No rows._"
|
| 777 |
+
lines = ["| " + " | ".join(columns) + " |", "|" + "|".join("---" for _ in columns) + "|"]
|
| 778 |
+
for row in rows:
|
| 779 |
+
values = []
|
| 780 |
+
for column in columns:
|
| 781 |
+
value = row.get(column, "")
|
| 782 |
+
values.append(f"{float(value):.4f}" if isinstance(value, (float, np.floating)) and finite(value) else str(value))
|
| 783 |
+
lines.append("| " + " | ".join(values) + " |")
|
| 784 |
+
return "\n".join(lines)
|
| 785 |
+
|
| 786 |
+
|
| 787 |
+
def write_report(output: Path, rows: list[dict[str, Any]], correspondence: list[dict[str, Any]], metadata: dict[str, Any], runs: list[FeatureRun]) -> None:
|
| 788 |
+
stage_summary = group_rows(rows, ["model", "action", "role", "stage"], ["boundary_raw_cosine", "translation_aligned_cosine", "affine_aligned_cosine", "homography_aligned_cosine", "flow_aligned_cosine", "action_rotation_cosine", "total_motion", "valid_flow_ratio"])
|
| 789 |
+
bin_summary = group_rows(rows, ["model", "action", "motion_bin"], ["total_motion", "boundary_raw_cosine", "flow_aligned_cosine", "homography_aligned_cosine", "object_motion", "occlusion_ratio"])
|
| 790 |
+
json_dump(output / "motion_alignment_summary.json", {"metadata": metadata, "stage_summary": stage_summary, "motion_bin_summary": bin_summary, "run_count": len(runs), "row_count": len(rows), "correspondence_count": len(correspondence)})
|
| 791 |
+
report = [
|
| 792 |
+
"# Experiment 3: motion stratification and explicit alignment",
|
| 793 |
+
"",
|
| 794 |
+
f"Runs: {len(runs)} prompt/action runs; metric rows: {len(rows)}; correspondence rows: {len(correspondence)}.",
|
| 795 |
+
"",
|
| 796 |
+
"The raw baseline compares the current chunk token with the previous chunk's boundary frame at the same spatial coordinate. `flow_aligned` uses the target-to-source flow from the backend listed below and a forward/backward consistency mask. Affine and homography are global-transform fits to the valid flow. `action_rotation` is the WorldPlay camera-rotation homography only; it does not claim to model depth-dependent translation.",
|
| 797 |
+
"",
|
| 798 |
+
"## By model/action/stage",
|
| 799 |
+
"",
|
| 800 |
+
markdown_table(stage_summary, ["model", "action", "role", "stage", "count", "boundary_raw_cosine", "translation_aligned_cosine", "affine_aligned_cosine", "homography_aligned_cosine", "flow_aligned_cosine", "action_rotation_cosine"]),
|
| 801 |
+
"",
|
| 802 |
+
"## Motion bins",
|
| 803 |
+
"",
|
| 804 |
+
markdown_table(bin_summary, ["model", "action", "motion_bin", "count", "total_motion", "boundary_raw_cosine", "homography_aligned_cosine", "flow_aligned_cosine", "object_motion", "occlusion_ratio"]),
|
| 805 |
+
"",
|
| 806 |
+
"## Interpretation guardrails",
|
| 807 |
+
"",
|
| 808 |
+
"- A positive dense-flow gain with lower raw cosine supports spatial migration rather than loss of content information.",
|
| 809 |
+
"- Action rotation is a physically informed control for HY; forward translation requires depth and is therefore reported separately rather than treated as an exact warp.",
|
| 810 |
+
"- Correlation and alignment rows are prompt-level repeated measurements; use prompt bootstrap or a mixed-effects model for significance claims.",
|
| 811 |
+
"",
|
| 812 |
+
"## Run settings",
|
| 813 |
+
"",
|
| 814 |
+
"```json",
|
| 815 |
+
json.dumps(metadata, indent=2, ensure_ascii=False),
|
| 816 |
+
"```",
|
| 817 |
+
]
|
| 818 |
+
(output / "REPORT.md").write_text("\n".join(report) + "\n", encoding="utf-8")
|
| 819 |
+
write_csv(output / "motion_alignment_metrics.csv", rows)
|
| 820 |
+
write_csv(output / "motion_alignment_summary.csv", stage_summary)
|
| 821 |
+
write_csv(output / "motion_alignment_motion_bins.csv", bin_summary)
|
| 822 |
+
write_csv(output / "token_correspondence.csv", correspondence)
|
| 823 |
+
|
| 824 |
+
|
| 825 |
+
def main() -> None:
|
| 826 |
+
args = parse_args()
|
| 827 |
+
output = args.output_root.resolve()
|
| 828 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 829 |
+
if not args.overwrite and (output / "motion_alignment_metrics.csv").exists():
|
| 830 |
+
print(f"[skip] existing analysis at {output}; pass --overwrite to recompute")
|
| 831 |
+
return
|
| 832 |
+
runs: list[FeatureRun] = []
|
| 833 |
+
runs.extend(load_self(args.self_root.resolve()))
|
| 834 |
+
runs.extend(load_causal(args.causal_root.resolve()))
|
| 835 |
+
runs.extend(_load_hy_action(args.hy_root.resolve(), "forward"))
|
| 836 |
+
runs.extend(_load_hy_action(args.hy_root.resolve(), "static"))
|
| 837 |
+
if args.hy_right_root is not None:
|
| 838 |
+
runs.extend(_load_hy_action(args.hy_right_root.resolve(), "right"))
|
| 839 |
+
if not runs:
|
| 840 |
+
raise RuntimeError("No normalized runs found")
|
| 841 |
+
estimator = FlowEstimator(args.flow_backend, args.flow_device)
|
| 842 |
+
rows, correspondence, metadata = motion_rows(runs, estimator, args.corr_max_tokens)
|
| 843 |
+
metadata.update({
|
| 844 |
+
"models": sorted({run.model for run in runs}),
|
| 845 |
+
"actions": sorted({run.action for run in runs}),
|
| 846 |
+
"prompt_ids": sorted({run.prompt_id for run in runs}),
|
| 847 |
+
"run_descriptions": [
|
| 848 |
+
{"model": run.model, "action": run.action, "prompt_id": run.prompt_id, "chunk_size": run.chunk_size, "chunks": run.chunks, "anchor_frames": len(run.anchors), "stages": run.stage_names}
|
| 849 |
+
for run in runs
|
| 850 |
+
],
|
| 851 |
+
})
|
| 852 |
+
plot_outputs(rows, correspondence, output)
|
| 853 |
+
write_report(output, rows, correspondence, metadata, runs)
|
| 854 |
+
json_dump(output / "analysis_config.json", metadata)
|
| 855 |
+
print(f"[complete] {output}: {len(runs)} runs, {len(rows)} motion rows, {len(correspondence)} correspondence rows")
|
| 856 |
+
|
| 857 |
+
|
| 858 |
+
if __name__ == "__main__":
|
| 859 |
+
main()
|
scripts/build_boundary_conditional_dataset.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Assemble the native-feature boundary-to-all conditional-probe dataset.
|
| 3 |
+
|
| 4 |
+
Self-Forcing and HY-static already use structured temporal/spatial sampling and
|
| 5 |
+
are linked read-only. Causal-Forcing is normalized from a fresh recorder run
|
| 6 |
+
whose features are stored as [temporal, spatial, channel].
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
ROLE_MAP = {7: "early", 14: "middle", 22: "late", 29: "final"}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def parse_args() -> argparse.Namespace:
|
| 24 |
+
parser = argparse.ArgumentParser()
|
| 25 |
+
parser.add_argument("--self_dir", type=Path, required=True)
|
| 26 |
+
parser.add_argument("--causal_root", type=Path, required=True)
|
| 27 |
+
parser.add_argument("--hy_dir", type=Path, required=True)
|
| 28 |
+
parser.add_argument("--output_root", type=Path, required=True)
|
| 29 |
+
parser.add_argument("--num_prompts", type=int, default=10)
|
| 30 |
+
parser.add_argument("--chunks", type=int, default=4)
|
| 31 |
+
parser.add_argument("--steps", type=int, default=4)
|
| 32 |
+
return parser.parse_args()
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def ensure_link(path: Path, target: Path) -> None:
|
| 36 |
+
target = target.resolve()
|
| 37 |
+
if path.is_symlink():
|
| 38 |
+
if path.resolve() != target:
|
| 39 |
+
raise ValueError(f"Existing symlink {path} points to {path.resolve()}, not {target}")
|
| 40 |
+
return
|
| 41 |
+
if path.exists():
|
| 42 |
+
raise FileExistsError(path)
|
| 43 |
+
path.symlink_to(target, target_is_directory=True)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def atomic_save(path: Path, value: dict) -> None:
|
| 47 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 48 |
+
torch.save(value, temporary)
|
| 49 |
+
os.replace(temporary, path)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def coords_from_indices(indices: torch.Tensor) -> np.ndarray:
|
| 53 |
+
flat = indices.detach().cpu().numpy().astype(np.int64).reshape(-1)
|
| 54 |
+
plane = 30 * 52
|
| 55 |
+
temporal = flat // plane
|
| 56 |
+
remainder = flat % plane
|
| 57 |
+
coords = np.stack([temporal, remainder // 52, remainder % 52], axis=1)
|
| 58 |
+
slots = sorted(int(value) for value in np.unique(temporal))
|
| 59 |
+
if slots != [0, 1, 2]:
|
| 60 |
+
raise ValueError(f"Expected three temporal slots, got {slots}")
|
| 61 |
+
reference = coords[coords[:, 0] == slots[-1], 1:]
|
| 62 |
+
for slot in slots:
|
| 63 |
+
if not np.array_equal(coords[coords[:, 0] == slot, 1:], reference):
|
| 64 |
+
raise ValueError(f"Causal slot {slot} does not share the structured spatial grid")
|
| 65 |
+
return coords
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def normalize_causal(path: Path, chunks: int, steps: int) -> dict:
|
| 69 |
+
state = torch.load(path, map_location="cpu", weights_only=False)
|
| 70 |
+
features = {}
|
| 71 |
+
for layer, role in ROLE_MAP.items():
|
| 72 |
+
chunk_rows = []
|
| 73 |
+
for chunk in range(chunks):
|
| 74 |
+
step_rows = []
|
| 75 |
+
for step in range(steps):
|
| 76 |
+
value = state["features"][f"{layer}:{chunk}:{step}"]
|
| 77 |
+
step_rows.append(value.reshape(-1, value.shape[-1]).to(torch.float16))
|
| 78 |
+
chunk_rows.append(torch.stack(step_rows, dim=0))
|
| 79 |
+
features[role] = torch.stack(chunk_rows, dim=0).contiguous()
|
| 80 |
+
index_key = f"{next(iter(ROLE_MAP))}:0:0"
|
| 81 |
+
coords = coords_from_indices(state["feature_indices"][index_key])
|
| 82 |
+
token_counts = {int(value.shape[2]) for value in features.values()}
|
| 83 |
+
if token_counts != {len(coords)}:
|
| 84 |
+
raise ValueError(f"Feature/coordinate mismatch: tokens={token_counts}, coords={len(coords)}")
|
| 85 |
+
return {
|
| 86 |
+
"prompt_id": int(state["prompt_id"]),
|
| 87 |
+
"prompt": state["prompt"],
|
| 88 |
+
"seed": int(state["seed"]),
|
| 89 |
+
"model_family": "causal_forcing",
|
| 90 |
+
"model_variant": "dmd4_boundary_structured",
|
| 91 |
+
"features": features,
|
| 92 |
+
"timesteps": np.asarray([1000.0, 937.5, 833.3333, 625.0], dtype=np.float32),
|
| 93 |
+
"coords": coords,
|
| 94 |
+
"grid_shape": np.asarray([3, 30, 52], dtype=np.int64),
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def main() -> None:
|
| 99 |
+
args = parse_args()
|
| 100 |
+
output = args.output_root.resolve()
|
| 101 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 102 |
+
ensure_link(output / "self_forcing", args.self_dir)
|
| 103 |
+
ensure_link(output / "hy_worldplay", args.hy_dir)
|
| 104 |
+
causal_output = output / "causal_forcing"
|
| 105 |
+
causal_output.mkdir(exist_ok=True)
|
| 106 |
+
inventory = []
|
| 107 |
+
for prompt_id in range(args.num_prompts):
|
| 108 |
+
source = (
|
| 109 |
+
args.causal_root
|
| 110 |
+
/ "runs"
|
| 111 |
+
/ f"prompt_{prompt_id:04d}"
|
| 112 |
+
/ "feature_snapshots.pt"
|
| 113 |
+
)
|
| 114 |
+
if not source.exists():
|
| 115 |
+
raise FileNotFoundError(source)
|
| 116 |
+
item = normalize_causal(source, args.chunks, args.steps)
|
| 117 |
+
if item["prompt_id"] != prompt_id:
|
| 118 |
+
raise ValueError(f"Prompt mismatch in {source}: {item['prompt_id']}")
|
| 119 |
+
destination = causal_output / f"prompt_{prompt_id:04d}.pt"
|
| 120 |
+
atomic_save(destination, item)
|
| 121 |
+
inventory.append({
|
| 122 |
+
"prompt_id": prompt_id,
|
| 123 |
+
"source": str(source),
|
| 124 |
+
"destination": str(destination),
|
| 125 |
+
"tokens": int(item["features"]["early"].shape[2]),
|
| 126 |
+
})
|
| 127 |
+
manifest = {
|
| 128 |
+
"dataset_version": 1,
|
| 129 |
+
"chunk_pairing": "boundary_to_all",
|
| 130 |
+
"prompt_ids": list(range(args.num_prompts)),
|
| 131 |
+
"chunks": args.chunks,
|
| 132 |
+
"steps": args.steps,
|
| 133 |
+
"self_forcing": str(args.self_dir.resolve()),
|
| 134 |
+
"hy_worldplay": str(args.hy_dir.resolve()),
|
| 135 |
+
"causal_source": str(args.causal_root.resolve()),
|
| 136 |
+
"causal_inventory": inventory,
|
| 137 |
+
}
|
| 138 |
+
(output / "manifest.json").write_text(
|
| 139 |
+
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
|
| 140 |
+
encoding="utf-8",
|
| 141 |
+
)
|
| 142 |
+
print(f"[complete] {output} prompts={args.num_prompts}", flush=True)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
if __name__ == "__main__":
|
| 146 |
+
main()
|
scripts/build_conditional_probe_dataset.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Normalize three-model hidden snapshots into one offline probe dataset.
|
| 3 |
+
|
| 4 |
+
The recorder implementations live in their respective projects. This script
|
| 5 |
+
only converts their per-prompt snapshots to a small common CPU format; it does
|
| 6 |
+
not run a model or manufacture control examples.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from typing import Any
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _preparse_gpu() -> str:
|
| 20 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 21 |
+
parser.add_argument("--gpu", default="0")
|
| 22 |
+
args, _ = parser.parse_known_args()
|
| 23 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 24 |
+
return str(args.gpu)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 28 |
+
|
| 29 |
+
import numpy as np
|
| 30 |
+
import torch
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
ROLES = {
|
| 34 |
+
"self_forcing": {7: "early", 14: "middle", 22: "late", 29: "final"},
|
| 35 |
+
"causal_forcing": {7: "early", 14: "middle", 22: "late", 29: "final"},
|
| 36 |
+
"hy_worldplay": {13: "early", 26: "middle", 40: "late", 53: "final"},
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def regular_coords(frames: int = 3, height: int = 30, width: int = 52, max_tokens: int = 240):
|
| 41 |
+
total = frames * height * width
|
| 42 |
+
if total <= max_tokens:
|
| 43 |
+
flat = np.arange(total, dtype=np.int64)
|
| 44 |
+
else:
|
| 45 |
+
per_frame = max(1, max_tokens // frames)
|
| 46 |
+
h_count = min(height, max(1, int(round((per_frame * height / width) ** 0.5))))
|
| 47 |
+
w_count = min(width, max(1, per_frame // h_count))
|
| 48 |
+
while frames * h_count * w_count > max_tokens and w_count > 1:
|
| 49 |
+
w_count -= 1
|
| 50 |
+
while frames * h_count * w_count > max_tokens and h_count > 1:
|
| 51 |
+
h_count -= 1
|
| 52 |
+
hs = np.unique(np.rint(np.linspace(0, height - 1, h_count)).astype(np.int64))
|
| 53 |
+
ws = np.unique(np.rint(np.linspace(0, width - 1, w_count)).astype(np.int64))
|
| 54 |
+
flat = np.asarray(
|
| 55 |
+
[t * height * width + h * width + w for t in range(frames) for h in hs for w in ws],
|
| 56 |
+
dtype=np.int64,
|
| 57 |
+
)
|
| 58 |
+
t = flat // (height * width)
|
| 59 |
+
rem = flat % (height * width)
|
| 60 |
+
return np.stack([t, rem // width, rem % width], axis=1)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def ensure_stack(values: dict[tuple[int, int], torch.Tensor], layer: int, chunks: int, steps: int):
|
| 64 |
+
rows = []
|
| 65 |
+
for chunk in range(chunks):
|
| 66 |
+
step_rows = []
|
| 67 |
+
for step in range(steps):
|
| 68 |
+
key = (chunk, step)
|
| 69 |
+
if key not in values:
|
| 70 |
+
raise ValueError(f"Missing layer={layer} chunk={chunk} step={step}")
|
| 71 |
+
step_rows.append(values[key].detach().cpu().to(torch.float16))
|
| 72 |
+
rows.append(torch.stack(step_rows, dim=0))
|
| 73 |
+
return torch.stack(rows, dim=0).contiguous()
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def load_self(path: Path, layers: list[int], chunks: int, steps: int) -> dict[str, Any]:
|
| 77 |
+
run = torch.load(path, map_location="cpu", weights_only=False)
|
| 78 |
+
features = {}
|
| 79 |
+
for layer in layers:
|
| 80 |
+
stage = f"block_{layer}_hidden"
|
| 81 |
+
values = {}
|
| 82 |
+
for key, value in run["records"][stage].items():
|
| 83 |
+
c, s = (int(part) for part in key.split(":"))
|
| 84 |
+
if c < chunks and s < steps:
|
| 85 |
+
values[(c, s)] = value
|
| 86 |
+
features[ROLES["self_forcing"][layer]] = ensure_stack(values, layer, chunks, steps)
|
| 87 |
+
return {
|
| 88 |
+
"prompt_id": int(run["run_index"]),
|
| 89 |
+
"prompt": run["prompt"],
|
| 90 |
+
"seed": int(run["seed"]),
|
| 91 |
+
"model_family": "self_forcing",
|
| 92 |
+
"model_variant": "dmd4",
|
| 93 |
+
"features": features,
|
| 94 |
+
"timesteps": np.asarray([1000.0, 937.5, 833.3333, 625.0], dtype=np.float32),
|
| 95 |
+
"coords": regular_coords(),
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def load_causal(path: Path, layers: list[int], chunks: int, steps: int) -> dict[str, Any]:
|
| 100 |
+
run = torch.load(path, map_location="cpu", weights_only=False)
|
| 101 |
+
raw = {}
|
| 102 |
+
for key, value in run["features"].items():
|
| 103 |
+
layer, chunk, step = (int(part) for part in key.split(":"))
|
| 104 |
+
if layer in layers and chunk < chunks and step < steps:
|
| 105 |
+
raw.setdefault(layer, {})[(chunk, step)] = value
|
| 106 |
+
features = {
|
| 107 |
+
ROLES["causal_forcing"][layer]: ensure_stack(raw.get(layer, {}), layer, chunks, steps)
|
| 108 |
+
for layer in layers
|
| 109 |
+
}
|
| 110 |
+
return {
|
| 111 |
+
"prompt_id": int(run["prompt_id"]),
|
| 112 |
+
"prompt": run["prompt"],
|
| 113 |
+
"seed": int(run["seed"]),
|
| 114 |
+
"model_family": "causal_forcing",
|
| 115 |
+
"model_variant": "dmd4",
|
| 116 |
+
"features": features,
|
| 117 |
+
"timesteps": np.asarray([1000.0, 937.5, 833.3333, 625.0], dtype=np.float32),
|
| 118 |
+
"coords": regular_coords(),
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def load_hy(path: Path, layers: list[int], chunks: int, steps: int) -> dict[str, Any]:
|
| 123 |
+
data = np.load(path, allow_pickle=False)
|
| 124 |
+
stages = [str(value) for value in data["stages"]]
|
| 125 |
+
raw = {}
|
| 126 |
+
for index, stage in enumerate(stages):
|
| 127 |
+
if not stage.startswith("block_"):
|
| 128 |
+
continue
|
| 129 |
+
layer = int(stage.split("_")[-1])
|
| 130 |
+
chunk = int(data["chunks"][index])
|
| 131 |
+
step = int(data["steps"][index])
|
| 132 |
+
if layer in layers and chunk < chunks and step < steps:
|
| 133 |
+
raw.setdefault(layer, {})[(chunk, step)] = torch.from_numpy(data["features"][index])
|
| 134 |
+
features = {
|
| 135 |
+
ROLES["hy_worldplay"][layer]: ensure_stack(raw.get(layer, {}), layer, chunks, steps)
|
| 136 |
+
for layer in layers
|
| 137 |
+
}
|
| 138 |
+
return {
|
| 139 |
+
"features": features,
|
| 140 |
+
"timesteps": np.asarray(data["timesteps"], dtype=np.float32),
|
| 141 |
+
"coords": np.asarray(data["coords"], dtype=np.int64),
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def atomic_save(path: Path, value: dict[str, Any]) -> None:
|
| 146 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 147 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 148 |
+
torch.save(value, temporary)
|
| 149 |
+
os.replace(temporary, path)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def parse_args() -> argparse.Namespace:
|
| 153 |
+
parser = argparse.ArgumentParser()
|
| 154 |
+
parser.add_argument("--self_root", type=Path, required=True)
|
| 155 |
+
parser.add_argument("--causal_root", type=Path, required=True)
|
| 156 |
+
parser.add_argument("--hy_root", type=Path, required=True)
|
| 157 |
+
parser.add_argument("--output_root", type=Path, required=True)
|
| 158 |
+
parser.add_argument("--chunks", type=int, default=4)
|
| 159 |
+
parser.add_argument("--steps", type=int, default=4)
|
| 160 |
+
parser.add_argument("--max_prompts", type=int, default=10)
|
| 161 |
+
return parser.parse_args()
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def main() -> None:
|
| 165 |
+
args = parse_args()
|
| 166 |
+
args.output_root.mkdir(parents=True, exist_ok=True)
|
| 167 |
+
specs = {
|
| 168 |
+
"self_forcing": ([7, 14, 22, 29], args.self_root / "runs", "self"),
|
| 169 |
+
"causal_forcing": ([7, 14, 22, 29], args.causal_root / "runs", "causal"),
|
| 170 |
+
}
|
| 171 |
+
inventory = []
|
| 172 |
+
for family, (layers, run_root, prefix) in specs.items():
|
| 173 |
+
out_dir = args.output_root / family
|
| 174 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 175 |
+
for prompt_id in range(args.max_prompts):
|
| 176 |
+
if family == "self_forcing":
|
| 177 |
+
source = run_root / f"prompt_{prompt_id:02d}.pt"
|
| 178 |
+
if not source.exists():
|
| 179 |
+
raise FileNotFoundError(source)
|
| 180 |
+
item = load_self(source, layers, args.chunks, args.steps)
|
| 181 |
+
else:
|
| 182 |
+
source = run_root / f"prompt_{prompt_id:04d}" / "feature_snapshots.pt"
|
| 183 |
+
if not source.exists():
|
| 184 |
+
raise FileNotFoundError(source)
|
| 185 |
+
item = load_causal(source, layers, args.chunks, args.steps)
|
| 186 |
+
destination = out_dir / f"prompt_{prompt_id:04d}.pt"
|
| 187 |
+
atomic_save(destination, item)
|
| 188 |
+
inventory.append({
|
| 189 |
+
"family": family,
|
| 190 |
+
"prompt_id": prompt_id,
|
| 191 |
+
"path": str(destination),
|
| 192 |
+
"bytes": destination.stat().st_size,
|
| 193 |
+
"roles": sorted(item["features"]),
|
| 194 |
+
})
|
| 195 |
+
|
| 196 |
+
hy_files = sorted(args.hy_root.glob("shard_gpu*/runs/prompt_*/forward/final_hidden_snapshots.npz"))
|
| 197 |
+
hy_by_prompt = {}
|
| 198 |
+
for source in hy_files:
|
| 199 |
+
prompt_id = int(source.parts[-3].split("_")[-1])
|
| 200 |
+
if prompt_id < args.max_prompts:
|
| 201 |
+
hy_by_prompt[prompt_id] = source
|
| 202 |
+
out_dir = args.output_root / "hy_worldplay"
|
| 203 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 204 |
+
for prompt_id in range(args.max_prompts):
|
| 205 |
+
source = hy_by_prompt.get(prompt_id)
|
| 206 |
+
if source is None:
|
| 207 |
+
raise FileNotFoundError(f"HY snapshot for prompt {prompt_id}")
|
| 208 |
+
item = load_hy(source, [13, 26, 40, 53], args.chunks, args.steps)
|
| 209 |
+
item.update({
|
| 210 |
+
"prompt_id": prompt_id,
|
| 211 |
+
"model_family": "hy_worldplay",
|
| 212 |
+
"model_variant": "ar4",
|
| 213 |
+
"seed": 0,
|
| 214 |
+
"prompt": f"prompt_{prompt_id:04d}",
|
| 215 |
+
})
|
| 216 |
+
destination = out_dir / f"prompt_{prompt_id:04d}.pt"
|
| 217 |
+
atomic_save(destination, item)
|
| 218 |
+
inventory.append({
|
| 219 |
+
"family": "hy_worldplay",
|
| 220 |
+
"prompt_id": prompt_id,
|
| 221 |
+
"path": str(destination),
|
| 222 |
+
"bytes": destination.stat().st_size,
|
| 223 |
+
"roles": sorted(item["features"]),
|
| 224 |
+
})
|
| 225 |
+
|
| 226 |
+
manifest = {
|
| 227 |
+
"dataset_version": 1,
|
| 228 |
+
"prompt_ids": list(range(args.max_prompts)),
|
| 229 |
+
"chunks": args.chunks,
|
| 230 |
+
"steps": args.steps,
|
| 231 |
+
"max_tokens": 240,
|
| 232 |
+
"roles": ["early", "middle", "late", "final"],
|
| 233 |
+
"source_roots": {
|
| 234 |
+
"self_forcing": str(args.self_root),
|
| 235 |
+
"causal_forcing": str(args.causal_root),
|
| 236 |
+
"hy_worldplay": str(args.hy_root),
|
| 237 |
+
},
|
| 238 |
+
"inventory": inventory,
|
| 239 |
+
}
|
| 240 |
+
(args.output_root / "manifest.json").write_text(
|
| 241 |
+
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
| 242 |
+
)
|
| 243 |
+
print(f"[complete] {args.output_root} prompts={args.max_prompts} files={len(inventory)}", flush=True)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
if __name__ == "__main__":
|
| 247 |
+
main()
|
scripts/build_predictor_offline_data.py
ADDED
|
@@ -0,0 +1,878 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Build offline FFFF trajectories for the lightweight Self-Forcing predictor.
|
| 3 |
+
|
| 4 |
+
The dataset is prompt-sharded and resumable. Common denoising tensors are
|
| 5 |
+
stored once per prompt, while clean self-attention prefeatures are stored in
|
| 6 |
+
one sidecar per Teacher block. This layout avoids duplicating history tensors
|
| 7 |
+
across the 18 adjacent-step training samples produced by each prompt.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import hashlib
|
| 14 |
+
import json
|
| 15 |
+
import os
|
| 16 |
+
import random
|
| 17 |
+
import shutil
|
| 18 |
+
import sys
|
| 19 |
+
import time
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Any
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _preparse_gpu() -> str:
|
| 25 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 26 |
+
parser.add_argument("--gpu", default="2")
|
| 27 |
+
args, _ = parser.parse_known_args()
|
| 28 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 29 |
+
return str(args.gpu)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 33 |
+
|
| 34 |
+
import torch
|
| 35 |
+
from omegaconf import OmegaConf
|
| 36 |
+
from safetensors.torch import save_file
|
| 37 |
+
|
| 38 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 39 |
+
if str(REPO_ROOT) not in sys.path:
|
| 40 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 41 |
+
|
| 42 |
+
from pipeline import CausalInferencePipeline
|
| 43 |
+
from utils.misc import set_seed
|
| 44 |
+
from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
DATASET_VERSION = 2
|
| 48 |
+
EXCLUDED_CHUNKS = (0,)
|
| 49 |
+
LATENT_CHANNELS = 16
|
| 50 |
+
LATENT_HEIGHT = 60
|
| 51 |
+
LATENT_WIDTH = 104
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def parse_args() -> argparse.Namespace:
|
| 55 |
+
parser = argparse.ArgumentParser(
|
| 56 |
+
description="Build full-step Self-Forcing predictor trajectories"
|
| 57 |
+
)
|
| 58 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 59 |
+
parser.add_argument(
|
| 60 |
+
"--config_path",
|
| 61 |
+
type=Path,
|
| 62 |
+
default=Path("configs/self_forcing_sid.yaml"),
|
| 63 |
+
)
|
| 64 |
+
parser.add_argument(
|
| 65 |
+
"--checkpoint_path",
|
| 66 |
+
type=Path,
|
| 67 |
+
default=Path("checkpoints/self_forcing_dmd.pt"),
|
| 68 |
+
)
|
| 69 |
+
parser.add_argument(
|
| 70 |
+
"--prompt_path",
|
| 71 |
+
type=Path,
|
| 72 |
+
default=Path("prompts/vidprom_filtered_extended.txt"),
|
| 73 |
+
)
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--validation_prompt_path",
|
| 76 |
+
type=Path,
|
| 77 |
+
default=Path("prompts/MovieGenVideoBench_extended.txt"),
|
| 78 |
+
)
|
| 79 |
+
parser.add_argument("--output_dir", type=Path, required=True)
|
| 80 |
+
parser.add_argument("--num_prompts", type=int, default=100)
|
| 81 |
+
parser.add_argument(
|
| 82 |
+
"--prompt_ids",
|
| 83 |
+
type=int,
|
| 84 |
+
nargs="*",
|
| 85 |
+
default=None,
|
| 86 |
+
help=(
|
| 87 |
+
"Only materialize these selected prompt IDs. The manifest still "
|
| 88 |
+
"records the full deterministic prompt selection."
|
| 89 |
+
),
|
| 90 |
+
)
|
| 91 |
+
parser.add_argument("--num_frames", type=int, default=21)
|
| 92 |
+
parser.add_argument("--selection_seed", type=int, default=0)
|
| 93 |
+
parser.add_argument("--generation_seed", type=int, default=0)
|
| 94 |
+
parser.add_argument(
|
| 95 |
+
"--layers",
|
| 96 |
+
type=int,
|
| 97 |
+
nargs="*",
|
| 98 |
+
default=None,
|
| 99 |
+
help="Teacher blocks to cache. Omit to cache every block.",
|
| 100 |
+
)
|
| 101 |
+
parser.add_argument(
|
| 102 |
+
"--max_new_prompts",
|
| 103 |
+
type=int,
|
| 104 |
+
default=None,
|
| 105 |
+
help="Stop after this many new prompt shards; use 1 for the dry run.",
|
| 106 |
+
)
|
| 107 |
+
parser.add_argument(
|
| 108 |
+
"--min_free_gib",
|
| 109 |
+
type=float,
|
| 110 |
+
default=50.0,
|
| 111 |
+
help="Stop before a new prompt if free disk space falls below this value.",
|
| 112 |
+
)
|
| 113 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 114 |
+
args = parser.parse_args()
|
| 115 |
+
|
| 116 |
+
if args.num_prompts < 1:
|
| 117 |
+
parser.error("--num_prompts must be positive")
|
| 118 |
+
if args.prompt_ids is not None and any(
|
| 119 |
+
value < 0 or value >= args.num_prompts for value in args.prompt_ids
|
| 120 |
+
):
|
| 121 |
+
parser.error("--prompt_ids must be within [0, --num_prompts)")
|
| 122 |
+
if args.num_frames < 1 or args.num_frames % 3:
|
| 123 |
+
parser.error("--num_frames must be a positive multiple of 3")
|
| 124 |
+
if args.max_new_prompts is not None and args.max_new_prompts < 0:
|
| 125 |
+
parser.error("--max_new_prompts must be non-negative")
|
| 126 |
+
return args
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def resolve_path(path: Path) -> Path:
|
| 130 |
+
path = path.expanduser()
|
| 131 |
+
return path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve()
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def atomic_write_json(path: Path, value: Any) -> None:
|
| 135 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 136 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 137 |
+
temporary.write_text(
|
| 138 |
+
json.dumps(value, indent=2, ensure_ascii=False) + "\n",
|
| 139 |
+
encoding="utf-8",
|
| 140 |
+
)
|
| 141 |
+
os.replace(temporary, path)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def file_sha256(path: Path) -> str:
|
| 145 |
+
digest = hashlib.sha256()
|
| 146 |
+
with path.open("rb") as handle:
|
| 147 |
+
while chunk := handle.read(8 * 1024 * 1024):
|
| 148 |
+
digest.update(chunk)
|
| 149 |
+
return digest.hexdigest()
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def read_nonempty_lines(path: Path) -> list[str]:
|
| 153 |
+
with path.open("r", encoding="utf-8") as handle:
|
| 154 |
+
return [line.strip() for line in handle if line.strip()]
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def select_prompts(
|
| 158 |
+
prompt_path: Path,
|
| 159 |
+
validation_prompt_path: Path,
|
| 160 |
+
num_prompts: int,
|
| 161 |
+
seed: int,
|
| 162 |
+
) -> list[dict[str, Any]]:
|
| 163 |
+
source = read_nonempty_lines(prompt_path)
|
| 164 |
+
validation = set(read_nonempty_lines(validation_prompt_path)[:100])
|
| 165 |
+
eligible = [
|
| 166 |
+
{"source_index": index, "prompt": prompt}
|
| 167 |
+
for index, prompt in enumerate(source)
|
| 168 |
+
if prompt not in validation
|
| 169 |
+
]
|
| 170 |
+
if len(eligible) < num_prompts:
|
| 171 |
+
raise ValueError(
|
| 172 |
+
f"Only {len(eligible)} eligible prompts remain after excluding "
|
| 173 |
+
f"the first 100 validation prompts; requested {num_prompts}"
|
| 174 |
+
)
|
| 175 |
+
return random.Random(seed).sample(eligible, num_prompts)
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def tensor_to_bf16_cpu(value: torch.Tensor) -> torch.Tensor:
|
| 179 |
+
return value.detach().to(device="cpu", dtype=torch.bfloat16).contiguous()
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def atomic_save_safetensors(
|
| 183 |
+
tensors: dict[str, torch.Tensor],
|
| 184 |
+
path: Path,
|
| 185 |
+
metadata: dict[str, str],
|
| 186 |
+
) -> None:
|
| 187 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 188 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 189 |
+
save_file(tensors, temporary, metadata=metadata)
|
| 190 |
+
os.replace(temporary, path)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def directory_size(path: Path) -> int:
|
| 194 |
+
return sum(item.stat().st_size for item in path.rglob("*") if item.is_file())
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class TrajectoryRecorder:
|
| 198 |
+
"""Capture final hidden states and clean K-projection inputs."""
|
| 199 |
+
|
| 200 |
+
def __init__(self, model: torch.nn.Module, layers: list[int]) -> None:
|
| 201 |
+
self.model = model
|
| 202 |
+
self.layers = layers
|
| 203 |
+
self.mode: str | None = None
|
| 204 |
+
self.final_hidden: torch.Tensor | None = None
|
| 205 |
+
self.current_clean: dict[int, torch.Tensor] = {}
|
| 206 |
+
self.clean_prefeatures: dict[int, list[torch.Tensor]] = {
|
| 207 |
+
layer: [] for layer in layers
|
| 208 |
+
}
|
| 209 |
+
self.handles: list[Any] = []
|
| 210 |
+
|
| 211 |
+
self.handles.append(
|
| 212 |
+
model.head.register_forward_pre_hook(self._head_pre_hook)
|
| 213 |
+
)
|
| 214 |
+
for layer in layers:
|
| 215 |
+
self.handles.append(
|
| 216 |
+
model.blocks[layer].self_attn.k.register_forward_pre_hook(
|
| 217 |
+
self._make_clean_prefeature_hook(layer)
|
| 218 |
+
)
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
def close(self) -> None:
|
| 222 |
+
for handle in self.handles:
|
| 223 |
+
handle.remove()
|
| 224 |
+
self.handles.clear()
|
| 225 |
+
|
| 226 |
+
def start_denoising_step(self) -> None:
|
| 227 |
+
self.mode = "denoise"
|
| 228 |
+
self.final_hidden = None
|
| 229 |
+
|
| 230 |
+
def finish_denoising_step(self) -> torch.Tensor:
|
| 231 |
+
if self.final_hidden is None:
|
| 232 |
+
raise RuntimeError("The Teacher head hook did not capture final_hidden")
|
| 233 |
+
value = self.final_hidden
|
| 234 |
+
self.final_hidden = None
|
| 235 |
+
self.mode = None
|
| 236 |
+
return value
|
| 237 |
+
|
| 238 |
+
def start_clean_pass(self) -> None:
|
| 239 |
+
self.mode = "clean"
|
| 240 |
+
self.current_clean = {}
|
| 241 |
+
|
| 242 |
+
def finish_clean_pass(self, *, store: bool = True) -> dict[int, torch.Tensor]:
|
| 243 |
+
missing = sorted(set(self.layers) - set(self.current_clean))
|
| 244 |
+
if missing:
|
| 245 |
+
raise RuntimeError(
|
| 246 |
+
f"Clean pass did not capture prefeatures for blocks {missing}"
|
| 247 |
+
)
|
| 248 |
+
captured = self.current_clean
|
| 249 |
+
if store:
|
| 250 |
+
for layer in self.layers:
|
| 251 |
+
self.clean_prefeatures[layer].append(captured[layer])
|
| 252 |
+
self.current_clean = {}
|
| 253 |
+
self.mode = None
|
| 254 |
+
return captured
|
| 255 |
+
|
| 256 |
+
def _head_pre_hook(
|
| 257 |
+
self, _module: torch.nn.Module, inputs: tuple[torch.Tensor, ...]
|
| 258 |
+
) -> None:
|
| 259 |
+
if self.mode != "denoise":
|
| 260 |
+
return
|
| 261 |
+
if self.final_hidden is not None:
|
| 262 |
+
raise RuntimeError("Captured final_hidden more than once in one step")
|
| 263 |
+
if not inputs or not isinstance(inputs[0], torch.Tensor):
|
| 264 |
+
raise RuntimeError("Unexpected Teacher head inputs")
|
| 265 |
+
self.final_hidden = tensor_to_bf16_cpu(inputs[0])
|
| 266 |
+
|
| 267 |
+
def _make_clean_prefeature_hook(self, layer: int):
|
| 268 |
+
def hook(
|
| 269 |
+
_module: torch.nn.Module, inputs: tuple[torch.Tensor, ...]
|
| 270 |
+
) -> None:
|
| 271 |
+
if self.mode != "clean":
|
| 272 |
+
return
|
| 273 |
+
if layer in self.current_clean:
|
| 274 |
+
raise RuntimeError(
|
| 275 |
+
f"Captured block {layer} clean prefeature more than once"
|
| 276 |
+
)
|
| 277 |
+
if not inputs or not isinstance(inputs[0], torch.Tensor):
|
| 278 |
+
raise RuntimeError(f"Unexpected block {layer} K inputs")
|
| 279 |
+
self.current_clean[layer] = tensor_to_bf16_cpu(inputs[0])
|
| 280 |
+
|
| 281 |
+
return hook
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def build_pipeline(
|
| 285 |
+
config: Any, checkpoint_path: Path, device: torch.device
|
| 286 |
+
) -> CausalInferencePipeline:
|
| 287 |
+
generator = WanDiffusionWrapper(
|
| 288 |
+
**getattr(config, "model_kwargs", {}), is_causal=True
|
| 289 |
+
)
|
| 290 |
+
text_encoder = WanTextEncoder()
|
| 291 |
+
pipeline = CausalInferencePipeline(
|
| 292 |
+
config,
|
| 293 |
+
device=device,
|
| 294 |
+
generator=generator,
|
| 295 |
+
text_encoder=text_encoder,
|
| 296 |
+
vae=torch.nn.Identity(),
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
checkpoint = torch.load(
|
| 300 |
+
checkpoint_path, map_location="cpu", weights_only=False, mmap=True
|
| 301 |
+
)
|
| 302 |
+
if set(checkpoint) != {"generator_ema"}:
|
| 303 |
+
raise KeyError(
|
| 304 |
+
f"Expected checkpoint key generator_ema, found {sorted(checkpoint)}"
|
| 305 |
+
)
|
| 306 |
+
pipeline.generator.load_state_dict(checkpoint["generator_ema"], strict=True)
|
| 307 |
+
del checkpoint
|
| 308 |
+
|
| 309 |
+
pipeline.to(dtype=torch.bfloat16)
|
| 310 |
+
pipeline.text_encoder.to(device=device)
|
| 311 |
+
pipeline.generator.to(device=device)
|
| 312 |
+
pipeline.eval()
|
| 313 |
+
pipeline.generator.model.requires_grad_(False)
|
| 314 |
+
pipeline.text_encoder.requires_grad_(False)
|
| 315 |
+
return pipeline
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def reset_caches(
|
| 319 |
+
pipeline: CausalInferencePipeline,
|
| 320 |
+
batch_size: int,
|
| 321 |
+
dtype: torch.dtype,
|
| 322 |
+
device: torch.device,
|
| 323 |
+
) -> None:
|
| 324 |
+
if pipeline.kv_cache1 is None:
|
| 325 |
+
pipeline._initialize_kv_cache(batch_size, dtype, device)
|
| 326 |
+
pipeline._initialize_crossattn_cache(batch_size, dtype, device)
|
| 327 |
+
return
|
| 328 |
+
|
| 329 |
+
for cache in pipeline.kv_cache1:
|
| 330 |
+
cache["global_end_index"].zero_()
|
| 331 |
+
cache["local_end_index"].zero_()
|
| 332 |
+
for cache in pipeline.crossattn_cache:
|
| 333 |
+
cache["is_init"] = False
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def collect_cross_attention_cache(
|
| 337 |
+
pipeline: CausalInferencePipeline,
|
| 338 |
+
layers: list[int],
|
| 339 |
+
) -> dict[str, torch.Tensor]:
|
| 340 |
+
output: dict[str, torch.Tensor] = {}
|
| 341 |
+
for layer in layers:
|
| 342 |
+
cache = pipeline.crossattn_cache[layer]
|
| 343 |
+
if not cache["is_init"]:
|
| 344 |
+
raise RuntimeError(f"Cross-attention cache for block {layer} is empty")
|
| 345 |
+
output[f"block_{layer:02d}_k"] = tensor_to_bf16_cpu(cache["k"])
|
| 346 |
+
output[f"block_{layer:02d}_v"] = tensor_to_bf16_cpu(cache["v"])
|
| 347 |
+
return output
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
@torch.inference_mode()
|
| 351 |
+
def generate_prompt(
|
| 352 |
+
pipeline: CausalInferencePipeline,
|
| 353 |
+
recorder: TrajectoryRecorder,
|
| 354 |
+
prompt: str,
|
| 355 |
+
num_frames: int,
|
| 356 |
+
generation_seed: int,
|
| 357 |
+
device: torch.device,
|
| 358 |
+
) -> tuple[
|
| 359 |
+
dict[str, torch.Tensor],
|
| 360 |
+
dict[int, list[torch.Tensor]],
|
| 361 |
+
dict[str, torch.Tensor],
|
| 362 |
+
dict[int, torch.Tensor],
|
| 363 |
+
dict[str, torch.Tensor],
|
| 364 |
+
float,
|
| 365 |
+
float,
|
| 366 |
+
]:
|
| 367 |
+
set_seed(generation_seed)
|
| 368 |
+
reset_caches(pipeline, 1, torch.bfloat16, device)
|
| 369 |
+
recorder.clean_prefeatures = {layer: [] for layer in recorder.layers}
|
| 370 |
+
|
| 371 |
+
conditional_dict = pipeline.text_encoder(text_prompts=[prompt])
|
| 372 |
+
noise = torch.randn(
|
| 373 |
+
1,
|
| 374 |
+
num_frames,
|
| 375 |
+
LATENT_CHANNELS,
|
| 376 |
+
LATENT_HEIGHT,
|
| 377 |
+
LATENT_WIDTH,
|
| 378 |
+
dtype=torch.bfloat16,
|
| 379 |
+
device=device,
|
| 380 |
+
)
|
| 381 |
+
trajectory: dict[str, torch.Tensor] = {}
|
| 382 |
+
chunk0_trajectory: dict[str, torch.Tensor] = {}
|
| 383 |
+
chunk0_prefeatures: dict[int, torch.Tensor] = {}
|
| 384 |
+
chunk_size = pipeline.num_frame_per_block
|
| 385 |
+
num_chunks = num_frames // chunk_size
|
| 386 |
+
timesteps = pipeline.denoising_step_list.to(device=device)
|
| 387 |
+
|
| 388 |
+
torch.cuda.reset_peak_memory_stats()
|
| 389 |
+
torch.cuda.synchronize()
|
| 390 |
+
start_time = time.perf_counter()
|
| 391 |
+
|
| 392 |
+
current_start_frame = 0
|
| 393 |
+
for chunk in range(num_chunks):
|
| 394 |
+
noisy_input = noise[
|
| 395 |
+
:, current_start_frame : current_start_frame + chunk_size
|
| 396 |
+
]
|
| 397 |
+
timestep: torch.Tensor | None = None
|
| 398 |
+
denoised_pred: torch.Tensor | None = None
|
| 399 |
+
|
| 400 |
+
for step, current_timestep in enumerate(timesteps):
|
| 401 |
+
timestep = (
|
| 402 |
+
torch.ones(
|
| 403 |
+
[1, chunk_size],
|
| 404 |
+
device=device,
|
| 405 |
+
dtype=torch.int64,
|
| 406 |
+
)
|
| 407 |
+
* current_timestep
|
| 408 |
+
)
|
| 409 |
+
prefix = f"chunk_{chunk:02d}_step_{step:02d}"
|
| 410 |
+
if chunk not in EXCLUDED_CHUNKS:
|
| 411 |
+
trajectory[f"{prefix}_noisy_latent"] = tensor_to_bf16_cpu(
|
| 412 |
+
noisy_input
|
| 413 |
+
)
|
| 414 |
+
trajectory[f"{prefix}_timestep"] = (
|
| 415 |
+
timestep.detach()
|
| 416 |
+
.to(device="cpu", dtype=torch.float32)
|
| 417 |
+
.contiguous()
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
recorder.start_denoising_step()
|
| 421 |
+
flow, denoised_pred = pipeline.generator(
|
| 422 |
+
noisy_image_or_video=noisy_input,
|
| 423 |
+
conditional_dict=conditional_dict,
|
| 424 |
+
timestep=timestep,
|
| 425 |
+
kv_cache=pipeline.kv_cache1,
|
| 426 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 427 |
+
current_start=current_start_frame * pipeline.frame_seq_length,
|
| 428 |
+
)
|
| 429 |
+
final_hidden = recorder.finish_denoising_step()
|
| 430 |
+
if chunk not in EXCLUDED_CHUNKS:
|
| 431 |
+
trajectory[f"{prefix}_final_hidden"] = final_hidden
|
| 432 |
+
trajectory[f"{prefix}_flow"] = tensor_to_bf16_cpu(flow)
|
| 433 |
+
else:
|
| 434 |
+
chunk0_trajectory[f"{prefix}_final_hidden"] = final_hidden
|
| 435 |
+
|
| 436 |
+
if step < len(timesteps) - 1:
|
| 437 |
+
next_timestep = timesteps[step + 1]
|
| 438 |
+
denoised_flat = denoised_pred.flatten(0, 1)
|
| 439 |
+
noisy_input = pipeline.scheduler.add_noise(
|
| 440 |
+
denoised_flat,
|
| 441 |
+
torch.randn_like(denoised_flat),
|
| 442 |
+
next_timestep
|
| 443 |
+
* torch.ones(
|
| 444 |
+
[chunk_size], device=device, dtype=torch.long
|
| 445 |
+
),
|
| 446 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 447 |
+
|
| 448 |
+
if denoised_pred is None or timestep is None:
|
| 449 |
+
raise RuntimeError("Denoising loop produced no output")
|
| 450 |
+
|
| 451 |
+
if chunk not in EXCLUDED_CHUNKS:
|
| 452 |
+
trajectory[f"chunk_{chunk:02d}_clean_latent"] = (
|
| 453 |
+
tensor_to_bf16_cpu(denoised_pred)
|
| 454 |
+
)
|
| 455 |
+
|
| 456 |
+
recorder.start_clean_pass()
|
| 457 |
+
context_timestep = torch.ones_like(timestep) * pipeline.args.context_noise
|
| 458 |
+
pipeline.generator(
|
| 459 |
+
noisy_image_or_video=denoised_pred,
|
| 460 |
+
conditional_dict=conditional_dict,
|
| 461 |
+
timestep=context_timestep,
|
| 462 |
+
kv_cache=pipeline.kv_cache1,
|
| 463 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 464 |
+
current_start=current_start_frame * pipeline.frame_seq_length,
|
| 465 |
+
)
|
| 466 |
+
captured_clean = recorder.finish_clean_pass(
|
| 467 |
+
store=chunk not in EXCLUDED_CHUNKS
|
| 468 |
+
)
|
| 469 |
+
if chunk in EXCLUDED_CHUNKS:
|
| 470 |
+
if chunk != 0:
|
| 471 |
+
raise RuntimeError(f"Unsupported excluded context chunk {chunk}")
|
| 472 |
+
chunk0_prefeatures = captured_clean
|
| 473 |
+
current_start_frame += chunk_size
|
| 474 |
+
|
| 475 |
+
cross_attention = collect_cross_attention_cache(pipeline, recorder.layers)
|
| 476 |
+
torch.cuda.synchronize()
|
| 477 |
+
elapsed = time.perf_counter() - start_time
|
| 478 |
+
peak_gib = torch.cuda.max_memory_allocated() / (1024**3)
|
| 479 |
+
|
| 480 |
+
del conditional_dict, noise
|
| 481 |
+
return (
|
| 482 |
+
trajectory,
|
| 483 |
+
recorder.clean_prefeatures,
|
| 484 |
+
chunk0_trajectory,
|
| 485 |
+
chunk0_prefeatures,
|
| 486 |
+
cross_attention,
|
| 487 |
+
elapsed,
|
| 488 |
+
peak_gib,
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
def save_prompt_shard(
|
| 493 |
+
output_dir: Path,
|
| 494 |
+
prompt_index: int,
|
| 495 |
+
selection: dict[str, Any],
|
| 496 |
+
trajectory: dict[str, torch.Tensor],
|
| 497 |
+
clean_prefeatures: dict[int, list[torch.Tensor]],
|
| 498 |
+
chunk0_trajectory: dict[str, torch.Tensor],
|
| 499 |
+
chunk0_prefeatures: dict[int, torch.Tensor],
|
| 500 |
+
cross_attention: dict[str, torch.Tensor],
|
| 501 |
+
elapsed_s: float,
|
| 502 |
+
peak_gpu_gib: float,
|
| 503 |
+
layers: list[int],
|
| 504 |
+
generation_seed: int,
|
| 505 |
+
num_chunks: int,
|
| 506 |
+
) -> Path:
|
| 507 |
+
destination = output_dir / f"prompt_{prompt_index:04d}"
|
| 508 |
+
partial = output_dir / f"prompt_{prompt_index:04d}.partial"
|
| 509 |
+
if partial.exists():
|
| 510 |
+
shutil.rmtree(partial)
|
| 511 |
+
partial.mkdir(parents=True)
|
| 512 |
+
|
| 513 |
+
shared_metadata = {
|
| 514 |
+
"dataset_version": str(DATASET_VERSION),
|
| 515 |
+
"dtype": "bfloat16",
|
| 516 |
+
"prompt_index": str(prompt_index),
|
| 517 |
+
}
|
| 518 |
+
atomic_save_safetensors(
|
| 519 |
+
trajectory,
|
| 520 |
+
partial / "trajectory.safetensors",
|
| 521 |
+
{**shared_metadata, "kind": "trajectory"},
|
| 522 |
+
)
|
| 523 |
+
atomic_save_safetensors(
|
| 524 |
+
cross_attention,
|
| 525 |
+
partial / "cross_attention.safetensors",
|
| 526 |
+
{**shared_metadata, "kind": "cross_attention_kv"},
|
| 527 |
+
)
|
| 528 |
+
atomic_save_safetensors(
|
| 529 |
+
chunk0_trajectory,
|
| 530 |
+
partial / "chunk0_context" / "trajectory.safetensors",
|
| 531 |
+
{**shared_metadata, "kind": "chunk0_context_final_hidden"},
|
| 532 |
+
)
|
| 533 |
+
for layer in layers:
|
| 534 |
+
atomic_save_safetensors(
|
| 535 |
+
{"chunk_00": chunk0_prefeatures[layer]},
|
| 536 |
+
partial
|
| 537 |
+
/ "chunk0_context"
|
| 538 |
+
/ "clean_prefeatures"
|
| 539 |
+
/ f"block_{layer:02d}.safetensors",
|
| 540 |
+
{
|
| 541 |
+
**shared_metadata,
|
| 542 |
+
"kind": "chunk0_context_clean_self_attention_k_input",
|
| 543 |
+
"block_id": str(layer),
|
| 544 |
+
},
|
| 545 |
+
)
|
| 546 |
+
atomic_write_json(
|
| 547 |
+
partial / "chunk0_context" / "metadata.json",
|
| 548 |
+
{
|
| 549 |
+
"kind": "context_only",
|
| 550 |
+
"chunk": 0,
|
| 551 |
+
"is_training_target": False,
|
| 552 |
+
"hidden_steps": [0, 1, 2, 3],
|
| 553 |
+
"layers": layers,
|
| 554 |
+
},
|
| 555 |
+
)
|
| 556 |
+
(partial / "chunk0_context" / "_SUCCESS").write_text(
|
| 557 |
+
"ok\n", encoding="utf-8"
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
prefeature_shapes: dict[str, list[int]] = {}
|
| 561 |
+
for layer in layers:
|
| 562 |
+
values = clean_prefeatures[layer]
|
| 563 |
+
stored_chunks = [
|
| 564 |
+
chunk for chunk in range(num_chunks) if chunk not in EXCLUDED_CHUNKS
|
| 565 |
+
]
|
| 566 |
+
if len(values) != len(stored_chunks):
|
| 567 |
+
raise RuntimeError(
|
| 568 |
+
f"Expected {len(stored_chunks)} stored chunks, got {len(values)}"
|
| 569 |
+
)
|
| 570 |
+
tensors = {
|
| 571 |
+
f"chunk_{chunk:02d}": value
|
| 572 |
+
for chunk, value in zip(stored_chunks, values)
|
| 573 |
+
}
|
| 574 |
+
atomic_save_safetensors(
|
| 575 |
+
tensors,
|
| 576 |
+
partial / "clean_prefeatures" / f"block_{layer:02d}.safetensors",
|
| 577 |
+
{
|
| 578 |
+
**shared_metadata,
|
| 579 |
+
"kind": "clean_self_attention_k_input",
|
| 580 |
+
"block_id": str(layer),
|
| 581 |
+
},
|
| 582 |
+
)
|
| 583 |
+
if values:
|
| 584 |
+
prefeature_shapes[str(layer)] = list(values[0].shape)
|
| 585 |
+
|
| 586 |
+
metadata = {
|
| 587 |
+
"dataset_version": DATASET_VERSION,
|
| 588 |
+
"prompt_index": prompt_index,
|
| 589 |
+
"source_index": selection["source_index"],
|
| 590 |
+
"prompt": selection["prompt"],
|
| 591 |
+
"generation_seed": generation_seed,
|
| 592 |
+
"dtype": "bfloat16",
|
| 593 |
+
"layers": layers,
|
| 594 |
+
"num_clean_chunks": len(clean_prefeatures[layers[0]]),
|
| 595 |
+
"excluded_chunks": list(EXCLUDED_CHUNKS),
|
| 596 |
+
"stored_chunks": [
|
| 597 |
+
chunk for chunk in range(num_chunks) if chunk not in EXCLUDED_CHUNKS
|
| 598 |
+
],
|
| 599 |
+
"prefeature_shapes": prefeature_shapes,
|
| 600 |
+
"elapsed_s": elapsed_s,
|
| 601 |
+
"peak_gpu_gib": peak_gpu_gib,
|
| 602 |
+
}
|
| 603 |
+
atomic_write_json(partial / "metadata.json", metadata)
|
| 604 |
+
(partial / "_SUCCESS").write_text("ok\n", encoding="utf-8")
|
| 605 |
+
os.replace(partial, destination)
|
| 606 |
+
return destination
|
| 607 |
+
|
| 608 |
+
|
| 609 |
+
def prepare_manifest(
|
| 610 |
+
args: argparse.Namespace,
|
| 611 |
+
config: Any,
|
| 612 |
+
prompt_path: Path,
|
| 613 |
+
validation_prompt_path: Path,
|
| 614 |
+
checkpoint_path: Path,
|
| 615 |
+
output_dir: Path,
|
| 616 |
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
| 617 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 618 |
+
prompt_selection_path = output_dir / "prompt_selection.json"
|
| 619 |
+
selected = select_prompts(
|
| 620 |
+
prompt_path,
|
| 621 |
+
validation_prompt_path,
|
| 622 |
+
args.num_prompts,
|
| 623 |
+
args.selection_seed,
|
| 624 |
+
)
|
| 625 |
+
selection_document = {
|
| 626 |
+
"selection_seed": args.selection_seed,
|
| 627 |
+
"num_prompts": args.num_prompts,
|
| 628 |
+
"prompt_source": str(prompt_path),
|
| 629 |
+
"prompt_source_sha256": file_sha256(prompt_path),
|
| 630 |
+
"validation_source": str(validation_prompt_path),
|
| 631 |
+
"validation_source_sha256": file_sha256(validation_prompt_path),
|
| 632 |
+
"excluded_validation_count": 100,
|
| 633 |
+
"prompts": selected,
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
if prompt_selection_path.exists() and not args.overwrite:
|
| 637 |
+
existing = json.loads(prompt_selection_path.read_text(encoding="utf-8"))
|
| 638 |
+
if existing != selection_document:
|
| 639 |
+
raise RuntimeError(
|
| 640 |
+
"Existing prompt_selection.json differs from the requested "
|
| 641 |
+
"selection. Use another output directory or --overwrite."
|
| 642 |
+
)
|
| 643 |
+
else:
|
| 644 |
+
atomic_write_json(prompt_selection_path, selection_document)
|
| 645 |
+
|
| 646 |
+
manifest = {
|
| 647 |
+
"dataset_version": DATASET_VERSION,
|
| 648 |
+
"config_path": str(resolve_path(args.config_path)),
|
| 649 |
+
"checkpoint_path": str(checkpoint_path),
|
| 650 |
+
"checkpoint_key": "generator_ema",
|
| 651 |
+
"checkpoint_sha256": file_sha256(checkpoint_path),
|
| 652 |
+
"model": "Wan2.1-T2V-1.3B causal generator_ema",
|
| 653 |
+
"model_hidden_dim": 1536,
|
| 654 |
+
"num_teacher_blocks": 30,
|
| 655 |
+
"cached_layers": args.layers,
|
| 656 |
+
"storage_dtype": "bfloat16",
|
| 657 |
+
"num_prompts": args.num_prompts,
|
| 658 |
+
"num_frames": args.num_frames,
|
| 659 |
+
"num_chunks": args.num_frames // int(config.num_frame_per_block),
|
| 660 |
+
"excluded_chunks": list(EXCLUDED_CHUNKS),
|
| 661 |
+
"stored_chunks": [
|
| 662 |
+
chunk
|
| 663 |
+
for chunk in range(
|
| 664 |
+
args.num_frames // int(config.num_frame_per_block)
|
| 665 |
+
)
|
| 666 |
+
if chunk not in EXCLUDED_CHUNKS
|
| 667 |
+
],
|
| 668 |
+
"num_frame_per_block": int(config.num_frame_per_block),
|
| 669 |
+
"local_attention_latents": (
|
| 670 |
+
int(config.model_kwargs.local_attn_size)
|
| 671 |
+
if args.num_frames > 21 else None
|
| 672 |
+
),
|
| 673 |
+
"denoising_step_source": list(config.denoising_step_list),
|
| 674 |
+
"selection_seed": args.selection_seed,
|
| 675 |
+
"generation_seed_reset_per_prompt": args.generation_seed,
|
| 676 |
+
"prompt_selection_file": str(prompt_selection_path),
|
| 677 |
+
"schema": {
|
| 678 |
+
"trajectory": "prompt_NNNN/trajectory.safetensors",
|
| 679 |
+
"cross_attention": "prompt_NNNN/cross_attention.safetensors",
|
| 680 |
+
"clean_prefeature": (
|
| 681 |
+
"prompt_NNNN/clean_prefeatures/block_XX.safetensors"
|
| 682 |
+
),
|
| 683 |
+
"chunk0_context": "prompt_NNNN/chunk0_context/",
|
| 684 |
+
},
|
| 685 |
+
}
|
| 686 |
+
atomic_write_json(output_dir / "manifest.json", manifest)
|
| 687 |
+
return manifest, selected
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
def update_progress(output_dir: Path, num_prompts: int) -> None:
|
| 691 |
+
completed = []
|
| 692 |
+
total_bytes = 0
|
| 693 |
+
for index in range(num_prompts):
|
| 694 |
+
prompt_dir = output_dir / f"prompt_{index:04d}"
|
| 695 |
+
if (prompt_dir / "_SUCCESS").exists():
|
| 696 |
+
completed.append(index)
|
| 697 |
+
total_bytes += directory_size(prompt_dir)
|
| 698 |
+
atomic_write_json(
|
| 699 |
+
output_dir / "progress.json",
|
| 700 |
+
{
|
| 701 |
+
"completed_prompts": completed,
|
| 702 |
+
"completed_count": len(completed),
|
| 703 |
+
"num_prompts": num_prompts,
|
| 704 |
+
"stored_bytes": total_bytes,
|
| 705 |
+
"stored_gib": total_bytes / (1024**3),
|
| 706 |
+
},
|
| 707 |
+
)
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
def main() -> None:
|
| 711 |
+
args = parse_args()
|
| 712 |
+
args.config_path = resolve_path(args.config_path)
|
| 713 |
+
args.checkpoint_path = resolve_path(args.checkpoint_path)
|
| 714 |
+
args.prompt_path = resolve_path(args.prompt_path)
|
| 715 |
+
args.validation_prompt_path = resolve_path(args.validation_prompt_path)
|
| 716 |
+
args.output_dir = resolve_path(args.output_dir)
|
| 717 |
+
|
| 718 |
+
config = OmegaConf.merge(
|
| 719 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 720 |
+
OmegaConf.load(args.config_path),
|
| 721 |
+
)
|
| 722 |
+
if int(config.num_frame_per_block) != 3:
|
| 723 |
+
raise ValueError("This dataset builder currently expects 3-frame chunks")
|
| 724 |
+
if args.num_frames > 21:
|
| 725 |
+
# Keep the released model's 21-latent training horizon as a rolling
|
| 726 |
+
# attention window while global RoPE positions continue increasing.
|
| 727 |
+
config.model_kwargs.local_attn_size = 21
|
| 728 |
+
|
| 729 |
+
checkpoint = torch.load(
|
| 730 |
+
args.checkpoint_path, map_location="cpu", weights_only=False, mmap=True
|
| 731 |
+
)
|
| 732 |
+
state_dict = checkpoint.get("generator_ema")
|
| 733 |
+
if state_dict is None:
|
| 734 |
+
raise KeyError("Checkpoint does not contain generator_ema")
|
| 735 |
+
checkpoint_layers = sorted(
|
| 736 |
+
{
|
| 737 |
+
int(key.split(".")[2])
|
| 738 |
+
for key in state_dict
|
| 739 |
+
if key.startswith("model.blocks.")
|
| 740 |
+
}
|
| 741 |
+
)
|
| 742 |
+
del checkpoint, state_dict
|
| 743 |
+
if checkpoint_layers != list(range(30)):
|
| 744 |
+
raise ValueError(
|
| 745 |
+
f"Expected checkpoint blocks 0..29, found {checkpoint_layers}"
|
| 746 |
+
)
|
| 747 |
+
|
| 748 |
+
layers = (
|
| 749 |
+
list(range(30))
|
| 750 |
+
if args.layers is None or len(args.layers) == 0
|
| 751 |
+
else sorted(set(args.layers))
|
| 752 |
+
)
|
| 753 |
+
invalid = [layer for layer in layers if layer not in checkpoint_layers]
|
| 754 |
+
if invalid:
|
| 755 |
+
raise ValueError(f"Invalid requested block IDs: {invalid}")
|
| 756 |
+
args.layers = layers
|
| 757 |
+
|
| 758 |
+
_, selected = prepare_manifest(
|
| 759 |
+
args,
|
| 760 |
+
config,
|
| 761 |
+
args.prompt_path,
|
| 762 |
+
args.validation_prompt_path,
|
| 763 |
+
args.checkpoint_path,
|
| 764 |
+
args.output_dir,
|
| 765 |
+
)
|
| 766 |
+
update_progress(args.output_dir, args.num_prompts)
|
| 767 |
+
if args.max_new_prompts == 0:
|
| 768 |
+
print("[prepare] prompt selection and manifest are ready", flush=True)
|
| 769 |
+
return
|
| 770 |
+
|
| 771 |
+
requested_prompt_ids = (
|
| 772 |
+
set(range(args.num_prompts))
|
| 773 |
+
if args.prompt_ids is None
|
| 774 |
+
else set(args.prompt_ids)
|
| 775 |
+
)
|
| 776 |
+
pending = []
|
| 777 |
+
for index, selection in enumerate(selected):
|
| 778 |
+
if index not in requested_prompt_ids:
|
| 779 |
+
continue
|
| 780 |
+
destination = args.output_dir / f"prompt_{index:04d}"
|
| 781 |
+
if (destination / "_SUCCESS").exists() and not args.overwrite:
|
| 782 |
+
continue
|
| 783 |
+
pending.append((index, selection))
|
| 784 |
+
if not pending:
|
| 785 |
+
print("[dataset] all prompt shards already exist", flush=True)
|
| 786 |
+
return
|
| 787 |
+
|
| 788 |
+
device = torch.device("cuda")
|
| 789 |
+
pipeline = build_pipeline(config, args.checkpoint_path, device)
|
| 790 |
+
if len(pipeline.generator.model.blocks) != 30:
|
| 791 |
+
raise ValueError(
|
| 792 |
+
f"Loaded generator has {len(pipeline.generator.model.blocks)} blocks"
|
| 793 |
+
)
|
| 794 |
+
recorder = TrajectoryRecorder(pipeline.generator.model, layers)
|
| 795 |
+
|
| 796 |
+
generated = 0
|
| 797 |
+
try:
|
| 798 |
+
for index, selection in pending:
|
| 799 |
+
if (
|
| 800 |
+
args.max_new_prompts is not None
|
| 801 |
+
and generated >= args.max_new_prompts
|
| 802 |
+
):
|
| 803 |
+
break
|
| 804 |
+
free_gib = shutil.disk_usage(args.output_dir).free / (1024**3)
|
| 805 |
+
if free_gib < args.min_free_gib:
|
| 806 |
+
raise RuntimeError(
|
| 807 |
+
f"Only {free_gib:.1f} GiB free, below --min_free_gib "
|
| 808 |
+
f"{args.min_free_gib:.1f}"
|
| 809 |
+
)
|
| 810 |
+
|
| 811 |
+
destination = args.output_dir / f"prompt_{index:04d}"
|
| 812 |
+
if destination.exists():
|
| 813 |
+
if not args.overwrite:
|
| 814 |
+
raise RuntimeError(
|
| 815 |
+
f"Incomplete destination exists: {destination}"
|
| 816 |
+
)
|
| 817 |
+
shutil.rmtree(destination)
|
| 818 |
+
|
| 819 |
+
print(
|
| 820 |
+
f"[dataset] prompt {index + 1}/{args.num_prompts}, "
|
| 821 |
+
f"free={free_gib:.1f} GiB",
|
| 822 |
+
flush=True,
|
| 823 |
+
)
|
| 824 |
+
(
|
| 825 |
+
trajectory,
|
| 826 |
+
clean_prefeatures,
|
| 827 |
+
chunk0_trajectory,
|
| 828 |
+
chunk0_prefeatures,
|
| 829 |
+
cross_attention,
|
| 830 |
+
elapsed_s,
|
| 831 |
+
peak_gpu_gib,
|
| 832 |
+
) = generate_prompt(
|
| 833 |
+
pipeline,
|
| 834 |
+
recorder,
|
| 835 |
+
selection["prompt"],
|
| 836 |
+
args.num_frames,
|
| 837 |
+
args.generation_seed,
|
| 838 |
+
device,
|
| 839 |
+
)
|
| 840 |
+
destination = save_prompt_shard(
|
| 841 |
+
args.output_dir,
|
| 842 |
+
index,
|
| 843 |
+
selection,
|
| 844 |
+
trajectory,
|
| 845 |
+
clean_prefeatures,
|
| 846 |
+
chunk0_trajectory,
|
| 847 |
+
chunk0_prefeatures,
|
| 848 |
+
cross_attention,
|
| 849 |
+
elapsed_s,
|
| 850 |
+
peak_gpu_gib,
|
| 851 |
+
layers,
|
| 852 |
+
args.generation_seed,
|
| 853 |
+
args.num_frames // int(config.num_frame_per_block),
|
| 854 |
+
)
|
| 855 |
+
shard_gib = directory_size(destination) / (1024**3)
|
| 856 |
+
print(
|
| 857 |
+
f"[dataset] saved {destination.name}: {shard_gib:.3f} GiB, "
|
| 858 |
+
f"{elapsed_s:.1f}s, peak={peak_gpu_gib:.1f} GiB",
|
| 859 |
+
flush=True,
|
| 860 |
+
)
|
| 861 |
+
generated += 1
|
| 862 |
+
update_progress(args.output_dir, args.num_prompts)
|
| 863 |
+
del (
|
| 864 |
+
trajectory,
|
| 865 |
+
clean_prefeatures,
|
| 866 |
+
chunk0_trajectory,
|
| 867 |
+
chunk0_prefeatures,
|
| 868 |
+
cross_attention,
|
| 869 |
+
)
|
| 870 |
+
torch.cuda.empty_cache()
|
| 871 |
+
finally:
|
| 872 |
+
recorder.close()
|
| 873 |
+
|
| 874 |
+
print(f"[dataset] generated {generated} new prompt shards", flush=True)
|
| 875 |
+
|
| 876 |
+
|
| 877 |
+
if __name__ == "__main__":
|
| 878 |
+
main()
|
scripts/build_vbench8_extended_mapping.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Build the auditable VBench-8 extended-prompt subset mapping.
|
| 3 |
+
|
| 4 |
+
The standard VBench metadata is the source of truth for the 946-item order and
|
| 5 |
+
for the prompt-suite membership. Self-Forcing's short prompt file is checked
|
| 6 |
+
against that order before the selected indices are transferred to the
|
| 7 |
+
extended prompt file.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import copy
|
| 14 |
+
import hashlib
|
| 15 |
+
import json
|
| 16 |
+
from collections import Counter
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
from typing import Any
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 22 |
+
SELECTED_SUITES = ("subject_consistency", "overall_consistency", "scene")
|
| 23 |
+
EXPECTED_COUNTS = {
|
| 24 |
+
"subject_consistency": 72,
|
| 25 |
+
"overall_consistency": 93,
|
| 26 |
+
"scene": 86,
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def read_prompts(path: Path) -> list[str]:
|
| 31 |
+
lines = path.read_text(encoding="utf-8").splitlines()
|
| 32 |
+
prompts = [line.strip() for line in lines]
|
| 33 |
+
if any(not prompt for prompt in prompts):
|
| 34 |
+
raise ValueError(f"Prompt file contains an empty line: {path}")
|
| 35 |
+
return prompts
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def sha256(path: Path) -> str:
|
| 39 |
+
digest = hashlib.sha256()
|
| 40 |
+
with path.open("rb") as handle:
|
| 41 |
+
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
| 42 |
+
digest.update(block)
|
| 43 |
+
return digest.hexdigest()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def build_mapping(
|
| 47 |
+
*,
|
| 48 |
+
short_prompts: list[str],
|
| 49 |
+
extended_prompts: list[str],
|
| 50 |
+
vbench_info: list[dict[str, Any]],
|
| 51 |
+
) -> list[dict[str, Any]]:
|
| 52 |
+
if len(short_prompts) != 946:
|
| 53 |
+
raise ValueError(f"Expected 946 short prompts, got {len(short_prompts)}")
|
| 54 |
+
if len(extended_prompts) != 946:
|
| 55 |
+
raise ValueError(
|
| 56 |
+
f"Expected 946 extended prompts, got {len(extended_prompts)}"
|
| 57 |
+
)
|
| 58 |
+
if len(vbench_info) != 946:
|
| 59 |
+
raise ValueError(f"Expected 946 VBench metadata rows, got {len(vbench_info)}")
|
| 60 |
+
|
| 61 |
+
canonical = [row.get("prompt_en") for row in vbench_info]
|
| 62 |
+
if any(not isinstance(prompt, str) or not prompt.strip() for prompt in canonical):
|
| 63 |
+
raise ValueError("VBench metadata contains a missing prompt_en")
|
| 64 |
+
mismatches = [
|
| 65 |
+
index
|
| 66 |
+
for index, (short, official) in enumerate(zip(short_prompts, canonical))
|
| 67 |
+
if short != official
|
| 68 |
+
]
|
| 69 |
+
if mismatches:
|
| 70 |
+
preview = mismatches[:10]
|
| 71 |
+
raise ValueError(
|
| 72 |
+
"Self-Forcing all_dimension.txt does not preserve VBench ordering; "
|
| 73 |
+
f"mismatching indices include {preview}"
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
counters: Counter[str] = Counter()
|
| 77 |
+
mapping: list[dict[str, Any]] = []
|
| 78 |
+
for global_index, row in enumerate(vbench_info):
|
| 79 |
+
suites = [suite for suite in SELECTED_SUITES if suite in row["dimension"]]
|
| 80 |
+
if not suites:
|
| 81 |
+
continue
|
| 82 |
+
if len(suites) != 1:
|
| 83 |
+
raise ValueError(
|
| 84 |
+
f"Metadata row {global_index} belongs to multiple selected suites: {suites}"
|
| 85 |
+
)
|
| 86 |
+
suite = suites[0]
|
| 87 |
+
suite_index = counters[suite]
|
| 88 |
+
counters[suite] += 1
|
| 89 |
+
item: dict[str, Any] = {
|
| 90 |
+
"global_index": global_index,
|
| 91 |
+
"prompt_suite": suite,
|
| 92 |
+
"suite_index": suite_index,
|
| 93 |
+
"original_prompt": short_prompts[global_index],
|
| 94 |
+
"extended_prompt": extended_prompts[global_index],
|
| 95 |
+
"official_dimensions": list(row["dimension"]),
|
| 96 |
+
}
|
| 97 |
+
if "auxiliary_info" in row:
|
| 98 |
+
item["auxiliary_info"] = copy.deepcopy(row["auxiliary_info"])
|
| 99 |
+
mapping.append(item)
|
| 100 |
+
|
| 101 |
+
if counters != Counter(EXPECTED_COUNTS):
|
| 102 |
+
raise ValueError(
|
| 103 |
+
f"Unexpected selected-suite counts: {dict(counters)}; "
|
| 104 |
+
f"expected {EXPECTED_COUNTS}"
|
| 105 |
+
)
|
| 106 |
+
if len(mapping) != 251:
|
| 107 |
+
raise ValueError(f"Expected 251 selected prompts, got {len(mapping)}")
|
| 108 |
+
if len({item["global_index"] for item in mapping}) != len(mapping):
|
| 109 |
+
raise ValueError("Duplicate global indices in mapping")
|
| 110 |
+
for suite, expected in EXPECTED_COUNTS.items():
|
| 111 |
+
indices = [item["suite_index"] for item in mapping if item["prompt_suite"] == suite]
|
| 112 |
+
if sorted(indices) != list(range(expected)):
|
| 113 |
+
raise ValueError(f"Suite indices for {suite} are not contiguous and unique")
|
| 114 |
+
return mapping
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def parse_args() -> argparse.Namespace:
|
| 118 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 119 |
+
parser.add_argument(
|
| 120 |
+
"--short-prompts",
|
| 121 |
+
type=Path,
|
| 122 |
+
default=REPO_ROOT / "prompts/vbench/all_dimension.txt",
|
| 123 |
+
)
|
| 124 |
+
parser.add_argument(
|
| 125 |
+
"--extended-prompts",
|
| 126 |
+
type=Path,
|
| 127 |
+
default=REPO_ROOT / "prompts/vbench/all_dimension_extended.txt",
|
| 128 |
+
)
|
| 129 |
+
parser.add_argument(
|
| 130 |
+
"--vbench-info",
|
| 131 |
+
type=Path,
|
| 132 |
+
default=Path(
|
| 133 |
+
"/data3/chenzhuo/workspace/HY-WorldPlay-light-interaction-run-DEV/"
|
| 134 |
+
".venv-vbench/lib/python3.10/site-packages/vbench/VBench_full_info.json"
|
| 135 |
+
),
|
| 136 |
+
)
|
| 137 |
+
parser.add_argument(
|
| 138 |
+
"--output",
|
| 139 |
+
type=Path,
|
| 140 |
+
default=REPO_ROOT / "assets/vbench8_extended_subset_mapping.json",
|
| 141 |
+
)
|
| 142 |
+
return parser.parse_args()
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def main() -> None:
|
| 146 |
+
args = parse_args()
|
| 147 |
+
short_path = args.short_prompts.expanduser().resolve()
|
| 148 |
+
extended_path = args.extended_prompts.expanduser().resolve()
|
| 149 |
+
info_path = args.vbench_info.expanduser().resolve()
|
| 150 |
+
output_path = args.output.expanduser().resolve()
|
| 151 |
+
mapping = build_mapping(
|
| 152 |
+
short_prompts=read_prompts(short_path),
|
| 153 |
+
extended_prompts=read_prompts(extended_path),
|
| 154 |
+
vbench_info=json.loads(info_path.read_text(encoding="utf-8")),
|
| 155 |
+
)
|
| 156 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 157 |
+
output_path.write_text(
|
| 158 |
+
json.dumps(mapping, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
| 159 |
+
)
|
| 160 |
+
print(f"mapping={output_path}")
|
| 161 |
+
print(f"counts={dict(Counter(item['prompt_suite'] for item in mapping))}")
|
| 162 |
+
print(f"total={len(mapping)}")
|
| 163 |
+
print(f"short_sha256={sha256(short_path)}")
|
| 164 |
+
print(f"extended_sha256={sha256(extended_path)}")
|
| 165 |
+
print(f"vbench_info_sha256={sha256(info_path)}")
|
| 166 |
+
print(f"mapping_sha256={sha256(output_path)}")
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
if __name__ == "__main__":
|
| 170 |
+
main()
|
scripts/calibrate_atc_confidence_token_beta2_vbench3.py
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Calibrate fixed-beta=2 thresholds for the ATC Confidence-token head."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import math
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import tempfile
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def preparse_gpu() -> str:
|
| 17 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 18 |
+
parser.add_argument("--gpu", default="0")
|
| 19 |
+
args, _ = parser.parse_known_args()
|
| 20 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 21 |
+
os.environ.setdefault(
|
| 22 |
+
"MPLCONFIGDIR", tempfile.mkdtemp(prefix="atc_confidence_calibration_")
|
| 23 |
+
)
|
| 24 |
+
return str(args.gpu)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
PHYSICAL_GPU = preparse_gpu()
|
| 28 |
+
|
| 29 |
+
import torch
|
| 30 |
+
from omegaconf import OmegaConf
|
| 31 |
+
from safetensors import safe_open
|
| 32 |
+
from safetensors.torch import load_file
|
| 33 |
+
|
| 34 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 35 |
+
if str(ROOT) not in sys.path:
|
| 36 |
+
sys.path.insert(0, str(ROOT))
|
| 37 |
+
|
| 38 |
+
from predictor_training.confidence import ConfidenceTokenHead
|
| 39 |
+
from scripts import evaluate_single_block_fppf as base
|
| 40 |
+
from scripts import generate_vbench8_extended_strategies as extended
|
| 41 |
+
from utils.wan_wrapper import WanTextEncoder
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
BETA = 2.0
|
| 45 |
+
CANDIDATE_STEPS = [1, 2, 3]
|
| 46 |
+
TARGETS = [6, 9, 12, 15]
|
| 47 |
+
GLOBAL_INDICES = [293, 729, 819]
|
| 48 |
+
MAPPING_DEFAULT = ROOT / "assets/vbench8_extended_subset_mapping.json"
|
| 49 |
+
PREDICTOR_DEFAULT = (
|
| 50 |
+
ROOT
|
| 51 |
+
/ "training_runs/layer17_atc_chunk_stage1_1000p_4gpu_b16_2000steps"
|
| 52 |
+
/ "checkpoint_step_2000/predictor.safetensors"
|
| 53 |
+
)
|
| 54 |
+
HEAD_DEFAULT = (
|
| 55 |
+
ROOT
|
| 56 |
+
/ "training_runs"
|
| 57 |
+
/ "layer17_atc_chunk_confidence_token_900train_100val_4gpu_b64_warmup_cosine"
|
| 58 |
+
/ "confidence_latest.safetensors"
|
| 59 |
+
)
|
| 60 |
+
OUTPUT_DEFAULT = (
|
| 61 |
+
ROOT
|
| 62 |
+
/ "confidence_experiments"
|
| 63 |
+
/ "layer17_atc_chunk_confidence_token_beta2_vbench3_20260902"
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def atomic_json(path: Path, value: Any) -> None:
|
| 68 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 69 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 70 |
+
temporary.write_text(
|
| 71 |
+
json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
|
| 72 |
+
encoding="utf-8",
|
| 73 |
+
)
|
| 74 |
+
os.replace(temporary, path)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def quantile(values: list[float], fraction: float) -> float:
|
| 78 |
+
ordered = sorted(values)
|
| 79 |
+
position = fraction * (len(ordered) - 1)
|
| 80 |
+
lower = math.floor(position)
|
| 81 |
+
upper = math.ceil(position)
|
| 82 |
+
if lower == upper:
|
| 83 |
+
return ordered[lower]
|
| 84 |
+
weight = position - lower
|
| 85 |
+
return ordered[lower] * (1.0 - weight) + ordered[upper] * weight
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def mapping_row(path: Path, global_index: int) -> dict[str, Any]:
|
| 89 |
+
matches = [
|
| 90 |
+
row
|
| 91 |
+
for row in extended.read_mapping(path)
|
| 92 |
+
if int(row["global_index"]) == global_index
|
| 93 |
+
]
|
| 94 |
+
if len(matches) != 1:
|
| 95 |
+
raise ValueError(f"Expected one mapping row for global={global_index}")
|
| 96 |
+
return matches[0]
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def read_head_config(path: Path) -> dict[str, Any]:
|
| 100 |
+
with safe_open(path, framework="pt", device="cpu") as handle:
|
| 101 |
+
metadata = handle.metadata() or {}
|
| 102 |
+
raw = metadata.get("head_config")
|
| 103 |
+
if raw is None:
|
| 104 |
+
raise ValueError(f"Missing head_config metadata: {path}")
|
| 105 |
+
config = json.loads(raw)
|
| 106 |
+
expected = {
|
| 107 |
+
"architecture": "ConfidenceTokenHead",
|
| 108 |
+
"dim": 1536,
|
| 109 |
+
"token_dim": 512,
|
| 110 |
+
"context_dim": 64,
|
| 111 |
+
"num_heads": 8,
|
| 112 |
+
"ffn_dim": 2048,
|
| 113 |
+
"num_steps": 3,
|
| 114 |
+
}
|
| 115 |
+
for key, value in expected.items():
|
| 116 |
+
if config.get(key) != value:
|
| 117 |
+
raise ValueError(f"Unexpected head config {key}={config.get(key)}")
|
| 118 |
+
return config
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def read_predictor_config(
|
| 122 |
+
path: Path,
|
| 123 |
+
input_variant: str,
|
| 124 |
+
atc_previous_scope: str,
|
| 125 |
+
) -> dict[str, Any]:
|
| 126 |
+
with safe_open(path, framework="pt", device="cpu") as handle:
|
| 127 |
+
metadata = handle.metadata() or {}
|
| 128 |
+
raw = metadata.get("predictor_config")
|
| 129 |
+
if raw is None:
|
| 130 |
+
if input_variant != "self_forcing":
|
| 131 |
+
raise ValueError(
|
| 132 |
+
f"Missing predictor_config metadata: {path}; only an explicitly "
|
| 133 |
+
"selected legacy self_forcing/concat checkpoint may omit it"
|
| 134 |
+
)
|
| 135 |
+
return {
|
| 136 |
+
"source_layer": 17,
|
| 137 |
+
"input_variant": "self_forcing",
|
| 138 |
+
"gate_mode": "baseline",
|
| 139 |
+
}
|
| 140 |
+
config = json.loads(raw)
|
| 141 |
+
actual_variant = str(config.get("input_variant", "self_forcing"))
|
| 142 |
+
if actual_variant != input_variant:
|
| 143 |
+
raise ValueError(
|
| 144 |
+
f"Predictor variant mismatch: expected={input_variant} "
|
| 145 |
+
f"actual={actual_variant}"
|
| 146 |
+
)
|
| 147 |
+
if input_variant == "atc":
|
| 148 |
+
actual_scope = str(config.get("atc_previous_scope", "chunk"))
|
| 149 |
+
if actual_scope != atc_previous_scope:
|
| 150 |
+
raise ValueError(
|
| 151 |
+
f"ATC scope mismatch: expected={atc_previous_scope} "
|
| 152 |
+
f"actual={actual_scope}"
|
| 153 |
+
)
|
| 154 |
+
return config
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def load_models(
|
| 158 |
+
predictor_weights: Path,
|
| 159 |
+
head_weights: Path,
|
| 160 |
+
input_variant: str,
|
| 161 |
+
atc_previous_scope: str,
|
| 162 |
+
device: torch.device,
|
| 163 |
+
) -> tuple[Any, Any, Any, ConfidenceTokenHead]:
|
| 164 |
+
predictor_config = read_predictor_config(
|
| 165 |
+
predictor_weights, input_variant, atc_previous_scope
|
| 166 |
+
)
|
| 167 |
+
config = OmegaConf.merge(
|
| 168 |
+
OmegaConf.load(ROOT / "configs/default_config.yaml"),
|
| 169 |
+
OmegaConf.load(ROOT / "configs/self_forcing_sid.yaml"),
|
| 170 |
+
)
|
| 171 |
+
pipeline = base.build_pipeline(
|
| 172 |
+
config,
|
| 173 |
+
ROOT / "checkpoints/self_forcing_dmd.pt",
|
| 174 |
+
torch.nn.Identity(),
|
| 175 |
+
device,
|
| 176 |
+
)
|
| 177 |
+
predictor = base.load_predictor(
|
| 178 |
+
pipeline.generator.model,
|
| 179 |
+
{
|
| 180 |
+
**predictor_config,
|
| 181 |
+
"weights": predictor_weights,
|
| 182 |
+
"atc_collect_diagnostics": False,
|
| 183 |
+
},
|
| 184 |
+
device,
|
| 185 |
+
)
|
| 186 |
+
text_encoder = WanTextEncoder().to(device=device, dtype=torch.bfloat16).eval()
|
| 187 |
+
text_encoder.requires_grad_(False)
|
| 188 |
+
head_config = read_head_config(head_weights)
|
| 189 |
+
head = ConfidenceTokenHead(
|
| 190 |
+
dim=int(head_config["dim"]),
|
| 191 |
+
token_dim=int(head_config["token_dim"]),
|
| 192 |
+
context_dim=int(head_config["context_dim"]),
|
| 193 |
+
num_heads=int(head_config["num_heads"]),
|
| 194 |
+
ffn_dim=int(head_config["ffn_dim"]),
|
| 195 |
+
dropout=float(head_config.get("dropout", 0.1)),
|
| 196 |
+
num_steps=int(head_config["num_steps"]),
|
| 197 |
+
).to(device=device).eval()
|
| 198 |
+
head.load_state_dict(load_file(str(head_weights), device="cpu"), strict=True)
|
| 199 |
+
head.requires_grad_(False)
|
| 200 |
+
return pipeline, predictor, text_encoder, head
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def rollout_config(name: str, threshold: float) -> dict[str, Any]:
|
| 204 |
+
return {
|
| 205 |
+
"name": name,
|
| 206 |
+
"policy": "dynamic",
|
| 207 |
+
"candidate_steps": CANDIDATE_STEPS,
|
| 208 |
+
"beta": BETA,
|
| 209 |
+
"threshold": threshold,
|
| 210 |
+
"target_accepts": None,
|
| 211 |
+
"head": "confidence_token",
|
| 212 |
+
"risk_mode": "outgoing_span",
|
| 213 |
+
"allow_chunk0_predictor": False,
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def latent_nrmse(predicted: torch.Tensor, reference: torch.Tensor) -> float:
|
| 218 |
+
numerator = (predicted.float() - reference.float()).square().sum()
|
| 219 |
+
denominator = reference.float().square().sum().clamp_min(1e-8)
|
| 220 |
+
return float(torch.sqrt(numerator / denominator))
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def run_probe(args: argparse.Namespace) -> None:
|
| 224 |
+
if args.global_index not in GLOBAL_INDICES:
|
| 225 |
+
raise ValueError(f"Probe index must be one of {GLOBAL_INDICES}")
|
| 226 |
+
destination = args.output_root / "probes" / f"global_{args.global_index:04d}.json"
|
| 227 |
+
if destination.is_file() and not args.overwrite:
|
| 228 |
+
print(f"[cached] {destination}", flush=True)
|
| 229 |
+
return
|
| 230 |
+
row = mapping_row(args.mapping, args.global_index)
|
| 231 |
+
device = torch.device("cuda")
|
| 232 |
+
torch.set_grad_enabled(False)
|
| 233 |
+
pipeline, predictor, text_encoder, head = load_models(
|
| 234 |
+
args.predictor_weights,
|
| 235 |
+
args.head_weights,
|
| 236 |
+
args.predictor_input_variant,
|
| 237 |
+
args.atc_previous_scope,
|
| 238 |
+
device,
|
| 239 |
+
)
|
| 240 |
+
conditional = text_encoder(text_prompts=[str(row["extended_prompt"])])
|
| 241 |
+
_, diagnostic = extended.generate_rollout(
|
| 242 |
+
pipeline=pipeline,
|
| 243 |
+
conditional_dict=conditional,
|
| 244 |
+
seed=args.seed,
|
| 245 |
+
device=device,
|
| 246 |
+
predictor=predictor,
|
| 247 |
+
head=head,
|
| 248 |
+
config=rollout_config("confidence_token_probe", -math.inf),
|
| 249 |
+
)
|
| 250 |
+
decisions = diagnostic.pop("decisions")
|
| 251 |
+
if len(decisions) != 18 or any(item["accepted"] for item in decisions):
|
| 252 |
+
raise RuntimeError("Full-fallback probe did not produce 18 rejected decisions")
|
| 253 |
+
atomic_json(
|
| 254 |
+
destination,
|
| 255 |
+
{
|
| 256 |
+
"status": "complete",
|
| 257 |
+
"physical_gpu": PHYSICAL_GPU,
|
| 258 |
+
"global_index": args.global_index,
|
| 259 |
+
"prompt_suite": row["prompt_suite"],
|
| 260 |
+
"suite_index": int(row["suite_index"]),
|
| 261 |
+
"prompt": row["extended_prompt"],
|
| 262 |
+
"seed": args.seed,
|
| 263 |
+
"generation": diagnostic,
|
| 264 |
+
"decisions": decisions,
|
| 265 |
+
},
|
| 266 |
+
)
|
| 267 |
+
print(f"[probe] global={args.global_index} decisions=18", flush=True)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def prepare_scan(args: argparse.Namespace) -> None:
|
| 271 |
+
paths = sorted((args.output_root / "probes").glob("global_*.json"))
|
| 272 |
+
if len(paths) != 3:
|
| 273 |
+
raise ValueError(f"Expected three probes, found {len(paths)}")
|
| 274 |
+
probes = [json.loads(path.read_text(encoding="utf-8")) for path in paths]
|
| 275 |
+
if sorted(int(item["global_index"]) for item in probes) != GLOBAL_INDICES:
|
| 276 |
+
raise ValueError("Probe indices differ from the requested calibration set")
|
| 277 |
+
decisions = [decision for probe in probes for decision in probe["decisions"]]
|
| 278 |
+
risks = []
|
| 279 |
+
spans: dict[int, set[float]] = {1: set(), 2: set(), 3: set()}
|
| 280 |
+
for decision in decisions:
|
| 281 |
+
step = int(decision["step"])
|
| 282 |
+
spans[step].add(float(decision["outgoing_span"]))
|
| 283 |
+
risks.append(
|
| 284 |
+
float(decision["predicted_local_error"])
|
| 285 |
+
* float(decision["span_weight"])
|
| 286 |
+
* (1.0 + BETA * float(decision["chunk_alpha"]))
|
| 287 |
+
)
|
| 288 |
+
expected_spans = {1: 104.1666, 2: 208.3333, 3: 625.0}
|
| 289 |
+
for step, expected in expected_spans.items():
|
| 290 |
+
if any(abs(value - expected) > 0.02 for value in spans[step]):
|
| 291 |
+
raise ValueError(f"Unexpected step-{step} spans: {spans[step]}")
|
| 292 |
+
configurations = []
|
| 293 |
+
for rank in range(1, 18):
|
| 294 |
+
threshold = quantile(risks, rank / 18)
|
| 295 |
+
config = rollout_config(f"confidence_token_beta2_thr{rank:02d}", threshold)
|
| 296 |
+
config["threshold_rank"] = rank
|
| 297 |
+
configurations.append(config)
|
| 298 |
+
atomic_json(
|
| 299 |
+
args.output_root / "scan_configs.json",
|
| 300 |
+
{
|
| 301 |
+
"status": "ready",
|
| 302 |
+
"beta": BETA,
|
| 303 |
+
"risk_definition": (
|
| 304 |
+
"exp(predicted_log_error) * (outgoing_span / 1000) * "
|
| 305 |
+
"(1 + 2.0 * chunk_alpha)"
|
| 306 |
+
),
|
| 307 |
+
"candidate_steps": CANDIDATE_STEPS,
|
| 308 |
+
"targets": TARGETS,
|
| 309 |
+
"probe_global_indices": GLOBAL_INDICES,
|
| 310 |
+
"head_weights": str(args.head_weights),
|
| 311 |
+
"predictor_weights": str(args.predictor_weights),
|
| 312 |
+
"predictor_input_variant": args.predictor_input_variant,
|
| 313 |
+
"atc_previous_scope": (
|
| 314 |
+
args.atc_previous_scope
|
| 315 |
+
if args.predictor_input_variant == "atc"
|
| 316 |
+
else None
|
| 317 |
+
),
|
| 318 |
+
"outgoing_spans": {
|
| 319 |
+
str(step): sorted(values) for step, values in spans.items()
|
| 320 |
+
},
|
| 321 |
+
"configs": configurations,
|
| 322 |
+
},
|
| 323 |
+
)
|
| 324 |
+
print(f"[prepare] thresholds={len(configurations)}", flush=True)
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def run_scan(args: argparse.Namespace) -> None:
|
| 328 |
+
if args.global_index not in GLOBAL_INDICES:
|
| 329 |
+
raise ValueError(f"Scan index must be one of {GLOBAL_INDICES}")
|
| 330 |
+
destination = args.output_root / "scans" / f"global_{args.global_index:04d}.json"
|
| 331 |
+
existing: dict[str, Any] = {}
|
| 332 |
+
if destination.is_file() and not args.overwrite:
|
| 333 |
+
existing = json.loads(destination.read_text(encoding="utf-8"))
|
| 334 |
+
if existing.get("status") == "complete":
|
| 335 |
+
print(f"[cached] {destination}", flush=True)
|
| 336 |
+
return
|
| 337 |
+
completed = {item["name"]: item for item in existing.get("results", [])}
|
| 338 |
+
manifest = json.loads((args.output_root / "scan_configs.json").read_text())
|
| 339 |
+
configurations = manifest["configs"]
|
| 340 |
+
row = mapping_row(args.mapping, args.global_index)
|
| 341 |
+
device = torch.device("cuda")
|
| 342 |
+
torch.set_grad_enabled(False)
|
| 343 |
+
pipeline, predictor, text_encoder, head = load_models(
|
| 344 |
+
args.predictor_weights,
|
| 345 |
+
args.head_weights,
|
| 346 |
+
args.predictor_input_variant,
|
| 347 |
+
args.atc_previous_scope,
|
| 348 |
+
device,
|
| 349 |
+
)
|
| 350 |
+
conditional = text_encoder(text_prompts=[str(row["extended_prompt"])])
|
| 351 |
+
reference, reference_diagnostic = extended.generate_rollout(
|
| 352 |
+
pipeline=pipeline,
|
| 353 |
+
conditional_dict=conditional,
|
| 354 |
+
seed=args.seed,
|
| 355 |
+
device=device,
|
| 356 |
+
predictor=predictor,
|
| 357 |
+
head=None,
|
| 358 |
+
config={
|
| 359 |
+
"name": "ffff",
|
| 360 |
+
"policy": "ffff",
|
| 361 |
+
"candidate_steps": [],
|
| 362 |
+
"beta": None,
|
| 363 |
+
"threshold": None,
|
| 364 |
+
"target_accepts": 0,
|
| 365 |
+
"head": None,
|
| 366 |
+
"risk_mode": "outgoing_span",
|
| 367 |
+
"allow_chunk0_predictor": False,
|
| 368 |
+
},
|
| 369 |
+
)
|
| 370 |
+
for position, config in enumerate(configurations, start=1):
|
| 371 |
+
name = str(config["name"])
|
| 372 |
+
if name in completed:
|
| 373 |
+
continue
|
| 374 |
+
latent, diagnostic = extended.generate_rollout(
|
| 375 |
+
pipeline=pipeline,
|
| 376 |
+
conditional_dict=conditional,
|
| 377 |
+
seed=args.seed,
|
| 378 |
+
device=device,
|
| 379 |
+
predictor=predictor,
|
| 380 |
+
head=head,
|
| 381 |
+
config=config,
|
| 382 |
+
)
|
| 383 |
+
decisions = diagnostic.pop("decisions")
|
| 384 |
+
record = {
|
| 385 |
+
**config,
|
| 386 |
+
"accepted_predictor_calls": diagnostic["accepted_predictor_calls"],
|
| 387 |
+
"full_calls": diagnostic["full_calls"],
|
| 388 |
+
"policy_latency_ms": diagnostic["policy_latency_ms"],
|
| 389 |
+
"model_path_time_ms": diagnostic["model_path_time_ms"],
|
| 390 |
+
"confidence_head_time_ms": diagnostic["confidence_head_time_ms"],
|
| 391 |
+
"latent_nrmse_vs_ffff": latent_nrmse(latent, reference),
|
| 392 |
+
"decisions": decisions,
|
| 393 |
+
}
|
| 394 |
+
completed[name] = record
|
| 395 |
+
atomic_json(
|
| 396 |
+
destination,
|
| 397 |
+
{
|
| 398 |
+
"status": "running",
|
| 399 |
+
"physical_gpu": PHYSICAL_GPU,
|
| 400 |
+
"global_index": args.global_index,
|
| 401 |
+
"prompt_suite": row["prompt_suite"],
|
| 402 |
+
"suite_index": int(row["suite_index"]),
|
| 403 |
+
"prompt": row["extended_prompt"],
|
| 404 |
+
"seed": args.seed,
|
| 405 |
+
"ffff": {
|
| 406 |
+
key: value
|
| 407 |
+
for key, value in reference_diagnostic.items()
|
| 408 |
+
if key != "decisions"
|
| 409 |
+
},
|
| 410 |
+
"results": [completed[key] for key in sorted(completed)],
|
| 411 |
+
},
|
| 412 |
+
)
|
| 413 |
+
print(
|
| 414 |
+
f"[scan] {position}/17 global={args.global_index} "
|
| 415 |
+
f"threshold={config['threshold']:.8f} "
|
| 416 |
+
f"K={record['accepted_predictor_calls']} "
|
| 417 |
+
f"nrmse={record['latent_nrmse_vs_ffff']:.6f}",
|
| 418 |
+
flush=True,
|
| 419 |
+
)
|
| 420 |
+
del latent
|
| 421 |
+
atomic_json(
|
| 422 |
+
destination,
|
| 423 |
+
{
|
| 424 |
+
"status": "complete",
|
| 425 |
+
"physical_gpu": PHYSICAL_GPU,
|
| 426 |
+
"global_index": args.global_index,
|
| 427 |
+
"prompt_suite": row["prompt_suite"],
|
| 428 |
+
"suite_index": int(row["suite_index"]),
|
| 429 |
+
"prompt": row["extended_prompt"],
|
| 430 |
+
"seed": args.seed,
|
| 431 |
+
"ffff": {
|
| 432 |
+
key: value
|
| 433 |
+
for key, value in reference_diagnostic.items()
|
| 434 |
+
if key != "decisions"
|
| 435 |
+
},
|
| 436 |
+
"results": [completed[key] for key in sorted(completed)],
|
| 437 |
+
},
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
def summarize(args: argparse.Namespace) -> None:
|
| 442 |
+
paths = sorted((args.output_root / "scans").glob("global_*.json"))
|
| 443 |
+
if len(paths) != 3:
|
| 444 |
+
raise ValueError(f"Expected three scans, found {len(paths)}")
|
| 445 |
+
scans = [json.loads(path.read_text(encoding="utf-8")) for path in paths]
|
| 446 |
+
if any(item.get("status") != "complete" for item in scans):
|
| 447 |
+
raise RuntimeError("At least one scan is incomplete")
|
| 448 |
+
ffff_latency = sum(
|
| 449 |
+
float(scan["ffff"]["policy_latency_ms"]) for scan in scans
|
| 450 |
+
) / len(scans)
|
| 451 |
+
by_scan = [
|
| 452 |
+
{record["name"]: record for record in scan["results"]}
|
| 453 |
+
for scan in scans
|
| 454 |
+
]
|
| 455 |
+
names = sorted(by_scan[0])
|
| 456 |
+
summary = []
|
| 457 |
+
for name in names:
|
| 458 |
+
rows = [records[name] for records in by_scan]
|
| 459 |
+
mean_latency = sum(float(row["policy_latency_ms"]) for row in rows) / 3
|
| 460 |
+
summary.append(
|
| 461 |
+
{
|
| 462 |
+
"name": name,
|
| 463 |
+
"beta": BETA,
|
| 464 |
+
"threshold": float(rows[0]["threshold"]),
|
| 465 |
+
"threshold_rank": int(rows[0]["threshold_rank"]),
|
| 466 |
+
"mean_accepted_predictor_calls": sum(
|
| 467 |
+
float(row["accepted_predictor_calls"]) for row in rows
|
| 468 |
+
)
|
| 469 |
+
/ 3,
|
| 470 |
+
"mean_full_calls": sum(float(row["full_calls"]) for row in rows) / 3,
|
| 471 |
+
"mean_policy_latency_ms": mean_latency,
|
| 472 |
+
"speedup_vs_ffff": ffff_latency / mean_latency,
|
| 473 |
+
"mean_confidence_head_time_ms": sum(
|
| 474 |
+
float(row["confidence_head_time_ms"]) for row in rows
|
| 475 |
+
)
|
| 476 |
+
/ 3,
|
| 477 |
+
"mean_latent_nrmse_vs_ffff": sum(
|
| 478 |
+
float(row["latent_nrmse_vs_ffff"]) for row in rows
|
| 479 |
+
)
|
| 480 |
+
/ 3,
|
| 481 |
+
"per_prompt_accepts": [
|
| 482 |
+
int(row["accepted_predictor_calls"]) for row in rows
|
| 483 |
+
],
|
| 484 |
+
"per_prompt_latent_nrmse": [
|
| 485 |
+
float(row["latent_nrmse_vs_ffff"]) for row in rows
|
| 486 |
+
],
|
| 487 |
+
}
|
| 488 |
+
)
|
| 489 |
+
selected = []
|
| 490 |
+
for target in TARGETS:
|
| 491 |
+
within = [
|
| 492 |
+
row
|
| 493 |
+
for row in summary
|
| 494 |
+
if abs(float(row["mean_accepted_predictor_calls"]) - target) <= 0.5
|
| 495 |
+
]
|
| 496 |
+
pool = within or summary
|
| 497 |
+
choice = min(
|
| 498 |
+
pool,
|
| 499 |
+
key=lambda row: (
|
| 500 |
+
float(row["mean_latent_nrmse_vs_ffff"]),
|
| 501 |
+
abs(float(row["mean_accepted_predictor_calls"]) - target),
|
| 502 |
+
float(row["threshold"]),
|
| 503 |
+
)
|
| 504 |
+
if within
|
| 505 |
+
else (
|
| 506 |
+
abs(float(row["mean_accepted_predictor_calls"]) - target),
|
| 507 |
+
float(row["mean_latent_nrmse_vs_ffff"]),
|
| 508 |
+
float(row["threshold"]),
|
| 509 |
+
),
|
| 510 |
+
)
|
| 511 |
+
selected.append({**choice, "target_accepts": target})
|
| 512 |
+
atomic_json(
|
| 513 |
+
args.output_root / "threshold_summary.json",
|
| 514 |
+
{
|
| 515 |
+
"status": "complete",
|
| 516 |
+
"beta": BETA,
|
| 517 |
+
"candidate_steps": CANDIDATE_STEPS,
|
| 518 |
+
"global_indices": GLOBAL_INDICES,
|
| 519 |
+
"ffff_mean_policy_latency_ms": ffff_latency,
|
| 520 |
+
"selection_rule": (
|
| 521 |
+
"For each target K, require mean K within +/-0.5 when possible; "
|
| 522 |
+
"then choose the lowest mean latent nRMSE"
|
| 523 |
+
),
|
| 524 |
+
"head_weights": str(args.head_weights),
|
| 525 |
+
"predictor_weights": str(args.predictor_weights),
|
| 526 |
+
"predictor_input_variant": args.predictor_input_variant,
|
| 527 |
+
"atc_previous_scope": (
|
| 528 |
+
args.atc_previous_scope
|
| 529 |
+
if args.predictor_input_variant == "atc"
|
| 530 |
+
else None
|
| 531 |
+
),
|
| 532 |
+
"summary": summary,
|
| 533 |
+
"selected": selected,
|
| 534 |
+
},
|
| 535 |
+
)
|
| 536 |
+
print(f"[summary] FFFF latency={ffff_latency:.2f}ms", flush=True)
|
| 537 |
+
for row in selected:
|
| 538 |
+
print(
|
| 539 |
+
f"[selected] K{row['target_accepts']:02d} "
|
| 540 |
+
f"threshold={row['threshold']:.8f} "
|
| 541 |
+
f"actual_K={row['mean_accepted_predictor_calls']:.3f} "
|
| 542 |
+
f"speedup={row['speedup_vs_ffff']:.3f}x "
|
| 543 |
+
f"nrmse={row['mean_latent_nrmse_vs_ffff']:.6f}",
|
| 544 |
+
flush=True,
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
|
| 548 |
+
def parse_args() -> argparse.Namespace:
|
| 549 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 550 |
+
parser.add_argument(
|
| 551 |
+
"--mode", choices=("probe", "prepare", "scan", "summarize"), required=True
|
| 552 |
+
)
|
| 553 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 554 |
+
parser.add_argument("--global-index", type=int, default=None)
|
| 555 |
+
parser.add_argument("--mapping", type=Path, default=MAPPING_DEFAULT)
|
| 556 |
+
parser.add_argument("--predictor-weights", type=Path, default=PREDICTOR_DEFAULT)
|
| 557 |
+
parser.add_argument("--head-weights", type=Path, default=HEAD_DEFAULT)
|
| 558 |
+
parser.add_argument(
|
| 559 |
+
"--predictor-input-variant",
|
| 560 |
+
choices=("self_forcing", "atc"),
|
| 561 |
+
default="atc",
|
| 562 |
+
)
|
| 563 |
+
parser.add_argument(
|
| 564 |
+
"--atc-previous-scope",
|
| 565 |
+
choices=("chunk", "last_frame"),
|
| 566 |
+
default="chunk",
|
| 567 |
+
)
|
| 568 |
+
parser.add_argument("--output-root", type=Path, default=OUTPUT_DEFAULT)
|
| 569 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 570 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 571 |
+
args = parser.parse_args()
|
| 572 |
+
for name in ("mapping", "predictor_weights", "head_weights", "output_root"):
|
| 573 |
+
setattr(args, name, getattr(args, name).expanduser().resolve())
|
| 574 |
+
return args
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
def main() -> None:
|
| 578 |
+
args = parse_args()
|
| 579 |
+
args.output_root.mkdir(parents=True, exist_ok=True)
|
| 580 |
+
if args.mode == "probe":
|
| 581 |
+
run_probe(args)
|
| 582 |
+
elif args.mode == "prepare":
|
| 583 |
+
prepare_scan(args)
|
| 584 |
+
elif args.mode == "scan":
|
| 585 |
+
run_scan(args)
|
| 586 |
+
else:
|
| 587 |
+
summarize(args)
|
| 588 |
+
|
| 589 |
+
|
| 590 |
+
if __name__ == "__main__":
|
| 591 |
+
main()
|
scripts/calibrate_vbench251_span_risk.py
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Calibrate outgoing-span confidence risk on three Extended-251 prompts."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import math
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import tempfile
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def preparse_gpu() -> str:
|
| 17 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 18 |
+
parser.add_argument("--gpu", default="5")
|
| 19 |
+
args, _ = parser.parse_known_args()
|
| 20 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 21 |
+
os.environ.setdefault(
|
| 22 |
+
"MPLCONFIGDIR", tempfile.mkdtemp(prefix="self_forcing_span_calibration_")
|
| 23 |
+
)
|
| 24 |
+
return str(args.gpu)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
PHYSICAL_GPU = preparse_gpu()
|
| 28 |
+
|
| 29 |
+
import torch
|
| 30 |
+
from omegaconf import OmegaConf
|
| 31 |
+
from safetensors.torch import load_file
|
| 32 |
+
|
| 33 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 34 |
+
if str(REPO_ROOT) not in sys.path:
|
| 35 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 36 |
+
|
| 37 |
+
from predictor_training.confidence import PredictorConfidenceHead
|
| 38 |
+
from scripts import evaluate_single_block_fppf as base
|
| 39 |
+
from scripts import generate_vbench8_extended_strategies as extended
|
| 40 |
+
from utils.wan_wrapper import WanTextEncoder
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
MAPPING_DEFAULT = REPO_ROOT / "assets/vbench8_extended_subset_mapping.json"
|
| 44 |
+
EXPERIMENT_DEFAULT = (
|
| 45 |
+
REPO_ROOT / "confidence_experiments/layer17_stage1_step2000_20260831"
|
| 46 |
+
)
|
| 47 |
+
OUTPUT_DEFAULT = (
|
| 48 |
+
REPO_ROOT
|
| 49 |
+
/ "confidence_experiments/layer17_stage1_step2000_spanrisk_vbench3_20260901"
|
| 50 |
+
)
|
| 51 |
+
BETAS = (0.0, 0.5, 1.0, 1.5, 2.0)
|
| 52 |
+
TARGETS = {
|
| 53 |
+
"step12": (6, 8, 10),
|
| 54 |
+
"step123": (6, 9, 12, 15),
|
| 55 |
+
}
|
| 56 |
+
CANDIDATE_STEPS = {
|
| 57 |
+
"step12": (1, 2),
|
| 58 |
+
"step123": (1, 2, 3),
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def atomic_json(path: Path, value: Any) -> None:
|
| 63 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 64 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 65 |
+
temporary.write_text(
|
| 66 |
+
json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
|
| 67 |
+
encoding="utf-8",
|
| 68 |
+
)
|
| 69 |
+
os.replace(temporary, path)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def quantile(values: list[float], fraction: float) -> float:
|
| 73 |
+
if not values:
|
| 74 |
+
raise ValueError("Cannot take a quantile of an empty list")
|
| 75 |
+
ordered = sorted(values)
|
| 76 |
+
position = fraction * (len(ordered) - 1)
|
| 77 |
+
lower = int(math.floor(position))
|
| 78 |
+
upper = int(math.ceil(position))
|
| 79 |
+
if lower == upper:
|
| 80 |
+
return ordered[lower]
|
| 81 |
+
weight = position - lower
|
| 82 |
+
return ordered[lower] * (1.0 - weight) + ordered[upper] * weight
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def beta_name(beta: float) -> str:
|
| 86 |
+
return str(beta).replace(".", "p")
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def read_mapping_row(path: Path, global_index: int) -> dict[str, Any]:
|
| 90 |
+
rows = extended.read_mapping(path)
|
| 91 |
+
matches = [row for row in rows if int(row["global_index"]) == global_index]
|
| 92 |
+
if len(matches) != 1:
|
| 93 |
+
raise ValueError(f"Expected one mapping row for global index {global_index}")
|
| 94 |
+
return matches[0]
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def load_models(
|
| 98 |
+
experiment_root: Path, device: torch.device
|
| 99 |
+
) -> tuple[Any, Any, Any, Any, Any]:
|
| 100 |
+
config = OmegaConf.merge(
|
| 101 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 102 |
+
OmegaConf.load(REPO_ROOT / "configs/self_forcing_sid.yaml"),
|
| 103 |
+
)
|
| 104 |
+
pipeline = base.build_pipeline(
|
| 105 |
+
config,
|
| 106 |
+
REPO_ROOT / "checkpoints/self_forcing_dmd.pt",
|
| 107 |
+
torch.nn.Identity(),
|
| 108 |
+
device,
|
| 109 |
+
)
|
| 110 |
+
predictor = base.load_predictor(
|
| 111 |
+
pipeline.generator.model,
|
| 112 |
+
{
|
| 113 |
+
"source_layer": 17,
|
| 114 |
+
"weights": REPO_ROOT
|
| 115 |
+
/ "training_runs/layer17_stage1_1000p_4gpu_b16_2000steps"
|
| 116 |
+
/ "checkpoint_step_2000/predictor.safetensors",
|
| 117 |
+
"gate_mode": "baseline",
|
| 118 |
+
},
|
| 119 |
+
device,
|
| 120 |
+
)
|
| 121 |
+
text_encoder = WanTextEncoder().to(device=device, dtype=torch.bfloat16).eval()
|
| 122 |
+
text_encoder.requires_grad_(False)
|
| 123 |
+
step12_head = PredictorConfidenceHead(num_steps=2).to(device=device).eval()
|
| 124 |
+
step12_head.load_state_dict(
|
| 125 |
+
load_file(
|
| 126 |
+
str(experiment_root / "confidence_step12/confidence_best.safetensors"),
|
| 127 |
+
device="cpu",
|
| 128 |
+
),
|
| 129 |
+
strict=True,
|
| 130 |
+
)
|
| 131 |
+
step12_head.requires_grad_(False)
|
| 132 |
+
step123_head = PredictorConfidenceHead(num_steps=3).to(device=device).eval()
|
| 133 |
+
step123_head.load_state_dict(
|
| 134 |
+
load_file(
|
| 135 |
+
str(experiment_root / "confidence_step123/confidence_best.safetensors"),
|
| 136 |
+
device="cpu",
|
| 137 |
+
),
|
| 138 |
+
strict=True,
|
| 139 |
+
)
|
| 140 |
+
step123_head.requires_grad_(False)
|
| 141 |
+
return pipeline, predictor, text_encoder, step12_head, step123_head
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def probe(args: argparse.Namespace) -> None:
|
| 145 |
+
if args.global_index is None:
|
| 146 |
+
raise ValueError("--global-index is required in probe mode")
|
| 147 |
+
row = read_mapping_row(args.mapping, args.global_index)
|
| 148 |
+
destination = args.output_root / "probes" / f"global_{args.global_index:04d}.json"
|
| 149 |
+
if destination.is_file() and not args.overwrite:
|
| 150 |
+
print(f"[cached] {destination}", flush=True)
|
| 151 |
+
return
|
| 152 |
+
device = torch.device("cuda")
|
| 153 |
+
torch.set_grad_enabled(False)
|
| 154 |
+
pipeline, predictor, text_encoder, step12_head, step123_head = load_models(
|
| 155 |
+
args.experiment_root, device
|
| 156 |
+
)
|
| 157 |
+
prompt = str(row["extended_prompt"])
|
| 158 |
+
conditional = text_encoder(text_prompts=[prompt])
|
| 159 |
+
probes: dict[str, Any] = {}
|
| 160 |
+
for family, head in (("step12", step12_head), ("step123", step123_head)):
|
| 161 |
+
config = {
|
| 162 |
+
"name": f"{family}_span_probe",
|
| 163 |
+
"policy": "dynamic",
|
| 164 |
+
"candidate_steps": list(CANDIDATE_STEPS[family]),
|
| 165 |
+
"beta": 0.0,
|
| 166 |
+
"threshold": -math.inf,
|
| 167 |
+
"target_accepts": 0,
|
| 168 |
+
"head": family,
|
| 169 |
+
"risk_mode": "outgoing_span",
|
| 170 |
+
}
|
| 171 |
+
_, diagnostic = extended.generate_rollout(
|
| 172 |
+
pipeline=pipeline,
|
| 173 |
+
conditional_dict=conditional,
|
| 174 |
+
seed=args.seed,
|
| 175 |
+
device=device,
|
| 176 |
+
predictor=predictor,
|
| 177 |
+
head=head,
|
| 178 |
+
config=config,
|
| 179 |
+
)
|
| 180 |
+
decisions = diagnostic.pop("decisions")
|
| 181 |
+
if any(decision["accepted"] for decision in decisions):
|
| 182 |
+
raise RuntimeError("Probe unexpectedly accepted a Predictor result")
|
| 183 |
+
probes[family] = {"generation": diagnostic, "decisions": decisions}
|
| 184 |
+
print(
|
| 185 |
+
f"[probe] global={args.global_index} family={family} "
|
| 186 |
+
f"decisions={len(decisions)}",
|
| 187 |
+
flush=True,
|
| 188 |
+
)
|
| 189 |
+
atomic_json(
|
| 190 |
+
destination,
|
| 191 |
+
{
|
| 192 |
+
"status": "complete",
|
| 193 |
+
"physical_gpu": PHYSICAL_GPU,
|
| 194 |
+
"global_index": int(row["global_index"]),
|
| 195 |
+
"prompt_suite": row["prompt_suite"],
|
| 196 |
+
"suite_index": int(row["suite_index"]),
|
| 197 |
+
"prompt": prompt,
|
| 198 |
+
"seed": args.seed,
|
| 199 |
+
"probes": probes,
|
| 200 |
+
},
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def prepare(args: argparse.Namespace) -> None:
|
| 205 |
+
paths = sorted((args.output_root / "probes").glob("global_*.json"))
|
| 206 |
+
if len(paths) != 3:
|
| 207 |
+
raise ValueError(f"Expected exactly three probe files, found {len(paths)}")
|
| 208 |
+
probes = [json.loads(path.read_text(encoding="utf-8")) for path in paths]
|
| 209 |
+
configurations: list[dict[str, Any]] = []
|
| 210 |
+
span_values: dict[int, set[float]] = {1: set(), 2: set(), 3: set()}
|
| 211 |
+
for family in ("step12", "step123"):
|
| 212 |
+
decisions = [
|
| 213 |
+
decision
|
| 214 |
+
for probe_record in probes
|
| 215 |
+
for decision in probe_record["probes"][family]["decisions"]
|
| 216 |
+
]
|
| 217 |
+
expected = 3 * 6 * len(CANDIDATE_STEPS[family])
|
| 218 |
+
if len(decisions) != expected:
|
| 219 |
+
raise ValueError(
|
| 220 |
+
f"Expected {expected} {family} decisions, got {len(decisions)}"
|
| 221 |
+
)
|
| 222 |
+
for decision in decisions:
|
| 223 |
+
span_values[int(decision["step"])].add(float(decision["outgoing_span"]))
|
| 224 |
+
max_calls = 6 * len(CANDIDATE_STEPS[family])
|
| 225 |
+
for beta in BETAS:
|
| 226 |
+
risks = [
|
| 227 |
+
float(decision["predicted_local_error"])
|
| 228 |
+
* float(decision["span_weight"])
|
| 229 |
+
* (1.0 + beta * float(decision["chunk_alpha"]))
|
| 230 |
+
for decision in decisions
|
| 231 |
+
]
|
| 232 |
+
for target in TARGETS[family]:
|
| 233 |
+
threshold = quantile(risks, target / max_calls)
|
| 234 |
+
configurations.append(
|
| 235 |
+
{
|
| 236 |
+
"name": f"{family}_span_b{beta_name(beta)}_k{target:02d}",
|
| 237 |
+
"policy": "dynamic",
|
| 238 |
+
"candidate_steps": list(CANDIDATE_STEPS[family]),
|
| 239 |
+
"beta": beta,
|
| 240 |
+
"threshold": threshold,
|
| 241 |
+
"target_accepts": target,
|
| 242 |
+
"head": family,
|
| 243 |
+
"risk_mode": "outgoing_span",
|
| 244 |
+
}
|
| 245 |
+
)
|
| 246 |
+
actual_spans = {
|
| 247 |
+
str(step): sorted(values) for step, values in span_values.items() if values
|
| 248 |
+
}
|
| 249 |
+
expected_spans = {1: 104.1666, 2: 208.3333, 3: 625.0}
|
| 250 |
+
for step, expected in expected_spans.items():
|
| 251 |
+
values = span_values[step]
|
| 252 |
+
if values and any(abs(value - expected) > 0.02 for value in values):
|
| 253 |
+
raise ValueError(f"Unexpected outgoing span at step {step}: {values}")
|
| 254 |
+
atomic_json(
|
| 255 |
+
args.output_root / "scan_configs.json",
|
| 256 |
+
{
|
| 257 |
+
"status": "ready",
|
| 258 |
+
"risk_definition": (
|
| 259 |
+
"predicted_local_error * (outgoing_span / 1000) * "
|
| 260 |
+
"(1 + beta * chunk_alpha)"
|
| 261 |
+
),
|
| 262 |
+
"probe_global_indices": [int(value["global_index"]) for value in probes],
|
| 263 |
+
"outgoing_spans": actual_spans,
|
| 264 |
+
"betas": list(BETAS),
|
| 265 |
+
"targets": {key: list(value) for key, value in TARGETS.items()},
|
| 266 |
+
"num_configs": len(configurations),
|
| 267 |
+
"configs": configurations,
|
| 268 |
+
},
|
| 269 |
+
)
|
| 270 |
+
print(f"[prepare] configurations={len(configurations)}", flush=True)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def prepare_threshold_only(args: argparse.Namespace) -> None:
|
| 274 |
+
probe_root = args.probe_root or args.output_root
|
| 275 |
+
paths = sorted((probe_root / "probes").glob("global_*.json"))
|
| 276 |
+
if len(paths) != 3:
|
| 277 |
+
raise ValueError(f"Expected exactly three probe files, found {len(paths)}")
|
| 278 |
+
probes = [json.loads(path.read_text(encoding="utf-8")) for path in paths]
|
| 279 |
+
configurations: list[dict[str, Any]] = []
|
| 280 |
+
span_values: dict[int, set[float]] = {1: set(), 2: set(), 3: set()}
|
| 281 |
+
beta = 2.0
|
| 282 |
+
for family in ("step12", "step123"):
|
| 283 |
+
decisions = [
|
| 284 |
+
decision
|
| 285 |
+
for probe_record in probes
|
| 286 |
+
for decision in probe_record["probes"][family]["decisions"]
|
| 287 |
+
]
|
| 288 |
+
expected = 3 * 6 * len(CANDIDATE_STEPS[family])
|
| 289 |
+
if len(decisions) != expected:
|
| 290 |
+
raise ValueError(
|
| 291 |
+
f"Expected {expected} {family} decisions, got {len(decisions)}"
|
| 292 |
+
)
|
| 293 |
+
for decision in decisions:
|
| 294 |
+
span_values[int(decision["step"])].add(float(decision["outgoing_span"]))
|
| 295 |
+
max_calls = 6 * len(CANDIDATE_STEPS[family])
|
| 296 |
+
risks = [
|
| 297 |
+
float(decision["predicted_local_error"])
|
| 298 |
+
* float(decision["span_weight"])
|
| 299 |
+
* (1.0 + beta * float(decision["chunk_alpha"]))
|
| 300 |
+
for decision in decisions
|
| 301 |
+
]
|
| 302 |
+
for rank in range(1, max_calls):
|
| 303 |
+
configurations.append(
|
| 304 |
+
{
|
| 305 |
+
"name": f"{family}_span_beta2_thr{rank:02d}",
|
| 306 |
+
"policy": "dynamic",
|
| 307 |
+
"candidate_steps": list(CANDIDATE_STEPS[family]),
|
| 308 |
+
"beta": beta,
|
| 309 |
+
"threshold": quantile(risks, rank / max_calls),
|
| 310 |
+
"target_accepts": rank,
|
| 311 |
+
"threshold_rank": rank,
|
| 312 |
+
"head": family,
|
| 313 |
+
"risk_mode": "outgoing_span",
|
| 314 |
+
}
|
| 315 |
+
)
|
| 316 |
+
atomic_json(
|
| 317 |
+
args.output_root / "scan_configs.json",
|
| 318 |
+
{
|
| 319 |
+
"status": "ready",
|
| 320 |
+
"risk_definition": (
|
| 321 |
+
"predicted_local_error * (outgoing_span / 1000) * "
|
| 322 |
+
"(1 + 2.0 * chunk_alpha)"
|
| 323 |
+
),
|
| 324 |
+
"beta": beta,
|
| 325 |
+
"probe_global_indices": [int(value["global_index"]) for value in probes],
|
| 326 |
+
"outgoing_spans": {
|
| 327 |
+
str(step): sorted(values)
|
| 328 |
+
for step, values in span_values.items()
|
| 329 |
+
if values
|
| 330 |
+
},
|
| 331 |
+
"threshold_search": "quantile rank 1..max_candidate_calls-1",
|
| 332 |
+
"targets": {key: list(value) for key, value in TARGETS.items()},
|
| 333 |
+
"num_configs": len(configurations),
|
| 334 |
+
"configs": configurations,
|
| 335 |
+
},
|
| 336 |
+
)
|
| 337 |
+
print(
|
| 338 |
+
f"[prepare-threshold-only] beta={beta} configurations={len(configurations)}",
|
| 339 |
+
flush=True,
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def latent_nrmse(predicted: torch.Tensor, reference: torch.Tensor) -> float:
|
| 344 |
+
numerator = (predicted.float() - reference.float()).square().sum()
|
| 345 |
+
denominator = reference.float().square().sum().clamp_min(1e-8)
|
| 346 |
+
return float(torch.sqrt(numerator / denominator))
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def scan(args: argparse.Namespace) -> None:
|
| 350 |
+
if args.global_index is None:
|
| 351 |
+
raise ValueError("--global-index is required in scan mode")
|
| 352 |
+
row = read_mapping_row(args.mapping, args.global_index)
|
| 353 |
+
config_path = args.output_root / "scan_configs.json"
|
| 354 |
+
config_manifest = json.loads(config_path.read_text(encoding="utf-8"))
|
| 355 |
+
configurations = list(config_manifest["configs"])
|
| 356 |
+
destination = args.output_root / "scans" / f"global_{args.global_index:04d}.json"
|
| 357 |
+
existing: dict[str, Any] = {}
|
| 358 |
+
if destination.is_file() and not args.overwrite:
|
| 359 |
+
existing = json.loads(destination.read_text(encoding="utf-8"))
|
| 360 |
+
if existing.get("status") == "complete":
|
| 361 |
+
print(f"[cached] {destination}", flush=True)
|
| 362 |
+
return
|
| 363 |
+
completed = {record["name"]: record for record in existing.get("results", [])}
|
| 364 |
+
device = torch.device("cuda")
|
| 365 |
+
torch.set_grad_enabled(False)
|
| 366 |
+
pipeline, predictor, text_encoder, step12_head, step123_head = load_models(
|
| 367 |
+
args.experiment_root, device
|
| 368 |
+
)
|
| 369 |
+
prompt = str(row["extended_prompt"])
|
| 370 |
+
conditional = text_encoder(text_prompts=[prompt])
|
| 371 |
+
reference, reference_diagnostic = extended.generate_rollout(
|
| 372 |
+
pipeline=pipeline,
|
| 373 |
+
conditional_dict=conditional,
|
| 374 |
+
seed=args.seed,
|
| 375 |
+
device=device,
|
| 376 |
+
predictor=predictor,
|
| 377 |
+
head=None,
|
| 378 |
+
config={
|
| 379 |
+
"name": "ffff",
|
| 380 |
+
"policy": "ffff",
|
| 381 |
+
"candidate_steps": [],
|
| 382 |
+
"beta": None,
|
| 383 |
+
"threshold": None,
|
| 384 |
+
"target_accepts": 0,
|
| 385 |
+
"head": None,
|
| 386 |
+
"risk_mode": "outgoing_span",
|
| 387 |
+
},
|
| 388 |
+
)
|
| 389 |
+
for position, config in enumerate(configurations, start=1):
|
| 390 |
+
name = str(config["name"])
|
| 391 |
+
if name in completed:
|
| 392 |
+
continue
|
| 393 |
+
head = step12_head if config["head"] == "step12" else step123_head
|
| 394 |
+
latent, diagnostic = extended.generate_rollout(
|
| 395 |
+
pipeline=pipeline,
|
| 396 |
+
conditional_dict=conditional,
|
| 397 |
+
seed=args.seed,
|
| 398 |
+
device=device,
|
| 399 |
+
predictor=predictor,
|
| 400 |
+
head=head,
|
| 401 |
+
config=config,
|
| 402 |
+
)
|
| 403 |
+
decisions = diagnostic.pop("decisions")
|
| 404 |
+
record = {
|
| 405 |
+
**config,
|
| 406 |
+
"accepted_predictor_calls": diagnostic["accepted_predictor_calls"],
|
| 407 |
+
"full_calls": diagnostic["full_calls"],
|
| 408 |
+
"policy_latency_ms": diagnostic["policy_latency_ms"],
|
| 409 |
+
"context_dit_time_ms": diagnostic["context_dit_time_ms"],
|
| 410 |
+
"latent_nrmse_vs_ffff": latent_nrmse(latent, reference),
|
| 411 |
+
"decisions": decisions,
|
| 412 |
+
}
|
| 413 |
+
completed[name] = record
|
| 414 |
+
atomic_json(
|
| 415 |
+
destination,
|
| 416 |
+
{
|
| 417 |
+
"status": "running",
|
| 418 |
+
"physical_gpu": PHYSICAL_GPU,
|
| 419 |
+
"global_index": int(row["global_index"]),
|
| 420 |
+
"prompt_suite": row["prompt_suite"],
|
| 421 |
+
"suite_index": int(row["suite_index"]),
|
| 422 |
+
"prompt": prompt,
|
| 423 |
+
"seed": args.seed,
|
| 424 |
+
"ffff": {
|
| 425 |
+
key: value
|
| 426 |
+
for key, value in reference_diagnostic.items()
|
| 427 |
+
if key != "decisions"
|
| 428 |
+
},
|
| 429 |
+
"results": [completed[key] for key in sorted(completed)],
|
| 430 |
+
},
|
| 431 |
+
)
|
| 432 |
+
print(
|
| 433 |
+
f"[scan] global={args.global_index} {position}/{len(configurations)} "
|
| 434 |
+
f"name={name} K={record['accepted_predictor_calls']} "
|
| 435 |
+
f"latent_nrmse={record['latent_nrmse_vs_ffff']:.6f}",
|
| 436 |
+
flush=True,
|
| 437 |
+
)
|
| 438 |
+
del latent
|
| 439 |
+
atomic_json(
|
| 440 |
+
destination,
|
| 441 |
+
{
|
| 442 |
+
"status": "complete",
|
| 443 |
+
"physical_gpu": PHYSICAL_GPU,
|
| 444 |
+
"global_index": int(row["global_index"]),
|
| 445 |
+
"prompt_suite": row["prompt_suite"],
|
| 446 |
+
"suite_index": int(row["suite_index"]),
|
| 447 |
+
"prompt": prompt,
|
| 448 |
+
"seed": args.seed,
|
| 449 |
+
"ffff": {
|
| 450 |
+
key: value
|
| 451 |
+
for key, value in reference_diagnostic.items()
|
| 452 |
+
if key != "decisions"
|
| 453 |
+
},
|
| 454 |
+
"results": [completed[key] for key in sorted(completed)],
|
| 455 |
+
},
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def summarize(args: argparse.Namespace) -> None:
|
| 460 |
+
paths = sorted((args.output_root / "scans").glob("global_*.json"))
|
| 461 |
+
if len(paths) != 3:
|
| 462 |
+
raise ValueError(f"Expected exactly three scan files, found {len(paths)}")
|
| 463 |
+
scans = [json.loads(path.read_text(encoding="utf-8")) for path in paths]
|
| 464 |
+
if any(scan.get("status") != "complete" for scan in scans):
|
| 465 |
+
raise RuntimeError("At least one scan is incomplete")
|
| 466 |
+
by_name = [
|
| 467 |
+
{record["name"]: record for record in scan["results"]} for scan in scans
|
| 468 |
+
]
|
| 469 |
+
names = sorted(set(by_name[0]))
|
| 470 |
+
if any(set(records) != set(names) for records in by_name[1:]):
|
| 471 |
+
raise RuntimeError("Scan configuration sets differ")
|
| 472 |
+
summary: list[dict[str, Any]] = []
|
| 473 |
+
for name in names:
|
| 474 |
+
rows = [records[name] for records in by_name]
|
| 475 |
+
first = rows[0]
|
| 476 |
+
summary.append(
|
| 477 |
+
{
|
| 478 |
+
"name": name,
|
| 479 |
+
"head": first["head"],
|
| 480 |
+
"candidate_steps": first["candidate_steps"],
|
| 481 |
+
"beta": first["beta"],
|
| 482 |
+
"threshold": first["threshold"],
|
| 483 |
+
"target_accepts": first["target_accepts"],
|
| 484 |
+
"mean_accepted_predictor_calls": sum(
|
| 485 |
+
float(row["accepted_predictor_calls"]) for row in rows
|
| 486 |
+
)
|
| 487 |
+
/ len(rows),
|
| 488 |
+
"mean_full_calls": sum(float(row["full_calls"]) for row in rows)
|
| 489 |
+
/ len(rows),
|
| 490 |
+
"mean_policy_latency_ms": sum(
|
| 491 |
+
float(row["policy_latency_ms"]) for row in rows
|
| 492 |
+
)
|
| 493 |
+
/ len(rows),
|
| 494 |
+
"mean_latent_nrmse_vs_ffff": sum(
|
| 495 |
+
float(row["latent_nrmse_vs_ffff"]) for row in rows
|
| 496 |
+
)
|
| 497 |
+
/ len(rows),
|
| 498 |
+
"per_prompt_accepts": [
|
| 499 |
+
int(row["accepted_predictor_calls"]) for row in rows
|
| 500 |
+
],
|
| 501 |
+
"per_prompt_latent_nrmse": [
|
| 502 |
+
float(row["latent_nrmse_vs_ffff"]) for row in rows
|
| 503 |
+
],
|
| 504 |
+
}
|
| 505 |
+
)
|
| 506 |
+
selected: list[dict[str, Any]] = []
|
| 507 |
+
for family, targets in TARGETS.items():
|
| 508 |
+
for target in targets:
|
| 509 |
+
candidates = [
|
| 510 |
+
row
|
| 511 |
+
for row in summary
|
| 512 |
+
if row["head"] == family and int(row["target_accepts"]) == target
|
| 513 |
+
]
|
| 514 |
+
within_budget = [
|
| 515 |
+
row
|
| 516 |
+
for row in candidates
|
| 517 |
+
if abs(float(row["mean_accepted_predictor_calls"]) - target) <= 0.5
|
| 518 |
+
]
|
| 519 |
+
pool = within_budget or candidates
|
| 520 |
+
choice = min(
|
| 521 |
+
pool,
|
| 522 |
+
key=lambda row: (
|
| 523 |
+
float(row["mean_latent_nrmse_vs_ffff"]),
|
| 524 |
+
abs(float(row["mean_accepted_predictor_calls"]) - target),
|
| 525 |
+
float(row["mean_policy_latency_ms"]),
|
| 526 |
+
float(row["beta"]),
|
| 527 |
+
)
|
| 528 |
+
if within_budget
|
| 529 |
+
else (
|
| 530 |
+
abs(float(row["mean_accepted_predictor_calls"]) - target),
|
| 531 |
+
float(row["mean_latent_nrmse_vs_ffff"]),
|
| 532 |
+
float(row["mean_policy_latency_ms"]),
|
| 533 |
+
float(row["beta"]),
|
| 534 |
+
),
|
| 535 |
+
)
|
| 536 |
+
selected.append(choice)
|
| 537 |
+
atomic_json(
|
| 538 |
+
args.output_root / "calibration_summary.json",
|
| 539 |
+
{
|
| 540 |
+
"status": "complete",
|
| 541 |
+
"global_indices": [int(scan["global_index"]) for scan in scans],
|
| 542 |
+
"selection_rule": (
|
| 543 |
+
"within mean K +/-0.5 choose lowest mean latent nRMSE; if no "
|
| 544 |
+
"candidate is within budget, choose nearest K first"
|
| 545 |
+
),
|
| 546 |
+
"summary": summary,
|
| 547 |
+
"selected": selected,
|
| 548 |
+
},
|
| 549 |
+
)
|
| 550 |
+
print("[selected]", flush=True)
|
| 551 |
+
for row in selected:
|
| 552 |
+
print(
|
| 553 |
+
f" {row['head']} K{row['target_accepts']:02d}: beta={row['beta']} "
|
| 554 |
+
f"threshold={row['threshold']:.8f} "
|
| 555 |
+
f"actual_K={row['mean_accepted_predictor_calls']:.3f} "
|
| 556 |
+
f"latent_nrmse={row['mean_latent_nrmse_vs_ffff']:.6f}",
|
| 557 |
+
flush=True,
|
| 558 |
+
)
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
def summarize_threshold_only(args: argparse.Namespace) -> None:
|
| 562 |
+
paths = sorted((args.output_root / "scans").glob("global_*.json"))
|
| 563 |
+
if len(paths) != 3:
|
| 564 |
+
raise ValueError(f"Expected exactly three scan files, found {len(paths)}")
|
| 565 |
+
scans = [json.loads(path.read_text(encoding="utf-8")) for path in paths]
|
| 566 |
+
if any(scan.get("status") != "complete" for scan in scans):
|
| 567 |
+
raise RuntimeError("At least one scan is incomplete")
|
| 568 |
+
by_name = [
|
| 569 |
+
{record["name"]: record for record in scan["results"]} for scan in scans
|
| 570 |
+
]
|
| 571 |
+
names = sorted(set(by_name[0]))
|
| 572 |
+
if any(set(records) != set(names) for records in by_name[1:]):
|
| 573 |
+
raise RuntimeError("Scan configuration sets differ")
|
| 574 |
+
summary: list[dict[str, Any]] = []
|
| 575 |
+
for name in names:
|
| 576 |
+
rows = [records[name] for records in by_name]
|
| 577 |
+
first = rows[0]
|
| 578 |
+
summary.append(
|
| 579 |
+
{
|
| 580 |
+
"name": name,
|
| 581 |
+
"head": first["head"],
|
| 582 |
+
"candidate_steps": first["candidate_steps"],
|
| 583 |
+
"beta": first["beta"],
|
| 584 |
+
"threshold": first["threshold"],
|
| 585 |
+
"threshold_rank": first.get("threshold_rank"),
|
| 586 |
+
"mean_accepted_predictor_calls": sum(
|
| 587 |
+
float(row["accepted_predictor_calls"]) for row in rows
|
| 588 |
+
)
|
| 589 |
+
/ len(rows),
|
| 590 |
+
"mean_full_calls": sum(float(row["full_calls"]) for row in rows)
|
| 591 |
+
/ len(rows),
|
| 592 |
+
"mean_policy_latency_ms": sum(
|
| 593 |
+
float(row["policy_latency_ms"]) for row in rows
|
| 594 |
+
)
|
| 595 |
+
/ len(rows),
|
| 596 |
+
"mean_latent_nrmse_vs_ffff": sum(
|
| 597 |
+
float(row["latent_nrmse_vs_ffff"]) for row in rows
|
| 598 |
+
)
|
| 599 |
+
/ len(rows),
|
| 600 |
+
"per_prompt_accepts": [
|
| 601 |
+
int(row["accepted_predictor_calls"]) for row in rows
|
| 602 |
+
],
|
| 603 |
+
}
|
| 604 |
+
)
|
| 605 |
+
selected: list[dict[str, Any]] = []
|
| 606 |
+
for family, targets in TARGETS.items():
|
| 607 |
+
for target in targets:
|
| 608 |
+
candidates = [row for row in summary if row["head"] == family]
|
| 609 |
+
within_budget = [
|
| 610 |
+
row
|
| 611 |
+
for row in candidates
|
| 612 |
+
if abs(float(row["mean_accepted_predictor_calls"]) - target) <= 0.5
|
| 613 |
+
]
|
| 614 |
+
pool = within_budget or candidates
|
| 615 |
+
choice = min(
|
| 616 |
+
pool,
|
| 617 |
+
key=lambda row: (
|
| 618 |
+
float(row["mean_latent_nrmse_vs_ffff"]),
|
| 619 |
+
abs(float(row["mean_accepted_predictor_calls"]) - target),
|
| 620 |
+
float(row["threshold"]),
|
| 621 |
+
)
|
| 622 |
+
if within_budget
|
| 623 |
+
else (
|
| 624 |
+
abs(float(row["mean_accepted_predictor_calls"]) - target),
|
| 625 |
+
float(row["mean_latent_nrmse_vs_ffff"]),
|
| 626 |
+
float(row["threshold"]),
|
| 627 |
+
),
|
| 628 |
+
)
|
| 629 |
+
selected.append(
|
| 630 |
+
{
|
| 631 |
+
**choice,
|
| 632 |
+
"target_accepts": target,
|
| 633 |
+
}
|
| 634 |
+
)
|
| 635 |
+
atomic_json(
|
| 636 |
+
args.output_root / "threshold_summary.json",
|
| 637 |
+
{
|
| 638 |
+
"status": "complete",
|
| 639 |
+
"beta": 2.0,
|
| 640 |
+
"global_indices": [int(scan["global_index"]) for scan in scans],
|
| 641 |
+
"selection_rule": (
|
| 642 |
+
"fixed beta=2; for each target K choose threshold with mean K "
|
| 643 |
+
"within +/-0.5 and lowest latent nRMSE"
|
| 644 |
+
),
|
| 645 |
+
"summary": summary,
|
| 646 |
+
"selected": selected,
|
| 647 |
+
},
|
| 648 |
+
)
|
| 649 |
+
print("[selected-threshold-only]", flush=True)
|
| 650 |
+
for row in selected:
|
| 651 |
+
print(
|
| 652 |
+
f" {row['head']} K{row['target_accepts']:02d}: "
|
| 653 |
+
f"threshold={row['threshold']:.8f} "
|
| 654 |
+
f"actual_K={row['mean_accepted_predictor_calls']:.3f} "
|
| 655 |
+
f"latent_nrmse={row['mean_latent_nrmse_vs_ffff']:.6f}",
|
| 656 |
+
flush=True,
|
| 657 |
+
)
|
| 658 |
+
|
| 659 |
+
|
| 660 |
+
def parse_args() -> argparse.Namespace:
|
| 661 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 662 |
+
parser.add_argument(
|
| 663 |
+
"--mode",
|
| 664 |
+
choices=(
|
| 665 |
+
"probe", "prepare", "prepare_threshold", "scan", "summarize",
|
| 666 |
+
"summarize_threshold",
|
| 667 |
+
),
|
| 668 |
+
required=True,
|
| 669 |
+
)
|
| 670 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 671 |
+
parser.add_argument("--global-index", type=int, default=None)
|
| 672 |
+
parser.add_argument("--mapping", type=Path, default=MAPPING_DEFAULT)
|
| 673 |
+
parser.add_argument("--experiment-root", type=Path, default=EXPERIMENT_DEFAULT)
|
| 674 |
+
parser.add_argument("--output-root", type=Path, default=OUTPUT_DEFAULT)
|
| 675 |
+
parser.add_argument("--probe-root", type=Path, default=None)
|
| 676 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 677 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 678 |
+
args = parser.parse_args()
|
| 679 |
+
args.mapping = args.mapping.resolve()
|
| 680 |
+
args.experiment_root = args.experiment_root.resolve()
|
| 681 |
+
args.output_root = args.output_root.resolve()
|
| 682 |
+
args.probe_root = args.probe_root.resolve() if args.probe_root is not None else None
|
| 683 |
+
return args
|
| 684 |
+
|
| 685 |
+
|
| 686 |
+
def main() -> None:
|
| 687 |
+
args = parse_args()
|
| 688 |
+
args.output_root.mkdir(parents=True, exist_ok=True)
|
| 689 |
+
if args.mode == "probe":
|
| 690 |
+
probe(args)
|
| 691 |
+
elif args.mode == "prepare":
|
| 692 |
+
prepare(args)
|
| 693 |
+
elif args.mode == "prepare_threshold":
|
| 694 |
+
prepare_threshold_only(args)
|
| 695 |
+
elif args.mode == "scan":
|
| 696 |
+
scan(args)
|
| 697 |
+
else:
|
| 698 |
+
if args.mode == "summarize_threshold":
|
| 699 |
+
summarize_threshold_only(args)
|
| 700 |
+
else:
|
| 701 |
+
summarize(args)
|
| 702 |
+
|
| 703 |
+
|
| 704 |
+
if __name__ == "__main__":
|
| 705 |
+
main()
|
scripts/create_lmdb_14b_shards.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
python create_lmdb_14b_shards.py \
|
| 3 |
+
--data_path /mnt/localssd/wanx_14b_data \
|
| 4 |
+
--lmdb_path /mnt/localssd/wanx_14B_shift-3.0_cfg-5.0_lmdb
|
| 5 |
+
"""
|
| 6 |
+
from tqdm import tqdm
|
| 7 |
+
import numpy as np
|
| 8 |
+
import argparse
|
| 9 |
+
import torch
|
| 10 |
+
import lmdb
|
| 11 |
+
import glob
|
| 12 |
+
import os
|
| 13 |
+
|
| 14 |
+
from utils.lmdb import store_arrays_to_lmdb, process_data_dict
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def main():
|
| 18 |
+
"""
|
| 19 |
+
Aggregate all ode pairs inside a folder into a lmdb dataset.
|
| 20 |
+
Each pt file should contain a (key, value) pair representing a
|
| 21 |
+
video's ODE trajectories.
|
| 22 |
+
"""
|
| 23 |
+
parser = argparse.ArgumentParser()
|
| 24 |
+
parser.add_argument("--data_path", type=str,
|
| 25 |
+
required=True, help="path to ode pairs")
|
| 26 |
+
parser.add_argument("--lmdb_path", type=str,
|
| 27 |
+
required=True, help="path to lmdb")
|
| 28 |
+
parser.add_argument("--num_shards", type=int,
|
| 29 |
+
default=16, help="num_shards")
|
| 30 |
+
|
| 31 |
+
args = parser.parse_args()
|
| 32 |
+
|
| 33 |
+
all_dirs = sorted(os.listdir(args.data_path))
|
| 34 |
+
|
| 35 |
+
# figure out the maximum map size needed
|
| 36 |
+
map_size = int(1e12) # adapt to your need, set to 1TB by default
|
| 37 |
+
os.makedirs(args.lmdb_path, exist_ok=True)
|
| 38 |
+
# 1) Open one LMDB env per shard
|
| 39 |
+
envs = []
|
| 40 |
+
num_shards = args.num_shards
|
| 41 |
+
for shard_id in range(num_shards):
|
| 42 |
+
print("shard_id ", shard_id)
|
| 43 |
+
path = os.path.join(args.lmdb_path, f"shard_{shard_id}")
|
| 44 |
+
env = lmdb.open(path,
|
| 45 |
+
map_size=map_size,
|
| 46 |
+
subdir=True, # set to True if you want a directory per env
|
| 47 |
+
readonly=False,
|
| 48 |
+
metasync=True,
|
| 49 |
+
sync=True,
|
| 50 |
+
lock=True,
|
| 51 |
+
readahead=False,
|
| 52 |
+
meminit=False)
|
| 53 |
+
envs.append(env)
|
| 54 |
+
|
| 55 |
+
counters = [0] * num_shards
|
| 56 |
+
seen_prompts = set() # for deduplication
|
| 57 |
+
total_samples = 0
|
| 58 |
+
all_files = []
|
| 59 |
+
|
| 60 |
+
for part_dir in all_dirs:
|
| 61 |
+
all_files += sorted(glob.glob(os.path.join(args.data_path, part_dir, "*.pt")))
|
| 62 |
+
|
| 63 |
+
# 2) Prepare a write transaction for each shard
|
| 64 |
+
for idx, file in tqdm(enumerate(all_files)):
|
| 65 |
+
try:
|
| 66 |
+
data_dict = torch.load(file)
|
| 67 |
+
data_dict = process_data_dict(data_dict, seen_prompts)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"Error processing {file}: {e}")
|
| 70 |
+
continue
|
| 71 |
+
|
| 72 |
+
if data_dict["latents"].shape != (1, 21, 16, 60, 104):
|
| 73 |
+
continue
|
| 74 |
+
|
| 75 |
+
shard_id = idx % num_shards
|
| 76 |
+
# write to lmdb file
|
| 77 |
+
store_arrays_to_lmdb(envs[shard_id], data_dict, start_index=counters[shard_id])
|
| 78 |
+
counters[shard_id] += len(data_dict['prompts'])
|
| 79 |
+
data_shape = data_dict["latents"].shape
|
| 80 |
+
|
| 81 |
+
total_samples += len(all_files)
|
| 82 |
+
|
| 83 |
+
print(len(seen_prompts))
|
| 84 |
+
|
| 85 |
+
# save each entry's shape to lmdb
|
| 86 |
+
for shard_id, env in enumerate(envs):
|
| 87 |
+
with env.begin(write=True) as txn:
|
| 88 |
+
for key, val in (data_dict.items()):
|
| 89 |
+
assert len(data_shape) == 5
|
| 90 |
+
array_shape = np.array(data_shape) # val.shape)
|
| 91 |
+
array_shape[0] = counters[shard_id]
|
| 92 |
+
shape_key = f"{key}_shape".encode()
|
| 93 |
+
print(shape_key, array_shape)
|
| 94 |
+
shape_str = " ".join(map(str, array_shape))
|
| 95 |
+
txn.put(shape_key, shape_str.encode())
|
| 96 |
+
|
| 97 |
+
print(f"Finished writing {total_samples} examples into {num_shards} shards under {args.lmdb_path}")
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
main()
|
scripts/create_lmdb_iterative.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from tqdm import tqdm
|
| 2 |
+
import numpy as np
|
| 3 |
+
import argparse
|
| 4 |
+
import torch
|
| 5 |
+
import lmdb
|
| 6 |
+
import glob
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
from utils.lmdb import store_arrays_to_lmdb, process_data_dict
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def main():
|
| 13 |
+
"""
|
| 14 |
+
Aggregate all ode pairs inside a folder into a lmdb dataset.
|
| 15 |
+
Each pt file should contain a (key, value) pair representing a
|
| 16 |
+
video's ODE trajectories.
|
| 17 |
+
"""
|
| 18 |
+
parser = argparse.ArgumentParser()
|
| 19 |
+
parser.add_argument("--data_path", type=str,
|
| 20 |
+
required=True, help="path to ode pairs")
|
| 21 |
+
parser.add_argument("--lmdb_path", type=str,
|
| 22 |
+
required=True, help="path to lmdb")
|
| 23 |
+
|
| 24 |
+
args = parser.parse_args()
|
| 25 |
+
|
| 26 |
+
all_files = sorted(glob.glob(os.path.join(args.data_path, "*.pt")))
|
| 27 |
+
|
| 28 |
+
# figure out the maximum map size needed
|
| 29 |
+
total_array_size = 5000000000000 # adapt to your need, set to 5TB by default
|
| 30 |
+
|
| 31 |
+
env = lmdb.open(args.lmdb_path, map_size=total_array_size * 2)
|
| 32 |
+
|
| 33 |
+
counter = 0
|
| 34 |
+
|
| 35 |
+
seen_prompts = set() # for deduplication
|
| 36 |
+
|
| 37 |
+
for index, file in tqdm(enumerate(all_files)):
|
| 38 |
+
# read from disk
|
| 39 |
+
data_dict = torch.load(file)
|
| 40 |
+
|
| 41 |
+
data_dict = process_data_dict(data_dict, seen_prompts)
|
| 42 |
+
|
| 43 |
+
# write to lmdb file
|
| 44 |
+
store_arrays_to_lmdb(env, data_dict, start_index=counter)
|
| 45 |
+
counter += len(data_dict['prompts'])
|
| 46 |
+
|
| 47 |
+
# save each entry's shape to lmdb
|
| 48 |
+
with env.begin(write=True) as txn:
|
| 49 |
+
for key, val in data_dict.items():
|
| 50 |
+
print(key, val)
|
| 51 |
+
array_shape = np.array(val.shape)
|
| 52 |
+
array_shape[0] = counter
|
| 53 |
+
|
| 54 |
+
shape_key = f"{key}_shape".encode()
|
| 55 |
+
shape_str = " ".join(map(str, array_shape))
|
| 56 |
+
txn.put(shape_key, shape_str.encode())
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if __name__ == "__main__":
|
| 60 |
+
main()
|
scripts/eval_vbench8_extended_naive_baselines.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Score generated naive baselines with VBench-8 and build final summaries.
|
| 3 |
+
|
| 4 |
+
This script does not generate videos. Run
|
| 5 |
+
``generate_vbench8_extended_naive_baselines.py`` first, then use this entry
|
| 6 |
+
point to score FFFF plus any selected naive strategies. Strategies are scored
|
| 7 |
+
sequentially on the requested GPU; separate processes may be used on different
|
| 8 |
+
GPUs by passing disjoint ``--strategy`` sets and ``--no-summarize``.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import os
|
| 15 |
+
import subprocess
|
| 16 |
+
import sys
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 21 |
+
if str(REPO_ROOT) not in sys.path:
|
| 22 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 23 |
+
|
| 24 |
+
from scripts.naive_vbench_policies import (
|
| 25 |
+
ALL_STRATEGY_NAMES,
|
| 26 |
+
EVALUATION_STRATEGY_NAMES,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
OUTPUT_DEFAULT = REPO_ROOT / "evaluation_runs/vbench8_extended_naive_baselines"
|
| 31 |
+
MAPPING_DEFAULT = REPO_ROOT / "assets/vbench8_extended_subset_mapping.json"
|
| 32 |
+
EXTENDED_PROMPTS_DEFAULT = REPO_ROOT / "prompts/vbench/all_dimension_extended.txt"
|
| 33 |
+
VBENCH_SITE_DEFAULT = REPO_ROOT / ".evaluation_env/vbench_site2"
|
| 34 |
+
VBENCH_INFO_DEFAULT = VBENCH_SITE_DEFAULT / "vbench/VBench_full_info.json"
|
| 35 |
+
VBENCH_CACHE_DEFAULT = Path("/data3/chenzhuo/.cache/vbench")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def parse_args() -> argparse.Namespace:
|
| 39 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 40 |
+
parser.add_argument("--gpu", default="0")
|
| 41 |
+
parser.add_argument("--output-root", type=Path, default=OUTPUT_DEFAULT)
|
| 42 |
+
parser.add_argument("--mapping", type=Path, default=MAPPING_DEFAULT)
|
| 43 |
+
parser.add_argument(
|
| 44 |
+
"--extended-prompts", type=Path, default=EXTENDED_PROMPTS_DEFAULT
|
| 45 |
+
)
|
| 46 |
+
parser.add_argument("--vbench-site", type=Path, default=VBENCH_SITE_DEFAULT)
|
| 47 |
+
parser.add_argument("--vbench-info", type=Path, default=VBENCH_INFO_DEFAULT)
|
| 48 |
+
parser.add_argument("--vbench-cache", type=Path, default=VBENCH_CACHE_DEFAULT)
|
| 49 |
+
parser.add_argument(
|
| 50 |
+
"--strategy",
|
| 51 |
+
action="append",
|
| 52 |
+
choices=EVALUATION_STRATEGY_NAMES,
|
| 53 |
+
default=None,
|
| 54 |
+
help="Naive strategy to score; repeat as needed. Default: all six.",
|
| 55 |
+
)
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"--score-ffff",
|
| 58 |
+
action=argparse.BooleanOptionalAction,
|
| 59 |
+
default=True,
|
| 60 |
+
help="Also score the matched FFFF videos.",
|
| 61 |
+
)
|
| 62 |
+
parser.add_argument(
|
| 63 |
+
"--skip-existing", action=argparse.BooleanOptionalAction, default=True
|
| 64 |
+
)
|
| 65 |
+
parser.add_argument(
|
| 66 |
+
"--summarize",
|
| 67 |
+
action=argparse.BooleanOptionalAction,
|
| 68 |
+
default=True,
|
| 69 |
+
help="Build pixel and final summaries for this exact strategy set.",
|
| 70 |
+
)
|
| 71 |
+
return parser.parse_args()
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def checked_file(path: Path, label: str) -> Path:
|
| 75 |
+
resolved = path.resolve()
|
| 76 |
+
if not resolved.is_file():
|
| 77 |
+
raise FileNotFoundError(f"Missing {label}: {resolved}")
|
| 78 |
+
return resolved
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def run(command: list[str], env: dict[str, str]) -> None:
|
| 82 |
+
print("[run] " + " ".join(command), flush=True)
|
| 83 |
+
subprocess.run(command, cwd=REPO_ROOT, env=env, check=True)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def main() -> None:
|
| 87 |
+
args = parse_args()
|
| 88 |
+
selected = args.strategy or list(EVALUATION_STRATEGY_NAMES)
|
| 89 |
+
if len(set(selected)) != len(selected):
|
| 90 |
+
raise ValueError("--strategy values must be unique")
|
| 91 |
+
strategies = (["ffff"] if args.score_ffff else []) + selected
|
| 92 |
+
if args.summarize and "ffff" not in strategies:
|
| 93 |
+
raise ValueError("Final summaries require --score-ffff")
|
| 94 |
+
unknown = set(strategies) - set(ALL_STRATEGY_NAMES)
|
| 95 |
+
if unknown:
|
| 96 |
+
raise ValueError(f"Unknown strategies: {sorted(unknown)}")
|
| 97 |
+
|
| 98 |
+
output_root = args.output_root.resolve()
|
| 99 |
+
mapping = checked_file(args.mapping, "mapping")
|
| 100 |
+
extended_prompts = checked_file(args.extended_prompts, "extended prompts")
|
| 101 |
+
vbench_site = args.vbench_site.resolve()
|
| 102 |
+
if not vbench_site.is_dir():
|
| 103 |
+
raise FileNotFoundError(f"Missing VBench site directory: {vbench_site}")
|
| 104 |
+
vbench_info = checked_file(args.vbench_info, "VBench full info")
|
| 105 |
+
videos_root = output_root / "generated_videos"
|
| 106 |
+
if not videos_root.is_dir():
|
| 107 |
+
raise FileNotFoundError(f"Missing generated videos: {videos_root}")
|
| 108 |
+
|
| 109 |
+
env = os.environ.copy()
|
| 110 |
+
existing_pythonpath = env.get("PYTHONPATH")
|
| 111 |
+
pythonpath_parts = [str(vbench_site), str(REPO_ROOT)]
|
| 112 |
+
if existing_pythonpath:
|
| 113 |
+
pythonpath_parts.append(existing_pythonpath)
|
| 114 |
+
env["PYTHONPATH"] = os.pathsep.join(pythonpath_parts)
|
| 115 |
+
env["VBENCH_CACHE_DIR"] = str(args.vbench_cache.resolve())
|
| 116 |
+
env["VBENCH_BERT_MODEL_DIR"] = str(
|
| 117 |
+
(args.vbench_cache / "bert-base-uncased").resolve()
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
if args.summarize:
|
| 121 |
+
run(
|
| 122 |
+
[
|
| 123 |
+
sys.executable,
|
| 124 |
+
str(REPO_ROOT / "scripts/summarize_vbench8_generation.py"),
|
| 125 |
+
"--output-root",
|
| 126 |
+
str(output_root),
|
| 127 |
+
"--strategies",
|
| 128 |
+
*strategies,
|
| 129 |
+
],
|
| 130 |
+
env,
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
for strategy in strategies:
|
| 134 |
+
command = [
|
| 135 |
+
sys.executable,
|
| 136 |
+
str(REPO_ROOT / "scripts/eval_vbench8_extended_subset.py"),
|
| 137 |
+
"--gpu",
|
| 138 |
+
str(args.gpu),
|
| 139 |
+
"--strategy",
|
| 140 |
+
strategy,
|
| 141 |
+
"--mapping",
|
| 142 |
+
str(mapping),
|
| 143 |
+
"--videos-root",
|
| 144 |
+
str(videos_root),
|
| 145 |
+
"--output-root",
|
| 146 |
+
str(output_root),
|
| 147 |
+
"--vbench-info",
|
| 148 |
+
str(vbench_info),
|
| 149 |
+
]
|
| 150 |
+
if args.skip_existing:
|
| 151 |
+
command.append("--skip-existing")
|
| 152 |
+
run(command, env)
|
| 153 |
+
|
| 154 |
+
if args.summarize:
|
| 155 |
+
run(
|
| 156 |
+
[
|
| 157 |
+
sys.executable,
|
| 158 |
+
str(REPO_ROOT / "scripts/summarize_vbench8_extended.py"),
|
| 159 |
+
"--output-root",
|
| 160 |
+
str(output_root),
|
| 161 |
+
"--mapping",
|
| 162 |
+
str(mapping),
|
| 163 |
+
"--extended-prompts",
|
| 164 |
+
str(extended_prompts),
|
| 165 |
+
"--vbench-info",
|
| 166 |
+
str(vbench_info),
|
| 167 |
+
"--strategies",
|
| 168 |
+
*strategies,
|
| 169 |
+
],
|
| 170 |
+
env,
|
| 171 |
+
)
|
| 172 |
+
print(f"[complete] strategies={','.join(strategies)}", flush=True)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
if __name__ == "__main__":
|
| 176 |
+
main()
|
scripts/eval_vbench8_extended_subset.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run the standard VBench metrics for one generated strategy.
|
| 3 |
+
|
| 4 |
+
The official VBench metric modules are used unchanged. Only the metadata
|
| 5 |
+
builder is replaced so that the evaluator reads the exact extended prompt used
|
| 6 |
+
for generation and the auditable mapping can use suite-indexed filenames.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
import hashlib
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
import tempfile
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
from typing import Any
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def preparse_gpu() -> str:
|
| 22 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 23 |
+
parser.add_argument("--gpu", default="0")
|
| 24 |
+
args, _ = parser.parse_known_args()
|
| 25 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 26 |
+
os.environ.setdefault("MPLCONFIGDIR", tempfile.mkdtemp(prefix="vbench_mpl_"))
|
| 27 |
+
return str(args.gpu)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
PHYSICAL_GPU = preparse_gpu()
|
| 31 |
+
|
| 32 |
+
_vbench_cache = Path(
|
| 33 |
+
os.environ.get("VBENCH_CACHE_DIR", "/data3/chenzhuo/.cache/vbench")
|
| 34 |
+
).expanduser()
|
| 35 |
+
os.environ.setdefault(
|
| 36 |
+
"VBENCH_BERT_MODEL_DIR", str(_vbench_cache / "bert-base-uncased")
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
from vbench import VBench
|
| 40 |
+
|
| 41 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 42 |
+
if str(REPO_ROOT) not in sys.path:
|
| 43 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 44 |
+
|
| 45 |
+
from scripts.vbench8_protocol import (
|
| 46 |
+
DIMENSIONS,
|
| 47 |
+
PROTOCOL_NAME,
|
| 48 |
+
SUITE_COUNTS,
|
| 49 |
+
aggregate_selected_score,
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def sha256(path: Path) -> str:
|
| 54 |
+
digest = hashlib.sha256()
|
| 55 |
+
with path.open("rb") as handle:
|
| 56 |
+
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
| 57 |
+
digest.update(block)
|
| 58 |
+
return digest.hexdigest()
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def read_mapping(path: Path) -> list[dict[str, Any]]:
|
| 62 |
+
value = json.loads(path.read_text(encoding="utf-8"))
|
| 63 |
+
if not isinstance(value, list) or len(value) != 251:
|
| 64 |
+
raise ValueError(f"Expected a 251-row mapping list: {path}")
|
| 65 |
+
counts = {suite: 0 for suite in SUITE_COUNTS}
|
| 66 |
+
globals_seen: set[int] = set()
|
| 67 |
+
suite_seen: dict[str, set[int]] = {suite: set() for suite in SUITE_COUNTS}
|
| 68 |
+
for row in value:
|
| 69 |
+
suite = str(row["prompt_suite"])
|
| 70 |
+
global_index = int(row["global_index"])
|
| 71 |
+
suite_index = int(row["suite_index"])
|
| 72 |
+
if suite not in counts:
|
| 73 |
+
raise ValueError(f"Unknown suite {suite}")
|
| 74 |
+
if global_index in globals_seen:
|
| 75 |
+
raise ValueError(f"Duplicate global index {global_index}")
|
| 76 |
+
if suite_index in suite_seen[suite]:
|
| 77 |
+
raise ValueError(f"Duplicate suite index {suite}/{suite_index}")
|
| 78 |
+
globals_seen.add(global_index)
|
| 79 |
+
suite_seen[suite].add(suite_index)
|
| 80 |
+
counts[suite] += 1
|
| 81 |
+
if counts != SUITE_COUNTS:
|
| 82 |
+
raise ValueError(f"Unexpected suite counts: {counts}")
|
| 83 |
+
return sorted(value, key=lambda row: int(row["global_index"]))
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def build_metadata(
|
| 87 |
+
*,
|
| 88 |
+
mapping: list[dict[str, Any]],
|
| 89 |
+
videos_root: Path,
|
| 90 |
+
strategy: str,
|
| 91 |
+
metadata_path: Path,
|
| 92 |
+
) -> None:
|
| 93 |
+
records: list[dict[str, Any]] = []
|
| 94 |
+
for row in mapping:
|
| 95 |
+
suite = str(row["prompt_suite"])
|
| 96 |
+
suite_index = int(row["suite_index"])
|
| 97 |
+
video_path = (
|
| 98 |
+
videos_root / strategy / suite / f"{suite_index:03d}.mp4"
|
| 99 |
+
).resolve()
|
| 100 |
+
if not video_path.is_file():
|
| 101 |
+
raise FileNotFoundError(video_path)
|
| 102 |
+
official_dimensions = list(row["official_dimensions"])
|
| 103 |
+
if not set(official_dimensions).issubset(set(DIMENSIONS)):
|
| 104 |
+
raise ValueError(
|
| 105 |
+
f"Unsupported dimensions at global index {row['global_index']}: "
|
| 106 |
+
f"{official_dimensions}"
|
| 107 |
+
)
|
| 108 |
+
record: dict[str, Any] = {
|
| 109 |
+
"prompt_en": str(row["extended_prompt"]),
|
| 110 |
+
"dimension": official_dimensions,
|
| 111 |
+
"video_list": [str(video_path)],
|
| 112 |
+
}
|
| 113 |
+
# The standard scene implementation in vbench==0.1.5 needs the
|
| 114 |
+
# official scene keyword in auxiliary_info. It is not a replacement
|
| 115 |
+
# for prompt_en: overall_consistency still receives the extended text.
|
| 116 |
+
if "auxiliary_info" in row:
|
| 117 |
+
record["auxiliary_info"] = row["auxiliary_info"]
|
| 118 |
+
records.append(record)
|
| 119 |
+
if len(records) != 251:
|
| 120 |
+
raise ValueError(f"Expected 251 metadata records, got {len(records)}")
|
| 121 |
+
metadata_path.parent.mkdir(parents=True, exist_ok=True)
|
| 122 |
+
metadata_path.write_text(
|
| 123 |
+
json.dumps(records, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class FixedMetadataVBench(VBench):
|
| 128 |
+
"""Use prepared metadata while retaining VBench's official evaluator."""
|
| 129 |
+
|
| 130 |
+
def __init__(self, *, device: str, metadata_path: Path, output_path: Path):
|
| 131 |
+
super().__init__(
|
| 132 |
+
device=device,
|
| 133 |
+
full_info_dir=str(metadata_path),
|
| 134 |
+
output_path=str(output_path),
|
| 135 |
+
)
|
| 136 |
+
self.prepared_metadata_path = metadata_path
|
| 137 |
+
|
| 138 |
+
def build_full_info_json(self, *args: Any, **kwargs: Any) -> str:
|
| 139 |
+
return str(self.prepared_metadata_path)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def parse_result(value: Any) -> tuple[float, Any]:
|
| 143 |
+
if isinstance(value, (list, tuple)) and len(value) == 2:
|
| 144 |
+
score, details = value
|
| 145 |
+
else:
|
| 146 |
+
score, details = value, None
|
| 147 |
+
score = float(score)
|
| 148 |
+
if not 0.0 <= score <= 1.0:
|
| 149 |
+
raise ValueError(f"VBench raw score is outside [0, 1]: {score}")
|
| 150 |
+
return score, details
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def parse_args() -> argparse.Namespace:
|
| 154 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 155 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 156 |
+
parser.add_argument("--strategy", required=True)
|
| 157 |
+
parser.add_argument("--mapping", type=Path, required=True)
|
| 158 |
+
parser.add_argument("--videos-root", type=Path, required=True)
|
| 159 |
+
parser.add_argument("--output-root", type=Path, required=True)
|
| 160 |
+
parser.add_argument("--vbench-info", type=Path, required=True)
|
| 161 |
+
parser.add_argument("--skip-existing", action="store_true")
|
| 162 |
+
return parser.parse_args()
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def main() -> None:
|
| 166 |
+
args = parse_args()
|
| 167 |
+
mapping_path = args.mapping.resolve()
|
| 168 |
+
videos_root = args.videos_root.resolve()
|
| 169 |
+
output_root = args.output_root.resolve()
|
| 170 |
+
mapping = read_mapping(mapping_path)
|
| 171 |
+
metadata_path = output_root / "vbench/metadata" / f"{args.strategy}_full_info.json"
|
| 172 |
+
raw_dir = output_root / "vbench/raw_results" / args.strategy
|
| 173 |
+
raw_name = f"vbench8_{args.strategy}"
|
| 174 |
+
raw_result_path = raw_dir / f"{raw_name}_eval_results.json"
|
| 175 |
+
score_path = output_root / "vbench/scores" / f"{args.strategy}.json"
|
| 176 |
+
if args.skip_existing and raw_result_path.is_file() and score_path.is_file():
|
| 177 |
+
print(f"[cached] strategy={args.strategy}", flush=True)
|
| 178 |
+
return
|
| 179 |
+
|
| 180 |
+
build_metadata(
|
| 181 |
+
mapping=mapping,
|
| 182 |
+
videos_root=videos_root,
|
| 183 |
+
strategy=args.strategy,
|
| 184 |
+
metadata_path=metadata_path,
|
| 185 |
+
)
|
| 186 |
+
raw_dir.mkdir(parents=True, exist_ok=True)
|
| 187 |
+
print(
|
| 188 |
+
f"[vbench] gpu={args.gpu} strategy={args.strategy} videos=251 "
|
| 189 |
+
f"dimensions={','.join(DIMENSIONS)}",
|
| 190 |
+
flush=True,
|
| 191 |
+
)
|
| 192 |
+
bench = FixedMetadataVBench(
|
| 193 |
+
device="cuda", metadata_path=metadata_path, output_path=raw_dir
|
| 194 |
+
)
|
| 195 |
+
bench.evaluate(
|
| 196 |
+
videos_path=str(videos_root / args.strategy),
|
| 197 |
+
name=raw_name,
|
| 198 |
+
dimension_list=list(DIMENSIONS),
|
| 199 |
+
local=True,
|
| 200 |
+
mode="vbench_standard",
|
| 201 |
+
)
|
| 202 |
+
if not raw_result_path.is_file():
|
| 203 |
+
raise FileNotFoundError(raw_result_path)
|
| 204 |
+
raw_json = json.loads(raw_result_path.read_text(encoding="utf-8"))
|
| 205 |
+
raw_scores: dict[str, float] = {}
|
| 206 |
+
details: dict[str, Any] = {}
|
| 207 |
+
for dimension in DIMENSIONS:
|
| 208 |
+
if dimension not in raw_json:
|
| 209 |
+
raise KeyError(f"Missing {dimension} in {raw_result_path}")
|
| 210 |
+
score, dimension_details = parse_result(raw_json[dimension])
|
| 211 |
+
raw_scores[dimension] = score
|
| 212 |
+
if dimension_details is not None:
|
| 213 |
+
details[dimension] = dimension_details
|
| 214 |
+
aggregate = aggregate_selected_score(raw_scores)
|
| 215 |
+
result = {
|
| 216 |
+
"protocol": PROTOCOL_NAME,
|
| 217 |
+
"strategy": args.strategy,
|
| 218 |
+
"physical_gpu": str(args.gpu),
|
| 219 |
+
"benchmark": "VBench",
|
| 220 |
+
"vbench_version": "0.1.5",
|
| 221 |
+
"vbench_long": False,
|
| 222 |
+
"vbench_info": str(args.vbench_info.resolve()),
|
| 223 |
+
"vbench_info_sha256": sha256(args.vbench_info.resolve()),
|
| 224 |
+
"mapping": str(mapping_path),
|
| 225 |
+
"mapping_sha256": sha256(mapping_path),
|
| 226 |
+
"videos_root": str(videos_root / args.strategy),
|
| 227 |
+
"num_videos": 251,
|
| 228 |
+
"dimensions": list(DIMENSIONS),
|
| 229 |
+
"raw_scores": raw_scores,
|
| 230 |
+
**aggregate,
|
| 231 |
+
"scene_prompt_note": (
|
| 232 |
+
"prompt_en is the Self-Forcing extended prompt; vbench==0.1.5 "
|
| 233 |
+
"scene uses the official auxiliary scene keyword by design."
|
| 234 |
+
),
|
| 235 |
+
}
|
| 236 |
+
if details:
|
| 237 |
+
details_path = output_root / "vbench/details" / f"{args.strategy}.json"
|
| 238 |
+
details_path.parent.mkdir(parents=True, exist_ok=True)
|
| 239 |
+
details_path.write_text(
|
| 240 |
+
json.dumps(details, ensure_ascii=False, indent=2) + "\n",
|
| 241 |
+
encoding="utf-8",
|
| 242 |
+
)
|
| 243 |
+
result["details"] = str(details_path)
|
| 244 |
+
score_path.parent.mkdir(parents=True, exist_ok=True)
|
| 245 |
+
score_path.write_text(
|
| 246 |
+
json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
| 247 |
+
)
|
| 248 |
+
print(
|
| 249 |
+
f"[complete] strategy={args.strategy} "
|
| 250 |
+
f"selected={result['selected_vbench_percent']:.4f}%",
|
| 251 |
+
flush=True,
|
| 252 |
+
)
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
if __name__ == "__main__":
|
| 256 |
+
main()
|
scripts/evaluate_layer17_chunk_impact.py
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Measure local and downstream impact of isolated Layer-17 Predictor calls.
|
| 3 |
+
|
| 4 |
+
For every prompt, chunk 1..6 is independently evaluated with FPFF, FFPF, and
|
| 5 |
+
FPPF while every other chunk remains FFFF. A shadow Full forward is executed
|
| 6 |
+
at each selected Predictor step to measure hidden/flow/x0 error on the exact
|
| 7 |
+
rollout state. The shadow output is never used by the generated trajectory.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import csv
|
| 14 |
+
import json
|
| 15 |
+
import math
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
import time
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Any
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _preparse_gpu() -> str:
|
| 24 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 25 |
+
parser.add_argument("--gpu", default="4")
|
| 26 |
+
args, _ = parser.parse_known_args()
|
| 27 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 28 |
+
return str(args.gpu)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 32 |
+
|
| 33 |
+
import lpips
|
| 34 |
+
import torch
|
| 35 |
+
from omegaconf import OmegaConf
|
| 36 |
+
|
| 37 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 38 |
+
if str(REPO_ROOT) not in sys.path:
|
| 39 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 40 |
+
|
| 41 |
+
from scripts import evaluate_single_block_fppf as base
|
| 42 |
+
from utils.misc import set_seed
|
| 43 |
+
from utils.wan_wrapper import WanVAEWrapper
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
SCHEDULES = ("FPFF", "FFPF", "FPPF")
|
| 47 |
+
EPS = 1e-8
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def parse_args() -> argparse.Namespace:
|
| 51 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 52 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 53 |
+
parser.add_argument(
|
| 54 |
+
"--config_path", type=Path, default=Path("configs/self_forcing_sid.yaml")
|
| 55 |
+
)
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"--checkpoint_path",
|
| 58 |
+
type=Path,
|
| 59 |
+
default=Path("checkpoints/self_forcing_dmd.pt"),
|
| 60 |
+
)
|
| 61 |
+
parser.add_argument(
|
| 62 |
+
"--dataset_root",
|
| 63 |
+
type=Path,
|
| 64 |
+
default=Path("outputs/predictor_offline_100_all_blocks"),
|
| 65 |
+
)
|
| 66 |
+
parser.add_argument(
|
| 67 |
+
"--sweep_dir",
|
| 68 |
+
type=Path,
|
| 69 |
+
default=Path("outputs/single_block_init_sweep"),
|
| 70 |
+
)
|
| 71 |
+
parser.add_argument(
|
| 72 |
+
"--reference_root",
|
| 73 |
+
type=Path,
|
| 74 |
+
default=Path("outputs/single_block_fppf_eval"),
|
| 75 |
+
)
|
| 76 |
+
parser.add_argument(
|
| 77 |
+
"--output_dir",
|
| 78 |
+
type=Path,
|
| 79 |
+
default=Path("outputs/layer17_chunk_impact_pilot"),
|
| 80 |
+
)
|
| 81 |
+
parser.add_argument(
|
| 82 |
+
"--prompt_ids", type=int, nargs="*", default=list(range(80, 90))
|
| 83 |
+
)
|
| 84 |
+
parser.add_argument("--schedules", nargs="*", choices=SCHEDULES, default=list(SCHEDULES))
|
| 85 |
+
parser.add_argument(
|
| 86 |
+
"--chunks", type=int, nargs="*", default=list(range(1, base.NUM_CHUNKS))
|
| 87 |
+
)
|
| 88 |
+
parser.add_argument("--max_prompts", type=int, default=None)
|
| 89 |
+
parser.add_argument("--metric_batch_size", type=int, default=4)
|
| 90 |
+
parser.add_argument("--generation_seed", type=int, default=0)
|
| 91 |
+
parser.add_argument(
|
| 92 |
+
"--skip_lpips", action=argparse.BooleanOptionalAction, default=False
|
| 93 |
+
)
|
| 94 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 95 |
+
args = parser.parse_args()
|
| 96 |
+
if not args.prompt_ids:
|
| 97 |
+
parser.error("At least one prompt ID is required")
|
| 98 |
+
if any(value < 0 or value >= 100 for value in args.prompt_ids):
|
| 99 |
+
parser.error("Prompt IDs must be in [0, 99]")
|
| 100 |
+
if args.metric_batch_size < 1:
|
| 101 |
+
parser.error("--metric_batch_size must be positive")
|
| 102 |
+
if not args.schedules:
|
| 103 |
+
parser.error("At least one schedule is required")
|
| 104 |
+
if not args.chunks or any(chunk < 1 or chunk >= base.NUM_CHUNKS for chunk in args.chunks):
|
| 105 |
+
parser.error("--chunks must contain values in [1, 6]")
|
| 106 |
+
return args
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def resolve(path: Path) -> Path:
|
| 110 |
+
return path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve()
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def rms(value: torch.Tensor) -> torch.Tensor:
|
| 114 |
+
return value.float().square().mean().sqrt()
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def nrmse(prediction: torch.Tensor, target: torch.Tensor) -> float:
|
| 118 |
+
return float(rms(prediction.float() - target.float()) / rms(target).clamp_min(EPS))
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def chunk_frame_slice(chunk: int) -> slice:
|
| 122 |
+
if chunk == 0:
|
| 123 |
+
return slice(0, base.PIXEL_FRAMES_FIRST_CHUNK)
|
| 124 |
+
start = base.PIXEL_FRAMES_FIRST_CHUNK + 12 * (chunk - 1)
|
| 125 |
+
return slice(start, start + 12)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def summarize_frame_range(metrics: dict[str, Any], selected: slice) -> dict[str, float]:
|
| 129 |
+
mse_values = metrics["mse_per_frame"][selected]
|
| 130 |
+
ssim_values = metrics["ssim_per_frame"][selected]
|
| 131 |
+
lpips_values = metrics["lpips_per_frame"][selected]
|
| 132 |
+
mean_mse = sum(mse_values) / len(mse_values)
|
| 133 |
+
return {
|
| 134 |
+
"pixel_mse": mean_mse,
|
| 135 |
+
"psnr": -10.0 * math.log10(max(mean_mse, 1e-12)),
|
| 136 |
+
"ssim": sum(ssim_values) / len(ssim_values),
|
| 137 |
+
"lpips": (
|
| 138 |
+
sum(lpips_values) / len(lpips_values) if lpips_values else float("nan")
|
| 139 |
+
),
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
@torch.inference_mode()
|
| 144 |
+
def generate_intervention(
|
| 145 |
+
*,
|
| 146 |
+
pipeline: Any,
|
| 147 |
+
dataset_root: Path,
|
| 148 |
+
prompt_id: int,
|
| 149 |
+
generation_seed: int,
|
| 150 |
+
device: torch.device,
|
| 151 |
+
predictor: Any,
|
| 152 |
+
source_layer: int,
|
| 153 |
+
intervention_chunk: int,
|
| 154 |
+
intervention_schedule: str,
|
| 155 |
+
) -> tuple[torch.Tensor, dict[str, Any]]:
|
| 156 |
+
if intervention_schedule not in SCHEDULES:
|
| 157 |
+
raise ValueError(intervention_schedule)
|
| 158 |
+
if intervention_chunk < 1 or intervention_chunk >= base.NUM_CHUNKS:
|
| 159 |
+
raise ValueError("Predictor intervention chunk must be 1..6")
|
| 160 |
+
|
| 161 |
+
base.reset_kv_and_load_cross_cache(pipeline, dataset_root, prompt_id, device)
|
| 162 |
+
set_seed(generation_seed)
|
| 163 |
+
noise = torch.randn(
|
| 164 |
+
1,
|
| 165 |
+
base.NUM_CHUNKS * base.FRAMES_PER_CHUNK,
|
| 166 |
+
base.LATENT_CHANNELS,
|
| 167 |
+
base.LATENT_HEIGHT,
|
| 168 |
+
base.LATENT_WIDTH,
|
| 169 |
+
dtype=torch.bfloat16,
|
| 170 |
+
device=device,
|
| 171 |
+
)
|
| 172 |
+
timesteps = pipeline.denoising_step_list.to(device=device)
|
| 173 |
+
output_chunks: list[torch.Tensor] = []
|
| 174 |
+
previous_chunk_hidden: list[torch.Tensor | None] | None = None
|
| 175 |
+
teacher = pipeline.generator.model
|
| 176 |
+
capture = base.FinalHiddenCapture(teacher)
|
| 177 |
+
full_calls = 0
|
| 178 |
+
predictor_calls = 0
|
| 179 |
+
shadow_full_calls = 0
|
| 180 |
+
local_errors: list[dict[str, float | int]] = []
|
| 181 |
+
started = time.perf_counter()
|
| 182 |
+
|
| 183 |
+
try:
|
| 184 |
+
for chunk in range(base.NUM_CHUNKS):
|
| 185 |
+
noisy_input = noise[
|
| 186 |
+
:,
|
| 187 |
+
chunk * base.FRAMES_PER_CHUNK : (chunk + 1) * base.FRAMES_PER_CHUNK,
|
| 188 |
+
]
|
| 189 |
+
current_hidden: list[torch.Tensor | None] = [None] * base.NUM_DENOISING_STEPS
|
| 190 |
+
denoised_pred: torch.Tensor | None = None
|
| 191 |
+
timestep: torch.Tensor | None = None
|
| 192 |
+
|
| 193 |
+
for step, current_timestep in enumerate(timesteps):
|
| 194 |
+
timestep = torch.ones(
|
| 195 |
+
[1, base.FRAMES_PER_CHUNK], dtype=torch.int64, device=device
|
| 196 |
+
) * current_timestep
|
| 197 |
+
selected = (
|
| 198 |
+
chunk == intervention_chunk
|
| 199 |
+
and intervention_schedule[step] == "P"
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
if selected:
|
| 203 |
+
anchor_hidden = current_hidden[step - 1]
|
| 204 |
+
if anchor_hidden is None or previous_chunk_hidden is None:
|
| 205 |
+
raise RuntimeError("Predictor inputs are unavailable")
|
| 206 |
+
previous_hidden = previous_chunk_hidden[step]
|
| 207 |
+
if previous_hidden is None:
|
| 208 |
+
raise RuntimeError("Previous-chunk hidden is unavailable")
|
| 209 |
+
history = pipeline.kv_cache1[source_layer]
|
| 210 |
+
cross = pipeline.crossattn_cache[source_layer]
|
| 211 |
+
pred_hidden, pred_flow, _ = base.predictor_step(
|
| 212 |
+
predictor=predictor,
|
| 213 |
+
teacher=teacher,
|
| 214 |
+
noisy_input=noisy_input,
|
| 215 |
+
timestep=timestep,
|
| 216 |
+
anchor_hidden=anchor_hidden,
|
| 217 |
+
previous_hidden=previous_hidden,
|
| 218 |
+
history_cache=history,
|
| 219 |
+
cross_cache=cross,
|
| 220 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 221 |
+
)
|
| 222 |
+
pred_x0 = pipeline.generator._convert_flow_pred_to_x0(
|
| 223 |
+
flow_pred=pred_flow.flatten(0, 1),
|
| 224 |
+
xt=noisy_input.flatten(0, 1),
|
| 225 |
+
timestep=timestep.flatten(0, 1),
|
| 226 |
+
).unflatten(0, pred_flow.shape[:2])
|
| 227 |
+
|
| 228 |
+
# Shadow Full measures the exact counterfactual target on
|
| 229 |
+
# this rollout state. Its x0/hidden are never accepted.
|
| 230 |
+
capture.start()
|
| 231 |
+
full_flow, full_x0 = pipeline.generator(
|
| 232 |
+
noisy_image_or_video=noisy_input,
|
| 233 |
+
conditional_dict={
|
| 234 |
+
"prompt_embeds": torch.zeros(
|
| 235 |
+
1,
|
| 236 |
+
1,
|
| 237 |
+
int(teacher.text_embedding[0].in_features),
|
| 238 |
+
dtype=torch.bfloat16,
|
| 239 |
+
device=device,
|
| 240 |
+
)
|
| 241 |
+
},
|
| 242 |
+
timestep=timestep,
|
| 243 |
+
kv_cache=pipeline.kv_cache1,
|
| 244 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 245 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 246 |
+
)
|
| 247 |
+
full_hidden = capture.finish()
|
| 248 |
+
local_errors.append(
|
| 249 |
+
{
|
| 250 |
+
"step": step,
|
| 251 |
+
"timestep": float(current_timestep),
|
| 252 |
+
"hidden_nrmse": nrmse(pred_hidden, full_hidden),
|
| 253 |
+
"flow_nrmse": nrmse(pred_flow, full_flow),
|
| 254 |
+
"x0_nrmse": nrmse(pred_x0, full_x0),
|
| 255 |
+
}
|
| 256 |
+
)
|
| 257 |
+
current_hidden[step] = pred_hidden
|
| 258 |
+
denoised_pred = pred_x0
|
| 259 |
+
predictor_calls += 1
|
| 260 |
+
shadow_full_calls += 1
|
| 261 |
+
del full_flow, full_x0, full_hidden
|
| 262 |
+
else:
|
| 263 |
+
capture.start()
|
| 264 |
+
_, denoised_pred = pipeline.generator(
|
| 265 |
+
noisy_image_or_video=noisy_input,
|
| 266 |
+
conditional_dict={
|
| 267 |
+
"prompt_embeds": torch.zeros(
|
| 268 |
+
1,
|
| 269 |
+
1,
|
| 270 |
+
int(teacher.text_embedding[0].in_features),
|
| 271 |
+
dtype=torch.bfloat16,
|
| 272 |
+
device=device,
|
| 273 |
+
)
|
| 274 |
+
},
|
| 275 |
+
timestep=timestep,
|
| 276 |
+
kv_cache=pipeline.kv_cache1,
|
| 277 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 278 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 279 |
+
)
|
| 280 |
+
current_hidden[step] = capture.finish()
|
| 281 |
+
full_calls += 1
|
| 282 |
+
|
| 283 |
+
if step < base.NUM_DENOISING_STEPS - 1:
|
| 284 |
+
if denoised_pred is None:
|
| 285 |
+
raise RuntimeError("Denoising step produced no x0")
|
| 286 |
+
next_timestep = timesteps[step + 1]
|
| 287 |
+
flat = denoised_pred.flatten(0, 1)
|
| 288 |
+
noisy_input = pipeline.scheduler.add_noise(
|
| 289 |
+
flat,
|
| 290 |
+
torch.randn_like(flat),
|
| 291 |
+
next_timestep
|
| 292 |
+
* torch.ones(
|
| 293 |
+
[base.FRAMES_PER_CHUNK], dtype=torch.long, device=device
|
| 294 |
+
),
|
| 295 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 296 |
+
|
| 297 |
+
if denoised_pred is None or timestep is None:
|
| 298 |
+
raise RuntimeError("Chunk produced no clean latent")
|
| 299 |
+
output_chunks.append(denoised_pred)
|
| 300 |
+
context_timestep = torch.ones_like(timestep) * pipeline.args.context_noise
|
| 301 |
+
pipeline.generator(
|
| 302 |
+
noisy_image_or_video=denoised_pred,
|
| 303 |
+
conditional_dict={
|
| 304 |
+
"prompt_embeds": torch.zeros(
|
| 305 |
+
1,
|
| 306 |
+
1,
|
| 307 |
+
int(teacher.text_embedding[0].in_features),
|
| 308 |
+
dtype=torch.bfloat16,
|
| 309 |
+
device=device,
|
| 310 |
+
)
|
| 311 |
+
},
|
| 312 |
+
timestep=context_timestep,
|
| 313 |
+
kv_cache=pipeline.kv_cache1,
|
| 314 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 315 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 316 |
+
)
|
| 317 |
+
previous_chunk_hidden = current_hidden
|
| 318 |
+
finally:
|
| 319 |
+
capture.close()
|
| 320 |
+
|
| 321 |
+
torch.cuda.synchronize()
|
| 322 |
+
return torch.cat(output_chunks, dim=1), {
|
| 323 |
+
"generation_time_s": time.perf_counter() - started,
|
| 324 |
+
"full_calls": full_calls,
|
| 325 |
+
"predictor_calls": predictor_calls,
|
| 326 |
+
"shadow_full_calls": shadow_full_calls,
|
| 327 |
+
"local_errors": local_errors,
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def average(values: list[float]) -> float:
|
| 332 |
+
return sum(values) / len(values)
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
def finite_average(values: list[Any]) -> float:
|
| 336 |
+
numeric = [
|
| 337 |
+
float(value)
|
| 338 |
+
for value in values
|
| 339 |
+
if value is not None and math.isfinite(float(value))
|
| 340 |
+
]
|
| 341 |
+
return average(numeric) if numeric else float("nan")
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def rankdata(values: list[float]) -> list[float]:
|
| 345 |
+
order = sorted(range(len(values)), key=values.__getitem__)
|
| 346 |
+
ranks = [0.0] * len(values)
|
| 347 |
+
start = 0
|
| 348 |
+
while start < len(order):
|
| 349 |
+
end = start + 1
|
| 350 |
+
while end < len(order) and values[order[end]] == values[order[start]]:
|
| 351 |
+
end += 1
|
| 352 |
+
rank = 0.5 * (start + end - 1)
|
| 353 |
+
for position in range(start, end):
|
| 354 |
+
ranks[order[position]] = rank
|
| 355 |
+
start = end
|
| 356 |
+
return ranks
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
def pearson(left: list[float], right: list[float]) -> float:
|
| 360 |
+
left_mean, right_mean = average(left), average(right)
|
| 361 |
+
left_centered = [value - left_mean for value in left]
|
| 362 |
+
right_centered = [value - right_mean for value in right]
|
| 363 |
+
numerator = sum(a * b for a, b in zip(left_centered, right_centered))
|
| 364 |
+
denominator = math.sqrt(
|
| 365 |
+
sum(value * value for value in left_centered)
|
| 366 |
+
* sum(value * value for value in right_centered)
|
| 367 |
+
)
|
| 368 |
+
return numerator / denominator if denominator > 0 else float("nan")
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def spearman(left: list[float], right: list[float]) -> float:
|
| 372 |
+
return pearson(rankdata(left), rankdata(right))
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def write_csv(path: Path, rows: list[dict[str, Any]], fields: list[str]) -> None:
|
| 376 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 377 |
+
with temporary.open("w", encoding="utf-8", newline="") as handle:
|
| 378 |
+
writer = csv.DictWriter(handle, fieldnames=fields)
|
| 379 |
+
writer.writeheader()
|
| 380 |
+
writer.writerows(rows)
|
| 381 |
+
os.replace(temporary, path)
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def group_centered_values(
|
| 385 |
+
records: list[dict[str, Any]], field: str
|
| 386 |
+
) -> list[float]:
|
| 387 |
+
"""Remove schedule-by-chunk means to isolate prompt/state variation."""
|
| 388 |
+
groups: dict[tuple[str, int], list[float]] = {}
|
| 389 |
+
for row in records:
|
| 390 |
+
key = (str(row["schedule"]), int(row["chunk"]))
|
| 391 |
+
groups.setdefault(key, []).append(float(row[field]))
|
| 392 |
+
means = {key: average(values) for key, values in groups.items()}
|
| 393 |
+
return [
|
| 394 |
+
float(row[field]) - means[(str(row["schedule"]), int(row["chunk"]))]
|
| 395 |
+
for row in records
|
| 396 |
+
]
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def aggregate(records: list[dict[str, Any]], output_dir: Path) -> None:
|
| 400 |
+
numeric_fields = [
|
| 401 |
+
"hidden_nrmse_mean",
|
| 402 |
+
"flow_nrmse_mean",
|
| 403 |
+
"x0_nrmse_mean",
|
| 404 |
+
"latent_all_nrmse",
|
| 405 |
+
"latent_current_nrmse",
|
| 406 |
+
"latent_tail_nrmse",
|
| 407 |
+
"psnr",
|
| 408 |
+
"ssim",
|
| 409 |
+
"lpips",
|
| 410 |
+
"current_psnr",
|
| 411 |
+
"current_ssim",
|
| 412 |
+
"current_lpips",
|
| 413 |
+
"tail_psnr",
|
| 414 |
+
"tail_ssim",
|
| 415 |
+
"tail_lpips",
|
| 416 |
+
"generation_time_s",
|
| 417 |
+
]
|
| 418 |
+
summary_rows: list[dict[str, Any]] = []
|
| 419 |
+
available_schedules = [
|
| 420 |
+
schedule for schedule in SCHEDULES if any(row["schedule"] == schedule for row in records)
|
| 421 |
+
]
|
| 422 |
+
available_chunks = sorted({int(row["chunk"]) for row in records})
|
| 423 |
+
for schedule in available_schedules:
|
| 424 |
+
for chunk in available_chunks:
|
| 425 |
+
selected = [
|
| 426 |
+
row
|
| 427 |
+
for row in records
|
| 428 |
+
if row["schedule"] == schedule and row["chunk"] == chunk
|
| 429 |
+
]
|
| 430 |
+
row: dict[str, Any] = {
|
| 431 |
+
"schedule": schedule,
|
| 432 |
+
"chunk": chunk,
|
| 433 |
+
"num_prompts": len(selected),
|
| 434 |
+
"alpha_eligible_linear": (base.NUM_CHUNKS - 1 - chunk)
|
| 435 |
+
/ (base.NUM_CHUNKS - 2),
|
| 436 |
+
}
|
| 437 |
+
for field in numeric_fields:
|
| 438 |
+
row[field] = finite_average([item[field] for item in selected])
|
| 439 |
+
summary_rows.append(row)
|
| 440 |
+
|
| 441 |
+
summary_fields = [
|
| 442 |
+
"schedule",
|
| 443 |
+
"chunk",
|
| 444 |
+
"num_prompts",
|
| 445 |
+
"alpha_eligible_linear",
|
| 446 |
+
*numeric_fields,
|
| 447 |
+
]
|
| 448 |
+
write_csv(output_dir / "summary_by_schedule_chunk.csv", summary_rows, summary_fields)
|
| 449 |
+
|
| 450 |
+
correlation_rows: list[dict[str, Any]] = []
|
| 451 |
+
for schedule in (*available_schedules, "ALL"):
|
| 452 |
+
selected = (
|
| 453 |
+
records if schedule == "ALL" else [r for r in records if r["schedule"] == schedule]
|
| 454 |
+
)
|
| 455 |
+
for local in ("hidden_nrmse_mean", "flow_nrmse_mean", "x0_nrmse_mean"):
|
| 456 |
+
for downstream in ("tail_lpips", "latent_tail_nrmse", "tail_pixel_mse"):
|
| 457 |
+
pairs = [
|
| 458 |
+
(float(row[local]), float(row[downstream]))
|
| 459 |
+
for row in selected
|
| 460 |
+
if row[local] is not None
|
| 461 |
+
and row[downstream] is not None
|
| 462 |
+
and math.isfinite(float(row[local]))
|
| 463 |
+
and math.isfinite(float(row[downstream]))
|
| 464 |
+
]
|
| 465 |
+
correlation_rows.append(
|
| 466 |
+
{
|
| 467 |
+
"schedule": schedule,
|
| 468 |
+
"local_metric": local,
|
| 469 |
+
"downstream_metric": downstream,
|
| 470 |
+
"num_observations": len(pairs),
|
| 471 |
+
"spearman": (
|
| 472 |
+
spearman(
|
| 473 |
+
[pair[0] for pair in pairs],
|
| 474 |
+
[pair[1] for pair in pairs],
|
| 475 |
+
)
|
| 476 |
+
if len(pairs) >= 2
|
| 477 |
+
else float("nan")
|
| 478 |
+
),
|
| 479 |
+
}
|
| 480 |
+
)
|
| 481 |
+
correlation_fields = [
|
| 482 |
+
"schedule",
|
| 483 |
+
"local_metric",
|
| 484 |
+
"downstream_metric",
|
| 485 |
+
"num_observations",
|
| 486 |
+
"spearman",
|
| 487 |
+
]
|
| 488 |
+
write_csv(output_dir / "local_downstream_correlations.csv", correlation_rows, correlation_fields)
|
| 489 |
+
|
| 490 |
+
controlled_rows: list[dict[str, Any]] = []
|
| 491 |
+
for local in ("hidden_nrmse_mean", "flow_nrmse_mean", "x0_nrmse_mean"):
|
| 492 |
+
local_residual = group_centered_values(records, local)
|
| 493 |
+
downstream_residual = group_centered_values(records, "tail_lpips")
|
| 494 |
+
within_cell = []
|
| 495 |
+
for schedule in available_schedules:
|
| 496 |
+
for chunk in available_chunks:
|
| 497 |
+
selected = [
|
| 498 |
+
row
|
| 499 |
+
for row in records
|
| 500 |
+
if row["schedule"] == schedule and row["chunk"] == chunk
|
| 501 |
+
]
|
| 502 |
+
if len(selected) >= 2:
|
| 503 |
+
within_cell.append(
|
| 504 |
+
spearman(
|
| 505 |
+
[float(row[local]) for row in selected],
|
| 506 |
+
[float(row["tail_lpips"]) for row in selected],
|
| 507 |
+
)
|
| 508 |
+
)
|
| 509 |
+
controlled_rows.append(
|
| 510 |
+
{
|
| 511 |
+
"local_metric": local,
|
| 512 |
+
"downstream_metric": "tail_lpips",
|
| 513 |
+
"controls": "schedule+chunk",
|
| 514 |
+
"residual_spearman": spearman(local_residual, downstream_residual),
|
| 515 |
+
"mean_within_cell_spearman": finite_average(within_cell),
|
| 516 |
+
"positive_cells": sum(value > 0 for value in within_cell),
|
| 517 |
+
"num_cells": len(within_cell),
|
| 518 |
+
}
|
| 519 |
+
)
|
| 520 |
+
controlled_fields = [
|
| 521 |
+
"local_metric",
|
| 522 |
+
"downstream_metric",
|
| 523 |
+
"controls",
|
| 524 |
+
"residual_spearman",
|
| 525 |
+
"mean_within_cell_spearman",
|
| 526 |
+
"positive_cells",
|
| 527 |
+
"num_cells",
|
| 528 |
+
]
|
| 529 |
+
write_csv(
|
| 530 |
+
output_dir / "controlled_local_downstream_correlations.csv",
|
| 531 |
+
controlled_rows,
|
| 532 |
+
controlled_fields,
|
| 533 |
+
)
|
| 534 |
+
|
| 535 |
+
report = [
|
| 536 |
+
"# Layer-17 Predictor chunk-impact pilot",
|
| 537 |
+
"",
|
| 538 |
+
f"Prompts: {len(set(row['prompt_id'] for row in records))}; seed 0; "
|
| 539 |
+
"all non-intervened chunks use FFFF.",
|
| 540 |
+
"",
|
| 541 |
+
"A shadow Full call measures local error at each Predictor decision, but the "
|
| 542 |
+
"generated trajectory always consumes the Predictor output.",
|
| 543 |
+
"",
|
| 544 |
+
]
|
| 545 |
+
for schedule in available_schedules:
|
| 546 |
+
report.extend(
|
| 547 |
+
[
|
| 548 |
+
f"## {schedule}",
|
| 549 |
+
"",
|
| 550 |
+
"| Chunk | x0 nRMSE | Tail PSNR | Tail LPIPS | Tail latent nRMSE |",
|
| 551 |
+
"|---:|---:|---:|---:|---:|",
|
| 552 |
+
]
|
| 553 |
+
)
|
| 554 |
+
for row in summary_rows:
|
| 555 |
+
if row["schedule"] != schedule:
|
| 556 |
+
continue
|
| 557 |
+
report.append(
|
| 558 |
+
f"| {row['chunk']} | {row['x0_nrmse_mean']:.6f} | "
|
| 559 |
+
f"{row['tail_psnr']:.4f} | {row['tail_lpips']:.6f} | "
|
| 560 |
+
f"{row['latent_tail_nrmse']:.6f} |"
|
| 561 |
+
)
|
| 562 |
+
report.append("")
|
| 563 |
+
report.extend(
|
| 564 |
+
[
|
| 565 |
+
"## Local-to-downstream Spearman correlations",
|
| 566 |
+
"",
|
| 567 |
+
"| Schedule | Local metric | Downstream metric | Spearman |",
|
| 568 |
+
"|---|---|---|---:|",
|
| 569 |
+
]
|
| 570 |
+
)
|
| 571 |
+
for row in correlation_rows:
|
| 572 |
+
if row["downstream_metric"] == "tail_lpips":
|
| 573 |
+
report.append(
|
| 574 |
+
f"| {row['schedule']} | {row['local_metric']} | tail LPIPS | "
|
| 575 |
+
f"{row['spearman']:.4f} |"
|
| 576 |
+
)
|
| 577 |
+
report.extend(
|
| 578 |
+
[
|
| 579 |
+
"",
|
| 580 |
+
"## Correlations after controlling schedule and chunk",
|
| 581 |
+
"",
|
| 582 |
+
"Residual correlations remove each schedule-by-chunk mean, so they test "
|
| 583 |
+
"whether local error explains prompt/state risk beyond the position prior.",
|
| 584 |
+
"",
|
| 585 |
+
"| Local metric | Residual Spearman | Mean within-cell Spearman | Positive cells |",
|
| 586 |
+
"|---|---:|---:|---:|",
|
| 587 |
+
]
|
| 588 |
+
)
|
| 589 |
+
for row in controlled_rows:
|
| 590 |
+
report.append(
|
| 591 |
+
f"| {row['local_metric']} | {row['residual_spearman']:.4f} | "
|
| 592 |
+
f"{row['mean_within_cell_spearman']:.4f} | "
|
| 593 |
+
f"{row['positive_cells']}/{row['num_cells']} |"
|
| 594 |
+
)
|
| 595 |
+
(output_dir / "REPORT.md").write_text("\n".join(report) + "\n", encoding="utf-8")
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
def main() -> None:
|
| 599 |
+
args = parse_args()
|
| 600 |
+
for name in (
|
| 601 |
+
"config_path",
|
| 602 |
+
"checkpoint_path",
|
| 603 |
+
"dataset_root",
|
| 604 |
+
"sweep_dir",
|
| 605 |
+
"reference_root",
|
| 606 |
+
"output_dir",
|
| 607 |
+
):
|
| 608 |
+
setattr(args, name, resolve(getattr(args, name)))
|
| 609 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 610 |
+
prompt_ids = sorted(set(args.prompt_ids))
|
| 611 |
+
if args.max_prompts is not None:
|
| 612 |
+
prompt_ids = prompt_ids[: args.max_prompts]
|
| 613 |
+
|
| 614 |
+
config = OmegaConf.merge(
|
| 615 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 616 |
+
OmegaConf.load(args.config_path),
|
| 617 |
+
)
|
| 618 |
+
device = torch.device("cuda")
|
| 619 |
+
torch.set_grad_enabled(False)
|
| 620 |
+
set_seed(args.generation_seed)
|
| 621 |
+
manifest = {
|
| 622 |
+
"status": "running",
|
| 623 |
+
"gpu": str(args.gpu),
|
| 624 |
+
"prompt_ids": prompt_ids,
|
| 625 |
+
"generation_seed": args.generation_seed,
|
| 626 |
+
"checkpoint_path": str(args.checkpoint_path),
|
| 627 |
+
"predictor_weights": str(
|
| 628 |
+
args.sweep_dir / "teacher_layer_17" / "predictor_final.safetensors"
|
| 629 |
+
),
|
| 630 |
+
"dataset_root": str(args.dataset_root),
|
| 631 |
+
"schedules": list(args.schedules),
|
| 632 |
+
"chunks": list(args.chunks),
|
| 633 |
+
"shadow_full_target": True,
|
| 634 |
+
}
|
| 635 |
+
base.atomic_json(args.output_dir / "manifest.json", manifest)
|
| 636 |
+
|
| 637 |
+
print("[setup] loading VAE", flush=True)
|
| 638 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 639 |
+
missing_references = [
|
| 640 |
+
prompt_id
|
| 641 |
+
for prompt_id in prompt_ids
|
| 642 |
+
if not (
|
| 643 |
+
args.reference_root
|
| 644 |
+
/ "ffff_reference_frames"
|
| 645 |
+
/ f"prompt_{prompt_id:04d}.safetensors"
|
| 646 |
+
).exists()
|
| 647 |
+
]
|
| 648 |
+
if missing_references:
|
| 649 |
+
base.prepare_reference_frames(
|
| 650 |
+
vae=vae,
|
| 651 |
+
dataset_root=args.dataset_root,
|
| 652 |
+
output_dir=args.reference_root,
|
| 653 |
+
prompt_ids=missing_references,
|
| 654 |
+
device=device,
|
| 655 |
+
rebuild=False,
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
+
print("[setup] loading frozen generator and Layer-17 Predictor", flush=True)
|
| 659 |
+
pipeline = base.build_pipeline(config, args.checkpoint_path, vae, device)
|
| 660 |
+
experiment = base.discover_experiments(
|
| 661 |
+
args.sweep_dir, ["teacher_layer_17"], None
|
| 662 |
+
)[0]
|
| 663 |
+
predictor = base.load_predictor(pipeline.generator.model, experiment, device)
|
| 664 |
+
lpips_model = None
|
| 665 |
+
if not args.skip_lpips:
|
| 666 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 667 |
+
lpips_model.requires_grad_(False)
|
| 668 |
+
|
| 669 |
+
records: list[dict[str, Any]] = []
|
| 670 |
+
total = len(prompt_ids) * len(args.schedules) * len(args.chunks)
|
| 671 |
+
completed = 0
|
| 672 |
+
for prompt_id in prompt_ids:
|
| 673 |
+
reference_latent = base.load_ffff_latent(args.dataset_root, prompt_id).to(
|
| 674 |
+
device=device, dtype=torch.bfloat16
|
| 675 |
+
)
|
| 676 |
+
reference_u8 = base.load_reference_frames(args.reference_root, prompt_id)
|
| 677 |
+
for schedule in args.schedules:
|
| 678 |
+
for chunk in args.chunks:
|
| 679 |
+
destination = (
|
| 680 |
+
args.output_dir
|
| 681 |
+
/ "per_intervention"
|
| 682 |
+
/ f"prompt_{prompt_id:04d}_{schedule}_chunk_{chunk:02d}.json"
|
| 683 |
+
)
|
| 684 |
+
if destination.exists() and not args.overwrite:
|
| 685 |
+
record = json.loads(destination.read_text(encoding="utf-8"))
|
| 686 |
+
records.append(record)
|
| 687 |
+
completed += 1
|
| 688 |
+
print(f"[cached] {completed}/{total} {destination.stem}", flush=True)
|
| 689 |
+
continue
|
| 690 |
+
started = time.perf_counter()
|
| 691 |
+
latent, diagnostics = generate_intervention(
|
| 692 |
+
pipeline=pipeline,
|
| 693 |
+
dataset_root=args.dataset_root,
|
| 694 |
+
prompt_id=prompt_id,
|
| 695 |
+
generation_seed=args.generation_seed,
|
| 696 |
+
device=device,
|
| 697 |
+
predictor=predictor,
|
| 698 |
+
source_layer=17,
|
| 699 |
+
intervention_chunk=chunk,
|
| 700 |
+
intervention_schedule=schedule,
|
| 701 |
+
)
|
| 702 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 703 |
+
pixels = vae.decode_to_pixel(latent, use_cache=False)
|
| 704 |
+
prediction_u8 = base.pixels_to_u8(pixels)
|
| 705 |
+
frame = base.frame_metrics(
|
| 706 |
+
reference_u8=reference_u8,
|
| 707 |
+
prediction_u8=prediction_u8,
|
| 708 |
+
lpips_model=lpips_model,
|
| 709 |
+
batch_size=args.metric_batch_size,
|
| 710 |
+
device=device,
|
| 711 |
+
)
|
| 712 |
+
current_slice = chunk_frame_slice(chunk)
|
| 713 |
+
current = summarize_frame_range(frame, current_slice)
|
| 714 |
+
tail = summarize_frame_range(frame, slice(current_slice.start, None))
|
| 715 |
+
latent_chunk_start = chunk * base.FRAMES_PER_CHUNK
|
| 716 |
+
latent_chunk_end = latent_chunk_start + base.FRAMES_PER_CHUNK
|
| 717 |
+
errors = diagnostics.pop("local_errors")
|
| 718 |
+
record = {
|
| 719 |
+
"prompt_id": prompt_id,
|
| 720 |
+
"schedule": schedule,
|
| 721 |
+
"chunk": chunk,
|
| 722 |
+
"predictor_steps": [int(item["step"]) for item in errors],
|
| 723 |
+
"hidden_nrmse_mean": average(
|
| 724 |
+
[float(item["hidden_nrmse"]) for item in errors]
|
| 725 |
+
),
|
| 726 |
+
"flow_nrmse_mean": average(
|
| 727 |
+
[float(item["flow_nrmse"]) for item in errors]
|
| 728 |
+
),
|
| 729 |
+
"x0_nrmse_mean": average(
|
| 730 |
+
[float(item["x0_nrmse"]) for item in errors]
|
| 731 |
+
),
|
| 732 |
+
"local_errors": errors,
|
| 733 |
+
"latent_all_nrmse": nrmse(latent, reference_latent),
|
| 734 |
+
"latent_current_nrmse": nrmse(
|
| 735 |
+
latent[:, latent_chunk_start:latent_chunk_end],
|
| 736 |
+
reference_latent[:, latent_chunk_start:latent_chunk_end],
|
| 737 |
+
),
|
| 738 |
+
"latent_tail_nrmse": nrmse(
|
| 739 |
+
latent[:, latent_chunk_start:],
|
| 740 |
+
reference_latent[:, latent_chunk_start:],
|
| 741 |
+
),
|
| 742 |
+
"psnr": frame["psnr"],
|
| 743 |
+
"ssim": frame["ssim"],
|
| 744 |
+
"lpips": frame["lpips"],
|
| 745 |
+
"current_pixel_mse": current["pixel_mse"],
|
| 746 |
+
"current_psnr": current["psnr"],
|
| 747 |
+
"current_ssim": current["ssim"],
|
| 748 |
+
"current_lpips": current["lpips"],
|
| 749 |
+
"tail_pixel_mse": tail["pixel_mse"],
|
| 750 |
+
"tail_psnr": tail["psnr"],
|
| 751 |
+
"tail_ssim": tail["ssim"],
|
| 752 |
+
"tail_lpips": tail["lpips"],
|
| 753 |
+
**diagnostics,
|
| 754 |
+
"total_time_s": time.perf_counter() - started,
|
| 755 |
+
}
|
| 756 |
+
base.atomic_json(destination, record)
|
| 757 |
+
records.append(record)
|
| 758 |
+
completed += 1
|
| 759 |
+
print(
|
| 760 |
+
f"[run] {completed}/{total} p={prompt_id} {schedule} c={chunk} "
|
| 761 |
+
f"x0={record['x0_nrmse_mean']:.5f} "
|
| 762 |
+
f"tail_lpips={record['tail_lpips']:.5f} "
|
| 763 |
+
f"time={record['total_time_s']:.1f}s",
|
| 764 |
+
flush=True,
|
| 765 |
+
)
|
| 766 |
+
if hasattr(vae.model, "clear_cache"):
|
| 767 |
+
vae.model.clear_cache()
|
| 768 |
+
del latent, pixels, prediction_u8, frame
|
| 769 |
+
torch.cuda.empty_cache()
|
| 770 |
+
del reference_latent, reference_u8
|
| 771 |
+
|
| 772 |
+
fields = sorted({key for record in records for key in record if key != "local_errors"})
|
| 773 |
+
flattened = [{key: row.get(key) for key in fields} for row in records]
|
| 774 |
+
write_csv(args.output_dir / "interventions.csv", flattened, fields)
|
| 775 |
+
aggregate(records, args.output_dir)
|
| 776 |
+
manifest["status"] = "complete"
|
| 777 |
+
base.atomic_json(args.output_dir / "manifest.json", manifest)
|
| 778 |
+
print(f"[complete] results -> {args.output_dir}", flush=True)
|
| 779 |
+
|
| 780 |
+
|
| 781 |
+
if __name__ == "__main__":
|
| 782 |
+
main()
|
scripts/evaluate_layer17_dynamic_gate.py
ADDED
|
@@ -0,0 +1,904 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Evaluate chunk-aware dynamic gating for the frozen Layer-17 Predictor."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
import json
|
| 9 |
+
import math
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
import time
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import Any
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _preparse_gpu() -> str:
|
| 18 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 19 |
+
parser.add_argument("--gpu", default="4")
|
| 20 |
+
args, _ = parser.parse_known_args()
|
| 21 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 22 |
+
return str(args.gpu)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 26 |
+
|
| 27 |
+
import lpips
|
| 28 |
+
import torch
|
| 29 |
+
from omegaconf import OmegaConf
|
| 30 |
+
from safetensors.torch import load_file
|
| 31 |
+
|
| 32 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 33 |
+
if str(REPO_ROOT) not in sys.path:
|
| 34 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 35 |
+
|
| 36 |
+
from predictor_training.confidence import PredictorConfidenceHead
|
| 37 |
+
from scripts import evaluate_layer17_chunk_impact as impact_eval
|
| 38 |
+
from scripts import evaluate_single_block_fppf as base
|
| 39 |
+
from scripts.run_single_block_init_sweep import hidden_to_flow
|
| 40 |
+
from utils.misc import set_seed
|
| 41 |
+
from utils.wan_wrapper import WanVAEWrapper
|
| 42 |
+
from wan.modules.model import sinusoidal_embedding_1d
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
BETAS = (0.0, 1.0, 1.5, 2.0)
|
| 46 |
+
TARGET_ACCEPTS = (4, 6, 8, 10)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def parse_args() -> argparse.Namespace:
|
| 50 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 51 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 52 |
+
parser.add_argument("--mode", choices=("smoke", "validation", "test"), required=True)
|
| 53 |
+
parser.add_argument(
|
| 54 |
+
"--config_path", type=Path, default=Path("configs/self_forcing_sid.yaml")
|
| 55 |
+
)
|
| 56 |
+
parser.add_argument(
|
| 57 |
+
"--checkpoint_path", type=Path, default=Path("checkpoints/self_forcing_dmd.pt")
|
| 58 |
+
)
|
| 59 |
+
parser.add_argument(
|
| 60 |
+
"--dataset_root", type=Path,
|
| 61 |
+
default=Path("outputs/predictor_offline_100_all_blocks"),
|
| 62 |
+
)
|
| 63 |
+
parser.add_argument(
|
| 64 |
+
"--sweep_dir", type=Path, default=Path("outputs/single_block_init_sweep")
|
| 65 |
+
)
|
| 66 |
+
parser.add_argument(
|
| 67 |
+
"--predictor_weights",
|
| 68 |
+
type=Path,
|
| 69 |
+
default=None,
|
| 70 |
+
help=(
|
| 71 |
+
"Optional direct Layer-17 Predictor weights. When supplied, this "
|
| 72 |
+
"takes precedence over teacher_layer_17 in --sweep_dir."
|
| 73 |
+
),
|
| 74 |
+
)
|
| 75 |
+
parser.add_argument(
|
| 76 |
+
"--confidence_weights", type=Path,
|
| 77 |
+
default=Path(
|
| 78 |
+
"outputs/layer17_confidence_teacher_forced_20260830/"
|
| 79 |
+
"confidence_best.safetensors"
|
| 80 |
+
),
|
| 81 |
+
)
|
| 82 |
+
parser.add_argument(
|
| 83 |
+
"--validation_predictions", type=Path,
|
| 84 |
+
default=Path(
|
| 85 |
+
"outputs/layer17_confidence_teacher_forced_20260830/"
|
| 86 |
+
"validation_predictions.csv"
|
| 87 |
+
),
|
| 88 |
+
)
|
| 89 |
+
parser.add_argument(
|
| 90 |
+
"--reference_root", type=Path, default=Path("outputs/single_block_fppf_eval")
|
| 91 |
+
)
|
| 92 |
+
parser.add_argument(
|
| 93 |
+
"--output_root", type=Path,
|
| 94 |
+
default=Path("outputs/layer17_dynamic_gate_20260830"),
|
| 95 |
+
)
|
| 96 |
+
parser.add_argument("--generation_seed", type=int, default=0)
|
| 97 |
+
parser.add_argument("--metric_batch_size", type=int, default=4)
|
| 98 |
+
parser.add_argument(
|
| 99 |
+
"--candidate_steps", type=int, nargs="+", choices=(1, 2, 3),
|
| 100 |
+
default=[1, 2],
|
| 101 |
+
)
|
| 102 |
+
parser.add_argument("--target_accepts", type=int, nargs="*", default=None)
|
| 103 |
+
parser.add_argument("--selected_path", type=Path, default=None)
|
| 104 |
+
parser.add_argument(
|
| 105 |
+
"--config_names", nargs="*", default=None,
|
| 106 |
+
help="Optional exact configuration names to run in validation/test mode.",
|
| 107 |
+
)
|
| 108 |
+
parser.add_argument(
|
| 109 |
+
"--prompt_ids", type=int, nargs="*", default=None,
|
| 110 |
+
help="Optional prompt shard; shard-level CSV/manifest files get a GPU suffix.",
|
| 111 |
+
)
|
| 112 |
+
parser.add_argument("--save_videos", action="store_true")
|
| 113 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 114 |
+
parser.add_argument(
|
| 115 |
+
"--skip_lpips", action=argparse.BooleanOptionalAction, default=False
|
| 116 |
+
)
|
| 117 |
+
args = parser.parse_args()
|
| 118 |
+
for name in (
|
| 119 |
+
"config_path", "checkpoint_path", "dataset_root", "sweep_dir",
|
| 120 |
+
"predictor_weights",
|
| 121 |
+
"confidence_weights", "validation_predictions", "reference_root",
|
| 122 |
+
"output_root", "selected_path",
|
| 123 |
+
):
|
| 124 |
+
value = getattr(args, name)
|
| 125 |
+
if value is None:
|
| 126 |
+
continue
|
| 127 |
+
path = value.expanduser()
|
| 128 |
+
setattr(args, name, path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve())
|
| 129 |
+
args.candidate_steps = sorted(set(args.candidate_steps))
|
| 130 |
+
max_accepts = 6 * len(args.candidate_steps)
|
| 131 |
+
if args.target_accepts is None:
|
| 132 |
+
args.target_accepts = (
|
| 133 |
+
[4, 6, 8, 10] if len(args.candidate_steps) == 2
|
| 134 |
+
else [6, 9, 12, 15]
|
| 135 |
+
)
|
| 136 |
+
if any(value < 1 or value >= max_accepts for value in args.target_accepts):
|
| 137 |
+
parser.error(f"target accepts must be in [1, {max_accepts - 1}]")
|
| 138 |
+
return args
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def atomic_json(path: Path, value: Any) -> None:
|
| 142 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 143 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 144 |
+
temporary.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8")
|
| 145 |
+
os.replace(temporary, path)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def write_csv(path: Path, rows: list[dict[str, Any]], fields: list[str]) -> None:
|
| 149 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 150 |
+
with temporary.open("w", encoding="utf-8", newline="") as handle:
|
| 151 |
+
writer = csv.DictWriter(handle, fieldnames=fields)
|
| 152 |
+
writer.writeheader()
|
| 153 |
+
writer.writerows(rows)
|
| 154 |
+
os.replace(temporary, path)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def quantile(values: list[float], fraction: float) -> float:
|
| 158 |
+
ordered = sorted(values)
|
| 159 |
+
position = fraction * (len(ordered) - 1)
|
| 160 |
+
lower = int(math.floor(position))
|
| 161 |
+
upper = int(math.ceil(position))
|
| 162 |
+
if lower == upper:
|
| 163 |
+
return ordered[lower]
|
| 164 |
+
weight = position - lower
|
| 165 |
+
return ordered[lower] * (1.0 - weight) + ordered[upper] * weight
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def threshold_grid(
|
| 169 |
+
predictions_path: Path,
|
| 170 |
+
candidate_steps: list[int],
|
| 171 |
+
targets: list[int],
|
| 172 |
+
) -> dict[tuple[float, int], float]:
|
| 173 |
+
rows = list(csv.DictReader(predictions_path.open(encoding="utf-8")))
|
| 174 |
+
expected = 10 * 6 * len(candidate_steps)
|
| 175 |
+
if len(rows) != expected:
|
| 176 |
+
raise ValueError(f"Expected {expected} validation predictions, got {len(rows)}")
|
| 177 |
+
thresholds = {}
|
| 178 |
+
for beta in BETAS:
|
| 179 |
+
risks = []
|
| 180 |
+
for row in rows:
|
| 181 |
+
chunk = int(row["chunk"])
|
| 182 |
+
alpha = (base.NUM_CHUNKS - 1 - chunk) / (base.NUM_CHUNKS - 2)
|
| 183 |
+
local_error = float(row["predicted_hidden_nrmse"])
|
| 184 |
+
risks.append(local_error * (1.0 + beta * alpha))
|
| 185 |
+
max_accepts = 6 * len(candidate_steps)
|
| 186 |
+
for target in targets:
|
| 187 |
+
thresholds[(beta, target)] = quantile(risks, target / max_accepts)
|
| 188 |
+
return thresholds
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def dynamic_configs(
|
| 192 |
+
predictions_path: Path,
|
| 193 |
+
candidate_steps: list[int],
|
| 194 |
+
targets: list[int],
|
| 195 |
+
) -> list[dict[str, Any]]:
|
| 196 |
+
thresholds = threshold_grid(predictions_path, candidate_steps, targets)
|
| 197 |
+
return [
|
| 198 |
+
{
|
| 199 |
+
"name": f"dynamic_b{str(beta).replace('.', 'p')}_k{target:02d}",
|
| 200 |
+
"policy": "dynamic",
|
| 201 |
+
"beta": beta,
|
| 202 |
+
"target_accepts": target,
|
| 203 |
+
"threshold": thresholds[(beta, target)],
|
| 204 |
+
}
|
| 205 |
+
for beta in BETAS
|
| 206 |
+
for target in targets
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def static_configs(targets: list[int]) -> list[dict[str, Any]]:
|
| 211 |
+
return [
|
| 212 |
+
{
|
| 213 |
+
"name": f"static_late_k{target:02d}",
|
| 214 |
+
"policy": "static_late",
|
| 215 |
+
"beta": None,
|
| 216 |
+
"target_accepts": target,
|
| 217 |
+
"threshold": None,
|
| 218 |
+
}
|
| 219 |
+
for target in targets
|
| 220 |
+
]
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def load_selected(path: Path) -> list[dict[str, Any]]:
|
| 224 |
+
value = json.loads(path.read_text(encoding="utf-8"))
|
| 225 |
+
return [
|
| 226 |
+
{
|
| 227 |
+
"name": row["config_name"],
|
| 228 |
+
"policy": "dynamic",
|
| 229 |
+
"beta": float(row["beta"]),
|
| 230 |
+
"target_accepts": int(row["target_accepts"]),
|
| 231 |
+
"threshold": float(row["threshold"]),
|
| 232 |
+
}
|
| 233 |
+
for row in value["selected_dynamic"]
|
| 234 |
+
]
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
@torch.no_grad()
|
| 238 |
+
def predictor_with_features(
|
| 239 |
+
*,
|
| 240 |
+
predictor: Any,
|
| 241 |
+
teacher: Any,
|
| 242 |
+
noisy_input: torch.Tensor,
|
| 243 |
+
timestep: torch.Tensor,
|
| 244 |
+
anchor_hidden: torch.Tensor,
|
| 245 |
+
previous_hidden: torch.Tensor,
|
| 246 |
+
history_cache: dict[str, torch.Tensor],
|
| 247 |
+
cross_cache: dict[str, torch.Tensor],
|
| 248 |
+
current_start: int,
|
| 249 |
+
anchor_timestep: torch.Tensor | None = None,
|
| 250 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 251 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 252 |
+
current_tokens = teacher.patch_embedding(
|
| 253 |
+
noisy_input.permute(0, 2, 1, 3, 4)
|
| 254 |
+
).flatten(2).transpose(1, 2)
|
| 255 |
+
time_embedding = teacher.time_embedding(
|
| 256 |
+
sinusoidal_embedding_1d(
|
| 257 |
+
teacher.freq_dim, timestep.flatten()
|
| 258 |
+
).type_as(current_tokens)
|
| 259 |
+
)
|
| 260 |
+
timestep_modulation = teacher.time_projection(
|
| 261 |
+
time_embedding
|
| 262 |
+
).unflatten(1, (6, teacher.dim)).unflatten(dim=0, sizes=timestep.shape)
|
| 263 |
+
head_embedding = time_embedding.unflatten(
|
| 264 |
+
dim=0, sizes=timestep.shape
|
| 265 |
+
).unsqueeze(2)
|
| 266 |
+
condition_per_frame = time_embedding.unflatten(
|
| 267 |
+
dim=0, sizes=timestep.shape
|
| 268 |
+
)
|
| 269 |
+
condition_tokens = (
|
| 270 |
+
condition_per_frame[:, :, None, :]
|
| 271 |
+
.expand(
|
| 272 |
+
timestep.shape[0],
|
| 273 |
+
timestep.shape[1],
|
| 274 |
+
30 * 52,
|
| 275 |
+
teacher.dim,
|
| 276 |
+
)
|
| 277 |
+
.reshape(timestep.shape[0], -1, teacher.dim)
|
| 278 |
+
)
|
| 279 |
+
anchor_distance = None
|
| 280 |
+
if predictor.input_variant == "atc":
|
| 281 |
+
if anchor_timestep is None:
|
| 282 |
+
raise ValueError("ATC inference requires anchor_timestep")
|
| 283 |
+
anchor_distance = (
|
| 284 |
+
timestep.float() - anchor_timestep.float()
|
| 285 |
+
).abs().mean(dim=1)
|
| 286 |
+
grid_sizes = torch.tensor(
|
| 287 |
+
[[base.FRAMES_PER_CHUNK, 30, 52]], dtype=torch.long, device="cpu"
|
| 288 |
+
)
|
| 289 |
+
history_length = int(history_cache["local_end_index"].item())
|
| 290 |
+
output = predictor(
|
| 291 |
+
current_tokens=current_tokens,
|
| 292 |
+
anchor_hidden=anchor_hidden,
|
| 293 |
+
previous_hidden=previous_hidden,
|
| 294 |
+
timestep_modulation=timestep_modulation,
|
| 295 |
+
grid_sizes=grid_sizes,
|
| 296 |
+
freqs=teacher.freqs,
|
| 297 |
+
history_k=history_cache["k"][:, :history_length],
|
| 298 |
+
history_v=history_cache["v"][:, :history_length],
|
| 299 |
+
cross_k=cross_cache["k"],
|
| 300 |
+
cross_v=cross_cache["v"],
|
| 301 |
+
current_start=current_start,
|
| 302 |
+
return_features=True,
|
| 303 |
+
condition_tokens=condition_tokens,
|
| 304 |
+
anchor_distance=anchor_distance,
|
| 305 |
+
)
|
| 306 |
+
if not isinstance(output, tuple):
|
| 307 |
+
raise RuntimeError("Predictor did not return internal features")
|
| 308 |
+
pred_hidden, transformed = output
|
| 309 |
+
pred_flow = hidden_to_flow(
|
| 310 |
+
pred_hidden, head_embedding, grid_sizes, teacher
|
| 311 |
+
)
|
| 312 |
+
return pred_hidden, pred_flow, transformed
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
@torch.inference_mode()
|
| 316 |
+
def generate(
|
| 317 |
+
*,
|
| 318 |
+
pipeline: Any,
|
| 319 |
+
dataset_root: Path,
|
| 320 |
+
prompt_id: int,
|
| 321 |
+
generation_seed: int,
|
| 322 |
+
device: torch.device,
|
| 323 |
+
predictor: Any,
|
| 324 |
+
head: PredictorConfidenceHead,
|
| 325 |
+
config: dict[str, Any],
|
| 326 |
+
candidate_steps: list[int],
|
| 327 |
+
) -> tuple[torch.Tensor, dict[str, Any]]:
|
| 328 |
+
base.reset_kv_and_load_cross_cache(pipeline, dataset_root, prompt_id, device)
|
| 329 |
+
set_seed(generation_seed)
|
| 330 |
+
noise = torch.randn(
|
| 331 |
+
1,
|
| 332 |
+
base.NUM_CHUNKS * base.FRAMES_PER_CHUNK,
|
| 333 |
+
base.LATENT_CHANNELS,
|
| 334 |
+
base.LATENT_HEIGHT,
|
| 335 |
+
base.LATENT_WIDTH,
|
| 336 |
+
dtype=torch.bfloat16,
|
| 337 |
+
device=device,
|
| 338 |
+
)
|
| 339 |
+
timesteps = pipeline.denoising_step_list.to(device=device)
|
| 340 |
+
teacher = pipeline.generator.model
|
| 341 |
+
text_dim = int(teacher.text_embedding[0].in_features)
|
| 342 |
+
conditional_dict = {
|
| 343 |
+
"prompt_embeds": torch.zeros(
|
| 344 |
+
1, 1, text_dim, dtype=torch.bfloat16, device=device
|
| 345 |
+
)
|
| 346 |
+
}
|
| 347 |
+
capture = base.FinalHiddenCapture(teacher)
|
| 348 |
+
output_chunks: list[torch.Tensor] = []
|
| 349 |
+
previous_chunk_hidden: list[torch.Tensor | None] | None = None
|
| 350 |
+
decisions: list[dict[str, Any]] = []
|
| 351 |
+
full_calls = 0
|
| 352 |
+
predictor_calls = 0
|
| 353 |
+
accepted_predictor_calls = 0
|
| 354 |
+
timing_events: dict[str, list[tuple[torch.cuda.Event, torch.cuda.Event]]] = {
|
| 355 |
+
"full_dit": [],
|
| 356 |
+
"predictor": [],
|
| 357 |
+
"confidence": [],
|
| 358 |
+
"context_dit": [],
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
def start_timing() -> tuple[torch.cuda.Event, torch.cuda.Event]:
|
| 362 |
+
start_event = torch.cuda.Event(enable_timing=True)
|
| 363 |
+
end_event = torch.cuda.Event(enable_timing=True)
|
| 364 |
+
start_event.record()
|
| 365 |
+
return start_event, end_event
|
| 366 |
+
|
| 367 |
+
def finish_timing(
|
| 368 |
+
category: str,
|
| 369 |
+
events: tuple[torch.cuda.Event, torch.cuda.Event],
|
| 370 |
+
) -> None:
|
| 371 |
+
events[1].record()
|
| 372 |
+
timing_events[category].append(events)
|
| 373 |
+
|
| 374 |
+
started = time.perf_counter()
|
| 375 |
+
try:
|
| 376 |
+
for chunk in range(base.NUM_CHUNKS):
|
| 377 |
+
noisy_input = noise[
|
| 378 |
+
:, chunk * base.FRAMES_PER_CHUNK : (chunk + 1) * base.FRAMES_PER_CHUNK
|
| 379 |
+
]
|
| 380 |
+
current_hidden: list[torch.Tensor | None] = [None] * base.NUM_DENOISING_STEPS
|
| 381 |
+
denoised_pred: torch.Tensor | None = None
|
| 382 |
+
timestep: torch.Tensor | None = None
|
| 383 |
+
for step, current_timestep in enumerate(timesteps):
|
| 384 |
+
timestep = torch.ones(
|
| 385 |
+
[1, base.FRAMES_PER_CHUNK], dtype=torch.int64, device=device
|
| 386 |
+
) * current_timestep
|
| 387 |
+
candidate = chunk > 0 and step in candidate_steps
|
| 388 |
+
policy = str(config["policy"])
|
| 389 |
+
run_predictor = False
|
| 390 |
+
static_accept = False
|
| 391 |
+
if candidate and policy == "dynamic":
|
| 392 |
+
run_predictor = True
|
| 393 |
+
elif candidate and policy == "fppf":
|
| 394 |
+
run_predictor = True
|
| 395 |
+
static_accept = True
|
| 396 |
+
elif candidate and policy == "static_late":
|
| 397 |
+
first_chunk = (
|
| 398 |
+
base.NUM_CHUNKS
|
| 399 |
+
- int(config["target_accepts"]) // len(candidate_steps)
|
| 400 |
+
)
|
| 401 |
+
static_accept = chunk >= first_chunk
|
| 402 |
+
run_predictor = static_accept
|
| 403 |
+
|
| 404 |
+
accepted = False
|
| 405 |
+
pred_hidden = None
|
| 406 |
+
pred_x0 = None
|
| 407 |
+
predicted_local_error = None
|
| 408 |
+
risk = None
|
| 409 |
+
alpha = None
|
| 410 |
+
if run_predictor:
|
| 411 |
+
if previous_chunk_hidden is None:
|
| 412 |
+
raise RuntimeError("Previous chunk hidden is unavailable")
|
| 413 |
+
anchor_hidden = current_hidden[step - 1]
|
| 414 |
+
previous_hidden = previous_chunk_hidden[step]
|
| 415 |
+
if anchor_hidden is None or previous_hidden is None:
|
| 416 |
+
raise RuntimeError("Predictor inputs are unavailable")
|
| 417 |
+
predictor_events = start_timing()
|
| 418 |
+
pred_hidden, pred_flow, transformed = predictor_with_features(
|
| 419 |
+
predictor=predictor,
|
| 420 |
+
teacher=teacher,
|
| 421 |
+
noisy_input=noisy_input,
|
| 422 |
+
timestep=timestep,
|
| 423 |
+
anchor_hidden=anchor_hidden,
|
| 424 |
+
previous_hidden=previous_hidden,
|
| 425 |
+
history_cache=pipeline.kv_cache1[17],
|
| 426 |
+
cross_cache=pipeline.crossattn_cache[17],
|
| 427 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 428 |
+
anchor_timestep=(
|
| 429 |
+
torch.ones_like(timestep) * timesteps[step - 1]
|
| 430 |
+
),
|
| 431 |
+
)
|
| 432 |
+
finish_timing("predictor", predictor_events)
|
| 433 |
+
pred_x0 = pipeline.generator._convert_flow_pred_to_x0(
|
| 434 |
+
flow_pred=pred_flow.flatten(0, 1),
|
| 435 |
+
xt=noisy_input.flatten(0, 1),
|
| 436 |
+
timestep=timestep.flatten(0, 1),
|
| 437 |
+
).unflatten(0, pred_flow.shape[:2])
|
| 438 |
+
predictor_calls += 1
|
| 439 |
+
if policy == "dynamic":
|
| 440 |
+
chunk_position = torch.tensor(
|
| 441 |
+
[(chunk - 1) / 5.0], device=device
|
| 442 |
+
)
|
| 443 |
+
step_tensor = torch.tensor([step], dtype=torch.long, device=device)
|
| 444 |
+
confidence_events = start_timing()
|
| 445 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 446 |
+
predicted_log = head(
|
| 447 |
+
transformed_hidden=transformed,
|
| 448 |
+
pred_hidden=pred_hidden,
|
| 449 |
+
anchor_hidden=anchor_hidden,
|
| 450 |
+
chunk_position=chunk_position,
|
| 451 |
+
step_id=step_tensor,
|
| 452 |
+
)
|
| 453 |
+
finish_timing("confidence", confidence_events)
|
| 454 |
+
predicted_local_error = float(predicted_log.exp()[0])
|
| 455 |
+
alpha = (base.NUM_CHUNKS - 1 - chunk) / (base.NUM_CHUNKS - 2)
|
| 456 |
+
risk = predicted_local_error * (
|
| 457 |
+
1.0 + float(config["beta"]) * alpha
|
| 458 |
+
)
|
| 459 |
+
accepted = risk <= float(config["threshold"])
|
| 460 |
+
else:
|
| 461 |
+
accepted = static_accept
|
| 462 |
+
|
| 463 |
+
if accepted:
|
| 464 |
+
assert pred_hidden is not None and pred_x0 is not None
|
| 465 |
+
current_hidden[step] = pred_hidden
|
| 466 |
+
denoised_pred = pred_x0
|
| 467 |
+
accepted_predictor_calls += 1
|
| 468 |
+
else:
|
| 469 |
+
full_events = start_timing()
|
| 470 |
+
capture.start()
|
| 471 |
+
_, denoised_pred = pipeline.generator(
|
| 472 |
+
noisy_image_or_video=noisy_input,
|
| 473 |
+
conditional_dict=conditional_dict,
|
| 474 |
+
timestep=timestep,
|
| 475 |
+
kv_cache=pipeline.kv_cache1,
|
| 476 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 477 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 478 |
+
)
|
| 479 |
+
current_hidden[step] = capture.finish()
|
| 480 |
+
finish_timing("full_dit", full_events)
|
| 481 |
+
full_calls += 1
|
| 482 |
+
|
| 483 |
+
if candidate:
|
| 484 |
+
decisions.append(
|
| 485 |
+
{
|
| 486 |
+
"chunk": chunk,
|
| 487 |
+
"step": step,
|
| 488 |
+
"ran_predictor": run_predictor,
|
| 489 |
+
"accepted": accepted,
|
| 490 |
+
"predicted_local_error": predicted_local_error,
|
| 491 |
+
"chunk_alpha": alpha,
|
| 492 |
+
"impact_risk": risk,
|
| 493 |
+
}
|
| 494 |
+
)
|
| 495 |
+
if step < base.NUM_DENOISING_STEPS - 1:
|
| 496 |
+
if denoised_pred is None:
|
| 497 |
+
raise RuntimeError("Denoising step produced no x0")
|
| 498 |
+
next_timestep = timesteps[step + 1]
|
| 499 |
+
flat = denoised_pred.flatten(0, 1)
|
| 500 |
+
noisy_input = pipeline.scheduler.add_noise(
|
| 501 |
+
flat,
|
| 502 |
+
torch.randn_like(flat),
|
| 503 |
+
next_timestep
|
| 504 |
+
* torch.ones(
|
| 505 |
+
[base.FRAMES_PER_CHUNK], dtype=torch.long, device=device
|
| 506 |
+
),
|
| 507 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 508 |
+
|
| 509 |
+
if denoised_pred is None or timestep is None:
|
| 510 |
+
raise RuntimeError("Chunk produced no clean latent")
|
| 511 |
+
output_chunks.append(denoised_pred)
|
| 512 |
+
context_timestep = torch.ones_like(timestep) * pipeline.args.context_noise
|
| 513 |
+
context_events = start_timing()
|
| 514 |
+
pipeline.generator(
|
| 515 |
+
noisy_image_or_video=denoised_pred,
|
| 516 |
+
conditional_dict=conditional_dict,
|
| 517 |
+
timestep=context_timestep,
|
| 518 |
+
kv_cache=pipeline.kv_cache1,
|
| 519 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 520 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 521 |
+
)
|
| 522 |
+
finish_timing("context_dit", context_events)
|
| 523 |
+
previous_chunk_hidden = current_hidden
|
| 524 |
+
finally:
|
| 525 |
+
capture.close()
|
| 526 |
+
torch.cuda.synchronize()
|
| 527 |
+
elapsed = {
|
| 528 |
+
category: sum(start.elapsed_time(end) for start, end in events)
|
| 529 |
+
for category, events in timing_events.items()
|
| 530 |
+
}
|
| 531 |
+
actual_dit_time_ms = (
|
| 532 |
+
elapsed["full_dit"] + elapsed["predictor"] + elapsed["context_dit"]
|
| 533 |
+
)
|
| 534 |
+
return torch.cat(output_chunks, dim=1), {
|
| 535 |
+
"generation_time_s": time.perf_counter() - started,
|
| 536 |
+
"full_calls": full_calls,
|
| 537 |
+
"predictor_calls": predictor_calls,
|
| 538 |
+
"accepted_predictor_calls": accepted_predictor_calls,
|
| 539 |
+
"rejected_predictor_calls": predictor_calls - accepted_predictor_calls,
|
| 540 |
+
"full_dit_time_ms": elapsed["full_dit"],
|
| 541 |
+
"predictor_time_ms": elapsed["predictor"],
|
| 542 |
+
"confidence_head_time_ms": elapsed["confidence"],
|
| 543 |
+
"context_dit_time_ms": elapsed["context_dit"],
|
| 544 |
+
"actual_dit_time_ms": actual_dit_time_ms,
|
| 545 |
+
"model_path_time_ms": actual_dit_time_ms + elapsed["confidence"],
|
| 546 |
+
"decisions": decisions,
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
def load_models(
|
| 551 |
+
args: argparse.Namespace, device: torch.device
|
| 552 |
+
) -> tuple[Any, Any, Any, Any, Any]:
|
| 553 |
+
print("[setup] loading VAE", flush=True)
|
| 554 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 555 |
+
config = OmegaConf.merge(
|
| 556 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 557 |
+
OmegaConf.load(args.config_path),
|
| 558 |
+
)
|
| 559 |
+
print("[setup] loading frozen generator and Predictor", flush=True)
|
| 560 |
+
pipeline = base.build_pipeline(config, args.checkpoint_path, vae, device)
|
| 561 |
+
if args.predictor_weights is not None:
|
| 562 |
+
experiment = {
|
| 563 |
+
"name": "direct_layer17_predictor",
|
| 564 |
+
"source_layer": 17,
|
| 565 |
+
"weights": args.predictor_weights,
|
| 566 |
+
"gate_mode": "baseline",
|
| 567 |
+
}
|
| 568 |
+
else:
|
| 569 |
+
experiment = base.discover_experiments(
|
| 570 |
+
args.sweep_dir, ["teacher_layer_17"], None
|
| 571 |
+
)[0]
|
| 572 |
+
predictor = base.load_predictor(pipeline.generator.model, experiment, device)
|
| 573 |
+
head = PredictorConfidenceHead(
|
| 574 |
+
num_steps=max(args.candidate_steps)
|
| 575 |
+
).to(device=device).eval()
|
| 576 |
+
head.load_state_dict(load_file(str(args.confidence_weights), device="cpu"), strict=True)
|
| 577 |
+
head.requires_grad_(False)
|
| 578 |
+
lpips_model = None
|
| 579 |
+
if not args.skip_lpips:
|
| 580 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 581 |
+
lpips_model.requires_grad_(False)
|
| 582 |
+
return vae, pipeline, predictor, head, lpips_model
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
def smoke(args: argparse.Namespace, pipeline: Any, predictor: Any, head: Any, device: torch.device) -> None:
|
| 586 |
+
max_accepts = 6 * len(args.candidate_steps)
|
| 587 |
+
all_name = "fppf" if args.candidate_steps == [1, 2] else "fppp"
|
| 588 |
+
configurations = [
|
| 589 |
+
{"name": "ffff", "policy": "ffff", "beta": None, "threshold": None, "target_accepts": 0},
|
| 590 |
+
{"name": "dynamic_all_fallback", "policy": "dynamic", "beta": 1.5, "threshold": -math.inf, "target_accepts": 0},
|
| 591 |
+
{"name": all_name, "policy": "fppf", "beta": None, "threshold": None, "target_accepts": max_accepts},
|
| 592 |
+
{"name": "dynamic_all_accept", "policy": "dynamic", "beta": 1.5, "threshold": math.inf, "target_accepts": max_accepts},
|
| 593 |
+
]
|
| 594 |
+
latents = {}
|
| 595 |
+
diagnostics = {}
|
| 596 |
+
for config in configurations:
|
| 597 |
+
latent, diagnostic = generate(
|
| 598 |
+
pipeline=pipeline, dataset_root=args.dataset_root, prompt_id=80,
|
| 599 |
+
generation_seed=args.generation_seed, device=device, predictor=predictor,
|
| 600 |
+
head=head, config=config, candidate_steps=args.candidate_steps,
|
| 601 |
+
)
|
| 602 |
+
latents[config["name"]] = latent.cpu()
|
| 603 |
+
diagnostics[config["name"]] = diagnostic
|
| 604 |
+
print(
|
| 605 |
+
f"[smoke] {config['name']} full={diagnostic['full_calls']} "
|
| 606 |
+
f"pred={diagnostic['predictor_calls']} accept={diagnostic['accepted_predictor_calls']}",
|
| 607 |
+
flush=True,
|
| 608 |
+
)
|
| 609 |
+
fallback_diff = float((latents["ffff"].float() - latents["dynamic_all_fallback"].float()).abs().max())
|
| 610 |
+
accept_diff = float((latents[all_name].float() - latents["dynamic_all_accept"].float()).abs().max())
|
| 611 |
+
result = {
|
| 612 |
+
"status": "complete",
|
| 613 |
+
"prompt_id": 80,
|
| 614 |
+
"ffff_vs_all_fallback_max_abs": fallback_diff,
|
| 615 |
+
"fppf_vs_all_accept_max_abs": accept_diff,
|
| 616 |
+
"diagnostics": diagnostics,
|
| 617 |
+
}
|
| 618 |
+
atomic_json(args.output_root / "smoke.json", result)
|
| 619 |
+
if fallback_diff != 0.0 or accept_diff != 0.0:
|
| 620 |
+
raise RuntimeError(f"Smoke consistency failed: {result}")
|
| 621 |
+
print("[smoke] exact consistency passed", flush=True)
|
| 622 |
+
|
| 623 |
+
|
| 624 |
+
def aggregate(records: list[dict[str, Any]], output_dir: Path) -> list[dict[str, Any]]:
|
| 625 |
+
numeric = [
|
| 626 |
+
"accepted_predictor_calls", "full_calls", "predictor_calls",
|
| 627 |
+
"full_dit_time_ms", "predictor_time_ms", "confidence_head_time_ms",
|
| 628 |
+
"context_dit_time_ms", "actual_dit_time_ms", "model_path_time_ms",
|
| 629 |
+
"generation_time_s", "total_time_s", "latent_nrmse", "latent_tail_nrmse",
|
| 630 |
+
"psnr", "ssim", "lpips", "tail_psnr", "tail_ssim", "tail_lpips",
|
| 631 |
+
]
|
| 632 |
+
summary = []
|
| 633 |
+
for name in sorted({str(row["config_name"]) for row in records}):
|
| 634 |
+
selected = [row for row in records if row["config_name"] == name]
|
| 635 |
+
first = selected[0]
|
| 636 |
+
item = {
|
| 637 |
+
"config_name": name,
|
| 638 |
+
"policy": first["policy"],
|
| 639 |
+
"beta": first["beta"],
|
| 640 |
+
"target_accepts": first["target_accepts"],
|
| 641 |
+
"threshold": first["threshold"],
|
| 642 |
+
"num_prompts": len(selected),
|
| 643 |
+
}
|
| 644 |
+
for field in numeric:
|
| 645 |
+
item[field] = sum(float(row[field]) for row in selected) / len(selected)
|
| 646 |
+
summary.append(item)
|
| 647 |
+
fields = [
|
| 648 |
+
"config_name", "policy", "beta", "target_accepts", "threshold",
|
| 649 |
+
"num_prompts", *numeric,
|
| 650 |
+
]
|
| 651 |
+
write_csv(output_dir / "summary.csv", summary, fields)
|
| 652 |
+
return summary
|
| 653 |
+
|
| 654 |
+
|
| 655 |
+
def select_validation(
|
| 656 |
+
summary: list[dict[str, Any]], output_dir: Path, targets: list[int]
|
| 657 |
+
) -> None:
|
| 658 |
+
selected_dynamic = []
|
| 659 |
+
for target in targets:
|
| 660 |
+
candidates = [
|
| 661 |
+
row for row in summary
|
| 662 |
+
if row["policy"] == "dynamic" and int(row["target_accepts"]) == target
|
| 663 |
+
]
|
| 664 |
+
same_budget = [
|
| 665 |
+
row for row in candidates
|
| 666 |
+
if abs(float(row["accepted_predictor_calls"]) - target) <= 0.5 + 1e-8
|
| 667 |
+
]
|
| 668 |
+
if not same_budget:
|
| 669 |
+
closest = min(
|
| 670 |
+
abs(float(row["accepted_predictor_calls"]) - target)
|
| 671 |
+
for row in candidates
|
| 672 |
+
)
|
| 673 |
+
same_budget = [
|
| 674 |
+
row for row in candidates
|
| 675 |
+
if abs(abs(float(row["accepted_predictor_calls"]) - target) - closest)
|
| 676 |
+
<= 1e-8
|
| 677 |
+
]
|
| 678 |
+
same_budget.sort(
|
| 679 |
+
key=lambda row: (
|
| 680 |
+
float(row["tail_lpips"]),
|
| 681 |
+
abs(float(row["accepted_predictor_calls"]) - target),
|
| 682 |
+
float(row["beta"]),
|
| 683 |
+
)
|
| 684 |
+
)
|
| 685 |
+
selected_dynamic.append(same_budget[0])
|
| 686 |
+
atomic_json(
|
| 687 |
+
output_dir / "selected.json",
|
| 688 |
+
{
|
| 689 |
+
"selection_rule": (
|
| 690 |
+
"within target accepted calls +/-0.5, lowest validation tail LPIPS; "
|
| 691 |
+
"then budget distance and lower beta"
|
| 692 |
+
),
|
| 693 |
+
"selected_dynamic": selected_dynamic,
|
| 694 |
+
},
|
| 695 |
+
)
|
| 696 |
+
|
| 697 |
+
|
| 698 |
+
def formal(
|
| 699 |
+
args: argparse.Namespace,
|
| 700 |
+
vae: Any,
|
| 701 |
+
pipeline: Any,
|
| 702 |
+
predictor: Any,
|
| 703 |
+
head: Any,
|
| 704 |
+
lpips_model: Any,
|
| 705 |
+
device: torch.device,
|
| 706 |
+
) -> None:
|
| 707 |
+
split = args.mode
|
| 708 |
+
split_prompt_ids = (
|
| 709 |
+
list(range(80, 90)) if split == "validation" else list(range(90, 100))
|
| 710 |
+
)
|
| 711 |
+
prompt_ids = args.prompt_ids or split_prompt_ids
|
| 712 |
+
invalid_prompt_ids = sorted(set(prompt_ids) - set(split_prompt_ids))
|
| 713 |
+
if invalid_prompt_ids:
|
| 714 |
+
raise ValueError(
|
| 715 |
+
f"Prompt IDs {invalid_prompt_ids} are outside the {split} split"
|
| 716 |
+
)
|
| 717 |
+
if split == "validation":
|
| 718 |
+
configurations = dynamic_configs(
|
| 719 |
+
args.validation_predictions, args.candidate_steps, args.target_accepts
|
| 720 |
+
) + static_configs(args.target_accepts)
|
| 721 |
+
max_accepts = 6 * len(args.candidate_steps)
|
| 722 |
+
all_name = "fppf" if args.candidate_steps == [1, 2] else "fppp"
|
| 723 |
+
configurations += [
|
| 724 |
+
{"name": "ffff", "policy": "ffff", "beta": None, "threshold": None, "target_accepts": 0},
|
| 725 |
+
{"name": all_name, "policy": "fppf", "beta": None, "threshold": None, "target_accepts": max_accepts},
|
| 726 |
+
]
|
| 727 |
+
else:
|
| 728 |
+
selected_path = args.selected_path or (
|
| 729 |
+
args.output_root / "validation" / "selected.json"
|
| 730 |
+
)
|
| 731 |
+
configurations = load_selected(selected_path) + static_configs(args.target_accepts)
|
| 732 |
+
max_accepts = 6 * len(args.candidate_steps)
|
| 733 |
+
all_name = "fppf" if args.candidate_steps == [1, 2] else "fppp"
|
| 734 |
+
configurations += [
|
| 735 |
+
{"name": "ffff", "policy": "ffff", "beta": None, "threshold": None, "target_accepts": 0},
|
| 736 |
+
{"name": all_name, "policy": "fppf", "beta": None, "threshold": None, "target_accepts": max_accepts},
|
| 737 |
+
]
|
| 738 |
+
if args.config_names:
|
| 739 |
+
requested = set(args.config_names)
|
| 740 |
+
available = {str(config["name"]) for config in configurations}
|
| 741 |
+
missing = requested - available
|
| 742 |
+
if missing:
|
| 743 |
+
raise ValueError(
|
| 744 |
+
f"Unknown config_names {sorted(missing)}; available={sorted(available)}"
|
| 745 |
+
)
|
| 746 |
+
configurations = [
|
| 747 |
+
config for config in configurations if config["name"] in requested
|
| 748 |
+
]
|
| 749 |
+
output_dir = args.output_root / split
|
| 750 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 751 |
+
missing_references = [
|
| 752 |
+
prompt_id for prompt_id in prompt_ids
|
| 753 |
+
if not (
|
| 754 |
+
args.reference_root / "ffff_reference_frames"
|
| 755 |
+
/ f"prompt_{prompt_id:04d}.safetensors"
|
| 756 |
+
).exists()
|
| 757 |
+
]
|
| 758 |
+
if missing_references:
|
| 759 |
+
base.prepare_reference_frames(
|
| 760 |
+
vae=vae, dataset_root=args.dataset_root, output_dir=args.reference_root,
|
| 761 |
+
prompt_ids=missing_references, device=device, rebuild=False,
|
| 762 |
+
)
|
| 763 |
+
total = len(configurations) * len(prompt_ids)
|
| 764 |
+
records = []
|
| 765 |
+
completed = 0
|
| 766 |
+
print("[warmup] one unmeasured Full+Predictor+Head rollout", flush=True)
|
| 767 |
+
warmup_config = {
|
| 768 |
+
"name": "warmup",
|
| 769 |
+
"policy": "dynamic",
|
| 770 |
+
"beta": 1.0,
|
| 771 |
+
"threshold": -math.inf,
|
| 772 |
+
"target_accepts": 0,
|
| 773 |
+
}
|
| 774 |
+
warmup_latent, _ = generate(
|
| 775 |
+
pipeline=pipeline,
|
| 776 |
+
dataset_root=args.dataset_root,
|
| 777 |
+
prompt_id=prompt_ids[0],
|
| 778 |
+
generation_seed=args.generation_seed,
|
| 779 |
+
device=device,
|
| 780 |
+
predictor=predictor,
|
| 781 |
+
head=head,
|
| 782 |
+
config=warmup_config,
|
| 783 |
+
candidate_steps=args.candidate_steps,
|
| 784 |
+
)
|
| 785 |
+
del warmup_latent
|
| 786 |
+
torch.cuda.empty_cache()
|
| 787 |
+
for config in configurations:
|
| 788 |
+
for prompt_id in prompt_ids:
|
| 789 |
+
destination = output_dir / "per_run" / config["name"] / f"prompt_{prompt_id:04d}.json"
|
| 790 |
+
if destination.exists() and not args.overwrite:
|
| 791 |
+
records.append(json.loads(destination.read_text(encoding="utf-8")))
|
| 792 |
+
completed += 1
|
| 793 |
+
print(f"[cached] {completed}/{total} {config['name']} p={prompt_id}", flush=True)
|
| 794 |
+
continue
|
| 795 |
+
started = time.perf_counter()
|
| 796 |
+
reference_latent = base.load_ffff_latent(args.dataset_root, prompt_id).to(
|
| 797 |
+
device=device, dtype=torch.bfloat16
|
| 798 |
+
)
|
| 799 |
+
reference_u8 = base.load_reference_frames(args.reference_root, prompt_id)
|
| 800 |
+
latent, diagnostic = generate(
|
| 801 |
+
pipeline=pipeline, dataset_root=args.dataset_root, prompt_id=prompt_id,
|
| 802 |
+
generation_seed=args.generation_seed, device=device, predictor=predictor,
|
| 803 |
+
head=head, config=config, candidate_steps=args.candidate_steps,
|
| 804 |
+
)
|
| 805 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 806 |
+
pixels = vae.decode_to_pixel(latent, use_cache=False)
|
| 807 |
+
prediction_u8 = base.pixels_to_u8(pixels)
|
| 808 |
+
if args.save_videos:
|
| 809 |
+
base.save_mp4(
|
| 810 |
+
prediction_u8,
|
| 811 |
+
output_dir / "videos" / config["name"]
|
| 812 |
+
/ f"prompt_{prompt_id:04d}.mp4",
|
| 813 |
+
)
|
| 814 |
+
frame = base.frame_metrics(
|
| 815 |
+
reference_u8=reference_u8, prediction_u8=prediction_u8,
|
| 816 |
+
lpips_model=lpips_model, batch_size=args.metric_batch_size, device=device,
|
| 817 |
+
)
|
| 818 |
+
tail_start = impact_eval.chunk_frame_slice(1).start
|
| 819 |
+
tail = impact_eval.summarize_frame_range(frame, slice(tail_start, None))
|
| 820 |
+
record = {
|
| 821 |
+
"config_name": config["name"],
|
| 822 |
+
"policy": config["policy"],
|
| 823 |
+
"beta": config["beta"],
|
| 824 |
+
"target_accepts": config["target_accepts"],
|
| 825 |
+
"threshold": config["threshold"],
|
| 826 |
+
"prompt_id": prompt_id,
|
| 827 |
+
"latent_nrmse": impact_eval.nrmse(latent, reference_latent),
|
| 828 |
+
"latent_tail_nrmse": impact_eval.nrmse(
|
| 829 |
+
latent[:, base.FRAMES_PER_CHUNK:],
|
| 830 |
+
reference_latent[:, base.FRAMES_PER_CHUNK:],
|
| 831 |
+
),
|
| 832 |
+
"psnr": frame["psnr"],
|
| 833 |
+
"ssim": frame["ssim"],
|
| 834 |
+
"lpips": frame["lpips"],
|
| 835 |
+
"tail_psnr": tail["psnr"],
|
| 836 |
+
"tail_ssim": tail["ssim"],
|
| 837 |
+
"tail_lpips": tail["lpips"],
|
| 838 |
+
**diagnostic,
|
| 839 |
+
"total_time_s": time.perf_counter() - started,
|
| 840 |
+
}
|
| 841 |
+
atomic_json(destination, record)
|
| 842 |
+
records.append(record)
|
| 843 |
+
completed += 1
|
| 844 |
+
print(
|
| 845 |
+
f"[run] {completed}/{total} {config['name']} p={prompt_id} "
|
| 846 |
+
f"accept={record['accepted_predictor_calls']} "
|
| 847 |
+
f"tail_lpips={record['tail_lpips']:.5f} "
|
| 848 |
+
f"time={record['total_time_s']:.1f}s",
|
| 849 |
+
flush=True,
|
| 850 |
+
)
|
| 851 |
+
if hasattr(vae.model, "clear_cache"):
|
| 852 |
+
vae.model.clear_cache()
|
| 853 |
+
del reference_latent, reference_u8, latent, pixels, prediction_u8, frame
|
| 854 |
+
torch.cuda.empty_cache()
|
| 855 |
+
flat_fields = sorted({key for row in records for key in row if key != "decisions"})
|
| 856 |
+
shard_suffix = f"_gpu{args.gpu}" if args.prompt_ids else ""
|
| 857 |
+
write_csv(
|
| 858 |
+
output_dir / f"runs{shard_suffix}.csv",
|
| 859 |
+
[{key: row.get(key) for key in flat_fields} for row in records],
|
| 860 |
+
flat_fields,
|
| 861 |
+
)
|
| 862 |
+
summary_output_dir = output_dir
|
| 863 |
+
if shard_suffix:
|
| 864 |
+
summary_output_dir = output_dir / f".summary_shard_gpu{args.gpu}"
|
| 865 |
+
summary_output_dir.mkdir(parents=True, exist_ok=True)
|
| 866 |
+
summary = aggregate(records, summary_output_dir)
|
| 867 |
+
if shard_suffix:
|
| 868 |
+
os.replace(
|
| 869 |
+
summary_output_dir / "summary.csv",
|
| 870 |
+
output_dir / f"summary{shard_suffix}.csv",
|
| 871 |
+
)
|
| 872 |
+
summary_output_dir.rmdir()
|
| 873 |
+
if split == "validation" and not shard_suffix:
|
| 874 |
+
select_validation(summary, output_dir, args.target_accepts)
|
| 875 |
+
atomic_json(
|
| 876 |
+
output_dir / f"manifest{shard_suffix}.json",
|
| 877 |
+
{
|
| 878 |
+
"status": "complete", "split": split, "prompt_ids": prompt_ids,
|
| 879 |
+
"num_configs": len(configurations), "num_runs": len(records),
|
| 880 |
+
"configs": configurations, "candidate_steps": args.candidate_steps,
|
| 881 |
+
"target_accepts": args.target_accepts,
|
| 882 |
+
"predictor_weights": (
|
| 883 |
+
str(args.predictor_weights) if args.predictor_weights else None
|
| 884 |
+
),
|
| 885 |
+
},
|
| 886 |
+
)
|
| 887 |
+
print(f"[complete] {split} -> {output_dir}", flush=True)
|
| 888 |
+
|
| 889 |
+
|
| 890 |
+
def main() -> None:
|
| 891 |
+
args = parse_args()
|
| 892 |
+
args.output_root.mkdir(parents=True, exist_ok=True)
|
| 893 |
+
device = torch.device("cuda")
|
| 894 |
+
torch.set_grad_enabled(False)
|
| 895 |
+
set_seed(args.generation_seed)
|
| 896 |
+
vae, pipeline, predictor, head, lpips_model = load_models(args, device)
|
| 897 |
+
if args.mode == "smoke":
|
| 898 |
+
smoke(args, pipeline, predictor, head, device)
|
| 899 |
+
else:
|
| 900 |
+
formal(args, vae, pipeline, predictor, head, lpips_model, device)
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
if __name__ == "__main__":
|
| 904 |
+
main()
|
scripts/evaluate_layer17_moviebench_step2000.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Evaluate a trained Self-Forcing Layer-17 predictor on MovieBench prompts."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def preparse_gpu() -> str:
|
| 15 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 16 |
+
parser.add_argument("--gpu", required=True)
|
| 17 |
+
args, _ = parser.parse_known_args()
|
| 18 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu
|
| 19 |
+
return args.gpu
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
GPU = preparse_gpu()
|
| 23 |
+
|
| 24 |
+
import lpips
|
| 25 |
+
import torch
|
| 26 |
+
from omegaconf import OmegaConf
|
| 27 |
+
|
| 28 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 29 |
+
if str(ROOT) not in sys.path:
|
| 30 |
+
sys.path.insert(0, str(ROOT))
|
| 31 |
+
|
| 32 |
+
from predictor_training.offline_data import TOKENS_PER_CHUNK
|
| 33 |
+
from scripts.evaluate_single_block_fppf import (
|
| 34 |
+
FinalHiddenCapture,
|
| 35 |
+
atomic_json,
|
| 36 |
+
build_pipeline,
|
| 37 |
+
frame_metrics,
|
| 38 |
+
load_predictor,
|
| 39 |
+
pixels_to_u8,
|
| 40 |
+
predictor_step,
|
| 41 |
+
save_mp4,
|
| 42 |
+
)
|
| 43 |
+
from utils.misc import set_seed
|
| 44 |
+
from utils.wan_wrapper import WanTextEncoder, WanVAEWrapper
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
NUM_CHUNKS = 7
|
| 48 |
+
FRAMES_PER_CHUNK = 3
|
| 49 |
+
NUM_STEPS = 4
|
| 50 |
+
LATENT_CHANNELS = 16
|
| 51 |
+
LATENT_HEIGHT = 60
|
| 52 |
+
LATENT_WIDTH = 104
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def read_lines(path: Path) -> list[str]:
|
| 56 |
+
return [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def reset_caches(pipeline, device: torch.device) -> None:
|
| 60 |
+
if pipeline.kv_cache1 is None:
|
| 61 |
+
pipeline._initialize_kv_cache(1, torch.bfloat16, device)
|
| 62 |
+
pipeline._initialize_crossattn_cache(1, torch.bfloat16, device)
|
| 63 |
+
for cache in pipeline.kv_cache1:
|
| 64 |
+
cache["global_end_index"].zero_()
|
| 65 |
+
cache["local_end_index"].zero_()
|
| 66 |
+
for cache in pipeline.crossattn_cache:
|
| 67 |
+
cache["is_init"] = False
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@torch.inference_mode()
|
| 71 |
+
def rollout(pipeline, conditional_dict, seed: int, device: torch.device, predictor=None):
|
| 72 |
+
reset_caches(pipeline, device)
|
| 73 |
+
set_seed(seed)
|
| 74 |
+
noise = torch.randn(
|
| 75 |
+
1,
|
| 76 |
+
NUM_CHUNKS * FRAMES_PER_CHUNK,
|
| 77 |
+
LATENT_CHANNELS,
|
| 78 |
+
LATENT_HEIGHT,
|
| 79 |
+
LATENT_WIDTH,
|
| 80 |
+
dtype=torch.bfloat16,
|
| 81 |
+
device=device,
|
| 82 |
+
)
|
| 83 |
+
teacher = pipeline.generator.model
|
| 84 |
+
timesteps = pipeline.denoising_step_list.to(device=device)
|
| 85 |
+
outputs = []
|
| 86 |
+
previous_chunk_hidden = None
|
| 87 |
+
capture = FinalHiddenCapture(teacher)
|
| 88 |
+
full_calls = predictor_calls = 0
|
| 89 |
+
started = time.perf_counter()
|
| 90 |
+
try:
|
| 91 |
+
for chunk in range(NUM_CHUNKS):
|
| 92 |
+
noisy_input = noise[:, chunk * 3 : (chunk + 1) * 3]
|
| 93 |
+
current_hidden = [None] * NUM_STEPS
|
| 94 |
+
denoised_pred = timestep = None
|
| 95 |
+
for step, current_timestep in enumerate(timesteps):
|
| 96 |
+
timestep = torch.ones([1, 3], dtype=torch.int64, device=device) * current_timestep
|
| 97 |
+
use_predictor = predictor is not None and chunk > 0 and step in {1, 2}
|
| 98 |
+
if use_predictor:
|
| 99 |
+
hidden, flow, _ = predictor_step(
|
| 100 |
+
predictor=predictor,
|
| 101 |
+
teacher=teacher,
|
| 102 |
+
noisy_input=noisy_input,
|
| 103 |
+
timestep=timestep,
|
| 104 |
+
anchor_hidden=current_hidden[step - 1],
|
| 105 |
+
previous_hidden=previous_chunk_hidden[step],
|
| 106 |
+
history_cache=pipeline.kv_cache1[17],
|
| 107 |
+
cross_cache=pipeline.crossattn_cache[17],
|
| 108 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 109 |
+
)
|
| 110 |
+
denoised_pred = pipeline.generator._convert_flow_pred_to_x0(
|
| 111 |
+
flow_pred=flow.flatten(0, 1),
|
| 112 |
+
xt=noisy_input.flatten(0, 1),
|
| 113 |
+
timestep=timestep.flatten(0, 1),
|
| 114 |
+
).unflatten(0, flow.shape[:2])
|
| 115 |
+
current_hidden[step] = hidden
|
| 116 |
+
predictor_calls += 1
|
| 117 |
+
else:
|
| 118 |
+
capture.start()
|
| 119 |
+
_, denoised_pred = pipeline.generator(
|
| 120 |
+
noisy_image_or_video=noisy_input,
|
| 121 |
+
conditional_dict=conditional_dict,
|
| 122 |
+
timestep=timestep,
|
| 123 |
+
kv_cache=pipeline.kv_cache1,
|
| 124 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 125 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 126 |
+
)
|
| 127 |
+
current_hidden[step] = capture.finish()
|
| 128 |
+
full_calls += 1
|
| 129 |
+
if step < NUM_STEPS - 1:
|
| 130 |
+
flat = denoised_pred.flatten(0, 1)
|
| 131 |
+
noisy_input = pipeline.scheduler.add_noise(
|
| 132 |
+
flat,
|
| 133 |
+
torch.randn_like(flat),
|
| 134 |
+
timesteps[step + 1] * torch.ones([3], dtype=torch.long, device=device),
|
| 135 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 136 |
+
outputs.append(denoised_pred)
|
| 137 |
+
pipeline.generator(
|
| 138 |
+
noisy_image_or_video=denoised_pred,
|
| 139 |
+
conditional_dict=conditional_dict,
|
| 140 |
+
timestep=torch.ones_like(timestep) * pipeline.args.context_noise,
|
| 141 |
+
kv_cache=pipeline.kv_cache1,
|
| 142 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 143 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 144 |
+
)
|
| 145 |
+
previous_chunk_hidden = current_hidden
|
| 146 |
+
finally:
|
| 147 |
+
capture.close()
|
| 148 |
+
torch.cuda.synchronize()
|
| 149 |
+
return torch.cat(outputs, dim=1), {
|
| 150 |
+
"generation_time_s": time.perf_counter() - started,
|
| 151 |
+
"full_calls": full_calls,
|
| 152 |
+
"predictor_calls": predictor_calls,
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def main() -> None:
|
| 157 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 158 |
+
parser.add_argument("--gpu", default=GPU)
|
| 159 |
+
parser.add_argument("--prompt_ids", type=int, nargs="+", required=True)
|
| 160 |
+
parser.add_argument("--original_prompts", type=Path, required=True)
|
| 161 |
+
parser.add_argument("--extended_prompts", type=Path, required=True)
|
| 162 |
+
parser.add_argument("--output_dir", type=Path, required=True)
|
| 163 |
+
parser.add_argument("--weights", type=Path, required=True)
|
| 164 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 165 |
+
parser.add_argument("--metric_batch_size", type=int, default=4)
|
| 166 |
+
args = parser.parse_args()
|
| 167 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 168 |
+
original = read_lines(args.original_prompts)
|
| 169 |
+
extended = read_lines(args.extended_prompts)
|
| 170 |
+
if len(original) != len(extended) or min(args.prompt_ids) < 0 or max(args.prompt_ids) >= len(original):
|
| 171 |
+
raise ValueError("MovieBench original/extended prompt pairing is invalid")
|
| 172 |
+
|
| 173 |
+
atomic_json(
|
| 174 |
+
args.output_dir / "manifest.json",
|
| 175 |
+
{
|
| 176 |
+
"status": "running",
|
| 177 |
+
"gpu": args.gpu,
|
| 178 |
+
"prompt_ids": args.prompt_ids,
|
| 179 |
+
"weights": str(args.weights.resolve()),
|
| 180 |
+
"teacher": str((ROOT / "checkpoints/self_forcing_dmd.pt").resolve()),
|
| 181 |
+
"generation_seed_reset_per_prompt": args.seed,
|
| 182 |
+
"generation_prompts": str(args.extended_prompts.resolve()),
|
| 183 |
+
"evaluation_prompts": str(args.original_prompts.resolve()),
|
| 184 |
+
"schedule": "chunk0=FFFF; chunks1-6=FPPF",
|
| 185 |
+
"metrics": ["PSNR", "SSIM", "LPIPS", "rollout-only PSNR/SSIM/LPIPS"],
|
| 186 |
+
},
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
device = torch.device("cuda")
|
| 190 |
+
torch.set_grad_enabled(False)
|
| 191 |
+
config = OmegaConf.merge(
|
| 192 |
+
OmegaConf.load(ROOT / "configs/default_config.yaml"),
|
| 193 |
+
OmegaConf.load(ROOT / "configs/self_forcing_sid.yaml"),
|
| 194 |
+
)
|
| 195 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 196 |
+
pipeline = build_pipeline(config, ROOT / "checkpoints/self_forcing_dmd.pt", vae, device)
|
| 197 |
+
text_encoder = WanTextEncoder().to(device=device, dtype=torch.bfloat16).eval()
|
| 198 |
+
text_encoder.requires_grad_(False)
|
| 199 |
+
predictor = load_predictor(
|
| 200 |
+
pipeline.generator.model,
|
| 201 |
+
{"source_layer": 17, "weights": args.weights, "gate_mode": "baseline"},
|
| 202 |
+
device,
|
| 203 |
+
)
|
| 204 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 205 |
+
lpips_model.requires_grad_(False)
|
| 206 |
+
|
| 207 |
+
reference_dir = args.output_dir / "videos" / "ffff"
|
| 208 |
+
prediction_dir = args.output_dir / "videos" / "fppf_step2000"
|
| 209 |
+
for offset, prompt_id in enumerate(args.prompt_ids, start=1):
|
| 210 |
+
result_path = args.output_dir / "per_prompt" / f"prompt_{prompt_id:04d}.json"
|
| 211 |
+
if (
|
| 212 |
+
result_path.exists()
|
| 213 |
+
and (reference_dir / f"{prompt_id:05d}.mp4").exists()
|
| 214 |
+
and (prediction_dir / f"{prompt_id:05d}.mp4").exists()
|
| 215 |
+
):
|
| 216 |
+
print(f"[skip] {offset}/{len(args.prompt_ids)} id={prompt_id}", flush=True)
|
| 217 |
+
continue
|
| 218 |
+
print(f"[encode] {offset}/{len(args.prompt_ids)} id={prompt_id}", flush=True)
|
| 219 |
+
conditional = text_encoder(text_prompts=[extended[prompt_id]])
|
| 220 |
+
reference_latent, ffff_counts = rollout(pipeline, conditional, args.seed, device)
|
| 221 |
+
prediction_latent, fppf_counts = rollout(
|
| 222 |
+
pipeline, conditional, args.seed, device, predictor=predictor
|
| 223 |
+
)
|
| 224 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 225 |
+
reference_pixels = vae.decode_to_pixel(reference_latent, use_cache=False)
|
| 226 |
+
prediction_pixels = vae.decode_to_pixel(prediction_latent, use_cache=False)
|
| 227 |
+
reference_u8 = pixels_to_u8(reference_pixels)
|
| 228 |
+
prediction_u8 = pixels_to_u8(prediction_pixels)
|
| 229 |
+
save_mp4(reference_u8, reference_dir / f"{prompt_id:05d}.mp4")
|
| 230 |
+
save_mp4(prediction_u8, prediction_dir / f"{prompt_id:05d}.mp4")
|
| 231 |
+
metrics = frame_metrics(
|
| 232 |
+
reference_u8=reference_u8,
|
| 233 |
+
prediction_u8=prediction_u8,
|
| 234 |
+
lpips_model=lpips_model,
|
| 235 |
+
batch_size=args.metric_batch_size,
|
| 236 |
+
device=device,
|
| 237 |
+
)
|
| 238 |
+
atomic_json(
|
| 239 |
+
result_path,
|
| 240 |
+
{
|
| 241 |
+
"status": "complete",
|
| 242 |
+
"prompt_id": prompt_id,
|
| 243 |
+
"original_prompt": original[prompt_id],
|
| 244 |
+
"generation_prompt": extended[prompt_id],
|
| 245 |
+
"seed": args.seed,
|
| 246 |
+
"latent_frames": NUM_CHUNKS * FRAMES_PER_CHUNK,
|
| 247 |
+
"decoded_frames": metrics["num_frames"],
|
| 248 |
+
"schedule": "chunk0=FFFF; chunks1-6=FPPF",
|
| 249 |
+
"ffff": ffff_counts,
|
| 250 |
+
"fppf": fppf_counts,
|
| 251 |
+
**metrics,
|
| 252 |
+
},
|
| 253 |
+
)
|
| 254 |
+
print(
|
| 255 |
+
f"[result] id={prompt_id} psnr={metrics['psnr']:.4f} "
|
| 256 |
+
f"ssim={metrics['ssim']:.6f} lpips={metrics['lpips']:.6f}",
|
| 257 |
+
flush=True,
|
| 258 |
+
)
|
| 259 |
+
if hasattr(vae.model, "clear_cache"):
|
| 260 |
+
vae.model.clear_cache()
|
| 261 |
+
del conditional, reference_latent, prediction_latent, reference_pixels
|
| 262 |
+
del prediction_pixels, reference_u8, prediction_u8
|
| 263 |
+
torch.cuda.empty_cache()
|
| 264 |
+
|
| 265 |
+
manifest = json.loads((args.output_dir / "manifest.json").read_text(encoding="utf-8"))
|
| 266 |
+
manifest["status"] = "complete"
|
| 267 |
+
atomic_json(args.output_dir / "manifest.json", manifest)
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
if __name__ == "__main__":
|
| 271 |
+
main()
|
scripts/evaluate_long_video_fppf.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Evaluate 2x/4x long FFFF and one-block Layer-17 FPPF rollouts."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _preparse_gpu() -> str:
|
| 15 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 16 |
+
parser.add_argument("--gpu", default="0")
|
| 17 |
+
args, _ = parser.parse_known_args()
|
| 18 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 19 |
+
return str(args.gpu)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 23 |
+
|
| 24 |
+
import lpips
|
| 25 |
+
import torch
|
| 26 |
+
from omegaconf import OmegaConf
|
| 27 |
+
from torchvision.io import write_video
|
| 28 |
+
|
| 29 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 30 |
+
if str(REPO_ROOT) not in sys.path:
|
| 31 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 32 |
+
|
| 33 |
+
from predictor_training.offline_data import TOKENS_PER_CHUNK
|
| 34 |
+
from scripts.evaluate_single_block_fppf import (
|
| 35 |
+
DEFAULT_PROMPT_IDS,
|
| 36 |
+
FRAMES_PER_CHUNK,
|
| 37 |
+
LATENT_CHANNELS,
|
| 38 |
+
LATENT_HEIGHT,
|
| 39 |
+
LATENT_WIDTH,
|
| 40 |
+
NUM_DENOISING_STEPS,
|
| 41 |
+
FinalHiddenCapture,
|
| 42 |
+
atomic_json,
|
| 43 |
+
build_pipeline,
|
| 44 |
+
discover_experiments,
|
| 45 |
+
frame_metrics,
|
| 46 |
+
load_predictor,
|
| 47 |
+
load_prompt_metadata,
|
| 48 |
+
pixels_to_u8,
|
| 49 |
+
predictor_step,
|
| 50 |
+
reset_kv_and_load_cross_cache,
|
| 51 |
+
)
|
| 52 |
+
from utils.misc import set_seed
|
| 53 |
+
from utils.wan_wrapper import WanVAEWrapper
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def parse_args() -> argparse.Namespace:
|
| 57 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 58 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 59 |
+
parser.add_argument("--prompt_ids", type=int, nargs="+", required=True)
|
| 60 |
+
parser.add_argument("--latent_lengths", type=int, nargs="+", default=[42, 84])
|
| 61 |
+
parser.add_argument(
|
| 62 |
+
"--config_path", type=Path, default=Path("configs/self_forcing_sid.yaml")
|
| 63 |
+
)
|
| 64 |
+
parser.add_argument(
|
| 65 |
+
"--checkpoint_path", type=Path, default=Path("checkpoints/self_forcing_dmd.pt")
|
| 66 |
+
)
|
| 67 |
+
parser.add_argument(
|
| 68 |
+
"--dataset_root", type=Path,
|
| 69 |
+
default=Path("outputs/predictor_offline_100_all_blocks"),
|
| 70 |
+
)
|
| 71 |
+
parser.add_argument(
|
| 72 |
+
"--sweep_dir", type=Path, default=Path("outputs/single_block_init_sweep")
|
| 73 |
+
)
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--output_dir", type=Path, default=Path("outputs/long_video_2x4x_eval")
|
| 76 |
+
)
|
| 77 |
+
parser.add_argument("--metric_batch_size", type=int, default=4)
|
| 78 |
+
parser.add_argument("--generation_seed", type=int, default=0)
|
| 79 |
+
args = parser.parse_args()
|
| 80 |
+
if any(length <= 0 or length % FRAMES_PER_CHUNK for length in args.latent_lengths):
|
| 81 |
+
parser.error("Latent lengths must be positive multiples of 3")
|
| 82 |
+
if any(prompt not in DEFAULT_PROMPT_IDS for prompt in args.prompt_ids):
|
| 83 |
+
parser.error("This evaluation is restricted to validation prompt IDs 80..99")
|
| 84 |
+
return args
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def resolve(path: Path) -> Path:
|
| 88 |
+
return path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve()
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@torch.inference_mode()
|
| 92 |
+
def generate_rollout(
|
| 93 |
+
*, pipeline, dataset_root: Path, prompt_id: int, latent_length: int,
|
| 94 |
+
generation_seed: int, device: torch.device, predictor, source_layer: int | None,
|
| 95 |
+
schedule: str,
|
| 96 |
+
) -> tuple[torch.Tensor, dict[str, float | int]]:
|
| 97 |
+
if schedule not in {"FFFF", "FPPF"}:
|
| 98 |
+
raise ValueError(schedule)
|
| 99 |
+
if schedule == "FPPF" and (predictor is None or source_layer is None):
|
| 100 |
+
raise ValueError("FPPF requires the Predictor")
|
| 101 |
+
num_chunks = latent_length // FRAMES_PER_CHUNK
|
| 102 |
+
reset_kv_and_load_cross_cache(pipeline, dataset_root, prompt_id, device)
|
| 103 |
+
set_seed(generation_seed)
|
| 104 |
+
noise = torch.randn(
|
| 105 |
+
1, latent_length, LATENT_CHANNELS, LATENT_HEIGHT, LATENT_WIDTH,
|
| 106 |
+
dtype=torch.bfloat16, device=device,
|
| 107 |
+
)
|
| 108 |
+
teacher = pipeline.generator.model
|
| 109 |
+
text_dim = int(teacher.text_embedding[0].in_features)
|
| 110 |
+
conditional_dict = {
|
| 111 |
+
"prompt_embeds": torch.zeros(
|
| 112 |
+
1, 1, text_dim, dtype=torch.bfloat16, device=device
|
| 113 |
+
)
|
| 114 |
+
}
|
| 115 |
+
timesteps = pipeline.denoising_step_list.to(device=device)
|
| 116 |
+
output_chunks = []
|
| 117 |
+
previous_chunk_hidden = None
|
| 118 |
+
capture = FinalHiddenCapture(teacher)
|
| 119 |
+
full_calls = predictor_calls = 0
|
| 120 |
+
started = time.perf_counter()
|
| 121 |
+
try:
|
| 122 |
+
for chunk in range(num_chunks):
|
| 123 |
+
noisy_input = noise[:, chunk * 3:(chunk + 1) * 3]
|
| 124 |
+
current_hidden = [None] * NUM_DENOISING_STEPS
|
| 125 |
+
denoised_pred = timestep = None
|
| 126 |
+
for step, current_timestep in enumerate(timesteps):
|
| 127 |
+
timestep = torch.ones(
|
| 128 |
+
[1, FRAMES_PER_CHUNK], dtype=torch.int64, device=device
|
| 129 |
+
) * current_timestep
|
| 130 |
+
use_predictor = schedule == "FPPF" and chunk > 0 and step in {1, 2}
|
| 131 |
+
if use_predictor:
|
| 132 |
+
pred_hidden, flow, _ = predictor_step(
|
| 133 |
+
predictor=predictor,
|
| 134 |
+
teacher=teacher,
|
| 135 |
+
noisy_input=noisy_input,
|
| 136 |
+
timestep=timestep,
|
| 137 |
+
anchor_hidden=current_hidden[step - 1],
|
| 138 |
+
previous_hidden=previous_chunk_hidden[step],
|
| 139 |
+
history_cache=pipeline.kv_cache1[source_layer],
|
| 140 |
+
cross_cache=pipeline.crossattn_cache[source_layer],
|
| 141 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 142 |
+
)
|
| 143 |
+
denoised_pred = pipeline.generator._convert_flow_pred_to_x0(
|
| 144 |
+
flow_pred=flow.flatten(0, 1),
|
| 145 |
+
xt=noisy_input.flatten(0, 1),
|
| 146 |
+
timestep=timestep.flatten(0, 1),
|
| 147 |
+
).unflatten(0, flow.shape[:2])
|
| 148 |
+
current_hidden[step] = pred_hidden
|
| 149 |
+
predictor_calls += 1
|
| 150 |
+
else:
|
| 151 |
+
capture.start()
|
| 152 |
+
_, denoised_pred = pipeline.generator(
|
| 153 |
+
noisy_image_or_video=noisy_input,
|
| 154 |
+
conditional_dict=conditional_dict,
|
| 155 |
+
timestep=timestep,
|
| 156 |
+
kv_cache=pipeline.kv_cache1,
|
| 157 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 158 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 159 |
+
)
|
| 160 |
+
current_hidden[step] = capture.finish()
|
| 161 |
+
full_calls += 1
|
| 162 |
+
if step < NUM_DENOISING_STEPS - 1:
|
| 163 |
+
flat = denoised_pred.flatten(0, 1)
|
| 164 |
+
noisy_input = pipeline.scheduler.add_noise(
|
| 165 |
+
flat, torch.randn_like(flat),
|
| 166 |
+
timesteps[step + 1] * torch.ones(
|
| 167 |
+
[FRAMES_PER_CHUNK], dtype=torch.long, device=device
|
| 168 |
+
),
|
| 169 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 170 |
+
output_chunks.append(denoised_pred)
|
| 171 |
+
pipeline.generator(
|
| 172 |
+
noisy_image_or_video=denoised_pred,
|
| 173 |
+
conditional_dict=conditional_dict,
|
| 174 |
+
timestep=torch.ones_like(timestep) * pipeline.args.context_noise,
|
| 175 |
+
kv_cache=pipeline.kv_cache1,
|
| 176 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 177 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 178 |
+
)
|
| 179 |
+
previous_chunk_hidden = current_hidden
|
| 180 |
+
finally:
|
| 181 |
+
capture.close()
|
| 182 |
+
torch.cuda.synchronize()
|
| 183 |
+
return torch.cat(output_chunks, dim=1), {
|
| 184 |
+
"generation_time_s": time.perf_counter() - started,
|
| 185 |
+
"full_calls": full_calls,
|
| 186 |
+
"predictor_calls": predictor_calls,
|
| 187 |
+
"num_chunks": num_chunks,
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def save_mp4(frames: torch.Tensor, path: Path) -> None:
|
| 192 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 193 |
+
write_video(
|
| 194 |
+
str(path), frames.permute(0, 2, 3, 1), fps=16,
|
| 195 |
+
video_codec="libx264", options={"crf": "18"},
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def main() -> None:
|
| 200 |
+
args = parse_args()
|
| 201 |
+
for field in ("config_path", "checkpoint_path", "dataset_root", "sweep_dir", "output_dir"):
|
| 202 |
+
setattr(args, field, resolve(getattr(args, field)))
|
| 203 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 204 |
+
atomic_json(args.output_dir / "manifest.json", {
|
| 205 |
+
"status": "running", "physical_gpu": str(args.gpu),
|
| 206 |
+
"prompt_ids": args.prompt_ids, "latent_lengths": args.latent_lengths,
|
| 207 |
+
"methods": ["FFFF", "FPPF_teacher_layer_17"],
|
| 208 |
+
"generation_seed": args.generation_seed,
|
| 209 |
+
})
|
| 210 |
+
device = torch.device("cuda")
|
| 211 |
+
torch.set_grad_enabled(False)
|
| 212 |
+
set_seed(args.generation_seed)
|
| 213 |
+
config = OmegaConf.merge(
|
| 214 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 215 |
+
OmegaConf.load(args.config_path),
|
| 216 |
+
)
|
| 217 |
+
# The released checkpoint uses full attention over its 21-latent training
|
| 218 |
+
# horizon. Long inference keeps exactly that horizon as a rolling window;
|
| 219 |
+
# within the first 21 latents this is numerically the same attention span.
|
| 220 |
+
config.model_kwargs.local_attn_size = 21
|
| 221 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 222 |
+
pipeline = build_pipeline(config, args.checkpoint_path, vae, device)
|
| 223 |
+
experiment = discover_experiments(
|
| 224 |
+
args.sweep_dir, ["teacher_layer_17"], None
|
| 225 |
+
)[0]
|
| 226 |
+
predictor = load_predictor(pipeline.generator.model, experiment, device)
|
| 227 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 228 |
+
lpips_model.requires_grad_(False)
|
| 229 |
+
|
| 230 |
+
for prompt_id in args.prompt_ids:
|
| 231 |
+
prompt = load_prompt_metadata(args.dataset_root, prompt_id)["prompt"]
|
| 232 |
+
for latent_length in args.latent_lengths:
|
| 233 |
+
run_dir = args.output_dir / f"latent_{latent_length}" / f"prompt_{prompt_id:04d}"
|
| 234 |
+
result_path = run_dir / "metrics.json"
|
| 235 |
+
if result_path.exists():
|
| 236 |
+
existing = json.loads(result_path.read_text(encoding="utf-8"))
|
| 237 |
+
if existing.get("status") == "complete":
|
| 238 |
+
print(f"[skip] latent={latent_length} prompt={prompt_id}", flush=True)
|
| 239 |
+
continue
|
| 240 |
+
print(f"[run] latent={latent_length} prompt={prompt_id} FFFF", flush=True)
|
| 241 |
+
reference_latent, ffff_counts = generate_rollout(
|
| 242 |
+
pipeline=pipeline, dataset_root=args.dataset_root,
|
| 243 |
+
prompt_id=prompt_id, latent_length=latent_length,
|
| 244 |
+
generation_seed=args.generation_seed, device=device,
|
| 245 |
+
predictor=None, source_layer=None, schedule="FFFF",
|
| 246 |
+
)
|
| 247 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 248 |
+
reference_pixels = vae.decode_to_pixel(reference_latent, use_cache=False)
|
| 249 |
+
reference_u8 = pixels_to_u8(reference_pixels)
|
| 250 |
+
save_mp4(reference_u8, run_dir / "ffff.mp4")
|
| 251 |
+
del reference_latent, reference_pixels
|
| 252 |
+
if hasattr(vae.model, "clear_cache"):
|
| 253 |
+
vae.model.clear_cache()
|
| 254 |
+
torch.cuda.empty_cache()
|
| 255 |
+
|
| 256 |
+
print(f"[run] latent={latent_length} prompt={prompt_id} FPPF", flush=True)
|
| 257 |
+
prediction_latent, fppf_counts = generate_rollout(
|
| 258 |
+
pipeline=pipeline, dataset_root=args.dataset_root,
|
| 259 |
+
prompt_id=prompt_id, latent_length=latent_length,
|
| 260 |
+
generation_seed=args.generation_seed, device=device,
|
| 261 |
+
predictor=predictor, source_layer=17, schedule="FPPF",
|
| 262 |
+
)
|
| 263 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 264 |
+
prediction_pixels = vae.decode_to_pixel(prediction_latent, use_cache=False)
|
| 265 |
+
prediction_u8 = pixels_to_u8(prediction_pixels)
|
| 266 |
+
save_mp4(prediction_u8, run_dir / "fppf_layer17.mp4")
|
| 267 |
+
metrics = frame_metrics(
|
| 268 |
+
reference_u8=reference_u8, prediction_u8=prediction_u8,
|
| 269 |
+
lpips_model=lpips_model, batch_size=args.metric_batch_size,
|
| 270 |
+
device=device,
|
| 271 |
+
)
|
| 272 |
+
atomic_json(result_path, {
|
| 273 |
+
"status": "complete", "prompt_id": prompt_id, "prompt": prompt,
|
| 274 |
+
"latent_length": latent_length, "decoded_frames": metrics["num_frames"],
|
| 275 |
+
"reference": "FFFF same prompt/seed/noise",
|
| 276 |
+
"predictor": "single_block_teacher_layer_17",
|
| 277 |
+
"ffff": ffff_counts, "fppf": fppf_counts, **metrics,
|
| 278 |
+
})
|
| 279 |
+
print(
|
| 280 |
+
f"[result] latent={latent_length} prompt={prompt_id} "
|
| 281 |
+
f"psnr={metrics['psnr']:.4f} ssim={metrics['ssim']:.6f} "
|
| 282 |
+
f"lpips={metrics['lpips']:.6f}", flush=True,
|
| 283 |
+
)
|
| 284 |
+
del prediction_latent, prediction_pixels, reference_u8, prediction_u8
|
| 285 |
+
if hasattr(vae.model, "clear_cache"):
|
| 286 |
+
vae.model.clear_cache()
|
| 287 |
+
torch.cuda.empty_cache()
|
| 288 |
+
|
| 289 |
+
manifest = json.loads((args.output_dir / "manifest.json").read_text(encoding="utf-8"))
|
| 290 |
+
manifest["status"] = "complete"
|
| 291 |
+
atomic_json(args.output_dir / "manifest.json", manifest)
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
if __name__ == "__main__":
|
| 295 |
+
main()
|
scripts/evaluate_long_video_vbench.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run all standard VBench dimensions for one long-video condition."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import os
|
| 8 |
+
import tempfile
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def preparse_gpu() -> str:
|
| 13 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 14 |
+
parser.add_argument("--gpu", required=True)
|
| 15 |
+
args, _ = parser.parse_known_args()
|
| 16 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu
|
| 17 |
+
os.environ.setdefault("MPLCONFIGDIR", tempfile.mkdtemp(prefix="vbench_mpl_"))
|
| 18 |
+
return args.gpu
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
GPU = preparse_gpu()
|
| 22 |
+
|
| 23 |
+
from vbench import VBench
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
DIMENSIONS = [
|
| 27 |
+
"subject_consistency", "background_consistency", "motion_smoothness",
|
| 28 |
+
"aesthetic_quality", "imaging_quality",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def main() -> None:
|
| 33 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 34 |
+
parser.add_argument("--gpu", default=GPU)
|
| 35 |
+
parser.add_argument("--video_dir", type=Path, required=True)
|
| 36 |
+
parser.add_argument("--output_dir", type=Path, required=True)
|
| 37 |
+
parser.add_argument("--name", required=True)
|
| 38 |
+
args = parser.parse_args()
|
| 39 |
+
args.video_dir = args.video_dir.resolve()
|
| 40 |
+
args.output_dir = args.output_dir.resolve()
|
| 41 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 42 |
+
bench = VBench(
|
| 43 |
+
device="cuda", full_info_dir=str(args.video_dir / "full_info.json"),
|
| 44 |
+
output_path=str(args.output_dir),
|
| 45 |
+
)
|
| 46 |
+
bench.evaluate(
|
| 47 |
+
videos_path=str(args.video_dir), name=args.name,
|
| 48 |
+
dimension_list=DIMENSIONS, mode="custom_input",
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
main()
|
scripts/evaluate_single_block_fppf.py
ADDED
|
@@ -0,0 +1,1135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Evaluate trained one-block Predictors with an FPPF rollout against FFFF.
|
| 3 |
+
|
| 4 |
+
The evaluation uses the held-out offline prompt shards. FFFF clean latents
|
| 5 |
+
are decoded once and cached as 8-bit RGB reference frames. For every trained
|
| 6 |
+
Predictor, chunk 0 is generated with FFFF (there is no previous chunk), while
|
| 7 |
+
chunks 1..6 use Full-Predictor-Predictor-Full. PSNR, Gaussian SSIM, and
|
| 8 |
+
AlexNet LPIPS are computed frame by frame against the matching FFFF video.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import csv
|
| 15 |
+
import json
|
| 16 |
+
import math
|
| 17 |
+
import os
|
| 18 |
+
import sys
|
| 19 |
+
import time
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Any
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _preparse_gpu() -> str:
|
| 25 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 26 |
+
parser.add_argument("--gpu", default="2")
|
| 27 |
+
args, _ = parser.parse_known_args()
|
| 28 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 29 |
+
return str(args.gpu)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 33 |
+
|
| 34 |
+
import lpips
|
| 35 |
+
import torch
|
| 36 |
+
import torch.nn.functional as F
|
| 37 |
+
from omegaconf import OmegaConf
|
| 38 |
+
from safetensors import safe_open
|
| 39 |
+
from safetensors.torch import load_file, save_file
|
| 40 |
+
from torchvision.io import write_video
|
| 41 |
+
|
| 42 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 43 |
+
if str(REPO_ROOT) not in sys.path:
|
| 44 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 45 |
+
|
| 46 |
+
from pipeline import CausalInferencePipeline
|
| 47 |
+
from predictor_training.offline_data import TOKENS_PER_CHUNK
|
| 48 |
+
from predictor_training.single_block import (
|
| 49 |
+
SingleBlockPredictor,
|
| 50 |
+
initialize_predictor_block,
|
| 51 |
+
)
|
| 52 |
+
from scripts.run_single_block_init_sweep import hidden_to_flow
|
| 53 |
+
from utils.misc import set_seed
|
| 54 |
+
from utils.wan_wrapper import WanDiffusionWrapper, WanVAEWrapper
|
| 55 |
+
from wan.modules.model import sinusoidal_embedding_1d
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
LATENT_CHANNELS = 16
|
| 59 |
+
LATENT_HEIGHT = 60
|
| 60 |
+
LATENT_WIDTH = 104
|
| 61 |
+
FRAMES_PER_CHUNK = 3
|
| 62 |
+
NUM_CHUNKS = 7
|
| 63 |
+
NUM_DENOISING_STEPS = 4
|
| 64 |
+
PIXEL_FRAMES_FIRST_CHUNK = 1 + 4 * (FRAMES_PER_CHUNK - 1)
|
| 65 |
+
DEFAULT_PROMPT_IDS = list(range(80, 100))
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def parse_args() -> argparse.Namespace:
|
| 69 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 70 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 71 |
+
parser.add_argument(
|
| 72 |
+
"--config_path",
|
| 73 |
+
type=Path,
|
| 74 |
+
default=Path("configs/self_forcing_sid.yaml"),
|
| 75 |
+
)
|
| 76 |
+
parser.add_argument(
|
| 77 |
+
"--checkpoint_path",
|
| 78 |
+
type=Path,
|
| 79 |
+
default=Path("checkpoints/self_forcing_dmd.pt"),
|
| 80 |
+
)
|
| 81 |
+
parser.add_argument(
|
| 82 |
+
"--dataset_root",
|
| 83 |
+
type=Path,
|
| 84 |
+
default=Path("outputs/predictor_offline_100_all_blocks"),
|
| 85 |
+
)
|
| 86 |
+
parser.add_argument(
|
| 87 |
+
"--sweep_dir",
|
| 88 |
+
type=Path,
|
| 89 |
+
default=Path("outputs/single_block_init_sweep"),
|
| 90 |
+
)
|
| 91 |
+
parser.add_argument(
|
| 92 |
+
"--output_dir",
|
| 93 |
+
type=Path,
|
| 94 |
+
default=Path("outputs/single_block_fppf_eval"),
|
| 95 |
+
)
|
| 96 |
+
parser.add_argument(
|
| 97 |
+
"--prompt_ids", type=int, nargs="*", default=DEFAULT_PROMPT_IDS
|
| 98 |
+
)
|
| 99 |
+
parser.add_argument(
|
| 100 |
+
"--experiments",
|
| 101 |
+
nargs="*",
|
| 102 |
+
default=None,
|
| 103 |
+
help="Experiment directory names. Omit to evaluate all summary rows.",
|
| 104 |
+
)
|
| 105 |
+
parser.add_argument("--max_prompts", type=int, default=None)
|
| 106 |
+
parser.add_argument("--max_experiments", type=int, default=None)
|
| 107 |
+
parser.add_argument("--metric_batch_size", type=int, default=4)
|
| 108 |
+
parser.add_argument("--generation_seed", type=int, default=0)
|
| 109 |
+
parser.add_argument(
|
| 110 |
+
"--verify_ffff",
|
| 111 |
+
action=argparse.BooleanOptionalAction,
|
| 112 |
+
default=True,
|
| 113 |
+
help="Re-run FFFF once and compare its latent exactly to offline data.",
|
| 114 |
+
)
|
| 115 |
+
parser.add_argument(
|
| 116 |
+
"--skip_lpips",
|
| 117 |
+
action=argparse.BooleanOptionalAction,
|
| 118 |
+
default=False,
|
| 119 |
+
help="Only for quick diagnostics; formal evaluation should keep LPIPS.",
|
| 120 |
+
)
|
| 121 |
+
parser.add_argument(
|
| 122 |
+
"--rebuild_references",
|
| 123 |
+
action=argparse.BooleanOptionalAction,
|
| 124 |
+
default=False,
|
| 125 |
+
)
|
| 126 |
+
parser.add_argument(
|
| 127 |
+
"--save_videos",
|
| 128 |
+
action=argparse.BooleanOptionalAction,
|
| 129 |
+
default=False,
|
| 130 |
+
help="Save each FPPF prediction as a 16-fps H.264 MP4 for VBench.",
|
| 131 |
+
)
|
| 132 |
+
args = parser.parse_args()
|
| 133 |
+
if args.metric_batch_size < 1:
|
| 134 |
+
parser.error("--metric_batch_size must be positive")
|
| 135 |
+
if not args.prompt_ids:
|
| 136 |
+
parser.error("At least one prompt ID is required")
|
| 137 |
+
if any(value < 0 or value >= 100 for value in args.prompt_ids):
|
| 138 |
+
parser.error("Prompt IDs must be in [0, 99]")
|
| 139 |
+
return args
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def resolve(path: Path) -> Path:
|
| 143 |
+
path = path.expanduser()
|
| 144 |
+
return path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve()
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def atomic_json(path: Path, value: Any) -> None:
|
| 148 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 149 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 150 |
+
temporary.write_text(
|
| 151 |
+
json.dumps(value, indent=2, ensure_ascii=False, allow_nan=True) + "\n",
|
| 152 |
+
encoding="utf-8",
|
| 153 |
+
)
|
| 154 |
+
os.replace(temporary, path)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def atomic_safetensors(
|
| 158 |
+
path: Path, tensors: dict[str, torch.Tensor], metadata: dict[str, str]
|
| 159 |
+
) -> None:
|
| 160 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 161 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 162 |
+
save_file(tensors, temporary, metadata=metadata)
|
| 163 |
+
os.replace(temporary, path)
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def load_prompt_metadata(dataset_root: Path, prompt_id: int) -> dict[str, Any]:
|
| 167 |
+
path = dataset_root / f"prompt_{prompt_id:04d}" / "metadata.json"
|
| 168 |
+
return json.loads(path.read_text(encoding="utf-8"))
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def load_ffff_latent(dataset_root: Path, prompt_id: int) -> torch.Tensor:
|
| 172 |
+
path = dataset_root / f"prompt_{prompt_id:04d}" / "trajectory.safetensors"
|
| 173 |
+
with safe_open(path, framework="pt", device="cpu") as handle:
|
| 174 |
+
chunks = [
|
| 175 |
+
handle.get_tensor(f"chunk_{chunk:02d}_clean_latent")
|
| 176 |
+
for chunk in range(NUM_CHUNKS)
|
| 177 |
+
]
|
| 178 |
+
return torch.cat(chunks, dim=1).contiguous()
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def pixels_to_u8(video: torch.Tensor) -> torch.Tensor:
|
| 182 |
+
"""Convert [1,T,3,H,W] pixels in [-1,1] to CPU uint8 frames."""
|
| 183 |
+
return (
|
| 184 |
+
((video.squeeze(0).float() + 1.0) * 127.5)
|
| 185 |
+
.round_()
|
| 186 |
+
.clamp_(0, 255)
|
| 187 |
+
.to(device="cpu", dtype=torch.uint8)
|
| 188 |
+
.contiguous()
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def save_mp4(frames: torch.Tensor, path: Path) -> None:
|
| 193 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 194 |
+
write_video(
|
| 195 |
+
str(path), frames.permute(0, 2, 3, 1), fps=16,
|
| 196 |
+
video_codec="libx264", options={"crf": "18"},
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
@torch.inference_mode()
|
| 201 |
+
def prepare_reference_frames(
|
| 202 |
+
*,
|
| 203 |
+
vae: WanVAEWrapper,
|
| 204 |
+
dataset_root: Path,
|
| 205 |
+
output_dir: Path,
|
| 206 |
+
prompt_ids: list[int],
|
| 207 |
+
device: torch.device,
|
| 208 |
+
rebuild: bool,
|
| 209 |
+
) -> None:
|
| 210 |
+
reference_dir = output_dir / "ffff_reference_frames"
|
| 211 |
+
reference_dir.mkdir(parents=True, exist_ok=True)
|
| 212 |
+
for offset, prompt_id in enumerate(prompt_ids, start=1):
|
| 213 |
+
destination = reference_dir / f"prompt_{prompt_id:04d}.safetensors"
|
| 214 |
+
if destination.exists() and not rebuild:
|
| 215 |
+
print(
|
| 216 |
+
f"[reference] {offset}/{len(prompt_ids)} prompt={prompt_id} cached",
|
| 217 |
+
flush=True,
|
| 218 |
+
)
|
| 219 |
+
continue
|
| 220 |
+
latent = load_ffff_latent(dataset_root, prompt_id).to(
|
| 221 |
+
device=device, dtype=torch.bfloat16
|
| 222 |
+
)
|
| 223 |
+
started = time.perf_counter()
|
| 224 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 225 |
+
pixels = vae.decode_to_pixel(latent, use_cache=False)
|
| 226 |
+
frames = pixels_to_u8(pixels)
|
| 227 |
+
atomic_safetensors(
|
| 228 |
+
destination,
|
| 229 |
+
{"frames": frames},
|
| 230 |
+
{
|
| 231 |
+
"reference": "FFFF",
|
| 232 |
+
"prompt_id": str(prompt_id),
|
| 233 |
+
"range": "uint8_0_255",
|
| 234 |
+
"layout": "TCHW",
|
| 235 |
+
},
|
| 236 |
+
)
|
| 237 |
+
if hasattr(vae.model, "clear_cache"):
|
| 238 |
+
vae.model.clear_cache()
|
| 239 |
+
del latent, pixels, frames
|
| 240 |
+
torch.cuda.empty_cache()
|
| 241 |
+
print(
|
| 242 |
+
f"[reference] {offset}/{len(prompt_ids)} prompt={prompt_id} "
|
| 243 |
+
f"decoded={time.perf_counter() - started:.1f}s",
|
| 244 |
+
flush=True,
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def load_reference_frames(output_dir: Path, prompt_id: int) -> torch.Tensor:
|
| 249 |
+
path = output_dir / "ffff_reference_frames" / f"prompt_{prompt_id:04d}.safetensors"
|
| 250 |
+
with safe_open(path, framework="pt", device="cpu") as handle:
|
| 251 |
+
return handle.get_tensor("frames")
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def discover_experiments(
|
| 255 |
+
sweep_dir: Path,
|
| 256 |
+
requested: list[str] | None,
|
| 257 |
+
max_experiments: int | None,
|
| 258 |
+
) -> list[dict[str, Any]]:
|
| 259 |
+
summary_path = sweep_dir / "summary.csv"
|
| 260 |
+
with summary_path.open("r", encoding="utf-8", newline="") as handle:
|
| 261 |
+
rows = list(csv.DictReader(handle))
|
| 262 |
+
by_name = {row["name"]: row for row in rows}
|
| 263 |
+
names = list(by_name) if requested is None else requested
|
| 264 |
+
unknown = [name for name in names if name not in by_name]
|
| 265 |
+
if unknown:
|
| 266 |
+
raise KeyError(f"Unknown sweep experiments: {unknown}")
|
| 267 |
+
if max_experiments is not None:
|
| 268 |
+
names = names[:max_experiments]
|
| 269 |
+
|
| 270 |
+
experiments = []
|
| 271 |
+
for name in names:
|
| 272 |
+
run_dir = sweep_dir / name
|
| 273 |
+
config = json.loads((run_dir / "config.json").read_text(encoding="utf-8"))
|
| 274 |
+
weights = run_dir / "predictor_final.safetensors"
|
| 275 |
+
if not weights.exists():
|
| 276 |
+
raise FileNotFoundError(weights)
|
| 277 |
+
row = by_name[name]
|
| 278 |
+
experiment = {
|
| 279 |
+
"name": name,
|
| 280 |
+
"initialization_method": config["initialization_method"],
|
| 281 |
+
"source_layer": int(config["source_layer"]),
|
| 282 |
+
"weights": weights,
|
| 283 |
+
"gate_mode": config.get("gate_mode", "baseline"),
|
| 284 |
+
"gate_hidden_dim": int(config.get("gate_hidden_dim", 128)),
|
| 285 |
+
"gate_initial_bias": float(
|
| 286 |
+
config.get("gate_initial_bias", 4.6)
|
| 287 |
+
),
|
| 288 |
+
"gate_floor": float(config.get("gate_floor", 0.0)),
|
| 289 |
+
"constant_gate": float(config.get("constant_gate", 1.0)),
|
| 290 |
+
"gate_override": config.get("gate_override"),
|
| 291 |
+
"offline_final_val_flow_mse": float(row["final_val_flow_mse"]),
|
| 292 |
+
"offline_final_val_hidden_mse": float(row["final_val_hidden_mse"]),
|
| 293 |
+
}
|
| 294 |
+
experiments.append(experiment)
|
| 295 |
+
if experiment["gate_mode"] == "learned":
|
| 296 |
+
training_metrics = json.loads(
|
| 297 |
+
(run_dir / "metrics.json").read_text(encoding="utf-8")
|
| 298 |
+
)
|
| 299 |
+
gate_mean = float(training_metrics["evaluations"][-1]["gate_mean"])
|
| 300 |
+
experiments.append(
|
| 301 |
+
{
|
| 302 |
+
**experiment,
|
| 303 |
+
"name": f"{name}_constant_mean",
|
| 304 |
+
"gate_override": gate_mean,
|
| 305 |
+
"constant_gate": gate_mean,
|
| 306 |
+
}
|
| 307 |
+
)
|
| 308 |
+
return experiments
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def build_pipeline(
|
| 312 |
+
config: Any,
|
| 313 |
+
checkpoint_path: Path,
|
| 314 |
+
vae: WanVAEWrapper,
|
| 315 |
+
device: torch.device,
|
| 316 |
+
) -> CausalInferencePipeline:
|
| 317 |
+
generator = WanDiffusionWrapper(
|
| 318 |
+
**getattr(config, "model_kwargs", {}), is_causal=True
|
| 319 |
+
)
|
| 320 |
+
pipeline = CausalInferencePipeline(
|
| 321 |
+
config,
|
| 322 |
+
device=device,
|
| 323 |
+
generator=generator,
|
| 324 |
+
text_encoder=torch.nn.Identity(),
|
| 325 |
+
vae=vae,
|
| 326 |
+
)
|
| 327 |
+
checkpoint = torch.load(
|
| 328 |
+
checkpoint_path, map_location="cpu", weights_only=False, mmap=True
|
| 329 |
+
)
|
| 330 |
+
if set(checkpoint) != {"generator_ema"}:
|
| 331 |
+
raise KeyError(f"Unexpected Teacher checkpoint keys: {sorted(checkpoint)}")
|
| 332 |
+
pipeline.generator.load_state_dict(checkpoint["generator_ema"], strict=True)
|
| 333 |
+
del checkpoint
|
| 334 |
+
pipeline.to(dtype=torch.bfloat16)
|
| 335 |
+
pipeline.generator.to(device=device)
|
| 336 |
+
pipeline.eval()
|
| 337 |
+
pipeline.generator.requires_grad_(False)
|
| 338 |
+
return pipeline
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
def reset_kv_and_load_cross_cache(
|
| 342 |
+
pipeline: CausalInferencePipeline,
|
| 343 |
+
dataset_root: Path,
|
| 344 |
+
prompt_id: int,
|
| 345 |
+
device: torch.device,
|
| 346 |
+
) -> None:
|
| 347 |
+
if pipeline.kv_cache1 is None:
|
| 348 |
+
pipeline._initialize_kv_cache(1, torch.bfloat16, device)
|
| 349 |
+
pipeline._initialize_crossattn_cache(1, torch.bfloat16, device)
|
| 350 |
+
for cache in pipeline.kv_cache1:
|
| 351 |
+
cache["global_end_index"].zero_()
|
| 352 |
+
cache["local_end_index"].zero_()
|
| 353 |
+
|
| 354 |
+
cross_path = (
|
| 355 |
+
dataset_root / f"prompt_{prompt_id:04d}" / "cross_attention.safetensors"
|
| 356 |
+
)
|
| 357 |
+
with safe_open(cross_path, framework="pt", device="cpu") as handle:
|
| 358 |
+
for layer, cache in enumerate(pipeline.crossattn_cache):
|
| 359 |
+
cache["k"] = handle.get_tensor(f"block_{layer:02d}_k").to(
|
| 360 |
+
device=device, dtype=torch.bfloat16
|
| 361 |
+
)
|
| 362 |
+
cache["v"] = handle.get_tensor(f"block_{layer:02d}_v").to(
|
| 363 |
+
device=device, dtype=torch.bfloat16
|
| 364 |
+
)
|
| 365 |
+
cache["is_init"] = True
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
class FinalHiddenCapture:
|
| 369 |
+
def __init__(self, teacher: torch.nn.Module) -> None:
|
| 370 |
+
self.enabled = False
|
| 371 |
+
self.value: torch.Tensor | None = None
|
| 372 |
+
self.handle = teacher.head.register_forward_pre_hook(self._hook)
|
| 373 |
+
|
| 374 |
+
def close(self) -> None:
|
| 375 |
+
self.handle.remove()
|
| 376 |
+
|
| 377 |
+
def _hook(
|
| 378 |
+
self, _module: torch.nn.Module, inputs: tuple[torch.Tensor, ...]
|
| 379 |
+
) -> None:
|
| 380 |
+
if self.enabled:
|
| 381 |
+
if self.value is not None:
|
| 382 |
+
raise RuntimeError("Teacher head was called twice in one Full step")
|
| 383 |
+
self.value = inputs[0].detach()
|
| 384 |
+
|
| 385 |
+
def start(self) -> None:
|
| 386 |
+
self.value = None
|
| 387 |
+
self.enabled = True
|
| 388 |
+
|
| 389 |
+
def finish(self) -> torch.Tensor:
|
| 390 |
+
self.enabled = False
|
| 391 |
+
if self.value is None:
|
| 392 |
+
raise RuntimeError("Teacher final hidden was not captured")
|
| 393 |
+
value = self.value
|
| 394 |
+
self.value = None
|
| 395 |
+
return value
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def load_predictor(
|
| 399 |
+
teacher: torch.nn.Module,
|
| 400 |
+
experiment: dict[str, Any],
|
| 401 |
+
device: torch.device,
|
| 402 |
+
) -> SingleBlockPredictor:
|
| 403 |
+
source_layer = int(experiment["source_layer"])
|
| 404 |
+
block = initialize_predictor_block(
|
| 405 |
+
teacher.blocks[source_layer], "teacher_full"
|
| 406 |
+
)
|
| 407 |
+
predictor = SingleBlockPredictor(
|
| 408 |
+
block=block,
|
| 409 |
+
dim=teacher.dim,
|
| 410 |
+
gradient_checkpointing=False,
|
| 411 |
+
input_variant=experiment.get("input_variant", "self_forcing"),
|
| 412 |
+
gate_mode=experiment.get("gate_mode", "baseline"),
|
| 413 |
+
gate_hidden_dim=int(experiment.get("gate_hidden_dim", 128)),
|
| 414 |
+
gate_initial_bias=float(experiment.get("gate_initial_bias", 4.6)),
|
| 415 |
+
gate_floor=float(experiment.get("gate_floor", 0.0)),
|
| 416 |
+
constant_gate=float(experiment.get("constant_gate", 1.0)),
|
| 417 |
+
atc_previous_scope=experiment.get("atc_previous_scope", "chunk"),
|
| 418 |
+
atc_freq_dim=int(experiment.get("atc_freq_dim", 256)),
|
| 419 |
+
atc_mlp_hidden_dim=int(experiment.get("atc_mlp_hidden_dim", 3072)),
|
| 420 |
+
atc_gate_hidden_dim=int(experiment.get("atc_gate_hidden_dim", 512)),
|
| 421 |
+
atc_transport_residual_scale=float(
|
| 422 |
+
experiment.get("atc_transport_residual_scale", 0.1)
|
| 423 |
+
),
|
| 424 |
+
atc_gate_initial_probability=float(
|
| 425 |
+
experiment.get("atc_gate_initial_probability", 0.3)
|
| 426 |
+
),
|
| 427 |
+
atc_collect_diagnostics=bool(
|
| 428 |
+
experiment.get("atc_collect_diagnostics", False)
|
| 429 |
+
),
|
| 430 |
+
)
|
| 431 |
+
state = load_file(str(experiment["weights"]), device="cpu")
|
| 432 |
+
predictor.load_state_dict(state, strict=True)
|
| 433 |
+
if experiment.get("gate_override") is not None:
|
| 434 |
+
predictor.fusion.gate_override = float(experiment["gate_override"])
|
| 435 |
+
predictor.to(device=device)
|
| 436 |
+
predictor.eval().requires_grad_(False)
|
| 437 |
+
return predictor
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
@torch.inference_mode()
|
| 441 |
+
def predictor_step(
|
| 442 |
+
*,
|
| 443 |
+
predictor: SingleBlockPredictor,
|
| 444 |
+
teacher: torch.nn.Module,
|
| 445 |
+
noisy_input: torch.Tensor,
|
| 446 |
+
timestep: torch.Tensor,
|
| 447 |
+
anchor_hidden: torch.Tensor,
|
| 448 |
+
previous_hidden: torch.Tensor,
|
| 449 |
+
history_cache: dict[str, torch.Tensor],
|
| 450 |
+
cross_cache: dict[str, torch.Tensor],
|
| 451 |
+
current_start: int,
|
| 452 |
+
anchor_timestep: torch.Tensor | None = None,
|
| 453 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 454 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 455 |
+
current_tokens = teacher.patch_embedding(
|
| 456 |
+
noisy_input.permute(0, 2, 1, 3, 4)
|
| 457 |
+
).flatten(2).transpose(1, 2)
|
| 458 |
+
time_embedding = teacher.time_embedding(
|
| 459 |
+
sinusoidal_embedding_1d(
|
| 460 |
+
teacher.freq_dim, timestep.flatten()
|
| 461 |
+
).type_as(current_tokens)
|
| 462 |
+
)
|
| 463 |
+
timestep_modulation = teacher.time_projection(
|
| 464 |
+
time_embedding
|
| 465 |
+
).unflatten(1, (6, teacher.dim)).unflatten(
|
| 466 |
+
dim=0, sizes=timestep.shape
|
| 467 |
+
)
|
| 468 |
+
head_embedding = time_embedding.unflatten(
|
| 469 |
+
dim=0, sizes=timestep.shape
|
| 470 |
+
).unsqueeze(2)
|
| 471 |
+
condition_per_frame = time_embedding.unflatten(
|
| 472 |
+
dim=0, sizes=timestep.shape
|
| 473 |
+
)
|
| 474 |
+
condition_tokens = (
|
| 475 |
+
condition_per_frame[:, :, None, :]
|
| 476 |
+
.expand(
|
| 477 |
+
timestep.shape[0],
|
| 478 |
+
timestep.shape[1],
|
| 479 |
+
30 * 52,
|
| 480 |
+
teacher.dim,
|
| 481 |
+
)
|
| 482 |
+
.reshape(timestep.shape[0], -1, teacher.dim)
|
| 483 |
+
)
|
| 484 |
+
anchor_distance = None
|
| 485 |
+
if predictor.input_variant == "atc":
|
| 486 |
+
if anchor_timestep is None:
|
| 487 |
+
raise ValueError("ATC inference requires anchor_timestep")
|
| 488 |
+
anchor_distance = (
|
| 489 |
+
timestep.float() - anchor_timestep.float()
|
| 490 |
+
).abs().mean(dim=1)
|
| 491 |
+
grid_sizes = torch.tensor(
|
| 492 |
+
[[FRAMES_PER_CHUNK, 30, 52]], dtype=torch.long, device="cpu"
|
| 493 |
+
)
|
| 494 |
+
history_length = int(history_cache["local_end_index"].item())
|
| 495 |
+
pred_hidden = predictor(
|
| 496 |
+
current_tokens=current_tokens,
|
| 497 |
+
anchor_hidden=anchor_hidden,
|
| 498 |
+
previous_hidden=previous_hidden,
|
| 499 |
+
timestep_modulation=timestep_modulation,
|
| 500 |
+
grid_sizes=grid_sizes,
|
| 501 |
+
freqs=teacher.freqs,
|
| 502 |
+
history_k=history_cache["k"][:, :history_length],
|
| 503 |
+
history_v=history_cache["v"][:, :history_length],
|
| 504 |
+
cross_k=cross_cache["k"],
|
| 505 |
+
cross_v=cross_cache["v"],
|
| 506 |
+
current_start=current_start,
|
| 507 |
+
condition_tokens=condition_tokens,
|
| 508 |
+
anchor_distance=anchor_distance,
|
| 509 |
+
)
|
| 510 |
+
pred_flow = hidden_to_flow(
|
| 511 |
+
pred_hidden, head_embedding, grid_sizes, teacher
|
| 512 |
+
)
|
| 513 |
+
return pred_hidden, pred_flow, current_tokens
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
@torch.inference_mode()
|
| 517 |
+
def generate_rollout(
|
| 518 |
+
*,
|
| 519 |
+
pipeline: CausalInferencePipeline,
|
| 520 |
+
dataset_root: Path,
|
| 521 |
+
prompt_id: int,
|
| 522 |
+
generation_seed: int,
|
| 523 |
+
device: torch.device,
|
| 524 |
+
predictor: SingleBlockPredictor | None,
|
| 525 |
+
source_layer: int | None,
|
| 526 |
+
schedule: str,
|
| 527 |
+
) -> tuple[torch.Tensor, dict[str, float | int]]:
|
| 528 |
+
if schedule not in {"FFFF", "FPPF"}:
|
| 529 |
+
raise ValueError(schedule)
|
| 530 |
+
if schedule == "FPPF" and (predictor is None or source_layer is None):
|
| 531 |
+
raise ValueError("FPPF requires a Predictor and source layer")
|
| 532 |
+
|
| 533 |
+
reset_kv_and_load_cross_cache(pipeline, dataset_root, prompt_id, device)
|
| 534 |
+
set_seed(generation_seed)
|
| 535 |
+
noise = torch.randn(
|
| 536 |
+
1,
|
| 537 |
+
NUM_CHUNKS * FRAMES_PER_CHUNK,
|
| 538 |
+
LATENT_CHANNELS,
|
| 539 |
+
LATENT_HEIGHT,
|
| 540 |
+
LATENT_WIDTH,
|
| 541 |
+
dtype=torch.bfloat16,
|
| 542 |
+
device=device,
|
| 543 |
+
)
|
| 544 |
+
teacher = pipeline.generator.model
|
| 545 |
+
text_dim = int(teacher.text_embedding[0].in_features)
|
| 546 |
+
conditional_dict = {
|
| 547 |
+
"prompt_embeds": torch.zeros(
|
| 548 |
+
1, 1, text_dim, dtype=torch.bfloat16, device=device
|
| 549 |
+
)
|
| 550 |
+
}
|
| 551 |
+
timesteps = pipeline.denoising_step_list.to(device=device)
|
| 552 |
+
output_chunks: list[torch.Tensor] = []
|
| 553 |
+
previous_chunk_hidden: list[torch.Tensor | None] | None = None
|
| 554 |
+
capture = FinalHiddenCapture(teacher)
|
| 555 |
+
full_calls = 0
|
| 556 |
+
predictor_calls = 0
|
| 557 |
+
started = time.perf_counter()
|
| 558 |
+
|
| 559 |
+
try:
|
| 560 |
+
for chunk in range(NUM_CHUNKS):
|
| 561 |
+
noisy_input = noise[
|
| 562 |
+
:, chunk * FRAMES_PER_CHUNK : (chunk + 1) * FRAMES_PER_CHUNK
|
| 563 |
+
]
|
| 564 |
+
current_hidden: list[torch.Tensor | None] = [None] * NUM_DENOISING_STEPS
|
| 565 |
+
denoised_pred: torch.Tensor | None = None
|
| 566 |
+
timestep: torch.Tensor | None = None
|
| 567 |
+
for step, current_timestep in enumerate(timesteps):
|
| 568 |
+
timestep = torch.ones(
|
| 569 |
+
[1, FRAMES_PER_CHUNK], dtype=torch.int64, device=device
|
| 570 |
+
) * current_timestep
|
| 571 |
+
use_predictor = schedule == "FPPF" and chunk > 0 and step in {1, 2}
|
| 572 |
+
|
| 573 |
+
if use_predictor:
|
| 574 |
+
anchor_hidden = current_hidden[step - 1]
|
| 575 |
+
assert anchor_hidden is not None
|
| 576 |
+
assert previous_chunk_hidden is not None
|
| 577 |
+
previous_hidden = previous_chunk_hidden[step]
|
| 578 |
+
assert previous_hidden is not None
|
| 579 |
+
history = pipeline.kv_cache1[int(source_layer)]
|
| 580 |
+
cross = pipeline.crossattn_cache[int(source_layer)]
|
| 581 |
+
pred_hidden, flow, _ = predictor_step(
|
| 582 |
+
predictor=predictor,
|
| 583 |
+
teacher=teacher,
|
| 584 |
+
noisy_input=noisy_input,
|
| 585 |
+
timestep=timestep,
|
| 586 |
+
anchor_hidden=anchor_hidden,
|
| 587 |
+
previous_hidden=previous_hidden,
|
| 588 |
+
history_cache=history,
|
| 589 |
+
cross_cache=cross,
|
| 590 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 591 |
+
anchor_timestep=(
|
| 592 |
+
torch.ones_like(timestep) * timesteps[step - 1]
|
| 593 |
+
),
|
| 594 |
+
)
|
| 595 |
+
denoised_pred = pipeline.generator._convert_flow_pred_to_x0(
|
| 596 |
+
flow_pred=flow.flatten(0, 1),
|
| 597 |
+
xt=noisy_input.flatten(0, 1),
|
| 598 |
+
timestep=timestep.flatten(0, 1),
|
| 599 |
+
).unflatten(0, flow.shape[:2])
|
| 600 |
+
current_hidden[step] = pred_hidden
|
| 601 |
+
predictor_calls += 1
|
| 602 |
+
else:
|
| 603 |
+
capture.start()
|
| 604 |
+
_, denoised_pred = pipeline.generator(
|
| 605 |
+
noisy_image_or_video=noisy_input,
|
| 606 |
+
conditional_dict=conditional_dict,
|
| 607 |
+
timestep=timestep,
|
| 608 |
+
kv_cache=pipeline.kv_cache1,
|
| 609 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 610 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 611 |
+
)
|
| 612 |
+
current_hidden[step] = capture.finish()
|
| 613 |
+
full_calls += 1
|
| 614 |
+
|
| 615 |
+
if step < NUM_DENOISING_STEPS - 1:
|
| 616 |
+
next_timestep = timesteps[step + 1]
|
| 617 |
+
denoised_flat = denoised_pred.flatten(0, 1)
|
| 618 |
+
noisy_input = pipeline.scheduler.add_noise(
|
| 619 |
+
denoised_flat,
|
| 620 |
+
torch.randn_like(denoised_flat),
|
| 621 |
+
next_timestep
|
| 622 |
+
* torch.ones(
|
| 623 |
+
[FRAMES_PER_CHUNK], dtype=torch.long, device=device
|
| 624 |
+
),
|
| 625 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 626 |
+
|
| 627 |
+
if denoised_pred is None or timestep is None:
|
| 628 |
+
raise RuntimeError("Denoising loop produced no clean latent")
|
| 629 |
+
output_chunks.append(denoised_pred)
|
| 630 |
+
|
| 631 |
+
context_timestep = torch.ones_like(timestep) * pipeline.args.context_noise
|
| 632 |
+
pipeline.generator(
|
| 633 |
+
noisy_image_or_video=denoised_pred,
|
| 634 |
+
conditional_dict=conditional_dict,
|
| 635 |
+
timestep=context_timestep,
|
| 636 |
+
kv_cache=pipeline.kv_cache1,
|
| 637 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 638 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 639 |
+
)
|
| 640 |
+
previous_chunk_hidden = current_hidden
|
| 641 |
+
finally:
|
| 642 |
+
capture.close()
|
| 643 |
+
|
| 644 |
+
torch.cuda.synchronize()
|
| 645 |
+
return torch.cat(output_chunks, dim=1), {
|
| 646 |
+
"generation_time_s": time.perf_counter() - started,
|
| 647 |
+
"full_calls": full_calls,
|
| 648 |
+
"predictor_calls": predictor_calls,
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
def gaussian_kernel(
|
| 653 |
+
device: torch.device, dtype: torch.dtype, channels: int = 3
|
| 654 |
+
) -> torch.Tensor:
|
| 655 |
+
coordinates = torch.arange(11, device=device, dtype=dtype) - 5
|
| 656 |
+
kernel_1d = torch.exp(-(coordinates.square()) / (2 * 1.5**2))
|
| 657 |
+
kernel_1d /= kernel_1d.sum()
|
| 658 |
+
kernel_2d = torch.outer(kernel_1d, kernel_1d)
|
| 659 |
+
return kernel_2d.expand(channels, 1, 11, 11).contiguous()
|
| 660 |
+
|
| 661 |
+
|
| 662 |
+
def ssim_per_frame(
|
| 663 |
+
reference: torch.Tensor, prediction: torch.Tensor, kernel: torch.Tensor
|
| 664 |
+
) -> torch.Tensor:
|
| 665 |
+
channels = reference.shape[1]
|
| 666 |
+
mu_x = F.conv2d(reference, kernel, groups=channels)
|
| 667 |
+
mu_y = F.conv2d(prediction, kernel, groups=channels)
|
| 668 |
+
mu_x2 = mu_x.square()
|
| 669 |
+
mu_y2 = mu_y.square()
|
| 670 |
+
mu_xy = mu_x * mu_y
|
| 671 |
+
sigma_x2 = F.conv2d(reference.square(), kernel, groups=channels) - mu_x2
|
| 672 |
+
sigma_y2 = F.conv2d(prediction.square(), kernel, groups=channels) - mu_y2
|
| 673 |
+
sigma_xy = F.conv2d(reference * prediction, kernel, groups=channels) - mu_xy
|
| 674 |
+
c1 = 0.01**2
|
| 675 |
+
c2 = 0.03**2
|
| 676 |
+
score = ((2 * mu_xy + c1) * (2 * sigma_xy + c2)) / (
|
| 677 |
+
(mu_x2 + mu_y2 + c1) * (sigma_x2 + sigma_y2 + c2)
|
| 678 |
+
)
|
| 679 |
+
return score.mean(dim=(1, 2, 3))
|
| 680 |
+
|
| 681 |
+
|
| 682 |
+
@torch.inference_mode()
|
| 683 |
+
def frame_metrics(
|
| 684 |
+
*,
|
| 685 |
+
reference_u8: torch.Tensor,
|
| 686 |
+
prediction_u8: torch.Tensor,
|
| 687 |
+
lpips_model: torch.nn.Module | None,
|
| 688 |
+
batch_size: int,
|
| 689 |
+
device: torch.device,
|
| 690 |
+
) -> dict[str, Any]:
|
| 691 |
+
if reference_u8.shape != prediction_u8.shape:
|
| 692 |
+
raise ValueError(
|
| 693 |
+
f"Reference/prediction shapes differ: {reference_u8.shape}, "
|
| 694 |
+
f"{prediction_u8.shape}"
|
| 695 |
+
)
|
| 696 |
+
kernel = gaussian_kernel(device, torch.float32)
|
| 697 |
+
psnr_values: list[float] = []
|
| 698 |
+
mse_values: list[float] = []
|
| 699 |
+
ssim_values: list[float] = []
|
| 700 |
+
lpips_values: list[float] = []
|
| 701 |
+
for start in range(0, reference_u8.shape[0], batch_size):
|
| 702 |
+
end = min(start + batch_size, reference_u8.shape[0])
|
| 703 |
+
reference = reference_u8[start:end].to(
|
| 704 |
+
device=device, dtype=torch.float32
|
| 705 |
+
) / 255.0
|
| 706 |
+
prediction = prediction_u8[start:end].to(
|
| 707 |
+
device=device, dtype=torch.float32
|
| 708 |
+
) / 255.0
|
| 709 |
+
mse = (reference - prediction).square().mean(dim=(1, 2, 3))
|
| 710 |
+
psnr = -10.0 * torch.log10(mse.clamp_min(1e-12))
|
| 711 |
+
ssim = ssim_per_frame(reference, prediction, kernel)
|
| 712 |
+
mse_values.extend(float(value) for value in mse.cpu())
|
| 713 |
+
psnr_values.extend(float(value) for value in psnr.cpu())
|
| 714 |
+
ssim_values.extend(float(value) for value in ssim.cpu())
|
| 715 |
+
if lpips_model is not None:
|
| 716 |
+
distance = lpips_model(
|
| 717 |
+
reference.mul(2).sub(1), prediction.mul(2).sub(1)
|
| 718 |
+
).flatten()
|
| 719 |
+
lpips_values.extend(float(value) for value in distance.cpu())
|
| 720 |
+
del reference, prediction, mse, psnr, ssim
|
| 721 |
+
global_mse = sum(mse_values) / len(mse_values)
|
| 722 |
+
rollout_mse = sum(mse_values[PIXEL_FRAMES_FIRST_CHUNK:]) / len(
|
| 723 |
+
mse_values[PIXEL_FRAMES_FIRST_CHUNK:]
|
| 724 |
+
)
|
| 725 |
+
return {
|
| 726 |
+
"mse_per_frame": mse_values,
|
| 727 |
+
"psnr_per_frame": psnr_values,
|
| 728 |
+
"ssim_per_frame": ssim_values,
|
| 729 |
+
"lpips_per_frame": lpips_values,
|
| 730 |
+
"pixel_mse": global_mse,
|
| 731 |
+
"psnr": -10.0 * math.log10(max(global_mse, 1e-12)),
|
| 732 |
+
"psnr_frame_mean": sum(psnr_values) / len(psnr_values),
|
| 733 |
+
"ssim": sum(ssim_values) / len(ssim_values),
|
| 734 |
+
"lpips": (
|
| 735 |
+
sum(lpips_values) / len(lpips_values)
|
| 736 |
+
if lpips_values
|
| 737 |
+
else None
|
| 738 |
+
),
|
| 739 |
+
"rollout_start_frame": PIXEL_FRAMES_FIRST_CHUNK,
|
| 740 |
+
"rollout_pixel_mse": rollout_mse,
|
| 741 |
+
"rollout_psnr": -10.0 * math.log10(max(rollout_mse, 1e-12)),
|
| 742 |
+
"rollout_ssim": sum(ssim_values[PIXEL_FRAMES_FIRST_CHUNK:])
|
| 743 |
+
/ len(ssim_values[PIXEL_FRAMES_FIRST_CHUNK:]),
|
| 744 |
+
"rollout_lpips": (
|
| 745 |
+
sum(lpips_values[PIXEL_FRAMES_FIRST_CHUNK:])
|
| 746 |
+
/ len(lpips_values[PIXEL_FRAMES_FIRST_CHUNK:])
|
| 747 |
+
if lpips_values
|
| 748 |
+
else None
|
| 749 |
+
),
|
| 750 |
+
"num_frames": len(psnr_values),
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
|
| 754 |
+
def mean_std(values: list[float]) -> tuple[float, float]:
|
| 755 |
+
mean = sum(values) / len(values)
|
| 756 |
+
variance = sum((value - mean) ** 2 for value in values) / len(values)
|
| 757 |
+
return mean, math.sqrt(variance)
|
| 758 |
+
|
| 759 |
+
|
| 760 |
+
def aggregate_prompt_results(
|
| 761 |
+
experiment: dict[str, Any], prompt_results: list[dict[str, Any]]
|
| 762 |
+
) -> dict[str, Any]:
|
| 763 |
+
mse_frames = [
|
| 764 |
+
value
|
| 765 |
+
for result in prompt_results
|
| 766 |
+
for value in result["mse_per_frame"]
|
| 767 |
+
]
|
| 768 |
+
psnr_frames = [
|
| 769 |
+
value
|
| 770 |
+
for result in prompt_results
|
| 771 |
+
for value in result["psnr_per_frame"]
|
| 772 |
+
]
|
| 773 |
+
ssim_frames = [
|
| 774 |
+
value
|
| 775 |
+
for result in prompt_results
|
| 776 |
+
for value in result["ssim_per_frame"]
|
| 777 |
+
]
|
| 778 |
+
lpips_frames = [
|
| 779 |
+
value
|
| 780 |
+
for result in prompt_results
|
| 781 |
+
for value in result["lpips_per_frame"]
|
| 782 |
+
]
|
| 783 |
+
rollout_mse_frames = [
|
| 784 |
+
value
|
| 785 |
+
for result in prompt_results
|
| 786 |
+
for value in result["mse_per_frame"][PIXEL_FRAMES_FIRST_CHUNK:]
|
| 787 |
+
]
|
| 788 |
+
rollout_ssim_frames = [
|
| 789 |
+
value
|
| 790 |
+
for result in prompt_results
|
| 791 |
+
for value in result["ssim_per_frame"][PIXEL_FRAMES_FIRST_CHUNK:]
|
| 792 |
+
]
|
| 793 |
+
rollout_lpips_frames = [
|
| 794 |
+
value
|
| 795 |
+
for result in prompt_results
|
| 796 |
+
for value in result["lpips_per_frame"][PIXEL_FRAMES_FIRST_CHUNK:]
|
| 797 |
+
]
|
| 798 |
+
pixel_mse = sum(mse_frames) / len(mse_frames)
|
| 799 |
+
psnr_frame_mean, psnr_std = mean_std(psnr_frames)
|
| 800 |
+
ssim, ssim_std = mean_std(ssim_frames)
|
| 801 |
+
if lpips_frames:
|
| 802 |
+
lpips_mean, lpips_std = mean_std(lpips_frames)
|
| 803 |
+
else:
|
| 804 |
+
lpips_mean, lpips_std = None, None
|
| 805 |
+
rollout_pixel_mse = sum(rollout_mse_frames) / len(rollout_mse_frames)
|
| 806 |
+
rollout_ssim = sum(rollout_ssim_frames) / len(rollout_ssim_frames)
|
| 807 |
+
rollout_lpips = (
|
| 808 |
+
sum(rollout_lpips_frames) / len(rollout_lpips_frames)
|
| 809 |
+
if rollout_lpips_frames
|
| 810 |
+
else None
|
| 811 |
+
)
|
| 812 |
+
return {
|
| 813 |
+
"status": "complete",
|
| 814 |
+
"name": experiment["name"],
|
| 815 |
+
"initialization_method": experiment["initialization_method"],
|
| 816 |
+
"source_layer": experiment["source_layer"],
|
| 817 |
+
"offline_final_val_flow_mse": experiment["offline_final_val_flow_mse"],
|
| 818 |
+
"offline_final_val_hidden_mse": experiment[
|
| 819 |
+
"offline_final_val_hidden_mse"
|
| 820 |
+
],
|
| 821 |
+
"schedule": "chunk0=FFFF; chunks1-6=FPPF",
|
| 822 |
+
"reference": "matching FFFF, same prompt and seed",
|
| 823 |
+
"pixel_domain": "VAE-decoded RGB, rounded to uint8",
|
| 824 |
+
"aggregation": (
|
| 825 |
+
"PSNR from global pixel MSE; SSIM/LPIPS mean over decoded frames"
|
| 826 |
+
),
|
| 827 |
+
"num_prompts": len(prompt_results),
|
| 828 |
+
"num_frames": len(psnr_frames),
|
| 829 |
+
"pixel_mse": pixel_mse,
|
| 830 |
+
"psnr": -10.0 * math.log10(max(pixel_mse, 1e-12)),
|
| 831 |
+
"psnr_frame_mean": psnr_frame_mean,
|
| 832 |
+
"psnr_frame_std": psnr_std,
|
| 833 |
+
"ssim": ssim,
|
| 834 |
+
"ssim_frame_std": ssim_std,
|
| 835 |
+
"lpips": lpips_mean,
|
| 836 |
+
"lpips_frame_std": lpips_std,
|
| 837 |
+
"rollout_start_frame": PIXEL_FRAMES_FIRST_CHUNK,
|
| 838 |
+
"rollout_num_frames": len(rollout_mse_frames),
|
| 839 |
+
"rollout_pixel_mse": rollout_pixel_mse,
|
| 840 |
+
"rollout_psnr": -10.0
|
| 841 |
+
* math.log10(max(rollout_pixel_mse, 1e-12)),
|
| 842 |
+
"rollout_ssim": rollout_ssim,
|
| 843 |
+
"rollout_lpips": rollout_lpips,
|
| 844 |
+
"mean_generation_time_s": sum(
|
| 845 |
+
result["generation_time_s"] for result in prompt_results
|
| 846 |
+
)
|
| 847 |
+
/ len(prompt_results),
|
| 848 |
+
"full_calls_per_prompt": prompt_results[0]["full_calls"],
|
| 849 |
+
"predictor_calls_per_prompt": prompt_results[0]["predictor_calls"],
|
| 850 |
+
"prompt_ids": [result["prompt_id"] for result in prompt_results],
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
|
| 854 |
+
def write_summary(
|
| 855 |
+
output_dir: Path, experiments: list[dict[str, Any]]
|
| 856 |
+
) -> None:
|
| 857 |
+
rows: list[dict[str, Any]] = []
|
| 858 |
+
for experiment in experiments:
|
| 859 |
+
path = output_dir / experiment["name"] / "metrics.json"
|
| 860 |
+
if not path.exists():
|
| 861 |
+
continue
|
| 862 |
+
metrics = json.loads(path.read_text(encoding="utf-8"))
|
| 863 |
+
if metrics.get("status") != "complete":
|
| 864 |
+
continue
|
| 865 |
+
rows.append(
|
| 866 |
+
{
|
| 867 |
+
"name": metrics["name"],
|
| 868 |
+
"initialization_method": metrics["initialization_method"],
|
| 869 |
+
"source_layer": metrics["source_layer"],
|
| 870 |
+
"num_prompts": metrics["num_prompts"],
|
| 871 |
+
"num_frames": metrics["num_frames"],
|
| 872 |
+
"psnr": metrics["psnr"],
|
| 873 |
+
"ssim": metrics["ssim"],
|
| 874 |
+
"lpips": metrics["lpips"],
|
| 875 |
+
"rollout_psnr": metrics["rollout_psnr"],
|
| 876 |
+
"rollout_ssim": metrics["rollout_ssim"],
|
| 877 |
+
"rollout_lpips": metrics["rollout_lpips"],
|
| 878 |
+
"offline_final_val_flow_mse": metrics[
|
| 879 |
+
"offline_final_val_flow_mse"
|
| 880 |
+
],
|
| 881 |
+
"mean_generation_time_s": metrics["mean_generation_time_s"],
|
| 882 |
+
}
|
| 883 |
+
)
|
| 884 |
+
rows.sort(key=lambda row: float(row["lpips"] or math.inf))
|
| 885 |
+
fields = [
|
| 886 |
+
"name",
|
| 887 |
+
"initialization_method",
|
| 888 |
+
"source_layer",
|
| 889 |
+
"num_prompts",
|
| 890 |
+
"num_frames",
|
| 891 |
+
"psnr",
|
| 892 |
+
"ssim",
|
| 893 |
+
"lpips",
|
| 894 |
+
"rollout_psnr",
|
| 895 |
+
"rollout_ssim",
|
| 896 |
+
"rollout_lpips",
|
| 897 |
+
"offline_final_val_flow_mse",
|
| 898 |
+
"mean_generation_time_s",
|
| 899 |
+
]
|
| 900 |
+
destination = output_dir / "summary.csv"
|
| 901 |
+
temporary = destination.with_suffix(".csv.tmp")
|
| 902 |
+
with temporary.open("w", encoding="utf-8", newline="") as handle:
|
| 903 |
+
writer = csv.DictWriter(handle, fieldnames=fields)
|
| 904 |
+
writer.writeheader()
|
| 905 |
+
writer.writerows(rows)
|
| 906 |
+
os.replace(temporary, destination)
|
| 907 |
+
|
| 908 |
+
|
| 909 |
+
def load_completed_prompt_results(
|
| 910 |
+
run_dir: Path, prompt_ids: list[int]
|
| 911 |
+
) -> list[dict[str, Any]]:
|
| 912 |
+
results = []
|
| 913 |
+
for prompt_id in prompt_ids:
|
| 914 |
+
path = run_dir / "per_prompt" / f"prompt_{prompt_id:04d}.json"
|
| 915 |
+
if path.exists():
|
| 916 |
+
results.append(json.loads(path.read_text(encoding="utf-8")))
|
| 917 |
+
return results
|
| 918 |
+
|
| 919 |
+
|
| 920 |
+
def main() -> None:
|
| 921 |
+
args = parse_args()
|
| 922 |
+
args.config_path = resolve(args.config_path)
|
| 923 |
+
args.checkpoint_path = resolve(args.checkpoint_path)
|
| 924 |
+
args.dataset_root = resolve(args.dataset_root)
|
| 925 |
+
args.sweep_dir = resolve(args.sweep_dir)
|
| 926 |
+
args.output_dir = resolve(args.output_dir)
|
| 927 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 928 |
+
|
| 929 |
+
prompt_ids = sorted(set(args.prompt_ids))
|
| 930 |
+
if args.max_prompts is not None:
|
| 931 |
+
prompt_ids = prompt_ids[: args.max_prompts]
|
| 932 |
+
experiments = discover_experiments(
|
| 933 |
+
args.sweep_dir, args.experiments, args.max_experiments
|
| 934 |
+
)
|
| 935 |
+
device = torch.device("cuda")
|
| 936 |
+
torch.set_grad_enabled(False)
|
| 937 |
+
set_seed(args.generation_seed)
|
| 938 |
+
|
| 939 |
+
config = OmegaConf.merge(
|
| 940 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 941 |
+
OmegaConf.load(args.config_path),
|
| 942 |
+
)
|
| 943 |
+
manifest = {
|
| 944 |
+
"status": "running",
|
| 945 |
+
"gpu": str(args.gpu),
|
| 946 |
+
"config_path": str(args.config_path),
|
| 947 |
+
"checkpoint_path": str(args.checkpoint_path),
|
| 948 |
+
"dataset_root": str(args.dataset_root),
|
| 949 |
+
"sweep_dir": str(args.sweep_dir),
|
| 950 |
+
"prompt_ids": prompt_ids,
|
| 951 |
+
"generation_seed_reset_per_prompt": args.generation_seed,
|
| 952 |
+
"experiments": [item["name"] for item in experiments],
|
| 953 |
+
"fppf_definition": "chunk0=FFFF; chunks1-6=FPPF",
|
| 954 |
+
"reference": "offline FFFF clean latents from the same prompt/seed",
|
| 955 |
+
"metrics": {
|
| 956 |
+
"psnr": "RGB PSNR from global pixel MSE",
|
| 957 |
+
"ssim": "11x11 Gaussian sigma=1.5 RGB SSIM, then frame mean",
|
| 958 |
+
"lpips": "AlexNet LPIPS on RGB [-1,1], then frame mean",
|
| 959 |
+
"pixel_quantization": "both inputs rounded to uint8",
|
| 960 |
+
"rollout_only": (
|
| 961 |
+
"also reported for decoded frames 9..80 after the FFFF-only "
|
| 962 |
+
"first chunk"
|
| 963 |
+
),
|
| 964 |
+
},
|
| 965 |
+
}
|
| 966 |
+
atomic_json(args.output_dir / "manifest.json", manifest)
|
| 967 |
+
|
| 968 |
+
print("[setup] loading VAE and preparing FFFF reference frames", flush=True)
|
| 969 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 970 |
+
prepare_reference_frames(
|
| 971 |
+
vae=vae,
|
| 972 |
+
dataset_root=args.dataset_root,
|
| 973 |
+
output_dir=args.output_dir,
|
| 974 |
+
prompt_ids=prompt_ids,
|
| 975 |
+
device=device,
|
| 976 |
+
rebuild=args.rebuild_references,
|
| 977 |
+
)
|
| 978 |
+
|
| 979 |
+
print("[setup] loading frozen generator_ema", flush=True)
|
| 980 |
+
pipeline = build_pipeline(config, args.checkpoint_path, vae, device)
|
| 981 |
+
teacher = pipeline.generator.model
|
| 982 |
+
lpips_model = None
|
| 983 |
+
if not args.skip_lpips:
|
| 984 |
+
print("[setup] loading AlexNet LPIPS", flush=True)
|
| 985 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 986 |
+
lpips_model.requires_grad_(False)
|
| 987 |
+
|
| 988 |
+
if args.verify_ffff:
|
| 989 |
+
prompt_id = prompt_ids[0]
|
| 990 |
+
print(f"[verify] reproducing FFFF prompt={prompt_id}", flush=True)
|
| 991 |
+
reproduced, counts = generate_rollout(
|
| 992 |
+
pipeline=pipeline,
|
| 993 |
+
dataset_root=args.dataset_root,
|
| 994 |
+
prompt_id=prompt_id,
|
| 995 |
+
generation_seed=args.generation_seed,
|
| 996 |
+
device=device,
|
| 997 |
+
predictor=None,
|
| 998 |
+
source_layer=None,
|
| 999 |
+
schedule="FFFF",
|
| 1000 |
+
)
|
| 1001 |
+
expected = load_ffff_latent(args.dataset_root, prompt_id).to(
|
| 1002 |
+
device=device, dtype=torch.bfloat16
|
| 1003 |
+
)
|
| 1004 |
+
difference = reproduced.float() - expected.float()
|
| 1005 |
+
verification = {
|
| 1006 |
+
"prompt_id": prompt_id,
|
| 1007 |
+
"max_abs_latent_error": float(difference.abs().max()),
|
| 1008 |
+
"latent_mse": float(difference.square().mean()),
|
| 1009 |
+
**counts,
|
| 1010 |
+
}
|
| 1011 |
+
atomic_json(args.output_dir / "ffff_reproduction.json", verification)
|
| 1012 |
+
print(f"[verify] {verification}", flush=True)
|
| 1013 |
+
if verification["max_abs_latent_error"] > 1e-3:
|
| 1014 |
+
raise RuntimeError(
|
| 1015 |
+
"FFFF reproduction differs from offline reference; refusing "
|
| 1016 |
+
"to evaluate FPPF with unmatched randomness/caches"
|
| 1017 |
+
)
|
| 1018 |
+
del reproduced, expected, difference
|
| 1019 |
+
torch.cuda.empty_cache()
|
| 1020 |
+
|
| 1021 |
+
for experiment_index, experiment in enumerate(experiments, start=1):
|
| 1022 |
+
run_dir = args.output_dir / experiment["name"]
|
| 1023 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 1024 |
+
metrics_path = run_dir / "metrics.json"
|
| 1025 |
+
if metrics_path.exists():
|
| 1026 |
+
existing = json.loads(metrics_path.read_text(encoding="utf-8"))
|
| 1027 |
+
if (
|
| 1028 |
+
existing.get("status") == "complete"
|
| 1029 |
+
and existing.get("prompt_ids") == prompt_ids
|
| 1030 |
+
and (args.skip_lpips or existing.get("lpips") is not None)
|
| 1031 |
+
):
|
| 1032 |
+
print(
|
| 1033 |
+
f"[run] {experiment_index}/{len(experiments)} "
|
| 1034 |
+
f"skip complete {experiment['name']}",
|
| 1035 |
+
flush=True,
|
| 1036 |
+
)
|
| 1037 |
+
continue
|
| 1038 |
+
|
| 1039 |
+
print(
|
| 1040 |
+
f"[run] {experiment_index}/{len(experiments)} "
|
| 1041 |
+
f"{experiment['name']} source={experiment['source_layer']}",
|
| 1042 |
+
flush=True,
|
| 1043 |
+
)
|
| 1044 |
+
predictor = load_predictor(teacher, experiment, device)
|
| 1045 |
+
existing_results = {
|
| 1046 |
+
result["prompt_id"]: result
|
| 1047 |
+
for result in load_completed_prompt_results(run_dir, prompt_ids)
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
for prompt_index, prompt_id in enumerate(prompt_ids, start=1):
|
| 1051 |
+
video_path = run_dir / "videos" / f"prompt_{prompt_id:04d}.mp4"
|
| 1052 |
+
if prompt_id in existing_results and (
|
| 1053 |
+
not args.save_videos or video_path.exists()
|
| 1054 |
+
):
|
| 1055 |
+
print(
|
| 1056 |
+
f"[prompt] {experiment['name']} {prompt_index}/{len(prompt_ids)} "
|
| 1057 |
+
f"id={prompt_id} cached",
|
| 1058 |
+
flush=True,
|
| 1059 |
+
)
|
| 1060 |
+
continue
|
| 1061 |
+
started = time.perf_counter()
|
| 1062 |
+
latent, counts = generate_rollout(
|
| 1063 |
+
pipeline=pipeline,
|
| 1064 |
+
dataset_root=args.dataset_root,
|
| 1065 |
+
prompt_id=prompt_id,
|
| 1066 |
+
generation_seed=args.generation_seed,
|
| 1067 |
+
device=device,
|
| 1068 |
+
predictor=predictor,
|
| 1069 |
+
source_layer=experiment["source_layer"],
|
| 1070 |
+
schedule="FPPF",
|
| 1071 |
+
)
|
| 1072 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 1073 |
+
pixels = vae.decode_to_pixel(latent, use_cache=False)
|
| 1074 |
+
prediction_u8 = pixels_to_u8(pixels)
|
| 1075 |
+
if args.save_videos:
|
| 1076 |
+
save_mp4(prediction_u8, video_path)
|
| 1077 |
+
reference_u8 = load_reference_frames(args.output_dir, prompt_id)
|
| 1078 |
+
metrics = frame_metrics(
|
| 1079 |
+
reference_u8=reference_u8,
|
| 1080 |
+
prediction_u8=prediction_u8,
|
| 1081 |
+
lpips_model=lpips_model,
|
| 1082 |
+
batch_size=args.metric_batch_size,
|
| 1083 |
+
device=device,
|
| 1084 |
+
)
|
| 1085 |
+
prompt_result = {
|
| 1086 |
+
"prompt_id": prompt_id,
|
| 1087 |
+
"prompt": load_prompt_metadata(args.dataset_root, prompt_id)[
|
| 1088 |
+
"prompt"
|
| 1089 |
+
],
|
| 1090 |
+
**counts,
|
| 1091 |
+
**metrics,
|
| 1092 |
+
"total_time_s": time.perf_counter() - started,
|
| 1093 |
+
}
|
| 1094 |
+
atomic_json(
|
| 1095 |
+
run_dir / "per_prompt" / f"prompt_{prompt_id:04d}.json",
|
| 1096 |
+
prompt_result,
|
| 1097 |
+
)
|
| 1098 |
+
existing_results[prompt_id] = prompt_result
|
| 1099 |
+
print(
|
| 1100 |
+
f"[prompt] {experiment['name']} {prompt_index}/{len(prompt_ids)} "
|
| 1101 |
+
f"id={prompt_id} psnr={metrics['psnr']:.4f} "
|
| 1102 |
+
f"ssim={metrics['ssim']:.6f} "
|
| 1103 |
+
f"lpips={metrics['lpips']} "
|
| 1104 |
+
f"time={prompt_result['total_time_s']:.1f}s",
|
| 1105 |
+
flush=True,
|
| 1106 |
+
)
|
| 1107 |
+
if hasattr(vae.model, "clear_cache"):
|
| 1108 |
+
vae.model.clear_cache()
|
| 1109 |
+
del latent, pixels, prediction_u8, reference_u8
|
| 1110 |
+
torch.cuda.empty_cache()
|
| 1111 |
+
|
| 1112 |
+
prompt_results = [existing_results[prompt_id] for prompt_id in prompt_ids]
|
| 1113 |
+
aggregate = aggregate_prompt_results(experiment, prompt_results)
|
| 1114 |
+
atomic_json(metrics_path, aggregate)
|
| 1115 |
+
write_summary(args.output_dir, experiments)
|
| 1116 |
+
print(
|
| 1117 |
+
f"[result] {experiment['name']} psnr={aggregate['psnr']:.4f} "
|
| 1118 |
+
f"ssim={aggregate['ssim']:.6f} lpips={aggregate['lpips']}",
|
| 1119 |
+
flush=True,
|
| 1120 |
+
)
|
| 1121 |
+
del predictor
|
| 1122 |
+
torch.cuda.empty_cache()
|
| 1123 |
+
|
| 1124 |
+
manifest["status"] = "complete"
|
| 1125 |
+
atomic_json(args.output_dir / "manifest.json", manifest)
|
| 1126 |
+
write_summary(args.output_dir, experiments)
|
| 1127 |
+
print(
|
| 1128 |
+
f"[complete] {len(experiments)} experiments -> "
|
| 1129 |
+
f"{args.output_dir / 'summary.csv'}",
|
| 1130 |
+
flush=True,
|
| 1131 |
+
)
|
| 1132 |
+
|
| 1133 |
+
|
| 1134 |
+
if __name__ == "__main__":
|
| 1135 |
+
main()
|
scripts/evaluate_single_chunk_frrr.py
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Measure the effect of introducing a reuse schedule in one AR chunk.
|
| 3 |
+
|
| 4 |
+
The four letters describe the four denoising steps of a chunk. ``F`` runs
|
| 5 |
+
the full generator. ``R`` reuses the flow prediction from the most recent
|
| 6 |
+
full step and only applies the timestep-dependent x0 conversion. For every
|
| 7 |
+
prompt this script creates an all-FFFF reference and seven interventions; in
|
| 8 |
+
intervention k only chunk k uses the requested schedule and all other chunks
|
| 9 |
+
use FFFF.
|
| 10 |
+
|
| 11 |
+
All rollouts for a prompt reset the RNG to the same seed, so the initial noise
|
| 12 |
+
and the three re-noising tensors per chunk are identical. Metrics are
|
| 13 |
+
computed on VAE-decoded, rounded uint8 RGB frames before MP4 compression.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import argparse
|
| 19 |
+
import csv
|
| 20 |
+
import json
|
| 21 |
+
import math
|
| 22 |
+
import os
|
| 23 |
+
import sys
|
| 24 |
+
import time
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
from typing import Any
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _preparse_gpu() -> str:
|
| 30 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 31 |
+
parser.add_argument("--gpu", default="0")
|
| 32 |
+
args, _ = parser.parse_known_args()
|
| 33 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 34 |
+
return str(args.gpu)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 38 |
+
|
| 39 |
+
import lpips
|
| 40 |
+
import torch
|
| 41 |
+
import torch.nn.functional as F
|
| 42 |
+
from omegaconf import OmegaConf
|
| 43 |
+
from safetensors.torch import load_file, save_file
|
| 44 |
+
from torchvision.io import write_video
|
| 45 |
+
|
| 46 |
+
REPO_ROOT = Path(
|
| 47 |
+
os.environ.get("EVAL_REPO_ROOT", str(Path(__file__).resolve().parents[1]))
|
| 48 |
+
).resolve()
|
| 49 |
+
if str(REPO_ROOT) not in sys.path:
|
| 50 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 51 |
+
|
| 52 |
+
from pipeline import CausalInferencePipeline
|
| 53 |
+
from utils.misc import set_seed
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
LATENT_CHANNELS = 16
|
| 57 |
+
LATENT_HEIGHT = 60
|
| 58 |
+
LATENT_WIDTH = 104
|
| 59 |
+
FRAMES_PER_CHUNK = 3
|
| 60 |
+
NUM_CHUNKS = 7
|
| 61 |
+
NUM_DENOISING_STEPS = 4
|
| 62 |
+
TOKENS_PER_FRAME = 30 * 52
|
| 63 |
+
TOKENS_PER_CHUNK = FRAMES_PER_CHUNK * TOKENS_PER_FRAME
|
| 64 |
+
DECODED_FRAMES = 1 + 4 * (NUM_CHUNKS * FRAMES_PER_CHUNK - 1)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def parse_args() -> argparse.Namespace:
|
| 68 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 69 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 70 |
+
parser.add_argument(
|
| 71 |
+
"--config_path", type=Path, default=Path("configs/self_forcing_dmd.yaml")
|
| 72 |
+
)
|
| 73 |
+
parser.add_argument(
|
| 74 |
+
"--checkpoint_path",
|
| 75 |
+
type=Path,
|
| 76 |
+
default=Path("checkpoints/self_forcing_dmd.pt"),
|
| 77 |
+
)
|
| 78 |
+
parser.add_argument(
|
| 79 |
+
"--prompt_path",
|
| 80 |
+
type=Path,
|
| 81 |
+
default=Path("prompts/MovieGenVideoBench_extended.txt"),
|
| 82 |
+
)
|
| 83 |
+
parser.add_argument(
|
| 84 |
+
"--output_dir",
|
| 85 |
+
type=Path,
|
| 86 |
+
default=Path("outputs/single_chunk_frrr_first10"),
|
| 87 |
+
)
|
| 88 |
+
parser.add_argument("--prompt_ids", type=int, nargs="*", default=list(range(10)))
|
| 89 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 90 |
+
parser.add_argument(
|
| 91 |
+
"--num_chunks",
|
| 92 |
+
type=int,
|
| 93 |
+
default=7,
|
| 94 |
+
help="Number of 3-latent-frame autoregressive chunks.",
|
| 95 |
+
)
|
| 96 |
+
parser.add_argument(
|
| 97 |
+
"--intervention_schedule",
|
| 98 |
+
choices=["FRRR", "FRRF"],
|
| 99 |
+
default="FRRR",
|
| 100 |
+
help="Four-step schedule used in the selected chunk.",
|
| 101 |
+
)
|
| 102 |
+
parser.add_argument("--metric_batch_size", type=int, default=4)
|
| 103 |
+
parser.add_argument("--use_ema", action=argparse.BooleanOptionalAction, default=True)
|
| 104 |
+
parser.add_argument("--save_videos", action=argparse.BooleanOptionalAction, default=True)
|
| 105 |
+
parser.add_argument(
|
| 106 |
+
"--low_memory",
|
| 107 |
+
action=argparse.BooleanOptionalAction,
|
| 108 |
+
default=True,
|
| 109 |
+
help="Keep only the currently used text encoder/generator/VAE on CUDA.",
|
| 110 |
+
)
|
| 111 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 112 |
+
parser.add_argument("--aggregate_only", action="store_true")
|
| 113 |
+
parser.add_argument(
|
| 114 |
+
"--worker",
|
| 115 |
+
action="store_true",
|
| 116 |
+
help="Shard worker: do not rewrite shared config or aggregate files.",
|
| 117 |
+
)
|
| 118 |
+
args = parser.parse_args()
|
| 119 |
+
if not args.prompt_ids:
|
| 120 |
+
parser.error("--prompt_ids cannot be empty")
|
| 121 |
+
if any(index < 0 for index in args.prompt_ids):
|
| 122 |
+
parser.error("prompt IDs must be non-negative")
|
| 123 |
+
if args.metric_batch_size < 1:
|
| 124 |
+
parser.error("--metric_batch_size must be positive")
|
| 125 |
+
if args.num_chunks < 1:
|
| 126 |
+
parser.error("--num_chunks must be positive")
|
| 127 |
+
global NUM_CHUNKS, DECODED_FRAMES
|
| 128 |
+
NUM_CHUNKS = args.num_chunks
|
| 129 |
+
DECODED_FRAMES = 1 + 4 * (NUM_CHUNKS * FRAMES_PER_CHUNK - 1)
|
| 130 |
+
return args
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def resolve(path: Path) -> Path:
|
| 134 |
+
return path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve()
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def atomic_json(path: Path, value: Any) -> None:
|
| 138 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 139 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 140 |
+
temporary.write_text(
|
| 141 |
+
json.dumps(value, indent=2, ensure_ascii=False, allow_nan=False) + "\n",
|
| 142 |
+
encoding="utf-8",
|
| 143 |
+
)
|
| 144 |
+
os.replace(temporary, path)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def read_prompts(path: Path) -> list[str]:
|
| 148 |
+
with path.open("r", encoding="utf-8") as handle:
|
| 149 |
+
return [line.strip() for line in handle if line.strip()]
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def build_pipeline(args: argparse.Namespace) -> CausalInferencePipeline:
|
| 153 |
+
config = OmegaConf.merge(
|
| 154 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 155 |
+
OmegaConf.load(resolve(args.config_path)),
|
| 156 |
+
)
|
| 157 |
+
pipeline = CausalInferencePipeline(config, device=torch.device("cuda"))
|
| 158 |
+
checkpoint = torch.load(
|
| 159 |
+
resolve(args.checkpoint_path), map_location="cpu", weights_only=False
|
| 160 |
+
)
|
| 161 |
+
state_key = "generator_ema" if args.use_ema else "generator"
|
| 162 |
+
pipeline.generator.load_state_dict(checkpoint[state_key])
|
| 163 |
+
del checkpoint
|
| 164 |
+
pipeline = pipeline.to(dtype=torch.bfloat16)
|
| 165 |
+
if not args.low_memory:
|
| 166 |
+
pipeline.text_encoder.to(device="cuda")
|
| 167 |
+
pipeline.generator.to(device="cuda")
|
| 168 |
+
pipeline.vae.to(device="cuda")
|
| 169 |
+
pipeline.eval()
|
| 170 |
+
if pipeline.num_frame_per_block != FRAMES_PER_CHUNK:
|
| 171 |
+
raise ValueError(
|
| 172 |
+
f"Expected {FRAMES_PER_CHUNK} latent frames/chunk, got "
|
| 173 |
+
f"{pipeline.num_frame_per_block}"
|
| 174 |
+
)
|
| 175 |
+
if len(pipeline.denoising_step_list) != NUM_DENOISING_STEPS:
|
| 176 |
+
raise ValueError(
|
| 177 |
+
f"Expected {NUM_DENOISING_STEPS} denoising steps, got "
|
| 178 |
+
f"{len(pipeline.denoising_step_list)}"
|
| 179 |
+
)
|
| 180 |
+
return pipeline
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def reset_caches(pipeline: CausalInferencePipeline) -> None:
|
| 184 |
+
device = torch.device("cuda")
|
| 185 |
+
if pipeline.kv_cache1 is None:
|
| 186 |
+
pipeline._initialize_kv_cache(1, torch.bfloat16, device)
|
| 187 |
+
pipeline._initialize_crossattn_cache(1, torch.bfloat16, device)
|
| 188 |
+
required_tokens = NUM_CHUNKS * TOKENS_PER_CHUNK
|
| 189 |
+
for cache in pipeline.kv_cache1:
|
| 190 |
+
if cache["k"].shape[1] < required_tokens:
|
| 191 |
+
heads, head_dim = cache["k"].shape[2:]
|
| 192 |
+
cache["k"] = torch.zeros(
|
| 193 |
+
[1, required_tokens, heads, head_dim],
|
| 194 |
+
dtype=torch.bfloat16,
|
| 195 |
+
device=device,
|
| 196 |
+
)
|
| 197 |
+
cache["v"] = torch.zeros_like(cache["k"])
|
| 198 |
+
for cache in pipeline.kv_cache1:
|
| 199 |
+
cache["global_end_index"].zero_()
|
| 200 |
+
cache["local_end_index"].zero_()
|
| 201 |
+
for cache in pipeline.crossattn_cache:
|
| 202 |
+
cache["is_init"] = False
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
@torch.inference_mode()
|
| 206 |
+
def generate_latents(
|
| 207 |
+
*,
|
| 208 |
+
pipeline: CausalInferencePipeline,
|
| 209 |
+
conditional_dict: dict[str, torch.Tensor],
|
| 210 |
+
seed: int,
|
| 211 |
+
reuse_chunk: int | None,
|
| 212 |
+
intervention_schedule: str,
|
| 213 |
+
) -> tuple[torch.Tensor, dict[str, Any]]:
|
| 214 |
+
"""Generate 21 latents with FFFF or exactly one reuse-schedule chunk."""
|
| 215 |
+
reset_caches(pipeline)
|
| 216 |
+
set_seed(seed)
|
| 217 |
+
noise = torch.randn(
|
| 218 |
+
1,
|
| 219 |
+
NUM_CHUNKS * FRAMES_PER_CHUNK,
|
| 220 |
+
LATENT_CHANNELS,
|
| 221 |
+
LATENT_HEIGHT,
|
| 222 |
+
LATENT_WIDTH,
|
| 223 |
+
dtype=torch.bfloat16,
|
| 224 |
+
device="cuda",
|
| 225 |
+
)
|
| 226 |
+
timesteps = pipeline.denoising_step_list.to(device="cuda")
|
| 227 |
+
output_chunks: list[torch.Tensor] = []
|
| 228 |
+
full_calls = 0
|
| 229 |
+
reuse_calls = 0
|
| 230 |
+
started = time.perf_counter()
|
| 231 |
+
|
| 232 |
+
for chunk in range(NUM_CHUNKS):
|
| 233 |
+
noisy_input = noise[
|
| 234 |
+
:, chunk * FRAMES_PER_CHUNK : (chunk + 1) * FRAMES_PER_CHUNK
|
| 235 |
+
]
|
| 236 |
+
cached_flow: torch.Tensor | None = None
|
| 237 |
+
denoised_pred: torch.Tensor | None = None
|
| 238 |
+
timestep: torch.Tensor | None = None
|
| 239 |
+
|
| 240 |
+
for step, current_timestep in enumerate(timesteps):
|
| 241 |
+
timestep = torch.ones(
|
| 242 |
+
[1, FRAMES_PER_CHUNK], dtype=torch.int64, device="cuda"
|
| 243 |
+
) * current_timestep
|
| 244 |
+
selected_step = (
|
| 245 |
+
intervention_schedule[step] if reuse_chunk == chunk else "F"
|
| 246 |
+
)
|
| 247 |
+
use_reuse = selected_step == "R"
|
| 248 |
+
if use_reuse:
|
| 249 |
+
if cached_flow is None:
|
| 250 |
+
raise RuntimeError("Reuse requested before any full step")
|
| 251 |
+
flow = cached_flow
|
| 252 |
+
denoised_pred = pipeline.generator._convert_flow_pred_to_x0(
|
| 253 |
+
flow_pred=flow.flatten(0, 1),
|
| 254 |
+
xt=noisy_input.flatten(0, 1),
|
| 255 |
+
timestep=timestep.flatten(0, 1),
|
| 256 |
+
).unflatten(0, flow.shape[:2])
|
| 257 |
+
reuse_calls += 1
|
| 258 |
+
else:
|
| 259 |
+
flow, denoised_pred = pipeline.generator(
|
| 260 |
+
noisy_image_or_video=noisy_input,
|
| 261 |
+
conditional_dict=conditional_dict,
|
| 262 |
+
timestep=timestep,
|
| 263 |
+
kv_cache=pipeline.kv_cache1,
|
| 264 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 265 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 266 |
+
)
|
| 267 |
+
if reuse_chunk == chunk:
|
| 268 |
+
cached_flow = flow.detach().clone()
|
| 269 |
+
full_calls += 1
|
| 270 |
+
|
| 271 |
+
if step < NUM_DENOISING_STEPS - 1:
|
| 272 |
+
if denoised_pred is None:
|
| 273 |
+
raise RuntimeError("Denoising step did not produce x0")
|
| 274 |
+
next_timestep = timesteps[step + 1]
|
| 275 |
+
flat = denoised_pred.flatten(0, 1)
|
| 276 |
+
noisy_input = pipeline.scheduler.add_noise(
|
| 277 |
+
flat,
|
| 278 |
+
torch.randn_like(flat),
|
| 279 |
+
next_timestep
|
| 280 |
+
* torch.ones(
|
| 281 |
+
[FRAMES_PER_CHUNK], dtype=torch.long, device="cuda"
|
| 282 |
+
),
|
| 283 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 284 |
+
|
| 285 |
+
if denoised_pred is None or timestep is None:
|
| 286 |
+
raise RuntimeError("Chunk did not produce a clean latent")
|
| 287 |
+
output_chunks.append(denoised_pred)
|
| 288 |
+
|
| 289 |
+
# The clean context pass is always full, as in baseline Self-Forcing.
|
| 290 |
+
context_timestep = torch.ones_like(timestep) * pipeline.args.context_noise
|
| 291 |
+
pipeline.generator(
|
| 292 |
+
noisy_image_or_video=denoised_pred,
|
| 293 |
+
conditional_dict=conditional_dict,
|
| 294 |
+
timestep=context_timestep,
|
| 295 |
+
kv_cache=pipeline.kv_cache1,
|
| 296 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 297 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
torch.cuda.synchronize()
|
| 301 |
+
return torch.cat(output_chunks, dim=1), {
|
| 302 |
+
"generation_time_s": time.perf_counter() - started,
|
| 303 |
+
"full_denoising_calls": full_calls,
|
| 304 |
+
"reuse_denoising_calls": reuse_calls,
|
| 305 |
+
"full_context_calls": NUM_CHUNKS,
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
@torch.inference_mode()
|
| 310 |
+
def decode_u8(pipeline: CausalInferencePipeline, latents: torch.Tensor) -> torch.Tensor:
|
| 311 |
+
video = pipeline.vae.decode_to_pixel(latents, use_cache=False)
|
| 312 |
+
frames = (
|
| 313 |
+
((video.squeeze(0).float() + 1.0) * 127.5)
|
| 314 |
+
.round_()
|
| 315 |
+
.clamp_(0, 255)
|
| 316 |
+
.to(device="cpu", dtype=torch.uint8)
|
| 317 |
+
.contiguous()
|
| 318 |
+
)
|
| 319 |
+
pipeline.vae.model.clear_cache()
|
| 320 |
+
if frames.shape != (DECODED_FRAMES, 3, 480, 832):
|
| 321 |
+
raise ValueError(f"Unexpected decoded shape: {tuple(frames.shape)}")
|
| 322 |
+
return frames
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def save_mp4(frames: torch.Tensor, path: Path) -> None:
|
| 326 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 327 |
+
write_video(
|
| 328 |
+
str(path),
|
| 329 |
+
frames.permute(0, 2, 3, 1),
|
| 330 |
+
fps=16,
|
| 331 |
+
video_codec="libx264",
|
| 332 |
+
options={"crf": "18"},
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def gaussian_kernel(device: torch.device, channels: int = 3) -> torch.Tensor:
|
| 337 |
+
coordinates = torch.arange(11, device=device, dtype=torch.float32) - 5
|
| 338 |
+
kernel_1d = torch.exp(-coordinates.square() / (2 * 1.5**2))
|
| 339 |
+
kernel_1d /= kernel_1d.sum()
|
| 340 |
+
return torch.outer(kernel_1d, kernel_1d).expand(channels, 1, 11, 11).contiguous()
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def ssim_per_frame(
|
| 344 |
+
reference: torch.Tensor, prediction: torch.Tensor, kernel: torch.Tensor
|
| 345 |
+
) -> torch.Tensor:
|
| 346 |
+
channels = reference.shape[1]
|
| 347 |
+
mu_x = F.conv2d(reference, kernel, groups=channels)
|
| 348 |
+
mu_y = F.conv2d(prediction, kernel, groups=channels)
|
| 349 |
+
mu_x2 = mu_x.square()
|
| 350 |
+
mu_y2 = mu_y.square()
|
| 351 |
+
mu_xy = mu_x * mu_y
|
| 352 |
+
sigma_x2 = F.conv2d(reference.square(), kernel, groups=channels) - mu_x2
|
| 353 |
+
sigma_y2 = F.conv2d(prediction.square(), kernel, groups=channels) - mu_y2
|
| 354 |
+
sigma_xy = F.conv2d(reference * prediction, kernel, groups=channels) - mu_xy
|
| 355 |
+
c1, c2 = 0.01**2, 0.03**2
|
| 356 |
+
score = ((2 * mu_xy + c1) * (2 * sigma_xy + c2)) / (
|
| 357 |
+
(mu_x2 + mu_y2 + c1) * (sigma_x2 + sigma_y2 + c2)
|
| 358 |
+
)
|
| 359 |
+
return score.mean(dim=(1, 2, 3))
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
def decoded_chunk_slices() -> list[slice]:
|
| 363 |
+
# Wan VAE maps the first latent to one pixel frame and every subsequent
|
| 364 |
+
# latent to four frames. The first 3-latent chunk therefore has 9 frames;
|
| 365 |
+
# each later chunk has 12.
|
| 366 |
+
result = [slice(0, 9)]
|
| 367 |
+
result.extend(
|
| 368 |
+
slice(9 + 12 * index, 9 + 12 * (index + 1))
|
| 369 |
+
for index in range(NUM_CHUNKS - 1)
|
| 370 |
+
)
|
| 371 |
+
if result[-1].stop != DECODED_FRAMES:
|
| 372 |
+
raise AssertionError(result)
|
| 373 |
+
return result
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
@torch.inference_mode()
|
| 377 |
+
def frame_metrics(
|
| 378 |
+
*,
|
| 379 |
+
reference_u8: torch.Tensor,
|
| 380 |
+
prediction_u8: torch.Tensor,
|
| 381 |
+
lpips_model: torch.nn.Module,
|
| 382 |
+
batch_size: int,
|
| 383 |
+
) -> dict[str, Any]:
|
| 384 |
+
if reference_u8.shape != prediction_u8.shape:
|
| 385 |
+
raise ValueError(
|
| 386 |
+
f"Frame shapes differ: {tuple(reference_u8.shape)} vs "
|
| 387 |
+
f"{tuple(prediction_u8.shape)}"
|
| 388 |
+
)
|
| 389 |
+
device = torch.device("cuda")
|
| 390 |
+
kernel = gaussian_kernel(device)
|
| 391 |
+
mse_values: list[float] = []
|
| 392 |
+
psnr_values: list[float] = []
|
| 393 |
+
ssim_values: list[float] = []
|
| 394 |
+
lpips_values: list[float] = []
|
| 395 |
+
max_abs_values: list[int] = []
|
| 396 |
+
|
| 397 |
+
for start in range(0, len(reference_u8), batch_size):
|
| 398 |
+
end = min(start + batch_size, len(reference_u8))
|
| 399 |
+
reference = reference_u8[start:end].to(device=device, dtype=torch.float32) / 255
|
| 400 |
+
prediction = prediction_u8[start:end].to(device=device, dtype=torch.float32) / 255
|
| 401 |
+
mse = (reference - prediction).square().mean(dim=(1, 2, 3))
|
| 402 |
+
psnr = -10 * torch.log10(mse.clamp_min(1e-12))
|
| 403 |
+
ssim = ssim_per_frame(reference, prediction, kernel)
|
| 404 |
+
distance = lpips_model(reference.mul(2).sub(1), prediction.mul(2).sub(1)).flatten()
|
| 405 |
+
mse_values.extend(float(value) for value in mse.cpu())
|
| 406 |
+
psnr_values.extend(float(value) for value in psnr.cpu())
|
| 407 |
+
ssim_values.extend(float(value) for value in ssim.cpu())
|
| 408 |
+
lpips_values.extend(float(value) for value in distance.cpu())
|
| 409 |
+
maximum = (
|
| 410 |
+
reference_u8[start:end].to(torch.int16)
|
| 411 |
+
- prediction_u8[start:end].to(torch.int16)
|
| 412 |
+
).abs().flatten(1).max(1).values
|
| 413 |
+
max_abs_values.extend(int(value) for value in maximum)
|
| 414 |
+
|
| 415 |
+
def summarize(indices: range) -> dict[str, float | int]:
|
| 416 |
+
selected_mse = [mse_values[index] for index in indices]
|
| 417 |
+
selected_ssim = [ssim_values[index] for index in indices]
|
| 418 |
+
selected_lpips = [lpips_values[index] for index in indices]
|
| 419 |
+
mse = sum(selected_mse) / len(selected_mse)
|
| 420 |
+
return {
|
| 421 |
+
"start_frame": indices.start,
|
| 422 |
+
"end_frame_exclusive": indices.stop,
|
| 423 |
+
"num_frames": len(selected_mse),
|
| 424 |
+
"pixel_mse": mse,
|
| 425 |
+
"psnr": -10 * math.log10(max(mse, 1e-12)),
|
| 426 |
+
"ssim": sum(selected_ssim) / len(selected_ssim),
|
| 427 |
+
"lpips": sum(selected_lpips) / len(selected_lpips),
|
| 428 |
+
"max_abs_u8": max(max_abs_values[index] for index in indices),
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
full = summarize(range(DECODED_FRAMES))
|
| 432 |
+
by_chunk = []
|
| 433 |
+
for chunk, frame_slice in enumerate(decoded_chunk_slices()):
|
| 434 |
+
summary = summarize(range(frame_slice.start, frame_slice.stop))
|
| 435 |
+
summary["chunk"] = chunk
|
| 436 |
+
by_chunk.append(summary)
|
| 437 |
+
return {
|
| 438 |
+
**full,
|
| 439 |
+
"mse_per_frame": mse_values,
|
| 440 |
+
"psnr_per_frame": psnr_values,
|
| 441 |
+
"ssim_per_frame": ssim_values,
|
| 442 |
+
"lpips_per_frame": lpips_values,
|
| 443 |
+
"max_abs_u8_per_frame": max_abs_values,
|
| 444 |
+
"by_output_chunk": by_chunk,
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
def save_reference_frames(path: Path, frames: torch.Tensor, prompt_id: int) -> None:
|
| 449 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 450 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 451 |
+
save_file(
|
| 452 |
+
{"frames": frames},
|
| 453 |
+
str(temporary),
|
| 454 |
+
metadata={"prompt_id": str(prompt_id), "pixel_domain": "uint8_rgb"},
|
| 455 |
+
)
|
| 456 |
+
os.replace(temporary, path)
|
| 457 |
+
|
| 458 |
+
|
| 459 |
+
def load_reference_frames(path: Path) -> torch.Tensor:
|
| 460 |
+
return load_file(str(path), device="cpu")["frames"]
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
def move_module(module: torch.nn.Module, device: str) -> None:
|
| 464 |
+
module.to(device=device)
|
| 465 |
+
if device == "cpu":
|
| 466 |
+
torch.cuda.empty_cache()
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
|
| 470 |
+
if not rows:
|
| 471 |
+
return
|
| 472 |
+
fields: list[str] = []
|
| 473 |
+
for row in rows:
|
| 474 |
+
for key in row:
|
| 475 |
+
if key not in fields:
|
| 476 |
+
fields.append(key)
|
| 477 |
+
with path.open("w", newline="", encoding="utf-8") as handle:
|
| 478 |
+
writer = csv.DictWriter(handle, fieldnames=fields)
|
| 479 |
+
writer.writeheader()
|
| 480 |
+
writer.writerows(rows)
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
def aggregate(
|
| 484 |
+
output_dir: Path, prompt_ids: list[int], intervention_schedule: str
|
| 485 |
+
) -> None:
|
| 486 |
+
prompt_rows: list[dict[str, Any]] = []
|
| 487 |
+
propagation_buckets: dict[tuple[int, int], list[dict[str, Any]]] = {}
|
| 488 |
+
intervention_metrics: dict[int, list[dict[str, Any]]] = {
|
| 489 |
+
chunk: [] for chunk in range(NUM_CHUNKS)
|
| 490 |
+
}
|
| 491 |
+
completed_prompt_ids: list[int] = []
|
| 492 |
+
|
| 493 |
+
for prompt_id in prompt_ids:
|
| 494 |
+
prompt_dir = output_dir / f"prompt_{prompt_id:04d}"
|
| 495 |
+
prompt_complete = True
|
| 496 |
+
for reuse_chunk in range(NUM_CHUNKS):
|
| 497 |
+
path = prompt_dir / f"reuse_chunk_{reuse_chunk}" / "metrics.json"
|
| 498 |
+
if not path.exists():
|
| 499 |
+
prompt_complete = False
|
| 500 |
+
continue
|
| 501 |
+
value = json.loads(path.read_text(encoding="utf-8"))
|
| 502 |
+
if value.get("status") != "complete":
|
| 503 |
+
prompt_complete = False
|
| 504 |
+
continue
|
| 505 |
+
prompt_rows.append(
|
| 506 |
+
{
|
| 507 |
+
"prompt_id": prompt_id,
|
| 508 |
+
"reuse_chunk": reuse_chunk,
|
| 509 |
+
"psnr": value["psnr"],
|
| 510 |
+
"ssim": value["ssim"],
|
| 511 |
+
"lpips": value["lpips"],
|
| 512 |
+
"pixel_mse": value["pixel_mse"],
|
| 513 |
+
"max_abs_u8": value["max_abs_u8"],
|
| 514 |
+
"generation_time_s": value["generation_time_s"],
|
| 515 |
+
}
|
| 516 |
+
)
|
| 517 |
+
intervention_metrics[reuse_chunk].append(value)
|
| 518 |
+
for chunk_value in value["by_output_chunk"]:
|
| 519 |
+
propagation_buckets.setdefault(
|
| 520 |
+
(reuse_chunk, int(chunk_value["chunk"])), []
|
| 521 |
+
).append(chunk_value)
|
| 522 |
+
if prompt_complete:
|
| 523 |
+
completed_prompt_ids.append(prompt_id)
|
| 524 |
+
|
| 525 |
+
summary_rows: list[dict[str, Any]] = []
|
| 526 |
+
for reuse_chunk in range(NUM_CHUNKS):
|
| 527 |
+
rows = [row for row in prompt_rows if row["reuse_chunk"] == reuse_chunk]
|
| 528 |
+
if not rows:
|
| 529 |
+
continue
|
| 530 |
+
global_mse = sum(float(row["pixel_mse"]) for row in rows) / len(rows)
|
| 531 |
+
summary_rows.append(
|
| 532 |
+
{
|
| 533 |
+
"reuse_chunk": reuse_chunk,
|
| 534 |
+
"num_prompts": len(rows),
|
| 535 |
+
"psnr_from_global_mse": -10 * math.log10(max(global_mse, 1e-12)),
|
| 536 |
+
"mean_prompt_psnr": sum(float(row["psnr"]) for row in rows) / len(rows),
|
| 537 |
+
"mean_ssim": sum(float(row["ssim"]) for row in rows) / len(rows),
|
| 538 |
+
"mean_lpips": sum(float(row["lpips"]) for row in rows) / len(rows),
|
| 539 |
+
"mean_generation_time_s": sum(
|
| 540 |
+
float(row["generation_time_s"]) for row in rows
|
| 541 |
+
) / len(rows),
|
| 542 |
+
}
|
| 543 |
+
)
|
| 544 |
+
|
| 545 |
+
propagation_rows: list[dict[str, Any]] = []
|
| 546 |
+
for reuse_chunk in range(NUM_CHUNKS):
|
| 547 |
+
for output_chunk in range(NUM_CHUNKS):
|
| 548 |
+
values = propagation_buckets.get((reuse_chunk, output_chunk), [])
|
| 549 |
+
if not values:
|
| 550 |
+
continue
|
| 551 |
+
mse = sum(float(value["pixel_mse"]) for value in values) / len(values)
|
| 552 |
+
propagation_rows.append(
|
| 553 |
+
{
|
| 554 |
+
"reuse_chunk": reuse_chunk,
|
| 555 |
+
"output_chunk": output_chunk,
|
| 556 |
+
"relative_chunk": output_chunk - reuse_chunk,
|
| 557 |
+
"num_prompts": len(values),
|
| 558 |
+
"psnr_from_global_mse": -10 * math.log10(max(mse, 1e-12)),
|
| 559 |
+
"mean_ssim": sum(float(value["ssim"]) for value in values)
|
| 560 |
+
/ len(values),
|
| 561 |
+
"mean_lpips": sum(float(value["lpips"]) for value in values)
|
| 562 |
+
/ len(values),
|
| 563 |
+
"mean_max_abs_u8": sum(float(value["max_abs_u8"]) for value in values)
|
| 564 |
+
/ len(values),
|
| 565 |
+
}
|
| 566 |
+
)
|
| 567 |
+
|
| 568 |
+
affected_tail_rows: list[dict[str, Any]] = []
|
| 569 |
+
chunk_frames = decoded_chunk_slices()
|
| 570 |
+
for reuse_chunk in range(NUM_CHUNKS):
|
| 571 |
+
values = intervention_metrics[reuse_chunk]
|
| 572 |
+
if not values:
|
| 573 |
+
continue
|
| 574 |
+
start_frame = int(chunk_frames[reuse_chunk].start)
|
| 575 |
+
mse_values = [
|
| 576 |
+
frame
|
| 577 |
+
for value in values
|
| 578 |
+
for frame in value["mse_per_frame"][start_frame:]
|
| 579 |
+
]
|
| 580 |
+
ssim_values = [
|
| 581 |
+
frame
|
| 582 |
+
for value in values
|
| 583 |
+
for frame in value["ssim_per_frame"][start_frame:]
|
| 584 |
+
]
|
| 585 |
+
lpips_values = [
|
| 586 |
+
frame
|
| 587 |
+
for value in values
|
| 588 |
+
for frame in value["lpips_per_frame"][start_frame:]
|
| 589 |
+
]
|
| 590 |
+
mse = sum(mse_values) / len(mse_values)
|
| 591 |
+
affected_tail_rows.append(
|
| 592 |
+
{
|
| 593 |
+
"reuse_chunk": reuse_chunk,
|
| 594 |
+
"start_frame": start_frame,
|
| 595 |
+
"affected_frames_per_prompt": DECODED_FRAMES - start_frame,
|
| 596 |
+
"num_prompts": len(values),
|
| 597 |
+
"psnr_from_global_mse": -10 * math.log10(max(mse, 1e-12)),
|
| 598 |
+
"mean_ssim": sum(ssim_values) / len(ssim_values),
|
| 599 |
+
"mean_lpips": sum(lpips_values) / len(lpips_values),
|
| 600 |
+
}
|
| 601 |
+
)
|
| 602 |
+
|
| 603 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 604 |
+
write_csv(output_dir / "per_prompt.csv", prompt_rows)
|
| 605 |
+
write_csv(output_dir / "summary_by_reuse_chunk.csv", summary_rows)
|
| 606 |
+
write_csv(output_dir / "error_propagation_matrix.csv", propagation_rows)
|
| 607 |
+
write_csv(output_dir / "summary_affected_tail.csv", affected_tail_rows)
|
| 608 |
+
atomic_json(
|
| 609 |
+
output_dir / "summary.json",
|
| 610 |
+
{
|
| 611 |
+
"status": (
|
| 612 |
+
"complete"
|
| 613 |
+
if sorted(completed_prompt_ids) == sorted(prompt_ids)
|
| 614 |
+
else "partial"
|
| 615 |
+
),
|
| 616 |
+
"requested_prompt_ids": prompt_ids,
|
| 617 |
+
"completed_prompt_ids": completed_prompt_ids,
|
| 618 |
+
"num_completed_prompts": len(completed_prompt_ids),
|
| 619 |
+
"schedule_reference": "all chunks FFFF",
|
| 620 |
+
"schedule_intervention": (
|
| 621 |
+
f"one selected chunk {intervention_schedule}; all others FFFF"
|
| 622 |
+
),
|
| 623 |
+
"reuse_definition": (
|
| 624 |
+
"R reuses the selected chunk's most recent F flow prediction, "
|
| 625 |
+
"then applies current-timestep x0 conversion"
|
| 626 |
+
),
|
| 627 |
+
"summary_by_reuse_chunk": summary_rows,
|
| 628 |
+
"summary_affected_tail": affected_tail_rows,
|
| 629 |
+
},
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
|
| 633 |
+
@torch.inference_mode()
|
| 634 |
+
def run(args: argparse.Namespace) -> None:
|
| 635 |
+
output_dir = resolve(args.output_dir)
|
| 636 |
+
prompt_path = resolve(args.prompt_path)
|
| 637 |
+
prompts = read_prompts(prompt_path)
|
| 638 |
+
if max(args.prompt_ids) >= len(prompts):
|
| 639 |
+
raise ValueError(
|
| 640 |
+
f"Prompt ID {max(args.prompt_ids)} exceeds {len(prompts)} prompts"
|
| 641 |
+
)
|
| 642 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 643 |
+
if not args.worker:
|
| 644 |
+
atomic_json(
|
| 645 |
+
output_dir / "experiment_config.json",
|
| 646 |
+
{
|
| 647 |
+
"config_path": str(resolve(args.config_path)),
|
| 648 |
+
"checkpoint_path": str(resolve(args.checkpoint_path)),
|
| 649 |
+
"prompt_path": str(prompt_path),
|
| 650 |
+
"prompt_ids": args.prompt_ids,
|
| 651 |
+
"seed": args.seed,
|
| 652 |
+
"physical_gpu": args.gpu,
|
| 653 |
+
"use_ema": args.use_ema,
|
| 654 |
+
"low_memory": args.low_memory,
|
| 655 |
+
"latent_frames": NUM_CHUNKS * FRAMES_PER_CHUNK,
|
| 656 |
+
"decoded_frames": DECODED_FRAMES,
|
| 657 |
+
"num_chunks": NUM_CHUNKS,
|
| 658 |
+
"denoising_steps_per_chunk": NUM_DENOISING_STEPS,
|
| 659 |
+
"reference_schedule": (
|
| 660 |
+
"FFFFFFF at chunk level; FFFF within every chunk"
|
| 661 |
+
),
|
| 662 |
+
"intervention_schedule": (
|
| 663 |
+
f"one {args.intervention_schedule} chunk and "
|
| 664 |
+
f"{NUM_CHUNKS - 1} FFFF chunks"
|
| 665 |
+
),
|
| 666 |
+
"reuse_definition": (
|
| 667 |
+
"R reuses the most recent F flow prediction and applies "
|
| 668 |
+
"the current-timestep x0 conversion"
|
| 669 |
+
),
|
| 670 |
+
"metric_domain": (
|
| 671 |
+
"VAE-decoded RGB rounded to uint8 before MP4 encoding"
|
| 672 |
+
),
|
| 673 |
+
},
|
| 674 |
+
)
|
| 675 |
+
if args.aggregate_only:
|
| 676 |
+
aggregate(output_dir, args.prompt_ids, args.intervention_schedule)
|
| 677 |
+
return
|
| 678 |
+
|
| 679 |
+
pipeline = build_pipeline(args)
|
| 680 |
+
lpips_model = lpips.LPIPS(net="alex").eval()
|
| 681 |
+
if not args.low_memory:
|
| 682 |
+
lpips_model.to("cuda")
|
| 683 |
+
|
| 684 |
+
for prompt_offset, prompt_id in enumerate(args.prompt_ids, start=1):
|
| 685 |
+
prompt = prompts[prompt_id]
|
| 686 |
+
prompt_dir = output_dir / f"prompt_{prompt_id:04d}"
|
| 687 |
+
prompt_dir.mkdir(parents=True, exist_ok=True)
|
| 688 |
+
atomic_json(
|
| 689 |
+
prompt_dir / "prompt.json", {"prompt_id": prompt_id, "prompt": prompt}
|
| 690 |
+
)
|
| 691 |
+
reference_frames_path = prompt_dir / "reference_ffff_frames.safetensors"
|
| 692 |
+
reference_video_path = prompt_dir / "reference_ffff.mp4"
|
| 693 |
+
reference_metrics_path = prompt_dir / "reference_ffff.json"
|
| 694 |
+
|
| 695 |
+
print(
|
| 696 |
+
f"[prompt] {prompt_offset}/{len(args.prompt_ids)} id={prompt_id}",
|
| 697 |
+
flush=True,
|
| 698 |
+
)
|
| 699 |
+
reference_pending = args.overwrite or not reference_frames_path.exists()
|
| 700 |
+
pending_reuse_chunks: list[int] = []
|
| 701 |
+
for reuse_chunk in range(NUM_CHUNKS):
|
| 702 |
+
variant_dir = prompt_dir / f"reuse_chunk_{reuse_chunk}"
|
| 703 |
+
metrics_path = variant_dir / "metrics.json"
|
| 704 |
+
if metrics_path.exists() and not args.overwrite:
|
| 705 |
+
existing = json.loads(metrics_path.read_text(encoding="utf-8"))
|
| 706 |
+
if existing.get("status") == "complete":
|
| 707 |
+
print(f"[skip] reuse_chunk={reuse_chunk}", flush=True)
|
| 708 |
+
continue
|
| 709 |
+
pending_reuse_chunks.append(reuse_chunk)
|
| 710 |
+
|
| 711 |
+
if not reference_pending and not pending_reuse_chunks:
|
| 712 |
+
if not args.worker:
|
| 713 |
+
aggregate(output_dir, args.prompt_ids, args.intervention_schedule)
|
| 714 |
+
continue
|
| 715 |
+
|
| 716 |
+
# Generation phase: keep only T5, then only the generator, on CUDA.
|
| 717 |
+
if args.low_memory:
|
| 718 |
+
move_module(pipeline.text_encoder, "cuda")
|
| 719 |
+
conditional_dict = pipeline.text_encoder(text_prompts=[prompt])
|
| 720 |
+
if args.low_memory:
|
| 721 |
+
move_module(pipeline.text_encoder, "cpu")
|
| 722 |
+
move_module(pipeline.generator, "cuda")
|
| 723 |
+
|
| 724 |
+
generated_latents: dict[int | None, torch.Tensor] = {}
|
| 725 |
+
generation_timings: dict[int | None, dict[str, Any]] = {}
|
| 726 |
+
if reference_pending:
|
| 727 |
+
print("[run] reference FFFF", flush=True)
|
| 728 |
+
latents, timing = generate_latents(
|
| 729 |
+
pipeline=pipeline,
|
| 730 |
+
conditional_dict=conditional_dict,
|
| 731 |
+
seed=args.seed,
|
| 732 |
+
reuse_chunk=None,
|
| 733 |
+
intervention_schedule=args.intervention_schedule,
|
| 734 |
+
)
|
| 735 |
+
generated_latents[None] = latents.to(device="cpu")
|
| 736 |
+
generation_timings[None] = timing
|
| 737 |
+
del latents
|
| 738 |
+
|
| 739 |
+
for reuse_chunk in pending_reuse_chunks:
|
| 740 |
+
print(
|
| 741 |
+
f"[run] reuse_chunk={reuse_chunk}: "
|
| 742 |
+
f"{args.intervention_schedule}",
|
| 743 |
+
flush=True,
|
| 744 |
+
)
|
| 745 |
+
latents, timing = generate_latents(
|
| 746 |
+
pipeline=pipeline,
|
| 747 |
+
conditional_dict=conditional_dict,
|
| 748 |
+
seed=args.seed,
|
| 749 |
+
reuse_chunk=reuse_chunk,
|
| 750 |
+
intervention_schedule=args.intervention_schedule,
|
| 751 |
+
)
|
| 752 |
+
generated_latents[reuse_chunk] = latents.to(device="cpu")
|
| 753 |
+
generation_timings[reuse_chunk] = timing
|
| 754 |
+
del latents
|
| 755 |
+
|
| 756 |
+
if args.low_memory:
|
| 757 |
+
pipeline.kv_cache1 = None
|
| 758 |
+
pipeline.crossattn_cache = None
|
| 759 |
+
move_module(pipeline.generator, "cpu")
|
| 760 |
+
move_module(pipeline.vae, "cuda")
|
| 761 |
+
move_module(lpips_model, "cuda")
|
| 762 |
+
|
| 763 |
+
# Decode/metric phase. Keeping latents on CPU makes the phase boundary
|
| 764 |
+
# cheap and limits peak CUDA memory while other jobs occupy the GPU.
|
| 765 |
+
if reference_pending:
|
| 766 |
+
frames = decode_u8(pipeline, generated_latents.pop(None).to("cuda"))
|
| 767 |
+
save_reference_frames(reference_frames_path, frames, prompt_id)
|
| 768 |
+
if args.save_videos:
|
| 769 |
+
save_mp4(frames, reference_video_path)
|
| 770 |
+
atomic_json(
|
| 771 |
+
reference_metrics_path,
|
| 772 |
+
{
|
| 773 |
+
"status": "complete",
|
| 774 |
+
"prompt_id": prompt_id,
|
| 775 |
+
"schedule": "all chunks FFFF",
|
| 776 |
+
**generation_timings[None],
|
| 777 |
+
},
|
| 778 |
+
)
|
| 779 |
+
del frames
|
| 780 |
+
reference_frames = load_reference_frames(reference_frames_path)
|
| 781 |
+
|
| 782 |
+
for reuse_chunk in pending_reuse_chunks:
|
| 783 |
+
variant_dir = prompt_dir / f"reuse_chunk_{reuse_chunk}"
|
| 784 |
+
metrics_path = variant_dir / "metrics.json"
|
| 785 |
+
frames = decode_u8(
|
| 786 |
+
pipeline, generated_latents.pop(reuse_chunk).to("cuda")
|
| 787 |
+
)
|
| 788 |
+
metrics = frame_metrics(
|
| 789 |
+
reference_u8=reference_frames,
|
| 790 |
+
prediction_u8=frames,
|
| 791 |
+
lpips_model=lpips_model,
|
| 792 |
+
batch_size=args.metric_batch_size,
|
| 793 |
+
)
|
| 794 |
+
variant_dir.mkdir(parents=True, exist_ok=True)
|
| 795 |
+
if args.save_videos:
|
| 796 |
+
save_mp4(frames, variant_dir / "video.mp4")
|
| 797 |
+
atomic_json(
|
| 798 |
+
metrics_path,
|
| 799 |
+
{
|
| 800 |
+
"status": "complete",
|
| 801 |
+
"prompt_id": prompt_id,
|
| 802 |
+
"reuse_chunk": reuse_chunk,
|
| 803 |
+
"schedule": (
|
| 804 |
+
f"chunk {reuse_chunk}={args.intervention_schedule}; "
|
| 805 |
+
"all other chunks=FFFF"
|
| 806 |
+
),
|
| 807 |
+
"reference": "matching FFFF; same prompt, seed, and RNG sequence",
|
| 808 |
+
**generation_timings[reuse_chunk],
|
| 809 |
+
**metrics,
|
| 810 |
+
},
|
| 811 |
+
)
|
| 812 |
+
print(
|
| 813 |
+
f"[metric] chunk={reuse_chunk} PSNR={metrics['psnr']:.4f} "
|
| 814 |
+
f"SSIM={metrics['ssim']:.6f} LPIPS={metrics['lpips']:.6f}",
|
| 815 |
+
flush=True,
|
| 816 |
+
)
|
| 817 |
+
del frames
|
| 818 |
+
torch.cuda.empty_cache()
|
| 819 |
+
|
| 820 |
+
if args.low_memory:
|
| 821 |
+
move_module(pipeline.vae, "cpu")
|
| 822 |
+
move_module(lpips_model, "cpu")
|
| 823 |
+
del reference_frames, conditional_dict, generated_latents
|
| 824 |
+
if not args.worker:
|
| 825 |
+
aggregate(output_dir, args.prompt_ids, args.intervention_schedule)
|
| 826 |
+
|
| 827 |
+
if not args.worker:
|
| 828 |
+
aggregate(output_dir, args.prompt_ids, args.intervention_schedule)
|
| 829 |
+
print(f"[done] {output_dir}", flush=True)
|
| 830 |
+
|
| 831 |
+
|
| 832 |
+
def main() -> None:
|
| 833 |
+
args = parse_args()
|
| 834 |
+
run(args)
|
| 835 |
+
|
| 836 |
+
|
| 837 |
+
if __name__ == "__main__":
|
| 838 |
+
main()
|
scripts/evaluate_three_block_fppf.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Evaluate consecutive three-block Predictors with the FPPF protocol."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
from evaluate_two_block_fppf import main
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
DEFAULTS = {
|
| 12 |
+
"--architecture": "three_block",
|
| 13 |
+
"--sweep_dir": "outputs/three_block_consecutive_sweep",
|
| 14 |
+
"--output_dir": "outputs/three_block_fppf_eval",
|
| 15 |
+
"--reference_root": "outputs/single_block_fppf_eval",
|
| 16 |
+
"--schedule": "FPPF",
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
if __name__ == "__main__":
|
| 21 |
+
present = set(sys.argv[1:])
|
| 22 |
+
for option, value in DEFAULTS.items():
|
| 23 |
+
if option not in present:
|
| 24 |
+
sys.argv.extend([option, value])
|
| 25 |
+
main()
|
scripts/evaluate_trained_long_predictors.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Evaluate 2x/4x-trained Layer-17 predictors at 1x, 2x, and 4x."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def preparse_gpu() -> str:
|
| 14 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 15 |
+
parser.add_argument("--gpu", required=True)
|
| 16 |
+
args, _ = parser.parse_known_args()
|
| 17 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu
|
| 18 |
+
return args.gpu
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
GPU = preparse_gpu()
|
| 22 |
+
|
| 23 |
+
import lpips
|
| 24 |
+
import torch
|
| 25 |
+
from omegaconf import OmegaConf
|
| 26 |
+
|
| 27 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 28 |
+
if str(ROOT) not in sys.path:
|
| 29 |
+
sys.path.insert(0, str(ROOT))
|
| 30 |
+
|
| 31 |
+
from scripts.evaluate_long_video_fppf import generate_rollout, save_mp4
|
| 32 |
+
from scripts.evaluate_single_block_fppf import (
|
| 33 |
+
atomic_json, build_pipeline, frame_metrics, load_predictor,
|
| 34 |
+
load_prompt_metadata, pixels_to_u8,
|
| 35 |
+
)
|
| 36 |
+
from utils.misc import set_seed
|
| 37 |
+
from utils.wan_wrapper import WanVAEWrapper
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
PREDICTORS = {
|
| 41 |
+
"trained_2x": ROOT / "outputs/layer17_long_training_four_gpu_v2/2x/predictor_final.safetensors",
|
| 42 |
+
"trained_4x": ROOT / "outputs/layer17_long_training_four_gpu_v2/4x/predictor_final.safetensors",
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def main() -> None:
|
| 47 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 48 |
+
parser.add_argument("--gpu", default=GPU)
|
| 49 |
+
parser.add_argument("--prompt_ids", type=int, nargs="+", required=True)
|
| 50 |
+
parser.add_argument("--latent_lengths", type=int, nargs="+", default=[21, 42, 84])
|
| 51 |
+
parser.add_argument(
|
| 52 |
+
"--dataset_root", type=Path,
|
| 53 |
+
default=Path("outputs/predictor_offline_100_all_blocks"),
|
| 54 |
+
)
|
| 55 |
+
parser.add_argument(
|
| 56 |
+
"--output_dir", type=Path,
|
| 57 |
+
default=Path("outputs/layer17_long_training_eval"),
|
| 58 |
+
)
|
| 59 |
+
parser.add_argument("--generation_seed", type=int, default=0)
|
| 60 |
+
parser.add_argument("--metric_batch_size", type=int, default=4)
|
| 61 |
+
args = parser.parse_args()
|
| 62 |
+
args.dataset_root = (ROOT / args.dataset_root).resolve() if not args.dataset_root.is_absolute() else args.dataset_root
|
| 63 |
+
args.output_dir = (ROOT / args.output_dir).resolve() if not args.output_dir.is_absolute() else args.output_dir
|
| 64 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 65 |
+
|
| 66 |
+
device = torch.device("cuda")
|
| 67 |
+
set_seed(args.generation_seed)
|
| 68 |
+
config = OmegaConf.merge(
|
| 69 |
+
OmegaConf.load(ROOT / "configs/default_config.yaml"),
|
| 70 |
+
OmegaConf.load(ROOT / "configs/self_forcing_sid.yaml"),
|
| 71 |
+
)
|
| 72 |
+
config.model_kwargs.local_attn_size = 21
|
| 73 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 74 |
+
pipeline = build_pipeline(
|
| 75 |
+
config, ROOT / "checkpoints/self_forcing_dmd.pt", vae, device,
|
| 76 |
+
)
|
| 77 |
+
predictors = {
|
| 78 |
+
name: load_predictor(
|
| 79 |
+
pipeline.generator.model,
|
| 80 |
+
{"source_layer": 17, "weights": path},
|
| 81 |
+
device,
|
| 82 |
+
)
|
| 83 |
+
for name, path in PREDICTORS.items()
|
| 84 |
+
}
|
| 85 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 86 |
+
lpips_model.requires_grad_(False)
|
| 87 |
+
|
| 88 |
+
for prompt_id in args.prompt_ids:
|
| 89 |
+
prompt = load_prompt_metadata(args.dataset_root, prompt_id)["prompt"]
|
| 90 |
+
for latent_length in args.latent_lengths:
|
| 91 |
+
run_dir = args.output_dir / f"latent_{latent_length}" / f"prompt_{prompt_id:04d}"
|
| 92 |
+
result_path = run_dir / "metrics.json"
|
| 93 |
+
if result_path.exists():
|
| 94 |
+
existing = json.loads(result_path.read_text())
|
| 95 |
+
if existing.get("status") == "complete":
|
| 96 |
+
print(f"[skip] prompt={prompt_id} latent={latent_length}", flush=True)
|
| 97 |
+
continue
|
| 98 |
+
|
| 99 |
+
print(f"[run] prompt={prompt_id} latent={latent_length} FFFF", flush=True)
|
| 100 |
+
reference_latent, ffff_counts = generate_rollout(
|
| 101 |
+
pipeline=pipeline, dataset_root=args.dataset_root,
|
| 102 |
+
prompt_id=prompt_id, latent_length=latent_length,
|
| 103 |
+
generation_seed=args.generation_seed, device=device,
|
| 104 |
+
predictor=None, source_layer=None, schedule="FFFF",
|
| 105 |
+
)
|
| 106 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 107 |
+
reference_pixels = vae.decode_to_pixel(reference_latent, use_cache=False)
|
| 108 |
+
reference_u8 = pixels_to_u8(reference_pixels)
|
| 109 |
+
save_mp4(reference_u8, run_dir / "ffff.mp4")
|
| 110 |
+
del reference_latent, reference_pixels
|
| 111 |
+
if hasattr(vae.model, "clear_cache"):
|
| 112 |
+
vae.model.clear_cache()
|
| 113 |
+
torch.cuda.empty_cache()
|
| 114 |
+
|
| 115 |
+
results = {}
|
| 116 |
+
for name, predictor in predictors.items():
|
| 117 |
+
print(f"[run] prompt={prompt_id} latent={latent_length} {name}", flush=True)
|
| 118 |
+
latent, counts = generate_rollout(
|
| 119 |
+
pipeline=pipeline, dataset_root=args.dataset_root,
|
| 120 |
+
prompt_id=prompt_id, latent_length=latent_length,
|
| 121 |
+
generation_seed=args.generation_seed, device=device,
|
| 122 |
+
predictor=predictor, source_layer=17, schedule="FPPF",
|
| 123 |
+
)
|
| 124 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 125 |
+
pixels = vae.decode_to_pixel(latent, use_cache=False)
|
| 126 |
+
prediction_u8 = pixels_to_u8(pixels)
|
| 127 |
+
save_mp4(prediction_u8, run_dir / f"{name}.mp4")
|
| 128 |
+
metrics = frame_metrics(
|
| 129 |
+
reference_u8=reference_u8,
|
| 130 |
+
prediction_u8=prediction_u8,
|
| 131 |
+
lpips_model=lpips_model,
|
| 132 |
+
batch_size=args.metric_batch_size,
|
| 133 |
+
device=device,
|
| 134 |
+
)
|
| 135 |
+
results[name] = {"fppf": counts, **metrics}
|
| 136 |
+
print(
|
| 137 |
+
f"[result] {name} prompt={prompt_id} latent={latent_length} "
|
| 138 |
+
f"psnr={metrics['psnr']:.4f} ssim={metrics['ssim']:.6f} "
|
| 139 |
+
f"lpips={metrics['lpips']:.6f}", flush=True,
|
| 140 |
+
)
|
| 141 |
+
del latent, pixels, prediction_u8
|
| 142 |
+
if hasattr(vae.model, "clear_cache"):
|
| 143 |
+
vae.model.clear_cache()
|
| 144 |
+
torch.cuda.empty_cache()
|
| 145 |
+
|
| 146 |
+
atomic_json(result_path, {
|
| 147 |
+
"status": "complete", "prompt_id": prompt_id,
|
| 148 |
+
"prompt": prompt, "latent_length": latent_length,
|
| 149 |
+
"decoded_frames": next(iter(results.values()))["num_frames"],
|
| 150 |
+
"reference": "FFFF same prompt/seed/noise",
|
| 151 |
+
"ffff": ffff_counts, "predictors": results,
|
| 152 |
+
})
|
| 153 |
+
del reference_u8
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
if __name__ == "__main__":
|
| 157 |
+
main()
|
scripts/evaluate_two_block_fppf.py
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Evaluate trained multi-block Predictors against matching FFFF rollouts."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import time
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _preparse_gpu() -> str:
|
| 17 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 18 |
+
parser.add_argument("--gpu", default="2")
|
| 19 |
+
args, _ = parser.parse_known_args()
|
| 20 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 21 |
+
return str(args.gpu)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 25 |
+
|
| 26 |
+
import lpips
|
| 27 |
+
import torch
|
| 28 |
+
from omegaconf import OmegaConf
|
| 29 |
+
from safetensors.torch import load_file
|
| 30 |
+
|
| 31 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 32 |
+
if str(REPO_ROOT) not in sys.path:
|
| 33 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 34 |
+
|
| 35 |
+
from predictor_training.offline_data import TOKENS_PER_CHUNK
|
| 36 |
+
from predictor_training.three_block import ThreeBlockPredictor
|
| 37 |
+
from predictor_training.two_block import TwoBlockPredictor
|
| 38 |
+
from scripts.evaluate_single_block_fppf import (
|
| 39 |
+
DEFAULT_PROMPT_IDS,
|
| 40 |
+
FRAMES_PER_CHUNK,
|
| 41 |
+
LATENT_CHANNELS,
|
| 42 |
+
LATENT_HEIGHT,
|
| 43 |
+
LATENT_WIDTH,
|
| 44 |
+
NUM_CHUNKS,
|
| 45 |
+
NUM_DENOISING_STEPS,
|
| 46 |
+
FinalHiddenCapture,
|
| 47 |
+
aggregate_prompt_results,
|
| 48 |
+
atomic_json,
|
| 49 |
+
build_pipeline,
|
| 50 |
+
frame_metrics,
|
| 51 |
+
load_ffff_latent,
|
| 52 |
+
load_prompt_metadata,
|
| 53 |
+
load_reference_frames,
|
| 54 |
+
pixels_to_u8,
|
| 55 |
+
prepare_reference_frames,
|
| 56 |
+
reset_kv_and_load_cross_cache,
|
| 57 |
+
)
|
| 58 |
+
from scripts.run_single_block_init_sweep import hidden_to_flow
|
| 59 |
+
from utils.misc import set_seed
|
| 60 |
+
from utils.wan_wrapper import WanVAEWrapper
|
| 61 |
+
from wan.modules.model import sinusoidal_embedding_1d
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def parse_args() -> argparse.Namespace:
|
| 65 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 66 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 67 |
+
parser.add_argument(
|
| 68 |
+
"--architecture",
|
| 69 |
+
choices=("two_block", "three_block"),
|
| 70 |
+
default="two_block",
|
| 71 |
+
help="Predictor architecture represented by the sweep directory.",
|
| 72 |
+
)
|
| 73 |
+
parser.add_argument(
|
| 74 |
+
"--config_path", type=Path, default=Path("configs/self_forcing_sid.yaml")
|
| 75 |
+
)
|
| 76 |
+
parser.add_argument(
|
| 77 |
+
"--checkpoint_path",
|
| 78 |
+
type=Path,
|
| 79 |
+
default=Path("checkpoints/self_forcing_dmd.pt"),
|
| 80 |
+
)
|
| 81 |
+
parser.add_argument(
|
| 82 |
+
"--dataset_root",
|
| 83 |
+
type=Path,
|
| 84 |
+
default=Path("outputs/predictor_offline_100_all_blocks"),
|
| 85 |
+
)
|
| 86 |
+
parser.add_argument(
|
| 87 |
+
"--sweep_dir", type=Path, default=Path("outputs/two_block_pair_sweep")
|
| 88 |
+
)
|
| 89 |
+
parser.add_argument(
|
| 90 |
+
"--output_dir", type=Path, default=Path("outputs/two_block_pair_fppf_eval")
|
| 91 |
+
)
|
| 92 |
+
parser.add_argument(
|
| 93 |
+
"--schedule",
|
| 94 |
+
choices=("FPPF", "FPPP"),
|
| 95 |
+
default="FPPF",
|
| 96 |
+
help=(
|
| 97 |
+
"Denoising schedule for chunks 1-6; chunk 0 always uses FFFF. "
|
| 98 |
+
"FPPF predicts steps 1-2, while FPPP predicts steps 1-3."
|
| 99 |
+
),
|
| 100 |
+
)
|
| 101 |
+
parser.add_argument(
|
| 102 |
+
"--reference_root",
|
| 103 |
+
type=Path,
|
| 104 |
+
default=Path("outputs/single_block_fppf_eval"),
|
| 105 |
+
help="Directory containing reusable ffff_reference_frames/.",
|
| 106 |
+
)
|
| 107 |
+
parser.add_argument(
|
| 108 |
+
"--prompt_ids", type=int, nargs="*", default=DEFAULT_PROMPT_IDS
|
| 109 |
+
)
|
| 110 |
+
parser.add_argument("--experiments", nargs="*", default=None)
|
| 111 |
+
parser.add_argument("--max_prompts", type=int, default=None)
|
| 112 |
+
parser.add_argument("--max_experiments", type=int, default=None)
|
| 113 |
+
parser.add_argument("--metric_batch_size", type=int, default=4)
|
| 114 |
+
parser.add_argument("--generation_seed", type=int, default=0)
|
| 115 |
+
parser.add_argument(
|
| 116 |
+
"--verify_ffff", action=argparse.BooleanOptionalAction, default=True
|
| 117 |
+
)
|
| 118 |
+
parser.add_argument(
|
| 119 |
+
"--skip_lpips", action=argparse.BooleanOptionalAction, default=False
|
| 120 |
+
)
|
| 121 |
+
args = parser.parse_args()
|
| 122 |
+
if not args.prompt_ids:
|
| 123 |
+
parser.error("At least one prompt ID is required")
|
| 124 |
+
if args.metric_batch_size < 1:
|
| 125 |
+
parser.error("--metric_batch_size must be positive")
|
| 126 |
+
return args
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def resolve(path: Path) -> Path:
|
| 130 |
+
path = path.expanduser()
|
| 131 |
+
return path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve()
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def discover_experiments(
|
| 135 |
+
sweep_dir: Path,
|
| 136 |
+
requested: list[str] | None,
|
| 137 |
+
max_experiments: int | None,
|
| 138 |
+
architecture: str = "two_block",
|
| 139 |
+
) -> list[dict[str, Any]]:
|
| 140 |
+
with (sweep_dir / "summary.csv").open(
|
| 141 |
+
"r", encoding="utf-8", newline=""
|
| 142 |
+
) as handle:
|
| 143 |
+
rows = list(csv.DictReader(handle))
|
| 144 |
+
by_name = {row["name"]: row for row in rows}
|
| 145 |
+
names = list(by_name) if requested is None else requested
|
| 146 |
+
unknown = [name for name in names if name not in by_name]
|
| 147 |
+
if unknown:
|
| 148 |
+
raise KeyError(f"Unknown experiments: {unknown}")
|
| 149 |
+
if max_experiments is not None:
|
| 150 |
+
names = names[:max_experiments]
|
| 151 |
+
|
| 152 |
+
output = []
|
| 153 |
+
for name in names:
|
| 154 |
+
run_dir = sweep_dir / name
|
| 155 |
+
config = json.loads((run_dir / "config.json").read_text(encoding="utf-8"))
|
| 156 |
+
weights = run_dir / "predictor_final.safetensors"
|
| 157 |
+
if not weights.exists():
|
| 158 |
+
raise FileNotFoundError(weights)
|
| 159 |
+
row = by_name[name]
|
| 160 |
+
kind_key = "pair_kind" if architecture == "two_block" else "triple_kind"
|
| 161 |
+
output.append(
|
| 162 |
+
{
|
| 163 |
+
"name": name,
|
| 164 |
+
"source_layers": [int(value) for value in config["source_layers"]],
|
| 165 |
+
"experiment_kind": config[kind_key],
|
| 166 |
+
"initialization_method": "teacher_full",
|
| 167 |
+
"weights": weights,
|
| 168 |
+
"offline_final_val_flow_mse": float(row["final_val_flow_mse"]),
|
| 169 |
+
"offline_final_val_hidden_mse": float(
|
| 170 |
+
row["final_val_hidden_mse"]
|
| 171 |
+
),
|
| 172 |
+
}
|
| 173 |
+
)
|
| 174 |
+
return output
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def load_predictor(
|
| 178 |
+
teacher: torch.nn.Module,
|
| 179 |
+
experiment: dict[str, Any],
|
| 180 |
+
device: torch.device,
|
| 181 |
+
) -> TwoBlockPredictor | ThreeBlockPredictor:
|
| 182 |
+
source_layers = experiment["source_layers"]
|
| 183 |
+
predictor_class = (
|
| 184 |
+
TwoBlockPredictor if len(source_layers) == 2 else ThreeBlockPredictor
|
| 185 |
+
)
|
| 186 |
+
predictor = predictor_class(
|
| 187 |
+
[teacher.blocks[layer] for layer in source_layers],
|
| 188 |
+
dim=teacher.dim,
|
| 189 |
+
gradient_checkpointing=False,
|
| 190 |
+
)
|
| 191 |
+
predictor.load_state_dict(
|
| 192 |
+
load_file(str(experiment["weights"]), device="cpu"), strict=True
|
| 193 |
+
)
|
| 194 |
+
predictor.to(device=device).eval().requires_grad_(False)
|
| 195 |
+
return predictor
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@torch.inference_mode()
|
| 199 |
+
def predictor_step(
|
| 200 |
+
*,
|
| 201 |
+
predictor: TwoBlockPredictor | ThreeBlockPredictor,
|
| 202 |
+
teacher: torch.nn.Module,
|
| 203 |
+
noisy_input: torch.Tensor,
|
| 204 |
+
timestep: torch.Tensor,
|
| 205 |
+
anchor_hidden: torch.Tensor,
|
| 206 |
+
previous_hidden: torch.Tensor,
|
| 207 |
+
history_caches: list[dict[str, torch.Tensor]],
|
| 208 |
+
cross_caches: list[dict[str, torch.Tensor]],
|
| 209 |
+
current_start: int,
|
| 210 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 211 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 212 |
+
current_tokens = teacher.patch_embedding(
|
| 213 |
+
noisy_input.permute(0, 2, 1, 3, 4)
|
| 214 |
+
).flatten(2).transpose(1, 2)
|
| 215 |
+
time_embedding = teacher.time_embedding(
|
| 216 |
+
sinusoidal_embedding_1d(
|
| 217 |
+
teacher.freq_dim, timestep.flatten()
|
| 218 |
+
).type_as(current_tokens)
|
| 219 |
+
)
|
| 220 |
+
timestep_modulation = teacher.time_projection(
|
| 221 |
+
time_embedding
|
| 222 |
+
).unflatten(1, (6, teacher.dim)).unflatten(
|
| 223 |
+
dim=0, sizes=timestep.shape
|
| 224 |
+
)
|
| 225 |
+
head_embedding = time_embedding.unflatten(
|
| 226 |
+
dim=0, sizes=timestep.shape
|
| 227 |
+
).unsqueeze(2)
|
| 228 |
+
grid_sizes = torch.tensor(
|
| 229 |
+
[[FRAMES_PER_CHUNK, 30, 52]], dtype=torch.long, device="cpu"
|
| 230 |
+
)
|
| 231 |
+
pred_hidden = predictor(
|
| 232 |
+
current_tokens=current_tokens,
|
| 233 |
+
anchor_hidden=anchor_hidden,
|
| 234 |
+
previous_hidden=previous_hidden,
|
| 235 |
+
timestep_modulation=timestep_modulation,
|
| 236 |
+
grid_sizes=grid_sizes,
|
| 237 |
+
freqs=teacher.freqs,
|
| 238 |
+
history_ks=[
|
| 239 |
+
cache["k"][:, :current_start] for cache in history_caches
|
| 240 |
+
],
|
| 241 |
+
history_vs=[
|
| 242 |
+
cache["v"][:, :current_start] for cache in history_caches
|
| 243 |
+
],
|
| 244 |
+
cross_ks=[cache["k"] for cache in cross_caches],
|
| 245 |
+
cross_vs=[cache["v"] for cache in cross_caches],
|
| 246 |
+
current_start=current_start,
|
| 247 |
+
)
|
| 248 |
+
pred_flow = hidden_to_flow(
|
| 249 |
+
pred_hidden, head_embedding, grid_sizes, teacher
|
| 250 |
+
)
|
| 251 |
+
return pred_hidden, pred_flow
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
@torch.inference_mode()
|
| 255 |
+
def generate_rollout(
|
| 256 |
+
*,
|
| 257 |
+
pipeline,
|
| 258 |
+
dataset_root: Path,
|
| 259 |
+
prompt_id: int,
|
| 260 |
+
generation_seed: int,
|
| 261 |
+
device: torch.device,
|
| 262 |
+
predictor: TwoBlockPredictor | ThreeBlockPredictor | None,
|
| 263 |
+
source_layers: list[int] | None,
|
| 264 |
+
schedule: str,
|
| 265 |
+
) -> tuple[torch.Tensor, dict[str, float | int]]:
|
| 266 |
+
if schedule not in {"FFFF", "FPPF", "FPPP"}:
|
| 267 |
+
raise ValueError(schedule)
|
| 268 |
+
if schedule != "FFFF" and (predictor is None or source_layers is None):
|
| 269 |
+
raise ValueError(f"{schedule} requires a Predictor and source layers")
|
| 270 |
+
reset_kv_and_load_cross_cache(pipeline, dataset_root, prompt_id, device)
|
| 271 |
+
set_seed(generation_seed)
|
| 272 |
+
noise = torch.randn(
|
| 273 |
+
1,
|
| 274 |
+
NUM_CHUNKS * FRAMES_PER_CHUNK,
|
| 275 |
+
LATENT_CHANNELS,
|
| 276 |
+
LATENT_HEIGHT,
|
| 277 |
+
LATENT_WIDTH,
|
| 278 |
+
dtype=torch.bfloat16,
|
| 279 |
+
device=device,
|
| 280 |
+
)
|
| 281 |
+
teacher = pipeline.generator.model
|
| 282 |
+
text_dim = int(teacher.text_embedding[0].in_features)
|
| 283 |
+
conditional_dict = {
|
| 284 |
+
"prompt_embeds": torch.zeros(
|
| 285 |
+
1, 1, text_dim, dtype=torch.bfloat16, device=device
|
| 286 |
+
)
|
| 287 |
+
}
|
| 288 |
+
timesteps = pipeline.denoising_step_list.to(device=device)
|
| 289 |
+
output_chunks: list[torch.Tensor] = []
|
| 290 |
+
previous_chunk_hidden: list[torch.Tensor | None] | None = None
|
| 291 |
+
capture = FinalHiddenCapture(teacher)
|
| 292 |
+
full_calls = 0
|
| 293 |
+
predictor_calls = 0
|
| 294 |
+
started = time.perf_counter()
|
| 295 |
+
|
| 296 |
+
try:
|
| 297 |
+
for chunk in range(NUM_CHUNKS):
|
| 298 |
+
noisy_input = noise[
|
| 299 |
+
:, chunk * FRAMES_PER_CHUNK : (chunk + 1) * FRAMES_PER_CHUNK
|
| 300 |
+
]
|
| 301 |
+
current_hidden: list[torch.Tensor | None] = [None] * NUM_DENOISING_STEPS
|
| 302 |
+
denoised_pred = None
|
| 303 |
+
timestep = None
|
| 304 |
+
for step, current_timestep in enumerate(timesteps):
|
| 305 |
+
timestep = torch.ones(
|
| 306 |
+
[1, FRAMES_PER_CHUNK], dtype=torch.int64, device=device
|
| 307 |
+
) * current_timestep
|
| 308 |
+
predictor_steps = {1, 2} if schedule == "FPPF" else {1, 2, 3}
|
| 309 |
+
use_predictor = (
|
| 310 |
+
schedule != "FFFF" and chunk > 0 and step in predictor_steps
|
| 311 |
+
)
|
| 312 |
+
if use_predictor:
|
| 313 |
+
anchor_hidden = current_hidden[step - 1]
|
| 314 |
+
assert anchor_hidden is not None
|
| 315 |
+
assert previous_chunk_hidden is not None
|
| 316 |
+
previous_hidden = previous_chunk_hidden[step]
|
| 317 |
+
assert previous_hidden is not None
|
| 318 |
+
pred_hidden, flow = predictor_step(
|
| 319 |
+
predictor=predictor,
|
| 320 |
+
teacher=teacher,
|
| 321 |
+
noisy_input=noisy_input,
|
| 322 |
+
timestep=timestep,
|
| 323 |
+
anchor_hidden=anchor_hidden,
|
| 324 |
+
previous_hidden=previous_hidden,
|
| 325 |
+
history_caches=[
|
| 326 |
+
pipeline.kv_cache1[layer] for layer in source_layers
|
| 327 |
+
],
|
| 328 |
+
cross_caches=[
|
| 329 |
+
pipeline.crossattn_cache[layer]
|
| 330 |
+
for layer in source_layers
|
| 331 |
+
],
|
| 332 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 333 |
+
)
|
| 334 |
+
denoised_pred = pipeline.generator._convert_flow_pred_to_x0(
|
| 335 |
+
flow_pred=flow.flatten(0, 1),
|
| 336 |
+
xt=noisy_input.flatten(0, 1),
|
| 337 |
+
timestep=timestep.flatten(0, 1),
|
| 338 |
+
).unflatten(0, flow.shape[:2])
|
| 339 |
+
current_hidden[step] = pred_hidden
|
| 340 |
+
predictor_calls += 1
|
| 341 |
+
else:
|
| 342 |
+
capture.start()
|
| 343 |
+
_, denoised_pred = pipeline.generator(
|
| 344 |
+
noisy_image_or_video=noisy_input,
|
| 345 |
+
conditional_dict=conditional_dict,
|
| 346 |
+
timestep=timestep,
|
| 347 |
+
kv_cache=pipeline.kv_cache1,
|
| 348 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 349 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 350 |
+
)
|
| 351 |
+
current_hidden[step] = capture.finish()
|
| 352 |
+
full_calls += 1
|
| 353 |
+
|
| 354 |
+
if step < NUM_DENOISING_STEPS - 1:
|
| 355 |
+
next_timestep = timesteps[step + 1]
|
| 356 |
+
denoised_flat = denoised_pred.flatten(0, 1)
|
| 357 |
+
noisy_input = pipeline.scheduler.add_noise(
|
| 358 |
+
denoised_flat,
|
| 359 |
+
torch.randn_like(denoised_flat),
|
| 360 |
+
next_timestep
|
| 361 |
+
* torch.ones(
|
| 362 |
+
[FRAMES_PER_CHUNK], dtype=torch.long, device=device
|
| 363 |
+
),
|
| 364 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 365 |
+
|
| 366 |
+
if denoised_pred is None or timestep is None:
|
| 367 |
+
raise RuntimeError("Denoising loop produced no clean latent")
|
| 368 |
+
output_chunks.append(denoised_pred)
|
| 369 |
+
pipeline.generator(
|
| 370 |
+
noisy_image_or_video=denoised_pred,
|
| 371 |
+
conditional_dict=conditional_dict,
|
| 372 |
+
timestep=torch.ones_like(timestep) * pipeline.args.context_noise,
|
| 373 |
+
kv_cache=pipeline.kv_cache1,
|
| 374 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 375 |
+
current_start=chunk * TOKENS_PER_CHUNK,
|
| 376 |
+
)
|
| 377 |
+
previous_chunk_hidden = current_hidden
|
| 378 |
+
finally:
|
| 379 |
+
capture.close()
|
| 380 |
+
|
| 381 |
+
torch.cuda.synchronize()
|
| 382 |
+
return torch.cat(output_chunks, dim=1), {
|
| 383 |
+
"generation_time_s": time.perf_counter() - started,
|
| 384 |
+
"full_calls": full_calls,
|
| 385 |
+
"predictor_calls": predictor_calls,
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def write_summary(output_dir: Path, experiments: list[dict[str, Any]]) -> None:
|
| 390 |
+
rows = []
|
| 391 |
+
for experiment in experiments:
|
| 392 |
+
path = output_dir / experiment["name"] / "metrics.json"
|
| 393 |
+
if not path.exists():
|
| 394 |
+
continue
|
| 395 |
+
metrics = json.loads(path.read_text(encoding="utf-8"))
|
| 396 |
+
if metrics.get("status") != "complete":
|
| 397 |
+
continue
|
| 398 |
+
source_layers = metrics["source_layers"]
|
| 399 |
+
kind_field = "pair_kind" if len(source_layers) == 2 else "triple_kind"
|
| 400 |
+
row = {
|
| 401 |
+
"name": metrics["name"],
|
| 402 |
+
**{
|
| 403 |
+
f"source_layer_{index + 1}": layer
|
| 404 |
+
for index, layer in enumerate(source_layers)
|
| 405 |
+
},
|
| 406 |
+
kind_field: metrics.get(
|
| 407 |
+
kind_field, metrics.get("experiment_kind")
|
| 408 |
+
),
|
| 409 |
+
"schedule": metrics["schedule"],
|
| 410 |
+
"num_prompts": metrics["num_prompts"],
|
| 411 |
+
"psnr": metrics["psnr"],
|
| 412 |
+
"ssim": metrics["ssim"],
|
| 413 |
+
"lpips": metrics["lpips"],
|
| 414 |
+
"rollout_psnr": metrics["rollout_psnr"],
|
| 415 |
+
"rollout_ssim": metrics["rollout_ssim"],
|
| 416 |
+
"rollout_lpips": metrics["rollout_lpips"],
|
| 417 |
+
"offline_final_val_flow_mse": metrics[
|
| 418 |
+
"offline_final_val_flow_mse"
|
| 419 |
+
],
|
| 420 |
+
"mean_generation_time_s": metrics["mean_generation_time_s"],
|
| 421 |
+
}
|
| 422 |
+
rows.append(row)
|
| 423 |
+
rows.sort(key=lambda row: float(row["lpips"]))
|
| 424 |
+
if not rows:
|
| 425 |
+
return
|
| 426 |
+
destination = output_dir / "summary.csv"
|
| 427 |
+
temporary = destination.with_suffix(".csv.tmp")
|
| 428 |
+
with temporary.open("w", encoding="utf-8", newline="") as handle:
|
| 429 |
+
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
|
| 430 |
+
writer.writeheader()
|
| 431 |
+
writer.writerows(rows)
|
| 432 |
+
os.replace(temporary, destination)
|
| 433 |
+
atomic_json(output_dir / "summary.json", rows)
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def main() -> None:
|
| 437 |
+
args = parse_args()
|
| 438 |
+
args.config_path = resolve(args.config_path)
|
| 439 |
+
args.checkpoint_path = resolve(args.checkpoint_path)
|
| 440 |
+
args.dataset_root = resolve(args.dataset_root)
|
| 441 |
+
args.sweep_dir = resolve(args.sweep_dir)
|
| 442 |
+
args.output_dir = resolve(args.output_dir)
|
| 443 |
+
args.reference_root = resolve(args.reference_root)
|
| 444 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 445 |
+
|
| 446 |
+
prompt_ids = sorted(set(args.prompt_ids))
|
| 447 |
+
if args.max_prompts is not None:
|
| 448 |
+
prompt_ids = prompt_ids[: args.max_prompts]
|
| 449 |
+
experiments = discover_experiments(
|
| 450 |
+
args.sweep_dir, args.experiments, args.max_experiments, args.architecture
|
| 451 |
+
)
|
| 452 |
+
device = torch.device("cuda")
|
| 453 |
+
torch.set_grad_enabled(False)
|
| 454 |
+
set_seed(args.generation_seed)
|
| 455 |
+
config = OmegaConf.merge(
|
| 456 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 457 |
+
OmegaConf.load(args.config_path),
|
| 458 |
+
)
|
| 459 |
+
schedule_description = f"chunk0=FFFF; chunks1-6={args.schedule}"
|
| 460 |
+
manifest = {
|
| 461 |
+
"status": "running",
|
| 462 |
+
"architecture": f"{args.architecture}_predictor",
|
| 463 |
+
"prompt_ids": prompt_ids,
|
| 464 |
+
"experiments": [item["name"] for item in experiments],
|
| 465 |
+
"rollout_schedule": args.schedule,
|
| 466 |
+
"rollout_definition": schedule_description,
|
| 467 |
+
"reference_root": str(args.reference_root),
|
| 468 |
+
"generation_seed_reset_per_prompt": args.generation_seed,
|
| 469 |
+
}
|
| 470 |
+
atomic_json(args.output_dir / "manifest.json", manifest)
|
| 471 |
+
|
| 472 |
+
print("[setup] loading VAE and checking FFFF reference frames", flush=True)
|
| 473 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 474 |
+
prepare_reference_frames(
|
| 475 |
+
vae=vae,
|
| 476 |
+
dataset_root=args.dataset_root,
|
| 477 |
+
output_dir=args.reference_root,
|
| 478 |
+
prompt_ids=prompt_ids,
|
| 479 |
+
device=device,
|
| 480 |
+
rebuild=False,
|
| 481 |
+
)
|
| 482 |
+
print("[setup] loading frozen generator_ema", flush=True)
|
| 483 |
+
pipeline = build_pipeline(config, args.checkpoint_path, vae, device)
|
| 484 |
+
teacher = pipeline.generator.model
|
| 485 |
+
lpips_model = None
|
| 486 |
+
if not args.skip_lpips:
|
| 487 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 488 |
+
lpips_model.requires_grad_(False)
|
| 489 |
+
|
| 490 |
+
if args.verify_ffff:
|
| 491 |
+
prompt_id = prompt_ids[0]
|
| 492 |
+
reproduced, counts = generate_rollout(
|
| 493 |
+
pipeline=pipeline,
|
| 494 |
+
dataset_root=args.dataset_root,
|
| 495 |
+
prompt_id=prompt_id,
|
| 496 |
+
generation_seed=args.generation_seed,
|
| 497 |
+
device=device,
|
| 498 |
+
predictor=None,
|
| 499 |
+
source_layers=None,
|
| 500 |
+
schedule="FFFF",
|
| 501 |
+
)
|
| 502 |
+
expected = load_ffff_latent(args.dataset_root, prompt_id).to(
|
| 503 |
+
device=device, dtype=torch.bfloat16
|
| 504 |
+
)
|
| 505 |
+
difference = reproduced.float() - expected.float()
|
| 506 |
+
verification = {
|
| 507 |
+
"prompt_id": prompt_id,
|
| 508 |
+
"max_abs_latent_error": float(difference.abs().max()),
|
| 509 |
+
"latent_mse": float(difference.square().mean()),
|
| 510 |
+
**counts,
|
| 511 |
+
}
|
| 512 |
+
atomic_json(args.output_dir / "ffff_reproduction.json", verification)
|
| 513 |
+
print(f"[verify] {verification}", flush=True)
|
| 514 |
+
if verification["max_abs_latent_error"] > 1e-3:
|
| 515 |
+
raise RuntimeError("FFFF reproduction does not match offline reference")
|
| 516 |
+
del reproduced, expected, difference
|
| 517 |
+
torch.cuda.empty_cache()
|
| 518 |
+
|
| 519 |
+
for experiment_index, experiment in enumerate(experiments, start=1):
|
| 520 |
+
run_dir = args.output_dir / experiment["name"]
|
| 521 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 522 |
+
metrics_path = run_dir / "metrics.json"
|
| 523 |
+
if metrics_path.exists():
|
| 524 |
+
existing = json.loads(metrics_path.read_text(encoding="utf-8"))
|
| 525 |
+
if (
|
| 526 |
+
existing.get("status") == "complete"
|
| 527 |
+
and existing.get("prompt_ids") == prompt_ids
|
| 528 |
+
and existing.get("schedule") == schedule_description
|
| 529 |
+
and (args.skip_lpips or existing.get("lpips") is not None)
|
| 530 |
+
):
|
| 531 |
+
print(f"[run] skip complete {experiment['name']}", flush=True)
|
| 532 |
+
continue
|
| 533 |
+
print(
|
| 534 |
+
f"[run] {experiment_index}/{len(experiments)} {experiment['name']}",
|
| 535 |
+
flush=True,
|
| 536 |
+
)
|
| 537 |
+
predictor = load_predictor(teacher, experiment, device)
|
| 538 |
+
existing_results = {}
|
| 539 |
+
for prompt_id in prompt_ids:
|
| 540 |
+
path = run_dir / "per_prompt" / f"prompt_{prompt_id:04d}.json"
|
| 541 |
+
if path.exists():
|
| 542 |
+
cached = json.loads(path.read_text(encoding="utf-8"))
|
| 543 |
+
if cached.get("rollout_schedule", "FPPF") == args.schedule:
|
| 544 |
+
existing_results[prompt_id] = cached
|
| 545 |
+
|
| 546 |
+
for prompt_index, prompt_id in enumerate(prompt_ids, start=1):
|
| 547 |
+
if prompt_id in existing_results:
|
| 548 |
+
print(
|
| 549 |
+
f"[prompt] {experiment['name']} {prompt_index}/{len(prompt_ids)} "
|
| 550 |
+
f"id={prompt_id} cached",
|
| 551 |
+
flush=True,
|
| 552 |
+
)
|
| 553 |
+
continue
|
| 554 |
+
started = time.perf_counter()
|
| 555 |
+
latent, counts = generate_rollout(
|
| 556 |
+
pipeline=pipeline,
|
| 557 |
+
dataset_root=args.dataset_root,
|
| 558 |
+
prompt_id=prompt_id,
|
| 559 |
+
generation_seed=args.generation_seed,
|
| 560 |
+
device=device,
|
| 561 |
+
predictor=predictor,
|
| 562 |
+
source_layers=experiment["source_layers"],
|
| 563 |
+
schedule=args.schedule,
|
| 564 |
+
)
|
| 565 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 566 |
+
pixels = vae.decode_to_pixel(latent, use_cache=False)
|
| 567 |
+
prediction_u8 = pixels_to_u8(pixels)
|
| 568 |
+
reference_u8 = load_reference_frames(args.reference_root, prompt_id)
|
| 569 |
+
metrics = frame_metrics(
|
| 570 |
+
reference_u8=reference_u8,
|
| 571 |
+
prediction_u8=prediction_u8,
|
| 572 |
+
lpips_model=lpips_model,
|
| 573 |
+
batch_size=args.metric_batch_size,
|
| 574 |
+
device=device,
|
| 575 |
+
)
|
| 576 |
+
prompt_result = {
|
| 577 |
+
"prompt_id": prompt_id,
|
| 578 |
+
"prompt": load_prompt_metadata(args.dataset_root, prompt_id)[
|
| 579 |
+
"prompt"
|
| 580 |
+
],
|
| 581 |
+
"rollout_schedule": args.schedule,
|
| 582 |
+
**counts,
|
| 583 |
+
**metrics,
|
| 584 |
+
"total_time_s": time.perf_counter() - started,
|
| 585 |
+
}
|
| 586 |
+
atomic_json(
|
| 587 |
+
run_dir / "per_prompt" / f"prompt_{prompt_id:04d}.json",
|
| 588 |
+
prompt_result,
|
| 589 |
+
)
|
| 590 |
+
existing_results[prompt_id] = prompt_result
|
| 591 |
+
print(
|
| 592 |
+
f"[prompt] {experiment['name']} {prompt_index}/{len(prompt_ids)} "
|
| 593 |
+
f"id={prompt_id} psnr={metrics['psnr']:.4f} "
|
| 594 |
+
f"ssim={metrics['ssim']:.6f} lpips={metrics['lpips']} "
|
| 595 |
+
f"time={prompt_result['total_time_s']:.1f}s",
|
| 596 |
+
flush=True,
|
| 597 |
+
)
|
| 598 |
+
if hasattr(vae.model, "clear_cache"):
|
| 599 |
+
vae.model.clear_cache()
|
| 600 |
+
del latent, pixels, prediction_u8, reference_u8
|
| 601 |
+
torch.cuda.empty_cache()
|
| 602 |
+
|
| 603 |
+
base_experiment = {
|
| 604 |
+
**experiment,
|
| 605 |
+
"source_layer": experiment["source_layers"],
|
| 606 |
+
}
|
| 607 |
+
aggregate = aggregate_prompt_results(
|
| 608 |
+
base_experiment,
|
| 609 |
+
[existing_results[prompt_id] for prompt_id in prompt_ids],
|
| 610 |
+
)
|
| 611 |
+
aggregate["source_layers"] = experiment["source_layers"]
|
| 612 |
+
kind_field = (
|
| 613 |
+
"pair_kind"
|
| 614 |
+
if len(experiment["source_layers"]) == 2
|
| 615 |
+
else "triple_kind"
|
| 616 |
+
)
|
| 617 |
+
aggregate[kind_field] = experiment["experiment_kind"]
|
| 618 |
+
aggregate["schedule"] = schedule_description
|
| 619 |
+
aggregate.pop("source_layer", None)
|
| 620 |
+
atomic_json(metrics_path, aggregate)
|
| 621 |
+
write_summary(args.output_dir, experiments)
|
| 622 |
+
print(
|
| 623 |
+
f"[result] {experiment['name']} psnr={aggregate['psnr']:.4f} "
|
| 624 |
+
f"ssim={aggregate['ssim']:.6f} lpips={aggregate['lpips']}",
|
| 625 |
+
flush=True,
|
| 626 |
+
)
|
| 627 |
+
del predictor
|
| 628 |
+
torch.cuda.empty_cache()
|
| 629 |
+
|
| 630 |
+
manifest["status"] = "complete"
|
| 631 |
+
atomic_json(args.output_dir / "manifest.json", manifest)
|
| 632 |
+
write_summary(args.output_dir, experiments)
|
| 633 |
+
print(
|
| 634 |
+
f"[complete] {len(experiments)} experiments -> {args.output_dir / 'summary.csv'}",
|
| 635 |
+
flush=True,
|
| 636 |
+
)
|
| 637 |
+
|
| 638 |
+
|
| 639 |
+
if __name__ == "__main__":
|
| 640 |
+
main()
|
scripts/generate_ode_pairs.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from utils.distributed import launch_distributed_job
|
| 2 |
+
from utils.scheduler import FlowMatchScheduler
|
| 3 |
+
from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder
|
| 4 |
+
from utils.dataset import TextDataset
|
| 5 |
+
import torch.distributed as dist
|
| 6 |
+
from tqdm import tqdm
|
| 7 |
+
import argparse
|
| 8 |
+
import torch
|
| 9 |
+
import math
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def init_model(device):
|
| 14 |
+
model = WanDiffusionWrapper().to(device).to(torch.float32)
|
| 15 |
+
encoder = WanTextEncoder().to(device).to(torch.float32)
|
| 16 |
+
model.model.requires_grad_(False)
|
| 17 |
+
|
| 18 |
+
scheduler = FlowMatchScheduler(
|
| 19 |
+
shift=8.0, sigma_min=0.0, extra_one_step=True)
|
| 20 |
+
scheduler.set_timesteps(num_inference_steps=48, denoising_strength=1.0)
|
| 21 |
+
scheduler.sigmas = scheduler.sigmas.to(device)
|
| 22 |
+
|
| 23 |
+
sample_neg_prompt = '色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走'
|
| 24 |
+
|
| 25 |
+
unconditional_dict = encoder(
|
| 26 |
+
text_prompts=[sample_neg_prompt]
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
return model, encoder, scheduler, unconditional_dict
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def main():
|
| 33 |
+
parser = argparse.ArgumentParser()
|
| 34 |
+
parser.add_argument("--local_rank", type=int, default=-1)
|
| 35 |
+
parser.add_argument("--output_folder", type=str)
|
| 36 |
+
parser.add_argument("--caption_path", type=str)
|
| 37 |
+
parser.add_argument("--guidance_scale", type=float, default=6.0)
|
| 38 |
+
|
| 39 |
+
args = parser.parse_args()
|
| 40 |
+
|
| 41 |
+
# launch_distributed_job()
|
| 42 |
+
launch_distributed_job()
|
| 43 |
+
|
| 44 |
+
device = torch.cuda.current_device()
|
| 45 |
+
|
| 46 |
+
torch.set_grad_enabled(False)
|
| 47 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 48 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 49 |
+
|
| 50 |
+
model, encoder, scheduler, unconditional_dict = init_model(device=device)
|
| 51 |
+
|
| 52 |
+
dataset = TextDataset(args.caption_path)
|
| 53 |
+
|
| 54 |
+
# if global_rank == 0:
|
| 55 |
+
os.makedirs(args.output_folder, exist_ok=True)
|
| 56 |
+
|
| 57 |
+
for index in tqdm(range(int(math.ceil(len(dataset) / dist.get_world_size()))), disable=dist.get_rank() != 0):
|
| 58 |
+
prompt_index = index * dist.get_world_size() + dist.get_rank()
|
| 59 |
+
if prompt_index >= len(dataset):
|
| 60 |
+
continue
|
| 61 |
+
prompt = dataset[prompt_index]
|
| 62 |
+
|
| 63 |
+
conditional_dict = encoder(text_prompts=prompt)
|
| 64 |
+
|
| 65 |
+
latents = torch.randn(
|
| 66 |
+
[1, 21, 16, 60, 104], dtype=torch.float32, device=device
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
noisy_input = []
|
| 70 |
+
|
| 71 |
+
for progress_id, t in enumerate(tqdm(scheduler.timesteps)):
|
| 72 |
+
timestep = t * \
|
| 73 |
+
torch.ones([1, 21], device=device, dtype=torch.float32)
|
| 74 |
+
|
| 75 |
+
noisy_input.append(latents)
|
| 76 |
+
|
| 77 |
+
_, x0_pred_cond = model(
|
| 78 |
+
latents, conditional_dict, timestep
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
_, x0_pred_uncond = model(
|
| 82 |
+
latents, unconditional_dict, timestep
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
x0_pred = x0_pred_uncond + args.guidance_scale * (
|
| 86 |
+
x0_pred_cond - x0_pred_uncond
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
flow_pred = model._convert_x0_to_flow_pred(
|
| 90 |
+
scheduler=scheduler,
|
| 91 |
+
x0_pred=x0_pred.flatten(0, 1),
|
| 92 |
+
xt=latents.flatten(0, 1),
|
| 93 |
+
timestep=timestep.flatten(0, 1)
|
| 94 |
+
).unflatten(0, x0_pred.shape[:2])
|
| 95 |
+
|
| 96 |
+
latents = scheduler.step(
|
| 97 |
+
flow_pred.flatten(0, 1),
|
| 98 |
+
scheduler.timesteps[progress_id] * torch.ones(
|
| 99 |
+
[1, 21], device=device, dtype=torch.long).flatten(0, 1),
|
| 100 |
+
latents.flatten(0, 1)
|
| 101 |
+
).unflatten(dim=0, sizes=flow_pred.shape[:2])
|
| 102 |
+
|
| 103 |
+
noisy_input.append(latents)
|
| 104 |
+
|
| 105 |
+
noisy_inputs = torch.stack(noisy_input, dim=1)
|
| 106 |
+
|
| 107 |
+
noisy_inputs = noisy_inputs[:, [0, 12, 24, 36, -1]]
|
| 108 |
+
|
| 109 |
+
stored_data = noisy_inputs
|
| 110 |
+
|
| 111 |
+
torch.save(
|
| 112 |
+
{prompt: stored_data.cpu().detach()},
|
| 113 |
+
os.path.join(args.output_folder, f"{prompt_index:05d}.pt")
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
dist.barrier()
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
if __name__ == "__main__":
|
| 120 |
+
main()
|
scripts/generate_vbench8_extended_atc.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate ATC FPPF/FPPP videos against the existing Extended-251 FFFF run."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
if str(REPO_ROOT) not in sys.path:
|
| 14 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 15 |
+
|
| 16 |
+
from scripts import generate_vbench8_extended_strategies as extended
|
| 17 |
+
|
| 18 |
+
import lpips
|
| 19 |
+
import torch
|
| 20 |
+
from omegaconf import OmegaConf
|
| 21 |
+
from safetensors import safe_open
|
| 22 |
+
|
| 23 |
+
from scripts import evaluate_single_block_fppf as base
|
| 24 |
+
from scripts.generate_vbench8_extended_disca import artifact_paths, read_u8_video
|
| 25 |
+
from utils.wan_wrapper import WanTextEncoder, WanVAEWrapper
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
MAPPING_DEFAULT = REPO_ROOT / "assets/vbench8_extended_subset_mapping.json"
|
| 29 |
+
REFERENCE_DEFAULT = (
|
| 30 |
+
REPO_ROOT / "evaluation_runs/vbench8_extended_stage1_step2000_20260901"
|
| 31 |
+
)
|
| 32 |
+
OUTPUT_DEFAULT = (
|
| 33 |
+
REPO_ROOT / "evaluation_runs/vbench8_extended_atc_stage1_step2000_20260901"
|
| 34 |
+
)
|
| 35 |
+
PATTERN_TO_STEPS = {"FPPF": [1, 2], "FPPP": [1, 2, 3]}
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def read_predictor_config(weights: Path) -> dict[str, Any]:
|
| 39 |
+
with safe_open(weights, framework="pt", device="cpu") as handle:
|
| 40 |
+
metadata = handle.metadata() or {}
|
| 41 |
+
raw = metadata.get("predictor_config")
|
| 42 |
+
if raw is None:
|
| 43 |
+
raise ValueError(f"ATC checkpoint has no predictor_config metadata: {weights}")
|
| 44 |
+
config = json.loads(raw)
|
| 45 |
+
if config.get("input_variant") != "atc":
|
| 46 |
+
raise ValueError(f"Checkpoint is not ATC: {config}")
|
| 47 |
+
return config
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def strategy_configs(
|
| 51 |
+
scope: str, patterns: list[str] | tuple[str, ...]
|
| 52 |
+
) -> list[dict[str, Any]]:
|
| 53 |
+
return [
|
| 54 |
+
{
|
| 55 |
+
"name": f"atc_{scope}_{pattern.lower()}",
|
| 56 |
+
"policy": "fppf",
|
| 57 |
+
"pattern": pattern,
|
| 58 |
+
"candidate_steps": PATTERN_TO_STEPS[pattern],
|
| 59 |
+
"allow_chunk0_predictor": False,
|
| 60 |
+
"beta": None,
|
| 61 |
+
"threshold": None,
|
| 62 |
+
"target_accepts": 6 * len(PATTERN_TO_STEPS[pattern]),
|
| 63 |
+
"head": None,
|
| 64 |
+
"input_variant": "atc",
|
| 65 |
+
"atc_previous_scope": scope,
|
| 66 |
+
}
|
| 67 |
+
for pattern in patterns
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def load_models(
|
| 72 |
+
weights: Path, scope: str, device: torch.device
|
| 73 |
+
) -> tuple[Any, Any, Any, Any, None, None, Any]:
|
| 74 |
+
predictor_config = read_predictor_config(weights)
|
| 75 |
+
if predictor_config.get("atc_previous_scope") != scope:
|
| 76 |
+
raise ValueError(
|
| 77 |
+
f"Checkpoint scope={predictor_config.get('atc_previous_scope')} "
|
| 78 |
+
f"does not match requested scope={scope}"
|
| 79 |
+
)
|
| 80 |
+
print("[setup] loading VAE", flush=True)
|
| 81 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 82 |
+
config = OmegaConf.merge(
|
| 83 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 84 |
+
OmegaConf.load(REPO_ROOT / "configs/self_forcing_sid.yaml"),
|
| 85 |
+
)
|
| 86 |
+
print(f"[setup] loading Teacher and ATC scope={scope}", flush=True)
|
| 87 |
+
pipeline = base.build_pipeline(
|
| 88 |
+
config, REPO_ROOT / "checkpoints/self_forcing_dmd.pt", vae, device
|
| 89 |
+
)
|
| 90 |
+
predictor = base.load_predictor(
|
| 91 |
+
pipeline.generator.model,
|
| 92 |
+
{
|
| 93 |
+
**predictor_config,
|
| 94 |
+
"weights": weights,
|
| 95 |
+
"atc_collect_diagnostics": False,
|
| 96 |
+
},
|
| 97 |
+
device,
|
| 98 |
+
)
|
| 99 |
+
text_encoder = WanTextEncoder().to(device=device, dtype=torch.bfloat16).eval()
|
| 100 |
+
text_encoder.requires_grad_(False)
|
| 101 |
+
print("[setup] loading LPIPS", flush=True)
|
| 102 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 103 |
+
lpips_model.requires_grad_(False)
|
| 104 |
+
return vae, pipeline, predictor, text_encoder, None, None, lpips_model
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def is_complete(
|
| 108 |
+
output_root: Path,
|
| 109 |
+
row: dict[str, Any],
|
| 110 |
+
strategies: list[dict[str, Any]],
|
| 111 |
+
) -> bool:
|
| 112 |
+
return all(
|
| 113 |
+
all(path.is_file() for path in artifact_paths(output_root, row, config["name"]))
|
| 114 |
+
for config in strategies
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def run_prompt(
|
| 119 |
+
*,
|
| 120 |
+
row: dict[str, Any],
|
| 121 |
+
strategies: list[dict[str, Any]],
|
| 122 |
+
reference_root: Path,
|
| 123 |
+
output_root: Path,
|
| 124 |
+
seed: int,
|
| 125 |
+
models: tuple[Any, ...],
|
| 126 |
+
device: torch.device,
|
| 127 |
+
) -> None:
|
| 128 |
+
global_index = int(row["global_index"])
|
| 129 |
+
suite = str(row["prompt_suite"])
|
| 130 |
+
suite_index = int(row["suite_index"])
|
| 131 |
+
prompt = str(row["extended_prompt"])
|
| 132 |
+
reference_path = (
|
| 133 |
+
reference_root / "generated_videos/ffff" / suite / f"{suite_index:03d}.mp4"
|
| 134 |
+
)
|
| 135 |
+
print(f"[prompt] global={global_index} suite={suite}/{suite_index}", flush=True)
|
| 136 |
+
conditional = models[3](text_prompts=[prompt])
|
| 137 |
+
for strategy in strategies:
|
| 138 |
+
name = str(strategy["name"])
|
| 139 |
+
video_path, record_path = artifact_paths(output_root, row, name)
|
| 140 |
+
if video_path.is_file() and record_path.is_file():
|
| 141 |
+
print(f"[cached-strategy] global={global_index} strategy={name}", flush=True)
|
| 142 |
+
continue
|
| 143 |
+
latent, diagnostic = extended.generate_rollout(
|
| 144 |
+
pipeline=models[1],
|
| 145 |
+
conditional_dict=conditional,
|
| 146 |
+
seed=seed,
|
| 147 |
+
device=device,
|
| 148 |
+
predictor=models[2],
|
| 149 |
+
head=None,
|
| 150 |
+
config=strategy,
|
| 151 |
+
)
|
| 152 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 153 |
+
pixels = models[0].decode_to_pixel(latent, use_cache=False)
|
| 154 |
+
prediction_u8 = base.pixels_to_u8(pixels)
|
| 155 |
+
extended.atomic_video(prediction_u8, video_path)
|
| 156 |
+
reference_decoded = read_u8_video(reference_path)
|
| 157 |
+
prediction_decoded = read_u8_video(video_path)
|
| 158 |
+
metrics = base.frame_metrics(
|
| 159 |
+
reference_u8=reference_decoded,
|
| 160 |
+
prediction_u8=prediction_decoded,
|
| 161 |
+
lpips_model=models[6],
|
| 162 |
+
batch_size=4,
|
| 163 |
+
device=device,
|
| 164 |
+
)
|
| 165 |
+
extended.atomic_json(
|
| 166 |
+
record_path,
|
| 167 |
+
{
|
| 168 |
+
"status": "complete",
|
| 169 |
+
"strategy": name,
|
| 170 |
+
"policy": "fppf",
|
| 171 |
+
"pattern": strategy["pattern"],
|
| 172 |
+
"input_variant": "atc",
|
| 173 |
+
"atc_previous_scope": strategy["atc_previous_scope"],
|
| 174 |
+
"candidate_steps": strategy["candidate_steps"],
|
| 175 |
+
"allow_chunk0_predictor": False,
|
| 176 |
+
"global_index": global_index,
|
| 177 |
+
"prompt_suite": suite,
|
| 178 |
+
"suite_index": suite_index,
|
| 179 |
+
"prompt": prompt,
|
| 180 |
+
"seed": seed,
|
| 181 |
+
"generation": {
|
| 182 |
+
key: value
|
| 183 |
+
for key, value in diagnostic.items()
|
| 184 |
+
if key != "decisions"
|
| 185 |
+
},
|
| 186 |
+
"decisions": diagnostic["decisions"],
|
| 187 |
+
"pixel_metrics_vs_ffff": extended.compact_metrics(metrics),
|
| 188 |
+
"pixel_metric_input": (
|
| 189 |
+
"MP4-decoded uint8 RGB, all 81 frames on both sides"
|
| 190 |
+
),
|
| 191 |
+
"reference_video": str(reference_path),
|
| 192 |
+
"video": str(video_path.relative_to(output_root)),
|
| 193 |
+
},
|
| 194 |
+
)
|
| 195 |
+
print(
|
| 196 |
+
f"[result] global={global_index} strategy={name} "
|
| 197 |
+
f"accept={diagnostic['accepted_predictor_calls']} "
|
| 198 |
+
f"psnr={metrics['psnr']:.4f} ssim={metrics['ssim']:.6f} "
|
| 199 |
+
f"lpips={metrics['lpips']:.6f}",
|
| 200 |
+
flush=True,
|
| 201 |
+
)
|
| 202 |
+
if hasattr(models[0].model, "clear_cache"):
|
| 203 |
+
models[0].model.clear_cache()
|
| 204 |
+
del latent, pixels, prediction_u8, reference_decoded, prediction_decoded
|
| 205 |
+
torch.cuda.empty_cache()
|
| 206 |
+
del conditional
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def parse_args() -> argparse.Namespace:
|
| 210 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 211 |
+
parser.add_argument("--gpu", default=extended.PHYSICAL_GPU)
|
| 212 |
+
parser.add_argument("--scope", choices=("chunk", "last_frame"), required=True)
|
| 213 |
+
parser.add_argument("--predictor-weights", type=Path, required=True)
|
| 214 |
+
parser.add_argument("--mapping", type=Path, default=MAPPING_DEFAULT)
|
| 215 |
+
parser.add_argument("--reference-root", type=Path, default=REFERENCE_DEFAULT)
|
| 216 |
+
parser.add_argument("--output-root", type=Path, default=OUTPUT_DEFAULT)
|
| 217 |
+
parser.add_argument("--pattern", action="append", choices=tuple(PATTERN_TO_STEPS))
|
| 218 |
+
parser.add_argument("--shard-index", type=int, default=0)
|
| 219 |
+
parser.add_argument("--num-shards", type=int, default=1)
|
| 220 |
+
parser.add_argument("--global-index", action="append", type=int, default=None)
|
| 221 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 222 |
+
return parser.parse_args()
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def main() -> None:
|
| 226 |
+
args = parse_args()
|
| 227 |
+
if args.num_shards < 1 or not 0 <= args.shard_index < args.num_shards:
|
| 228 |
+
raise ValueError("Invalid shard index/number")
|
| 229 |
+
weights = args.predictor_weights.resolve()
|
| 230 |
+
mapping = extended.read_mapping(args.mapping.resolve())
|
| 231 |
+
if args.global_index is None:
|
| 232 |
+
rows = [
|
| 233 |
+
row for index, row in enumerate(mapping)
|
| 234 |
+
if index % args.num_shards == args.shard_index
|
| 235 |
+
]
|
| 236 |
+
else:
|
| 237 |
+
requested = set(args.global_index)
|
| 238 |
+
rows = [row for row in mapping if int(row["global_index"]) in requested]
|
| 239 |
+
if len(rows) != len(requested):
|
| 240 |
+
raise ValueError("Unknown global index")
|
| 241 |
+
patterns = args.pattern or ["FPPF", "FPPP"]
|
| 242 |
+
strategies = strategy_configs(args.scope, patterns)
|
| 243 |
+
reference_root = args.reference_root.resolve()
|
| 244 |
+
if len(list((reference_root / "generated_videos/ffff").rglob("*.mp4"))) != 251:
|
| 245 |
+
raise ValueError("Incomplete FFFF reference")
|
| 246 |
+
output_root = args.output_root.resolve()
|
| 247 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 248 |
+
extended.write_shard_manifest(
|
| 249 |
+
output_root,
|
| 250 |
+
str(args.gpu),
|
| 251 |
+
args.shard_index,
|
| 252 |
+
args.num_shards,
|
| 253 |
+
rows,
|
| 254 |
+
strategies,
|
| 255 |
+
"running",
|
| 256 |
+
)
|
| 257 |
+
device = torch.device("cuda")
|
| 258 |
+
torch.set_grad_enabled(False)
|
| 259 |
+
models = load_models(weights, args.scope, device)
|
| 260 |
+
completed = 0
|
| 261 |
+
try:
|
| 262 |
+
for row in rows:
|
| 263 |
+
if is_complete(output_root, row, strategies):
|
| 264 |
+
completed += 1
|
| 265 |
+
print(f"[cached] {completed}/{len(rows)} global={row['global_index']}", flush=True)
|
| 266 |
+
continue
|
| 267 |
+
run_prompt(
|
| 268 |
+
row=row,
|
| 269 |
+
strategies=strategies,
|
| 270 |
+
reference_root=reference_root,
|
| 271 |
+
output_root=output_root,
|
| 272 |
+
seed=args.seed,
|
| 273 |
+
models=models,
|
| 274 |
+
device=device,
|
| 275 |
+
)
|
| 276 |
+
completed += 1
|
| 277 |
+
print(f"[progress] {completed}/{len(rows)} prompts", flush=True)
|
| 278 |
+
finally:
|
| 279 |
+
extended.write_shard_manifest(
|
| 280 |
+
output_root,
|
| 281 |
+
str(args.gpu),
|
| 282 |
+
args.shard_index,
|
| 283 |
+
args.num_shards,
|
| 284 |
+
rows,
|
| 285 |
+
strategies,
|
| 286 |
+
"complete" if completed == len(rows) else "failed",
|
| 287 |
+
)
|
| 288 |
+
print(f"[complete] gpu={args.gpu} scope={args.scope} prompts={completed}", flush=True)
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
if __name__ == "__main__":
|
| 292 |
+
main()
|
scripts/generate_vbench8_extended_atc_confidence_token.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate one Confidence-token strategy for Extended-251."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
import tempfile
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def preparse_gpu() -> str:
|
| 16 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 17 |
+
parser.add_argument("--gpu", default="0")
|
| 18 |
+
args, _ = parser.parse_known_args()
|
| 19 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 20 |
+
os.environ.setdefault(
|
| 21 |
+
"MPLCONFIGDIR", tempfile.mkdtemp(prefix="atc_confidence_token_vbench_")
|
| 22 |
+
)
|
| 23 |
+
return str(args.gpu)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
PHYSICAL_GPU = preparse_gpu()
|
| 27 |
+
|
| 28 |
+
import lpips
|
| 29 |
+
import torch
|
| 30 |
+
from omegaconf import OmegaConf
|
| 31 |
+
from safetensors import safe_open
|
| 32 |
+
from safetensors.torch import load_file
|
| 33 |
+
|
| 34 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 35 |
+
if str(ROOT) not in sys.path:
|
| 36 |
+
sys.path.insert(0, str(ROOT))
|
| 37 |
+
|
| 38 |
+
from predictor_training.confidence import ConfidenceTokenHead
|
| 39 |
+
from scripts import evaluate_single_block_fppf as base
|
| 40 |
+
from scripts import generate_vbench8_extended_strategies as extended
|
| 41 |
+
from scripts.generate_vbench8_extended_disca import artifact_paths, read_u8_video
|
| 42 |
+
from utils.wan_wrapper import WanTextEncoder, WanVAEWrapper
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
MAPPING_DEFAULT = ROOT / "assets/vbench8_extended_subset_mapping.json"
|
| 46 |
+
REFERENCE_DEFAULT = ROOT / "evaluation_runs/vbench8_extended_stage1_step2000_20260901"
|
| 47 |
+
SELECTION_DEFAULT = (
|
| 48 |
+
ROOT
|
| 49 |
+
/ "confidence_experiments"
|
| 50 |
+
/ "layer17_atc_chunk_confidence_token_beta2_vbench3_20260902"
|
| 51 |
+
/ "threshold_summary.json"
|
| 52 |
+
)
|
| 53 |
+
PREDICTOR_DEFAULT = (
|
| 54 |
+
ROOT
|
| 55 |
+
/ "training_runs/layer17_atc_chunk_stage1_1000p_4gpu_b16_2000steps"
|
| 56 |
+
/ "checkpoint_step_2000/predictor.safetensors"
|
| 57 |
+
)
|
| 58 |
+
HEAD_DEFAULT = (
|
| 59 |
+
ROOT
|
| 60 |
+
/ "training_runs"
|
| 61 |
+
/ "layer17_atc_chunk_confidence_token_900train_100val_4gpu_b64_warmup_cosine"
|
| 62 |
+
/ "confidence_latest.safetensors"
|
| 63 |
+
)
|
| 64 |
+
OUTPUT_DEFAULT = (
|
| 65 |
+
ROOT
|
| 66 |
+
/ "evaluation_runs"
|
| 67 |
+
/ "vbench8_extended_atc_chunk_confidence_token_beta2_20260902"
|
| 68 |
+
)
|
| 69 |
+
TARGETS = (6, 9, 12, 15)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def read_head_config(path: Path) -> dict[str, Any]:
|
| 73 |
+
with safe_open(path, framework="pt", device="cpu") as handle:
|
| 74 |
+
metadata = handle.metadata() or {}
|
| 75 |
+
raw = metadata.get("head_config")
|
| 76 |
+
if raw is None:
|
| 77 |
+
raise ValueError(f"Missing head_config metadata: {path}")
|
| 78 |
+
config = json.loads(raw)
|
| 79 |
+
expected = {
|
| 80 |
+
"architecture": "ConfidenceTokenHead",
|
| 81 |
+
"dim": 1536,
|
| 82 |
+
"token_dim": 512,
|
| 83 |
+
"context_dim": 64,
|
| 84 |
+
"num_heads": 8,
|
| 85 |
+
"ffn_dim": 2048,
|
| 86 |
+
"num_steps": 3,
|
| 87 |
+
}
|
| 88 |
+
for key, value in expected.items():
|
| 89 |
+
if config.get(key) != value:
|
| 90 |
+
raise ValueError(f"Unexpected head config {key}={config.get(key)}")
|
| 91 |
+
return config
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def read_predictor_config(
|
| 95 |
+
path: Path, input_variant: str, atc_previous_scope: str
|
| 96 |
+
) -> dict[str, Any]:
|
| 97 |
+
with safe_open(path, framework="pt", device="cpu") as handle:
|
| 98 |
+
metadata = handle.metadata() or {}
|
| 99 |
+
raw = metadata.get("predictor_config")
|
| 100 |
+
if raw is None:
|
| 101 |
+
if input_variant != "self_forcing":
|
| 102 |
+
raise ValueError(
|
| 103 |
+
f"Missing predictor_config metadata: {path}; only an explicitly "
|
| 104 |
+
"selected legacy self_forcing/concat checkpoint may omit it"
|
| 105 |
+
)
|
| 106 |
+
return {
|
| 107 |
+
"source_layer": 17,
|
| 108 |
+
"input_variant": "self_forcing",
|
| 109 |
+
"gate_mode": "baseline",
|
| 110 |
+
}
|
| 111 |
+
config = json.loads(raw)
|
| 112 |
+
actual_variant = str(config.get("input_variant", "self_forcing"))
|
| 113 |
+
if actual_variant != input_variant:
|
| 114 |
+
raise ValueError(
|
| 115 |
+
f"Predictor variant mismatch: expected={input_variant} "
|
| 116 |
+
f"actual={actual_variant}"
|
| 117 |
+
)
|
| 118 |
+
if input_variant == "atc":
|
| 119 |
+
actual_scope = str(config.get("atc_previous_scope", "chunk"))
|
| 120 |
+
if actual_scope != atc_previous_scope:
|
| 121 |
+
raise ValueError(
|
| 122 |
+
f"ATC scope mismatch: expected={atc_previous_scope} "
|
| 123 |
+
f"actual={actual_scope}"
|
| 124 |
+
)
|
| 125 |
+
return config
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def strategy_prefix(input_variant: str, atc_previous_scope: str) -> str:
|
| 129 |
+
if input_variant == "self_forcing":
|
| 130 |
+
return "concat"
|
| 131 |
+
return f"atc_{atc_previous_scope}"
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def load_strategy(
|
| 135 |
+
path: Path,
|
| 136 |
+
target_k: int,
|
| 137 |
+
input_variant: str,
|
| 138 |
+
atc_previous_scope: str,
|
| 139 |
+
) -> dict[str, Any]:
|
| 140 |
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
| 141 |
+
if payload.get("status") != "complete" or float(payload.get("beta")) != 2.0:
|
| 142 |
+
raise ValueError(f"Selection is not complete fixed-beta=2: {path}")
|
| 143 |
+
if [int(value) for value in payload.get("candidate_steps", [])] != [1, 2, 3]:
|
| 144 |
+
raise ValueError("Selection is not Step123")
|
| 145 |
+
if payload.get("predictor_input_variant") != input_variant:
|
| 146 |
+
raise ValueError(
|
| 147 |
+
"Threshold selection Predictor variant does not match evaluation"
|
| 148 |
+
)
|
| 149 |
+
expected_scope = atc_previous_scope if input_variant == "atc" else None
|
| 150 |
+
if payload.get("atc_previous_scope") != expected_scope:
|
| 151 |
+
raise ValueError("Threshold selection ATC scope does not match evaluation")
|
| 152 |
+
matches = [
|
| 153 |
+
row for row in payload.get("selected", [])
|
| 154 |
+
if int(row["target_accepts"]) == target_k
|
| 155 |
+
]
|
| 156 |
+
if len(matches) != 1:
|
| 157 |
+
raise ValueError(f"Expected one selected K{target_k:02d}, got {len(matches)}")
|
| 158 |
+
row = matches[0]
|
| 159 |
+
return {
|
| 160 |
+
"name": (
|
| 161 |
+
f"{strategy_prefix(input_variant, atc_previous_scope)}"
|
| 162 |
+
f"_confidence_token_beta2_k{target_k:02d}"
|
| 163 |
+
),
|
| 164 |
+
"policy": "dynamic",
|
| 165 |
+
"candidate_steps": [1, 2, 3],
|
| 166 |
+
"beta": 2.0,
|
| 167 |
+
"threshold": float(row["threshold"]),
|
| 168 |
+
"target_accepts": target_k,
|
| 169 |
+
"head": "confidence_token",
|
| 170 |
+
"risk_mode": "outgoing_span",
|
| 171 |
+
"allow_chunk0_predictor": False,
|
| 172 |
+
"source_config_name": str(row["name"]),
|
| 173 |
+
"calibration_mean_accepted_predictor_calls": float(
|
| 174 |
+
row["mean_accepted_predictor_calls"]
|
| 175 |
+
),
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def load_models(
|
| 180 |
+
predictor_weights: Path,
|
| 181 |
+
head_weights: Path,
|
| 182 |
+
input_variant: str,
|
| 183 |
+
atc_previous_scope: str,
|
| 184 |
+
device: torch.device,
|
| 185 |
+
) -> tuple[Any, Any, Any, ConfidenceTokenHead, Any]:
|
| 186 |
+
predictor_config = read_predictor_config(
|
| 187 |
+
predictor_weights, input_variant, atc_previous_scope
|
| 188 |
+
)
|
| 189 |
+
|
| 190 |
+
print("[setup] loading VAE", flush=True)
|
| 191 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 192 |
+
config = OmegaConf.merge(
|
| 193 |
+
OmegaConf.load(ROOT / "configs/default_config.yaml"),
|
| 194 |
+
OmegaConf.load(ROOT / "configs/self_forcing_sid.yaml"),
|
| 195 |
+
)
|
| 196 |
+
print(
|
| 197 |
+
f"[setup] loading frozen Teacher and {input_variant} "
|
| 198 |
+
f"scope={atc_previous_scope if input_variant == 'atc' else 'n/a'} Predictor",
|
| 199 |
+
flush=True,
|
| 200 |
+
)
|
| 201 |
+
pipeline = base.build_pipeline(
|
| 202 |
+
config, ROOT / "checkpoints/self_forcing_dmd.pt", vae, device
|
| 203 |
+
)
|
| 204 |
+
predictor = base.load_predictor(
|
| 205 |
+
pipeline.generator.model,
|
| 206 |
+
{
|
| 207 |
+
**predictor_config,
|
| 208 |
+
"weights": predictor_weights,
|
| 209 |
+
"atc_collect_diagnostics": False,
|
| 210 |
+
},
|
| 211 |
+
device,
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
print("[setup] loading text encoder and Confidence-token Head", flush=True)
|
| 215 |
+
text_encoder = WanTextEncoder().to(device=device, dtype=torch.bfloat16).eval()
|
| 216 |
+
text_encoder.requires_grad_(False)
|
| 217 |
+
head_config = read_head_config(head_weights)
|
| 218 |
+
head = ConfidenceTokenHead(
|
| 219 |
+
dim=int(head_config["dim"]),
|
| 220 |
+
token_dim=int(head_config["token_dim"]),
|
| 221 |
+
context_dim=int(head_config["context_dim"]),
|
| 222 |
+
num_heads=int(head_config["num_heads"]),
|
| 223 |
+
ffn_dim=int(head_config["ffn_dim"]),
|
| 224 |
+
dropout=float(head_config.get("dropout", 0.1)),
|
| 225 |
+
num_steps=int(head_config["num_steps"]),
|
| 226 |
+
).to(device=device).eval()
|
| 227 |
+
head.load_state_dict(load_file(str(head_weights), device="cpu"), strict=True)
|
| 228 |
+
head.requires_grad_(False)
|
| 229 |
+
|
| 230 |
+
print("[setup] loading LPIPS", flush=True)
|
| 231 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 232 |
+
lpips_model.requires_grad_(False)
|
| 233 |
+
return vae, pipeline, predictor, text_encoder, head, lpips_model
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def run_prompt(
|
| 237 |
+
*,
|
| 238 |
+
row: dict[str, Any],
|
| 239 |
+
strategy: dict[str, Any],
|
| 240 |
+
reference_root: Path,
|
| 241 |
+
output_root: Path,
|
| 242 |
+
seed: int,
|
| 243 |
+
models: tuple[Any, ...],
|
| 244 |
+
device: torch.device,
|
| 245 |
+
) -> None:
|
| 246 |
+
global_index = int(row["global_index"])
|
| 247 |
+
suite = str(row["prompt_suite"])
|
| 248 |
+
suite_index = int(row["suite_index"])
|
| 249 |
+
prompt = str(row["extended_prompt"])
|
| 250 |
+
name = str(strategy["name"])
|
| 251 |
+
reference_path = (
|
| 252 |
+
reference_root / "generated_videos/ffff" / suite / f"{suite_index:03d}.mp4"
|
| 253 |
+
)
|
| 254 |
+
print(f"[prompt] global={global_index} suite={suite}/{suite_index}", flush=True)
|
| 255 |
+
conditional = models[3](text_prompts=[prompt])
|
| 256 |
+
latent, diagnostic = extended.generate_rollout(
|
| 257 |
+
pipeline=models[1],
|
| 258 |
+
conditional_dict=conditional,
|
| 259 |
+
seed=seed,
|
| 260 |
+
device=device,
|
| 261 |
+
predictor=models[2],
|
| 262 |
+
head=models[4],
|
| 263 |
+
config=strategy,
|
| 264 |
+
)
|
| 265 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 266 |
+
pixels = models[0].decode_to_pixel(latent, use_cache=False)
|
| 267 |
+
prediction_u8 = base.pixels_to_u8(pixels)
|
| 268 |
+
video_path, record_path = artifact_paths(output_root, row, name)
|
| 269 |
+
extended.atomic_video(prediction_u8, video_path)
|
| 270 |
+
|
| 271 |
+
reference_decoded = read_u8_video(reference_path)
|
| 272 |
+
prediction_decoded = read_u8_video(video_path)
|
| 273 |
+
metrics = base.frame_metrics(
|
| 274 |
+
reference_u8=reference_decoded,
|
| 275 |
+
prediction_u8=prediction_decoded,
|
| 276 |
+
lpips_model=models[5],
|
| 277 |
+
batch_size=4,
|
| 278 |
+
device=device,
|
| 279 |
+
)
|
| 280 |
+
extended.atomic_json(
|
| 281 |
+
record_path,
|
| 282 |
+
{
|
| 283 |
+
"status": "complete",
|
| 284 |
+
"strategy": name,
|
| 285 |
+
"policy": "dynamic",
|
| 286 |
+
"input_variant": strategy["input_variant"],
|
| 287 |
+
"atc_previous_scope": strategy["atc_previous_scope"],
|
| 288 |
+
"candidate_steps": [1, 2, 3],
|
| 289 |
+
"beta": 2.0,
|
| 290 |
+
"threshold": float(strategy["threshold"]),
|
| 291 |
+
"target_accepts": int(strategy["target_accepts"]),
|
| 292 |
+
"risk_mode": "outgoing_span",
|
| 293 |
+
"allow_chunk0_predictor": False,
|
| 294 |
+
"source_config_name": strategy["source_config_name"],
|
| 295 |
+
"global_index": global_index,
|
| 296 |
+
"prompt_suite": suite,
|
| 297 |
+
"suite_index": suite_index,
|
| 298 |
+
"prompt": prompt,
|
| 299 |
+
"seed": seed,
|
| 300 |
+
"generation": {
|
| 301 |
+
key: value for key, value in diagnostic.items() if key != "decisions"
|
| 302 |
+
},
|
| 303 |
+
"decisions": diagnostic["decisions"],
|
| 304 |
+
"pixel_metrics_vs_ffff": extended.compact_metrics(metrics),
|
| 305 |
+
"pixel_metric_input": "MP4-decoded uint8 RGB, all 81 frames on both sides",
|
| 306 |
+
"reference_video": str(reference_path),
|
| 307 |
+
"video": str(video_path.relative_to(output_root)),
|
| 308 |
+
},
|
| 309 |
+
)
|
| 310 |
+
print(
|
| 311 |
+
f"[result] global={global_index} strategy={name} "
|
| 312 |
+
f"accept={diagnostic['accepted_predictor_calls']} "
|
| 313 |
+
f"full={diagnostic['full_calls']} "
|
| 314 |
+
f"latency={diagnostic['policy_latency_ms']:.2f}ms "
|
| 315 |
+
f"psnr={metrics['psnr']:.4f} ssim={metrics['ssim']:.6f} "
|
| 316 |
+
f"lpips={metrics['lpips']:.6f}",
|
| 317 |
+
flush=True,
|
| 318 |
+
)
|
| 319 |
+
if hasattr(models[0].model, "clear_cache"):
|
| 320 |
+
models[0].model.clear_cache()
|
| 321 |
+
del conditional, latent, pixels, prediction_u8, reference_decoded, prediction_decoded
|
| 322 |
+
torch.cuda.empty_cache()
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
def parse_args() -> argparse.Namespace:
|
| 326 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 327 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 328 |
+
parser.add_argument("--target-k", type=int, choices=TARGETS, required=True)
|
| 329 |
+
parser.add_argument("--mapping", type=Path, default=MAPPING_DEFAULT)
|
| 330 |
+
parser.add_argument("--selection", type=Path, default=SELECTION_DEFAULT)
|
| 331 |
+
parser.add_argument("--predictor-weights", type=Path, default=PREDICTOR_DEFAULT)
|
| 332 |
+
parser.add_argument("--head-weights", type=Path, default=HEAD_DEFAULT)
|
| 333 |
+
parser.add_argument(
|
| 334 |
+
"--predictor-input-variant",
|
| 335 |
+
choices=("self_forcing", "atc"),
|
| 336 |
+
default="atc",
|
| 337 |
+
)
|
| 338 |
+
parser.add_argument(
|
| 339 |
+
"--atc-previous-scope",
|
| 340 |
+
choices=("chunk", "last_frame"),
|
| 341 |
+
default="chunk",
|
| 342 |
+
)
|
| 343 |
+
parser.add_argument("--reference-root", type=Path, default=REFERENCE_DEFAULT)
|
| 344 |
+
parser.add_argument("--output-root", type=Path, default=OUTPUT_DEFAULT)
|
| 345 |
+
parser.add_argument("--global-index", action="append", type=int, default=None)
|
| 346 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 347 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 348 |
+
args = parser.parse_args()
|
| 349 |
+
for name in (
|
| 350 |
+
"mapping", "selection", "predictor_weights", "head_weights",
|
| 351 |
+
"reference_root", "output_root",
|
| 352 |
+
):
|
| 353 |
+
setattr(args, name, getattr(args, name).expanduser().resolve())
|
| 354 |
+
return args
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def main() -> None:
|
| 358 |
+
args = parse_args()
|
| 359 |
+
strategy = load_strategy(
|
| 360 |
+
args.selection,
|
| 361 |
+
args.target_k,
|
| 362 |
+
args.predictor_input_variant,
|
| 363 |
+
args.atc_previous_scope,
|
| 364 |
+
)
|
| 365 |
+
strategy["input_variant"] = args.predictor_input_variant
|
| 366 |
+
strategy["atc_previous_scope"] = (
|
| 367 |
+
args.atc_previous_scope
|
| 368 |
+
if args.predictor_input_variant == "atc"
|
| 369 |
+
else None
|
| 370 |
+
)
|
| 371 |
+
mapping = extended.read_mapping(args.mapping)
|
| 372 |
+
if args.global_index is not None:
|
| 373 |
+
requested = set(args.global_index)
|
| 374 |
+
mapping = [row for row in mapping if int(row["global_index"]) in requested]
|
| 375 |
+
if len(mapping) != len(requested):
|
| 376 |
+
raise ValueError("At least one requested global index is unknown")
|
| 377 |
+
if len(list((args.reference_root / "generated_videos/ffff").rglob("*.mp4"))) != 251:
|
| 378 |
+
raise ValueError("Incomplete FFFF reference videos")
|
| 379 |
+
if len(list((args.reference_root / "generation_metrics/per_prompt/ffff").glob("global_*.json"))) != 251:
|
| 380 |
+
raise ValueError("Incomplete FFFF reference records")
|
| 381 |
+
|
| 382 |
+
args.output_root.mkdir(parents=True, exist_ok=True)
|
| 383 |
+
extended.write_shard_manifest(
|
| 384 |
+
args.output_root, PHYSICAL_GPU, 0, 1, mapping, [strategy], "running"
|
| 385 |
+
)
|
| 386 |
+
print(
|
| 387 |
+
f"[setup] physical_gpu={PHYSICAL_GPU} strategy={strategy['name']} "
|
| 388 |
+
f"threshold={strategy['threshold']:.10f} prompts={len(mapping)} "
|
| 389 |
+
"candidate_steps=1,2,3 chunk0=FFFF",
|
| 390 |
+
flush=True,
|
| 391 |
+
)
|
| 392 |
+
device = torch.device("cuda")
|
| 393 |
+
torch.set_grad_enabled(False)
|
| 394 |
+
models = load_models(
|
| 395 |
+
args.predictor_weights,
|
| 396 |
+
args.head_weights,
|
| 397 |
+
args.predictor_input_variant,
|
| 398 |
+
args.atc_previous_scope,
|
| 399 |
+
device,
|
| 400 |
+
)
|
| 401 |
+
completed = 0
|
| 402 |
+
try:
|
| 403 |
+
for row in mapping:
|
| 404 |
+
video_path, record_path = artifact_paths(
|
| 405 |
+
args.output_root, row, strategy["name"]
|
| 406 |
+
)
|
| 407 |
+
if not args.overwrite and video_path.is_file() and record_path.is_file():
|
| 408 |
+
completed += 1
|
| 409 |
+
print(f"[cached] {completed}/{len(mapping)} global={row['global_index']}", flush=True)
|
| 410 |
+
continue
|
| 411 |
+
run_prompt(
|
| 412 |
+
row=row,
|
| 413 |
+
strategy=strategy,
|
| 414 |
+
reference_root=args.reference_root,
|
| 415 |
+
output_root=args.output_root,
|
| 416 |
+
seed=args.seed,
|
| 417 |
+
models=models,
|
| 418 |
+
device=device,
|
| 419 |
+
)
|
| 420 |
+
completed += 1
|
| 421 |
+
print(f"[progress] {completed}/{len(mapping)} prompts", flush=True)
|
| 422 |
+
finally:
|
| 423 |
+
extended.write_shard_manifest(
|
| 424 |
+
args.output_root,
|
| 425 |
+
PHYSICAL_GPU,
|
| 426 |
+
0,
|
| 427 |
+
1,
|
| 428 |
+
mapping,
|
| 429 |
+
[strategy],
|
| 430 |
+
"complete" if completed == len(mapping) else "failed",
|
| 431 |
+
)
|
| 432 |
+
print(
|
| 433 |
+
f"[complete] gpu={PHYSICAL_GPU} strategy={strategy['name']} prompts={completed}",
|
| 434 |
+
flush=True,
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
|
| 438 |
+
if __name__ == "__main__":
|
| 439 |
+
main()
|
scripts/generate_vbench8_extended_concat.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate original-concat FPPP videos against the existing FFFF reference."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 11 |
+
if str(REPO_ROOT) not in sys.path:
|
| 12 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 13 |
+
|
| 14 |
+
from scripts import evaluate_single_block_fppf as base
|
| 15 |
+
from scripts import generate_vbench8_extended_disca as runner
|
| 16 |
+
|
| 17 |
+
import lpips
|
| 18 |
+
import torch
|
| 19 |
+
from omegaconf import OmegaConf
|
| 20 |
+
|
| 21 |
+
from utils.wan_wrapper import WanTextEncoder, WanVAEWrapper
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
WEIGHTS_DEFAULT = (
|
| 25 |
+
REPO_ROOT
|
| 26 |
+
/ "training_runs/layer17_stage1_1000p_4gpu_b16_2000steps"
|
| 27 |
+
/ "checkpoint_step_2000/predictor.safetensors"
|
| 28 |
+
)
|
| 29 |
+
OUTPUT_DEFAULT = (
|
| 30 |
+
REPO_ROOT
|
| 31 |
+
/ "evaluation_runs/vbench8_extended_concat_fppp_stage1_step2000_20260902"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def strategy_configs(patterns: list[str] | tuple[str, ...]) -> list[dict[str, Any]]:
|
| 36 |
+
if patterns != ["FPPP"] and tuple(patterns) != ("FPPP",):
|
| 37 |
+
raise ValueError("Original-concat evaluation supports exactly --pattern FPPP")
|
| 38 |
+
return [
|
| 39 |
+
{
|
| 40 |
+
"name": "concat_fppp",
|
| 41 |
+
"policy": "fppf",
|
| 42 |
+
"pattern": "FPPP",
|
| 43 |
+
"candidate_steps": [1, 2, 3],
|
| 44 |
+
"allow_chunk0_predictor": False,
|
| 45 |
+
"beta": None,
|
| 46 |
+
"threshold": None,
|
| 47 |
+
"target_accepts": 18,
|
| 48 |
+
"head": None,
|
| 49 |
+
"input_variant": "self_forcing",
|
| 50 |
+
}
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def load_models(
|
| 55 |
+
weights: Path, device: torch.device
|
| 56 |
+
) -> tuple[Any, Any, Any, Any, None, None, Any]:
|
| 57 |
+
print("[setup] loading VAE", flush=True)
|
| 58 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 59 |
+
config = OmegaConf.merge(
|
| 60 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 61 |
+
OmegaConf.load(REPO_ROOT / "configs/self_forcing_sid.yaml"),
|
| 62 |
+
)
|
| 63 |
+
print("[setup] loading frozen Teacher and original concat Predictor", flush=True)
|
| 64 |
+
pipeline = base.build_pipeline(
|
| 65 |
+
config, REPO_ROOT / "checkpoints/self_forcing_dmd.pt", vae, device
|
| 66 |
+
)
|
| 67 |
+
predictor = base.load_predictor(
|
| 68 |
+
pipeline.generator.model,
|
| 69 |
+
{
|
| 70 |
+
"source_layer": 17,
|
| 71 |
+
"weights": weights,
|
| 72 |
+
"input_variant": "self_forcing",
|
| 73 |
+
"gate_mode": "baseline",
|
| 74 |
+
},
|
| 75 |
+
device,
|
| 76 |
+
)
|
| 77 |
+
if predictor.input_variant != "self_forcing":
|
| 78 |
+
raise RuntimeError("Loaded Predictor is not the original concat variant")
|
| 79 |
+
text_encoder = WanTextEncoder().to(device=device, dtype=torch.bfloat16).eval()
|
| 80 |
+
text_encoder.requires_grad_(False)
|
| 81 |
+
print("[setup] loading LPIPS", flush=True)
|
| 82 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 83 |
+
lpips_model.requires_grad_(False)
|
| 84 |
+
return vae, pipeline, predictor, text_encoder, None, None, lpips_model
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def main() -> None:
|
| 88 |
+
runner.WEIGHTS_DEFAULT = WEIGHTS_DEFAULT
|
| 89 |
+
runner.OUTPUT_DEFAULT = OUTPUT_DEFAULT
|
| 90 |
+
runner.strategy_configs = strategy_configs
|
| 91 |
+
runner.load_models = load_models
|
| 92 |
+
runner.main()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
if __name__ == "__main__":
|
| 96 |
+
main()
|
scripts/generate_vbench8_extended_disca.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate DISCA pattern videos using an existing Extended-251 FFFF reference."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 12 |
+
if str(REPO_ROOT) not in sys.path:
|
| 13 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 14 |
+
|
| 15 |
+
from scripts import generate_vbench8_extended_strategies as extended
|
| 16 |
+
|
| 17 |
+
import lpips
|
| 18 |
+
import torch
|
| 19 |
+
from omegaconf import OmegaConf
|
| 20 |
+
from torchvision.io import read_video
|
| 21 |
+
|
| 22 |
+
from scripts import evaluate_single_block_fppf as base
|
| 23 |
+
from utils.wan_wrapper import WanTextEncoder, WanVAEWrapper
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
MAPPING_DEFAULT = REPO_ROOT / "assets/vbench8_extended_subset_mapping.json"
|
| 27 |
+
WEIGHTS_DEFAULT = (
|
| 28 |
+
REPO_ROOT
|
| 29 |
+
/ "training_runs/disca_layer17_stage1_1000p_4gpu_b16_2000steps"
|
| 30 |
+
/ "checkpoint_step_2000/predictor.safetensors"
|
| 31 |
+
)
|
| 32 |
+
OUTPUT_DEFAULT = (
|
| 33 |
+
REPO_ROOT
|
| 34 |
+
/ "evaluation_runs/vbench8_extended_disca_chunk0_fppf_stage1_step2000"
|
| 35 |
+
)
|
| 36 |
+
REFERENCE_DEFAULT = (
|
| 37 |
+
REPO_ROOT / "evaluation_runs/vbench8_extended_stage1_step2000_20260901"
|
| 38 |
+
)
|
| 39 |
+
PATTERN_TO_CANDIDATE_STEPS = {
|
| 40 |
+
"FPFF": [1],
|
| 41 |
+
"FPPF": [1, 2],
|
| 42 |
+
"FPPP": [1, 2, 3],
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def strategy_configs(patterns: list[str] | tuple[str, ...]) -> list[dict[str, Any]]:
|
| 47 |
+
configs: list[dict[str, Any]] = []
|
| 48 |
+
for pattern in patterns:
|
| 49 |
+
candidate_steps = PATTERN_TO_CANDIDATE_STEPS[pattern]
|
| 50 |
+
configs.append(
|
| 51 |
+
{
|
| 52 |
+
"name": f"disca_{pattern.lower()}",
|
| 53 |
+
"policy": "fppf",
|
| 54 |
+
"pattern": pattern,
|
| 55 |
+
"candidate_steps": candidate_steps,
|
| 56 |
+
"allow_chunk0_predictor": True,
|
| 57 |
+
"beta": None,
|
| 58 |
+
"threshold": None,
|
| 59 |
+
"target_accepts": 7 * len(candidate_steps),
|
| 60 |
+
"head": None,
|
| 61 |
+
"input_variant": "disca",
|
| 62 |
+
}
|
| 63 |
+
)
|
| 64 |
+
return configs
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def load_models(
|
| 68 |
+
weights: Path, device: torch.device
|
| 69 |
+
) -> tuple[Any, Any, Any, Any, None, None, Any]:
|
| 70 |
+
print("[setup] loading VAE", flush=True)
|
| 71 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 72 |
+
config = OmegaConf.merge(
|
| 73 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 74 |
+
OmegaConf.load(REPO_ROOT / "configs/self_forcing_sid.yaml"),
|
| 75 |
+
)
|
| 76 |
+
print("[setup] loading frozen Teacher and DISCA Predictor", flush=True)
|
| 77 |
+
pipeline = base.build_pipeline(
|
| 78 |
+
config, REPO_ROOT / "checkpoints/self_forcing_dmd.pt", vae, device
|
| 79 |
+
)
|
| 80 |
+
predictor = base.load_predictor(
|
| 81 |
+
pipeline.generator.model,
|
| 82 |
+
{
|
| 83 |
+
"source_layer": 17,
|
| 84 |
+
"weights": weights,
|
| 85 |
+
"input_variant": "disca",
|
| 86 |
+
"gate_mode": "baseline",
|
| 87 |
+
},
|
| 88 |
+
device,
|
| 89 |
+
)
|
| 90 |
+
if predictor.input_variant != "disca":
|
| 91 |
+
raise RuntimeError("Loaded Predictor is not the DISCA input variant")
|
| 92 |
+
text_encoder = WanTextEncoder().to(device=device, dtype=torch.bfloat16).eval()
|
| 93 |
+
text_encoder.requires_grad_(False)
|
| 94 |
+
print("[setup] loading LPIPS", flush=True)
|
| 95 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 96 |
+
lpips_model.requires_grad_(False)
|
| 97 |
+
return vae, pipeline, predictor, text_encoder, None, None, lpips_model
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def read_u8_video(path: Path) -> torch.Tensor:
|
| 101 |
+
if not path.is_file():
|
| 102 |
+
raise FileNotFoundError(path)
|
| 103 |
+
frames, _, _ = read_video(str(path), pts_unit="sec", output_format="TCHW")
|
| 104 |
+
frames = frames.to(device="cpu", dtype=torch.uint8).contiguous()
|
| 105 |
+
if frames.ndim != 4 or frames.shape[0] != 81 or frames.shape[1] != 3:
|
| 106 |
+
raise RuntimeError(f"Unexpected decoded video shape {tuple(frames.shape)}: {path}")
|
| 107 |
+
return frames
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def artifact_paths(
|
| 111 |
+
output_root: Path, mapping_row: dict[str, Any], strategy_name: str
|
| 112 |
+
) -> tuple[Path, Path]:
|
| 113 |
+
suite = str(mapping_row["prompt_suite"])
|
| 114 |
+
suite_index = int(mapping_row["suite_index"])
|
| 115 |
+
global_index = int(mapping_row["global_index"])
|
| 116 |
+
return (
|
| 117 |
+
output_root
|
| 118 |
+
/ "generated_videos"
|
| 119 |
+
/ strategy_name
|
| 120 |
+
/ suite
|
| 121 |
+
/ f"{suite_index:03d}.mp4",
|
| 122 |
+
output_root
|
| 123 |
+
/ "generation_metrics/per_prompt"
|
| 124 |
+
/ strategy_name
|
| 125 |
+
/ f"global_{global_index:04d}.json",
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def is_prompt_complete(
|
| 130 |
+
output_root: Path,
|
| 131 |
+
mapping_row: dict[str, Any],
|
| 132 |
+
strategies: list[dict[str, Any]],
|
| 133 |
+
) -> bool:
|
| 134 |
+
return all(
|
| 135 |
+
all(
|
| 136 |
+
path.is_file()
|
| 137 |
+
for path in artifact_paths(
|
| 138 |
+
output_root, mapping_row, str(strategy["name"])
|
| 139 |
+
)
|
| 140 |
+
)
|
| 141 |
+
for strategy in strategies
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def run_prompt(
|
| 146 |
+
*,
|
| 147 |
+
mapping_row: dict[str, Any],
|
| 148 |
+
strategy: dict[str, Any],
|
| 149 |
+
reference_root: Path,
|
| 150 |
+
output_root: Path,
|
| 151 |
+
seed: int,
|
| 152 |
+
vae: Any,
|
| 153 |
+
pipeline: Any,
|
| 154 |
+
predictor: Any,
|
| 155 |
+
text_encoder: Any,
|
| 156 |
+
lpips_model: Any,
|
| 157 |
+
device: torch.device,
|
| 158 |
+
) -> None:
|
| 159 |
+
global_index = int(mapping_row["global_index"])
|
| 160 |
+
suite = str(mapping_row["prompt_suite"])
|
| 161 |
+
suite_index = int(mapping_row["suite_index"])
|
| 162 |
+
prompt = str(mapping_row["extended_prompt"])
|
| 163 |
+
strategy_name = str(strategy["name"])
|
| 164 |
+
reference_path = (
|
| 165 |
+
reference_root
|
| 166 |
+
/ "generated_videos/ffff"
|
| 167 |
+
/ suite
|
| 168 |
+
/ f"{suite_index:03d}.mp4"
|
| 169 |
+
)
|
| 170 |
+
print(f"[prompt] global={global_index} suite={suite}/{suite_index}", flush=True)
|
| 171 |
+
conditional = text_encoder(text_prompts=[prompt])
|
| 172 |
+
latent, diagnostic = extended.generate_rollout(
|
| 173 |
+
pipeline=pipeline,
|
| 174 |
+
conditional_dict=conditional,
|
| 175 |
+
seed=seed,
|
| 176 |
+
device=device,
|
| 177 |
+
predictor=predictor,
|
| 178 |
+
head=None,
|
| 179 |
+
config=strategy,
|
| 180 |
+
)
|
| 181 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 182 |
+
pixels = vae.decode_to_pixel(latent, use_cache=False)
|
| 183 |
+
prediction_u8 = base.pixels_to_u8(pixels)
|
| 184 |
+
video_path, record_path = artifact_paths(output_root, mapping_row, strategy_name)
|
| 185 |
+
extended.atomic_video(prediction_u8, video_path)
|
| 186 |
+
|
| 187 |
+
# The original run retained FFFF MP4s, not its pre-encoding uint8 frames.
|
| 188 |
+
# Decode both sides so the reused reference and DISCA prediction receive
|
| 189 |
+
# the same H.264/CRF-18 treatment before full-81-frame pixel metrics.
|
| 190 |
+
reference_decoded = read_u8_video(reference_path)
|
| 191 |
+
prediction_decoded = read_u8_video(video_path)
|
| 192 |
+
metrics = base.frame_metrics(
|
| 193 |
+
reference_u8=reference_decoded,
|
| 194 |
+
prediction_u8=prediction_decoded,
|
| 195 |
+
lpips_model=lpips_model,
|
| 196 |
+
batch_size=4,
|
| 197 |
+
device=device,
|
| 198 |
+
)
|
| 199 |
+
record = {
|
| 200 |
+
"status": "complete",
|
| 201 |
+
"strategy": strategy_name,
|
| 202 |
+
"policy": "fppf",
|
| 203 |
+
"pattern": strategy["pattern"],
|
| 204 |
+
"input_variant": strategy["input_variant"],
|
| 205 |
+
"candidate_steps": strategy["candidate_steps"],
|
| 206 |
+
"allow_chunk0_predictor": strategy["allow_chunk0_predictor"],
|
| 207 |
+
"global_index": global_index,
|
| 208 |
+
"prompt_suite": suite,
|
| 209 |
+
"suite_index": suite_index,
|
| 210 |
+
"prompt": prompt,
|
| 211 |
+
"seed": seed,
|
| 212 |
+
"generation": {
|
| 213 |
+
key: value
|
| 214 |
+
for key, value in diagnostic.items()
|
| 215 |
+
if key != "decisions"
|
| 216 |
+
},
|
| 217 |
+
"decisions": diagnostic["decisions"],
|
| 218 |
+
"pixel_metrics_vs_ffff": extended.compact_metrics(metrics),
|
| 219 |
+
"pixel_metric_input": "MP4-decoded uint8 RGB, all 81 frames on both sides",
|
| 220 |
+
"reference_video": str(reference_path),
|
| 221 |
+
"video": str(video_path.relative_to(output_root)),
|
| 222 |
+
}
|
| 223 |
+
extended.atomic_json(record_path, record)
|
| 224 |
+
print(
|
| 225 |
+
f"[result] global={global_index} strategy={strategy_name} "
|
| 226 |
+
f"accept={diagnostic['accepted_predictor_calls']} "
|
| 227 |
+
f"psnr={metrics['psnr']:.4f} ssim={metrics['ssim']:.6f} "
|
| 228 |
+
f"lpips={metrics['lpips']:.6f}",
|
| 229 |
+
flush=True,
|
| 230 |
+
)
|
| 231 |
+
if hasattr(vae.model, "clear_cache"):
|
| 232 |
+
vae.model.clear_cache()
|
| 233 |
+
del (
|
| 234 |
+
conditional,
|
| 235 |
+
latent,
|
| 236 |
+
pixels,
|
| 237 |
+
prediction_u8,
|
| 238 |
+
reference_decoded,
|
| 239 |
+
prediction_decoded,
|
| 240 |
+
metrics,
|
| 241 |
+
)
|
| 242 |
+
torch.cuda.empty_cache()
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def parse_args() -> argparse.Namespace:
|
| 246 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 247 |
+
parser.add_argument("--gpu", default=extended.PHYSICAL_GPU)
|
| 248 |
+
parser.add_argument("--mapping", type=Path, default=MAPPING_DEFAULT)
|
| 249 |
+
parser.add_argument("--predictor-weights", type=Path, default=WEIGHTS_DEFAULT)
|
| 250 |
+
parser.add_argument("--output-root", type=Path, default=OUTPUT_DEFAULT)
|
| 251 |
+
parser.add_argument("--reference-root", type=Path, default=REFERENCE_DEFAULT)
|
| 252 |
+
parser.add_argument(
|
| 253 |
+
"--pattern",
|
| 254 |
+
action="append",
|
| 255 |
+
choices=tuple(PATTERN_TO_CANDIDATE_STEPS),
|
| 256 |
+
default=None,
|
| 257 |
+
help="DISCA denoise pattern; repeat to generate several. Default: FPPF.",
|
| 258 |
+
)
|
| 259 |
+
parser.add_argument("--shard-index", type=int, default=0)
|
| 260 |
+
parser.add_argument("--num-shards", type=int, default=1)
|
| 261 |
+
parser.add_argument(
|
| 262 |
+
"--global-index",
|
| 263 |
+
dest="global_indices",
|
| 264 |
+
action="append",
|
| 265 |
+
type=int,
|
| 266 |
+
default=None,
|
| 267 |
+
help="Process only this global mapping index; may be repeated.",
|
| 268 |
+
)
|
| 269 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 270 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 271 |
+
return parser.parse_args()
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
def main() -> None:
|
| 275 |
+
args = parse_args()
|
| 276 |
+
if args.num_shards < 1 or not 0 <= args.shard_index < args.num_shards:
|
| 277 |
+
raise ValueError("Invalid shard index/number of shards")
|
| 278 |
+
weights = args.predictor_weights.resolve()
|
| 279 |
+
if not weights.is_file():
|
| 280 |
+
raise FileNotFoundError(weights)
|
| 281 |
+
mapping = extended.read_mapping(args.mapping.resolve())
|
| 282 |
+
patterns = args.pattern or ["FPPF"]
|
| 283 |
+
if len(set(patterns)) != len(patterns):
|
| 284 |
+
raise ValueError("--pattern values must be unique")
|
| 285 |
+
strategies = strategy_configs(patterns)
|
| 286 |
+
reference_root = args.reference_root.resolve()
|
| 287 |
+
reference_records = reference_root / "generation_metrics/per_prompt/ffff"
|
| 288 |
+
reference_videos = reference_root / "generated_videos/ffff"
|
| 289 |
+
if len(list(reference_records.glob("global_*.json"))) != 251:
|
| 290 |
+
raise ValueError(f"Incomplete FFFF reference records: {reference_records}")
|
| 291 |
+
if len(list(reference_videos.rglob("*.mp4"))) != 251:
|
| 292 |
+
raise ValueError(f"Incomplete FFFF reference videos: {reference_videos}")
|
| 293 |
+
if args.global_indices is None:
|
| 294 |
+
rows = [
|
| 295 |
+
row
|
| 296 |
+
for position, row in enumerate(mapping)
|
| 297 |
+
if position % args.num_shards == args.shard_index
|
| 298 |
+
]
|
| 299 |
+
else:
|
| 300 |
+
requested = set(args.global_indices)
|
| 301 |
+
mapping_by_global = {int(row["global_index"]): row for row in mapping}
|
| 302 |
+
unknown = sorted(requested - set(mapping_by_global))
|
| 303 |
+
if unknown:
|
| 304 |
+
raise ValueError(f"Unknown global indices: {unknown}")
|
| 305 |
+
rows = [row for row in mapping if int(row["global_index"]) in requested]
|
| 306 |
+
|
| 307 |
+
output_root = args.output_root.resolve()
|
| 308 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 309 |
+
extended.write_shard_manifest(
|
| 310 |
+
output_root,
|
| 311 |
+
str(args.gpu),
|
| 312 |
+
args.shard_index,
|
| 313 |
+
args.num_shards,
|
| 314 |
+
rows,
|
| 315 |
+
strategies,
|
| 316 |
+
"running",
|
| 317 |
+
)
|
| 318 |
+
print(
|
| 319 |
+
f"[setup] physical_gpu={args.gpu} shard={args.shard_index}/{args.num_shards} "
|
| 320 |
+
f"prompts={len(rows)} strategies={','.join(strategy['name'] for strategy in strategies)} "
|
| 321 |
+
"reference=existing_ffff",
|
| 322 |
+
flush=True,
|
| 323 |
+
)
|
| 324 |
+
device = torch.device("cuda")
|
| 325 |
+
torch.set_grad_enabled(False)
|
| 326 |
+
models = load_models(weights, device)
|
| 327 |
+
completed = 0
|
| 328 |
+
try:
|
| 329 |
+
for row in rows:
|
| 330 |
+
if not args.overwrite and is_prompt_complete(
|
| 331 |
+
output_root, row, strategies
|
| 332 |
+
):
|
| 333 |
+
completed += 1
|
| 334 |
+
print(
|
| 335 |
+
f"[cached] {completed}/{len(rows)} global={row['global_index']}",
|
| 336 |
+
flush=True,
|
| 337 |
+
)
|
| 338 |
+
continue
|
| 339 |
+
for strategy in strategies:
|
| 340 |
+
video, record = artifact_paths(
|
| 341 |
+
output_root, row, str(strategy["name"])
|
| 342 |
+
)
|
| 343 |
+
if not args.overwrite and video.is_file() and record.is_file():
|
| 344 |
+
continue
|
| 345 |
+
run_prompt(
|
| 346 |
+
mapping_row=row,
|
| 347 |
+
strategy=strategy,
|
| 348 |
+
reference_root=reference_root,
|
| 349 |
+
output_root=output_root,
|
| 350 |
+
seed=args.seed,
|
| 351 |
+
vae=models[0],
|
| 352 |
+
pipeline=models[1],
|
| 353 |
+
predictor=models[2],
|
| 354 |
+
text_encoder=models[3],
|
| 355 |
+
lpips_model=models[6],
|
| 356 |
+
device=device,
|
| 357 |
+
)
|
| 358 |
+
completed += 1
|
| 359 |
+
print(f"[progress] {completed}/{len(rows)} prompts", flush=True)
|
| 360 |
+
finally:
|
| 361 |
+
extended.write_shard_manifest(
|
| 362 |
+
output_root,
|
| 363 |
+
str(args.gpu),
|
| 364 |
+
args.shard_index,
|
| 365 |
+
args.num_shards,
|
| 366 |
+
rows,
|
| 367 |
+
strategies,
|
| 368 |
+
"complete" if completed == len(rows) else "failed",
|
| 369 |
+
)
|
| 370 |
+
print(f"[complete] gpu={args.gpu} prompts={completed}/{len(rows)}", flush=True)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
if __name__ == "__main__":
|
| 374 |
+
main()
|
scripts/generate_vbench8_extended_naive_baselines.py
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate Extended-251 videos for timestep-skipping and velocity-cache baselines.
|
| 3 |
+
|
| 4 |
+
For every prompt, an original four-step FFFF rollout is generated first with
|
| 5 |
+
the same seed and used as the PSNR/SSIM/LPIPS reference. Timestep-skipping
|
| 6 |
+
policies shorten the denoising schedule itself. Velocity-cache policies retain
|
| 7 |
+
the original four timesteps and, at each R, reuse the velocity from the most
|
| 8 |
+
recent F while converting it to x0 using the current noisy latent and timestep.
|
| 9 |
+
The cache pattern is applied independently to every autoregressive chunk.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import hashlib
|
| 16 |
+
import json
|
| 17 |
+
import os
|
| 18 |
+
import sys
|
| 19 |
+
import tempfile
|
| 20 |
+
import time
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Any
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def preparse_gpu() -> str:
|
| 26 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 27 |
+
parser.add_argument("--gpu", default="0")
|
| 28 |
+
args, _ = parser.parse_known_args()
|
| 29 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 30 |
+
os.environ.setdefault("MPLCONFIGDIR", tempfile.mkdtemp(prefix="self_forcing_mpl_"))
|
| 31 |
+
return str(args.gpu)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
PHYSICAL_GPU = preparse_gpu()
|
| 35 |
+
|
| 36 |
+
import lpips
|
| 37 |
+
import torch
|
| 38 |
+
from omegaconf import OmegaConf
|
| 39 |
+
|
| 40 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 41 |
+
if str(REPO_ROOT) not in sys.path:
|
| 42 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 43 |
+
|
| 44 |
+
from scripts import evaluate_single_block_fppf as base
|
| 45 |
+
from scripts.naive_vbench_policies import (
|
| 46 |
+
EVALUATION_STRATEGY_NAMES,
|
| 47 |
+
POLICIES_BY_NAME,
|
| 48 |
+
REFERENCE_POLICY,
|
| 49 |
+
NaivePolicy,
|
| 50 |
+
)
|
| 51 |
+
from scripts.vbench8_protocol import SUITE_COUNTS
|
| 52 |
+
from utils.misc import set_seed
|
| 53 |
+
from utils.wan_wrapper import WanTextEncoder, WanVAEWrapper
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
NUM_CHUNKS = base.NUM_CHUNKS
|
| 57 |
+
FRAMES_PER_CHUNK = base.FRAMES_PER_CHUNK
|
| 58 |
+
MAPPING_DEFAULT = REPO_ROOT / "assets/vbench8_extended_subset_mapping.json"
|
| 59 |
+
OUTPUT_DEFAULT = REPO_ROOT / "evaluation_runs/vbench8_extended_naive_baselines"
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def atomic_json(path: Path, value: Any) -> None:
|
| 63 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 64 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 65 |
+
temporary.write_text(
|
| 66 |
+
json.dumps(value, ensure_ascii=False, indent=2, allow_nan=True) + "\n",
|
| 67 |
+
encoding="utf-8",
|
| 68 |
+
)
|
| 69 |
+
os.replace(temporary, path)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def atomic_video(frames: torch.Tensor, path: Path) -> None:
|
| 73 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 74 |
+
temporary = path.with_name(path.name + ".partial.mp4")
|
| 75 |
+
if temporary.exists():
|
| 76 |
+
temporary.unlink()
|
| 77 |
+
base.save_mp4(frames, temporary)
|
| 78 |
+
os.replace(temporary, path)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def read_mapping(path: Path) -> list[dict[str, Any]]:
|
| 82 |
+
value = json.loads(path.read_text(encoding="utf-8"))
|
| 83 |
+
if not isinstance(value, list) or len(value) != 251:
|
| 84 |
+
raise ValueError(f"Expected a 251-row mapping list: {path}")
|
| 85 |
+
counts = {suite: 0 for suite in SUITE_COUNTS}
|
| 86 |
+
globals_seen: set[int] = set()
|
| 87 |
+
suite_seen: dict[str, set[int]] = {suite: set() for suite in SUITE_COUNTS}
|
| 88 |
+
for row in value:
|
| 89 |
+
suite = str(row["prompt_suite"])
|
| 90 |
+
global_index = int(row["global_index"])
|
| 91 |
+
suite_index = int(row["suite_index"])
|
| 92 |
+
if suite not in counts:
|
| 93 |
+
raise ValueError(f"Unknown prompt suite: {suite}")
|
| 94 |
+
if global_index in globals_seen or suite_index in suite_seen[suite]:
|
| 95 |
+
raise ValueError(
|
| 96 |
+
f"Duplicate mapping index: {global_index} / {suite}:{suite_index}"
|
| 97 |
+
)
|
| 98 |
+
if not str(row["extended_prompt"]).strip():
|
| 99 |
+
raise ValueError(f"Empty prompt at global index {global_index}")
|
| 100 |
+
counts[suite] += 1
|
| 101 |
+
globals_seen.add(global_index)
|
| 102 |
+
suite_seen[suite].add(suite_index)
|
| 103 |
+
if counts != SUITE_COUNTS:
|
| 104 |
+
raise ValueError(f"Unexpected prompt-suite counts: {counts}")
|
| 105 |
+
return sorted(value, key=lambda row: int(row["global_index"]))
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def reset_runtime_caches(pipeline: Any, device: torch.device) -> None:
|
| 109 |
+
if pipeline.kv_cache1 is None:
|
| 110 |
+
pipeline._initialize_kv_cache(1, torch.bfloat16, device)
|
| 111 |
+
pipeline._initialize_crossattn_cache(1, torch.bfloat16, device)
|
| 112 |
+
for cache in pipeline.kv_cache1:
|
| 113 |
+
cache["global_end_index"].zero_()
|
| 114 |
+
cache["local_end_index"].zero_()
|
| 115 |
+
for cache in pipeline.crossattn_cache:
|
| 116 |
+
cache["is_init"] = False
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def warped_timesteps(pipeline: Any, raw_timesteps: tuple[int, ...]) -> torch.Tensor:
|
| 120 |
+
raw = torch.tensor(raw_timesteps, dtype=torch.long)
|
| 121 |
+
if not bool(pipeline.args.warp_denoising_step):
|
| 122 |
+
return raw
|
| 123 |
+
scheduler_timesteps = torch.cat(
|
| 124 |
+
(pipeline.scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32))
|
| 125 |
+
)
|
| 126 |
+
return scheduler_timesteps[1000 - raw]
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def policy_timesteps(pipeline: Any, policy: NaivePolicy) -> torch.Tensor:
|
| 130 |
+
if policy.family in {"full", "velocity_reuse"}:
|
| 131 |
+
actual = pipeline.denoising_step_list.detach().cpu()
|
| 132 |
+
else:
|
| 133 |
+
actual = warped_timesteps(pipeline, policy.raw_timesteps)
|
| 134 |
+
if len(actual) != policy.denoise_steps:
|
| 135 |
+
raise ValueError(
|
| 136 |
+
f"{policy.name}: expected {policy.denoise_steps} timesteps, got {len(actual)}"
|
| 137 |
+
)
|
| 138 |
+
return actual
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def start_timing() -> tuple[torch.cuda.Event, torch.cuda.Event]:
|
| 142 |
+
start = torch.cuda.Event(enable_timing=True)
|
| 143 |
+
end = torch.cuda.Event(enable_timing=True)
|
| 144 |
+
start.record()
|
| 145 |
+
return start, end
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def finish_timing(
|
| 149 |
+
events_by_category: dict[str, list[tuple[torch.cuda.Event, torch.cuda.Event]]],
|
| 150 |
+
category: str,
|
| 151 |
+
events: tuple[torch.cuda.Event, torch.cuda.Event],
|
| 152 |
+
) -> None:
|
| 153 |
+
events[1].record()
|
| 154 |
+
events_by_category[category].append(events)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
@torch.inference_mode()
|
| 158 |
+
def generate_rollout(
|
| 159 |
+
*,
|
| 160 |
+
pipeline: Any,
|
| 161 |
+
conditional_dict: dict[str, torch.Tensor],
|
| 162 |
+
seed: int,
|
| 163 |
+
device: torch.device,
|
| 164 |
+
policy: NaivePolicy,
|
| 165 |
+
) -> tuple[torch.Tensor, dict[str, Any]]:
|
| 166 |
+
reset_runtime_caches(pipeline, device)
|
| 167 |
+
set_seed(seed)
|
| 168 |
+
noise = torch.randn(
|
| 169 |
+
1,
|
| 170 |
+
NUM_CHUNKS * FRAMES_PER_CHUNK,
|
| 171 |
+
base.LATENT_CHANNELS,
|
| 172 |
+
base.LATENT_HEIGHT,
|
| 173 |
+
base.LATENT_WIDTH,
|
| 174 |
+
dtype=torch.bfloat16,
|
| 175 |
+
device=device,
|
| 176 |
+
)
|
| 177 |
+
timesteps_cpu = policy_timesteps(pipeline, policy)
|
| 178 |
+
actual_timestep_values = [float(value) for value in timesteps_cpu]
|
| 179 |
+
timesteps = timesteps_cpu.to(device=device)
|
| 180 |
+
output_chunks: list[torch.Tensor] = []
|
| 181 |
+
decisions: list[dict[str, Any]] = []
|
| 182 |
+
full_calls = 0
|
| 183 |
+
reuse_calls = 0
|
| 184 |
+
timing_events: dict[str, list[tuple[torch.cuda.Event, torch.cuda.Event]]] = {
|
| 185 |
+
"full_dit": [],
|
| 186 |
+
"context_dit": [],
|
| 187 |
+
}
|
| 188 |
+
started = time.perf_counter()
|
| 189 |
+
|
| 190 |
+
for chunk in range(NUM_CHUNKS):
|
| 191 |
+
noisy_input = noise[
|
| 192 |
+
:, chunk * FRAMES_PER_CHUNK : (chunk + 1) * FRAMES_PER_CHUNK
|
| 193 |
+
]
|
| 194 |
+
cached_flow: torch.Tensor | None = None
|
| 195 |
+
denoised_pred: torch.Tensor | None = None
|
| 196 |
+
timestep: torch.Tensor | None = None
|
| 197 |
+
for step, current_timestep in enumerate(timesteps):
|
| 198 |
+
timestep = torch.ones(
|
| 199 |
+
[1, FRAMES_PER_CHUNK], dtype=torch.int64, device=device
|
| 200 |
+
) * current_timestep
|
| 201 |
+
action = (
|
| 202 |
+
policy.cache_pattern[step]
|
| 203 |
+
if policy.family == "velocity_reuse" and policy.cache_pattern is not None
|
| 204 |
+
else "F"
|
| 205 |
+
)
|
| 206 |
+
if action == "R":
|
| 207 |
+
if cached_flow is None:
|
| 208 |
+
raise RuntimeError(
|
| 209 |
+
f"{policy.name}: reuse requested before a full step in chunk {chunk}"
|
| 210 |
+
)
|
| 211 |
+
denoised_pred = pipeline.generator._convert_flow_pred_to_x0(
|
| 212 |
+
flow_pred=cached_flow.flatten(0, 1),
|
| 213 |
+
xt=noisy_input.flatten(0, 1),
|
| 214 |
+
timestep=timestep.flatten(0, 1),
|
| 215 |
+
).unflatten(0, cached_flow.shape[:2])
|
| 216 |
+
reuse_calls += 1
|
| 217 |
+
else:
|
| 218 |
+
full_events = start_timing()
|
| 219 |
+
flow, denoised_pred = pipeline.generator(
|
| 220 |
+
noisy_image_or_video=noisy_input,
|
| 221 |
+
conditional_dict=conditional_dict,
|
| 222 |
+
timestep=timestep,
|
| 223 |
+
kv_cache=pipeline.kv_cache1,
|
| 224 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 225 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 226 |
+
)
|
| 227 |
+
finish_timing(timing_events, "full_dit", full_events)
|
| 228 |
+
cached_flow = flow.detach()
|
| 229 |
+
full_calls += 1
|
| 230 |
+
decisions.append(
|
| 231 |
+
{
|
| 232 |
+
"chunk": chunk,
|
| 233 |
+
"step": step,
|
| 234 |
+
"action": action,
|
| 235 |
+
"timestep": actual_timestep_values[step],
|
| 236 |
+
}
|
| 237 |
+
)
|
| 238 |
+
if step < len(timesteps) - 1:
|
| 239 |
+
if denoised_pred is None:
|
| 240 |
+
raise RuntimeError("Denoising step did not produce x0")
|
| 241 |
+
next_timestep = timesteps[step + 1]
|
| 242 |
+
flat = denoised_pred.flatten(0, 1)
|
| 243 |
+
noisy_input = pipeline.scheduler.add_noise(
|
| 244 |
+
flat,
|
| 245 |
+
torch.randn_like(flat),
|
| 246 |
+
next_timestep
|
| 247 |
+
* torch.ones(
|
| 248 |
+
[FRAMES_PER_CHUNK], dtype=torch.long, device=device
|
| 249 |
+
),
|
| 250 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 251 |
+
|
| 252 |
+
if denoised_pred is None or timestep is None:
|
| 253 |
+
raise RuntimeError(f"{policy.name}: chunk {chunk} produced no clean latent")
|
| 254 |
+
output_chunks.append(denoised_pred)
|
| 255 |
+
context_events = start_timing()
|
| 256 |
+
pipeline.generator(
|
| 257 |
+
noisy_image_or_video=denoised_pred,
|
| 258 |
+
conditional_dict=conditional_dict,
|
| 259 |
+
timestep=torch.ones_like(timestep) * pipeline.args.context_noise,
|
| 260 |
+
kv_cache=pipeline.kv_cache1,
|
| 261 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 262 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 263 |
+
)
|
| 264 |
+
finish_timing(timing_events, "context_dit", context_events)
|
| 265 |
+
|
| 266 |
+
torch.cuda.synchronize()
|
| 267 |
+
elapsed = {
|
| 268 |
+
category: sum(start.elapsed_time(end) for start, end in events)
|
| 269 |
+
for category, events in timing_events.items()
|
| 270 |
+
}
|
| 271 |
+
expected_full = NUM_CHUNKS * policy.full_calls_per_chunk
|
| 272 |
+
expected_reuse = NUM_CHUNKS * policy.reuse_calls_per_chunk
|
| 273 |
+
if (full_calls, reuse_calls) != (expected_full, expected_reuse):
|
| 274 |
+
raise RuntimeError(
|
| 275 |
+
f"{policy.name}: got calls F={full_calls}, R={reuse_calls}; "
|
| 276 |
+
f"expected F={expected_full}, R={expected_reuse}"
|
| 277 |
+
)
|
| 278 |
+
return torch.cat(output_chunks, dim=1), {
|
| 279 |
+
"generation_time_s": time.perf_counter() - started,
|
| 280 |
+
"full_calls": full_calls,
|
| 281 |
+
"reuse_calls": reuse_calls,
|
| 282 |
+
"predictor_calls": 0,
|
| 283 |
+
"accepted_predictor_calls": 0,
|
| 284 |
+
"rejected_predictor_calls": 0,
|
| 285 |
+
"full_dit_time_ms": elapsed["full_dit"],
|
| 286 |
+
"predictor_time_ms": 0.0,
|
| 287 |
+
"confidence_head_time_ms": 0.0,
|
| 288 |
+
"context_dit_time_ms": elapsed["context_dit"],
|
| 289 |
+
"actual_dit_time_ms": elapsed["full_dit"] + elapsed["context_dit"],
|
| 290 |
+
"model_path_time_ms": elapsed["full_dit"],
|
| 291 |
+
"raw_timesteps": list(policy.raw_timesteps),
|
| 292 |
+
"actual_timesteps": actual_timestep_values,
|
| 293 |
+
"decisions": decisions,
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def compact_metrics(value: dict[str, Any]) -> dict[str, Any]:
|
| 298 |
+
keys = (
|
| 299 |
+
"pixel_mse",
|
| 300 |
+
"psnr",
|
| 301 |
+
"psnr_frame_mean",
|
| 302 |
+
"ssim",
|
| 303 |
+
"lpips",
|
| 304 |
+
"rollout_start_frame",
|
| 305 |
+
"rollout_pixel_mse",
|
| 306 |
+
"rollout_psnr",
|
| 307 |
+
"rollout_ssim",
|
| 308 |
+
"rollout_lpips",
|
| 309 |
+
"num_frames",
|
| 310 |
+
)
|
| 311 |
+
return {key: value[key] for key in keys}
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def identity_metrics(num_frames: int) -> dict[str, Any]:
|
| 315 |
+
return {
|
| 316 |
+
"pixel_mse": 0.0,
|
| 317 |
+
"psnr": 120.0,
|
| 318 |
+
"psnr_frame_mean": 120.0,
|
| 319 |
+
"ssim": 1.0,
|
| 320 |
+
"lpips": 0.0,
|
| 321 |
+
"rollout_start_frame": base.PIXEL_FRAMES_FIRST_CHUNK,
|
| 322 |
+
"rollout_pixel_mse": 0.0,
|
| 323 |
+
"rollout_psnr": 120.0,
|
| 324 |
+
"rollout_ssim": 1.0,
|
| 325 |
+
"rollout_lpips": 0.0,
|
| 326 |
+
"num_frames": num_frames,
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
def load_models(device: torch.device) -> tuple[Any, Any, Any, Any]:
|
| 331 |
+
print("[setup] loading VAE", flush=True)
|
| 332 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 333 |
+
config = OmegaConf.merge(
|
| 334 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 335 |
+
OmegaConf.load(REPO_ROOT / "configs/self_forcing_sid.yaml"),
|
| 336 |
+
)
|
| 337 |
+
print("[setup] loading frozen Self-Forcing Teacher", flush=True)
|
| 338 |
+
pipeline = base.build_pipeline(
|
| 339 |
+
config, REPO_ROOT / "checkpoints/self_forcing_dmd.pt", vae, device
|
| 340 |
+
)
|
| 341 |
+
text_encoder = WanTextEncoder().to(device=device, dtype=torch.bfloat16).eval()
|
| 342 |
+
text_encoder.requires_grad_(False)
|
| 343 |
+
print("[setup] loading LPIPS", flush=True)
|
| 344 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 345 |
+
lpips_model.requires_grad_(False)
|
| 346 |
+
return vae, pipeline, text_encoder, lpips_model
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def artifact_paths(
|
| 350 |
+
output_root: Path, mapping_row: dict[str, Any], strategy: str
|
| 351 |
+
) -> tuple[Path, Path]:
|
| 352 |
+
suite = str(mapping_row["prompt_suite"])
|
| 353 |
+
suite_index = int(mapping_row["suite_index"])
|
| 354 |
+
global_index = int(mapping_row["global_index"])
|
| 355 |
+
return (
|
| 356 |
+
output_root / "generated_videos" / strategy / suite / f"{suite_index:03d}.mp4",
|
| 357 |
+
output_root
|
| 358 |
+
/ "generation_metrics/per_prompt"
|
| 359 |
+
/ strategy
|
| 360 |
+
/ f"global_{global_index:04d}.json",
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
def is_prompt_complete(
|
| 365 |
+
output_root: Path, mapping_row: dict[str, Any], policies: tuple[NaivePolicy, ...]
|
| 366 |
+
) -> bool:
|
| 367 |
+
for policy in (REFERENCE_POLICY, *policies):
|
| 368 |
+
video, record = artifact_paths(output_root, mapping_row, policy.name)
|
| 369 |
+
if not video.is_file() or not record.is_file():
|
| 370 |
+
return False
|
| 371 |
+
return True
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
def write_result(
|
| 375 |
+
*,
|
| 376 |
+
output_root: Path,
|
| 377 |
+
mapping_row: dict[str, Any],
|
| 378 |
+
policy: NaivePolicy,
|
| 379 |
+
seed: int,
|
| 380 |
+
diagnostic: dict[str, Any],
|
| 381 |
+
pixel_metrics: dict[str, Any],
|
| 382 |
+
frames: torch.Tensor,
|
| 383 |
+
) -> None:
|
| 384 |
+
video_path, record_path = artifact_paths(output_root, mapping_row, policy.name)
|
| 385 |
+
atomic_video(frames, video_path)
|
| 386 |
+
diagnostic_without_decisions = {
|
| 387 |
+
key: value for key, value in diagnostic.items() if key != "decisions"
|
| 388 |
+
}
|
| 389 |
+
record = {
|
| 390 |
+
"status": "complete",
|
| 391 |
+
"strategy": policy.name,
|
| 392 |
+
"label": policy.label,
|
| 393 |
+
"policy": policy.to_dict(),
|
| 394 |
+
"global_index": int(mapping_row["global_index"]),
|
| 395 |
+
"prompt_suite": str(mapping_row["prompt_suite"]),
|
| 396 |
+
"suite_index": int(mapping_row["suite_index"]),
|
| 397 |
+
"prompt": str(mapping_row["extended_prompt"]),
|
| 398 |
+
"seed": seed,
|
| 399 |
+
"generation": diagnostic_without_decisions,
|
| 400 |
+
"decisions": diagnostic["decisions"],
|
| 401 |
+
"pixel_metrics_vs_ffff": pixel_metrics,
|
| 402 |
+
"video": str(video_path.relative_to(output_root)),
|
| 403 |
+
}
|
| 404 |
+
atomic_json(record_path, record)
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def run_prompt(
|
| 408 |
+
*,
|
| 409 |
+
mapping_row: dict[str, Any],
|
| 410 |
+
policies: tuple[NaivePolicy, ...],
|
| 411 |
+
output_root: Path,
|
| 412 |
+
seed: int,
|
| 413 |
+
vae: Any,
|
| 414 |
+
pipeline: Any,
|
| 415 |
+
text_encoder: Any,
|
| 416 |
+
lpips_model: Any,
|
| 417 |
+
device: torch.device,
|
| 418 |
+
) -> None:
|
| 419 |
+
global_index = int(mapping_row["global_index"])
|
| 420 |
+
suite = str(mapping_row["prompt_suite"])
|
| 421 |
+
suite_index = int(mapping_row["suite_index"])
|
| 422 |
+
prompt = str(mapping_row["extended_prompt"])
|
| 423 |
+
print(f"[prompt] global={global_index} suite={suite}/{suite_index}", flush=True)
|
| 424 |
+
conditional = text_encoder(text_prompts=[prompt])
|
| 425 |
+
|
| 426 |
+
reference_latent, reference_diagnostic = generate_rollout(
|
| 427 |
+
pipeline=pipeline,
|
| 428 |
+
conditional_dict=conditional,
|
| 429 |
+
seed=seed,
|
| 430 |
+
device=device,
|
| 431 |
+
policy=REFERENCE_POLICY,
|
| 432 |
+
)
|
| 433 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 434 |
+
reference_pixels = vae.decode_to_pixel(reference_latent, use_cache=False)
|
| 435 |
+
reference_u8 = base.pixels_to_u8(reference_pixels)
|
| 436 |
+
if reference_u8.ndim != 4 or reference_u8.shape[0] != 81:
|
| 437 |
+
raise RuntimeError(f"Unexpected reference shape: {tuple(reference_u8.shape)}")
|
| 438 |
+
write_result(
|
| 439 |
+
output_root=output_root,
|
| 440 |
+
mapping_row=mapping_row,
|
| 441 |
+
policy=REFERENCE_POLICY,
|
| 442 |
+
seed=seed,
|
| 443 |
+
diagnostic=reference_diagnostic,
|
| 444 |
+
pixel_metrics=identity_metrics(int(reference_u8.shape[0])),
|
| 445 |
+
frames=reference_u8,
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
for policy in policies:
|
| 449 |
+
latent, diagnostic = generate_rollout(
|
| 450 |
+
pipeline=pipeline,
|
| 451 |
+
conditional_dict=conditional,
|
| 452 |
+
seed=seed,
|
| 453 |
+
device=device,
|
| 454 |
+
policy=policy,
|
| 455 |
+
)
|
| 456 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 457 |
+
pixels = vae.decode_to_pixel(latent, use_cache=False)
|
| 458 |
+
prediction_u8 = base.pixels_to_u8(pixels)
|
| 459 |
+
metrics = base.frame_metrics(
|
| 460 |
+
reference_u8=reference_u8,
|
| 461 |
+
prediction_u8=prediction_u8,
|
| 462 |
+
lpips_model=lpips_model,
|
| 463 |
+
batch_size=4,
|
| 464 |
+
device=device,
|
| 465 |
+
)
|
| 466 |
+
write_result(
|
| 467 |
+
output_root=output_root,
|
| 468 |
+
mapping_row=mapping_row,
|
| 469 |
+
policy=policy,
|
| 470 |
+
seed=seed,
|
| 471 |
+
diagnostic=diagnostic,
|
| 472 |
+
pixel_metrics=compact_metrics(metrics),
|
| 473 |
+
frames=prediction_u8,
|
| 474 |
+
)
|
| 475 |
+
print(
|
| 476 |
+
f"[result] global={global_index} strategy={policy.name} "
|
| 477 |
+
f"F={diagnostic['full_calls']} R={diagnostic['reuse_calls']} "
|
| 478 |
+
f"psnr={metrics['psnr']:.4f} ssim={metrics['ssim']:.6f} "
|
| 479 |
+
f"lpips={metrics['lpips']:.6f}",
|
| 480 |
+
flush=True,
|
| 481 |
+
)
|
| 482 |
+
if hasattr(vae.model, "clear_cache"):
|
| 483 |
+
vae.model.clear_cache()
|
| 484 |
+
del latent, pixels, prediction_u8, metrics
|
| 485 |
+
torch.cuda.empty_cache()
|
| 486 |
+
|
| 487 |
+
if hasattr(vae.model, "clear_cache"):
|
| 488 |
+
vae.model.clear_cache()
|
| 489 |
+
del conditional, reference_latent, reference_pixels, reference_u8
|
| 490 |
+
torch.cuda.empty_cache()
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
def write_manifest(
|
| 494 |
+
*,
|
| 495 |
+
output_root: Path,
|
| 496 |
+
gpu: str,
|
| 497 |
+
shard_index: int,
|
| 498 |
+
num_shards: int,
|
| 499 |
+
rows: list[dict[str, Any]],
|
| 500 |
+
policies: tuple[NaivePolicy, ...],
|
| 501 |
+
seed: int,
|
| 502 |
+
status: str,
|
| 503 |
+
) -> None:
|
| 504 |
+
selected = "_".join(policy.name for policy in policies)
|
| 505 |
+
selection_id = hashlib.sha256(selected.encode("utf-8")).hexdigest()[:8]
|
| 506 |
+
manifest_name = f"naive_gpu{gpu}_shard{shard_index:02d}_{selection_id}.json"
|
| 507 |
+
atomic_json(
|
| 508 |
+
output_root / "generation_metrics/manifests" / manifest_name,
|
| 509 |
+
{
|
| 510 |
+
"status": status,
|
| 511 |
+
"physical_gpu": gpu,
|
| 512 |
+
"shard_index": shard_index,
|
| 513 |
+
"num_shards": num_shards,
|
| 514 |
+
"global_indices": [int(row["global_index"]) for row in rows],
|
| 515 |
+
"num_prompts": len(rows),
|
| 516 |
+
"strategies": [REFERENCE_POLICY.name, *(policy.name for policy in policies)],
|
| 517 |
+
"seed": seed,
|
| 518 |
+
},
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def parse_args() -> argparse.Namespace:
|
| 523 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 524 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 525 |
+
parser.add_argument("--mapping", type=Path, default=MAPPING_DEFAULT)
|
| 526 |
+
parser.add_argument("--output-root", type=Path, default=OUTPUT_DEFAULT)
|
| 527 |
+
parser.add_argument("--shard-index", type=int, default=0)
|
| 528 |
+
parser.add_argument("--num-shards", type=int, default=1)
|
| 529 |
+
parser.add_argument(
|
| 530 |
+
"--strategy",
|
| 531 |
+
action="append",
|
| 532 |
+
choices=EVALUATION_STRATEGY_NAMES,
|
| 533 |
+
default=None,
|
| 534 |
+
help="Baseline to generate; repeat to select several. Default: all six.",
|
| 535 |
+
)
|
| 536 |
+
parser.add_argument(
|
| 537 |
+
"--global-index",
|
| 538 |
+
dest="global_indices",
|
| 539 |
+
action="append",
|
| 540 |
+
type=int,
|
| 541 |
+
default=None,
|
| 542 |
+
help="Process only this global mapping index; may be repeated.",
|
| 543 |
+
)
|
| 544 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 545 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 546 |
+
return parser.parse_args()
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
def main() -> None:
|
| 550 |
+
args = parse_args()
|
| 551 |
+
if args.num_shards < 1 or not 0 <= args.shard_index < args.num_shards:
|
| 552 |
+
raise ValueError("Invalid shard index/number of shards")
|
| 553 |
+
strategy_names = args.strategy or list(EVALUATION_STRATEGY_NAMES)
|
| 554 |
+
if len(set(strategy_names)) != len(strategy_names):
|
| 555 |
+
raise ValueError("--strategy values must be unique")
|
| 556 |
+
policies = tuple(POLICIES_BY_NAME[name] for name in strategy_names)
|
| 557 |
+
mapping = read_mapping(args.mapping.resolve())
|
| 558 |
+
if args.global_indices is None:
|
| 559 |
+
rows = [
|
| 560 |
+
row
|
| 561 |
+
for position, row in enumerate(mapping)
|
| 562 |
+
if position % args.num_shards == args.shard_index
|
| 563 |
+
]
|
| 564 |
+
else:
|
| 565 |
+
requested = set(args.global_indices)
|
| 566 |
+
mapping_by_global = {int(row["global_index"]): row for row in mapping}
|
| 567 |
+
unknown = sorted(requested - set(mapping_by_global))
|
| 568 |
+
if unknown:
|
| 569 |
+
raise ValueError(f"Unknown global indices: {unknown}")
|
| 570 |
+
rows = [row for row in mapping if int(row["global_index"]) in requested]
|
| 571 |
+
|
| 572 |
+
output_root = args.output_root.resolve()
|
| 573 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 574 |
+
write_manifest(
|
| 575 |
+
output_root=output_root,
|
| 576 |
+
gpu=str(args.gpu),
|
| 577 |
+
shard_index=args.shard_index,
|
| 578 |
+
num_shards=args.num_shards,
|
| 579 |
+
rows=rows,
|
| 580 |
+
policies=policies,
|
| 581 |
+
seed=args.seed,
|
| 582 |
+
status="running",
|
| 583 |
+
)
|
| 584 |
+
print(
|
| 585 |
+
f"[setup] physical_gpu={args.gpu} shard={args.shard_index}/{args.num_shards} "
|
| 586 |
+
f"prompts={len(rows)} strategies={','.join(strategy_names)}",
|
| 587 |
+
flush=True,
|
| 588 |
+
)
|
| 589 |
+
device = torch.device("cuda")
|
| 590 |
+
torch.set_grad_enabled(False)
|
| 591 |
+
vae, pipeline, text_encoder, lpips_model = load_models(device)
|
| 592 |
+
completed = 0
|
| 593 |
+
try:
|
| 594 |
+
for row in rows:
|
| 595 |
+
if not args.overwrite and is_prompt_complete(output_root, row, policies):
|
| 596 |
+
completed += 1
|
| 597 |
+
print(
|
| 598 |
+
f"[cached] {completed}/{len(rows)} global={row['global_index']}",
|
| 599 |
+
flush=True,
|
| 600 |
+
)
|
| 601 |
+
continue
|
| 602 |
+
run_prompt(
|
| 603 |
+
mapping_row=row,
|
| 604 |
+
policies=policies,
|
| 605 |
+
output_root=output_root,
|
| 606 |
+
seed=args.seed,
|
| 607 |
+
vae=vae,
|
| 608 |
+
pipeline=pipeline,
|
| 609 |
+
text_encoder=text_encoder,
|
| 610 |
+
lpips_model=lpips_model,
|
| 611 |
+
device=device,
|
| 612 |
+
)
|
| 613 |
+
completed += 1
|
| 614 |
+
print(f"[progress] {completed}/{len(rows)} prompts", flush=True)
|
| 615 |
+
finally:
|
| 616 |
+
write_manifest(
|
| 617 |
+
output_root=output_root,
|
| 618 |
+
gpu=str(args.gpu),
|
| 619 |
+
shard_index=args.shard_index,
|
| 620 |
+
num_shards=args.num_shards,
|
| 621 |
+
rows=rows,
|
| 622 |
+
policies=policies,
|
| 623 |
+
seed=args.seed,
|
| 624 |
+
status="complete" if completed == len(rows) else "failed",
|
| 625 |
+
)
|
| 626 |
+
print(f"[complete] gpu={args.gpu} prompts={completed}/{len(rows)}", flush=True)
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
if __name__ == "__main__":
|
| 630 |
+
main()
|
scripts/generate_vbench8_extended_spanrisk.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate one outgoing-span Confidence-Head strategy for Extended-251.
|
| 3 |
+
|
| 4 |
+
The FFFF baseline is reused from the established Extended-251 run. Pixel
|
| 5 |
+
metrics decode both MP4s, so reference and prediction receive identical video
|
| 6 |
+
encoding/decoding treatment.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
import json
|
| 13 |
+
import sys
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 18 |
+
if str(REPO_ROOT) not in sys.path:
|
| 19 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 20 |
+
|
| 21 |
+
from scripts import generate_vbench8_extended_strategies as extended
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
from torchvision.io import read_video
|
| 25 |
+
|
| 26 |
+
from scripts import evaluate_single_block_fppf as base
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
MAPPING_DEFAULT = REPO_ROOT / "assets/vbench8_extended_subset_mapping.json"
|
| 30 |
+
EXPERIMENT_DEFAULT = (
|
| 31 |
+
REPO_ROOT / "confidence_experiments/layer17_stage1_step2000_20260831"
|
| 32 |
+
)
|
| 33 |
+
SELECTION_DEFAULT = (
|
| 34 |
+
REPO_ROOT
|
| 35 |
+
/ "confidence_experiments"
|
| 36 |
+
/ "layer17_stage1_step2000_spanrisk_beta2_threshold_vbench3_20260901"
|
| 37 |
+
/ "threshold_summary.json"
|
| 38 |
+
)
|
| 39 |
+
REFERENCE_DEFAULT = (
|
| 40 |
+
REPO_ROOT / "evaluation_runs/vbench8_extended_stage1_step2000_20260901"
|
| 41 |
+
)
|
| 42 |
+
OUTPUT_DEFAULT = (
|
| 43 |
+
REPO_ROOT
|
| 44 |
+
/ "evaluation_runs/vbench8_extended_stage1_step2000_spanrisk_beta2_20260901"
|
| 45 |
+
)
|
| 46 |
+
STRATEGIES = (
|
| 47 |
+
"step12_span_k06",
|
| 48 |
+
"step12_span_k08",
|
| 49 |
+
"step12_span_k10",
|
| 50 |
+
"step123_span_k06",
|
| 51 |
+
"step123_span_k09",
|
| 52 |
+
"step123_span_k12",
|
| 53 |
+
"step123_span_k15",
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def load_strategy_configs(path: Path) -> dict[str, dict[str, Any]]:
|
| 58 |
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
| 59 |
+
if payload.get("status") != "complete" or float(payload["beta"]) != 2.0:
|
| 60 |
+
raise ValueError(f"Threshold selection is not completed fixed-beta=2: {path}")
|
| 61 |
+
configs: dict[str, dict[str, Any]] = {}
|
| 62 |
+
for row in payload["selected"]:
|
| 63 |
+
head = str(row["head"])
|
| 64 |
+
target = int(row["target_accepts"])
|
| 65 |
+
name = f"{head}_span_k{target:02d}"
|
| 66 |
+
configs[name] = {
|
| 67 |
+
"name": name,
|
| 68 |
+
"policy": "dynamic",
|
| 69 |
+
"candidate_steps": [int(step) for step in row["candidate_steps"]],
|
| 70 |
+
"beta": float(row["beta"]),
|
| 71 |
+
"threshold": float(row["threshold"]),
|
| 72 |
+
"target_accepts": target,
|
| 73 |
+
"head": head,
|
| 74 |
+
"risk_mode": "outgoing_span",
|
| 75 |
+
"source_config_name": str(row["name"]),
|
| 76 |
+
}
|
| 77 |
+
if tuple(configs) != STRATEGIES:
|
| 78 |
+
raise ValueError(f"Unexpected selected strategies: {tuple(configs)}")
|
| 79 |
+
return configs
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def read_u8_video(path: Path) -> torch.Tensor:
|
| 83 |
+
if not path.is_file():
|
| 84 |
+
raise FileNotFoundError(path)
|
| 85 |
+
frames, _, _ = read_video(str(path), pts_unit="sec", output_format="TCHW")
|
| 86 |
+
frames = frames.to(device="cpu", dtype=torch.uint8).contiguous()
|
| 87 |
+
if frames.ndim != 4 or frames.shape[0] != 81 or frames.shape[1] != 3:
|
| 88 |
+
raise RuntimeError(f"Unexpected decoded video shape {tuple(frames.shape)}: {path}")
|
| 89 |
+
return frames
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def artifact_paths(
|
| 93 |
+
output_root: Path, mapping_row: dict[str, Any], strategy_name: str
|
| 94 |
+
) -> tuple[Path, Path]:
|
| 95 |
+
suite = str(mapping_row["prompt_suite"])
|
| 96 |
+
suite_index = int(mapping_row["suite_index"])
|
| 97 |
+
global_index = int(mapping_row["global_index"])
|
| 98 |
+
return (
|
| 99 |
+
output_root / "generated_videos" / strategy_name / suite / f"{suite_index:03d}.mp4",
|
| 100 |
+
output_root
|
| 101 |
+
/ "generation_metrics/per_prompt"
|
| 102 |
+
/ strategy_name
|
| 103 |
+
/ f"global_{global_index:04d}.json",
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def run_prompt(
|
| 108 |
+
*,
|
| 109 |
+
mapping_row: dict[str, Any],
|
| 110 |
+
strategy: dict[str, Any],
|
| 111 |
+
reference_root: Path,
|
| 112 |
+
output_root: Path,
|
| 113 |
+
seed: int,
|
| 114 |
+
models: tuple[Any, ...],
|
| 115 |
+
device: torch.device,
|
| 116 |
+
) -> None:
|
| 117 |
+
global_index = int(mapping_row["global_index"])
|
| 118 |
+
suite = str(mapping_row["prompt_suite"])
|
| 119 |
+
suite_index = int(mapping_row["suite_index"])
|
| 120 |
+
prompt = str(mapping_row["extended_prompt"])
|
| 121 |
+
name = str(strategy["name"])
|
| 122 |
+
reference_path = (
|
| 123 |
+
reference_root / "generated_videos/ffff" / suite / f"{suite_index:03d}.mp4"
|
| 124 |
+
)
|
| 125 |
+
print(f"[prompt] global={global_index} suite={suite}/{suite_index}", flush=True)
|
| 126 |
+
conditional = models[3](text_prompts=[prompt])
|
| 127 |
+
head = models[4] if strategy["head"] == "step12" else models[5]
|
| 128 |
+
latent, diagnostic = extended.generate_rollout(
|
| 129 |
+
pipeline=models[1],
|
| 130 |
+
conditional_dict=conditional,
|
| 131 |
+
seed=seed,
|
| 132 |
+
device=device,
|
| 133 |
+
predictor=models[2],
|
| 134 |
+
head=head,
|
| 135 |
+
config=strategy,
|
| 136 |
+
)
|
| 137 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 138 |
+
pixels = models[0].decode_to_pixel(latent, use_cache=False)
|
| 139 |
+
prediction_u8 = base.pixels_to_u8(pixels)
|
| 140 |
+
video_path, record_path = artifact_paths(output_root, mapping_row, name)
|
| 141 |
+
extended.atomic_video(prediction_u8, video_path)
|
| 142 |
+
|
| 143 |
+
reference_decoded = read_u8_video(reference_path)
|
| 144 |
+
prediction_decoded = read_u8_video(video_path)
|
| 145 |
+
metrics = base.frame_metrics(
|
| 146 |
+
reference_u8=reference_decoded,
|
| 147 |
+
prediction_u8=prediction_decoded,
|
| 148 |
+
lpips_model=models[6],
|
| 149 |
+
batch_size=4,
|
| 150 |
+
device=device,
|
| 151 |
+
)
|
| 152 |
+
record = {
|
| 153 |
+
"status": "complete",
|
| 154 |
+
"strategy": name,
|
| 155 |
+
"policy": "dynamic",
|
| 156 |
+
"candidate_steps": strategy["candidate_steps"],
|
| 157 |
+
"beta": strategy["beta"],
|
| 158 |
+
"threshold": strategy["threshold"],
|
| 159 |
+
"target_accepts": strategy["target_accepts"],
|
| 160 |
+
"risk_mode": "outgoing_span",
|
| 161 |
+
"allow_chunk0_predictor": False,
|
| 162 |
+
"source_config_name": strategy["source_config_name"],
|
| 163 |
+
"global_index": global_index,
|
| 164 |
+
"prompt_suite": suite,
|
| 165 |
+
"suite_index": suite_index,
|
| 166 |
+
"prompt": prompt,
|
| 167 |
+
"seed": seed,
|
| 168 |
+
"generation": {
|
| 169 |
+
key: value for key, value in diagnostic.items() if key != "decisions"
|
| 170 |
+
},
|
| 171 |
+
"decisions": diagnostic["decisions"],
|
| 172 |
+
"pixel_metrics_vs_ffff": extended.compact_metrics(metrics),
|
| 173 |
+
"pixel_metric_input": "MP4-decoded uint8 RGB, all 81 frames on both sides",
|
| 174 |
+
"reference_video": str(reference_path),
|
| 175 |
+
"video": str(video_path.relative_to(output_root)),
|
| 176 |
+
}
|
| 177 |
+
extended.atomic_json(record_path, record)
|
| 178 |
+
print(
|
| 179 |
+
f"[result] global={global_index} strategy={name} "
|
| 180 |
+
f"accept={diagnostic['accepted_predictor_calls']} "
|
| 181 |
+
f"psnr={metrics['psnr']:.4f} ssim={metrics['ssim']:.6f} "
|
| 182 |
+
f"lpips={metrics['lpips']:.6f}",
|
| 183 |
+
flush=True,
|
| 184 |
+
)
|
| 185 |
+
if hasattr(models[0].model, "clear_cache"):
|
| 186 |
+
models[0].model.clear_cache()
|
| 187 |
+
del conditional, latent, pixels, prediction_u8, reference_decoded, prediction_decoded
|
| 188 |
+
torch.cuda.empty_cache()
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def parse_args() -> argparse.Namespace:
|
| 192 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 193 |
+
parser.add_argument("--gpu", default=extended.PHYSICAL_GPU)
|
| 194 |
+
parser.add_argument("--strategy", required=True, choices=STRATEGIES)
|
| 195 |
+
parser.add_argument("--mapping", type=Path, default=MAPPING_DEFAULT)
|
| 196 |
+
parser.add_argument("--experiment-root", type=Path, default=EXPERIMENT_DEFAULT)
|
| 197 |
+
parser.add_argument("--selection", type=Path, default=SELECTION_DEFAULT)
|
| 198 |
+
parser.add_argument("--reference-root", type=Path, default=REFERENCE_DEFAULT)
|
| 199 |
+
parser.add_argument("--output-root", type=Path, default=OUTPUT_DEFAULT)
|
| 200 |
+
parser.add_argument("--global-index", action="append", type=int, default=None)
|
| 201 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 202 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 203 |
+
return parser.parse_args()
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def main() -> None:
|
| 207 |
+
args = parse_args()
|
| 208 |
+
mapping = extended.read_mapping(args.mapping.resolve())
|
| 209 |
+
configs = load_strategy_configs(args.selection.resolve())
|
| 210 |
+
strategy = configs[args.strategy]
|
| 211 |
+
reference_root = args.reference_root.resolve()
|
| 212 |
+
if len(list((reference_root / "generation_metrics/per_prompt/ffff").glob("global_*.json"))) != 251:
|
| 213 |
+
raise ValueError("Incomplete FFFF reference records")
|
| 214 |
+
if len(list((reference_root / "generated_videos/ffff").rglob("*.mp4"))) != 251:
|
| 215 |
+
raise ValueError("Incomplete FFFF reference videos")
|
| 216 |
+
if args.global_index is not None:
|
| 217 |
+
requested = set(args.global_index)
|
| 218 |
+
mapping = [row for row in mapping if int(row["global_index"]) in requested]
|
| 219 |
+
if len(mapping) != len(requested):
|
| 220 |
+
raise ValueError("At least one requested global index is unknown")
|
| 221 |
+
|
| 222 |
+
output_root = args.output_root.resolve()
|
| 223 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 224 |
+
extended.write_shard_manifest(
|
| 225 |
+
output_root, str(args.gpu), 0, 1, mapping, [strategy], "running"
|
| 226 |
+
)
|
| 227 |
+
print(
|
| 228 |
+
f"[setup] physical_gpu={args.gpu} strategy={args.strategy} "
|
| 229 |
+
f"prompts={len(mapping)} reference=existing_ffff chunk0=ffff",
|
| 230 |
+
flush=True,
|
| 231 |
+
)
|
| 232 |
+
device = torch.device("cuda")
|
| 233 |
+
torch.set_grad_enabled(False)
|
| 234 |
+
models = extended.load_models(args.experiment_root.resolve(), device)
|
| 235 |
+
completed = 0
|
| 236 |
+
try:
|
| 237 |
+
for row in mapping:
|
| 238 |
+
video_path, record_path = artifact_paths(output_root, row, args.strategy)
|
| 239 |
+
if not args.overwrite and video_path.is_file() and record_path.is_file():
|
| 240 |
+
completed += 1
|
| 241 |
+
print(f"[cached] {completed}/{len(mapping)} global={row['global_index']}", flush=True)
|
| 242 |
+
continue
|
| 243 |
+
run_prompt(
|
| 244 |
+
mapping_row=row,
|
| 245 |
+
strategy=strategy,
|
| 246 |
+
reference_root=reference_root,
|
| 247 |
+
output_root=output_root,
|
| 248 |
+
seed=args.seed,
|
| 249 |
+
models=models,
|
| 250 |
+
device=device,
|
| 251 |
+
)
|
| 252 |
+
completed += 1
|
| 253 |
+
print(f"[progress] {completed}/{len(mapping)} prompts", flush=True)
|
| 254 |
+
finally:
|
| 255 |
+
extended.write_shard_manifest(
|
| 256 |
+
output_root,
|
| 257 |
+
str(args.gpu),
|
| 258 |
+
0,
|
| 259 |
+
1,
|
| 260 |
+
mapping,
|
| 261 |
+
[strategy],
|
| 262 |
+
"complete" if completed == len(mapping) else "failed",
|
| 263 |
+
)
|
| 264 |
+
print(f"[complete] gpu={args.gpu} strategy={args.strategy} prompts={completed}", flush=True)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
if __name__ == "__main__":
|
| 268 |
+
main()
|
scripts/generate_vbench8_extended_strategies.py
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Generate all Layer-17 strategies for the VBench-8 extended subset.
|
| 3 |
+
|
| 4 |
+
One process handles a prompt shard and keeps the frozen Teacher, Layer-17
|
| 5 |
+
Predictor, both confidence heads, VAE, text encoder, and LPIPS model resident
|
| 6 |
+
on one GPU. For each prompt, FFFF is generated first and all other strategies
|
| 7 |
+
are compared with that same-prompt, same-seed reference before their videos
|
| 8 |
+
are written.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import argparse
|
| 14 |
+
import csv
|
| 15 |
+
import json
|
| 16 |
+
import math
|
| 17 |
+
import os
|
| 18 |
+
import sys
|
| 19 |
+
import tempfile
|
| 20 |
+
import time
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Any
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def preparse_gpu() -> str:
|
| 26 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 27 |
+
parser.add_argument("--gpu", default="0")
|
| 28 |
+
args, _ = parser.parse_known_args()
|
| 29 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 30 |
+
os.environ.setdefault("MPLCONFIGDIR", tempfile.mkdtemp(prefix="self_forcing_mpl_"))
|
| 31 |
+
return str(args.gpu)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
PHYSICAL_GPU = preparse_gpu()
|
| 35 |
+
|
| 36 |
+
import lpips
|
| 37 |
+
import torch
|
| 38 |
+
from omegaconf import OmegaConf
|
| 39 |
+
from safetensors.torch import load_file
|
| 40 |
+
|
| 41 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 42 |
+
if str(REPO_ROOT) not in sys.path:
|
| 43 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 44 |
+
|
| 45 |
+
from predictor_training.confidence import PredictorConfidenceHead
|
| 46 |
+
from scripts import evaluate_single_block_fppf as base
|
| 47 |
+
from scripts.evaluate_layer17_dynamic_gate import predictor_with_features
|
| 48 |
+
from scripts.vbench8_protocol import DIMENSIONS, SUITE_COUNTS
|
| 49 |
+
from utils.misc import set_seed
|
| 50 |
+
from utils.wan_wrapper import WanTextEncoder, WanVAEWrapper
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
NUM_CHUNKS = base.NUM_CHUNKS
|
| 54 |
+
FRAMES_PER_CHUNK = base.FRAMES_PER_CHUNK
|
| 55 |
+
NUM_DENOISING_STEPS = base.NUM_DENOISING_STEPS
|
| 56 |
+
|
| 57 |
+
MAPPING_DEFAULT = REPO_ROOT / "assets/vbench8_extended_subset_mapping.json"
|
| 58 |
+
EXPERIMENT_DEFAULT = REPO_ROOT / "confidence_experiments/layer17_stage1_step2000_20260831"
|
| 59 |
+
OUTPUT_DEFAULT = REPO_ROOT / "evaluation_runs/vbench8_extended_stage1_step2000_20260901"
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def atomic_json(path: Path, value: Any) -> None:
|
| 63 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 64 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 65 |
+
temporary.write_text(
|
| 66 |
+
json.dumps(value, ensure_ascii=False, indent=2, allow_nan=True) + "\n",
|
| 67 |
+
encoding="utf-8",
|
| 68 |
+
)
|
| 69 |
+
os.replace(temporary, path)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def atomic_video(frames: torch.Tensor, path: Path) -> None:
|
| 73 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 74 |
+
temporary = path.with_name(path.name + ".partial.mp4")
|
| 75 |
+
if temporary.exists():
|
| 76 |
+
temporary.unlink()
|
| 77 |
+
base.save_mp4(frames, temporary)
|
| 78 |
+
os.replace(temporary, path)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def read_mapping(path: Path) -> list[dict[str, Any]]:
|
| 82 |
+
value = json.loads(path.read_text(encoding="utf-8"))
|
| 83 |
+
if not isinstance(value, list):
|
| 84 |
+
raise ValueError(f"Mapping must be a JSON list: {path}")
|
| 85 |
+
if len(value) != 251:
|
| 86 |
+
raise ValueError(f"Expected 251 mapping rows, got {len(value)}")
|
| 87 |
+
counts = {suite: 0 for suite in SUITE_COUNTS}
|
| 88 |
+
seen_global: set[int] = set()
|
| 89 |
+
seen_suite: dict[str, set[int]] = {suite: set() for suite in SUITE_COUNTS}
|
| 90 |
+
for row in value:
|
| 91 |
+
suite = str(row["prompt_suite"])
|
| 92 |
+
global_index = int(row["global_index"])
|
| 93 |
+
suite_index = int(row["suite_index"])
|
| 94 |
+
if suite not in counts:
|
| 95 |
+
raise ValueError(f"Unknown prompt suite in mapping: {suite}")
|
| 96 |
+
if global_index in seen_global:
|
| 97 |
+
raise ValueError(f"Duplicate global index: {global_index}")
|
| 98 |
+
if suite_index in seen_suite[suite]:
|
| 99 |
+
raise ValueError(f"Duplicate suite index: {suite}/{suite_index}")
|
| 100 |
+
if not str(row["extended_prompt"]).strip():
|
| 101 |
+
raise ValueError(f"Empty extended prompt at global index {global_index}")
|
| 102 |
+
seen_global.add(global_index)
|
| 103 |
+
seen_suite[suite].add(suite_index)
|
| 104 |
+
counts[suite] += 1
|
| 105 |
+
if counts != SUITE_COUNTS:
|
| 106 |
+
raise ValueError(f"Unexpected mapping counts: {counts}")
|
| 107 |
+
for suite, expected in SUITE_COUNTS.items():
|
| 108 |
+
if seen_suite[suite] != set(range(expected)):
|
| 109 |
+
raise ValueError(f"Non-contiguous suite indices for {suite}")
|
| 110 |
+
return sorted(value, key=lambda row: int(row["global_index"]))
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def find_selected(path: Path, config_name: str) -> dict[str, Any]:
|
| 114 |
+
value = json.loads(path.read_text(encoding="utf-8"))
|
| 115 |
+
matches = [row for row in value["selected_dynamic"] if row["config_name"] == config_name]
|
| 116 |
+
if len(matches) != 1:
|
| 117 |
+
raise ValueError(f"Expected one {config_name} in {path}, got {len(matches)}")
|
| 118 |
+
return matches[0]
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def load_strategy_configs(experiment_root: Path) -> list[dict[str, Any]]:
|
| 122 |
+
step12_selected = experiment_root / "dynamic_step12/validation/selected.json"
|
| 123 |
+
step123_selected = experiment_root / "dynamic_step123/validation/selected.json"
|
| 124 |
+
configs: list[dict[str, Any]] = [
|
| 125 |
+
{
|
| 126 |
+
"name": "ffff",
|
| 127 |
+
"policy": "ffff",
|
| 128 |
+
"candidate_steps": [],
|
| 129 |
+
"beta": None,
|
| 130 |
+
"threshold": None,
|
| 131 |
+
"target_accepts": 0,
|
| 132 |
+
"head": None,
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"name": "fppf",
|
| 136 |
+
"policy": "fppf",
|
| 137 |
+
"candidate_steps": [1, 2],
|
| 138 |
+
"beta": None,
|
| 139 |
+
"threshold": None,
|
| 140 |
+
"target_accepts": 12,
|
| 141 |
+
"head": None,
|
| 142 |
+
},
|
| 143 |
+
]
|
| 144 |
+
for source, head, names in (
|
| 145 |
+
(step12_selected, "step12", ("dynamic_b1p0_k06", "dynamic_b2p0_k08", "dynamic_b0p0_k10")),
|
| 146 |
+
(step123_selected, "step123", ("dynamic_b1p0_k06", "dynamic_b2p0_k09", "dynamic_b1p5_k12", "dynamic_b2p0_k15")),
|
| 147 |
+
):
|
| 148 |
+
for source_name in names:
|
| 149 |
+
row = find_selected(source, source_name)
|
| 150 |
+
target = int(row["target_accepts"])
|
| 151 |
+
candidates = [1, 2] if head == "step12" else [1, 2, 3]
|
| 152 |
+
configs.append(
|
| 153 |
+
{
|
| 154 |
+
"name": f"{head}_k{target:02d}",
|
| 155 |
+
"policy": "dynamic",
|
| 156 |
+
"candidate_steps": candidates,
|
| 157 |
+
"beta": float(row["beta"]),
|
| 158 |
+
"threshold": float(row["threshold"]),
|
| 159 |
+
"target_accepts": target,
|
| 160 |
+
"head": head,
|
| 161 |
+
"source_config_name": source_name,
|
| 162 |
+
}
|
| 163 |
+
)
|
| 164 |
+
if [config["name"] for config in configs] != [
|
| 165 |
+
"ffff", "fppf", "step12_k06", "step12_k08", "step12_k10",
|
| 166 |
+
"step123_k06", "step123_k09", "step123_k12", "step123_k15",
|
| 167 |
+
]:
|
| 168 |
+
raise AssertionError("Unexpected strategy order")
|
| 169 |
+
return configs
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def reset_runtime_caches(pipeline: Any, device: torch.device) -> None:
|
| 173 |
+
if pipeline.kv_cache1 is None:
|
| 174 |
+
pipeline._initialize_kv_cache(1, torch.bfloat16, device)
|
| 175 |
+
pipeline._initialize_crossattn_cache(1, torch.bfloat16, device)
|
| 176 |
+
for cache in pipeline.kv_cache1:
|
| 177 |
+
cache["global_end_index"].zero_()
|
| 178 |
+
cache["local_end_index"].zero_()
|
| 179 |
+
for cache in pipeline.crossattn_cache:
|
| 180 |
+
cache["is_init"] = False
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
@torch.inference_mode()
|
| 184 |
+
def generate_rollout(
|
| 185 |
+
*,
|
| 186 |
+
pipeline: Any,
|
| 187 |
+
conditional_dict: dict[str, torch.Tensor],
|
| 188 |
+
seed: int,
|
| 189 |
+
device: torch.device,
|
| 190 |
+
predictor: Any,
|
| 191 |
+
head: Any,
|
| 192 |
+
config: dict[str, Any],
|
| 193 |
+
) -> tuple[torch.Tensor, dict[str, Any]]:
|
| 194 |
+
reset_runtime_caches(pipeline, device)
|
| 195 |
+
set_seed(seed)
|
| 196 |
+
noise = torch.randn(
|
| 197 |
+
1,
|
| 198 |
+
NUM_CHUNKS * FRAMES_PER_CHUNK,
|
| 199 |
+
base.LATENT_CHANNELS,
|
| 200 |
+
base.LATENT_HEIGHT,
|
| 201 |
+
base.LATENT_WIDTH,
|
| 202 |
+
dtype=torch.bfloat16,
|
| 203 |
+
device=device,
|
| 204 |
+
)
|
| 205 |
+
teacher = pipeline.generator.model
|
| 206 |
+
timesteps = pipeline.denoising_step_list.to(device=device)
|
| 207 |
+
capture = base.FinalHiddenCapture(teacher)
|
| 208 |
+
output_chunks: list[torch.Tensor] = []
|
| 209 |
+
previous_chunk_hidden: list[torch.Tensor | None] | None = None
|
| 210 |
+
decisions: list[dict[str, Any]] = []
|
| 211 |
+
full_calls = 0
|
| 212 |
+
predictor_calls = 0
|
| 213 |
+
accepted_predictor_calls = 0
|
| 214 |
+
timing_events: dict[str, list[tuple[torch.cuda.Event, torch.cuda.Event]]] = {
|
| 215 |
+
"full_dit": [],
|
| 216 |
+
"predictor": [],
|
| 217 |
+
"confidence": [],
|
| 218 |
+
"context_dit": [],
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
def start_timing() -> tuple[torch.cuda.Event, torch.cuda.Event]:
|
| 222 |
+
start_event = torch.cuda.Event(enable_timing=True)
|
| 223 |
+
end_event = torch.cuda.Event(enable_timing=True)
|
| 224 |
+
start_event.record()
|
| 225 |
+
return start_event, end_event
|
| 226 |
+
|
| 227 |
+
def finish_timing(category: str, events: tuple[torch.cuda.Event, torch.cuda.Event]) -> None:
|
| 228 |
+
events[1].record()
|
| 229 |
+
timing_events[category].append(events)
|
| 230 |
+
|
| 231 |
+
started = time.perf_counter()
|
| 232 |
+
try:
|
| 233 |
+
for chunk in range(NUM_CHUNKS):
|
| 234 |
+
noisy_input = noise[
|
| 235 |
+
:, chunk * FRAMES_PER_CHUNK : (chunk + 1) * FRAMES_PER_CHUNK
|
| 236 |
+
]
|
| 237 |
+
current_hidden: list[torch.Tensor | None] = [None] * NUM_DENOISING_STEPS
|
| 238 |
+
denoised_pred: torch.Tensor | None = None
|
| 239 |
+
timestep: torch.Tensor | None = None
|
| 240 |
+
for step, current_timestep in enumerate(timesteps):
|
| 241 |
+
timestep = torch.ones(
|
| 242 |
+
[1, FRAMES_PER_CHUNK], dtype=torch.int64, device=device
|
| 243 |
+
) * current_timestep
|
| 244 |
+
allow_chunk0_predictor = bool(
|
| 245 |
+
config.get("allow_chunk0_predictor", False)
|
| 246 |
+
)
|
| 247 |
+
candidate = (
|
| 248 |
+
step in config["candidate_steps"]
|
| 249 |
+
and (chunk > 0 or allow_chunk0_predictor)
|
| 250 |
+
)
|
| 251 |
+
policy = str(config["policy"])
|
| 252 |
+
run_predictor = candidate and policy in {"dynamic", "fppf"}
|
| 253 |
+
accepted = False
|
| 254 |
+
pred_hidden = None
|
| 255 |
+
pred_x0 = None
|
| 256 |
+
predicted_local_error = None
|
| 257 |
+
risk = None
|
| 258 |
+
alpha = None
|
| 259 |
+
outgoing_span = None
|
| 260 |
+
span_weight = None
|
| 261 |
+
if run_predictor:
|
| 262 |
+
anchor_hidden = current_hidden[step - 1]
|
| 263 |
+
if anchor_hidden is None:
|
| 264 |
+
raise RuntimeError("Predictor inputs are unavailable")
|
| 265 |
+
if previous_chunk_hidden is None:
|
| 266 |
+
if not (
|
| 267 |
+
chunk == 0
|
| 268 |
+
and allow_chunk0_predictor
|
| 269 |
+
and getattr(predictor, "input_variant", None) == "disca"
|
| 270 |
+
):
|
| 271 |
+
raise RuntimeError("Previous chunk hidden is unavailable")
|
| 272 |
+
# DISCA physically has no previous-chunk channel. Its
|
| 273 |
+
# forward signature retains this argument only for API
|
| 274 |
+
# compatibility, so an anchor-shaped placeholder is safe.
|
| 275 |
+
previous_hidden = anchor_hidden
|
| 276 |
+
else:
|
| 277 |
+
previous_hidden = previous_chunk_hidden[step]
|
| 278 |
+
if previous_hidden is None:
|
| 279 |
+
raise RuntimeError("Previous chunk hidden is unavailable")
|
| 280 |
+
predictor_events = start_timing()
|
| 281 |
+
pred_hidden, pred_flow, transformed = predictor_with_features(
|
| 282 |
+
predictor=predictor,
|
| 283 |
+
teacher=teacher,
|
| 284 |
+
noisy_input=noisy_input,
|
| 285 |
+
timestep=timestep,
|
| 286 |
+
anchor_hidden=anchor_hidden,
|
| 287 |
+
previous_hidden=previous_hidden,
|
| 288 |
+
history_cache=pipeline.kv_cache1[17],
|
| 289 |
+
cross_cache=pipeline.crossattn_cache[17],
|
| 290 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 291 |
+
anchor_timestep=(
|
| 292 |
+
torch.ones_like(timestep) * timesteps[step - 1]
|
| 293 |
+
),
|
| 294 |
+
)
|
| 295 |
+
finish_timing("predictor", predictor_events)
|
| 296 |
+
pred_x0 = pipeline.generator._convert_flow_pred_to_x0(
|
| 297 |
+
flow_pred=pred_flow.flatten(0, 1),
|
| 298 |
+
xt=noisy_input.flatten(0, 1),
|
| 299 |
+
timestep=timestep.flatten(0, 1),
|
| 300 |
+
).unflatten(0, pred_flow.shape[:2])
|
| 301 |
+
predictor_calls += 1
|
| 302 |
+
if policy == "fppf":
|
| 303 |
+
accepted = True
|
| 304 |
+
else:
|
| 305 |
+
chunk_position = torch.tensor(
|
| 306 |
+
[(chunk - 1) / 5.0], device=device
|
| 307 |
+
)
|
| 308 |
+
step_tensor = torch.tensor([step], dtype=torch.long, device=device)
|
| 309 |
+
confidence_events = start_timing()
|
| 310 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 311 |
+
predicted_log = head(
|
| 312 |
+
transformed_hidden=transformed,
|
| 313 |
+
pred_hidden=pred_hidden,
|
| 314 |
+
anchor_hidden=anchor_hidden,
|
| 315 |
+
chunk_position=chunk_position,
|
| 316 |
+
step_id=step_tensor,
|
| 317 |
+
)
|
| 318 |
+
finish_timing("confidence", confidence_events)
|
| 319 |
+
predicted_local_error = float(predicted_log.exp()[0])
|
| 320 |
+
alpha = (NUM_CHUNKS - 1 - chunk) / (NUM_CHUNKS - 2)
|
| 321 |
+
next_policy_timestep = (
|
| 322 |
+
timesteps[step + 1]
|
| 323 |
+
if step + 1 < len(timesteps)
|
| 324 |
+
else timesteps.new_zeros(())
|
| 325 |
+
)
|
| 326 |
+
outgoing_span = abs(
|
| 327 |
+
float(current_timestep) - float(next_policy_timestep)
|
| 328 |
+
)
|
| 329 |
+
risk_mode = str(config.get("risk_mode", "legacy"))
|
| 330 |
+
if risk_mode == "legacy":
|
| 331 |
+
span_weight = 1.0
|
| 332 |
+
elif risk_mode == "outgoing_span":
|
| 333 |
+
span_weight = outgoing_span / 1000.0
|
| 334 |
+
else:
|
| 335 |
+
raise ValueError(f"Unknown risk mode: {risk_mode}")
|
| 336 |
+
risk = predicted_local_error * span_weight * (
|
| 337 |
+
1.0 + float(config["beta"]) * alpha
|
| 338 |
+
)
|
| 339 |
+
accepted = risk <= float(config["threshold"])
|
| 340 |
+
|
| 341 |
+
if accepted:
|
| 342 |
+
assert pred_hidden is not None and pred_x0 is not None
|
| 343 |
+
current_hidden[step] = pred_hidden
|
| 344 |
+
denoised_pred = pred_x0
|
| 345 |
+
accepted_predictor_calls += 1
|
| 346 |
+
else:
|
| 347 |
+
full_events = start_timing()
|
| 348 |
+
capture.start()
|
| 349 |
+
_, denoised_pred = pipeline.generator(
|
| 350 |
+
noisy_image_or_video=noisy_input,
|
| 351 |
+
conditional_dict=conditional_dict,
|
| 352 |
+
timestep=timestep,
|
| 353 |
+
kv_cache=pipeline.kv_cache1,
|
| 354 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 355 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 356 |
+
)
|
| 357 |
+
current_hidden[step] = capture.finish()
|
| 358 |
+
finish_timing("full_dit", full_events)
|
| 359 |
+
full_calls += 1
|
| 360 |
+
|
| 361 |
+
if candidate:
|
| 362 |
+
decisions.append(
|
| 363 |
+
{
|
| 364 |
+
"chunk": chunk,
|
| 365 |
+
"step": step,
|
| 366 |
+
"ran_predictor": run_predictor,
|
| 367 |
+
"accepted": accepted,
|
| 368 |
+
"predicted_local_error": predicted_local_error,
|
| 369 |
+
"chunk_alpha": alpha,
|
| 370 |
+
"current_timestep": float(current_timestep),
|
| 371 |
+
"next_policy_timestep": (
|
| 372 |
+
float(timesteps[step + 1])
|
| 373 |
+
if step + 1 < len(timesteps)
|
| 374 |
+
else 0.0
|
| 375 |
+
),
|
| 376 |
+
"outgoing_span": outgoing_span,
|
| 377 |
+
"span_weight": span_weight,
|
| 378 |
+
"impact_risk": risk,
|
| 379 |
+
}
|
| 380 |
+
)
|
| 381 |
+
if step < NUM_DENOISING_STEPS - 1:
|
| 382 |
+
assert denoised_pred is not None
|
| 383 |
+
next_timestep = timesteps[step + 1]
|
| 384 |
+
flat = denoised_pred.flatten(0, 1)
|
| 385 |
+
noisy_input = pipeline.scheduler.add_noise(
|
| 386 |
+
flat,
|
| 387 |
+
torch.randn_like(flat),
|
| 388 |
+
next_timestep * torch.ones(
|
| 389 |
+
[FRAMES_PER_CHUNK], dtype=torch.long, device=device
|
| 390 |
+
),
|
| 391 |
+
).unflatten(0, denoised_pred.shape[:2])
|
| 392 |
+
|
| 393 |
+
assert denoised_pred is not None and timestep is not None
|
| 394 |
+
output_chunks.append(denoised_pred)
|
| 395 |
+
context_events = start_timing()
|
| 396 |
+
pipeline.generator(
|
| 397 |
+
noisy_image_or_video=denoised_pred,
|
| 398 |
+
conditional_dict=conditional_dict,
|
| 399 |
+
timestep=torch.ones_like(timestep) * pipeline.args.context_noise,
|
| 400 |
+
kv_cache=pipeline.kv_cache1,
|
| 401 |
+
crossattn_cache=pipeline.crossattn_cache,
|
| 402 |
+
current_start=chunk * base.TOKENS_PER_CHUNK,
|
| 403 |
+
)
|
| 404 |
+
finish_timing("context_dit", context_events)
|
| 405 |
+
previous_chunk_hidden = current_hidden
|
| 406 |
+
finally:
|
| 407 |
+
capture.close()
|
| 408 |
+
|
| 409 |
+
torch.cuda.synchronize()
|
| 410 |
+
elapsed = {
|
| 411 |
+
category: sum(start.elapsed_time(end) for start, end in events)
|
| 412 |
+
for category, events in timing_events.items()
|
| 413 |
+
}
|
| 414 |
+
actual_dit_time_ms = (
|
| 415 |
+
elapsed["full_dit"] + elapsed["predictor"] + elapsed["context_dit"]
|
| 416 |
+
)
|
| 417 |
+
policy_latency_ms = (
|
| 418 |
+
elapsed["full_dit"] + elapsed["predictor"] + elapsed["confidence"]
|
| 419 |
+
)
|
| 420 |
+
return torch.cat(output_chunks, dim=1), {
|
| 421 |
+
"generation_time_s": time.perf_counter() - started,
|
| 422 |
+
"full_calls": full_calls,
|
| 423 |
+
"predictor_calls": predictor_calls,
|
| 424 |
+
"accepted_predictor_calls": accepted_predictor_calls,
|
| 425 |
+
"rejected_predictor_calls": predictor_calls - accepted_predictor_calls,
|
| 426 |
+
"full_dit_time_ms": elapsed["full_dit"],
|
| 427 |
+
"predictor_time_ms": elapsed["predictor"],
|
| 428 |
+
"confidence_head_time_ms": elapsed["confidence"],
|
| 429 |
+
"context_dit_time_ms": elapsed["context_dit"],
|
| 430 |
+
"actual_dit_time_ms": actual_dit_time_ms,
|
| 431 |
+
"model_path_time_ms": actual_dit_time_ms + elapsed["confidence"],
|
| 432 |
+
"policy_latency_ms": policy_latency_ms,
|
| 433 |
+
"decisions": decisions,
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
def compact_metrics(value: dict[str, Any]) -> dict[str, Any]:
|
| 438 |
+
keys = (
|
| 439 |
+
"pixel_mse", "psnr", "psnr_frame_mean", "ssim", "lpips",
|
| 440 |
+
"rollout_start_frame", "rollout_pixel_mse", "rollout_psnr",
|
| 441 |
+
"rollout_ssim", "rollout_lpips", "num_frames",
|
| 442 |
+
)
|
| 443 |
+
return {key: value[key] for key in keys}
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
def identity_metrics(num_frames: int) -> dict[str, Any]:
|
| 447 |
+
return {
|
| 448 |
+
"pixel_mse": 0.0,
|
| 449 |
+
"psnr": 120.0,
|
| 450 |
+
"psnr_frame_mean": 120.0,
|
| 451 |
+
"ssim": 1.0,
|
| 452 |
+
"lpips": 0.0,
|
| 453 |
+
"rollout_start_frame": base.PIXEL_FRAMES_FIRST_CHUNK,
|
| 454 |
+
"rollout_pixel_mse": 0.0,
|
| 455 |
+
"rollout_psnr": 120.0,
|
| 456 |
+
"rollout_ssim": 1.0,
|
| 457 |
+
"rollout_lpips": 0.0,
|
| 458 |
+
"num_frames": num_frames,
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
def load_models(
|
| 463 |
+
experiment_root: Path, device: torch.device
|
| 464 |
+
) -> tuple[Any, Any, Any, Any, Any, Any, Any]:
|
| 465 |
+
print("[setup] loading VAE", flush=True)
|
| 466 |
+
vae = WanVAEWrapper().to(device=device, dtype=torch.bfloat16).eval()
|
| 467 |
+
config = OmegaConf.merge(
|
| 468 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 469 |
+
OmegaConf.load(REPO_ROOT / "configs/self_forcing_sid.yaml"),
|
| 470 |
+
)
|
| 471 |
+
print("[setup] loading frozen Teacher and Layer-17 Predictor", flush=True)
|
| 472 |
+
pipeline = base.build_pipeline(
|
| 473 |
+
config, REPO_ROOT / "checkpoints/self_forcing_dmd.pt", vae, device
|
| 474 |
+
)
|
| 475 |
+
predictor = base.load_predictor(
|
| 476 |
+
pipeline.generator.model,
|
| 477 |
+
{
|
| 478 |
+
"source_layer": 17,
|
| 479 |
+
"weights": REPO_ROOT
|
| 480 |
+
/ "training_runs/layer17_stage1_1000p_4gpu_b16_2000steps/checkpoint_step_2000/predictor.safetensors",
|
| 481 |
+
"gate_mode": "baseline",
|
| 482 |
+
},
|
| 483 |
+
device,
|
| 484 |
+
)
|
| 485 |
+
print("[setup] loading text encoder and confidence heads", flush=True)
|
| 486 |
+
text_encoder = WanTextEncoder().to(device=device, dtype=torch.bfloat16).eval()
|
| 487 |
+
text_encoder.requires_grad_(False)
|
| 488 |
+
step12_head = PredictorConfidenceHead(num_steps=2).to(device=device).eval()
|
| 489 |
+
step12_head.load_state_dict(
|
| 490 |
+
load_file(
|
| 491 |
+
str(experiment_root / "confidence_step12/confidence_best.safetensors"),
|
| 492 |
+
device="cpu",
|
| 493 |
+
),
|
| 494 |
+
strict=True,
|
| 495 |
+
)
|
| 496 |
+
step12_head.requires_grad_(False)
|
| 497 |
+
step123_head = PredictorConfidenceHead(num_steps=3).to(device=device).eval()
|
| 498 |
+
step123_head.load_state_dict(
|
| 499 |
+
load_file(
|
| 500 |
+
str(experiment_root / "confidence_step123/confidence_best.safetensors"),
|
| 501 |
+
device="cpu",
|
| 502 |
+
),
|
| 503 |
+
strict=True,
|
| 504 |
+
)
|
| 505 |
+
step123_head.requires_grad_(False)
|
| 506 |
+
print("[setup] loading LPIPS", flush=True)
|
| 507 |
+
lpips_model = lpips.LPIPS(net="alex", verbose=False).to(device).eval()
|
| 508 |
+
lpips_model.requires_grad_(False)
|
| 509 |
+
return (
|
| 510 |
+
vae,
|
| 511 |
+
pipeline,
|
| 512 |
+
predictor,
|
| 513 |
+
text_encoder,
|
| 514 |
+
step12_head,
|
| 515 |
+
step123_head,
|
| 516 |
+
lpips_model,
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
def is_prompt_complete(output_root: Path, mapping_row: dict[str, Any], strategies: list[dict[str, Any]]) -> bool:
|
| 521 |
+
global_index = int(mapping_row["global_index"])
|
| 522 |
+
suite = str(mapping_row["prompt_suite"])
|
| 523 |
+
suite_index = int(mapping_row["suite_index"])
|
| 524 |
+
for strategy in strategies:
|
| 525 |
+
name = strategy["name"]
|
| 526 |
+
video = output_root / "generated_videos" / name / suite / f"{suite_index:03d}.mp4"
|
| 527 |
+
record = output_root / "generation_metrics/per_prompt" / name / f"global_{global_index:04d}.json"
|
| 528 |
+
if not video.is_file() or not record.is_file():
|
| 529 |
+
return False
|
| 530 |
+
return True
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
def run_prompt(
|
| 534 |
+
*,
|
| 535 |
+
mapping_row: dict[str, Any],
|
| 536 |
+
strategies: list[dict[str, Any]],
|
| 537 |
+
output_root: Path,
|
| 538 |
+
seed: int,
|
| 539 |
+
vae: Any,
|
| 540 |
+
pipeline: Any,
|
| 541 |
+
predictor: Any,
|
| 542 |
+
text_encoder: Any,
|
| 543 |
+
step12_head: Any,
|
| 544 |
+
step123_head: Any,
|
| 545 |
+
lpips_model: Any,
|
| 546 |
+
device: torch.device,
|
| 547 |
+
) -> None:
|
| 548 |
+
global_index = int(mapping_row["global_index"])
|
| 549 |
+
suite = str(mapping_row["prompt_suite"])
|
| 550 |
+
suite_index = int(mapping_row["suite_index"])
|
| 551 |
+
prompt = str(mapping_row["extended_prompt"])
|
| 552 |
+
print(f"[prompt] global={global_index} suite={suite}/{suite_index}", flush=True)
|
| 553 |
+
conditional = text_encoder(text_prompts=[prompt])
|
| 554 |
+
ffff_config = strategies[0]
|
| 555 |
+
reference_latent, ffff_diagnostic = generate_rollout(
|
| 556 |
+
pipeline=pipeline,
|
| 557 |
+
conditional_dict=conditional,
|
| 558 |
+
seed=seed,
|
| 559 |
+
device=device,
|
| 560 |
+
predictor=predictor,
|
| 561 |
+
head=None,
|
| 562 |
+
config=ffff_config,
|
| 563 |
+
)
|
| 564 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 565 |
+
reference_pixels = vae.decode_to_pixel(reference_latent, use_cache=False)
|
| 566 |
+
reference_u8 = base.pixels_to_u8(reference_pixels)
|
| 567 |
+
if reference_u8.ndim != 4 or reference_u8.shape[0] != 81:
|
| 568 |
+
raise RuntimeError(f"Unexpected decoded reference shape: {tuple(reference_u8.shape)}")
|
| 569 |
+
reference_path = (
|
| 570 |
+
output_root / "generated_videos" / "ffff" / suite / f"{suite_index:03d}.mp4"
|
| 571 |
+
)
|
| 572 |
+
atomic_video(reference_u8, reference_path)
|
| 573 |
+
reference_record = {
|
| 574 |
+
"status": "complete",
|
| 575 |
+
"strategy": "ffff",
|
| 576 |
+
"policy": "ffff",
|
| 577 |
+
"global_index": global_index,
|
| 578 |
+
"prompt_suite": suite,
|
| 579 |
+
"suite_index": suite_index,
|
| 580 |
+
"prompt": prompt,
|
| 581 |
+
"seed": seed,
|
| 582 |
+
"generation": {key: value for key, value in ffff_diagnostic.items() if key != "decisions"},
|
| 583 |
+
"pixel_metrics_vs_ffff": identity_metrics(int(reference_u8.shape[0])),
|
| 584 |
+
"video": str(reference_path.relative_to(output_root)),
|
| 585 |
+
}
|
| 586 |
+
atomic_json(
|
| 587 |
+
output_root
|
| 588 |
+
/ "generation_metrics/per_prompt/ffff"
|
| 589 |
+
/ f"global_{global_index:04d}.json",
|
| 590 |
+
reference_record,
|
| 591 |
+
)
|
| 592 |
+
|
| 593 |
+
for strategy in strategies[1:]:
|
| 594 |
+
name = str(strategy["name"])
|
| 595 |
+
head = step12_head if strategy["head"] == "step12" else step123_head
|
| 596 |
+
latent, diagnostic = generate_rollout(
|
| 597 |
+
pipeline=pipeline,
|
| 598 |
+
conditional_dict=conditional,
|
| 599 |
+
seed=seed,
|
| 600 |
+
device=device,
|
| 601 |
+
predictor=predictor,
|
| 602 |
+
head=head,
|
| 603 |
+
config=strategy,
|
| 604 |
+
)
|
| 605 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 606 |
+
pixels = vae.decode_to_pixel(latent, use_cache=False)
|
| 607 |
+
prediction_u8 = base.pixels_to_u8(pixels)
|
| 608 |
+
metrics = base.frame_metrics(
|
| 609 |
+
reference_u8=reference_u8,
|
| 610 |
+
prediction_u8=prediction_u8,
|
| 611 |
+
lpips_model=lpips_model,
|
| 612 |
+
batch_size=4,
|
| 613 |
+
device=device,
|
| 614 |
+
)
|
| 615 |
+
video_path = output_root / "generated_videos" / name / suite / f"{suite_index:03d}.mp4"
|
| 616 |
+
atomic_video(prediction_u8, video_path)
|
| 617 |
+
record = {
|
| 618 |
+
"status": "complete",
|
| 619 |
+
"strategy": name,
|
| 620 |
+
"policy": strategy["policy"],
|
| 621 |
+
"candidate_steps": strategy["candidate_steps"],
|
| 622 |
+
"beta": strategy["beta"],
|
| 623 |
+
"threshold": strategy["threshold"],
|
| 624 |
+
"target_accepts": strategy["target_accepts"],
|
| 625 |
+
"source_config_name": strategy.get("source_config_name"),
|
| 626 |
+
"global_index": global_index,
|
| 627 |
+
"prompt_suite": suite,
|
| 628 |
+
"suite_index": suite_index,
|
| 629 |
+
"prompt": prompt,
|
| 630 |
+
"seed": seed,
|
| 631 |
+
"generation": {key: value for key, value in diagnostic.items() if key != "decisions"},
|
| 632 |
+
"decisions": diagnostic["decisions"],
|
| 633 |
+
"pixel_metrics_vs_ffff": compact_metrics(metrics),
|
| 634 |
+
"video": str(video_path.relative_to(output_root)),
|
| 635 |
+
}
|
| 636 |
+
atomic_json(
|
| 637 |
+
output_root
|
| 638 |
+
/ "generation_metrics/per_prompt"
|
| 639 |
+
/ name
|
| 640 |
+
/ f"global_{global_index:04d}.json",
|
| 641 |
+
record,
|
| 642 |
+
)
|
| 643 |
+
print(
|
| 644 |
+
f"[result] global={global_index} strategy={name} "
|
| 645 |
+
f"accept={diagnostic['accepted_predictor_calls']} "
|
| 646 |
+
f"psnr={metrics['psnr']:.4f} ssim={metrics['ssim']:.6f} "
|
| 647 |
+
f"lpips={metrics['lpips']:.6f}",
|
| 648 |
+
flush=True,
|
| 649 |
+
)
|
| 650 |
+
if hasattr(vae.model, "clear_cache"):
|
| 651 |
+
vae.model.clear_cache()
|
| 652 |
+
del latent, pixels, prediction_u8, metrics
|
| 653 |
+
torch.cuda.empty_cache()
|
| 654 |
+
|
| 655 |
+
if hasattr(vae.model, "clear_cache"):
|
| 656 |
+
vae.model.clear_cache()
|
| 657 |
+
del conditional, reference_latent, reference_pixels, reference_u8
|
| 658 |
+
torch.cuda.empty_cache()
|
| 659 |
+
|
| 660 |
+
|
| 661 |
+
def write_shard_manifest(
|
| 662 |
+
output_root: Path,
|
| 663 |
+
gpu: str,
|
| 664 |
+
shard_index: int,
|
| 665 |
+
num_shards: int,
|
| 666 |
+
rows: list[dict[str, Any]],
|
| 667 |
+
strategies: list[dict[str, Any]],
|
| 668 |
+
status: str,
|
| 669 |
+
) -> None:
|
| 670 |
+
atomic_json(
|
| 671 |
+
output_root / "generation_metrics" / f"manifest_gpu{gpu}.json",
|
| 672 |
+
{
|
| 673 |
+
"status": status,
|
| 674 |
+
"physical_gpu": gpu,
|
| 675 |
+
"shard_index": shard_index,
|
| 676 |
+
"num_shards": num_shards,
|
| 677 |
+
"global_indices": [int(row["global_index"]) for row in rows],
|
| 678 |
+
"num_prompts": len(rows),
|
| 679 |
+
"strategies": [strategy["name"] for strategy in strategies],
|
| 680 |
+
"seed": 0,
|
| 681 |
+
},
|
| 682 |
+
)
|
| 683 |
+
|
| 684 |
+
|
| 685 |
+
def parse_args() -> argparse.Namespace:
|
| 686 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 687 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 688 |
+
parser.add_argument("--mapping", type=Path, default=MAPPING_DEFAULT)
|
| 689 |
+
parser.add_argument("--experiment-root", type=Path, default=EXPERIMENT_DEFAULT)
|
| 690 |
+
parser.add_argument("--output-root", type=Path, default=OUTPUT_DEFAULT)
|
| 691 |
+
parser.add_argument("--shard-index", type=int, default=0)
|
| 692 |
+
parser.add_argument("--num-shards", type=int, default=1)
|
| 693 |
+
parser.add_argument(
|
| 694 |
+
"--global-index",
|
| 695 |
+
dest="global_indices",
|
| 696 |
+
action="append",
|
| 697 |
+
type=int,
|
| 698 |
+
default=None,
|
| 699 |
+
help="Process only the specified global index; may be repeated.",
|
| 700 |
+
)
|
| 701 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 702 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 703 |
+
return parser.parse_args()
|
| 704 |
+
|
| 705 |
+
|
| 706 |
+
def main() -> None:
|
| 707 |
+
args = parse_args()
|
| 708 |
+
if args.num_shards < 1 or not 0 <= args.shard_index < args.num_shards:
|
| 709 |
+
raise ValueError("Invalid shard index/number of shards")
|
| 710 |
+
mapping = read_mapping(args.mapping.resolve())
|
| 711 |
+
strategies = load_strategy_configs(args.experiment_root.resolve())
|
| 712 |
+
if args.global_indices is None:
|
| 713 |
+
rows = [
|
| 714 |
+
row for position, row in enumerate(mapping)
|
| 715 |
+
if position % args.num_shards == args.shard_index
|
| 716 |
+
]
|
| 717 |
+
else:
|
| 718 |
+
requested = set(args.global_indices)
|
| 719 |
+
mapping_by_global = {int(row["global_index"]): row for row in mapping}
|
| 720 |
+
unknown = sorted(requested - set(mapping_by_global))
|
| 721 |
+
if unknown:
|
| 722 |
+
raise ValueError(f"Unknown global indices: {unknown}")
|
| 723 |
+
rows = [row for row in mapping if int(row["global_index"]) in requested]
|
| 724 |
+
output_root = args.output_root.resolve()
|
| 725 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 726 |
+
write_shard_manifest(
|
| 727 |
+
output_root, str(args.gpu), args.shard_index, args.num_shards,
|
| 728 |
+
rows, strategies, "running",
|
| 729 |
+
)
|
| 730 |
+
print(
|
| 731 |
+
f"[setup] physical_gpu={args.gpu} shard={args.shard_index}/{args.num_shards} "
|
| 732 |
+
f"prompts={len(rows)} strategies={len(strategies)}",
|
| 733 |
+
flush=True,
|
| 734 |
+
)
|
| 735 |
+
device = torch.device("cuda")
|
| 736 |
+
torch.set_grad_enabled(False)
|
| 737 |
+
(
|
| 738 |
+
vae,
|
| 739 |
+
pipeline,
|
| 740 |
+
predictor,
|
| 741 |
+
text_encoder,
|
| 742 |
+
step12_head,
|
| 743 |
+
step123_head,
|
| 744 |
+
lpips_model,
|
| 745 |
+
) = load_models(args.experiment_root.resolve(), device)
|
| 746 |
+
completed = 0
|
| 747 |
+
try:
|
| 748 |
+
for row in rows:
|
| 749 |
+
if not args.overwrite and is_prompt_complete(output_root, row, strategies):
|
| 750 |
+
completed += 1
|
| 751 |
+
print(
|
| 752 |
+
f"[cached] {completed}/{len(rows)} global={row['global_index']}",
|
| 753 |
+
flush=True,
|
| 754 |
+
)
|
| 755 |
+
continue
|
| 756 |
+
run_prompt(
|
| 757 |
+
mapping_row=row,
|
| 758 |
+
strategies=strategies,
|
| 759 |
+
output_root=output_root,
|
| 760 |
+
seed=args.seed,
|
| 761 |
+
vae=vae,
|
| 762 |
+
pipeline=pipeline,
|
| 763 |
+
predictor=predictor,
|
| 764 |
+
text_encoder=text_encoder,
|
| 765 |
+
step12_head=step12_head,
|
| 766 |
+
step123_head=step123_head,
|
| 767 |
+
lpips_model=lpips_model,
|
| 768 |
+
device=device,
|
| 769 |
+
)
|
| 770 |
+
completed += 1
|
| 771 |
+
print(f"[progress] {completed}/{len(rows)} prompts", flush=True)
|
| 772 |
+
finally:
|
| 773 |
+
write_shard_manifest(
|
| 774 |
+
output_root, str(args.gpu), args.shard_index, args.num_shards,
|
| 775 |
+
rows, strategies, "complete" if completed == len(rows) else "failed",
|
| 776 |
+
)
|
| 777 |
+
print(f"[complete] gpu={args.gpu} prompts={completed}/{len(rows)}", flush=True)
|
| 778 |
+
|
| 779 |
+
|
| 780 |
+
if __name__ == "__main__":
|
| 781 |
+
main()
|
scripts/merge_three_block_sweep_shards.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Merge four independent consecutive three-block training sweeps."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import shutil
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
EXPECTED_TRIPLES = [(index, index + 1, index + 2) for index in range(28)]
|
| 16 |
+
EXPECTED_PROMPTS = {"train": list(range(80)), "val": list(range(80, 100))}
|
| 17 |
+
GPU_SHARDS = {
|
| 18 |
+
"0": EXPECTED_TRIPLES[0:7],
|
| 19 |
+
"2": EXPECTED_TRIPLES[7:14],
|
| 20 |
+
"6": EXPECTED_TRIPLES[14:21],
|
| 21 |
+
"7": EXPECTED_TRIPLES[21:28],
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def parse_args() -> argparse.Namespace:
|
| 26 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 27 |
+
parser.add_argument("--shard_root", type=Path, required=True)
|
| 28 |
+
parser.add_argument("--output_dir", type=Path, required=True)
|
| 29 |
+
return parser.parse_args()
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def experiment_name(triple: tuple[int, int, int]) -> str:
|
| 33 |
+
return "triple_" + "_".join(f"{layer:02d}" for layer in triple)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def atomic_json(path: Path, value: Any) -> None:
|
| 37 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 38 |
+
temporary.write_text(
|
| 39 |
+
json.dumps(value, indent=2, ensure_ascii=False) + "\n",
|
| 40 |
+
encoding="utf-8",
|
| 41 |
+
)
|
| 42 |
+
os.replace(temporary, path)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def validate_metrics(
|
| 46 |
+
metrics: dict[str, Any], triple: tuple[int, int, int]
|
| 47 |
+
) -> None:
|
| 48 |
+
name = experiment_name(triple)
|
| 49 |
+
expected = {
|
| 50 |
+
"status": "complete",
|
| 51 |
+
"name": name,
|
| 52 |
+
"architecture": "three_block_predictor",
|
| 53 |
+
"initialization_method": "teacher_full",
|
| 54 |
+
"source_layers": list(triple),
|
| 55 |
+
"seed": 0,
|
| 56 |
+
"max_steps": 1000,
|
| 57 |
+
"train_prompt_ids": EXPECTED_PROMPTS["train"],
|
| 58 |
+
"val_prompt_ids": EXPECTED_PROMPTS["val"],
|
| 59 |
+
}
|
| 60 |
+
for key, value in expected.items():
|
| 61 |
+
if metrics.get(key) != value:
|
| 62 |
+
raise RuntimeError(
|
| 63 |
+
f"{name}: expected {key}={value!r}, got {metrics.get(key)!r}"
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def result_row(metrics: dict[str, Any]) -> dict[str, Any]:
|
| 68 |
+
return {
|
| 69 |
+
"name": metrics["name"],
|
| 70 |
+
"source_layer_1": metrics["source_layers"][0],
|
| 71 |
+
"source_layer_2": metrics["source_layers"][1],
|
| 72 |
+
"source_layer_3": metrics["source_layers"][2],
|
| 73 |
+
"final_val_flow_mse": metrics["final_val_flow_mse"],
|
| 74 |
+
"final_val_hidden_mse": metrics["final_val_hidden_mse"],
|
| 75 |
+
"final_val_total_loss": metrics["final_val_total_loss"],
|
| 76 |
+
"val_flow_mse_auc": metrics["val_flow_mse_auc"],
|
| 77 |
+
"val_hidden_mse_auc": metrics["val_hidden_mse_auc"],
|
| 78 |
+
"val_total_loss_auc": metrics["val_total_loss_auc"],
|
| 79 |
+
"training_time_s": metrics["training_time_s"],
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def write_summary(output_dir: Path, rows: list[dict[str, Any]]) -> None:
|
| 84 |
+
rows.sort(key=lambda row: float(row["final_val_flow_mse"]))
|
| 85 |
+
destination = output_dir / "summary.csv"
|
| 86 |
+
temporary = destination.with_suffix(".csv.tmp")
|
| 87 |
+
with temporary.open("w", encoding="utf-8", newline="") as handle:
|
| 88 |
+
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
|
| 89 |
+
writer.writeheader()
|
| 90 |
+
writer.writerows(rows)
|
| 91 |
+
os.replace(temporary, destination)
|
| 92 |
+
atomic_json(output_dir / "summary.json", rows)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def main() -> None:
|
| 96 |
+
args = parse_args()
|
| 97 |
+
shard_root = args.shard_root.resolve()
|
| 98 |
+
output_dir = args.output_dir.resolve()
|
| 99 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 100 |
+
|
| 101 |
+
rows = []
|
| 102 |
+
fingerprints = set()
|
| 103 |
+
for gpu, triples in GPU_SHARDS.items():
|
| 104 |
+
shard_dir = shard_root / f"gpu{gpu}"
|
| 105 |
+
shard_manifest = json.loads(
|
| 106 |
+
(shard_dir / "sweep_manifest.json").read_text(encoding="utf-8")
|
| 107 |
+
)
|
| 108 |
+
if shard_manifest.get("status") != "complete":
|
| 109 |
+
raise RuntimeError(f"GPU {gpu} shard is not complete")
|
| 110 |
+
if shard_manifest.get("triples") != [list(value) for value in triples]:
|
| 111 |
+
raise RuntimeError(f"GPU {gpu} shard contains unexpected triples")
|
| 112 |
+
fingerprints.add(shard_manifest["batch_schedule_sha256"])
|
| 113 |
+
|
| 114 |
+
for triple in triples:
|
| 115 |
+
name = experiment_name(triple)
|
| 116 |
+
source = shard_dir / name
|
| 117 |
+
metrics = json.loads(
|
| 118 |
+
(source / "metrics.json").read_text(encoding="utf-8")
|
| 119 |
+
)
|
| 120 |
+
validate_metrics(metrics, triple)
|
| 121 |
+
destination = output_dir / name
|
| 122 |
+
if destination.exists():
|
| 123 |
+
shutil.rmtree(destination)
|
| 124 |
+
shutil.copytree(source, destination)
|
| 125 |
+
rows.append(result_row(metrics))
|
| 126 |
+
|
| 127 |
+
if len(rows) != 28:
|
| 128 |
+
raise RuntimeError(f"Expected 28 results, found {len(rows)}")
|
| 129 |
+
if len(fingerprints) != 1:
|
| 130 |
+
raise RuntimeError(f"Batch schedules differ across shards: {fingerprints}")
|
| 131 |
+
write_summary(output_dir, rows)
|
| 132 |
+
atomic_json(
|
| 133 |
+
output_dir / "sweep_manifest.json",
|
| 134 |
+
{
|
| 135 |
+
"status": "complete",
|
| 136 |
+
"architecture": "three_block_predictor",
|
| 137 |
+
"initialization_method": "teacher_full",
|
| 138 |
+
"triples": [list(value) for value in EXPECTED_TRIPLES],
|
| 139 |
+
"num_triples": 28,
|
| 140 |
+
"max_steps": 1000,
|
| 141 |
+
"seed": 0,
|
| 142 |
+
"train_prompt_ids": EXPECTED_PROMPTS["train"],
|
| 143 |
+
"val_prompt_ids": EXPECTED_PROMPTS["val"],
|
| 144 |
+
"batch_schedule_sha256": fingerprints.pop(),
|
| 145 |
+
"gpu_shards": {
|
| 146 |
+
gpu: [list(value) for value in triples]
|
| 147 |
+
for gpu, triples in GPU_SHARDS.items()
|
| 148 |
+
},
|
| 149 |
+
"source_shard_root": str(shard_root),
|
| 150 |
+
},
|
| 151 |
+
)
|
| 152 |
+
print(
|
| 153 |
+
f"[merge] {len(rows)} triples -> {output_dir / 'summary.csv'}",
|
| 154 |
+
flush=True,
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
if __name__ == "__main__":
|
| 159 |
+
main()
|
scripts/merge_two_block_fppp_shards.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Merge independently evaluated two-block FPPP shards into one result set."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import shutil
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
PROMPT_IDS = list(range(80, 100))
|
| 16 |
+
SCHEDULE = "chunk0=FFFF; chunks1-6=FPPP"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def parse_args() -> argparse.Namespace:
|
| 20 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 21 |
+
parser.add_argument("--main_dir", type=Path, required=True)
|
| 22 |
+
parser.add_argument("--shard_dir", type=Path, required=True)
|
| 23 |
+
parser.add_argument("--sweep_dir", type=Path, required=True)
|
| 24 |
+
return parser.parse_args()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def atomic_json(path: Path, value: Any) -> None:
|
| 28 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 29 |
+
temporary.write_text(
|
| 30 |
+
json.dumps(value, indent=2, ensure_ascii=False) + "\n",
|
| 31 |
+
encoding="utf-8",
|
| 32 |
+
)
|
| 33 |
+
os.replace(temporary, path)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def read_experiment_names(sweep_dir: Path) -> list[str]:
|
| 37 |
+
with (sweep_dir / "summary.csv").open(
|
| 38 |
+
"r", encoding="utf-8", newline=""
|
| 39 |
+
) as handle:
|
| 40 |
+
return [row["name"] for row in csv.DictReader(handle)]
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def merge_pair_directories(main_dir: Path, shard_dir: Path) -> None:
|
| 44 |
+
for source in shard_dir.glob("pair_*"):
|
| 45 |
+
if source.is_dir():
|
| 46 |
+
shutil.copytree(source, main_dir / source.name, dirs_exist_ok=True)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def collect_rows(main_dir: Path, names: list[str]) -> list[dict[str, Any]]:
|
| 50 |
+
rows = []
|
| 51 |
+
for name in names:
|
| 52 |
+
metrics_path = main_dir / name / "metrics.json"
|
| 53 |
+
if not metrics_path.exists():
|
| 54 |
+
raise FileNotFoundError(f"Missing completed metrics: {metrics_path}")
|
| 55 |
+
metrics = json.loads(metrics_path.read_text(encoding="utf-8"))
|
| 56 |
+
if metrics.get("status") != "complete":
|
| 57 |
+
raise RuntimeError(f"Incomplete metrics for {name}")
|
| 58 |
+
if metrics.get("schedule") != SCHEDULE:
|
| 59 |
+
raise RuntimeError(f"Unexpected schedule for {name}: {metrics.get('schedule')}")
|
| 60 |
+
if metrics.get("prompt_ids") != PROMPT_IDS:
|
| 61 |
+
raise RuntimeError(f"Unexpected prompt split for {name}")
|
| 62 |
+
rows.append(
|
| 63 |
+
{
|
| 64 |
+
"name": metrics["name"],
|
| 65 |
+
"source_layer_1": metrics["source_layers"][0],
|
| 66 |
+
"source_layer_2": metrics["source_layers"][1],
|
| 67 |
+
"pair_kind": metrics["pair_kind"],
|
| 68 |
+
"schedule": metrics["schedule"],
|
| 69 |
+
"num_prompts": metrics["num_prompts"],
|
| 70 |
+
"psnr": metrics["psnr"],
|
| 71 |
+
"ssim": metrics["ssim"],
|
| 72 |
+
"lpips": metrics["lpips"],
|
| 73 |
+
"rollout_psnr": metrics["rollout_psnr"],
|
| 74 |
+
"rollout_ssim": metrics["rollout_ssim"],
|
| 75 |
+
"rollout_lpips": metrics["rollout_lpips"],
|
| 76 |
+
"offline_final_val_flow_mse": metrics[
|
| 77 |
+
"offline_final_val_flow_mse"
|
| 78 |
+
],
|
| 79 |
+
"mean_generation_time_s": metrics["mean_generation_time_s"],
|
| 80 |
+
}
|
| 81 |
+
)
|
| 82 |
+
rows.sort(key=lambda row: float(row["lpips"]))
|
| 83 |
+
return rows
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def write_summary(main_dir: Path, rows: list[dict[str, Any]]) -> None:
|
| 87 |
+
destination = main_dir / "summary.csv"
|
| 88 |
+
temporary = destination.with_suffix(".csv.tmp")
|
| 89 |
+
with temporary.open("w", encoding="utf-8", newline="") as handle:
|
| 90 |
+
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
|
| 91 |
+
writer.writeheader()
|
| 92 |
+
writer.writerows(rows)
|
| 93 |
+
os.replace(temporary, destination)
|
| 94 |
+
atomic_json(main_dir / "summary.json", rows)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def main() -> None:
|
| 98 |
+
args = parse_args()
|
| 99 |
+
main_dir = args.main_dir.resolve()
|
| 100 |
+
shard_dir = args.shard_dir.resolve()
|
| 101 |
+
sweep_dir = args.sweep_dir.resolve()
|
| 102 |
+
names = read_experiment_names(sweep_dir)
|
| 103 |
+
if len(names) != 43:
|
| 104 |
+
raise RuntimeError(f"Expected 43 experiments, found {len(names)}")
|
| 105 |
+
merge_pair_directories(main_dir, shard_dir)
|
| 106 |
+
rows = collect_rows(main_dir, names)
|
| 107 |
+
write_summary(main_dir, rows)
|
| 108 |
+
atomic_json(
|
| 109 |
+
main_dir / "manifest.json",
|
| 110 |
+
{
|
| 111 |
+
"status": "complete",
|
| 112 |
+
"architecture": "two_block_predictor",
|
| 113 |
+
"prompt_ids": PROMPT_IDS,
|
| 114 |
+
"experiments": names,
|
| 115 |
+
"rollout_schedule": "FPPP",
|
| 116 |
+
"rollout_definition": SCHEDULE,
|
| 117 |
+
"generation_seed_reset_per_prompt": 0,
|
| 118 |
+
"gpu_shards": {"2": 22, "4": 21},
|
| 119 |
+
"merged_shard_dir": str(shard_dir),
|
| 120 |
+
},
|
| 121 |
+
)
|
| 122 |
+
print(f"[merge] {len(rows)} pairs -> {main_dir / 'summary.csv'}", flush=True)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
if __name__ == "__main__":
|
| 126 |
+
main()
|
scripts/naive_vbench_policies.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Pure policy definitions for the Extended-251 naive baselines."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
from dataclasses import asdict, dataclass
|
| 7 |
+
from typing import Literal
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
PolicyFamily = Literal["full", "timestep_skipping", "velocity_reuse"]
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def raw_timestep_schedule(num_steps: int) -> tuple[int, ...]:
|
| 14 |
+
"""Return floor-spaced training noise levels in [1000, 0)."""
|
| 15 |
+
if num_steps < 1:
|
| 16 |
+
raise ValueError("num_steps must be positive")
|
| 17 |
+
return tuple(1000 * (num_steps - index) // num_steps for index in range(num_steps))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass(frozen=True)
|
| 21 |
+
class NaivePolicy:
|
| 22 |
+
name: str
|
| 23 |
+
label: str
|
| 24 |
+
family: PolicyFamily
|
| 25 |
+
raw_timesteps: tuple[int, ...]
|
| 26 |
+
cache_pattern: str | None = None
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def denoise_steps(self) -> int:
|
| 30 |
+
return len(self.raw_timesteps)
|
| 31 |
+
|
| 32 |
+
@property
|
| 33 |
+
def full_calls_per_chunk(self) -> int:
|
| 34 |
+
if self.cache_pattern is None:
|
| 35 |
+
return self.denoise_steps
|
| 36 |
+
return self.cache_pattern.count("F")
|
| 37 |
+
|
| 38 |
+
@property
|
| 39 |
+
def reuse_calls_per_chunk(self) -> int:
|
| 40 |
+
return 0 if self.cache_pattern is None else self.cache_pattern.count("R")
|
| 41 |
+
|
| 42 |
+
def validate(self) -> None:
|
| 43 |
+
if not self.name or not self.label:
|
| 44 |
+
raise ValueError("Policy name and label must be non-empty")
|
| 45 |
+
if self.family == "velocity_reuse":
|
| 46 |
+
if self.cache_pattern is None:
|
| 47 |
+
raise ValueError(f"{self.name}: velocity reuse requires a cache pattern")
|
| 48 |
+
if len(self.cache_pattern) != 4 or self.cache_pattern[0] != "F":
|
| 49 |
+
raise ValueError(f"{self.name}: cache pattern must be four steps and start with F")
|
| 50 |
+
if set(self.cache_pattern) - {"F", "R"}:
|
| 51 |
+
raise ValueError(f"{self.name}: cache pattern may contain only F and R")
|
| 52 |
+
if self.raw_timesteps != (1000, 750, 500, 250):
|
| 53 |
+
raise ValueError(f"{self.name}: cache baselines must keep the original schedule")
|
| 54 |
+
elif self.cache_pattern is not None:
|
| 55 |
+
raise ValueError(f"{self.name}: only velocity-reuse policies have cache patterns")
|
| 56 |
+
|
| 57 |
+
def to_dict(self) -> dict[str, object]:
|
| 58 |
+
value = asdict(self)
|
| 59 |
+
value.update(
|
| 60 |
+
denoise_steps=self.denoise_steps,
|
| 61 |
+
full_calls_per_chunk=self.full_calls_per_chunk,
|
| 62 |
+
reuse_calls_per_chunk=self.reuse_calls_per_chunk,
|
| 63 |
+
)
|
| 64 |
+
return value
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
ORIGINAL_RAW_TIMESTEPS = (1000, 750, 500, 250)
|
| 68 |
+
|
| 69 |
+
REFERENCE_POLICY = NaivePolicy(
|
| 70 |
+
name="ffff",
|
| 71 |
+
label="FFFF",
|
| 72 |
+
family="full",
|
| 73 |
+
raw_timesteps=ORIGINAL_RAW_TIMESTEPS,
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
EVALUATION_POLICIES = (
|
| 77 |
+
NaivePolicy(
|
| 78 |
+
name="naive_3step",
|
| 79 |
+
label="Naive 3-step / timestep skipping",
|
| 80 |
+
family="timestep_skipping",
|
| 81 |
+
raw_timesteps=raw_timestep_schedule(3),
|
| 82 |
+
),
|
| 83 |
+
NaivePolicy(
|
| 84 |
+
name="naive_2step",
|
| 85 |
+
label="Naive 2-step / timestep skipping",
|
| 86 |
+
family="timestep_skipping",
|
| 87 |
+
raw_timesteps=raw_timestep_schedule(2),
|
| 88 |
+
),
|
| 89 |
+
NaivePolicy(
|
| 90 |
+
name="naive_1step",
|
| 91 |
+
label="Naive 1-step / timestep skipping",
|
| 92 |
+
family="timestep_skipping",
|
| 93 |
+
raw_timesteps=raw_timestep_schedule(1),
|
| 94 |
+
),
|
| 95 |
+
NaivePolicy(
|
| 96 |
+
name="naive_cache_3step_frff",
|
| 97 |
+
label="Naive Cache 3-step (FRFF)",
|
| 98 |
+
family="velocity_reuse",
|
| 99 |
+
raw_timesteps=ORIGINAL_RAW_TIMESTEPS,
|
| 100 |
+
cache_pattern="FRFF",
|
| 101 |
+
),
|
| 102 |
+
NaivePolicy(
|
| 103 |
+
name="naive_cache_2step_frrf",
|
| 104 |
+
label="Naive Cache 2-step (FRRF)",
|
| 105 |
+
family="velocity_reuse",
|
| 106 |
+
raw_timesteps=ORIGINAL_RAW_TIMESTEPS,
|
| 107 |
+
cache_pattern="FRRF",
|
| 108 |
+
),
|
| 109 |
+
NaivePolicy(
|
| 110 |
+
name="naive_cache_1step_frrr",
|
| 111 |
+
label="Naive Cache 1-step (FRRR)",
|
| 112 |
+
family="velocity_reuse",
|
| 113 |
+
raw_timesteps=ORIGINAL_RAW_TIMESTEPS,
|
| 114 |
+
cache_pattern="FRRR",
|
| 115 |
+
),
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
ALL_POLICIES = (REFERENCE_POLICY, *EVALUATION_POLICIES)
|
| 119 |
+
POLICIES_BY_NAME = {policy.name: policy for policy in ALL_POLICIES}
|
| 120 |
+
EVALUATION_STRATEGY_NAMES = tuple(policy.name for policy in EVALUATION_POLICIES)
|
| 121 |
+
ALL_STRATEGY_NAMES = tuple(policy.name for policy in ALL_POLICIES)
|
| 122 |
+
|
| 123 |
+
for _policy in ALL_POLICIES:
|
| 124 |
+
_policy.validate()
|
| 125 |
+
if len(POLICIES_BY_NAME) != len(ALL_POLICIES):
|
| 126 |
+
raise ValueError("Naive policy names must be unique")
|
| 127 |
+
|
scripts/plot_frrf_chunk_metrics.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Plot 10-prompt mean FRRF metrics versus the reused chunk.
|
| 3 |
+
|
| 4 |
+
The three input ``per_prompt.csv`` files use slightly different identifiers
|
| 5 |
+
(``prompt_id`` for Self/Causal-Forcing and ``case`` for WorldPlay), but share
|
| 6 |
+
the metric columns. We deliberately aggregate from the per-prompt rows so
|
| 7 |
+
that PSNR is also an arithmetic mean over the ten prompts.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import csv
|
| 14 |
+
from collections import defaultdict
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
import matplotlib.pyplot as plt
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
DEFAULT_ROOTS = {
|
| 21 |
+
"Self-Forcing": Path(
|
| 22 |
+
"/data3/chenzhuo/workspace/Self-Forcing/outputs/"
|
| 23 |
+
"single_chunk_frrf_14chunks_first10"
|
| 24 |
+
),
|
| 25 |
+
"Causal-Forcing": Path(
|
| 26 |
+
"/data3/chenzhuo/workspace/Causal-Forcing/outputs/"
|
| 27 |
+
"single_chunk_frrf_14chunks_first10"
|
| 28 |
+
),
|
| 29 |
+
"HY-WorldPlay": Path(
|
| 30 |
+
"/data3/chenzhuo/workspace/HY-WorldPlay-DEV/outputs/"
|
| 31 |
+
"moviebench_single_chunk_frrf_14chunks_first10"
|
| 32 |
+
),
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
METRICS = ("psnr", "ssim", "lpips")
|
| 36 |
+
Y_LABELS = {"psnr": "PSNR (dB)", "ssim": "SSIM", "lpips": "LPIPS"}
|
| 37 |
+
COLORS = {
|
| 38 |
+
"Self-Forcing": "#1f77b4",
|
| 39 |
+
"Causal-Forcing": "#d62728",
|
| 40 |
+
"HY-WorldPlay": "#2ca02c",
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def read_prompt_means(root: Path, num_chunks: int = 14) -> dict[str, list[float]]:
|
| 45 |
+
"""Return arithmetic means over prompts for every metric and chunk."""
|
| 46 |
+
csv_path = root / "per_prompt.csv"
|
| 47 |
+
if not csv_path.exists():
|
| 48 |
+
raise FileNotFoundError(csv_path)
|
| 49 |
+
|
| 50 |
+
# values[chunk][metric] -> list of prompt-level values
|
| 51 |
+
values: dict[int, dict[str, list[float]]] = defaultdict(
|
| 52 |
+
lambda: {metric: [] for metric in METRICS}
|
| 53 |
+
)
|
| 54 |
+
with csv_path.open(newline="") as handle:
|
| 55 |
+
reader = csv.DictReader(handle)
|
| 56 |
+
required = {"reuse_chunk", *METRICS}
|
| 57 |
+
missing = required.difference(reader.fieldnames or ())
|
| 58 |
+
if missing:
|
| 59 |
+
raise ValueError(f"{csv_path} is missing columns: {sorted(missing)}")
|
| 60 |
+
for row in reader:
|
| 61 |
+
chunk = int(row["reuse_chunk"])
|
| 62 |
+
if not 0 <= chunk < num_chunks:
|
| 63 |
+
raise ValueError(f"unexpected reuse_chunk={chunk} in {csv_path}")
|
| 64 |
+
for metric in METRICS:
|
| 65 |
+
values[chunk][metric].append(float(row[metric]))
|
| 66 |
+
|
| 67 |
+
result: dict[str, list[float]] = {}
|
| 68 |
+
for metric in METRICS:
|
| 69 |
+
means = []
|
| 70 |
+
for chunk in range(num_chunks):
|
| 71 |
+
prompt_values = values[chunk][metric]
|
| 72 |
+
if len(prompt_values) != 10:
|
| 73 |
+
raise ValueError(
|
| 74 |
+
f"{csv_path}: chunk {chunk} has {len(prompt_values)} rows; "
|
| 75 |
+
"expected 10 prompts"
|
| 76 |
+
)
|
| 77 |
+
means.append(sum(prompt_values) / len(prompt_values))
|
| 78 |
+
result[metric] = means
|
| 79 |
+
return result
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def parse_args() -> argparse.Namespace:
|
| 83 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 84 |
+
parser.add_argument(
|
| 85 |
+
"--output",
|
| 86 |
+
type=Path,
|
| 87 |
+
default=Path(
|
| 88 |
+
"/data3/chenzhuo/workspace/Self-Forcing/outputs/plots/"
|
| 89 |
+
"frrf_14chunks_metrics_10prompt_mean.png"
|
| 90 |
+
),
|
| 91 |
+
help="PNG output path (a PDF with the same stem is written too).",
|
| 92 |
+
)
|
| 93 |
+
parser.add_argument("--num-chunks", type=int, default=14)
|
| 94 |
+
return parser.parse_args()
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def main() -> None:
|
| 98 |
+
args = parse_args()
|
| 99 |
+
data = {
|
| 100 |
+
label: read_prompt_means(root, args.num_chunks)
|
| 101 |
+
for label, root in DEFAULT_ROOTS.items()
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
plt.rcParams.update(
|
| 105 |
+
{
|
| 106 |
+
"font.size": 11,
|
| 107 |
+
"axes.labelsize": 12,
|
| 108 |
+
"axes.titlesize": 13,
|
| 109 |
+
"legend.fontsize": 10.5,
|
| 110 |
+
"xtick.labelsize": 10,
|
| 111 |
+
"ytick.labelsize": 10,
|
| 112 |
+
"savefig.bbox": "tight",
|
| 113 |
+
}
|
| 114 |
+
)
|
| 115 |
+
fig, axes = plt.subplots(1, 3, figsize=(15.2, 4.7), sharex=True)
|
| 116 |
+
chunks = list(range(args.num_chunks))
|
| 117 |
+
|
| 118 |
+
for axis, metric in zip(axes, METRICS):
|
| 119 |
+
for label, values in data.items():
|
| 120 |
+
axis.plot(
|
| 121 |
+
chunks,
|
| 122 |
+
values[metric],
|
| 123 |
+
color=COLORS[label],
|
| 124 |
+
marker="o",
|
| 125 |
+
markersize=4.5,
|
| 126 |
+
linewidth=2.0,
|
| 127 |
+
label=label,
|
| 128 |
+
)
|
| 129 |
+
axis.set_title(metric.upper())
|
| 130 |
+
axis.set_xlabel("Reuse chunk")
|
| 131 |
+
axis.set_ylabel(Y_LABELS[metric])
|
| 132 |
+
axis.set_xticks(chunks)
|
| 133 |
+
axis.grid(True, linestyle="--", linewidth=0.7, alpha=0.35)
|
| 134 |
+
axis.set_axisbelow(True)
|
| 135 |
+
axis.spines["top"].set_visible(False)
|
| 136 |
+
axis.spines["right"].set_visible(False)
|
| 137 |
+
|
| 138 |
+
# One shared legend for all three panels.
|
| 139 |
+
handles, labels = axes[0].get_legend_handles_labels()
|
| 140 |
+
fig.legend(
|
| 141 |
+
handles,
|
| 142 |
+
labels,
|
| 143 |
+
loc="upper center",
|
| 144 |
+
bbox_to_anchor=(0.5, 0.995),
|
| 145 |
+
ncol=3,
|
| 146 |
+
frameon=False,
|
| 147 |
+
)
|
| 148 |
+
fig.suptitle(
|
| 149 |
+
"FRRF reuse-chunk error",
|
| 150 |
+
y=1.045,
|
| 151 |
+
fontsize=14,
|
| 152 |
+
fontweight="semibold",
|
| 153 |
+
)
|
| 154 |
+
fig.tight_layout(rect=(0, 0, 1, 1.0), w_pad=2.0)
|
| 155 |
+
|
| 156 |
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
| 157 |
+
fig.savefig(args.output, dpi=300)
|
| 158 |
+
fig.savefig(args.output.with_suffix(".pdf"))
|
| 159 |
+
print(f"saved {args.output}")
|
| 160 |
+
print(f"saved {args.output.with_suffix('.pdf')}")
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
if __name__ == "__main__":
|
| 164 |
+
main()
|
scripts/plot_self_causal_evidence.py
ADDED
|
@@ -0,0 +1,776 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Create compact publication-style figures for the Self/Causal evidence chain."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import colorsys
|
| 8 |
+
import csv
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import matplotlib
|
| 12 |
+
|
| 13 |
+
matplotlib.use("Agg")
|
| 14 |
+
import matplotlib.pyplot as plt
|
| 15 |
+
import numpy as np
|
| 16 |
+
from matplotlib.lines import Line2D
|
| 17 |
+
from matplotlib.patches import Patch
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
SELF = "#0F766E"
|
| 21 |
+
CAUSAL = "#4F46E5"
|
| 22 |
+
INK = "#172033"
|
| 23 |
+
MUTED = "#64748B"
|
| 24 |
+
GRID = "#DCE3EC"
|
| 25 |
+
PALE = "#F1F5F9"
|
| 26 |
+
WARM = "#D97706"
|
| 27 |
+
MODEL_COLOR = {"self_forcing": SELF, "causal_forcing": CAUSAL}
|
| 28 |
+
MODEL_LABEL = {"self_forcing": "Self-Forcing", "causal_forcing": "Causal-Forcing"}
|
| 29 |
+
LAYERS = ("early", "middle", "late", "final")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def parse_args() -> argparse.Namespace:
|
| 33 |
+
parser = argparse.ArgumentParser()
|
| 34 |
+
parser.add_argument("--redundancy_csv", type=Path, required=True)
|
| 35 |
+
parser.add_argument("--motion_bins_csv", type=Path, required=True)
|
| 36 |
+
parser.add_argument("--motion_summary_csv", type=Path, required=True)
|
| 37 |
+
parser.add_argument("--native_gain_csv", type=Path, required=True)
|
| 38 |
+
parser.add_argument(
|
| 39 |
+
"--causal_late_sensitivity_csv",
|
| 40 |
+
type=Path,
|
| 41 |
+
help="Optional sensitivity summary used to replace only Causal late Ridge.",
|
| 42 |
+
)
|
| 43 |
+
parser.add_argument("--sensitivity_scenario", default="exclude_two_folds")
|
| 44 |
+
parser.add_argument("--aligned_probe_csv", type=Path, required=True)
|
| 45 |
+
parser.add_argument(
|
| 46 |
+
"--unified_aligned_csv",
|
| 47 |
+
type=Path,
|
| 48 |
+
help="Optional four-layer 64-D/common-mask Ridge summary used for panels D/E.",
|
| 49 |
+
)
|
| 50 |
+
parser.add_argument("--output_dir", type=Path, required=True)
|
| 51 |
+
return parser.parse_args()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def read_csv(path: Path) -> list[dict[str, str]]:
|
| 55 |
+
with path.open(newline="", encoding="utf-8") as handle:
|
| 56 |
+
return list(csv.DictReader(handle))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def apply_causal_late_sensitivity(
|
| 60 |
+
native_rows: list[dict[str, str]],
|
| 61 |
+
sensitivity_path: Path | None,
|
| 62 |
+
scenario: str,
|
| 63 |
+
) -> bool:
|
| 64 |
+
"""Replace only the Causal late Ridge aggregate with an explicit sensitivity result."""
|
| 65 |
+
if sensitivity_path is None:
|
| 66 |
+
return False
|
| 67 |
+
sensitivity_rows = read_csv(sensitivity_path)
|
| 68 |
+
selected = [row for row in sensitivity_rows if row["scenario"] == scenario]
|
| 69 |
+
if len(selected) != 1:
|
| 70 |
+
raise ValueError(f"Expected one sensitivity row for {scenario!r}, found {len(selected)}")
|
| 71 |
+
source = selected[0]
|
| 72 |
+
targets = [
|
| 73 |
+
row
|
| 74 |
+
for row in native_rows
|
| 75 |
+
if row["method"] == "linear"
|
| 76 |
+
and row["model_family"] == "causal_forcing"
|
| 77 |
+
and row["layer_role"] == "late"
|
| 78 |
+
and row["probe"] == "fusion_same"
|
| 79 |
+
]
|
| 80 |
+
if len(targets) != 1:
|
| 81 |
+
raise ValueError(f"Expected one Causal late Ridge row, found {len(targets)}")
|
| 82 |
+
target = targets[0]
|
| 83 |
+
for key in ("gain_mean", "gain_ci95_low", "gain_ci95_high", "wins", "signflip_p"):
|
| 84 |
+
target[key] = source[key]
|
| 85 |
+
target["prompt_count"] = source["prompt_count"]
|
| 86 |
+
return True
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def lighten(color: str, amount: float = 0.45) -> str:
|
| 90 |
+
r, g, b = matplotlib.colors.to_rgb(color)
|
| 91 |
+
h, l, s = colorsys.rgb_to_hls(r, g, b)
|
| 92 |
+
return matplotlib.colors.to_hex(colorsys.hls_to_rgb(h, 1 - amount * (1 - l), s * 0.85))
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def setup_style() -> None:
|
| 96 |
+
plt.rcParams.update({
|
| 97 |
+
"font.family": "DejaVu Sans",
|
| 98 |
+
"font.size": 9,
|
| 99 |
+
"axes.titlesize": 10.5,
|
| 100 |
+
"axes.labelsize": 9,
|
| 101 |
+
"axes.titleweight": "semibold",
|
| 102 |
+
"axes.labelcolor": INK,
|
| 103 |
+
"axes.edgecolor": GRID,
|
| 104 |
+
"axes.linewidth": 0.8,
|
| 105 |
+
"xtick.color": MUTED,
|
| 106 |
+
"ytick.color": MUTED,
|
| 107 |
+
"xtick.labelsize": 8.5,
|
| 108 |
+
"ytick.labelsize": 8.5,
|
| 109 |
+
"grid.color": GRID,
|
| 110 |
+
"grid.linewidth": 0.7,
|
| 111 |
+
"grid.alpha": 0.65,
|
| 112 |
+
"legend.fontsize": 7.7,
|
| 113 |
+
"legend.frameon": False,
|
| 114 |
+
"figure.facecolor": "white",
|
| 115 |
+
"axes.facecolor": "white",
|
| 116 |
+
"savefig.facecolor": "white",
|
| 117 |
+
"savefig.bbox": "tight",
|
| 118 |
+
})
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def polish(ax: plt.Axes, grid_axis: str = "y") -> None:
|
| 122 |
+
ax.spines["top"].set_visible(False)
|
| 123 |
+
ax.spines["right"].set_visible(False)
|
| 124 |
+
ax.grid(axis=grid_axis, zorder=0)
|
| 125 |
+
ax.tick_params(length=0)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def panel_label(ax: plt.Axes, label: str) -> None:
|
| 129 |
+
ax.text(
|
| 130 |
+
-0.13,
|
| 131 |
+
1.08,
|
| 132 |
+
label,
|
| 133 |
+
transform=ax.transAxes,
|
| 134 |
+
fontsize=12,
|
| 135 |
+
fontweight="bold",
|
| 136 |
+
color=INK,
|
| 137 |
+
va="top",
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def lookup_rows(rows: list[dict[str, str]], keys: tuple[str, ...]) -> dict[tuple[str, ...], dict[str, str]]:
|
| 142 |
+
return {tuple(row[key] for key in keys): row for row in rows}
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def plot_redundancy(
|
| 146 |
+
ax: plt.Axes,
|
| 147 |
+
rows: list[dict[str, str]],
|
| 148 |
+
family: str,
|
| 149 |
+
variant_50: str,
|
| 150 |
+
panel: str,
|
| 151 |
+
) -> None:
|
| 152 |
+
table = lookup_rows(rows, ("model_family", "model_variant", "layer_role", "comparison"))
|
| 153 |
+
x = np.arange(len(LAYERS))
|
| 154 |
+
color = MODEL_COLOR[family]
|
| 155 |
+
series = [
|
| 156 |
+
(variant_50, "within_adjacent", "50-step · denoising", "#334155", "-", "o"),
|
| 157 |
+
(variant_50, "cross_boundary_to_all", "50-step · boundary", "#94A3B8", "--", "D"),
|
| 158 |
+
("dmd4", "within_adjacent", "4-step · denoising", color, "-", "o"),
|
| 159 |
+
("dmd4", "cross_boundary_to_all", "4-step · boundary", lighten(color), "--", "D"),
|
| 160 |
+
]
|
| 161 |
+
for variant, comparison, label, line_color, linestyle, marker in series:
|
| 162 |
+
values, lows, highs = [], [], []
|
| 163 |
+
for role in LAYERS:
|
| 164 |
+
row = table[(family, variant, role, comparison)]
|
| 165 |
+
value = float(row["token_cosine_mean"])
|
| 166 |
+
values.append(value)
|
| 167 |
+
lows.append(value - float(row["ci95_low"]))
|
| 168 |
+
highs.append(float(row["ci95_high"]) - value)
|
| 169 |
+
ax.errorbar(
|
| 170 |
+
x,
|
| 171 |
+
values,
|
| 172 |
+
yerr=np.asarray([lows, highs]),
|
| 173 |
+
color=line_color,
|
| 174 |
+
linestyle=linestyle,
|
| 175 |
+
linewidth=1.65,
|
| 176 |
+
marker=marker,
|
| 177 |
+
markersize=4.3,
|
| 178 |
+
markeredgewidth=0.7,
|
| 179 |
+
capsize=2,
|
| 180 |
+
label=label,
|
| 181 |
+
zorder=3,
|
| 182 |
+
)
|
| 183 |
+
ax.axvspan(-0.35, 2.35, color=PALE, alpha=0.58, zorder=-2)
|
| 184 |
+
ax.set_xticks(x, [value.title() for value in LAYERS])
|
| 185 |
+
ax.set_ylim(0.62, 1.015)
|
| 186 |
+
ax.set_yticks([0.65, 0.75, 0.85, 0.95, 1.00])
|
| 187 |
+
ax.set_ylabel("Token cosine")
|
| 188 |
+
ax.set_title(MODEL_LABEL[family], loc="left", pad=8)
|
| 189 |
+
polish(ax)
|
| 190 |
+
panel_label(ax, panel)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def plot_motion(ax: plt.Axes, rows: list[dict[str, str]], panel: str) -> None:
|
| 194 |
+
table = lookup_rows(rows, ("model", "action", "motion_bin"))
|
| 195 |
+
bins = ("low", "medium", "high")
|
| 196 |
+
x = np.arange(3)
|
| 197 |
+
for family in ("self_forcing", "causal_forcing"):
|
| 198 |
+
color = MODEL_COLOR[family]
|
| 199 |
+
raw = [float(table[(family, "none", motion)]["raw_cosine"]) for motion in bins]
|
| 200 |
+
flow = [float(table[(family, "none", motion)]["flow_aligned_cosine"]) for motion in bins]
|
| 201 |
+
ax.plot(x, raw, color=color, linewidth=1.8, marker="o", markersize=4.5, zorder=3)
|
| 202 |
+
ax.plot(x, flow, color=color, linewidth=1.45, linestyle="--", marker="^", markersize=4.5, zorder=3)
|
| 203 |
+
ax.fill_between(x, raw, flow, color=color, alpha=0.09, zorder=1)
|
| 204 |
+
ax.annotate(
|
| 205 |
+
f"+{100 * (flow[-1] - raw[-1]):.2f} pts",
|
| 206 |
+
(x[-1], flow[-1]),
|
| 207 |
+
xytext=(5, 3),
|
| 208 |
+
textcoords="offset points",
|
| 209 |
+
color=color,
|
| 210 |
+
fontsize=7.3,
|
| 211 |
+
fontweight="semibold",
|
| 212 |
+
)
|
| 213 |
+
ax.set_xticks(x, ["Low", "Medium", "High"])
|
| 214 |
+
ax.set_ylim(0.875, 0.942)
|
| 215 |
+
ax.set_ylabel("Boundary cosine")
|
| 216 |
+
ax.set_title("Motion exposes spatial mismatch", loc="left", pad=8)
|
| 217 |
+
handles = [
|
| 218 |
+
Line2D([], [], color=SELF, marker="o", label="Self · raw"),
|
| 219 |
+
Line2D([], [], color=SELF, marker="^", linestyle="--", label="Self · flow"),
|
| 220 |
+
Line2D([], [], color=CAUSAL, marker="o", label="Causal · raw"),
|
| 221 |
+
Line2D([], [], color=CAUSAL, marker="^", linestyle="--", label="Causal · flow"),
|
| 222 |
+
]
|
| 223 |
+
ax.legend(handles=handles, ncol=2, loc="lower left", columnspacing=0.8, handlelength=2.0)
|
| 224 |
+
polish(ax)
|
| 225 |
+
panel_label(ax, panel)
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def plot_native_gain(
|
| 229 |
+
ax: plt.Axes,
|
| 230 |
+
rows: list[dict[str, str]],
|
| 231 |
+
panel: str,
|
| 232 |
+
sensitivity_applied: bool = False,
|
| 233 |
+
) -> None:
|
| 234 |
+
table = lookup_rows(rows, ("method", "model_family", "layer_role", "probe"))
|
| 235 |
+
x = np.arange(len(LAYERS))
|
| 236 |
+
offsets = {"self_forcing": -0.11, "causal_forcing": 0.11}
|
| 237 |
+
for family in ("self_forcing", "causal_forcing"):
|
| 238 |
+
color = MODEL_COLOR[family]
|
| 239 |
+
for method, probe, marker, filled in (
|
| 240 |
+
("linear", "fusion_same", "o", True),
|
| 241 |
+
("nonlinear", "both_correct", "D", False),
|
| 242 |
+
):
|
| 243 |
+
values, lows, highs = [], [], []
|
| 244 |
+
for role in LAYERS:
|
| 245 |
+
row = table[(method, family, role, probe)]
|
| 246 |
+
value = 100 * float(row["gain_mean"])
|
| 247 |
+
values.append(value)
|
| 248 |
+
lows.append(value - 100 * float(row["gain_ci95_low"]))
|
| 249 |
+
highs.append(100 * float(row["gain_ci95_high"]) - value)
|
| 250 |
+
ax.errorbar(
|
| 251 |
+
x + offsets[family],
|
| 252 |
+
values,
|
| 253 |
+
yerr=np.asarray([lows, highs]),
|
| 254 |
+
linestyle="none",
|
| 255 |
+
marker=marker,
|
| 256 |
+
markersize=5.2 if method == "linear" else 4.6,
|
| 257 |
+
markerfacecolor=color if filled else "white",
|
| 258 |
+
markeredgecolor=color,
|
| 259 |
+
markeredgewidth=1.25,
|
| 260 |
+
ecolor=color,
|
| 261 |
+
elinewidth=1.1,
|
| 262 |
+
capsize=2.5,
|
| 263 |
+
zorder=4,
|
| 264 |
+
)
|
| 265 |
+
ax.axhline(0, color=INK, linewidth=0.8, zorder=1)
|
| 266 |
+
ax.set_xticks(x, [value.title() for value in LAYERS])
|
| 267 |
+
ax.set_ylabel("Held-out MSE reduction (%)")
|
| 268 |
+
ax.set_ylim(-2.8, 5.8)
|
| 269 |
+
title = "Native previous-boundary adds predictive value"
|
| 270 |
+
if sensitivity_applied:
|
| 271 |
+
title += "†"
|
| 272 |
+
ax.set_title(title, loc="left", pad=8)
|
| 273 |
+
handles = [
|
| 274 |
+
Line2D([], [], color=SELF, marker="o", linestyle="none", label="Self · Ridge"),
|
| 275 |
+
Line2D([], [], color=SELF, marker="D", markerfacecolor="white", linestyle="none", label="Self · MLP"),
|
| 276 |
+
Line2D([], [], color=CAUSAL, marker="o", linestyle="none", label="Causal · Ridge"),
|
| 277 |
+
Line2D([], [], color=CAUSAL, marker="D", markerfacecolor="white", linestyle="none", label="Causal · MLP"),
|
| 278 |
+
]
|
| 279 |
+
ax.legend(handles=handles, ncol=2, loc="lower left", columnspacing=1.0)
|
| 280 |
+
ax.text(
|
| 281 |
+
0.995,
|
| 282 |
+
0.03,
|
| 283 |
+
"95% prompt-bootstrap CI",
|
| 284 |
+
transform=ax.transAxes,
|
| 285 |
+
ha="right",
|
| 286 |
+
color=MUTED,
|
| 287 |
+
fontsize=7.3,
|
| 288 |
+
)
|
| 289 |
+
polish(ax)
|
| 290 |
+
panel_label(ax, panel)
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def plot_aligned_predictor(ax: plt.Axes, rows: list[dict[str, str]], panel: str) -> None:
|
| 294 |
+
table = lookup_rows(rows, ("model", "probe"))
|
| 295 |
+
x = np.arange(2)
|
| 296 |
+
width = 0.29
|
| 297 |
+
for index, family in enumerate(("self_forcing", "causal_forcing")):
|
| 298 |
+
color = MODEL_COLOR[family]
|
| 299 |
+
for offset, probe, label, shade in (
|
| 300 |
+
(-width / 2, "both_raw", "Raw boundary", lighten(color, 0.28)),
|
| 301 |
+
(width / 2, "both_flow", "Flow-aligned", color),
|
| 302 |
+
):
|
| 303 |
+
row = table[(family, probe)]
|
| 304 |
+
value = 100 * float(row["mse_gain_vs_step_mean"])
|
| 305 |
+
low = value - 100 * float(row["mse_gain_vs_step_ci95_low"])
|
| 306 |
+
high = 100 * float(row["mse_gain_vs_step_ci95_high"]) - value
|
| 307 |
+
ax.bar(
|
| 308 |
+
x[index] + offset,
|
| 309 |
+
value,
|
| 310 |
+
width=width,
|
| 311 |
+
color=shade,
|
| 312 |
+
edgecolor=color if probe == "both_raw" else "white",
|
| 313 |
+
linewidth=0.9,
|
| 314 |
+
hatch="///" if probe == "both_raw" else None,
|
| 315 |
+
zorder=3,
|
| 316 |
+
)
|
| 317 |
+
ax.errorbar(
|
| 318 |
+
x[index] + offset,
|
| 319 |
+
value,
|
| 320 |
+
yerr=np.asarray([[low], [high]]),
|
| 321 |
+
fmt="none",
|
| 322 |
+
ecolor=INK,
|
| 323 |
+
elinewidth=0.9,
|
| 324 |
+
capsize=2.2,
|
| 325 |
+
zorder=4,
|
| 326 |
+
)
|
| 327 |
+
flow = table[(family, "both_flow")]
|
| 328 |
+
delta = 100 * float(flow["mse_gain_vs_raw_mean"])
|
| 329 |
+
ymax = max(
|
| 330 |
+
100 * float(table[(family, "both_raw")]["mse_gain_vs_step_ci95_high"]),
|
| 331 |
+
100 * float(flow["mse_gain_vs_step_ci95_high"]),
|
| 332 |
+
)
|
| 333 |
+
ax.text(x[index], ymax + 0.32, f"+{delta:.2f} pp", ha="center", color=color, fontsize=8, fontweight="semibold")
|
| 334 |
+
ax.axhline(0, color=INK, linewidth=0.8)
|
| 335 |
+
ax.set_xticks(x, ["Self", "Causal"])
|
| 336 |
+
ax.set_ylim(0, 9.3)
|
| 337 |
+
ax.set_ylabel("MSE reduction vs step-only (%)")
|
| 338 |
+
ax.set_title("Alignment further improves prediction", loc="left", pad=8)
|
| 339 |
+
ax.legend(
|
| 340 |
+
handles=[
|
| 341 |
+
Patch(facecolor="white", edgecolor=MUTED, hatch="///", label="Raw boundary"),
|
| 342 |
+
Patch(facecolor="#334155", edgecolor="white", label="Flow-aligned"),
|
| 343 |
+
],
|
| 344 |
+
loc="lower right",
|
| 345 |
+
)
|
| 346 |
+
polish(ax)
|
| 347 |
+
panel_label(ax, panel)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
def plot_unified_raw_gain(ax: plt.Axes, rows: list[dict[str, str]], panel: str) -> None:
|
| 351 |
+
table = lookup_rows(rows, ("model", "layer_role", "probe"))
|
| 352 |
+
x = np.arange(len(LAYERS))
|
| 353 |
+
offsets = {"self_forcing": -0.08, "causal_forcing": 0.08}
|
| 354 |
+
for family in ("self_forcing", "causal_forcing"):
|
| 355 |
+
values, lows, highs = [], [], []
|
| 356 |
+
for role in LAYERS:
|
| 357 |
+
row = table[(family, role, "both_raw")]
|
| 358 |
+
value = 100 * float(row["mse_gain_vs_step_mean"])
|
| 359 |
+
values.append(value)
|
| 360 |
+
lows.append(value - 100 * float(row["mse_gain_vs_step_ci95_low"]))
|
| 361 |
+
highs.append(100 * float(row["mse_gain_vs_step_ci95_high"]) - value)
|
| 362 |
+
ax.errorbar(
|
| 363 |
+
x + offsets[family],
|
| 364 |
+
values,
|
| 365 |
+
yerr=np.asarray([lows, highs]),
|
| 366 |
+
color=MODEL_COLOR[family],
|
| 367 |
+
linestyle="none",
|
| 368 |
+
marker="o",
|
| 369 |
+
markeredgecolor="white",
|
| 370 |
+
markeredgewidth=0.7,
|
| 371 |
+
markersize=5.3,
|
| 372 |
+
elinewidth=1.1,
|
| 373 |
+
capsize=2.5,
|
| 374 |
+
label=MODEL_LABEL[family],
|
| 375 |
+
zorder=4,
|
| 376 |
+
)
|
| 377 |
+
ax.axhline(0, color=INK, linewidth=0.8)
|
| 378 |
+
ax.set_xticks(x, [value.title() for value in LAYERS])
|
| 379 |
+
ax.set_ylim(0, 7.25)
|
| 380 |
+
ax.set_ylabel("MSE reduction vs step-only (%)")
|
| 381 |
+
ax.set_title("Raw previous-boundary adds predictive value", loc="left", pad=8)
|
| 382 |
+
ax.legend(ncol=2, loc="upper left", columnspacing=1.0)
|
| 383 |
+
ax.text(
|
| 384 |
+
0.995,
|
| 385 |
+
0.03,
|
| 386 |
+
"95% prompt-bootstrap CI",
|
| 387 |
+
transform=ax.transAxes,
|
| 388 |
+
ha="right",
|
| 389 |
+
color=MUTED,
|
| 390 |
+
fontsize=7.3,
|
| 391 |
+
)
|
| 392 |
+
polish(ax)
|
| 393 |
+
panel_label(ax, panel)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def plot_unified_alignment_gain(ax: plt.Axes, rows: list[dict[str, str]], panel: str) -> None:
|
| 397 |
+
table = lookup_rows(rows, ("model", "layer_role", "probe"))
|
| 398 |
+
x = np.arange(len(LAYERS))
|
| 399 |
+
for family in ("self_forcing", "causal_forcing"):
|
| 400 |
+
values, lows, highs = [], [], []
|
| 401 |
+
for role in LAYERS:
|
| 402 |
+
row = table[(family, role, "both_flow")]
|
| 403 |
+
value = 100 * float(row["mse_gain_vs_raw_mean"])
|
| 404 |
+
values.append(value)
|
| 405 |
+
lows.append(value - 100 * float(row["mse_gain_vs_raw_ci95_low"]))
|
| 406 |
+
highs.append(100 * float(row["mse_gain_vs_raw_ci95_high"]) - value)
|
| 407 |
+
ax.errorbar(
|
| 408 |
+
x,
|
| 409 |
+
values,
|
| 410 |
+
yerr=np.asarray([lows, highs]),
|
| 411 |
+
color=MODEL_COLOR[family],
|
| 412 |
+
linewidth=1.55,
|
| 413 |
+
marker="o" if family == "self_forcing" else "D",
|
| 414 |
+
markersize=4.7,
|
| 415 |
+
markeredgecolor="white",
|
| 416 |
+
markeredgewidth=0.6,
|
| 417 |
+
capsize=2.2,
|
| 418 |
+
label=MODEL_LABEL[family],
|
| 419 |
+
zorder=4,
|
| 420 |
+
)
|
| 421 |
+
ax.axhline(0, color=INK, linewidth=0.8)
|
| 422 |
+
ax.set_xticks(x, ["Early", "Mid", "Late", "Final"])
|
| 423 |
+
ax.set_ylim(0, 3.0)
|
| 424 |
+
ax.set_ylabel("Additional MSE reduction (%)")
|
| 425 |
+
ax.set_title("Flow alignment adds beyond raw", loc="left", pad=8)
|
| 426 |
+
ax.legend(loc="upper left")
|
| 427 |
+
polish(ax)
|
| 428 |
+
panel_label(ax, panel)
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
def plot_unified_combined_gain(ax: plt.Axes, rows: list[dict[str, str]], panel: str) -> None:
|
| 432 |
+
"""Grouped raw/flow bars under one directly comparable y-axis."""
|
| 433 |
+
table = lookup_rows(rows, ("model", "layer_role", "probe"))
|
| 434 |
+
positions = np.asarray([0, 1, 2, 3, 5, 6, 7, 8], dtype=float)
|
| 435 |
+
groups = [
|
| 436 |
+
(family, role)
|
| 437 |
+
for family in ("self_forcing", "causal_forcing")
|
| 438 |
+
for role in LAYERS
|
| 439 |
+
]
|
| 440 |
+
width = 0.34
|
| 441 |
+
ax.axvspan(-0.55, 3.55, color=SELF, alpha=0.035, zorder=-3)
|
| 442 |
+
ax.axvspan(4.45, 8.55, color=CAUSAL, alpha=0.035, zorder=-3)
|
| 443 |
+
for index, (family, role) in enumerate(groups):
|
| 444 |
+
color = MODEL_COLOR[family]
|
| 445 |
+
raw = table[(family, role, "both_raw")]
|
| 446 |
+
flow = table[(family, role, "both_flow")]
|
| 447 |
+
raw_value = 100 * float(raw["mse_gain_vs_step_mean"])
|
| 448 |
+
flow_value = 100 * float(flow["mse_gain_vs_step_mean"])
|
| 449 |
+
raw_error = np.asarray([[
|
| 450 |
+
raw_value - 100 * float(raw["mse_gain_vs_step_ci95_low"])
|
| 451 |
+
], [
|
| 452 |
+
100 * float(raw["mse_gain_vs_step_ci95_high"]) - raw_value
|
| 453 |
+
]])
|
| 454 |
+
flow_error = np.asarray([[
|
| 455 |
+
flow_value - 100 * float(flow["mse_gain_vs_step_ci95_low"])
|
| 456 |
+
], [
|
| 457 |
+
100 * float(flow["mse_gain_vs_step_ci95_high"]) - flow_value
|
| 458 |
+
]])
|
| 459 |
+
x = positions[index]
|
| 460 |
+
ax.bar(
|
| 461 |
+
x - width / 2,
|
| 462 |
+
raw_value,
|
| 463 |
+
width=width,
|
| 464 |
+
facecolor=lighten(color, 0.27),
|
| 465 |
+
edgecolor=color,
|
| 466 |
+
linewidth=0.9,
|
| 467 |
+
hatch="///",
|
| 468 |
+
zorder=3,
|
| 469 |
+
)
|
| 470 |
+
ax.bar(
|
| 471 |
+
x + width / 2,
|
| 472 |
+
flow_value,
|
| 473 |
+
width=width,
|
| 474 |
+
color=color,
|
| 475 |
+
edgecolor="white",
|
| 476 |
+
linewidth=0.7,
|
| 477 |
+
zorder=3,
|
| 478 |
+
)
|
| 479 |
+
ax.errorbar(
|
| 480 |
+
x - width / 2,
|
| 481 |
+
raw_value,
|
| 482 |
+
yerr=raw_error,
|
| 483 |
+
fmt="none",
|
| 484 |
+
ecolor=INK,
|
| 485 |
+
elinewidth=0.85,
|
| 486 |
+
capsize=2.1,
|
| 487 |
+
zorder=4,
|
| 488 |
+
)
|
| 489 |
+
ax.errorbar(
|
| 490 |
+
x + width / 2,
|
| 491 |
+
flow_value,
|
| 492 |
+
yerr=flow_error,
|
| 493 |
+
fmt="none",
|
| 494 |
+
ecolor=INK,
|
| 495 |
+
elinewidth=0.85,
|
| 496 |
+
capsize=2.1,
|
| 497 |
+
zorder=4,
|
| 498 |
+
)
|
| 499 |
+
delta = 100 * float(flow["mse_gain_vs_raw_mean"])
|
| 500 |
+
annotation_y = 100 * float(flow["mse_gain_vs_step_ci95_high"]) + 0.18
|
| 501 |
+
ax.text(
|
| 502 |
+
x + width / 2,
|
| 503 |
+
annotation_y,
|
| 504 |
+
f"+{delta:.2f}%",
|
| 505 |
+
ha="center",
|
| 506 |
+
va="bottom",
|
| 507 |
+
color=color,
|
| 508 |
+
fontsize=7.0,
|
| 509 |
+
fontweight="semibold",
|
| 510 |
+
)
|
| 511 |
+
ax.axhline(0, color=INK, linewidth=0.8)
|
| 512 |
+
ax.axvline(4.0, color=GRID, linewidth=0.9)
|
| 513 |
+
ax.set_xticks(
|
| 514 |
+
positions,
|
| 515 |
+
[
|
| 516 |
+
"Early\nSelf", "Middle\nSelf", "Late\nSelf", "Final\nSelf",
|
| 517 |
+
"Early\nCausal", "Middle\nCausal", "Late\nCausal", "Final\nCausal",
|
| 518 |
+
],
|
| 519 |
+
)
|
| 520 |
+
ax.set_xlim(-0.65, 8.65)
|
| 521 |
+
ax.set_ylim(0, 10.5)
|
| 522 |
+
ax.set_ylabel("MSE reduction vs step-only (%)")
|
| 523 |
+
ax.set_title("Raw and flow-aligned boundaries add predictive value", loc="left", pad=8)
|
| 524 |
+
ax.legend(
|
| 525 |
+
handles=[
|
| 526 |
+
Patch(facecolor=lighten(SELF, 0.27), edgecolor=SELF, hatch="///", label="Self · raw"),
|
| 527 |
+
Patch(facecolor=SELF, edgecolor="white", label="Self · flow-aligned"),
|
| 528 |
+
Patch(facecolor=lighten(CAUSAL, 0.27), edgecolor=CAUSAL, hatch="///", label="Causal · raw"),
|
| 529 |
+
Patch(facecolor=CAUSAL, edgecolor="white", label="Causal · flow-aligned"),
|
| 530 |
+
],
|
| 531 |
+
ncol=4,
|
| 532 |
+
loc="upper left",
|
| 533 |
+
columnspacing=1.0,
|
| 534 |
+
handlelength=1.6,
|
| 535 |
+
)
|
| 536 |
+
polish(ax)
|
| 537 |
+
panel_label(ax, panel)
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def main_figure(args: argparse.Namespace, output: Path) -> None:
|
| 541 |
+
redundancy = read_csv(args.redundancy_csv)
|
| 542 |
+
motion = read_csv(args.motion_bins_csv)
|
| 543 |
+
native = read_csv(args.native_gain_csv)
|
| 544 |
+
sensitivity_applied = apply_causal_late_sensitivity(
|
| 545 |
+
native,
|
| 546 |
+
args.causal_late_sensitivity_csv,
|
| 547 |
+
args.sensitivity_scenario,
|
| 548 |
+
)
|
| 549 |
+
aligned = read_csv(args.aligned_probe_csv)
|
| 550 |
+
unified = read_csv(args.unified_aligned_csv) if args.unified_aligned_csv else None
|
| 551 |
+
fig = plt.figure(figsize=(13.2, 7.35), constrained_layout=True)
|
| 552 |
+
grid = fig.add_gridspec(2, 3, height_ratios=(1.0, 1.07), width_ratios=(1, 1, 1.03))
|
| 553 |
+
ax_a = fig.add_subplot(grid[0, 0])
|
| 554 |
+
ax_b = fig.add_subplot(grid[0, 1], sharey=ax_a)
|
| 555 |
+
ax_c = fig.add_subplot(grid[0, 2])
|
| 556 |
+
if unified is None:
|
| 557 |
+
ax_d = fig.add_subplot(grid[1, :2])
|
| 558 |
+
ax_e = fig.add_subplot(grid[1, 2])
|
| 559 |
+
else:
|
| 560 |
+
ax_d = fig.add_subplot(grid[1, :])
|
| 561 |
+
plot_redundancy(ax_a, redundancy, "self_forcing", "wan14b50", "A")
|
| 562 |
+
plot_redundancy(ax_b, redundancy, "causal_forcing", "ar50", "B")
|
| 563 |
+
ax_b.set_ylabel("")
|
| 564 |
+
ax_b.tick_params(labelleft=False)
|
| 565 |
+
ax_a.legend(
|
| 566 |
+
ncol=1,
|
| 567 |
+
loc="center left",
|
| 568 |
+
bbox_to_anchor=(0.015, 0.50),
|
| 569 |
+
handlelength=2.0,
|
| 570 |
+
labelspacing=0.35,
|
| 571 |
+
frameon=True,
|
| 572 |
+
facecolor="white",
|
| 573 |
+
edgecolor="none",
|
| 574 |
+
framealpha=0.88,
|
| 575 |
+
)
|
| 576 |
+
plot_motion(ax_c, motion, "C")
|
| 577 |
+
if unified is None:
|
| 578 |
+
plot_native_gain(ax_d, native, "D", sensitivity_applied=sensitivity_applied)
|
| 579 |
+
plot_aligned_predictor(ax_e, aligned, "E")
|
| 580 |
+
else:
|
| 581 |
+
plot_unified_combined_gain(ax_d, unified, "D")
|
| 582 |
+
fig.suptitle(
|
| 583 |
+
"Previous-chunk boundaries retain useful information—but spatial alignment matters",
|
| 584 |
+
x=0.012,
|
| 585 |
+
ha="left",
|
| 586 |
+
fontsize=14,
|
| 587 |
+
fontweight="bold",
|
| 588 |
+
color=INK,
|
| 589 |
+
)
|
| 590 |
+
footer = (
|
| 591 |
+
"10 prompts; prompt-first aggregation. Error bars show 95% prompt-bootstrap CI where available. "
|
| 592 |
+
"Boundary = previous chunk's final temporal slot broadcast to all current slots."
|
| 593 |
+
)
|
| 594 |
+
if unified is not None:
|
| 595 |
+
footer += " D: 64-D full grid/common mask; bars vs step-only; labels = flow gain over raw; 9-train/1-test; no exclusions."
|
| 596 |
+
elif sensitivity_applied:
|
| 597 |
+
footer += " † Causal late Ridge excludes prompt 6/step 1 and prompt 9/step 2 only."
|
| 598 |
+
fig.text(
|
| 599 |
+
0.012,
|
| 600 |
+
-0.012,
|
| 601 |
+
footer,
|
| 602 |
+
ha="left",
|
| 603 |
+
fontsize=7.8,
|
| 604 |
+
color=MUTED,
|
| 605 |
+
)
|
| 606 |
+
fig.savefig(output / "self_causal_evidence_overview.png", dpi=240)
|
| 607 |
+
fig.savefig(output / "self_causal_evidence_overview.pdf")
|
| 608 |
+
plt.close(fig)
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
def plot_alignment_controls(ax: plt.Axes, rows: list[dict[str, str]], panel: str) -> None:
|
| 612 |
+
table = lookup_rows(rows, ("model", "action"))
|
| 613 |
+
controls = [
|
| 614 |
+
("global_gain", "Global shift"),
|
| 615 |
+
("negated_gain", "Negated flow"),
|
| 616 |
+
("shuffled_gain", "Shuffled flow"),
|
| 617 |
+
("flow_gain", "Correct flow"),
|
| 618 |
+
]
|
| 619 |
+
y = np.arange(len(controls))
|
| 620 |
+
ax.axhspan(2.55, 3.45, color="#ECFDF5", zorder=-2)
|
| 621 |
+
values_by_family = {}
|
| 622 |
+
for family, offset in (("self_forcing", -0.09), ("causal_forcing", 0.09)):
|
| 623 |
+
row = table[(family, "none")]
|
| 624 |
+
values = [100 * float(row[key]) for key, _ in controls]
|
| 625 |
+
values_by_family[family] = values
|
| 626 |
+
ax.scatter(values, y + offset, s=30, color=MODEL_COLOR[family], edgecolor="white", linewidth=0.6, zorder=3, label=MODEL_LABEL[family])
|
| 627 |
+
for index in range(len(controls)):
|
| 628 |
+
ax.plot(
|
| 629 |
+
[values_by_family["self_forcing"][index], values_by_family["causal_forcing"][index]],
|
| 630 |
+
[y[index] - 0.09, y[index] + 0.09],
|
| 631 |
+
color="#AAB7C8",
|
| 632 |
+
linewidth=1.0,
|
| 633 |
+
zorder=1,
|
| 634 |
+
)
|
| 635 |
+
ax.set_yticks(y, [label for _, label in controls])
|
| 636 |
+
ax.set_xlabel("Cosine recovery over raw (×100)")
|
| 637 |
+
ax.set_xlim(0, 1.82)
|
| 638 |
+
ax.set_title("Interpolation controls", loc="left", pad=8)
|
| 639 |
+
ax.legend(loc="upper left")
|
| 640 |
+
polish(ax, "x")
|
| 641 |
+
panel_label(ax, panel)
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
def plot_predictor_controls(ax: plt.Axes, rows: list[dict[str, str]], panel: str) -> None:
|
| 645 |
+
table = lookup_rows(rows, ("method", "model_family", "layer_role", "probe"))
|
| 646 |
+
controls = [
|
| 647 |
+
("fusion_same", "Correct boundary"),
|
| 648 |
+
("fusion_wrong_step", "Wrong timestep"),
|
| 649 |
+
("fusion_distant", "Distant boundary"),
|
| 650 |
+
("fusion_token_shuffle", "Spatial shuffle"),
|
| 651 |
+
("fusion_batch_shuffle", "Other video"),
|
| 652 |
+
("fusion_zero", "Zero"),
|
| 653 |
+
("fusion_noise", "Matched noise"),
|
| 654 |
+
]
|
| 655 |
+
y = np.arange(len(controls))
|
| 656 |
+
ax.axhspan(-0.43, 0.43, color="#ECFDF5", zorder=-2)
|
| 657 |
+
for family, offset in (("self_forcing", -0.10), ("causal_forcing", 0.10)):
|
| 658 |
+
values, lows, highs = [], [], []
|
| 659 |
+
for probe, _ in controls:
|
| 660 |
+
row = table[("linear", family, "final", probe)]
|
| 661 |
+
value = 100 * float(row["gain_mean"])
|
| 662 |
+
values.append(value)
|
| 663 |
+
lows.append(value - 100 * float(row["gain_ci95_low"]))
|
| 664 |
+
highs.append(100 * float(row["gain_ci95_high"]) - value)
|
| 665 |
+
ax.errorbar(
|
| 666 |
+
values,
|
| 667 |
+
y + offset,
|
| 668 |
+
xerr=np.asarray([lows, highs]),
|
| 669 |
+
fmt="o",
|
| 670 |
+
color=MODEL_COLOR[family],
|
| 671 |
+
markeredgecolor="white",
|
| 672 |
+
markeredgewidth=0.6,
|
| 673 |
+
markersize=5.0,
|
| 674 |
+
elinewidth=1.0,
|
| 675 |
+
capsize=2,
|
| 676 |
+
label=MODEL_LABEL[family],
|
| 677 |
+
zorder=3,
|
| 678 |
+
)
|
| 679 |
+
ax.axvline(0, color=INK, linewidth=0.8)
|
| 680 |
+
ax.set_yticks(y, [label for _, label in controls])
|
| 681 |
+
ax.set_xlabel("Final-layer held-out MSE reduction (%)")
|
| 682 |
+
ax.set_xlim(-0.45, 4.5)
|
| 683 |
+
ax.set_title("Predictor controls", loc="left", pad=8)
|
| 684 |
+
ax.legend(loc="lower right")
|
| 685 |
+
ax.invert_yaxis()
|
| 686 |
+
polish(ax, "x")
|
| 687 |
+
panel_label(ax, panel)
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
def plot_unified_predictor_controls(ax: plt.Axes, rows: list[dict[str, str]], panel: str) -> None:
|
| 691 |
+
table = lookup_rows(rows, ("model", "layer_role", "probe"))
|
| 692 |
+
controls = [
|
| 693 |
+
("both_raw", "Raw boundary"),
|
| 694 |
+
("both_global", "Global shift"),
|
| 695 |
+
("both_negated_flow", "Negated flow"),
|
| 696 |
+
("both_shuffled_flow", "Shuffled flow"),
|
| 697 |
+
("both_flow", "Correct flow"),
|
| 698 |
+
]
|
| 699 |
+
y = np.arange(len(controls))
|
| 700 |
+
ax.axhspan(3.57, 4.43, color="#ECFDF5", zorder=-2)
|
| 701 |
+
for family, offset in (("self_forcing", -0.10), ("causal_forcing", 0.10)):
|
| 702 |
+
values, lows, highs = [], [], []
|
| 703 |
+
for probe, _ in controls:
|
| 704 |
+
row = table[(family, "final", probe)]
|
| 705 |
+
value = 100 * float(row["mse_gain_vs_step_mean"])
|
| 706 |
+
values.append(value)
|
| 707 |
+
lows.append(value - 100 * float(row["mse_gain_vs_step_ci95_low"]))
|
| 708 |
+
highs.append(100 * float(row["mse_gain_vs_step_ci95_high"]) - value)
|
| 709 |
+
ax.errorbar(
|
| 710 |
+
values,
|
| 711 |
+
y + offset,
|
| 712 |
+
xerr=np.asarray([lows, highs]),
|
| 713 |
+
fmt="o",
|
| 714 |
+
color=MODEL_COLOR[family],
|
| 715 |
+
markeredgecolor="white",
|
| 716 |
+
markeredgewidth=0.6,
|
| 717 |
+
markersize=5.0,
|
| 718 |
+
elinewidth=1.0,
|
| 719 |
+
capsize=2,
|
| 720 |
+
label=MODEL_LABEL[family],
|
| 721 |
+
zorder=3,
|
| 722 |
+
)
|
| 723 |
+
ax.axvline(0, color=INK, linewidth=0.8)
|
| 724 |
+
ax.set_yticks(y, [label for _, label in controls])
|
| 725 |
+
ax.set_xlabel("Final-layer MSE reduction vs step-only (%)")
|
| 726 |
+
ax.set_xlim(0, 9.5)
|
| 727 |
+
ax.set_title("Unified predictor controls", loc="left", pad=8)
|
| 728 |
+
ax.legend(loc="upper left")
|
| 729 |
+
ax.invert_yaxis()
|
| 730 |
+
polish(ax, "x")
|
| 731 |
+
panel_label(ax, panel)
|
| 732 |
+
|
| 733 |
+
|
| 734 |
+
def controls_figure(args: argparse.Namespace, output: Path) -> None:
|
| 735 |
+
motion = read_csv(args.motion_summary_csv)
|
| 736 |
+
native = read_csv(args.native_gain_csv)
|
| 737 |
+
unified = read_csv(args.unified_aligned_csv) if args.unified_aligned_csv else None
|
| 738 |
+
fig, axes = plt.subplots(1, 2, figsize=(11.4, 4.25), constrained_layout=True)
|
| 739 |
+
plot_alignment_controls(axes[0], motion, "A")
|
| 740 |
+
if unified is None:
|
| 741 |
+
plot_predictor_controls(axes[1], native, "B")
|
| 742 |
+
else:
|
| 743 |
+
plot_unified_predictor_controls(axes[1], unified, "B")
|
| 744 |
+
fig.suptitle(
|
| 745 |
+
"Control experiments isolate correct spatial correspondence",
|
| 746 |
+
x=0.012,
|
| 747 |
+
ha="left",
|
| 748 |
+
fontsize=13,
|
| 749 |
+
fontweight="bold",
|
| 750 |
+
color=INK,
|
| 751 |
+
)
|
| 752 |
+
fig.text(
|
| 753 |
+
0.012,
|
| 754 |
+
-0.02,
|
| 755 |
+
"Green bands mark correct-flow conditions. Predictor error bars: 95% prompt-bootstrap CI.",
|
| 756 |
+
ha="left",
|
| 757 |
+
fontsize=7.8,
|
| 758 |
+
color=MUTED,
|
| 759 |
+
)
|
| 760 |
+
fig.savefig(output / "self_causal_control_checks.png", dpi=240)
|
| 761 |
+
fig.savefig(output / "self_causal_control_checks.pdf")
|
| 762 |
+
plt.close(fig)
|
| 763 |
+
|
| 764 |
+
|
| 765 |
+
def main() -> None:
|
| 766 |
+
args = parse_args()
|
| 767 |
+
setup_style()
|
| 768 |
+
output = args.output_dir.resolve()
|
| 769 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 770 |
+
main_figure(args, output)
|
| 771 |
+
controls_figure(args, output)
|
| 772 |
+
print(f"[complete] {output}", flush=True)
|
| 773 |
+
|
| 774 |
+
|
| 775 |
+
if __name__ == "__main__":
|
| 776 |
+
main()
|
scripts/prepare_confidence_stage1_vbench.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Prepare the representative Stage-1 confidence-gating VBench conditions."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
DIMENSIONS = [
|
| 13 |
+
"subject_consistency",
|
| 14 |
+
"background_consistency",
|
| 15 |
+
"motion_smoothness",
|
| 16 |
+
"aesthetic_quality",
|
| 17 |
+
"imaging_quality",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def selected_name(root: Path, target: int) -> str:
|
| 22 |
+
value = json.loads((root / "validation/selected.json").read_text())
|
| 23 |
+
matches = [
|
| 24 |
+
row["config_name"]
|
| 25 |
+
for row in value["selected_dynamic"]
|
| 26 |
+
if int(row["target_accepts"]) == target
|
| 27 |
+
]
|
| 28 |
+
if len(matches) != 1:
|
| 29 |
+
raise RuntimeError(f"Expected one target={target} selection under {root}: {matches}")
|
| 30 |
+
return str(matches[0])
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def main() -> None:
|
| 34 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 35 |
+
parser.add_argument("--experiment_root", type=Path, required=True)
|
| 36 |
+
parser.add_argument("--dataset_root", type=Path, required=True)
|
| 37 |
+
args = parser.parse_args()
|
| 38 |
+
root = args.experiment_root.resolve()
|
| 39 |
+
step12 = root / "dynamic_step12"
|
| 40 |
+
step123 = root / "dynamic_step123"
|
| 41 |
+
conditions = {
|
| 42 |
+
"ffff": step123 / "test/videos/ffff",
|
| 43 |
+
"fppf": step12 / "test/videos/fppf",
|
| 44 |
+
"step12_dynamic_k06": step12 / "test/videos" / selected_name(step12, 6),
|
| 45 |
+
"step12_dynamic_k08": step12 / "test/videos" / selected_name(step12, 8),
|
| 46 |
+
"step12_dynamic_k10": step12 / "test/videos" / selected_name(step12, 10),
|
| 47 |
+
"step123_dynamic_k06": step123 / "test/videos" / selected_name(step123, 6),
|
| 48 |
+
"step123_dynamic_k09": step123 / "test/videos" / selected_name(step123, 9),
|
| 49 |
+
"step123_dynamic_k12": step123 / "test/videos" / selected_name(step123, 12),
|
| 50 |
+
"step123_dynamic_k15": step123 / "test/videos" / selected_name(step123, 15),
|
| 51 |
+
}
|
| 52 |
+
output_root = root / "vbench/inputs"
|
| 53 |
+
for condition, source_dir in conditions.items():
|
| 54 |
+
destination = output_root / condition
|
| 55 |
+
destination.mkdir(parents=True, exist_ok=True)
|
| 56 |
+
info = []
|
| 57 |
+
for prompt_id in range(90, 100):
|
| 58 |
+
filename = f"prompt_{prompt_id:04d}.mp4"
|
| 59 |
+
source = (source_dir / filename).resolve()
|
| 60 |
+
if not source.is_file():
|
| 61 |
+
raise FileNotFoundError(source)
|
| 62 |
+
target = destination / filename
|
| 63 |
+
if target.exists() or target.is_symlink():
|
| 64 |
+
target.unlink()
|
| 65 |
+
target.symlink_to(os.path.relpath(source, destination))
|
| 66 |
+
metadata = json.loads(
|
| 67 |
+
(args.dataset_root / f"prompt_{prompt_id:04d}/metadata.json").read_text()
|
| 68 |
+
)
|
| 69 |
+
info.append(
|
| 70 |
+
{
|
| 71 |
+
"video_list": [filename],
|
| 72 |
+
"prompt_en": metadata["prompt"],
|
| 73 |
+
"dimension": DIMENSIONS,
|
| 74 |
+
}
|
| 75 |
+
)
|
| 76 |
+
(destination / "full_info.json").write_text(
|
| 77 |
+
json.dumps(info, indent=2) + "\n", encoding="utf-8"
|
| 78 |
+
)
|
| 79 |
+
print(f"[prepared] {condition}: {source_dir}")
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
main()
|
scripts/prepare_layer17_dynamic_vbench.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Build VBench custom-input directories from dynamic-gate video outputs."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
DIMENSIONS = [
|
| 13 |
+
"subject_consistency",
|
| 14 |
+
"background_consistency",
|
| 15 |
+
"motion_smoothness",
|
| 16 |
+
"aesthetic_quality",
|
| 17 |
+
"imaging_quality",
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def parse_args() -> argparse.Namespace:
|
| 22 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 23 |
+
parser.add_argument("--dataset_root", type=Path, required=True)
|
| 24 |
+
parser.add_argument("--output_root", type=Path, required=True)
|
| 25 |
+
parser.add_argument(
|
| 26 |
+
"--condition",
|
| 27 |
+
action="append",
|
| 28 |
+
nargs=2,
|
| 29 |
+
metavar=("NAME", "VIDEO_DIR"),
|
| 30 |
+
required=True,
|
| 31 |
+
help="Condition name and directory containing prompt_XXXX.mp4 files.",
|
| 32 |
+
)
|
| 33 |
+
parser.add_argument("--prompt_start", type=int, default=90)
|
| 34 |
+
parser.add_argument("--prompt_end", type=int, default=100)
|
| 35 |
+
parser.add_argument(
|
| 36 |
+
"--combined_name",
|
| 37 |
+
default=None,
|
| 38 |
+
help="Also build one flattened input directory for a single shared VBench run.",
|
| 39 |
+
)
|
| 40 |
+
return parser.parse_args()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def main() -> None:
|
| 44 |
+
args = parse_args()
|
| 45 |
+
dataset_root = args.dataset_root.resolve()
|
| 46 |
+
output_root = args.output_root.resolve()
|
| 47 |
+
output_root.mkdir(parents=True, exist_ok=True)
|
| 48 |
+
combined_dir = output_root / args.combined_name if args.combined_name else None
|
| 49 |
+
combined_info = []
|
| 50 |
+
if combined_dir is not None:
|
| 51 |
+
combined_dir.mkdir(parents=True, exist_ok=True)
|
| 52 |
+
|
| 53 |
+
for name, source_text in args.condition:
|
| 54 |
+
source_dir = Path(source_text).resolve()
|
| 55 |
+
destination_dir = output_root / name
|
| 56 |
+
destination_dir.mkdir(parents=True, exist_ok=True)
|
| 57 |
+
full_info = []
|
| 58 |
+
for prompt_id in range(args.prompt_start, args.prompt_end):
|
| 59 |
+
filename = f"prompt_{prompt_id:04d}.mp4"
|
| 60 |
+
source = source_dir / filename
|
| 61 |
+
if not source.is_file():
|
| 62 |
+
raise FileNotFoundError(source)
|
| 63 |
+
destination = destination_dir / filename
|
| 64 |
+
if destination.is_symlink() or destination.exists():
|
| 65 |
+
destination.unlink()
|
| 66 |
+
destination.symlink_to(os.path.relpath(source, destination_dir))
|
| 67 |
+
|
| 68 |
+
metadata_path = dataset_root / f"prompt_{prompt_id:04d}" / "metadata.json"
|
| 69 |
+
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
| 70 |
+
full_info.append(
|
| 71 |
+
{
|
| 72 |
+
"video_list": [filename],
|
| 73 |
+
"prompt_en": metadata["prompt"],
|
| 74 |
+
"dimension": DIMENSIONS,
|
| 75 |
+
}
|
| 76 |
+
)
|
| 77 |
+
if combined_dir is not None:
|
| 78 |
+
combined_filename = f"{name}__{filename}"
|
| 79 |
+
combined_destination = combined_dir / combined_filename
|
| 80 |
+
if combined_destination.is_symlink() or combined_destination.exists():
|
| 81 |
+
combined_destination.unlink()
|
| 82 |
+
combined_destination.symlink_to(
|
| 83 |
+
os.path.relpath(source, combined_dir)
|
| 84 |
+
)
|
| 85 |
+
combined_info.append(
|
| 86 |
+
{
|
| 87 |
+
"video_list": [combined_filename],
|
| 88 |
+
"prompt_en": metadata["prompt"],
|
| 89 |
+
"dimension": DIMENSIONS,
|
| 90 |
+
}
|
| 91 |
+
)
|
| 92 |
+
(destination_dir / "full_info.json").write_text(
|
| 93 |
+
json.dumps(full_info, indent=2) + "\n", encoding="utf-8"
|
| 94 |
+
)
|
| 95 |
+
print(f"[prepared] {name}: {len(full_info)} videos -> {destination_dir}")
|
| 96 |
+
|
| 97 |
+
if combined_dir is not None:
|
| 98 |
+
(combined_dir / "full_info.json").write_text(
|
| 99 |
+
json.dumps(combined_info, indent=2) + "\n", encoding="utf-8"
|
| 100 |
+
)
|
| 101 |
+
print(
|
| 102 |
+
f"[prepared] combined: {len(combined_info)} videos -> {combined_dir}"
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
if __name__ == "__main__":
|
| 107 |
+
main()
|
scripts/probe_confidence_token_batch_size.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Probe one real frozen-Predictor + Confidence-token-head optimizer step."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
import traceback
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _preparse_gpu() -> str:
|
| 17 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 18 |
+
parser.add_argument("--gpu", required=True)
|
| 19 |
+
args, _ = parser.parse_known_args()
|
| 20 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 21 |
+
return str(args.gpu)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
import torch.nn.functional as F
|
| 28 |
+
from safetensors import safe_open
|
| 29 |
+
from safetensors.torch import load_file
|
| 30 |
+
from torch.optim import AdamW
|
| 31 |
+
|
| 32 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 33 |
+
if str(REPO_ROOT) not in sys.path:
|
| 34 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 35 |
+
|
| 36 |
+
from predictor_training.confidence import ConfidenceTokenHead
|
| 37 |
+
from predictor_training.lazy_offline_data import (
|
| 38 |
+
LazyLayer17Dataset,
|
| 39 |
+
collate_lazy_samples,
|
| 40 |
+
)
|
| 41 |
+
from predictor_training.offline_data import TOKENS_PER_CHUNK
|
| 42 |
+
from predictor_training.single_block import (
|
| 43 |
+
SingleBlockPredictor,
|
| 44 |
+
initialize_predictor_block,
|
| 45 |
+
)
|
| 46 |
+
from scripts.run_single_block_init_sweep import frozen_inputs, load_teacher
|
| 47 |
+
from scripts.train_layer17_stage1_lazy_ddp import move_training_batch
|
| 48 |
+
from utils.misc import set_seed
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def parse_args() -> argparse.Namespace:
|
| 52 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 53 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 54 |
+
parser.add_argument("--batch_size", type=int, required=True)
|
| 55 |
+
parser.add_argument("--prompt_start", type=int, default=0)
|
| 56 |
+
parser.add_argument("--chunk", type=int, default=6)
|
| 57 |
+
parser.add_argument("--target_step", type=int, default=3)
|
| 58 |
+
parser.add_argument(
|
| 59 |
+
"--dataset_root",
|
| 60 |
+
type=Path,
|
| 61 |
+
default=REPO_ROOT
|
| 62 |
+
/ "offline_training_datasets"
|
| 63 |
+
/ "predictor_offline_layer17_1000p_21f_seed0_no_chunk0",
|
| 64 |
+
)
|
| 65 |
+
parser.add_argument(
|
| 66 |
+
"--predictor_weights",
|
| 67 |
+
type=Path,
|
| 68 |
+
default=REPO_ROOT
|
| 69 |
+
/ "training_runs"
|
| 70 |
+
/ "layer17_atc_chunk_stage1_1000p_4gpu_b16_2000steps"
|
| 71 |
+
/ "checkpoint_step_2000"
|
| 72 |
+
/ "predictor.safetensors",
|
| 73 |
+
)
|
| 74 |
+
parser.add_argument(
|
| 75 |
+
"--predictor_input_variant",
|
| 76 |
+
choices=("auto", "self_forcing", "disca", "atc"),
|
| 77 |
+
default="auto",
|
| 78 |
+
)
|
| 79 |
+
parser.add_argument(
|
| 80 |
+
"--checkpoint_path",
|
| 81 |
+
type=Path,
|
| 82 |
+
default=REPO_ROOT / "checkpoints/self_forcing_dmd.pt",
|
| 83 |
+
)
|
| 84 |
+
parser.add_argument(
|
| 85 |
+
"--config_path",
|
| 86 |
+
type=Path,
|
| 87 |
+
default=REPO_ROOT / "configs/self_forcing_sid.yaml",
|
| 88 |
+
)
|
| 89 |
+
parser.add_argument("--output", type=Path, required=True)
|
| 90 |
+
parser.add_argument("--learning_rate", type=float, default=3e-4)
|
| 91 |
+
parser.add_argument("--weight_decay", type=float, default=0.01)
|
| 92 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 93 |
+
args = parser.parse_args()
|
| 94 |
+
if args.batch_size < 1:
|
| 95 |
+
parser.error("--batch_size must be positive")
|
| 96 |
+
if args.prompt_start < 0 or args.prompt_start + args.batch_size > 900:
|
| 97 |
+
parser.error("probe prompts must stay in the training split 0..899")
|
| 98 |
+
if not 1 <= args.chunk <= 6:
|
| 99 |
+
parser.error("--chunk must be in 1..6")
|
| 100 |
+
if not 1 <= args.target_step <= 3:
|
| 101 |
+
parser.error("--target_step must be in 1..3")
|
| 102 |
+
for name in (
|
| 103 |
+
"dataset_root",
|
| 104 |
+
"predictor_weights",
|
| 105 |
+
"checkpoint_path",
|
| 106 |
+
"config_path",
|
| 107 |
+
"output",
|
| 108 |
+
):
|
| 109 |
+
path = getattr(args, name).expanduser()
|
| 110 |
+
setattr(
|
| 111 |
+
args,
|
| 112 |
+
name,
|
| 113 |
+
path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve(),
|
| 114 |
+
)
|
| 115 |
+
return args
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def atomic_json(path: Path, value: dict[str, Any]) -> None:
|
| 119 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 120 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 121 |
+
temporary.write_text(
|
| 122 |
+
json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
| 123 |
+
)
|
| 124 |
+
os.replace(temporary, path)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def read_predictor_config(
|
| 128 |
+
path: Path, expected_input_variant: str = "auto"
|
| 129 |
+
) -> dict[str, Any]:
|
| 130 |
+
with safe_open(path, framework="pt", device="cpu") as handle:
|
| 131 |
+
metadata = handle.metadata() or {}
|
| 132 |
+
raw = metadata.get("predictor_config")
|
| 133 |
+
if raw is None:
|
| 134 |
+
if expected_input_variant != "self_forcing":
|
| 135 |
+
raise ValueError(
|
| 136 |
+
f"Missing predictor_config metadata: {path}; legacy concat "
|
| 137 |
+
"checkpoints require --predictor_input_variant self_forcing"
|
| 138 |
+
)
|
| 139 |
+
return {
|
| 140 |
+
"source_layer": 17,
|
| 141 |
+
"input_variant": "self_forcing",
|
| 142 |
+
"gate_mode": "baseline",
|
| 143 |
+
"metadata_source": "explicit_legacy_concat_override",
|
| 144 |
+
}
|
| 145 |
+
config = json.loads(raw)
|
| 146 |
+
actual = str(config.get("input_variant", "self_forcing"))
|
| 147 |
+
if expected_input_variant != "auto" and actual != expected_input_variant:
|
| 148 |
+
raise ValueError(
|
| 149 |
+
f"Predictor input variant mismatch: expected={expected_input_variant} "
|
| 150 |
+
f"actual={actual}"
|
| 151 |
+
)
|
| 152 |
+
return config
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def load_predictor(
|
| 156 |
+
teacher: torch.nn.Module,
|
| 157 |
+
weights: Path,
|
| 158 |
+
device: torch.device,
|
| 159 |
+
expected_input_variant: str = "auto",
|
| 160 |
+
) -> tuple[SingleBlockPredictor, dict[str, Any]]:
|
| 161 |
+
config = read_predictor_config(weights, expected_input_variant)
|
| 162 |
+
source_layer = int(config.get("source_layer", 17))
|
| 163 |
+
predictor = SingleBlockPredictor(
|
| 164 |
+
block=initialize_predictor_block(
|
| 165 |
+
teacher.blocks[source_layer], "teacher_full"
|
| 166 |
+
),
|
| 167 |
+
dim=teacher.dim,
|
| 168 |
+
gradient_checkpointing=False,
|
| 169 |
+
input_variant=str(config.get("input_variant", "self_forcing")),
|
| 170 |
+
atc_previous_scope=config.get("atc_previous_scope", "chunk"),
|
| 171 |
+
atc_freq_dim=int(config.get("atc_freq_dim", 256)),
|
| 172 |
+
atc_mlp_hidden_dim=int(config.get("atc_mlp_hidden_dim", 3072)),
|
| 173 |
+
atc_gate_hidden_dim=int(config.get("atc_gate_hidden_dim", 512)),
|
| 174 |
+
atc_transport_residual_scale=float(
|
| 175 |
+
config.get("atc_transport_residual_scale", 0.1)
|
| 176 |
+
),
|
| 177 |
+
atc_gate_initial_probability=float(
|
| 178 |
+
config.get("atc_gate_initial_probability", 0.3)
|
| 179 |
+
),
|
| 180 |
+
atc_collect_diagnostics=False,
|
| 181 |
+
)
|
| 182 |
+
predictor.load_state_dict(load_file(str(weights), device="cpu"), strict=True)
|
| 183 |
+
predictor.to(device=device, dtype=torch.bfloat16)
|
| 184 |
+
predictor.eval().requires_grad_(False)
|
| 185 |
+
return predictor, config
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@torch.no_grad()
|
| 189 |
+
def extract_predictor_features(
|
| 190 |
+
predictor: SingleBlockPredictor,
|
| 191 |
+
batch: dict[str, Any],
|
| 192 |
+
teacher: torch.nn.Module,
|
| 193 |
+
device: torch.device,
|
| 194 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 195 |
+
frozen = frozen_inputs(batch, teacher, device)
|
| 196 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 197 |
+
output = predictor(
|
| 198 |
+
current_tokens=frozen["current_tokens"],
|
| 199 |
+
anchor_hidden=batch["anchor_hidden"],
|
| 200 |
+
previous_hidden=batch["previous_hidden"],
|
| 201 |
+
timestep_modulation=frozen["timestep_modulation"],
|
| 202 |
+
grid_sizes=frozen["grid_sizes"],
|
| 203 |
+
freqs=frozen["freqs"],
|
| 204 |
+
history_k=batch["history_k"],
|
| 205 |
+
history_v=batch["history_v"],
|
| 206 |
+
cross_k=batch["cross_k"],
|
| 207 |
+
cross_v=batch["cross_v"],
|
| 208 |
+
current_start=batch["chunk"] * TOKENS_PER_CHUNK,
|
| 209 |
+
return_features=True,
|
| 210 |
+
condition_tokens=frozen["condition_tokens"],
|
| 211 |
+
anchor_distance=batch["anchor_distance"],
|
| 212 |
+
)
|
| 213 |
+
if not isinstance(output, tuple):
|
| 214 |
+
raise RuntimeError("Predictor did not return (pred_hidden, transformed)")
|
| 215 |
+
return output
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def hidden_nrmse(predicted: torch.Tensor, target: torch.Tensor) -> torch.Tensor:
|
| 219 |
+
error_energy = (predicted.float() - target.float()).square().sum(dim=(1, 2))
|
| 220 |
+
target_energy = target.float().square().sum(dim=(1, 2))
|
| 221 |
+
return torch.sqrt(error_energy / target_energy.clamp_min(1e-8))
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def memory_gib(value: int) -> float:
|
| 225 |
+
return value / 2**30
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def main() -> None:
|
| 229 |
+
args = parse_args()
|
| 230 |
+
started = time.perf_counter()
|
| 231 |
+
result: dict[str, Any] = {
|
| 232 |
+
"success": False,
|
| 233 |
+
"physical_gpu": str(args.gpu),
|
| 234 |
+
"batch_size": args.batch_size,
|
| 235 |
+
"prompt_ids": [args.prompt_start, args.prompt_start + args.batch_size - 1],
|
| 236 |
+
"split": {"train": "0..899", "validation": "900..999"},
|
| 237 |
+
"chunk": args.chunk,
|
| 238 |
+
"target_step": args.target_step,
|
| 239 |
+
"predictor_weights": str(args.predictor_weights),
|
| 240 |
+
}
|
| 241 |
+
try:
|
| 242 |
+
torch.cuda.set_device(0)
|
| 243 |
+
device = torch.device("cuda", 0)
|
| 244 |
+
set_seed(args.seed)
|
| 245 |
+
torch.set_num_threads(4)
|
| 246 |
+
torch.set_num_interop_threads(1)
|
| 247 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 248 |
+
torch.set_float32_matmul_precision("high")
|
| 249 |
+
|
| 250 |
+
print(f"[probe] gpu={args.gpu} batch={args.batch_size} loading teacher", flush=True)
|
| 251 |
+
teacher = load_teacher(args.checkpoint_path, args.config_path, device)
|
| 252 |
+
predictor, predictor_config = load_predictor(
|
| 253 |
+
teacher,
|
| 254 |
+
args.predictor_weights,
|
| 255 |
+
device,
|
| 256 |
+
expected_input_variant=args.predictor_input_variant,
|
| 257 |
+
)
|
| 258 |
+
result["predictor_config"] = predictor_config
|
| 259 |
+
head = ConfidenceTokenHead(num_steps=3, dropout=0.1).to(device=device)
|
| 260 |
+
result["head_parameters"] = sum(
|
| 261 |
+
parameter.numel() for parameter in head.parameters()
|
| 262 |
+
)
|
| 263 |
+
optimizer = AdamW(
|
| 264 |
+
head.parameters(),
|
| 265 |
+
lr=args.learning_rate,
|
| 266 |
+
betas=(0.9, 0.95),
|
| 267 |
+
weight_decay=args.weight_decay,
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
print(f"[probe] gpu={args.gpu} batch={args.batch_size} loading samples", flush=True)
|
| 271 |
+
dataset = LazyLayer17Dataset(args.dataset_root, layer_id=17)
|
| 272 |
+
cpu_batch = collate_lazy_samples(
|
| 273 |
+
[
|
| 274 |
+
dataset[(prompt_id, args.chunk, args.target_step)]
|
| 275 |
+
for prompt_id in range(
|
| 276 |
+
args.prompt_start, args.prompt_start + args.batch_size
|
| 277 |
+
)
|
| 278 |
+
]
|
| 279 |
+
)
|
| 280 |
+
torch.cuda.reset_peak_memory_stats()
|
| 281 |
+
batch = move_training_batch(cpu_batch, teacher, device)
|
| 282 |
+
del cpu_batch
|
| 283 |
+
torch.cuda.synchronize()
|
| 284 |
+
feature_started = time.perf_counter()
|
| 285 |
+
pred_hidden, transformed = extract_predictor_features(
|
| 286 |
+
predictor, batch, teacher, device
|
| 287 |
+
)
|
| 288 |
+
target_log = torch.log(
|
| 289 |
+
hidden_nrmse(pred_hidden, batch["target_hidden"]) + 1e-6
|
| 290 |
+
).detach()
|
| 291 |
+
torch.cuda.synchronize()
|
| 292 |
+
result["predictor_forward_s"] = time.perf_counter() - feature_started
|
| 293 |
+
|
| 294 |
+
chunk_position = torch.full(
|
| 295 |
+
(args.batch_size,),
|
| 296 |
+
(args.chunk - 1) / 5.0,
|
| 297 |
+
dtype=torch.float32,
|
| 298 |
+
device=device,
|
| 299 |
+
)
|
| 300 |
+
step_id = torch.full(
|
| 301 |
+
(args.batch_size,),
|
| 302 |
+
args.target_step,
|
| 303 |
+
dtype=torch.long,
|
| 304 |
+
device=device,
|
| 305 |
+
)
|
| 306 |
+
head_started = time.perf_counter()
|
| 307 |
+
optimizer.zero_grad(set_to_none=True)
|
| 308 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 309 |
+
predicted_log = head(
|
| 310 |
+
transformed_hidden=transformed,
|
| 311 |
+
pred_hidden=pred_hidden,
|
| 312 |
+
anchor_hidden=batch["anchor_hidden"],
|
| 313 |
+
chunk_position=chunk_position,
|
| 314 |
+
step_id=step_id,
|
| 315 |
+
)
|
| 316 |
+
loss = F.smooth_l1_loss(predicted_log, target_log)
|
| 317 |
+
loss.backward()
|
| 318 |
+
grad_norm = torch.nn.utils.clip_grad_norm_(head.parameters(), 1.0)
|
| 319 |
+
optimizer.step()
|
| 320 |
+
torch.cuda.synchronize()
|
| 321 |
+
|
| 322 |
+
free_bytes, total_bytes = torch.cuda.mem_get_info()
|
| 323 |
+
result.update(
|
| 324 |
+
success=True,
|
| 325 |
+
loss=float(loss.detach()),
|
| 326 |
+
grad_norm=float(grad_norm),
|
| 327 |
+
head_step_s=time.perf_counter() - head_started,
|
| 328 |
+
peak_allocated_gib=memory_gib(torch.cuda.max_memory_allocated()),
|
| 329 |
+
peak_reserved_gib=memory_gib(torch.cuda.max_memory_reserved()),
|
| 330 |
+
final_free_gib=memory_gib(free_bytes),
|
| 331 |
+
gpu_total_gib=memory_gib(total_bytes),
|
| 332 |
+
elapsed_s=time.perf_counter() - started,
|
| 333 |
+
)
|
| 334 |
+
print(
|
| 335 |
+
f"[probe] PASS gpu={args.gpu} batch={args.batch_size} "
|
| 336 |
+
f"peak={result['peak_allocated_gib']:.2f}GiB "
|
| 337 |
+
f"reserved={result['peak_reserved_gib']:.2f}GiB",
|
| 338 |
+
flush=True,
|
| 339 |
+
)
|
| 340 |
+
except torch.cuda.OutOfMemoryError as error:
|
| 341 |
+
result.update(
|
| 342 |
+
error_type="CUDAOutOfMemoryError",
|
| 343 |
+
error=str(error),
|
| 344 |
+
peak_allocated_gib=memory_gib(torch.cuda.max_memory_allocated()),
|
| 345 |
+
peak_reserved_gib=memory_gib(torch.cuda.max_memory_reserved()),
|
| 346 |
+
elapsed_s=time.perf_counter() - started,
|
| 347 |
+
)
|
| 348 |
+
print(f"[probe] OOM gpu={args.gpu} batch={args.batch_size}: {error}", flush=True)
|
| 349 |
+
except Exception as error:
|
| 350 |
+
result.update(
|
| 351 |
+
error_type=type(error).__name__,
|
| 352 |
+
error=str(error),
|
| 353 |
+
traceback=traceback.format_exc(),
|
| 354 |
+
elapsed_s=time.perf_counter() - started,
|
| 355 |
+
)
|
| 356 |
+
atomic_json(args.output, result)
|
| 357 |
+
raise
|
| 358 |
+
atomic_json(args.output, result)
|
| 359 |
+
if not result["success"]:
|
| 360 |
+
raise SystemExit(2)
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
if __name__ == "__main__":
|
| 364 |
+
main()
|
scripts/run_aligned_conditional_probe_3models.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Oracle flow-aligned conditional Ridge probes on three AR4 backbones.
|
| 3 |
+
|
| 4 |
+
The target is the current chunk/current denoising-step full-grid feature. The
|
| 5 |
+
first input is the current chunk/previous-step feature. The second input is
|
| 6 |
+
the previous chunk/same-step boundary map, either raw or warped by global,
|
| 7 |
+
correct, negated, or spatially shuffled target-to-source flow.
|
| 8 |
+
|
| 9 |
+
This is an oracle diagnostic because the flow is computed from the generated
|
| 10 |
+
current RGB frame. It tests whether alignment makes the previous-chunk route
|
| 11 |
+
more predictive; it is not an inference-time implementation.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import csv
|
| 18 |
+
import json
|
| 19 |
+
import os
|
| 20 |
+
from collections import defaultdict
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Any
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def preparse_gpu() -> str:
|
| 26 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 27 |
+
parser.add_argument("--gpu", default="0")
|
| 28 |
+
args, _ = parser.parse_known_args()
|
| 29 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 30 |
+
return str(args.gpu)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
PHYSICAL_GPU = preparse_gpu()
|
| 34 |
+
|
| 35 |
+
import numpy as np
|
| 36 |
+
import torch
|
| 37 |
+
import torch.nn.functional as F
|
| 38 |
+
|
| 39 |
+
from analyze_fullgrid_bilinear_3models import (
|
| 40 |
+
GridRun,
|
| 41 |
+
farneback,
|
| 42 |
+
load_causal_runs,
|
| 43 |
+
load_hy_runs,
|
| 44 |
+
load_self_runs,
|
| 45 |
+
resize_flow,
|
| 46 |
+
shuffled_flow,
|
| 47 |
+
warp,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
PROBES = (
|
| 52 |
+
"step_only",
|
| 53 |
+
"both_raw",
|
| 54 |
+
"both_global",
|
| 55 |
+
"both_flow",
|
| 56 |
+
"both_negated_flow",
|
| 57 |
+
"both_shuffled_flow",
|
| 58 |
+
)
|
| 59 |
+
LAYER_ROLES = {7: "early", 14: "middle", 22: "late", 29: "final"}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def parse_args() -> argparse.Namespace:
|
| 63 |
+
parser = argparse.ArgumentParser()
|
| 64 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 65 |
+
parser.add_argument("--self_root", type=Path, required=True)
|
| 66 |
+
parser.add_argument("--causal_root", type=Path, required=True)
|
| 67 |
+
parser.add_argument("--hy_root", type=Path)
|
| 68 |
+
parser.add_argument("--hy_cache_root", type=Path)
|
| 69 |
+
parser.add_argument("--output_root", type=Path, required=True)
|
| 70 |
+
parser.add_argument("--projection_dim", type=int, default=64)
|
| 71 |
+
parser.add_argument("--ridge", type=float, default=1e-4)
|
| 72 |
+
parser.add_argument("--seed", type=int, default=20260828)
|
| 73 |
+
parser.add_argument(
|
| 74 |
+
"--multilayer_self_causal",
|
| 75 |
+
action="store_true",
|
| 76 |
+
help="Use four-layer projected full grids for Self/Causal and skip HY.",
|
| 77 |
+
)
|
| 78 |
+
return parser.parse_args()
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
|
| 82 |
+
if not rows:
|
| 83 |
+
return
|
| 84 |
+
fields: list[str] = []
|
| 85 |
+
for row in rows:
|
| 86 |
+
for key in row:
|
| 87 |
+
if key not in fields:
|
| 88 |
+
fields.append(key)
|
| 89 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 90 |
+
with path.open("w", newline="", encoding="utf-8") as handle:
|
| 91 |
+
writer = csv.DictWriter(handle, fieldnames=fields)
|
| 92 |
+
writer.writeheader()
|
| 93 |
+
writer.writerows(rows)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def load_multilayer_self_runs(root: Path) -> dict[str, list[GridRun]]:
|
| 97 |
+
result = {role: [] for role in LAYER_ROLES.values()}
|
| 98 |
+
for path in sorted((root / "runs").glob("prompt_*.pt")):
|
| 99 |
+
state = torch.load(path, map_location="cpu", weights_only=False)
|
| 100 |
+
projected = state.get("projected_by_layer", {})
|
| 101 |
+
if not projected:
|
| 102 |
+
raise ValueError(f"No multilayer projected features in {path}")
|
| 103 |
+
anchors = np.load(path.with_suffix(".anchors.npz"), allow_pickle=False)["frames"]
|
| 104 |
+
by_layer: dict[int, dict[tuple[int, int], torch.Tensor]] = defaultdict(dict)
|
| 105 |
+
for key, tensor in projected.items():
|
| 106 |
+
layer, chunk, step = (int(value) for value in key.split(":"))
|
| 107 |
+
by_layer[layer][(chunk, step)] = tensor.float()
|
| 108 |
+
for layer, role in LAYER_ROLES.items():
|
| 109 |
+
if layer not in by_layer:
|
| 110 |
+
raise ValueError(f"Missing layer {layer} in {path}")
|
| 111 |
+
result[role].append(
|
| 112 |
+
GridRun(
|
| 113 |
+
"self_forcing",
|
| 114 |
+
"none",
|
| 115 |
+
int(state.get("run_index", len(result[role]))),
|
| 116 |
+
anchors,
|
| 117 |
+
int(state["num_frame_per_block"]),
|
| 118 |
+
by_layer[layer],
|
| 119 |
+
path,
|
| 120 |
+
)
|
| 121 |
+
)
|
| 122 |
+
return result
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def load_multilayer_causal_runs(root: Path) -> dict[str, list[GridRun]]:
|
| 126 |
+
result = {role: [] for role in LAYER_ROLES.values()}
|
| 127 |
+
for run_dir in sorted((root / "runs").glob("prompt_*")):
|
| 128 |
+
path = run_dir / "feature_snapshots.pt"
|
| 129 |
+
anchor_path = run_dir / "rgb_anchor_frames.npz"
|
| 130 |
+
if not path.exists() or not anchor_path.exists():
|
| 131 |
+
continue
|
| 132 |
+
state = torch.load(path, map_location="cpu", weights_only=False)
|
| 133 |
+
projected = state.get("projected", {})
|
| 134 |
+
if not projected:
|
| 135 |
+
raise ValueError(f"No projected features in {path}")
|
| 136 |
+
anchors = np.load(anchor_path, allow_pickle=False)["frames"]
|
| 137 |
+
by_layer: dict[int, dict[tuple[int, int], torch.Tensor]] = defaultdict(dict)
|
| 138 |
+
for key, tensor in projected.items():
|
| 139 |
+
layer, chunk, step = (int(value) for value in key.split(":"))
|
| 140 |
+
by_layer[layer][(chunk, step)] = tensor.float()
|
| 141 |
+
for layer, role in LAYER_ROLES.items():
|
| 142 |
+
if layer not in by_layer:
|
| 143 |
+
raise ValueError(f"Missing layer {layer} in {path}")
|
| 144 |
+
result[role].append(
|
| 145 |
+
GridRun(
|
| 146 |
+
"causal_forcing",
|
| 147 |
+
"none",
|
| 148 |
+
int(state["prompt_id"]),
|
| 149 |
+
anchors,
|
| 150 |
+
3,
|
| 151 |
+
by_layer[layer],
|
| 152 |
+
path,
|
| 153 |
+
)
|
| 154 |
+
)
|
| 155 |
+
return result
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def columns(name: str, data: dict[str, torch.Tensor]) -> list[torch.Tensor]:
|
| 159 |
+
ones = torch.ones_like(data["step"])
|
| 160 |
+
mapping = {
|
| 161 |
+
"step_only": [data["step"], ones],
|
| 162 |
+
"both_raw": [data["step"], data["raw"], ones],
|
| 163 |
+
"both_global": [data["step"], data["global"], ones],
|
| 164 |
+
"both_flow": [data["step"], data["flow"], ones],
|
| 165 |
+
"both_negated_flow": [data["step"], data["negated"], ones],
|
| 166 |
+
"both_shuffled_flow": [data["step"], data["shuffled"], ones],
|
| 167 |
+
}
|
| 168 |
+
return mapping[name]
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def collect_prompt_step(run: GridRun, target_step: int) -> dict[str, torch.Tensor]:
|
| 172 |
+
collected: dict[str, list[torch.Tensor]] = defaultdict(list)
|
| 173 |
+
for chunk in range(1, run.chunks):
|
| 174 |
+
source_frame_index = chunk * run.chunk_size - 1
|
| 175 |
+
source_frame = run.anchors[source_frame_index]
|
| 176 |
+
source_map = run.features[(chunk - 1, target_step)][-1].float()
|
| 177 |
+
target_maps = run.features[(chunk, target_step)].float()
|
| 178 |
+
step_maps = run.features[(chunk, target_step - 1)].float()
|
| 179 |
+
for slot in range(run.chunk_size):
|
| 180 |
+
target_frame = run.anchors[chunk * run.chunk_size + slot]
|
| 181 |
+
flow = resize_flow(farneback(target_frame, source_frame))
|
| 182 |
+
global_flow = torch.zeros_like(flow)
|
| 183 |
+
global_flow[0].fill_(float(torch.median(flow[0])))
|
| 184 |
+
global_flow[1].fill_(float(torch.median(flow[1])))
|
| 185 |
+
control_flows = {
|
| 186 |
+
"global": global_flow,
|
| 187 |
+
"flow": flow,
|
| 188 |
+
"negated": -flow,
|
| 189 |
+
"shuffled": shuffled_flow(
|
| 190 |
+
flow,
|
| 191 |
+
seed=(run.prompt_id + 1) * 100000
|
| 192 |
+
+ chunk * 1000
|
| 193 |
+
+ slot * 10
|
| 194 |
+
+ target_step,
|
| 195 |
+
),
|
| 196 |
+
}
|
| 197 |
+
aligned: dict[str, torch.Tensor] = {}
|
| 198 |
+
masks: list[torch.Tensor] = []
|
| 199 |
+
for name, control_flow in control_flows.items():
|
| 200 |
+
aligned[name], mask = warp(source_map, control_flow)
|
| 201 |
+
masks.append(mask)
|
| 202 |
+
common_mask = torch.stack(masks).all(dim=0)
|
| 203 |
+
if not bool(common_mask.any()):
|
| 204 |
+
continue
|
| 205 |
+
collected["target"].append(target_maps[slot][common_mask])
|
| 206 |
+
collected["step"].append(step_maps[slot][common_mask])
|
| 207 |
+
collected["raw"].append(source_map[common_mask])
|
| 208 |
+
for name in control_flows:
|
| 209 |
+
collected[name].append(aligned[name][common_mask])
|
| 210 |
+
result = {key: torch.cat(values, dim=0).contiguous() for key, values in collected.items()}
|
| 211 |
+
expected = {"target", "step", "raw", "global", "flow", "negated", "shuffled"}
|
| 212 |
+
if set(result) != expected:
|
| 213 |
+
raise ValueError(f"Incomplete aligned sample for {run.source}: {set(result)}")
|
| 214 |
+
return result
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def ridge_sufficient_statistics(
|
| 218 |
+
data: dict[str, torch.Tensor],
|
| 219 |
+
probe: str,
|
| 220 |
+
device: torch.device,
|
| 221 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 222 |
+
design = torch.stack(columns(probe, data), dim=-1).to(device=device, dtype=torch.float64)
|
| 223 |
+
target = data["target"].to(device=device, dtype=torch.float64)
|
| 224 |
+
gram = torch.einsum("ndp,ndq->dpq", design, design)
|
| 225 |
+
rhs = torch.einsum("ndp,nd->dp", design, target)
|
| 226 |
+
return gram, rhs
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def solve_ridge(
|
| 230 |
+
gram: torch.Tensor,
|
| 231 |
+
rhs: torch.Tensor,
|
| 232 |
+
ridge: float,
|
| 233 |
+
) -> torch.Tensor:
|
| 234 |
+
parameter_count = gram.shape[-1]
|
| 235 |
+
device = gram.device
|
| 236 |
+
scale = gram.diagonal(dim1=-2, dim2=-1).mean(dim=-1).clamp_min(1e-8)
|
| 237 |
+
regularizer = torch.eye(parameter_count, dtype=torch.float64, device=device)[None]
|
| 238 |
+
regularizer = regularizer * (float(ridge) * scale[:, None, None])
|
| 239 |
+
regularizer[:, -1, -1] = 0.0
|
| 240 |
+
try:
|
| 241 |
+
weights = torch.linalg.solve(gram + regularizer, rhs.unsqueeze(-1)).squeeze(-1)
|
| 242 |
+
except torch.linalg.LinAlgError:
|
| 243 |
+
weights = (torch.linalg.pinv(gram + regularizer) @ rhs.unsqueeze(-1)).squeeze(-1)
|
| 244 |
+
return weights.float()
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def evaluate(
|
| 248 |
+
data: dict[str, torch.Tensor],
|
| 249 |
+
probe: str,
|
| 250 |
+
weights: torch.Tensor,
|
| 251 |
+
device: torch.device,
|
| 252 |
+
) -> dict[str, float]:
|
| 253 |
+
design = torch.stack(columns(probe, data), dim=-1).to(device=device, dtype=torch.float32)
|
| 254 |
+
target = data["target"].to(device=device, dtype=torch.float32)
|
| 255 |
+
prediction = torch.einsum("ndp,dp->nd", design, weights)
|
| 256 |
+
error = prediction - target
|
| 257 |
+
mse = error.square().mean()
|
| 258 |
+
variance = (target - target.mean()).square().mean().clamp_min(1e-12)
|
| 259 |
+
nmse = mse / variance
|
| 260 |
+
cosine = F.cosine_similarity(prediction, target, dim=-1, eps=1e-8).mean()
|
| 261 |
+
return {
|
| 262 |
+
"mse": float(mse),
|
| 263 |
+
"nMSE": float(nmse),
|
| 264 |
+
"nRMSE": float(torch.sqrt(nmse)),
|
| 265 |
+
"r2": float(1.0 - nmse),
|
| 266 |
+
"cosine": float(cosine),
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
def bootstrap(values: list[float], seed: int, rounds: int = 10000) -> tuple[float, float, float]:
|
| 271 |
+
array = np.asarray(values, dtype=np.float64)
|
| 272 |
+
generator = np.random.default_rng(seed)
|
| 273 |
+
indices = generator.integers(0, len(array), size=(rounds, len(array)))
|
| 274 |
+
means = array[indices].mean(axis=1)
|
| 275 |
+
return float(array.mean()), float(np.quantile(means, 0.025)), float(np.quantile(means, 0.975))
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def summarize(rows: list[dict[str, Any]], seed: int) -> list[dict[str, Any]]:
|
| 279 |
+
groups: dict[tuple[str, str, str], list[dict[str, Any]]] = defaultdict(list)
|
| 280 |
+
for row in rows:
|
| 281 |
+
groups[(row["model"], row["layer_role"], row["probe"])].append(row)
|
| 282 |
+
output = []
|
| 283 |
+
for (model, role, probe), selected in sorted(groups.items()):
|
| 284 |
+
by_prompt: dict[int, list[dict[str, Any]]] = defaultdict(list)
|
| 285 |
+
for row in selected:
|
| 286 |
+
by_prompt[int(row["held_out_prompt"])].append(row)
|
| 287 |
+
prompt_rows = []
|
| 288 |
+
for prompt_id, values in sorted(by_prompt.items()):
|
| 289 |
+
item = {"prompt_id": prompt_id}
|
| 290 |
+
for metric in ("mse", "nMSE", "nRMSE", "r2", "cosine"):
|
| 291 |
+
item[metric] = float(np.mean([float(row[metric]) for row in values]))
|
| 292 |
+
prompt_rows.append(item)
|
| 293 |
+
item: dict[str, Any] = {
|
| 294 |
+
"model": model,
|
| 295 |
+
"layer_role": role,
|
| 296 |
+
"probe": probe,
|
| 297 |
+
"prompt_count": len(prompt_rows),
|
| 298 |
+
"fold_count": len(selected),
|
| 299 |
+
}
|
| 300 |
+
for metric in ("mse", "nMSE", "nRMSE", "r2", "cosine"):
|
| 301 |
+
stable = seed + sum(map(ord, model + role + probe + metric))
|
| 302 |
+
avg, low, high = bootstrap([row[metric] for row in prompt_rows], stable)
|
| 303 |
+
item[f"{metric}_mean"] = avg
|
| 304 |
+
item[f"{metric}_ci95_low"] = low
|
| 305 |
+
item[f"{metric}_ci95_high"] = high
|
| 306 |
+
|
| 307 |
+
output.append(item)
|
| 308 |
+
|
| 309 |
+
prompt_metric: dict[tuple[str, str, str, int], dict[str, float]] = {}
|
| 310 |
+
grouped: dict[tuple[str, str, str, int], list[dict[str, Any]]] = defaultdict(list)
|
| 311 |
+
for row in rows:
|
| 312 |
+
grouped[
|
| 313 |
+
(row["model"], row["layer_role"], row["probe"], int(row["held_out_prompt"]))
|
| 314 |
+
].append(row)
|
| 315 |
+
for key, values in grouped.items():
|
| 316 |
+
prompt_metric[key] = {
|
| 317 |
+
metric: float(np.mean([float(row[metric]) for row in values]))
|
| 318 |
+
for metric in ("mse", "nMSE", "nRMSE", "r2", "cosine")
|
| 319 |
+
}
|
| 320 |
+
for item in output:
|
| 321 |
+
model, role, probe = item["model"], item["layer_role"], item["probe"]
|
| 322 |
+
if probe == "step_only":
|
| 323 |
+
continue
|
| 324 |
+
gain_step, gain_raw = [], []
|
| 325 |
+
prompt_ids = sorted(
|
| 326 |
+
prompt_id
|
| 327 |
+
for candidate_model, candidate_role, candidate_probe, prompt_id in prompt_metric
|
| 328 |
+
if candidate_model == model and candidate_role == role and candidate_probe == probe
|
| 329 |
+
)
|
| 330 |
+
for prompt_id in prompt_ids:
|
| 331 |
+
current = prompt_metric[(model, role, probe, prompt_id)]["mse"]
|
| 332 |
+
step = prompt_metric[(model, role, "step_only", prompt_id)]["mse"]
|
| 333 |
+
raw = prompt_metric[(model, role, "both_raw", prompt_id)]["mse"]
|
| 334 |
+
gain_step.append((step - current) / max(step, 1e-12))
|
| 335 |
+
gain_raw.append((raw - current) / max(raw, 1e-12))
|
| 336 |
+
avg, low, high = bootstrap(
|
| 337 |
+
gain_step, seed + 300000 + sum(map(ord, model + role + probe))
|
| 338 |
+
)
|
| 339 |
+
item.update({
|
| 340 |
+
"mse_gain_vs_step_mean": avg,
|
| 341 |
+
"mse_gain_vs_step_ci95_low": low,
|
| 342 |
+
"mse_gain_vs_step_ci95_high": high,
|
| 343 |
+
"mse_gain_vs_step_wins": int(sum(value > 0 for value in gain_step)),
|
| 344 |
+
})
|
| 345 |
+
avg, low, high = bootstrap(
|
| 346 |
+
gain_raw, seed + 600000 + sum(map(ord, model + role + probe))
|
| 347 |
+
)
|
| 348 |
+
item.update({
|
| 349 |
+
"mse_gain_vs_raw_mean": avg,
|
| 350 |
+
"mse_gain_vs_raw_ci95_low": low,
|
| 351 |
+
"mse_gain_vs_raw_ci95_high": high,
|
| 352 |
+
"mse_gain_vs_raw_wins": int(sum(value > 0 for value in gain_raw)),
|
| 353 |
+
})
|
| 354 |
+
if probe == "both_flow":
|
| 355 |
+
for baseline_probe, label in (
|
| 356 |
+
("both_global", "global"),
|
| 357 |
+
("both_negated_flow", "negated_flow"),
|
| 358 |
+
("both_shuffled_flow", "shuffled_flow"),
|
| 359 |
+
):
|
| 360 |
+
gains = []
|
| 361 |
+
for prompt_id in prompt_ids:
|
| 362 |
+
current = prompt_metric[(model, role, probe, prompt_id)]["mse"]
|
| 363 |
+
baseline = prompt_metric[(model, role, baseline_probe, prompt_id)]["mse"]
|
| 364 |
+
gains.append((baseline - current) / max(baseline, 1e-12))
|
| 365 |
+
avg, low, high = bootstrap(
|
| 366 |
+
gains,
|
| 367 |
+
seed + 900000 + sum(map(ord, model + role + baseline_probe)),
|
| 368 |
+
)
|
| 369 |
+
item.update({
|
| 370 |
+
f"mse_gain_vs_{label}_mean": avg,
|
| 371 |
+
f"mse_gain_vs_{label}_ci95_low": low,
|
| 372 |
+
f"mse_gain_vs_{label}_ci95_high": high,
|
| 373 |
+
f"mse_gain_vs_{label}_wins": int(sum(value > 0 for value in gains)),
|
| 374 |
+
})
|
| 375 |
+
return output
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
def main() -> None:
|
| 379 |
+
args = parse_args()
|
| 380 |
+
output = args.output_root.resolve()
|
| 381 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 382 |
+
device = torch.device("cuda:0")
|
| 383 |
+
if not torch.cuda.is_available():
|
| 384 |
+
raise RuntimeError("CUDA is required for this experiment")
|
| 385 |
+
|
| 386 |
+
if args.multilayer_self_causal:
|
| 387 |
+
model_runs = {
|
| 388 |
+
"self_forcing": load_multilayer_self_runs(args.self_root.resolve()),
|
| 389 |
+
"causal_forcing": load_multilayer_causal_runs(args.causal_root.resolve()),
|
| 390 |
+
}
|
| 391 |
+
else:
|
| 392 |
+
if args.hy_root is None or args.hy_cache_root is None:
|
| 393 |
+
raise ValueError("--hy_root and --hy_cache_root are required without multilayer mode")
|
| 394 |
+
model_runs = {
|
| 395 |
+
"self_forcing": {"final": load_self_runs(args.self_root.resolve())},
|
| 396 |
+
"causal_forcing": {"final": load_causal_runs(args.causal_root.resolve())},
|
| 397 |
+
"hy_static": {
|
| 398 |
+
"final": load_hy_runs(
|
| 399 |
+
args.hy_root.resolve(),
|
| 400 |
+
"static",
|
| 401 |
+
args.hy_cache_root.resolve(),
|
| 402 |
+
args.projection_dim,
|
| 403 |
+
device,
|
| 404 |
+
False,
|
| 405 |
+
)
|
| 406 |
+
},
|
| 407 |
+
}
|
| 408 |
+
fold_rows: list[dict[str, Any]] = []
|
| 409 |
+
for model, role_runs in model_runs.items():
|
| 410 |
+
for role, runs in role_runs.items():
|
| 411 |
+
if len(runs) != 10:
|
| 412 |
+
raise ValueError(f"Expected 10 runs for {model}/{role}, found {len(runs)}")
|
| 413 |
+
for target_step in range(1, 4):
|
| 414 |
+
prepared = [collect_prompt_step(run, target_step) for run in runs]
|
| 415 |
+
token_counts = [int(data["target"].shape[0]) for data in prepared]
|
| 416 |
+
print(
|
| 417 |
+
f"[prepare] {model}/{role} step={target_step} tokens={token_counts}",
|
| 418 |
+
flush=True,
|
| 419 |
+
)
|
| 420 |
+
statistics = {
|
| 421 |
+
probe: [ridge_sufficient_statistics(data, probe, device) for data in prepared]
|
| 422 |
+
for probe in PROBES
|
| 423 |
+
}
|
| 424 |
+
for held_out in range(10):
|
| 425 |
+
test = prepared[held_out]
|
| 426 |
+
for probe in PROBES:
|
| 427 |
+
grams, right_sides = zip(*statistics[probe])
|
| 428 |
+
train_gram = torch.stack(grams).sum(dim=0) - grams[held_out]
|
| 429 |
+
train_rhs = torch.stack(right_sides).sum(dim=0) - right_sides[held_out]
|
| 430 |
+
weights = solve_ridge(train_gram, train_rhs, args.ridge)
|
| 431 |
+
values = evaluate(test, probe, weights, device)
|
| 432 |
+
fold_rows.append({
|
| 433 |
+
"model": model,
|
| 434 |
+
"layer_role": role,
|
| 435 |
+
"target_step": target_step,
|
| 436 |
+
"held_out_prompt": held_out,
|
| 437 |
+
"train_prompts": 9,
|
| 438 |
+
"test_tokens": int(test["target"].shape[0]),
|
| 439 |
+
"probe": probe,
|
| 440 |
+
**values,
|
| 441 |
+
})
|
| 442 |
+
print(
|
| 443 |
+
f"[fold] {model}/{role} step={target_step} heldout={held_out}",
|
| 444 |
+
flush=True,
|
| 445 |
+
)
|
| 446 |
+
del prepared
|
| 447 |
+
torch.cuda.empty_cache()
|
| 448 |
+
|
| 449 |
+
summary = summarize(fold_rows, args.seed)
|
| 450 |
+
write_csv(output / "aligned_probe_folds.csv", fold_rows)
|
| 451 |
+
write_csv(output / "aligned_probe_summary.csv", summary)
|
| 452 |
+
summary_lookup = {
|
| 453 |
+
(row["model"], row["layer_role"], row["probe"]): row for row in summary
|
| 454 |
+
}
|
| 455 |
+
report = [
|
| 456 |
+
"# Oracle flow-aligned conditional Ridge probe",
|
| 457 |
+
"",
|
| 458 |
+
"All gains are prompt-wise relative MSE reductions averaged over 10 held-out prompts and three target denoising steps. Flow is computed from the generated current RGB frame and is therefore an oracle diagnostic.",
|
| 459 |
+
"",
|
| 460 |
+
"| model | layer | raw chunk vs step-only | flow-aligned vs step-only | flow-aligned vs raw chunk | flow-aligned vs shuffled flow | flow-vs-raw wins |",
|
| 461 |
+
"|---|---|---:|---:|---:|---:|---:|",
|
| 462 |
+
]
|
| 463 |
+
for model, role_runs in model_runs.items():
|
| 464 |
+
for role in role_runs:
|
| 465 |
+
raw = summary_lookup[(model, role, "both_raw")]
|
| 466 |
+
flow = summary_lookup[(model, role, "both_flow")]
|
| 467 |
+
report.append(
|
| 468 |
+
f"| {model} | {role} | {100 * raw['mse_gain_vs_step_mean']:.2f}% | "
|
| 469 |
+
f"{100 * flow['mse_gain_vs_step_mean']:.2f}% | "
|
| 470 |
+
f"{100 * flow['mse_gain_vs_raw_mean']:.2f}% | "
|
| 471 |
+
f"{100 * flow['mse_gain_vs_shuffled_flow_mean']:.2f}% | "
|
| 472 |
+
f"{flow['mse_gain_vs_raw_wins']}/10 |"
|
| 473 |
+
)
|
| 474 |
+
report.extend([
|
| 475 |
+
"",
|
| 476 |
+
"Self-Forcing and Causal-Forcing are evaluated at early, middle, late, and final layers under one identical feature space, mask, split, and Ridge capacity.",
|
| 477 |
+
"",
|
| 478 |
+
"The experiment uses the common intersection of in-bounds masks for every warp, so all predictor variants see identical target tokens. Full-grid features are fixed 64-D signed random projections; conclusions concern within-model paired gains rather than native-space or cross-model absolute errors.",
|
| 479 |
+
])
|
| 480 |
+
(output / "REPORT.md").write_text("\n".join(report) + "\n", encoding="utf-8")
|
| 481 |
+
config = {
|
| 482 |
+
"gpu": str(args.gpu),
|
| 483 |
+
"models": list(model_runs),
|
| 484 |
+
"layer_roles": {model: list(role_runs) for model, role_runs in model_runs.items()},
|
| 485 |
+
"prompt_count": 10,
|
| 486 |
+
"target_steps": [1, 2, 3],
|
| 487 |
+
"probes": list(PROBES),
|
| 488 |
+
"ridge": args.ridge,
|
| 489 |
+
"projection_dim": args.projection_dim,
|
| 490 |
+
"grid": [30, 52],
|
| 491 |
+
"split": "leave-one-prompt-out (9 train, 1 test)",
|
| 492 |
+
"support": "intersection of in-bounds masks for global/correct/negated/shuffled warps",
|
| 493 |
+
"flow": "Farneback target RGB to previous-chunk boundary RGB; oracle diagnostic",
|
| 494 |
+
"row_count": len(fold_rows),
|
| 495 |
+
}
|
| 496 |
+
(output / "config.json").write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
| 497 |
+
print(f"[complete] {output} rows={len(fold_rows)}", flush=True)
|
| 498 |
+
|
| 499 |
+
|
| 500 |
+
if __name__ == "__main__":
|
| 501 |
+
main()
|
scripts/run_conditional_probe_offline.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run grouped, channel-wise Linear/Ridge conditional probes offline.
|
| 3 |
+
|
| 4 |
+
The input is the normalized per-prompt dataset produced by
|
| 5 |
+
``build_conditional_probe_dataset.py``. All controls are derived in memory from
|
| 6 |
+
the same canonical features, so no model forward is repeated and every probe
|
| 7 |
+
uses identical target tokens.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import csv
|
| 14 |
+
import json
|
| 15 |
+
import math
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
from typing import Any
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _preparse_gpu() -> str:
|
| 23 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 24 |
+
parser.add_argument("--gpu", default="0")
|
| 25 |
+
args, _ = parser.parse_known_args()
|
| 26 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 27 |
+
return str(args.gpu)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
_preparse_gpu()
|
| 31 |
+
|
| 32 |
+
import numpy as np
|
| 33 |
+
import torch
|
| 34 |
+
import torch.nn.functional as F
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
FAMILIES = ("self_forcing", "causal_forcing", "hy_worldplay")
|
| 38 |
+
ROLES = ("early", "middle", "late", "final")
|
| 39 |
+
LAYER_INDICES = {
|
| 40 |
+
"self_forcing": {"early": 7, "middle": 14, "late": 22, "final": 29},
|
| 41 |
+
"causal_forcing": {"early": 7, "middle": 14, "late": 22, "final": 29},
|
| 42 |
+
"hy_worldplay": {"early": 13, "middle": 26, "late": 40, "final": 53},
|
| 43 |
+
}
|
| 44 |
+
PROBES = (
|
| 45 |
+
"within_affine",
|
| 46 |
+
"within_quadratic",
|
| 47 |
+
"cross_affine",
|
| 48 |
+
"fusion_same",
|
| 49 |
+
"fusion_step_duplicate",
|
| 50 |
+
"fusion_distant",
|
| 51 |
+
"fusion_wrong_step",
|
| 52 |
+
"fusion_token_shuffle",
|
| 53 |
+
"fusion_batch_shuffle",
|
| 54 |
+
"fusion_zero",
|
| 55 |
+
"fusion_noise",
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
|
| 60 |
+
if not rows:
|
| 61 |
+
return
|
| 62 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 63 |
+
fields: list[str] = []
|
| 64 |
+
for row in rows:
|
| 65 |
+
for key in row:
|
| 66 |
+
if key not in fields:
|
| 67 |
+
fields.append(key)
|
| 68 |
+
with path.open("w", newline="", encoding="utf-8") as handle:
|
| 69 |
+
writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore")
|
| 70 |
+
writer.writeheader()
|
| 71 |
+
writer.writerows(rows)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def parse_args() -> argparse.Namespace:
|
| 75 |
+
parser = argparse.ArgumentParser()
|
| 76 |
+
parser.add_argument("--dataset_root", type=Path, required=True)
|
| 77 |
+
parser.add_argument("--output_dir", type=Path, required=True)
|
| 78 |
+
parser.add_argument("--num_prompts", type=int, default=10)
|
| 79 |
+
parser.add_argument("--chunks", type=int, default=4)
|
| 80 |
+
parser.add_argument("--steps", type=int, default=4)
|
| 81 |
+
parser.add_argument("--ridge", type=float, default=1e-4)
|
| 82 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 83 |
+
parser.add_argument(
|
| 84 |
+
"--chunk_pairing",
|
| 85 |
+
choices=("matched_slot", "boundary_to_all"),
|
| 86 |
+
default="matched_slot",
|
| 87 |
+
help=(
|
| 88 |
+
"How the previous-chunk auxiliary feature is paired with the current chunk. "
|
| 89 |
+
"boundary_to_all broadcasts the previous chunk's final temporal slot to "
|
| 90 |
+
"all current temporal slots at matched spatial coordinates."
|
| 91 |
+
),
|
| 92 |
+
)
|
| 93 |
+
return parser.parse_args()
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def load_family(root: Path, family: str, count: int) -> list[dict[str, Any]]:
|
| 97 |
+
runs = []
|
| 98 |
+
for prompt_id in range(count):
|
| 99 |
+
pt_path = root / family / f"prompt_{prompt_id:04d}.pt"
|
| 100 |
+
npz_path = root / family / f"prompt_{prompt_id:04d}.npz"
|
| 101 |
+
path = npz_path if family == "hy_worldplay" and not pt_path.exists() else pt_path
|
| 102 |
+
if not path.exists():
|
| 103 |
+
raise FileNotFoundError(path)
|
| 104 |
+
if path.suffix == ".npz":
|
| 105 |
+
data = np.load(path, allow_pickle=False)
|
| 106 |
+
raw: dict[int, dict[tuple[int, int], torch.Tensor]] = {}
|
| 107 |
+
for index, stage_value in enumerate(data["stages"]):
|
| 108 |
+
stage = str(stage_value)
|
| 109 |
+
if not stage.startswith("block_"):
|
| 110 |
+
continue
|
| 111 |
+
layer = int(stage.split("_")[-1])
|
| 112 |
+
chunk = int(data["chunks"][index])
|
| 113 |
+
step = int(data["steps"][index])
|
| 114 |
+
raw.setdefault(layer, {})[(chunk, step)] = torch.from_numpy(data["features"][index])
|
| 115 |
+
layer_roles = {13: "early", 26: "middle", 40: "late", 53: "final"}
|
| 116 |
+
features = {}
|
| 117 |
+
for layer, role in layer_roles.items():
|
| 118 |
+
rows = []
|
| 119 |
+
for chunk in range(4):
|
| 120 |
+
rows.append(torch.stack([raw[layer][(chunk, step)] for step in range(4)], dim=0))
|
| 121 |
+
features[role] = torch.stack(rows, dim=0).contiguous()
|
| 122 |
+
run = {
|
| 123 |
+
"prompt_id": prompt_id,
|
| 124 |
+
"features": features,
|
| 125 |
+
"timesteps": np.asarray(data["timesteps"], dtype=np.float32),
|
| 126 |
+
"coords": np.asarray(data["coords"], dtype=np.int64),
|
| 127 |
+
"grid_shape": np.asarray(data["grid_shape"], dtype=np.int64),
|
| 128 |
+
}
|
| 129 |
+
else:
|
| 130 |
+
run = torch.load(path, map_location="cpu", weights_only=False)
|
| 131 |
+
if int(run.get("prompt_id", prompt_id)) != prompt_id:
|
| 132 |
+
raise ValueError(f"Prompt id mismatch in {path}")
|
| 133 |
+
runs.append(run)
|
| 134 |
+
return runs
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def boundary_to_all(reference: torch.Tensor, run: dict[str, Any]) -> torch.Tensor:
|
| 138 |
+
"""Broadcast the last temporal slot while preserving target token order."""
|
| 139 |
+
coords = np.asarray(run.get("coords"))
|
| 140 |
+
if coords.ndim != 2 or coords.shape[1] != 3 or len(coords) != reference.shape[0]:
|
| 141 |
+
raise ValueError(
|
| 142 |
+
"boundary_to_all requires one (temporal,y,x) coordinate per feature token; "
|
| 143 |
+
f"coords={coords.shape}, features={tuple(reference.shape)}"
|
| 144 |
+
)
|
| 145 |
+
slots = sorted(int(value) for value in np.unique(coords[:, 0]))
|
| 146 |
+
if not slots:
|
| 147 |
+
raise ValueError("No temporal slots in coordinates")
|
| 148 |
+
last_mask = coords[:, 0] == slots[-1]
|
| 149 |
+
source_coords = coords[last_mask, 1:]
|
| 150 |
+
source = reference[torch.from_numpy(last_mask)]
|
| 151 |
+
result = torch.empty_like(reference)
|
| 152 |
+
for slot in slots:
|
| 153 |
+
target_mask = coords[:, 0] == slot
|
| 154 |
+
target_coords = coords[target_mask, 1:]
|
| 155 |
+
if not np.array_equal(target_coords, source_coords):
|
| 156 |
+
raise ValueError(
|
| 157 |
+
f"Temporal slot {slot} does not share the boundary slot's spatial grid"
|
| 158 |
+
)
|
| 159 |
+
result[torch.from_numpy(target_mask)] = source
|
| 160 |
+
return result
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def samples(
|
| 164 |
+
run: dict[str, Any],
|
| 165 |
+
role: str,
|
| 166 |
+
chunks: int,
|
| 167 |
+
step: int,
|
| 168 |
+
chunk_pairing: str = "matched_slot",
|
| 169 |
+
) -> dict[str, torch.Tensor]:
|
| 170 |
+
values = run["features"][role].float()
|
| 171 |
+
if values.ndim != 4:
|
| 172 |
+
raise ValueError(f"Expected [chunk,step,token,channel], got {values.shape}")
|
| 173 |
+
if values.shape[0] < chunks or values.shape[1] < 4:
|
| 174 |
+
raise ValueError(f"Insufficient feature grid for {role}: {values.shape}")
|
| 175 |
+
targets, within, cross, distant, wrong = [], [], [], [], []
|
| 176 |
+
# c>=2 is required for the distant-chunk control. Pool c=2 and c=3 for
|
| 177 |
+
# the common four-chunk protocol, while retaining chunk_id downstream.
|
| 178 |
+
for chunk in range(2, chunks):
|
| 179 |
+
targets.append(values[chunk, step])
|
| 180 |
+
within.append(values[chunk, step - 1])
|
| 181 |
+
if chunk_pairing == "boundary_to_all":
|
| 182 |
+
cross.append(boundary_to_all(values[chunk - 1, step], run))
|
| 183 |
+
distant.append(boundary_to_all(values[chunk - 2, step], run))
|
| 184 |
+
wrong.append(boundary_to_all(values[chunk - 1, step - 1], run))
|
| 185 |
+
else:
|
| 186 |
+
cross.append(values[chunk - 1, step])
|
| 187 |
+
distant.append(values[chunk - 2, step])
|
| 188 |
+
wrong.append(values[chunk - 1, step - 1])
|
| 189 |
+
return {
|
| 190 |
+
"target": torch.cat(targets, dim=0),
|
| 191 |
+
"within": torch.cat(within, dim=0),
|
| 192 |
+
"cross": torch.cat(cross, dim=0),
|
| 193 |
+
"distant": torch.cat(distant, dim=0),
|
| 194 |
+
"wrong": torch.cat(wrong, dim=0),
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def token_shuffle(value: torch.Tensor, spatial_count: int) -> torch.Tensor:
|
| 199 |
+
# Roll spatial tokens independently inside every chunk/temporal slice,
|
| 200 |
+
# preserving the marginal feature distribution while breaking coordinate
|
| 201 |
+
# correspondence. This supports both 3-slot Self/Causal and 4-slot HY.
|
| 202 |
+
if spatial_count <= 0 or value.shape[0] % spatial_count:
|
| 203 |
+
return value.roll(shifts=max(1, value.shape[0] // 2), dims=0)
|
| 204 |
+
frames = value.reshape(-1, spatial_count, value.shape[1])
|
| 205 |
+
return frames.roll(shifts=1, dims=1).reshape_as(value)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def noise_like(value: torch.Tensor, seed: int) -> torch.Tensor:
|
| 209 |
+
generator = torch.Generator(device="cpu").manual_seed(int(seed))
|
| 210 |
+
noise = torch.randn(value.shape, generator=generator, dtype=value.dtype)
|
| 211 |
+
return noise * value.std(dim=0, keepdim=True).clamp_min(1e-6) + value.mean(dim=0, keepdim=True)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def columns(
|
| 215 |
+
name: str,
|
| 216 |
+
data: dict[str, torch.Tensor],
|
| 217 |
+
batch: torch.Tensor,
|
| 218 |
+
seed: int,
|
| 219 |
+
spatial_count: int,
|
| 220 |
+
):
|
| 221 |
+
within, cross = data["within"], data["cross"]
|
| 222 |
+
ones = torch.ones_like(within)
|
| 223 |
+
mapping = {
|
| 224 |
+
"within_affine": [within, ones],
|
| 225 |
+
"within_quadratic": [within, within.square(), ones],
|
| 226 |
+
"cross_affine": [cross, ones],
|
| 227 |
+
"fusion_same": [within, cross, ones],
|
| 228 |
+
"fusion_step_duplicate": [within, within, ones],
|
| 229 |
+
"fusion_distant": [within, data["distant"], ones],
|
| 230 |
+
"fusion_wrong_step": [within, data["wrong"], ones],
|
| 231 |
+
"fusion_token_shuffle": [within, token_shuffle(cross, spatial_count), ones],
|
| 232 |
+
"fusion_batch_shuffle": [within, batch, ones],
|
| 233 |
+
"fusion_zero": [within, torch.zeros_like(cross), ones],
|
| 234 |
+
"fusion_noise": [within, noise_like(cross, seed), ones],
|
| 235 |
+
}
|
| 236 |
+
return mapping[name]
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def fit_ridge(features: list[torch.Tensor], target: torch.Tensor, ridge: float) -> torch.Tensor:
|
| 240 |
+
design = torch.stack(features, dim=-1).double() # [N,D,P]
|
| 241 |
+
y = target.double()
|
| 242 |
+
gram = torch.einsum("ndp,ndq->dpq", design, design)
|
| 243 |
+
rhs = torch.einsum("ndp,nd->dp", design, y)
|
| 244 |
+
p = gram.shape[-1]
|
| 245 |
+
scale = gram.diagonal(dim1=-2, dim2=-1).mean(dim=-1).clamp_min(1e-8)
|
| 246 |
+
reg = torch.eye(p, dtype=gram.dtype).unsqueeze(0) * (float(ridge) * scale[:, None, None])
|
| 247 |
+
# The last column is the explicit bias and is not regularized.
|
| 248 |
+
reg[:, -1, -1] = 0.0
|
| 249 |
+
try:
|
| 250 |
+
return torch.linalg.solve(gram + reg, rhs.unsqueeze(-1)).squeeze(-1).float()
|
| 251 |
+
except torch.linalg.LinAlgError:
|
| 252 |
+
return (torch.linalg.pinv(gram + reg) @ rhs.unsqueeze(-1)).squeeze(-1).float()
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def predict(features: list[torch.Tensor], weights: torch.Tensor) -> torch.Tensor:
|
| 256 |
+
return torch.einsum("ndp,dp->nd", torch.stack(features, dim=-1).float(), weights)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def metrics(pred: torch.Tensor, target: torch.Tensor) -> dict[str, float]:
|
| 260 |
+
pred, target = pred.float(), target.float()
|
| 261 |
+
error = pred - target
|
| 262 |
+
mse = error.square().mean()
|
| 263 |
+
centered = target - target.mean()
|
| 264 |
+
variance = centered.square().mean().clamp_min(1e-12)
|
| 265 |
+
nmse = mse / variance
|
| 266 |
+
cosine = F.cosine_similarity(pred.reshape(1, -1), target.reshape(1, -1), dim=1, eps=1e-8)[0]
|
| 267 |
+
return {
|
| 268 |
+
"mse": float(mse),
|
| 269 |
+
"nMSE": float(nmse),
|
| 270 |
+
"nRMSE": float(torch.sqrt(nmse)),
|
| 271 |
+
"r2": float(1.0 - nmse),
|
| 272 |
+
"cosine": float(cosine),
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
def bootstrap(values: list[float], seed: int, rounds: int = 4000):
|
| 277 |
+
values = np.asarray(values, dtype=np.float64)
|
| 278 |
+
rng = np.random.default_rng(seed)
|
| 279 |
+
if values.size == 0:
|
| 280 |
+
return float("nan"), float("nan"), float("nan")
|
| 281 |
+
draws = rng.integers(0, values.size, size=(rounds, values.size))
|
| 282 |
+
means = values[draws].mean(axis=1)
|
| 283 |
+
return float(values.mean()), float(np.quantile(means, 0.025)), float(np.quantile(means, 0.975))
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def main() -> None:
|
| 287 |
+
args = parse_args()
|
| 288 |
+
args.dataset_root = args.dataset_root.resolve()
|
| 289 |
+
args.output_dir = args.output_dir.resolve()
|
| 290 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 291 |
+
all_rows: list[dict[str, Any]] = []
|
| 292 |
+
config = {
|
| 293 |
+
"dataset_root": str(args.dataset_root),
|
| 294 |
+
"num_prompts": args.num_prompts,
|
| 295 |
+
"chunks": args.chunks,
|
| 296 |
+
"steps": args.steps,
|
| 297 |
+
"target_chunks": list(range(2, args.chunks)),
|
| 298 |
+
"ridge": args.ridge,
|
| 299 |
+
"chunk_pairing": args.chunk_pairing,
|
| 300 |
+
"outer_split": "test prompt p; donor (p+1)%N also excluded from training",
|
| 301 |
+
"probes": list(PROBES),
|
| 302 |
+
}
|
| 303 |
+
for family in FAMILIES:
|
| 304 |
+
print(f"[load] {family}", flush=True)
|
| 305 |
+
runs = load_family(args.dataset_root, family, args.num_prompts)
|
| 306 |
+
for layer_index, role in enumerate(ROLES):
|
| 307 |
+
coords = np.asarray(runs[0].get("coords"))
|
| 308 |
+
slots = sorted(int(value) for value in np.unique(coords[:, 0]))
|
| 309 |
+
if not slots or len(coords) != runs[0]["features"][role].shape[2]:
|
| 310 |
+
raise ValueError(
|
| 311 |
+
f"Invalid temporal coordinates for {family}/{role}: "
|
| 312 |
+
f"coords={coords.shape}, tokens={runs[0]['features'][role].shape[2]}"
|
| 313 |
+
)
|
| 314 |
+
spatial_count = int((coords[:, 0] == slots[0]).sum())
|
| 315 |
+
for step in range(1, args.steps):
|
| 316 |
+
prepared = [
|
| 317 |
+
samples(run, role, args.chunks, step, args.chunk_pairing)
|
| 318 |
+
for run in runs
|
| 319 |
+
]
|
| 320 |
+
for held_out in range(args.num_prompts):
|
| 321 |
+
donor = (held_out + 1) % args.num_prompts
|
| 322 |
+
train_ids = [i for i in range(args.num_prompts) if i not in {held_out, donor}]
|
| 323 |
+
train_data = {key: torch.cat([prepared[i][key] for i in train_ids], dim=0) for key in prepared[0]}
|
| 324 |
+
test_data = prepared[held_out]
|
| 325 |
+
donor_data = prepared[donor]
|
| 326 |
+
train_batch = torch.cat(
|
| 327 |
+
[prepared[train_ids[(position + 1) % len(train_ids)]]["cross"]
|
| 328 |
+
for position in range(len(train_ids))],
|
| 329 |
+
dim=0,
|
| 330 |
+
)
|
| 331 |
+
for probe in PROBES:
|
| 332 |
+
train_cols = columns(
|
| 333 |
+
probe,
|
| 334 |
+
train_data,
|
| 335 |
+
train_batch,
|
| 336 |
+
seed=args.seed + held_out * 100 + step,
|
| 337 |
+
spatial_count=spatial_count,
|
| 338 |
+
)
|
| 339 |
+
test_cols = columns(
|
| 340 |
+
probe,
|
| 341 |
+
test_data,
|
| 342 |
+
donor_data["cross"],
|
| 343 |
+
seed=args.seed + 10000 + held_out * 100 + step,
|
| 344 |
+
spatial_count=spatial_count,
|
| 345 |
+
)
|
| 346 |
+
weights = fit_ridge(train_cols, train_data["target"], args.ridge)
|
| 347 |
+
pred = predict(test_cols, weights)
|
| 348 |
+
row = {
|
| 349 |
+
"model_family": family,
|
| 350 |
+
"layer_role": role,
|
| 351 |
+
"layer_index": LAYER_INDICES[family][role],
|
| 352 |
+
"target_step": step,
|
| 353 |
+
"held_out_prompt": held_out,
|
| 354 |
+
"other_video_prompt": donor,
|
| 355 |
+
"train_prompts": len(train_ids),
|
| 356 |
+
"test_tokens": int(test_data["target"].shape[0]),
|
| 357 |
+
"probe": probe,
|
| 358 |
+
**metrics(pred, test_data["target"]),
|
| 359 |
+
}
|
| 360 |
+
all_rows.append(row)
|
| 361 |
+
if held_out % 2 == 0:
|
| 362 |
+
print(
|
| 363 |
+
f"[progress] {family} {role} step={step} heldout={held_out}",
|
| 364 |
+
flush=True,
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
write_csv(args.output_dir / "linear_probe_folds.csv", all_rows)
|
| 368 |
+
summary_rows = []
|
| 369 |
+
for family in FAMILIES:
|
| 370 |
+
for role in ROLES:
|
| 371 |
+
for step in range(1, args.steps):
|
| 372 |
+
for probe in PROBES:
|
| 373 |
+
selected = [
|
| 374 |
+
row for row in all_rows
|
| 375 |
+
if row["model_family"] == family
|
| 376 |
+
and row["layer_role"] == role
|
| 377 |
+
and row["target_step"] == step
|
| 378 |
+
and row["probe"] == probe
|
| 379 |
+
]
|
| 380 |
+
if not selected:
|
| 381 |
+
continue
|
| 382 |
+
item = {
|
| 383 |
+
"model_family": family,
|
| 384 |
+
"layer_role": role,
|
| 385 |
+
"target_step": step,
|
| 386 |
+
"probe": probe,
|
| 387 |
+
"prompt_count": len(selected),
|
| 388 |
+
}
|
| 389 |
+
for metric in ("mse", "nMSE", "nRMSE", "r2", "cosine"):
|
| 390 |
+
stable_seed = (
|
| 391 |
+
args.seed
|
| 392 |
+
+ 100000 * FAMILIES.index(family)
|
| 393 |
+
+ 10000 * ROLES.index(role)
|
| 394 |
+
+ 100 * int(step)
|
| 395 |
+
+ sum(ord(ch) for ch in probe)
|
| 396 |
+
+ sum(ord(ch) for ch in metric)
|
| 397 |
+
)
|
| 398 |
+
mean, low, high = bootstrap(
|
| 399 |
+
[float(row[metric]) for row in selected],
|
| 400 |
+
stable_seed,
|
| 401 |
+
)
|
| 402 |
+
item[f"{metric}_mean"] = mean
|
| 403 |
+
item[f"{metric}_ci95_low"] = low
|
| 404 |
+
item[f"{metric}_ci95_high"] = high
|
| 405 |
+
baseline = [
|
| 406 |
+
row for row in all_rows
|
| 407 |
+
if row["model_family"] == family
|
| 408 |
+
and row["layer_role"] == role
|
| 409 |
+
and row["target_step"] == step
|
| 410 |
+
and row["probe"] == "within_affine"
|
| 411 |
+
]
|
| 412 |
+
if baseline:
|
| 413 |
+
gains = [
|
| 414 |
+
(float(base["mse"]) - float(cur["mse"])) / max(float(base["mse"]), 1e-12)
|
| 415 |
+
for base, cur in zip(
|
| 416 |
+
sorted(baseline, key=lambda row: row["held_out_prompt"]),
|
| 417 |
+
sorted(selected, key=lambda row: row["held_out_prompt"]),
|
| 418 |
+
)
|
| 419 |
+
]
|
| 420 |
+
mean, low, high = bootstrap(gains, args.seed + 700000 + step)
|
| 421 |
+
item.update({
|
| 422 |
+
"gain_vs_within_affine_mean": mean,
|
| 423 |
+
"gain_vs_within_affine_ci95_low": low,
|
| 424 |
+
"gain_vs_within_affine_ci95_high": high,
|
| 425 |
+
"gain_vs_within_affine_wins": sum(value > 0 for value in gains),
|
| 426 |
+
})
|
| 427 |
+
summary_rows.append(item)
|
| 428 |
+
write_csv(args.output_dir / "linear_probe_summary.csv", summary_rows)
|
| 429 |
+
config["fold_rows"] = len(all_rows)
|
| 430 |
+
config["summary_rows"] = len(summary_rows)
|
| 431 |
+
(args.output_dir / "config.json").write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
| 432 |
+
print(f"[complete] {args.output_dir} rows={len(all_rows)}", flush=True)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
if __name__ == "__main__":
|
| 436 |
+
main()
|
scripts/run_single_block_init_sweep.py
ADDED
|
@@ -0,0 +1,1082 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Train and compare single-block Self-Forcing Predictor initializations."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
import hashlib
|
| 9 |
+
import json
|
| 10 |
+
import math
|
| 11 |
+
import os
|
| 12 |
+
import random
|
| 13 |
+
import sys
|
| 14 |
+
import time
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from typing import Any
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _preparse_gpu() -> str:
|
| 20 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 21 |
+
parser.add_argument("--gpu", default="2")
|
| 22 |
+
args, _ = parser.parse_known_args()
|
| 23 |
+
# Respect torchrun's inherited multi-GPU visibility. Standalone invocations
|
| 24 |
+
# keep the historical GPU-2 default, while an explicit --gpu still wins.
|
| 25 |
+
gpu_was_explicit = any(
|
| 26 |
+
value == "--gpu" or value.startswith("--gpu=") for value in sys.argv[1:]
|
| 27 |
+
)
|
| 28 |
+
if gpu_was_explicit or "CUDA_VISIBLE_DEVICES" not in os.environ:
|
| 29 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 30 |
+
return str(args.gpu)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 34 |
+
|
| 35 |
+
import torch
|
| 36 |
+
import torch.nn.functional as F
|
| 37 |
+
from omegaconf import OmegaConf
|
| 38 |
+
from safetensors.torch import save_file
|
| 39 |
+
from torch.optim import AdamW
|
| 40 |
+
|
| 41 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 42 |
+
if str(REPO_ROOT) not in sys.path:
|
| 43 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 44 |
+
|
| 45 |
+
from predictor_training.offline_data import (
|
| 46 |
+
OfflinePredictorStore,
|
| 47 |
+
TOKENS_PER_CHUNK,
|
| 48 |
+
)
|
| 49 |
+
from predictor_training.single_block import (
|
| 50 |
+
SingleBlockPredictor,
|
| 51 |
+
TripleFeatureFusion,
|
| 52 |
+
initialize_predictor_block,
|
| 53 |
+
)
|
| 54 |
+
from utils.misc import set_seed
|
| 55 |
+
from utils.wan_wrapper import WanDiffusionWrapper
|
| 56 |
+
from wan.modules.model import sinusoidal_embedding_1d
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
OTHER_METHODS = (
|
| 60 |
+
"random_full",
|
| 61 |
+
"teacher_identity",
|
| 62 |
+
"random_identity",
|
| 63 |
+
"full_zero",
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def parse_args() -> argparse.Namespace:
|
| 68 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 69 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 70 |
+
parser.add_argument(
|
| 71 |
+
"--dataset_root",
|
| 72 |
+
type=Path,
|
| 73 |
+
default=Path("outputs/predictor_offline_100_all_blocks"),
|
| 74 |
+
)
|
| 75 |
+
parser.add_argument(
|
| 76 |
+
"--checkpoint_path",
|
| 77 |
+
type=Path,
|
| 78 |
+
default=Path("checkpoints/self_forcing_dmd.pt"),
|
| 79 |
+
)
|
| 80 |
+
parser.add_argument(
|
| 81 |
+
"--config_path",
|
| 82 |
+
type=Path,
|
| 83 |
+
default=Path("configs/self_forcing_sid.yaml"),
|
| 84 |
+
)
|
| 85 |
+
parser.add_argument(
|
| 86 |
+
"--output_dir",
|
| 87 |
+
type=Path,
|
| 88 |
+
default=Path("outputs/single_block_init_sweep"),
|
| 89 |
+
)
|
| 90 |
+
parser.add_argument(
|
| 91 |
+
"--teacher_layers",
|
| 92 |
+
type=int,
|
| 93 |
+
nargs="*",
|
| 94 |
+
default=None,
|
| 95 |
+
help="Teacher source layers. Omit to sweep 0..29.",
|
| 96 |
+
)
|
| 97 |
+
parser.add_argument("--max_steps", type=int, default=1000)
|
| 98 |
+
parser.add_argument("--batch_size", type=int, default=32)
|
| 99 |
+
parser.add_argument("--eval_batch_size", type=int, default=10)
|
| 100 |
+
parser.add_argument("--eval_every", type=int, default=100)
|
| 101 |
+
parser.add_argument("--log_every", type=int, default=20)
|
| 102 |
+
parser.add_argument("--save_every", type=int, default=100)
|
| 103 |
+
parser.add_argument("--train_prompts", type=int, default=80)
|
| 104 |
+
parser.add_argument("--val_prompts", type=int, default=20)
|
| 105 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 106 |
+
parser.add_argument("--fusion_lr", type=float, default=1e-4)
|
| 107 |
+
parser.add_argument("--block_lr", type=float, default=1e-5)
|
| 108 |
+
parser.add_argument("--weight_decay", type=float, default=0.01)
|
| 109 |
+
parser.add_argument("--hidden_weight", type=float, default=0.1)
|
| 110 |
+
parser.add_argument("--flow_weight", type=float, default=1.0)
|
| 111 |
+
parser.add_argument(
|
| 112 |
+
"--gate_mode", choices=("baseline", "learned", "constant"),
|
| 113 |
+
default="baseline",
|
| 114 |
+
)
|
| 115 |
+
parser.add_argument("--gate_hidden_dim", type=int, default=128)
|
| 116 |
+
parser.add_argument("--gate_initial_bias", type=float, default=4.6)
|
| 117 |
+
parser.add_argument("--gate_floor", type=float, default=0.0)
|
| 118 |
+
parser.add_argument("--gate_lr", type=float, default=None)
|
| 119 |
+
parser.add_argument("--gate_freeze_steps", type=int, default=0)
|
| 120 |
+
parser.add_argument("--constant_gate", type=float, default=1.0)
|
| 121 |
+
parser.add_argument("--grad_clip", type=float, default=1.0)
|
| 122 |
+
parser.add_argument("--fusion_warmup_steps", type=int, default=100)
|
| 123 |
+
parser.add_argument("--block_freeze_steps", type=int, default=100)
|
| 124 |
+
parser.add_argument("--block_warmup_steps", type=int, default=100)
|
| 125 |
+
parser.add_argument(
|
| 126 |
+
"--gradient_checkpointing",
|
| 127 |
+
action=argparse.BooleanOptionalAction,
|
| 128 |
+
default=True,
|
| 129 |
+
)
|
| 130 |
+
parser.add_argument(
|
| 131 |
+
"--run_other_initializations",
|
| 132 |
+
action=argparse.BooleanOptionalAction,
|
| 133 |
+
default=True,
|
| 134 |
+
)
|
| 135 |
+
parser.add_argument(
|
| 136 |
+
"--save_final_weights",
|
| 137 |
+
action=argparse.BooleanOptionalAction,
|
| 138 |
+
default=True,
|
| 139 |
+
)
|
| 140 |
+
args = parser.parse_args()
|
| 141 |
+
if args.max_steps < 1:
|
| 142 |
+
parser.error("--max_steps must be positive")
|
| 143 |
+
if args.train_prompts < 1 or args.val_prompts < 1:
|
| 144 |
+
parser.error("Train and validation prompt counts must be positive")
|
| 145 |
+
if args.train_prompts + args.val_prompts > 100:
|
| 146 |
+
parser.error("The offline dataset contains 100 prompts")
|
| 147 |
+
if args.batch_size > args.train_prompts:
|
| 148 |
+
parser.error("--batch_size cannot exceed --train_prompts")
|
| 149 |
+
if args.eval_batch_size > args.val_prompts:
|
| 150 |
+
args.eval_batch_size = args.val_prompts
|
| 151 |
+
if not 0.0 <= args.constant_gate <= 1.0:
|
| 152 |
+
parser.error("--constant_gate must be in [0, 1]")
|
| 153 |
+
if not 0.0 <= args.gate_floor < 1.0:
|
| 154 |
+
parser.error("--gate_floor must be in [0, 1)")
|
| 155 |
+
if args.gate_freeze_steps < 0:
|
| 156 |
+
parser.error("--gate_freeze_steps must be non-negative")
|
| 157 |
+
if args.gate_lr is None:
|
| 158 |
+
args.gate_lr = args.fusion_lr
|
| 159 |
+
return args
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def resolve(path: Path) -> Path:
|
| 163 |
+
path = path.expanduser()
|
| 164 |
+
return path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve()
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def atomic_json(path: Path, value: Any) -> None:
|
| 168 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 169 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 170 |
+
temporary.write_text(
|
| 171 |
+
json.dumps(value, indent=2, ensure_ascii=False) + "\n",
|
| 172 |
+
encoding="utf-8",
|
| 173 |
+
)
|
| 174 |
+
os.replace(temporary, path)
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def append_jsonl(path: Path, value: dict[str, Any]) -> None:
|
| 178 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 179 |
+
with path.open("a", encoding="utf-8") as handle:
|
| 180 |
+
handle.write(json.dumps(value, sort_keys=True) + "\n")
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def parameter_norm(parameters: list[torch.nn.Parameter]) -> float:
|
| 184 |
+
total = 0.0
|
| 185 |
+
for parameter in parameters:
|
| 186 |
+
total += float(parameter.detach().float().square().sum())
|
| 187 |
+
return math.sqrt(total)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def gradient_norm(parameters: list[torch.nn.Parameter]) -> float:
|
| 191 |
+
total = 0.0
|
| 192 |
+
for parameter in parameters:
|
| 193 |
+
if parameter.grad is not None:
|
| 194 |
+
total += float(parameter.grad.detach().float().square().sum())
|
| 195 |
+
return math.sqrt(total)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def build_shared_nonblock_state(seed: int) -> dict[str, dict[str, torch.Tensor]]:
|
| 199 |
+
set_seed(seed)
|
| 200 |
+
fusion = TripleFeatureFusion(1536)
|
| 201 |
+
residual = torch.nn.Linear(1536, 1536)
|
| 202 |
+
torch.nn.init.zeros_(residual.weight)
|
| 203 |
+
torch.nn.init.zeros_(residual.bias)
|
| 204 |
+
return {
|
| 205 |
+
"fusion": {
|
| 206 |
+
key: value.detach().clone()
|
| 207 |
+
for key, value in fusion.state_dict().items()
|
| 208 |
+
},
|
| 209 |
+
"residual_out": {
|
| 210 |
+
key: value.detach().clone()
|
| 211 |
+
for key, value in residual.state_dict().items()
|
| 212 |
+
},
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
def load_teacher(
|
| 217 |
+
checkpoint_path: Path,
|
| 218 |
+
config_path: Path,
|
| 219 |
+
device: torch.device,
|
| 220 |
+
) -> torch.nn.Module:
|
| 221 |
+
config = OmegaConf.merge(
|
| 222 |
+
OmegaConf.load(REPO_ROOT / "configs/default_config.yaml"),
|
| 223 |
+
OmegaConf.load(config_path),
|
| 224 |
+
)
|
| 225 |
+
wrapper = WanDiffusionWrapper(
|
| 226 |
+
**getattr(config, "model_kwargs", {}),
|
| 227 |
+
is_causal=True,
|
| 228 |
+
)
|
| 229 |
+
checkpoint = torch.load(
|
| 230 |
+
checkpoint_path,
|
| 231 |
+
map_location="cpu",
|
| 232 |
+
weights_only=False,
|
| 233 |
+
mmap=True,
|
| 234 |
+
)
|
| 235 |
+
wrapper.load_state_dict(checkpoint["generator_ema"], strict=True)
|
| 236 |
+
del checkpoint
|
| 237 |
+
wrapper.to(device=device, dtype=torch.bfloat16)
|
| 238 |
+
wrapper.eval().requires_grad_(False)
|
| 239 |
+
model = wrapper.model
|
| 240 |
+
if len(model.blocks) != 30 or model.dim != 1536:
|
| 241 |
+
raise ValueError(
|
| 242 |
+
f"Expected Wan 1.3B with 30×1536 blocks, got "
|
| 243 |
+
f"{len(model.blocks)}×{model.dim}"
|
| 244 |
+
)
|
| 245 |
+
if model.freqs.device != device:
|
| 246 |
+
model.freqs = model.freqs.to(device)
|
| 247 |
+
return model
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
class BatchSchedule:
|
| 251 |
+
"""Precompute identical prompt/group batches for every initialization."""
|
| 252 |
+
|
| 253 |
+
def __init__(
|
| 254 |
+
self,
|
| 255 |
+
prompt_ids: list[int],
|
| 256 |
+
batch_size: int,
|
| 257 |
+
steps: int,
|
| 258 |
+
seed: int,
|
| 259 |
+
) -> None:
|
| 260 |
+
self.entries: list[tuple[int, int, list[int]]] = []
|
| 261 |
+
rng = random.Random(seed)
|
| 262 |
+
groups = [
|
| 263 |
+
(chunk, target_step)
|
| 264 |
+
for chunk in range(1, 7)
|
| 265 |
+
for target_step in range(1, 4)
|
| 266 |
+
]
|
| 267 |
+
while len(self.entries) < steps:
|
| 268 |
+
epoch_groups = groups.copy()
|
| 269 |
+
rng.shuffle(epoch_groups)
|
| 270 |
+
for chunk, target_step in epoch_groups:
|
| 271 |
+
prompts = rng.sample(prompt_ids, batch_size)
|
| 272 |
+
self.entries.append((chunk, target_step, prompts))
|
| 273 |
+
if len(self.entries) == steps:
|
| 274 |
+
break
|
| 275 |
+
|
| 276 |
+
def fingerprint(self) -> str:
|
| 277 |
+
payload = json.dumps(self.entries, separators=(",", ":")).encode()
|
| 278 |
+
return hashlib.sha256(payload).hexdigest()
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def lr_values(
|
| 282 |
+
step: int,
|
| 283 |
+
max_steps: int,
|
| 284 |
+
fusion_lr: float,
|
| 285 |
+
block_lr: float,
|
| 286 |
+
fusion_warmup_steps: int,
|
| 287 |
+
block_freeze_steps: int,
|
| 288 |
+
block_warmup_steps: int,
|
| 289 |
+
) -> tuple[float, float]:
|
| 290 |
+
if step < fusion_warmup_steps:
|
| 291 |
+
fusion_factor = float(step + 1) / max(1, fusion_warmup_steps)
|
| 292 |
+
else:
|
| 293 |
+
progress = (step - fusion_warmup_steps) / max(
|
| 294 |
+
1, max_steps - fusion_warmup_steps
|
| 295 |
+
)
|
| 296 |
+
fusion_factor = 0.5 * (1.0 + math.cos(math.pi * min(1.0, progress)))
|
| 297 |
+
|
| 298 |
+
if step < block_freeze_steps:
|
| 299 |
+
block_factor = 0.0
|
| 300 |
+
elif step < block_freeze_steps + block_warmup_steps:
|
| 301 |
+
block_factor = float(step - block_freeze_steps + 1) / max(
|
| 302 |
+
1, block_warmup_steps
|
| 303 |
+
)
|
| 304 |
+
else:
|
| 305 |
+
progress = (
|
| 306 |
+
step - block_freeze_steps - block_warmup_steps
|
| 307 |
+
) / max(1, max_steps - block_freeze_steps - block_warmup_steps)
|
| 308 |
+
block_factor = 0.5 * (
|
| 309 |
+
1.0 + math.cos(math.pi * min(1.0, progress))
|
| 310 |
+
)
|
| 311 |
+
return fusion_lr * fusion_factor, block_lr * block_factor
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
def move_batch(
|
| 315 |
+
batch: dict[str, Any],
|
| 316 |
+
device: torch.device,
|
| 317 |
+
) -> dict[str, Any]:
|
| 318 |
+
output = {
|
| 319 |
+
key: value.to(
|
| 320 |
+
device=device,
|
| 321 |
+
dtype=(
|
| 322 |
+
torch.bfloat16
|
| 323 |
+
if value.is_floating_point() and key != "timestep"
|
| 324 |
+
else value.dtype
|
| 325 |
+
),
|
| 326 |
+
non_blocking=False,
|
| 327 |
+
)
|
| 328 |
+
for key, value in batch.items()
|
| 329 |
+
if isinstance(value, torch.Tensor)
|
| 330 |
+
}
|
| 331 |
+
output.update(
|
| 332 |
+
{
|
| 333 |
+
"prompt_ids": batch["prompt_ids"],
|
| 334 |
+
"chunk": batch["chunk"],
|
| 335 |
+
"anchor_step": batch["anchor_step"],
|
| 336 |
+
"target_step": batch["target_step"],
|
| 337 |
+
}
|
| 338 |
+
)
|
| 339 |
+
return output
|
| 340 |
+
|
| 341 |
+
|
| 342 |
+
def frozen_inputs(
|
| 343 |
+
batch: dict[str, Any],
|
| 344 |
+
teacher: torch.nn.Module,
|
| 345 |
+
device: torch.device,
|
| 346 |
+
) -> dict[str, torch.Tensor]:
|
| 347 |
+
noisy = batch["noisy_latent"]
|
| 348 |
+
batch_size = noisy.shape[0]
|
| 349 |
+
with torch.no_grad(), torch.autocast(
|
| 350 |
+
device_type="cuda", dtype=torch.bfloat16
|
| 351 |
+
):
|
| 352 |
+
current_tokens = teacher.patch_embedding(
|
| 353 |
+
noisy.permute(0, 2, 1, 3, 4)
|
| 354 |
+
).flatten(2).transpose(1, 2)
|
| 355 |
+
timestep = batch["timestep"]
|
| 356 |
+
time_embedding = teacher.time_embedding(
|
| 357 |
+
sinusoidal_embedding_1d(
|
| 358 |
+
teacher.freq_dim, timestep.flatten()
|
| 359 |
+
).type_as(current_tokens)
|
| 360 |
+
)
|
| 361 |
+
timestep_modulation = teacher.time_projection(
|
| 362 |
+
time_embedding
|
| 363 |
+
).unflatten(1, (6, teacher.dim)).unflatten(
|
| 364 |
+
dim=0, sizes=timestep.shape
|
| 365 |
+
)
|
| 366 |
+
head_embedding = time_embedding.unflatten(
|
| 367 |
+
dim=0, sizes=timestep.shape
|
| 368 |
+
).unsqueeze(2)
|
| 369 |
+
condition_per_frame = time_embedding.unflatten(
|
| 370 |
+
dim=0, sizes=timestep.shape
|
| 371 |
+
)
|
| 372 |
+
tokens_per_frame = 30 * 52
|
| 373 |
+
condition_tokens = (
|
| 374 |
+
condition_per_frame[:, :, None, :]
|
| 375 |
+
.expand(batch_size, timestep.shape[1], tokens_per_frame, teacher.dim)
|
| 376 |
+
.reshape(batch_size, -1, teacher.dim)
|
| 377 |
+
)
|
| 378 |
+
grid_sizes = torch.tensor(
|
| 379 |
+
[[3, 30, 52]] * batch_size,
|
| 380 |
+
dtype=torch.long,
|
| 381 |
+
device="cpu",
|
| 382 |
+
)
|
| 383 |
+
return {
|
| 384 |
+
"current_tokens": current_tokens,
|
| 385 |
+
"timestep_modulation": timestep_modulation,
|
| 386 |
+
"head_embedding": head_embedding,
|
| 387 |
+
"condition_tokens": condition_tokens,
|
| 388 |
+
"grid_sizes": grid_sizes,
|
| 389 |
+
"freqs": teacher.freqs,
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
|
| 393 |
+
def hidden_to_flow(
|
| 394 |
+
pred_hidden: torch.Tensor,
|
| 395 |
+
head_embedding: torch.Tensor,
|
| 396 |
+
grid_sizes: torch.Tensor,
|
| 397 |
+
teacher: torch.nn.Module,
|
| 398 |
+
) -> torch.Tensor:
|
| 399 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 400 |
+
head_tokens = teacher.head(pred_hidden, head_embedding)
|
| 401 |
+
flow_channels_first = torch.stack(
|
| 402 |
+
teacher.unpatchify(head_tokens, grid_sizes)
|
| 403 |
+
)
|
| 404 |
+
return flow_channels_first.permute(0, 2, 1, 3, 4)
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
def forward_predictor(
|
| 408 |
+
model: SingleBlockPredictor,
|
| 409 |
+
batch: dict[str, Any],
|
| 410 |
+
teacher: torch.nn.Module,
|
| 411 |
+
device: torch.device,
|
| 412 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 413 |
+
frozen = frozen_inputs(batch, teacher, device)
|
| 414 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 415 |
+
pred_hidden = model(
|
| 416 |
+
current_tokens=frozen["current_tokens"],
|
| 417 |
+
anchor_hidden=batch["anchor_hidden"],
|
| 418 |
+
previous_hidden=batch["previous_hidden"],
|
| 419 |
+
timestep_modulation=frozen["timestep_modulation"],
|
| 420 |
+
grid_sizes=frozen["grid_sizes"],
|
| 421 |
+
freqs=frozen["freqs"],
|
| 422 |
+
history_k=batch["history_k"],
|
| 423 |
+
history_v=batch["history_v"],
|
| 424 |
+
cross_k=batch["cross_k"],
|
| 425 |
+
cross_v=batch["cross_v"],
|
| 426 |
+
current_start=batch["chunk"] * TOKENS_PER_CHUNK,
|
| 427 |
+
condition_tokens=frozen["condition_tokens"],
|
| 428 |
+
anchor_distance=batch.get("anchor_distance"),
|
| 429 |
+
)
|
| 430 |
+
pred_flow = hidden_to_flow(
|
| 431 |
+
pred_hidden,
|
| 432 |
+
frozen["head_embedding"],
|
| 433 |
+
frozen["grid_sizes"],
|
| 434 |
+
teacher,
|
| 435 |
+
)
|
| 436 |
+
return pred_hidden, pred_flow
|
| 437 |
+
|
| 438 |
+
|
| 439 |
+
@torch.inference_mode()
|
| 440 |
+
def evaluate(
|
| 441 |
+
model: SingleBlockPredictor,
|
| 442 |
+
store: OfflinePredictorStore,
|
| 443 |
+
val_prompt_ids: list[int],
|
| 444 |
+
batch_size: int,
|
| 445 |
+
teacher: torch.nn.Module,
|
| 446 |
+
device: torch.device,
|
| 447 |
+
hidden_weight: float,
|
| 448 |
+
flow_weight: float,
|
| 449 |
+
) -> dict[str, float]:
|
| 450 |
+
model.eval()
|
| 451 |
+
hidden_squared = 0.0
|
| 452 |
+
hidden_elements = 0
|
| 453 |
+
flow_squared = 0.0
|
| 454 |
+
flow_elements = 0
|
| 455 |
+
gate_sum = 0.0
|
| 456 |
+
gate_square_sum = 0.0
|
| 457 |
+
gate_count = 0
|
| 458 |
+
gate_histogram = torch.zeros(10, dtype=torch.int64)
|
| 459 |
+
started = time.perf_counter()
|
| 460 |
+
for chunk in range(1, 7):
|
| 461 |
+
for target_step in range(1, 4):
|
| 462 |
+
for start in range(0, len(val_prompt_ids), batch_size):
|
| 463 |
+
prompt_ids = val_prompt_ids[start : start + batch_size]
|
| 464 |
+
batch = move_batch(
|
| 465 |
+
store.batch(prompt_ids, chunk, target_step), device
|
| 466 |
+
)
|
| 467 |
+
pred_hidden, pred_flow = forward_predictor(
|
| 468 |
+
model, batch, teacher, device
|
| 469 |
+
)
|
| 470 |
+
gate = model.fusion.last_gate
|
| 471 |
+
if gate is None:
|
| 472 |
+
raise RuntimeError("Fusion did not expose gate values")
|
| 473 |
+
gate_float = gate.float()
|
| 474 |
+
gate_sum += float(gate_float.sum())
|
| 475 |
+
gate_square_sum += float(gate_float.square().sum())
|
| 476 |
+
gate_count += gate_float.numel()
|
| 477 |
+
gate_histogram += torch.histc(
|
| 478 |
+
gate_float, bins=10, min=0.0, max=1.0
|
| 479 |
+
).to(device="cpu", dtype=torch.int64)
|
| 480 |
+
hidden_error = pred_hidden.float() - batch["target_hidden"].float()
|
| 481 |
+
flow_error = pred_flow.float() - batch["target_flow"].float()
|
| 482 |
+
hidden_squared += float(hidden_error.square().sum())
|
| 483 |
+
hidden_elements += hidden_error.numel()
|
| 484 |
+
flow_squared += float(flow_error.square().sum())
|
| 485 |
+
flow_elements += flow_error.numel()
|
| 486 |
+
del batch, pred_hidden, pred_flow, hidden_error, flow_error
|
| 487 |
+
hidden_mse = hidden_squared / hidden_elements
|
| 488 |
+
flow_mse = flow_squared / flow_elements
|
| 489 |
+
gate_mean = gate_sum / gate_count
|
| 490 |
+
gate_variance = max(0.0, gate_square_sum / gate_count - gate_mean**2)
|
| 491 |
+
model.train()
|
| 492 |
+
return {
|
| 493 |
+
"hidden_mse": hidden_mse,
|
| 494 |
+
"flow_mse": flow_mse,
|
| 495 |
+
"total_loss": hidden_weight * hidden_mse + flow_weight * flow_mse,
|
| 496 |
+
"gate_mean": gate_mean,
|
| 497 |
+
"gate_std": math.sqrt(gate_variance),
|
| 498 |
+
"gate_histogram_10_bins": gate_histogram.tolist(),
|
| 499 |
+
"gate_count": gate_count,
|
| 500 |
+
"eval_time_s": time.perf_counter() - started,
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
def normalized_auc(
|
| 505 |
+
evaluations: list[dict[str, Any]], key: str, max_steps: int
|
| 506 |
+
) -> float:
|
| 507 |
+
if len(evaluations) < 2:
|
| 508 |
+
return float(evaluations[0][key])
|
| 509 |
+
area = 0.0
|
| 510 |
+
for left, right in zip(evaluations, evaluations[1:]):
|
| 511 |
+
width = int(right["step"]) - int(left["step"])
|
| 512 |
+
area += width * (float(left[key]) + float(right[key])) * 0.5
|
| 513 |
+
return area / max_steps
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
def save_predictor_weights(
|
| 517 |
+
model: SingleBlockPredictor, path: Path
|
| 518 |
+
) -> None:
|
| 519 |
+
tensors = {
|
| 520 |
+
key: value.detach().to(device="cpu").contiguous()
|
| 521 |
+
for key, value in model.state_dict().items()
|
| 522 |
+
}
|
| 523 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 524 |
+
save_file(tensors, temporary)
|
| 525 |
+
os.replace(temporary, path)
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
def make_model(
|
| 529 |
+
teacher: torch.nn.Module,
|
| 530 |
+
source_layer: int,
|
| 531 |
+
method: str,
|
| 532 |
+
shared_nonblock_state: dict[str, dict[str, torch.Tensor]],
|
| 533 |
+
seed: int,
|
| 534 |
+
gradient_checkpointing: bool,
|
| 535 |
+
device: torch.device,
|
| 536 |
+
gate_mode: str = "baseline",
|
| 537 |
+
gate_hidden_dim: int = 128,
|
| 538 |
+
gate_initial_bias: float = 4.6,
|
| 539 |
+
gate_floor: float = 0.0,
|
| 540 |
+
constant_gate: float = 1.0,
|
| 541 |
+
input_variant: str = "self_forcing",
|
| 542 |
+
atc_previous_scope: str = "chunk",
|
| 543 |
+
atc_freq_dim: int = 256,
|
| 544 |
+
atc_mlp_hidden_dim: int = 3072,
|
| 545 |
+
atc_gate_hidden_dim: int = 512,
|
| 546 |
+
atc_transport_residual_scale: float = 0.1,
|
| 547 |
+
atc_gate_initial_probability: float = 0.3,
|
| 548 |
+
) -> SingleBlockPredictor:
|
| 549 |
+
# Random block baselines use the same seed independently of the shared
|
| 550 |
+
# fusion initialization.
|
| 551 |
+
set_seed(seed)
|
| 552 |
+
block = initialize_predictor_block(
|
| 553 |
+
teacher.blocks[source_layer],
|
| 554 |
+
method,
|
| 555 |
+
)
|
| 556 |
+
model = SingleBlockPredictor(
|
| 557 |
+
block,
|
| 558 |
+
dim=1536,
|
| 559 |
+
gradient_checkpointing=gradient_checkpointing,
|
| 560 |
+
input_variant=input_variant,
|
| 561 |
+
gate_mode=gate_mode,
|
| 562 |
+
gate_hidden_dim=gate_hidden_dim,
|
| 563 |
+
gate_initial_bias=gate_initial_bias,
|
| 564 |
+
gate_floor=gate_floor,
|
| 565 |
+
constant_gate=constant_gate,
|
| 566 |
+
atc_previous_scope=atc_previous_scope,
|
| 567 |
+
atc_freq_dim=atc_freq_dim,
|
| 568 |
+
atc_mlp_hidden_dim=atc_mlp_hidden_dim,
|
| 569 |
+
atc_gate_hidden_dim=atc_gate_hidden_dim,
|
| 570 |
+
atc_transport_residual_scale=atc_transport_residual_scale,
|
| 571 |
+
atc_gate_initial_probability=atc_gate_initial_probability,
|
| 572 |
+
)
|
| 573 |
+
fusion_state = shared_nonblock_state["fusion"]
|
| 574 |
+
if input_variant == "disca":
|
| 575 |
+
# Keep all overlapping initialization exactly matched to the
|
| 576 |
+
# Self-Forcing baseline, while physically removing the third D-wide
|
| 577 |
+
# previous-chunk input channel from proj_in.
|
| 578 |
+
disca_state = {
|
| 579 |
+
key: value
|
| 580 |
+
for key, value in fusion_state.items()
|
| 581 |
+
if key.startswith(("current_norm.", "anchor_norm.", "proj_out."))
|
| 582 |
+
}
|
| 583 |
+
disca_state["proj_in.weight"] = fusion_state["proj_in.weight"][
|
| 584 |
+
:, : 2 * teacher.dim
|
| 585 |
+
].clone()
|
| 586 |
+
disca_state["proj_in.bias"] = fusion_state["proj_in.bias"].clone()
|
| 587 |
+
model.fusion.load_state_dict(disca_state, strict=True)
|
| 588 |
+
elif input_variant == "self_forcing":
|
| 589 |
+
incompatible = model.fusion.load_state_dict(fusion_state, strict=False)
|
| 590 |
+
unexpected = list(incompatible.unexpected_keys)
|
| 591 |
+
missing = [
|
| 592 |
+
key
|
| 593 |
+
for key in incompatible.missing_keys
|
| 594 |
+
if not key.startswith("gate.")
|
| 595 |
+
]
|
| 596 |
+
if unexpected or missing:
|
| 597 |
+
raise RuntimeError(
|
| 598 |
+
f"Shared fusion state mismatch: missing={missing}, "
|
| 599 |
+
f"unexpected={unexpected}"
|
| 600 |
+
)
|
| 601 |
+
elif input_variant != "atc":
|
| 602 |
+
raise ValueError(f"Unknown input variant: {input_variant}")
|
| 603 |
+
model.residual_out.load_state_dict(
|
| 604 |
+
shared_nonblock_state["residual_out"], strict=True
|
| 605 |
+
)
|
| 606 |
+
return model.to(device=device)
|
| 607 |
+
|
| 608 |
+
|
| 609 |
+
def run_experiment(
|
| 610 |
+
*,
|
| 611 |
+
name: str,
|
| 612 |
+
method: str,
|
| 613 |
+
source_layer: int,
|
| 614 |
+
args: argparse.Namespace,
|
| 615 |
+
store: OfflinePredictorStore,
|
| 616 |
+
teacher: torch.nn.Module,
|
| 617 |
+
train_prompt_ids: list[int],
|
| 618 |
+
val_prompt_ids: list[int],
|
| 619 |
+
schedule: BatchSchedule,
|
| 620 |
+
shared_nonblock_state: dict[str, dict[str, torch.Tensor]],
|
| 621 |
+
device: torch.device,
|
| 622 |
+
) -> dict[str, Any]:
|
| 623 |
+
run_dir = args.output_dir / name
|
| 624 |
+
metrics_path = run_dir / "metrics.json"
|
| 625 |
+
if metrics_path.exists():
|
| 626 |
+
existing = json.loads(metrics_path.read_text(encoding="utf-8"))
|
| 627 |
+
if existing.get("status") == "complete":
|
| 628 |
+
print(f"[run] skip completed {name}", flush=True)
|
| 629 |
+
return existing
|
| 630 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 631 |
+
log_path = run_dir / "train_log.jsonl"
|
| 632 |
+
|
| 633 |
+
model = make_model(
|
| 634 |
+
teacher,
|
| 635 |
+
source_layer,
|
| 636 |
+
method,
|
| 637 |
+
shared_nonblock_state,
|
| 638 |
+
args.seed,
|
| 639 |
+
args.gradient_checkpointing,
|
| 640 |
+
device,
|
| 641 |
+
args.gate_mode,
|
| 642 |
+
args.gate_hidden_dim,
|
| 643 |
+
args.gate_initial_bias,
|
| 644 |
+
args.gate_floor,
|
| 645 |
+
args.constant_gate,
|
| 646 |
+
)
|
| 647 |
+
fusion_parameters = model.fusion_parameters_without_gate()
|
| 648 |
+
gate_parameters = model.gate_parameters()
|
| 649 |
+
block_parameters = model.block_parameters()
|
| 650 |
+
optimizer_groups = [
|
| 651 |
+
{"params": fusion_parameters, "lr": args.fusion_lr},
|
| 652 |
+
]
|
| 653 |
+
if gate_parameters:
|
| 654 |
+
optimizer_groups.append({"params": gate_parameters, "lr": args.gate_lr})
|
| 655 |
+
block_group_index = len(optimizer_groups)
|
| 656 |
+
optimizer_groups.append({"params": block_parameters, "lr": args.block_lr})
|
| 657 |
+
optimizer = AdamW(
|
| 658 |
+
optimizer_groups,
|
| 659 |
+
betas=(0.9, 0.95),
|
| 660 |
+
weight_decay=args.weight_decay,
|
| 661 |
+
)
|
| 662 |
+
initial_block_norm = parameter_norm(block_parameters)
|
| 663 |
+
evaluations: list[dict[str, Any]] = []
|
| 664 |
+
start_step = 0
|
| 665 |
+
latest_path = run_dir / "training_latest.pt"
|
| 666 |
+
if latest_path.exists():
|
| 667 |
+
state = torch.load(
|
| 668 |
+
latest_path, map_location="cpu", weights_only=False
|
| 669 |
+
)
|
| 670 |
+
model.load_state_dict(state["model"], strict=True)
|
| 671 |
+
optimizer.load_state_dict(state["optimizer"])
|
| 672 |
+
evaluations = state["evaluations"]
|
| 673 |
+
start_step = int(state["step"])
|
| 674 |
+
print(f"[run] resume {name} at step {start_step}", flush=True)
|
| 675 |
+
|
| 676 |
+
model.set_block_trainable(start_step >= args.block_freeze_steps)
|
| 677 |
+
config = {
|
| 678 |
+
"name": name,
|
| 679 |
+
"initialization_method": method,
|
| 680 |
+
"source_layer": source_layer,
|
| 681 |
+
"source_definition": (
|
| 682 |
+
"block weights, clean-history K/V projection/input, and text K/V "
|
| 683 |
+
"all use this generator_ema Teacher layer"
|
| 684 |
+
),
|
| 685 |
+
"seed": args.seed,
|
| 686 |
+
"train_prompt_ids": train_prompt_ids,
|
| 687 |
+
"val_prompt_ids": val_prompt_ids,
|
| 688 |
+
"max_steps": args.max_steps,
|
| 689 |
+
"batch_size": args.batch_size,
|
| 690 |
+
"eval_batch_size": args.eval_batch_size,
|
| 691 |
+
"fusion_lr": args.fusion_lr,
|
| 692 |
+
"block_lr": args.block_lr,
|
| 693 |
+
"fusion_warmup_steps": args.fusion_warmup_steps,
|
| 694 |
+
"block_freeze_steps": args.block_freeze_steps,
|
| 695 |
+
"block_warmup_steps": args.block_warmup_steps,
|
| 696 |
+
"weight_decay": args.weight_decay,
|
| 697 |
+
"hidden_weight": args.hidden_weight,
|
| 698 |
+
"flow_weight": args.flow_weight,
|
| 699 |
+
"gradient_checkpointing": args.gradient_checkpointing,
|
| 700 |
+
"gate_mode": args.gate_mode,
|
| 701 |
+
"gate_hidden_dim": args.gate_hidden_dim,
|
| 702 |
+
"gate_initial_bias": args.gate_initial_bias,
|
| 703 |
+
"gate_floor": args.gate_floor,
|
| 704 |
+
"gate_lr": args.gate_lr,
|
| 705 |
+
"gate_freeze_steps": args.gate_freeze_steps,
|
| 706 |
+
"constant_gate": args.constant_gate,
|
| 707 |
+
"batch_schedule_sha256": schedule.fingerprint(),
|
| 708 |
+
"initial_block_parameter_norm": initial_block_norm,
|
| 709 |
+
"trainable_parameters": sum(
|
| 710 |
+
parameter.numel() for parameter in model.parameters()
|
| 711 |
+
),
|
| 712 |
+
}
|
| 713 |
+
atomic_json(run_dir / "config.json", config)
|
| 714 |
+
print(
|
| 715 |
+
f"[run] {name}: method={method}, source={source_layer}, "
|
| 716 |
+
f"start={start_step}",
|
| 717 |
+
flush=True,
|
| 718 |
+
)
|
| 719 |
+
|
| 720 |
+
if not evaluations:
|
| 721 |
+
initial_eval = evaluate(
|
| 722 |
+
model,
|
| 723 |
+
store,
|
| 724 |
+
val_prompt_ids,
|
| 725 |
+
args.eval_batch_size,
|
| 726 |
+
teacher,
|
| 727 |
+
device,
|
| 728 |
+
args.hidden_weight,
|
| 729 |
+
args.flow_weight,
|
| 730 |
+
)
|
| 731 |
+
evaluations.append({"step": 0, **initial_eval})
|
| 732 |
+
print(
|
| 733 |
+
f"[eval] {name} step=0 flow={initial_eval['flow_mse']:.8f} "
|
| 734 |
+
f"hidden={initial_eval['hidden_mse']:.8f}",
|
| 735 |
+
flush=True,
|
| 736 |
+
)
|
| 737 |
+
|
| 738 |
+
optimizer.zero_grad(set_to_none=True)
|
| 739 |
+
model.train()
|
| 740 |
+
run_started = time.perf_counter()
|
| 741 |
+
for step in range(start_step, args.max_steps):
|
| 742 |
+
block_enabled = step >= args.block_freeze_steps
|
| 743 |
+
if any(parameter.requires_grad != block_enabled for parameter in block_parameters):
|
| 744 |
+
model.set_block_trainable(block_enabled)
|
| 745 |
+
|
| 746 |
+
fusion_lr, block_lr = lr_values(
|
| 747 |
+
step,
|
| 748 |
+
args.max_steps,
|
| 749 |
+
args.fusion_lr,
|
| 750 |
+
args.block_lr,
|
| 751 |
+
args.fusion_warmup_steps,
|
| 752 |
+
args.block_freeze_steps,
|
| 753 |
+
args.block_warmup_steps,
|
| 754 |
+
)
|
| 755 |
+
optimizer.param_groups[0]["lr"] = fusion_lr
|
| 756 |
+
if gate_parameters:
|
| 757 |
+
gate_lr, _ = lr_values(
|
| 758 |
+
max(0, step - args.gate_freeze_steps),
|
| 759 |
+
max(1, args.max_steps - args.gate_freeze_steps),
|
| 760 |
+
args.gate_lr, args.block_lr,
|
| 761 |
+
args.fusion_warmup_steps, 0, args.block_warmup_steps,
|
| 762 |
+
)
|
| 763 |
+
if step < args.gate_freeze_steps:
|
| 764 |
+
gate_lr = 0.0
|
| 765 |
+
optimizer.param_groups[1]["lr"] = gate_lr
|
| 766 |
+
else:
|
| 767 |
+
gate_lr = 0.0
|
| 768 |
+
optimizer.param_groups[block_group_index]["lr"] = block_lr
|
| 769 |
+
|
| 770 |
+
chunk, target_step, prompt_ids = schedule.entries[step]
|
| 771 |
+
batch = move_batch(
|
| 772 |
+
store.batch(prompt_ids, chunk, target_step), device
|
| 773 |
+
)
|
| 774 |
+
step_started = time.perf_counter()
|
| 775 |
+
pred_hidden, pred_flow = forward_predictor(
|
| 776 |
+
model, batch, teacher, device
|
| 777 |
+
)
|
| 778 |
+
hidden_loss = F.mse_loss(
|
| 779 |
+
pred_hidden.float(), batch["target_hidden"].float()
|
| 780 |
+
)
|
| 781 |
+
flow_loss = F.mse_loss(
|
| 782 |
+
pred_flow.float(), batch["target_flow"].float()
|
| 783 |
+
)
|
| 784 |
+
loss = (
|
| 785 |
+
args.hidden_weight * hidden_loss
|
| 786 |
+
+ args.flow_weight * flow_loss
|
| 787 |
+
)
|
| 788 |
+
loss.backward()
|
| 789 |
+
if step < args.gate_freeze_steps:
|
| 790 |
+
for parameter in gate_parameters:
|
| 791 |
+
parameter.grad = None
|
| 792 |
+
fusion_grad_norm = gradient_norm(fusion_parameters)
|
| 793 |
+
block_grad_norm = gradient_norm(block_parameters)
|
| 794 |
+
total_grad_norm = torch.nn.utils.clip_grad_norm_(
|
| 795 |
+
model.parameters(), args.grad_clip
|
| 796 |
+
)
|
| 797 |
+
optimizer.step()
|
| 798 |
+
optimizer.zero_grad(set_to_none=True)
|
| 799 |
+
completed_step = step + 1
|
| 800 |
+
|
| 801 |
+
if completed_step == 1 or completed_step % args.log_every == 0:
|
| 802 |
+
torch.cuda.synchronize()
|
| 803 |
+
record = {
|
| 804 |
+
"step": completed_step,
|
| 805 |
+
"train_total_loss": float(loss.detach()),
|
| 806 |
+
"train_hidden_mse": float(hidden_loss.detach()),
|
| 807 |
+
"train_flow_mse": float(flow_loss.detach()),
|
| 808 |
+
"fusion_grad_norm": fusion_grad_norm,
|
| 809 |
+
"block_grad_norm": block_grad_norm,
|
| 810 |
+
"total_grad_norm_before_clip": float(total_grad_norm),
|
| 811 |
+
"fusion_lr": fusion_lr,
|
| 812 |
+
"gate_lr": gate_lr,
|
| 813 |
+
"block_lr": block_lr,
|
| 814 |
+
"chunk": chunk,
|
| 815 |
+
"target_step": target_step,
|
| 816 |
+
"step_time_s": time.perf_counter() - step_started,
|
| 817 |
+
"peak_gpu_gib": torch.cuda.max_memory_allocated() / (1024**3),
|
| 818 |
+
}
|
| 819 |
+
append_jsonl(log_path, record)
|
| 820 |
+
print(
|
| 821 |
+
f"[train] {name} {completed_step}/{args.max_steps} "
|
| 822 |
+
f"flow={record['train_flow_mse']:.8f} "
|
| 823 |
+
f"time={record['step_time_s']:.2f}s "
|
| 824 |
+
f"mem={record['peak_gpu_gib']:.1f}G",
|
| 825 |
+
flush=True,
|
| 826 |
+
)
|
| 827 |
+
|
| 828 |
+
should_eval = (
|
| 829 |
+
completed_step % args.eval_every == 0
|
| 830 |
+
or completed_step == args.max_steps
|
| 831 |
+
)
|
| 832 |
+
if should_eval:
|
| 833 |
+
validation = evaluate(
|
| 834 |
+
model,
|
| 835 |
+
store,
|
| 836 |
+
val_prompt_ids,
|
| 837 |
+
args.eval_batch_size,
|
| 838 |
+
teacher,
|
| 839 |
+
device,
|
| 840 |
+
args.hidden_weight,
|
| 841 |
+
args.flow_weight,
|
| 842 |
+
)
|
| 843 |
+
evaluations.append({"step": completed_step, **validation})
|
| 844 |
+
print(
|
| 845 |
+
f"[eval] {name} step={completed_step} "
|
| 846 |
+
f"flow={validation['flow_mse']:.8f} "
|
| 847 |
+
f"hidden={validation['hidden_mse']:.8f}",
|
| 848 |
+
flush=True,
|
| 849 |
+
)
|
| 850 |
+
|
| 851 |
+
should_save = (
|
| 852 |
+
completed_step % args.save_every == 0
|
| 853 |
+
or completed_step == args.max_steps
|
| 854 |
+
)
|
| 855 |
+
if should_save:
|
| 856 |
+
temporary = latest_path.with_suffix(".pt.tmp")
|
| 857 |
+
torch.save(
|
| 858 |
+
{
|
| 859 |
+
"model": {
|
| 860 |
+
key: value.detach().cpu()
|
| 861 |
+
for key, value in model.state_dict().items()
|
| 862 |
+
},
|
| 863 |
+
"optimizer": optimizer.state_dict(),
|
| 864 |
+
"evaluations": evaluations,
|
| 865 |
+
"step": completed_step,
|
| 866 |
+
},
|
| 867 |
+
temporary,
|
| 868 |
+
)
|
| 869 |
+
os.replace(temporary, latest_path)
|
| 870 |
+
|
| 871 |
+
del (
|
| 872 |
+
batch,
|
| 873 |
+
pred_hidden,
|
| 874 |
+
pred_flow,
|
| 875 |
+
hidden_loss,
|
| 876 |
+
flow_loss,
|
| 877 |
+
loss,
|
| 878 |
+
total_grad_norm,
|
| 879 |
+
)
|
| 880 |
+
|
| 881 |
+
final = evaluations[-1]
|
| 882 |
+
result = {
|
| 883 |
+
"status": "complete",
|
| 884 |
+
**config,
|
| 885 |
+
"final_val_hidden_mse": final["hidden_mse"],
|
| 886 |
+
"final_val_flow_mse": final["flow_mse"],
|
| 887 |
+
"final_val_total_loss": final["total_loss"],
|
| 888 |
+
"val_hidden_mse_auc": normalized_auc(
|
| 889 |
+
evaluations, "hidden_mse", args.max_steps
|
| 890 |
+
),
|
| 891 |
+
"val_flow_mse_auc": normalized_auc(
|
| 892 |
+
evaluations, "flow_mse", args.max_steps
|
| 893 |
+
),
|
| 894 |
+
"val_total_loss_auc": normalized_auc(
|
| 895 |
+
evaluations, "total_loss", args.max_steps
|
| 896 |
+
),
|
| 897 |
+
"evaluations": evaluations,
|
| 898 |
+
"training_time_s": time.perf_counter() - run_started,
|
| 899 |
+
"final_block_parameter_norm": parameter_norm(block_parameters),
|
| 900 |
+
}
|
| 901 |
+
if args.save_final_weights:
|
| 902 |
+
save_predictor_weights(model, run_dir / "predictor_final.safetensors")
|
| 903 |
+
atomic_json(metrics_path, result)
|
| 904 |
+
if latest_path.exists():
|
| 905 |
+
latest_path.unlink()
|
| 906 |
+
del model, optimizer
|
| 907 |
+
torch.cuda.empty_cache()
|
| 908 |
+
return result
|
| 909 |
+
|
| 910 |
+
|
| 911 |
+
def write_summary(output_dir: Path, results: list[dict[str, Any]]) -> None:
|
| 912 |
+
rows = [
|
| 913 |
+
{
|
| 914 |
+
"name": result["name"],
|
| 915 |
+
"initialization_method": result["initialization_method"],
|
| 916 |
+
"source_layer": result["source_layer"],
|
| 917 |
+
"final_val_flow_mse": result["final_val_flow_mse"],
|
| 918 |
+
"final_val_hidden_mse": result["final_val_hidden_mse"],
|
| 919 |
+
"final_val_total_loss": result["final_val_total_loss"],
|
| 920 |
+
"val_flow_mse_auc": result["val_flow_mse_auc"],
|
| 921 |
+
"val_hidden_mse_auc": result["val_hidden_mse_auc"],
|
| 922 |
+
"val_total_loss_auc": result["val_total_loss_auc"],
|
| 923 |
+
"training_time_s": result["training_time_s"],
|
| 924 |
+
}
|
| 925 |
+
for result in results
|
| 926 |
+
]
|
| 927 |
+
rows.sort(key=lambda row: float(row["final_val_flow_mse"]))
|
| 928 |
+
atomic_json(output_dir / "summary.json", rows)
|
| 929 |
+
temporary = output_dir / "summary.csv.tmp"
|
| 930 |
+
with temporary.open("w", newline="", encoding="utf-8") as handle:
|
| 931 |
+
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
|
| 932 |
+
writer.writeheader()
|
| 933 |
+
writer.writerows(rows)
|
| 934 |
+
os.replace(temporary, output_dir / "summary.csv")
|
| 935 |
+
|
| 936 |
+
|
| 937 |
+
def main() -> None:
|
| 938 |
+
args = parse_args()
|
| 939 |
+
args.dataset_root = resolve(args.dataset_root)
|
| 940 |
+
args.checkpoint_path = resolve(args.checkpoint_path)
|
| 941 |
+
args.config_path = resolve(args.config_path)
|
| 942 |
+
args.output_dir = resolve(args.output_dir)
|
| 943 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 944 |
+
layers = (
|
| 945 |
+
list(range(30))
|
| 946 |
+
if args.teacher_layers is None or len(args.teacher_layers) == 0
|
| 947 |
+
else sorted(set(args.teacher_layers))
|
| 948 |
+
)
|
| 949 |
+
if any(layer < 0 or layer >= 30 for layer in layers):
|
| 950 |
+
raise ValueError(f"Invalid Teacher layers: {layers}")
|
| 951 |
+
|
| 952 |
+
set_seed(args.seed)
|
| 953 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 954 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 955 |
+
torch.set_float32_matmul_precision("high")
|
| 956 |
+
device = torch.device("cuda")
|
| 957 |
+
|
| 958 |
+
train_prompt_ids = list(range(args.train_prompts))
|
| 959 |
+
val_prompt_ids = list(
|
| 960 |
+
range(80, 80 + args.val_prompts)
|
| 961 |
+
if args.train_prompts == 80
|
| 962 |
+
else range(args.train_prompts, args.train_prompts + args.val_prompts)
|
| 963 |
+
)
|
| 964 |
+
prompt_ids = sorted(set(train_prompt_ids + val_prompt_ids))
|
| 965 |
+
schedule = BatchSchedule(
|
| 966 |
+
train_prompt_ids,
|
| 967 |
+
args.batch_size,
|
| 968 |
+
args.max_steps,
|
| 969 |
+
args.seed,
|
| 970 |
+
)
|
| 971 |
+
shared_nonblock_state = build_shared_nonblock_state(args.seed)
|
| 972 |
+
|
| 973 |
+
atomic_json(
|
| 974 |
+
args.output_dir / "sweep_config.json",
|
| 975 |
+
{
|
| 976 |
+
**{
|
| 977 |
+
key: str(value) if isinstance(value, Path) else value
|
| 978 |
+
for key, value in vars(args).items()
|
| 979 |
+
},
|
| 980 |
+
"teacher_layers": layers,
|
| 981 |
+
"train_prompt_ids": train_prompt_ids,
|
| 982 |
+
"val_prompt_ids": val_prompt_ids,
|
| 983 |
+
"batch_schedule_sha256": schedule.fingerprint(),
|
| 984 |
+
"other_initializations": list(OTHER_METHODS),
|
| 985 |
+
},
|
| 986 |
+
)
|
| 987 |
+
|
| 988 |
+
print("[setup] loading frozen generator_ema", flush=True)
|
| 989 |
+
teacher = load_teacher(
|
| 990 |
+
args.checkpoint_path,
|
| 991 |
+
args.config_path,
|
| 992 |
+
device,
|
| 993 |
+
)
|
| 994 |
+
print("[setup] loading common offline trajectories into RAM", flush=True)
|
| 995 |
+
store = OfflinePredictorStore(args.dataset_root, prompt_ids)
|
| 996 |
+
|
| 997 |
+
results: list[dict[str, Any]] = []
|
| 998 |
+
for layer in layers:
|
| 999 |
+
name = f"{args.gate_mode}_layer_{layer:02d}"
|
| 1000 |
+
metrics_path = args.output_dir / name / "metrics.json"
|
| 1001 |
+
if metrics_path.exists():
|
| 1002 |
+
existing = json.loads(metrics_path.read_text(encoding="utf-8"))
|
| 1003 |
+
if existing.get("status") == "complete":
|
| 1004 |
+
results.append(existing)
|
| 1005 |
+
print(f"[sweep] already complete: {name}", flush=True)
|
| 1006 |
+
continue
|
| 1007 |
+
store.load_layer_cache(layer, teacher, device)
|
| 1008 |
+
results.append(
|
| 1009 |
+
run_experiment(
|
| 1010 |
+
name=name,
|
| 1011 |
+
method="teacher_full",
|
| 1012 |
+
source_layer=layer,
|
| 1013 |
+
args=args,
|
| 1014 |
+
store=store,
|
| 1015 |
+
teacher=teacher,
|
| 1016 |
+
train_prompt_ids=train_prompt_ids,
|
| 1017 |
+
val_prompt_ids=val_prompt_ids,
|
| 1018 |
+
schedule=schedule,
|
| 1019 |
+
shared_nonblock_state=shared_nonblock_state,
|
| 1020 |
+
device=device,
|
| 1021 |
+
)
|
| 1022 |
+
)
|
| 1023 |
+
write_summary(args.output_dir, results)
|
| 1024 |
+
|
| 1025 |
+
teacher_results = [
|
| 1026 |
+
result
|
| 1027 |
+
for result in results
|
| 1028 |
+
if result["initialization_method"] == "teacher_full"
|
| 1029 |
+
]
|
| 1030 |
+
if not teacher_results:
|
| 1031 |
+
raise RuntimeError("No Teacher layer experiment completed")
|
| 1032 |
+
best_teacher = min(
|
| 1033 |
+
teacher_results,
|
| 1034 |
+
key=lambda result: float(result["final_val_flow_mse"]),
|
| 1035 |
+
)
|
| 1036 |
+
best_layer = int(best_teacher["source_layer"])
|
| 1037 |
+
atomic_json(
|
| 1038 |
+
args.output_dir / "best_teacher_layer.json",
|
| 1039 |
+
{
|
| 1040 |
+
"source_layer": best_layer,
|
| 1041 |
+
"selection_metric": "final_val_flow_mse",
|
| 1042 |
+
"value": best_teacher["final_val_flow_mse"],
|
| 1043 |
+
"run": best_teacher["name"],
|
| 1044 |
+
},
|
| 1045 |
+
)
|
| 1046 |
+
print(
|
| 1047 |
+
f"[sweep] best Teacher layer={best_layer}, "
|
| 1048 |
+
f"flow={best_teacher['final_val_flow_mse']:.8f}",
|
| 1049 |
+
flush=True,
|
| 1050 |
+
)
|
| 1051 |
+
|
| 1052 |
+
if args.run_other_initializations:
|
| 1053 |
+
store.load_layer_cache(best_layer, teacher, device)
|
| 1054 |
+
for method in OTHER_METHODS:
|
| 1055 |
+
name = f"{method}_source_{best_layer:02d}"
|
| 1056 |
+
results.append(
|
| 1057 |
+
run_experiment(
|
| 1058 |
+
name=name,
|
| 1059 |
+
method=method,
|
| 1060 |
+
source_layer=best_layer,
|
| 1061 |
+
args=args,
|
| 1062 |
+
store=store,
|
| 1063 |
+
teacher=teacher,
|
| 1064 |
+
train_prompt_ids=train_prompt_ids,
|
| 1065 |
+
val_prompt_ids=val_prompt_ids,
|
| 1066 |
+
schedule=schedule,
|
| 1067 |
+
shared_nonblock_state=shared_nonblock_state,
|
| 1068 |
+
device=device,
|
| 1069 |
+
)
|
| 1070 |
+
)
|
| 1071 |
+
write_summary(args.output_dir, results)
|
| 1072 |
+
|
| 1073 |
+
write_summary(args.output_dir, results)
|
| 1074 |
+
print(
|
| 1075 |
+
f"[sweep] complete: {len(results)} experiments -> "
|
| 1076 |
+
f"{args.output_dir / 'summary.csv'}",
|
| 1077 |
+
flush=True,
|
| 1078 |
+
)
|
| 1079 |
+
|
| 1080 |
+
|
| 1081 |
+
if __name__ == "__main__":
|
| 1082 |
+
main()
|
scripts/run_three_block_consecutive_sweep.py
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Train three-block Predictors initialized from consecutive Teacher layers."""
|
| 3 |
+
|
| 4 |
+
# ruff: noqa: E402 -- CUDA_VISIBLE_DEVICES must be set before importing torch.
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import argparse
|
| 9 |
+
import csv
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
import time
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def _preparse_gpu() -> str:
|
| 19 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 20 |
+
parser.add_argument("--gpu", default="2")
|
| 21 |
+
args, _ = parser.parse_known_args()
|
| 22 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 23 |
+
return str(args.gpu)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 27 |
+
|
| 28 |
+
import torch
|
| 29 |
+
import torch.nn.functional as F
|
| 30 |
+
from safetensors.torch import save_file
|
| 31 |
+
from torch.optim import AdamW
|
| 32 |
+
|
| 33 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 34 |
+
if str(REPO_ROOT) not in sys.path:
|
| 35 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 36 |
+
|
| 37 |
+
from predictor_training.offline_data import OfflinePredictorStore, TOKENS_PER_CHUNK
|
| 38 |
+
from predictor_training.three_block import ThreeBlockPredictor
|
| 39 |
+
from scripts.run_single_block_init_sweep import (
|
| 40 |
+
BatchSchedule,
|
| 41 |
+
append_jsonl,
|
| 42 |
+
atomic_json,
|
| 43 |
+
build_shared_nonblock_state,
|
| 44 |
+
frozen_inputs,
|
| 45 |
+
gradient_norm,
|
| 46 |
+
hidden_to_flow,
|
| 47 |
+
load_teacher,
|
| 48 |
+
lr_values,
|
| 49 |
+
move_batch,
|
| 50 |
+
normalized_auc,
|
| 51 |
+
parameter_norm,
|
| 52 |
+
)
|
| 53 |
+
from utils.misc import set_seed
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
Triple = tuple[int, int, int]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def default_triples() -> list[Triple]:
|
| 60 |
+
return [(index, index + 1, index + 2) for index in range(28)]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def parse_triple(value: str) -> Triple:
|
| 64 |
+
try:
|
| 65 |
+
values = tuple(int(item) for item in value.split(","))
|
| 66 |
+
except ValueError as error:
|
| 67 |
+
raise argparse.ArgumentTypeError(
|
| 68 |
+
f"Triple must look like 0,1,2; got {value!r}"
|
| 69 |
+
) from error
|
| 70 |
+
if len(values) != 3:
|
| 71 |
+
raise argparse.ArgumentTypeError(
|
| 72 |
+
f"Triple must contain exactly three layers; got {value!r}"
|
| 73 |
+
)
|
| 74 |
+
triple = (values[0], values[1], values[2])
|
| 75 |
+
if not (
|
| 76 |
+
0 <= triple[0]
|
| 77 |
+
and triple[1] == triple[0] + 1
|
| 78 |
+
and triple[2] == triple[1] + 1
|
| 79 |
+
and triple[2] < 30
|
| 80 |
+
):
|
| 81 |
+
raise argparse.ArgumentTypeError(
|
| 82 |
+
"Triple must be three consecutive layers within 0..29; "
|
| 83 |
+
f"got {triple}"
|
| 84 |
+
)
|
| 85 |
+
return triple
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def parse_args() -> argparse.Namespace:
|
| 89 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 90 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 91 |
+
parser.add_argument(
|
| 92 |
+
"--dataset_root",
|
| 93 |
+
type=Path,
|
| 94 |
+
default=Path("outputs/predictor_offline_100_all_blocks"),
|
| 95 |
+
)
|
| 96 |
+
parser.add_argument(
|
| 97 |
+
"--checkpoint_path",
|
| 98 |
+
type=Path,
|
| 99 |
+
default=Path("checkpoints/self_forcing_dmd.pt"),
|
| 100 |
+
)
|
| 101 |
+
parser.add_argument(
|
| 102 |
+
"--config_path", type=Path, default=Path("configs/self_forcing_sid.yaml")
|
| 103 |
+
)
|
| 104 |
+
parser.add_argument(
|
| 105 |
+
"--output_dir",
|
| 106 |
+
type=Path,
|
| 107 |
+
default=Path("outputs/three_block_consecutive_sweep"),
|
| 108 |
+
)
|
| 109 |
+
parser.add_argument(
|
| 110 |
+
"--triples",
|
| 111 |
+
type=parse_triple,
|
| 112 |
+
nargs="*",
|
| 113 |
+
default=None,
|
| 114 |
+
help=(
|
| 115 |
+
"Optional subset such as 0,1,2 1,2,3. "
|
| 116 |
+
"Omit to train all 28 consecutive triples."
|
| 117 |
+
),
|
| 118 |
+
)
|
| 119 |
+
parser.add_argument("--max_triples", type=int, default=None)
|
| 120 |
+
parser.add_argument("--max_steps", type=int, default=1000)
|
| 121 |
+
parser.add_argument("--batch_size", type=int, default=32)
|
| 122 |
+
parser.add_argument("--eval_batch_size", type=int, default=10)
|
| 123 |
+
parser.add_argument("--eval_every", type=int, default=100)
|
| 124 |
+
parser.add_argument("--log_every", type=int, default=20)
|
| 125 |
+
parser.add_argument("--save_every", type=int, default=100)
|
| 126 |
+
parser.add_argument("--train_prompts", type=int, default=80)
|
| 127 |
+
parser.add_argument("--val_prompts", type=int, default=20)
|
| 128 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 129 |
+
parser.add_argument("--fusion_lr", type=float, default=1e-4)
|
| 130 |
+
parser.add_argument("--block_lr", type=float, default=1e-5)
|
| 131 |
+
parser.add_argument("--weight_decay", type=float, default=0.01)
|
| 132 |
+
parser.add_argument("--hidden_weight", type=float, default=0.1)
|
| 133 |
+
parser.add_argument("--flow_weight", type=float, default=1.0)
|
| 134 |
+
parser.add_argument("--grad_clip", type=float, default=1.0)
|
| 135 |
+
parser.add_argument("--fusion_warmup_steps", type=int, default=100)
|
| 136 |
+
parser.add_argument("--block_freeze_steps", type=int, default=100)
|
| 137 |
+
parser.add_argument("--block_warmup_steps", type=int, default=100)
|
| 138 |
+
parser.add_argument(
|
| 139 |
+
"--gradient_checkpointing",
|
| 140 |
+
action=argparse.BooleanOptionalAction,
|
| 141 |
+
default=True,
|
| 142 |
+
)
|
| 143 |
+
parser.add_argument(
|
| 144 |
+
"--save_final_weights",
|
| 145 |
+
action=argparse.BooleanOptionalAction,
|
| 146 |
+
default=True,
|
| 147 |
+
)
|
| 148 |
+
args = parser.parse_args()
|
| 149 |
+
if args.max_steps < 1:
|
| 150 |
+
parser.error("--max_steps must be positive")
|
| 151 |
+
if args.train_prompts < 1 or args.val_prompts < 1:
|
| 152 |
+
parser.error("Prompt counts must be positive")
|
| 153 |
+
if args.train_prompts + args.val_prompts > 100:
|
| 154 |
+
parser.error("The offline dataset contains 100 prompts")
|
| 155 |
+
if args.batch_size > args.train_prompts:
|
| 156 |
+
parser.error("--batch_size cannot exceed --train_prompts")
|
| 157 |
+
if args.eval_batch_size > args.val_prompts:
|
| 158 |
+
args.eval_batch_size = args.val_prompts
|
| 159 |
+
if args.max_triples is not None and args.max_triples < 1:
|
| 160 |
+
parser.error("--max_triples must be positive")
|
| 161 |
+
return args
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def resolve(path: Path) -> Path:
|
| 165 |
+
path = path.expanduser()
|
| 166 |
+
return path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve()
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def experiment_name(triple: Triple) -> str:
|
| 170 |
+
return "triple_" + "_".join(f"{layer:02d}" for layer in triple)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def make_model(
|
| 174 |
+
teacher: torch.nn.Module,
|
| 175 |
+
triple: Triple,
|
| 176 |
+
shared_nonblock_state: dict[str, dict[str, torch.Tensor]],
|
| 177 |
+
seed: int,
|
| 178 |
+
gradient_checkpointing: bool,
|
| 179 |
+
device: torch.device,
|
| 180 |
+
) -> ThreeBlockPredictor:
|
| 181 |
+
set_seed(seed)
|
| 182 |
+
model = ThreeBlockPredictor(
|
| 183 |
+
[teacher.blocks[layer] for layer in triple],
|
| 184 |
+
dim=teacher.dim,
|
| 185 |
+
gradient_checkpointing=gradient_checkpointing,
|
| 186 |
+
)
|
| 187 |
+
model.fusion.load_state_dict(shared_nonblock_state["fusion"], strict=True)
|
| 188 |
+
model.residual_out.load_state_dict(
|
| 189 |
+
shared_nonblock_state["residual_out"], strict=True
|
| 190 |
+
)
|
| 191 |
+
return model.to(device=device)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def forward_predictor(
|
| 195 |
+
model: ThreeBlockPredictor,
|
| 196 |
+
batch: dict[str, Any],
|
| 197 |
+
teacher: torch.nn.Module,
|
| 198 |
+
device: torch.device,
|
| 199 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 200 |
+
frozen = frozen_inputs(batch, teacher, device)
|
| 201 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 202 |
+
pred_hidden = model(
|
| 203 |
+
current_tokens=frozen["current_tokens"],
|
| 204 |
+
anchor_hidden=batch["anchor_hidden"],
|
| 205 |
+
previous_hidden=batch["previous_hidden"],
|
| 206 |
+
timestep_modulation=frozen["timestep_modulation"],
|
| 207 |
+
grid_sizes=frozen["grid_sizes"],
|
| 208 |
+
freqs=frozen["freqs"],
|
| 209 |
+
history_ks=[batch[f"history_k_{index}"] for index in range(3)],
|
| 210 |
+
history_vs=[batch[f"history_v_{index}"] for index in range(3)],
|
| 211 |
+
cross_ks=[batch[f"cross_k_{index}"] for index in range(3)],
|
| 212 |
+
cross_vs=[batch[f"cross_v_{index}"] for index in range(3)],
|
| 213 |
+
current_start=batch["chunk"] * TOKENS_PER_CHUNK,
|
| 214 |
+
)
|
| 215 |
+
pred_flow = hidden_to_flow(
|
| 216 |
+
pred_hidden,
|
| 217 |
+
frozen["head_embedding"],
|
| 218 |
+
frozen["grid_sizes"],
|
| 219 |
+
teacher,
|
| 220 |
+
)
|
| 221 |
+
return pred_hidden, pred_flow
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
@torch.inference_mode()
|
| 225 |
+
def evaluate(
|
| 226 |
+
model: ThreeBlockPredictor,
|
| 227 |
+
store: OfflinePredictorStore,
|
| 228 |
+
triple: Triple,
|
| 229 |
+
val_prompt_ids: list[int],
|
| 230 |
+
batch_size: int,
|
| 231 |
+
teacher: torch.nn.Module,
|
| 232 |
+
device: torch.device,
|
| 233 |
+
hidden_weight: float,
|
| 234 |
+
flow_weight: float,
|
| 235 |
+
) -> dict[str, float]:
|
| 236 |
+
model.eval()
|
| 237 |
+
hidden_squared = 0.0
|
| 238 |
+
hidden_elements = 0
|
| 239 |
+
flow_squared = 0.0
|
| 240 |
+
flow_elements = 0
|
| 241 |
+
started = time.perf_counter()
|
| 242 |
+
for chunk in range(1, 7):
|
| 243 |
+
for target_step in range(1, 4):
|
| 244 |
+
for start in range(0, len(val_prompt_ids), batch_size):
|
| 245 |
+
prompt_ids = val_prompt_ids[start : start + batch_size]
|
| 246 |
+
batch = move_batch(
|
| 247 |
+
store.batch_layers(prompt_ids, chunk, target_step, triple),
|
| 248 |
+
device,
|
| 249 |
+
)
|
| 250 |
+
pred_hidden, pred_flow = forward_predictor(
|
| 251 |
+
model, batch, teacher, device
|
| 252 |
+
)
|
| 253 |
+
hidden_error = pred_hidden.float() - batch["target_hidden"].float()
|
| 254 |
+
flow_error = pred_flow.float() - batch["target_flow"].float()
|
| 255 |
+
hidden_squared += float(hidden_error.square().sum())
|
| 256 |
+
hidden_elements += hidden_error.numel()
|
| 257 |
+
flow_squared += float(flow_error.square().sum())
|
| 258 |
+
flow_elements += flow_error.numel()
|
| 259 |
+
del batch, pred_hidden, pred_flow, hidden_error, flow_error
|
| 260 |
+
hidden_mse = hidden_squared / hidden_elements
|
| 261 |
+
flow_mse = flow_squared / flow_elements
|
| 262 |
+
model.train()
|
| 263 |
+
return {
|
| 264 |
+
"hidden_mse": hidden_mse,
|
| 265 |
+
"flow_mse": flow_mse,
|
| 266 |
+
"total_loss": hidden_weight * hidden_mse + flow_weight * flow_mse,
|
| 267 |
+
"eval_time_s": time.perf_counter() - started,
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def save_predictor_weights(model: ThreeBlockPredictor, path: Path) -> None:
|
| 272 |
+
tensors = {
|
| 273 |
+
key: value.detach().to(device="cpu").contiguous()
|
| 274 |
+
for key, value in model.state_dict().items()
|
| 275 |
+
}
|
| 276 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 277 |
+
save_file(tensors, temporary)
|
| 278 |
+
os.replace(temporary, path)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def run_experiment(
|
| 282 |
+
*,
|
| 283 |
+
triple: Triple,
|
| 284 |
+
args: argparse.Namespace,
|
| 285 |
+
store: OfflinePredictorStore,
|
| 286 |
+
teacher: torch.nn.Module,
|
| 287 |
+
train_prompt_ids: list[int],
|
| 288 |
+
val_prompt_ids: list[int],
|
| 289 |
+
schedule: BatchSchedule,
|
| 290 |
+
shared_nonblock_state: dict[str, dict[str, torch.Tensor]],
|
| 291 |
+
device: torch.device,
|
| 292 |
+
) -> dict[str, Any]:
|
| 293 |
+
name = experiment_name(triple)
|
| 294 |
+
run_dir = args.output_dir / name
|
| 295 |
+
metrics_path = run_dir / "metrics.json"
|
| 296 |
+
if metrics_path.exists():
|
| 297 |
+
existing = json.loads(metrics_path.read_text(encoding="utf-8"))
|
| 298 |
+
if existing.get("status") == "complete":
|
| 299 |
+
print(f"[run] skip completed {name}", flush=True)
|
| 300 |
+
return existing
|
| 301 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 302 |
+
log_path = run_dir / "train_log.jsonl"
|
| 303 |
+
|
| 304 |
+
model = make_model(
|
| 305 |
+
teacher,
|
| 306 |
+
triple,
|
| 307 |
+
shared_nonblock_state,
|
| 308 |
+
args.seed,
|
| 309 |
+
args.gradient_checkpointing,
|
| 310 |
+
device,
|
| 311 |
+
)
|
| 312 |
+
fusion_parameters = model.fusion_parameters()
|
| 313 |
+
block_parameters = model.block_parameters()
|
| 314 |
+
optimizer = AdamW(
|
| 315 |
+
[
|
| 316 |
+
{"params": fusion_parameters, "lr": args.fusion_lr},
|
| 317 |
+
{"params": block_parameters, "lr": args.block_lr},
|
| 318 |
+
],
|
| 319 |
+
betas=(0.9, 0.95),
|
| 320 |
+
weight_decay=args.weight_decay,
|
| 321 |
+
)
|
| 322 |
+
initial_block_norm = parameter_norm(block_parameters)
|
| 323 |
+
evaluations: list[dict[str, Any]] = []
|
| 324 |
+
start_step = 0
|
| 325 |
+
latest_path = run_dir / "training_latest.pt"
|
| 326 |
+
if latest_path.exists():
|
| 327 |
+
state = torch.load(latest_path, map_location="cpu", weights_only=False)
|
| 328 |
+
model.load_state_dict(state["model"], strict=True)
|
| 329 |
+
optimizer.load_state_dict(state["optimizer"])
|
| 330 |
+
evaluations = state["evaluations"]
|
| 331 |
+
start_step = int(state["step"])
|
| 332 |
+
print(f"[run] resume {name} at step {start_step}", flush=True)
|
| 333 |
+
|
| 334 |
+
model.set_blocks_trainable(start_step >= args.block_freeze_steps)
|
| 335 |
+
config = {
|
| 336 |
+
"name": name,
|
| 337 |
+
"architecture": "three_block_predictor",
|
| 338 |
+
"initialization_method": "teacher_full",
|
| 339 |
+
"source_layers": list(triple),
|
| 340 |
+
"triple_kind": "consecutive",
|
| 341 |
+
"source_definition": (
|
| 342 |
+
"Predictor block i, its clean-history K/V, and its text K/V use "
|
| 343 |
+
"generator_ema source_layers[i]"
|
| 344 |
+
),
|
| 345 |
+
"seed": args.seed,
|
| 346 |
+
"train_prompt_ids": train_prompt_ids,
|
| 347 |
+
"val_prompt_ids": val_prompt_ids,
|
| 348 |
+
"max_steps": args.max_steps,
|
| 349 |
+
"batch_size": args.batch_size,
|
| 350 |
+
"eval_batch_size": args.eval_batch_size,
|
| 351 |
+
"fusion_lr": args.fusion_lr,
|
| 352 |
+
"block_lr": args.block_lr,
|
| 353 |
+
"fusion_warmup_steps": args.fusion_warmup_steps,
|
| 354 |
+
"block_freeze_steps": args.block_freeze_steps,
|
| 355 |
+
"block_warmup_steps": args.block_warmup_steps,
|
| 356 |
+
"weight_decay": args.weight_decay,
|
| 357 |
+
"hidden_weight": args.hidden_weight,
|
| 358 |
+
"flow_weight": args.flow_weight,
|
| 359 |
+
"gradient_checkpointing": args.gradient_checkpointing,
|
| 360 |
+
"batch_schedule_sha256": schedule.fingerprint(),
|
| 361 |
+
"initial_block_parameter_norm": initial_block_norm,
|
| 362 |
+
"trainable_parameters": sum(
|
| 363 |
+
parameter.numel() for parameter in model.parameters()
|
| 364 |
+
),
|
| 365 |
+
}
|
| 366 |
+
atomic_json(run_dir / "config.json", config)
|
| 367 |
+
print(f"[run] {name}: start={start_step}", flush=True)
|
| 368 |
+
|
| 369 |
+
if not evaluations:
|
| 370 |
+
initial_eval = evaluate(
|
| 371 |
+
model,
|
| 372 |
+
store,
|
| 373 |
+
triple,
|
| 374 |
+
val_prompt_ids,
|
| 375 |
+
args.eval_batch_size,
|
| 376 |
+
teacher,
|
| 377 |
+
device,
|
| 378 |
+
args.hidden_weight,
|
| 379 |
+
args.flow_weight,
|
| 380 |
+
)
|
| 381 |
+
evaluations.append({"step": 0, **initial_eval})
|
| 382 |
+
print(
|
| 383 |
+
f"[eval] {name} step=0 flow={initial_eval['flow_mse']:.8f} "
|
| 384 |
+
f"hidden={initial_eval['hidden_mse']:.8f}",
|
| 385 |
+
flush=True,
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
optimizer.zero_grad(set_to_none=True)
|
| 389 |
+
model.train()
|
| 390 |
+
run_started = time.perf_counter()
|
| 391 |
+
for step in range(start_step, args.max_steps):
|
| 392 |
+
block_enabled = step >= args.block_freeze_steps
|
| 393 |
+
if any(
|
| 394 |
+
parameter.requires_grad != block_enabled
|
| 395 |
+
for parameter in block_parameters
|
| 396 |
+
):
|
| 397 |
+
model.set_blocks_trainable(block_enabled)
|
| 398 |
+
|
| 399 |
+
fusion_lr, block_lr = lr_values(
|
| 400 |
+
step,
|
| 401 |
+
args.max_steps,
|
| 402 |
+
args.fusion_lr,
|
| 403 |
+
args.block_lr,
|
| 404 |
+
args.fusion_warmup_steps,
|
| 405 |
+
args.block_freeze_steps,
|
| 406 |
+
args.block_warmup_steps,
|
| 407 |
+
)
|
| 408 |
+
optimizer.param_groups[0]["lr"] = fusion_lr
|
| 409 |
+
optimizer.param_groups[1]["lr"] = block_lr
|
| 410 |
+
|
| 411 |
+
chunk, target_step, prompt_ids = schedule.entries[step]
|
| 412 |
+
batch = move_batch(
|
| 413 |
+
store.batch_layers(prompt_ids, chunk, target_step, triple), device
|
| 414 |
+
)
|
| 415 |
+
step_started = time.perf_counter()
|
| 416 |
+
pred_hidden, pred_flow = forward_predictor(model, batch, teacher, device)
|
| 417 |
+
hidden_loss = F.mse_loss(
|
| 418 |
+
pred_hidden.float(), batch["target_hidden"].float()
|
| 419 |
+
)
|
| 420 |
+
flow_loss = F.mse_loss(pred_flow.float(), batch["target_flow"].float())
|
| 421 |
+
loss = args.hidden_weight * hidden_loss + args.flow_weight * flow_loss
|
| 422 |
+
loss.backward()
|
| 423 |
+
fusion_grad_norm = gradient_norm(fusion_parameters)
|
| 424 |
+
block_grad_norm = gradient_norm(block_parameters)
|
| 425 |
+
per_block_grad_norms = [
|
| 426 |
+
gradient_norm(list(block.parameters())) for block in model.blocks
|
| 427 |
+
]
|
| 428 |
+
total_grad_norm = torch.nn.utils.clip_grad_norm_(
|
| 429 |
+
model.parameters(), args.grad_clip
|
| 430 |
+
)
|
| 431 |
+
optimizer.step()
|
| 432 |
+
optimizer.zero_grad(set_to_none=True)
|
| 433 |
+
completed_step = step + 1
|
| 434 |
+
|
| 435 |
+
if completed_step == 1 or completed_step % args.log_every == 0:
|
| 436 |
+
torch.cuda.synchronize()
|
| 437 |
+
record = {
|
| 438 |
+
"step": completed_step,
|
| 439 |
+
"train_total_loss": float(loss.detach()),
|
| 440 |
+
"train_hidden_mse": float(hidden_loss.detach()),
|
| 441 |
+
"train_flow_mse": float(flow_loss.detach()),
|
| 442 |
+
"fusion_grad_norm": fusion_grad_norm,
|
| 443 |
+
"block_grad_norm": block_grad_norm,
|
| 444 |
+
"total_grad_norm_before_clip": float(total_grad_norm),
|
| 445 |
+
"fusion_lr": fusion_lr,
|
| 446 |
+
"block_lr": block_lr,
|
| 447 |
+
"chunk": chunk,
|
| 448 |
+
"target_step": target_step,
|
| 449 |
+
"step_time_s": time.perf_counter() - step_started,
|
| 450 |
+
"peak_gpu_gib": torch.cuda.max_memory_allocated() / (1024**3),
|
| 451 |
+
}
|
| 452 |
+
record.update(
|
| 453 |
+
{
|
| 454 |
+
f"block_{index}_grad_norm": value
|
| 455 |
+
for index, value in enumerate(per_block_grad_norms)
|
| 456 |
+
}
|
| 457 |
+
)
|
| 458 |
+
append_jsonl(log_path, record)
|
| 459 |
+
print(
|
| 460 |
+
f"[train] {name} {completed_step}/{args.max_steps} "
|
| 461 |
+
f"flow={record['train_flow_mse']:.8f} "
|
| 462 |
+
f"time={record['step_time_s']:.2f}s "
|
| 463 |
+
f"mem={record['peak_gpu_gib']:.1f}G",
|
| 464 |
+
flush=True,
|
| 465 |
+
)
|
| 466 |
+
|
| 467 |
+
if completed_step % args.eval_every == 0 or completed_step == args.max_steps:
|
| 468 |
+
validation = evaluate(
|
| 469 |
+
model,
|
| 470 |
+
store,
|
| 471 |
+
triple,
|
| 472 |
+
val_prompt_ids,
|
| 473 |
+
args.eval_batch_size,
|
| 474 |
+
teacher,
|
| 475 |
+
device,
|
| 476 |
+
args.hidden_weight,
|
| 477 |
+
args.flow_weight,
|
| 478 |
+
)
|
| 479 |
+
evaluations.append({"step": completed_step, **validation})
|
| 480 |
+
print(
|
| 481 |
+
f"[eval] {name} step={completed_step} "
|
| 482 |
+
f"flow={validation['flow_mse']:.8f} "
|
| 483 |
+
f"hidden={validation['hidden_mse']:.8f}",
|
| 484 |
+
flush=True,
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
if completed_step % args.save_every == 0 or completed_step == args.max_steps:
|
| 488 |
+
temporary = latest_path.with_suffix(".pt.tmp")
|
| 489 |
+
torch.save(
|
| 490 |
+
{
|
| 491 |
+
"model": {
|
| 492 |
+
key: value.detach().cpu()
|
| 493 |
+
for key, value in model.state_dict().items()
|
| 494 |
+
},
|
| 495 |
+
"optimizer": optimizer.state_dict(),
|
| 496 |
+
"evaluations": evaluations,
|
| 497 |
+
"step": completed_step,
|
| 498 |
+
},
|
| 499 |
+
temporary,
|
| 500 |
+
)
|
| 501 |
+
os.replace(temporary, latest_path)
|
| 502 |
+
|
| 503 |
+
del batch, pred_hidden, pred_flow, hidden_loss, flow_loss, loss
|
| 504 |
+
del total_grad_norm
|
| 505 |
+
|
| 506 |
+
final = evaluations[-1]
|
| 507 |
+
result = {
|
| 508 |
+
"status": "complete",
|
| 509 |
+
**config,
|
| 510 |
+
"final_val_hidden_mse": final["hidden_mse"],
|
| 511 |
+
"final_val_flow_mse": final["flow_mse"],
|
| 512 |
+
"final_val_total_loss": final["total_loss"],
|
| 513 |
+
"val_hidden_mse_auc": normalized_auc(
|
| 514 |
+
evaluations, "hidden_mse", args.max_steps
|
| 515 |
+
),
|
| 516 |
+
"val_flow_mse_auc": normalized_auc(
|
| 517 |
+
evaluations, "flow_mse", args.max_steps
|
| 518 |
+
),
|
| 519 |
+
"val_total_loss_auc": normalized_auc(
|
| 520 |
+
evaluations, "total_loss", args.max_steps
|
| 521 |
+
),
|
| 522 |
+
"evaluations": evaluations,
|
| 523 |
+
"training_time_s": time.perf_counter() - run_started,
|
| 524 |
+
"final_block_parameter_norm": parameter_norm(block_parameters),
|
| 525 |
+
}
|
| 526 |
+
if args.save_final_weights:
|
| 527 |
+
save_predictor_weights(model, run_dir / "predictor_final.safetensors")
|
| 528 |
+
atomic_json(metrics_path, result)
|
| 529 |
+
if latest_path.exists():
|
| 530 |
+
latest_path.unlink()
|
| 531 |
+
del model, optimizer
|
| 532 |
+
torch.cuda.empty_cache()
|
| 533 |
+
return result
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
def write_summary(output_dir: Path, results: list[dict[str, Any]]) -> None:
|
| 537 |
+
rows = [
|
| 538 |
+
{
|
| 539 |
+
"name": result["name"],
|
| 540 |
+
"source_layer_1": result["source_layers"][0],
|
| 541 |
+
"source_layer_2": result["source_layers"][1],
|
| 542 |
+
"source_layer_3": result["source_layers"][2],
|
| 543 |
+
"final_val_flow_mse": result["final_val_flow_mse"],
|
| 544 |
+
"final_val_hidden_mse": result["final_val_hidden_mse"],
|
| 545 |
+
"final_val_total_loss": result["final_val_total_loss"],
|
| 546 |
+
"val_flow_mse_auc": result["val_flow_mse_auc"],
|
| 547 |
+
"val_hidden_mse_auc": result["val_hidden_mse_auc"],
|
| 548 |
+
"val_total_loss_auc": result["val_total_loss_auc"],
|
| 549 |
+
"training_time_s": result["training_time_s"],
|
| 550 |
+
}
|
| 551 |
+
for result in results
|
| 552 |
+
]
|
| 553 |
+
rows.sort(key=lambda row: float(row["final_val_flow_mse"]))
|
| 554 |
+
if not rows:
|
| 555 |
+
return
|
| 556 |
+
destination = output_dir / "summary.csv"
|
| 557 |
+
temporary = destination.with_suffix(".csv.tmp")
|
| 558 |
+
with temporary.open("w", encoding="utf-8", newline="") as handle:
|
| 559 |
+
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
|
| 560 |
+
writer.writeheader()
|
| 561 |
+
writer.writerows(rows)
|
| 562 |
+
os.replace(temporary, destination)
|
| 563 |
+
atomic_json(output_dir / "summary.json", rows)
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
def main() -> None:
|
| 567 |
+
args = parse_args()
|
| 568 |
+
args.dataset_root = resolve(args.dataset_root)
|
| 569 |
+
args.checkpoint_path = resolve(args.checkpoint_path)
|
| 570 |
+
args.config_path = resolve(args.config_path)
|
| 571 |
+
args.output_dir = resolve(args.output_dir)
|
| 572 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 573 |
+
|
| 574 |
+
triples = (
|
| 575 |
+
default_triples()
|
| 576 |
+
if args.triples is None
|
| 577 |
+
else list(dict.fromkeys(args.triples))
|
| 578 |
+
)
|
| 579 |
+
if args.max_triples is not None:
|
| 580 |
+
triples = triples[: args.max_triples]
|
| 581 |
+
train_prompt_ids = list(range(args.train_prompts))
|
| 582 |
+
val_prompt_ids = list(
|
| 583 |
+
range(args.train_prompts, args.train_prompts + args.val_prompts)
|
| 584 |
+
)
|
| 585 |
+
all_prompt_ids = train_prompt_ids + val_prompt_ids
|
| 586 |
+
device = torch.device("cuda")
|
| 587 |
+
set_seed(args.seed)
|
| 588 |
+
torch.set_grad_enabled(True)
|
| 589 |
+
|
| 590 |
+
print("[setup] loading frozen generator_ema", flush=True)
|
| 591 |
+
teacher = load_teacher(args.checkpoint_path, args.config_path, device)
|
| 592 |
+
print("[setup] loading common offline trajectories into RAM", flush=True)
|
| 593 |
+
store = OfflinePredictorStore(args.dataset_root, all_prompt_ids)
|
| 594 |
+
schedule = BatchSchedule(
|
| 595 |
+
train_prompt_ids, args.batch_size, args.max_steps, args.seed
|
| 596 |
+
)
|
| 597 |
+
shared_nonblock_state = build_shared_nonblock_state(args.seed)
|
| 598 |
+
manifest = {
|
| 599 |
+
"status": "running",
|
| 600 |
+
"architecture": "three_block_predictor",
|
| 601 |
+
"initialization_method": "teacher_full",
|
| 602 |
+
"triples": [list(triple) for triple in triples],
|
| 603 |
+
"num_triples": len(triples),
|
| 604 |
+
"default_consecutive_triples": 28,
|
| 605 |
+
"max_steps": args.max_steps,
|
| 606 |
+
"seed": args.seed,
|
| 607 |
+
"train_prompt_ids": train_prompt_ids,
|
| 608 |
+
"val_prompt_ids": val_prompt_ids,
|
| 609 |
+
"batch_schedule_sha256": schedule.fingerprint(),
|
| 610 |
+
}
|
| 611 |
+
atomic_json(args.output_dir / "sweep_manifest.json", manifest)
|
| 612 |
+
|
| 613 |
+
results: list[dict[str, Any]] = []
|
| 614 |
+
for index, triple in enumerate(triples, start=1):
|
| 615 |
+
name = experiment_name(triple)
|
| 616 |
+
metrics_path = args.output_dir / name / "metrics.json"
|
| 617 |
+
if metrics_path.exists():
|
| 618 |
+
existing = json.loads(metrics_path.read_text(encoding="utf-8"))
|
| 619 |
+
if existing.get("status") == "complete":
|
| 620 |
+
print(f"[sweep] {index}/{len(triples)} skip {name}", flush=True)
|
| 621 |
+
results.append(existing)
|
| 622 |
+
continue
|
| 623 |
+
print(
|
| 624 |
+
f"[data] {index}/{len(triples)} loading layers {triple}", flush=True
|
| 625 |
+
)
|
| 626 |
+
store.load_layer_caches(triple, teacher, device)
|
| 627 |
+
result = run_experiment(
|
| 628 |
+
triple=triple,
|
| 629 |
+
args=args,
|
| 630 |
+
store=store,
|
| 631 |
+
teacher=teacher,
|
| 632 |
+
train_prompt_ids=train_prompt_ids,
|
| 633 |
+
val_prompt_ids=val_prompt_ids,
|
| 634 |
+
schedule=schedule,
|
| 635 |
+
shared_nonblock_state=shared_nonblock_state,
|
| 636 |
+
device=device,
|
| 637 |
+
)
|
| 638 |
+
results.append(result)
|
| 639 |
+
write_summary(args.output_dir, results)
|
| 640 |
+
|
| 641 |
+
manifest["status"] = "complete"
|
| 642 |
+
atomic_json(args.output_dir / "sweep_manifest.json", manifest)
|
| 643 |
+
write_summary(args.output_dir, results)
|
| 644 |
+
print(
|
| 645 |
+
f"[complete] {len(results)} triples -> {args.output_dir / 'summary.csv'}",
|
| 646 |
+
flush=True,
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
if __name__ == "__main__":
|
| 651 |
+
main()
|
scripts/run_two_block_pair_sweep.py
ADDED
|
@@ -0,0 +1,632 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Train two-block Predictors over mirror and consecutive Teacher pairs."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
import time
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _preparse_gpu() -> str:
|
| 17 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 18 |
+
parser.add_argument("--gpu", default="2")
|
| 19 |
+
args, _ = parser.parse_known_args()
|
| 20 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 21 |
+
return str(args.gpu)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 25 |
+
|
| 26 |
+
import torch
|
| 27 |
+
import torch.nn.functional as F
|
| 28 |
+
from safetensors.torch import save_file
|
| 29 |
+
from torch.optim import AdamW
|
| 30 |
+
|
| 31 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 32 |
+
if str(REPO_ROOT) not in sys.path:
|
| 33 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 34 |
+
|
| 35 |
+
from predictor_training.offline_data import OfflinePredictorStore, TOKENS_PER_CHUNK
|
| 36 |
+
from predictor_training.two_block import TwoBlockPredictor
|
| 37 |
+
from scripts.run_single_block_init_sweep import (
|
| 38 |
+
BatchSchedule,
|
| 39 |
+
append_jsonl,
|
| 40 |
+
atomic_json,
|
| 41 |
+
build_shared_nonblock_state,
|
| 42 |
+
frozen_inputs,
|
| 43 |
+
gradient_norm,
|
| 44 |
+
hidden_to_flow,
|
| 45 |
+
load_teacher,
|
| 46 |
+
lr_values,
|
| 47 |
+
move_batch,
|
| 48 |
+
normalized_auc,
|
| 49 |
+
parameter_norm,
|
| 50 |
+
)
|
| 51 |
+
from utils.misc import set_seed
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def default_pairs() -> list[tuple[int, int]]:
|
| 55 |
+
mirror = [(index, 29 - index) for index in range(15)]
|
| 56 |
+
consecutive = [(index, index + 1) for index in range(29)]
|
| 57 |
+
return list(dict.fromkeys(mirror + consecutive))
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def pair_kind(pair: tuple[int, int]) -> str:
|
| 61 |
+
mirror = pair[0] + pair[1] == 29
|
| 62 |
+
consecutive = pair[1] == pair[0] + 1
|
| 63 |
+
if mirror and consecutive:
|
| 64 |
+
return "mirror+consecutive"
|
| 65 |
+
if mirror:
|
| 66 |
+
return "mirror"
|
| 67 |
+
if consecutive:
|
| 68 |
+
return "consecutive"
|
| 69 |
+
return "custom"
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def parse_pair(value: str) -> tuple[int, int]:
|
| 73 |
+
try:
|
| 74 |
+
left_text, right_text = value.split(",", maxsplit=1)
|
| 75 |
+
pair = (int(left_text), int(right_text))
|
| 76 |
+
except Exception as error:
|
| 77 |
+
raise argparse.ArgumentTypeError(
|
| 78 |
+
f"Pair must look like 0,29; got {value!r}"
|
| 79 |
+
) from error
|
| 80 |
+
if not (0 <= pair[0] < pair[1] < 30):
|
| 81 |
+
raise argparse.ArgumentTypeError(
|
| 82 |
+
f"Pair must satisfy 0 <= first < second < 30; got {pair}"
|
| 83 |
+
)
|
| 84 |
+
return pair
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def parse_args() -> argparse.Namespace:
|
| 88 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 89 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 90 |
+
parser.add_argument(
|
| 91 |
+
"--dataset_root",
|
| 92 |
+
type=Path,
|
| 93 |
+
default=Path("outputs/predictor_offline_100_all_blocks"),
|
| 94 |
+
)
|
| 95 |
+
parser.add_argument(
|
| 96 |
+
"--checkpoint_path",
|
| 97 |
+
type=Path,
|
| 98 |
+
default=Path("checkpoints/self_forcing_dmd.pt"),
|
| 99 |
+
)
|
| 100 |
+
parser.add_argument(
|
| 101 |
+
"--config_path", type=Path, default=Path("configs/self_forcing_sid.yaml")
|
| 102 |
+
)
|
| 103 |
+
parser.add_argument(
|
| 104 |
+
"--output_dir", type=Path, default=Path("outputs/two_block_pair_sweep")
|
| 105 |
+
)
|
| 106 |
+
parser.add_argument(
|
| 107 |
+
"--pairs",
|
| 108 |
+
type=parse_pair,
|
| 109 |
+
nargs="*",
|
| 110 |
+
default=None,
|
| 111 |
+
help="Pairs such as 0,29 1,28. Omit for all mirror+consecutive pairs.",
|
| 112 |
+
)
|
| 113 |
+
parser.add_argument("--max_pairs", type=int, default=None)
|
| 114 |
+
parser.add_argument("--max_steps", type=int, default=1000)
|
| 115 |
+
parser.add_argument("--batch_size", type=int, default=32)
|
| 116 |
+
parser.add_argument("--eval_batch_size", type=int, default=10)
|
| 117 |
+
parser.add_argument("--eval_every", type=int, default=100)
|
| 118 |
+
parser.add_argument("--log_every", type=int, default=20)
|
| 119 |
+
parser.add_argument("--save_every", type=int, default=100)
|
| 120 |
+
parser.add_argument("--train_prompts", type=int, default=80)
|
| 121 |
+
parser.add_argument("--val_prompts", type=int, default=20)
|
| 122 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 123 |
+
parser.add_argument("--fusion_lr", type=float, default=1e-4)
|
| 124 |
+
parser.add_argument("--block_lr", type=float, default=1e-5)
|
| 125 |
+
parser.add_argument("--weight_decay", type=float, default=0.01)
|
| 126 |
+
parser.add_argument("--hidden_weight", type=float, default=0.1)
|
| 127 |
+
parser.add_argument("--flow_weight", type=float, default=1.0)
|
| 128 |
+
parser.add_argument("--grad_clip", type=float, default=1.0)
|
| 129 |
+
parser.add_argument("--fusion_warmup_steps", type=int, default=100)
|
| 130 |
+
parser.add_argument("--block_freeze_steps", type=int, default=100)
|
| 131 |
+
parser.add_argument("--block_warmup_steps", type=int, default=100)
|
| 132 |
+
parser.add_argument(
|
| 133 |
+
"--gradient_checkpointing",
|
| 134 |
+
action=argparse.BooleanOptionalAction,
|
| 135 |
+
default=True,
|
| 136 |
+
)
|
| 137 |
+
parser.add_argument(
|
| 138 |
+
"--save_final_weights",
|
| 139 |
+
action=argparse.BooleanOptionalAction,
|
| 140 |
+
default=True,
|
| 141 |
+
)
|
| 142 |
+
args = parser.parse_args()
|
| 143 |
+
if args.max_steps < 1:
|
| 144 |
+
parser.error("--max_steps must be positive")
|
| 145 |
+
if args.train_prompts < 1 or args.val_prompts < 1:
|
| 146 |
+
parser.error("Prompt counts must be positive")
|
| 147 |
+
if args.train_prompts + args.val_prompts > 100:
|
| 148 |
+
parser.error("The offline dataset contains 100 prompts")
|
| 149 |
+
if args.batch_size > args.train_prompts:
|
| 150 |
+
parser.error("--batch_size cannot exceed --train_prompts")
|
| 151 |
+
if args.eval_batch_size > args.val_prompts:
|
| 152 |
+
args.eval_batch_size = args.val_prompts
|
| 153 |
+
return args
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def resolve(path: Path) -> Path:
|
| 157 |
+
path = path.expanduser()
|
| 158 |
+
return path.resolve() if path.is_absolute() else (REPO_ROOT / path).resolve()
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def make_model(
|
| 162 |
+
teacher: torch.nn.Module,
|
| 163 |
+
pair: tuple[int, int],
|
| 164 |
+
shared_nonblock_state: dict[str, dict[str, torch.Tensor]],
|
| 165 |
+
seed: int,
|
| 166 |
+
gradient_checkpointing: bool,
|
| 167 |
+
device: torch.device,
|
| 168 |
+
) -> TwoBlockPredictor:
|
| 169 |
+
set_seed(seed)
|
| 170 |
+
model = TwoBlockPredictor(
|
| 171 |
+
[teacher.blocks[pair[0]], teacher.blocks[pair[1]]],
|
| 172 |
+
dim=teacher.dim,
|
| 173 |
+
gradient_checkpointing=gradient_checkpointing,
|
| 174 |
+
)
|
| 175 |
+
model.fusion.load_state_dict(shared_nonblock_state["fusion"], strict=True)
|
| 176 |
+
model.residual_out.load_state_dict(
|
| 177 |
+
shared_nonblock_state["residual_out"], strict=True
|
| 178 |
+
)
|
| 179 |
+
return model.to(device=device)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def forward_predictor(
|
| 183 |
+
model: TwoBlockPredictor,
|
| 184 |
+
batch: dict[str, Any],
|
| 185 |
+
teacher: torch.nn.Module,
|
| 186 |
+
device: torch.device,
|
| 187 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 188 |
+
frozen = frozen_inputs(batch, teacher, device)
|
| 189 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 190 |
+
pred_hidden = model(
|
| 191 |
+
current_tokens=frozen["current_tokens"],
|
| 192 |
+
anchor_hidden=batch["anchor_hidden"],
|
| 193 |
+
previous_hidden=batch["previous_hidden"],
|
| 194 |
+
timestep_modulation=frozen["timestep_modulation"],
|
| 195 |
+
grid_sizes=frozen["grid_sizes"],
|
| 196 |
+
freqs=frozen["freqs"],
|
| 197 |
+
history_ks=[batch["history_k_0"], batch["history_k_1"]],
|
| 198 |
+
history_vs=[batch["history_v_0"], batch["history_v_1"]],
|
| 199 |
+
cross_ks=[batch["cross_k_0"], batch["cross_k_1"]],
|
| 200 |
+
cross_vs=[batch["cross_v_0"], batch["cross_v_1"]],
|
| 201 |
+
current_start=batch["chunk"] * TOKENS_PER_CHUNK,
|
| 202 |
+
)
|
| 203 |
+
pred_flow = hidden_to_flow(
|
| 204 |
+
pred_hidden,
|
| 205 |
+
frozen["head_embedding"],
|
| 206 |
+
frozen["grid_sizes"],
|
| 207 |
+
teacher,
|
| 208 |
+
)
|
| 209 |
+
return pred_hidden, pred_flow
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
@torch.inference_mode()
|
| 213 |
+
def evaluate(
|
| 214 |
+
model: TwoBlockPredictor,
|
| 215 |
+
store: OfflinePredictorStore,
|
| 216 |
+
pair: tuple[int, int],
|
| 217 |
+
val_prompt_ids: list[int],
|
| 218 |
+
batch_size: int,
|
| 219 |
+
teacher: torch.nn.Module,
|
| 220 |
+
device: torch.device,
|
| 221 |
+
hidden_weight: float,
|
| 222 |
+
flow_weight: float,
|
| 223 |
+
) -> dict[str, float]:
|
| 224 |
+
model.eval()
|
| 225 |
+
hidden_squared = 0.0
|
| 226 |
+
hidden_elements = 0
|
| 227 |
+
flow_squared = 0.0
|
| 228 |
+
flow_elements = 0
|
| 229 |
+
started = time.perf_counter()
|
| 230 |
+
for chunk in range(1, 7):
|
| 231 |
+
for target_step in range(1, 4):
|
| 232 |
+
for start in range(0, len(val_prompt_ids), batch_size):
|
| 233 |
+
prompt_ids = val_prompt_ids[start : start + batch_size]
|
| 234 |
+
batch = move_batch(
|
| 235 |
+
store.batch_layers(prompt_ids, chunk, target_step, pair), device
|
| 236 |
+
)
|
| 237 |
+
pred_hidden, pred_flow = forward_predictor(
|
| 238 |
+
model, batch, teacher, device
|
| 239 |
+
)
|
| 240 |
+
hidden_error = pred_hidden.float() - batch["target_hidden"].float()
|
| 241 |
+
flow_error = pred_flow.float() - batch["target_flow"].float()
|
| 242 |
+
hidden_squared += float(hidden_error.square().sum())
|
| 243 |
+
hidden_elements += hidden_error.numel()
|
| 244 |
+
flow_squared += float(flow_error.square().sum())
|
| 245 |
+
flow_elements += flow_error.numel()
|
| 246 |
+
del batch, pred_hidden, pred_flow, hidden_error, flow_error
|
| 247 |
+
hidden_mse = hidden_squared / hidden_elements
|
| 248 |
+
flow_mse = flow_squared / flow_elements
|
| 249 |
+
model.train()
|
| 250 |
+
return {
|
| 251 |
+
"hidden_mse": hidden_mse,
|
| 252 |
+
"flow_mse": flow_mse,
|
| 253 |
+
"total_loss": hidden_weight * hidden_mse + flow_weight * flow_mse,
|
| 254 |
+
"eval_time_s": time.perf_counter() - started,
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def save_predictor_weights(model: TwoBlockPredictor, path: Path) -> None:
|
| 259 |
+
tensors = {
|
| 260 |
+
key: value.detach().to(device="cpu").contiguous()
|
| 261 |
+
for key, value in model.state_dict().items()
|
| 262 |
+
}
|
| 263 |
+
temporary = path.with_suffix(path.suffix + ".tmp")
|
| 264 |
+
save_file(tensors, temporary)
|
| 265 |
+
os.replace(temporary, path)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def run_experiment(
|
| 269 |
+
*,
|
| 270 |
+
pair: tuple[int, int],
|
| 271 |
+
args: argparse.Namespace,
|
| 272 |
+
store: OfflinePredictorStore,
|
| 273 |
+
teacher: torch.nn.Module,
|
| 274 |
+
train_prompt_ids: list[int],
|
| 275 |
+
val_prompt_ids: list[int],
|
| 276 |
+
schedule: BatchSchedule,
|
| 277 |
+
shared_nonblock_state: dict[str, dict[str, torch.Tensor]],
|
| 278 |
+
device: torch.device,
|
| 279 |
+
) -> dict[str, Any]:
|
| 280 |
+
name = f"pair_{pair[0]:02d}_{pair[1]:02d}"
|
| 281 |
+
run_dir = args.output_dir / name
|
| 282 |
+
metrics_path = run_dir / "metrics.json"
|
| 283 |
+
if metrics_path.exists():
|
| 284 |
+
existing = json.loads(metrics_path.read_text(encoding="utf-8"))
|
| 285 |
+
if existing.get("status") == "complete":
|
| 286 |
+
print(f"[run] skip completed {name}", flush=True)
|
| 287 |
+
return existing
|
| 288 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 289 |
+
log_path = run_dir / "train_log.jsonl"
|
| 290 |
+
|
| 291 |
+
model = make_model(
|
| 292 |
+
teacher,
|
| 293 |
+
pair,
|
| 294 |
+
shared_nonblock_state,
|
| 295 |
+
args.seed,
|
| 296 |
+
args.gradient_checkpointing,
|
| 297 |
+
device,
|
| 298 |
+
)
|
| 299 |
+
fusion_parameters = model.fusion_parameters()
|
| 300 |
+
block_parameters = model.block_parameters()
|
| 301 |
+
optimizer = AdamW(
|
| 302 |
+
[
|
| 303 |
+
{"params": fusion_parameters, "lr": args.fusion_lr},
|
| 304 |
+
{"params": block_parameters, "lr": args.block_lr},
|
| 305 |
+
],
|
| 306 |
+
betas=(0.9, 0.95),
|
| 307 |
+
weight_decay=args.weight_decay,
|
| 308 |
+
)
|
| 309 |
+
initial_block_norm = parameter_norm(block_parameters)
|
| 310 |
+
evaluations: list[dict[str, Any]] = []
|
| 311 |
+
start_step = 0
|
| 312 |
+
latest_path = run_dir / "training_latest.pt"
|
| 313 |
+
if latest_path.exists():
|
| 314 |
+
state = torch.load(latest_path, map_location="cpu", weights_only=False)
|
| 315 |
+
model.load_state_dict(state["model"], strict=True)
|
| 316 |
+
optimizer.load_state_dict(state["optimizer"])
|
| 317 |
+
evaluations = state["evaluations"]
|
| 318 |
+
start_step = int(state["step"])
|
| 319 |
+
print(f"[run] resume {name} at step {start_step}", flush=True)
|
| 320 |
+
|
| 321 |
+
model.set_blocks_trainable(start_step >= args.block_freeze_steps)
|
| 322 |
+
config = {
|
| 323 |
+
"name": name,
|
| 324 |
+
"architecture": "two_block_predictor",
|
| 325 |
+
"initialization_method": "teacher_full",
|
| 326 |
+
"source_layers": list(pair),
|
| 327 |
+
"pair_kind": pair_kind(pair),
|
| 328 |
+
"source_definition": (
|
| 329 |
+
"Predictor block i, its clean-history K/V, and its text K/V use "
|
| 330 |
+
"generator_ema source_layers[i]"
|
| 331 |
+
),
|
| 332 |
+
"seed": args.seed,
|
| 333 |
+
"train_prompt_ids": train_prompt_ids,
|
| 334 |
+
"val_prompt_ids": val_prompt_ids,
|
| 335 |
+
"max_steps": args.max_steps,
|
| 336 |
+
"batch_size": args.batch_size,
|
| 337 |
+
"eval_batch_size": args.eval_batch_size,
|
| 338 |
+
"fusion_lr": args.fusion_lr,
|
| 339 |
+
"block_lr": args.block_lr,
|
| 340 |
+
"fusion_warmup_steps": args.fusion_warmup_steps,
|
| 341 |
+
"block_freeze_steps": args.block_freeze_steps,
|
| 342 |
+
"block_warmup_steps": args.block_warmup_steps,
|
| 343 |
+
"weight_decay": args.weight_decay,
|
| 344 |
+
"hidden_weight": args.hidden_weight,
|
| 345 |
+
"flow_weight": args.flow_weight,
|
| 346 |
+
"gradient_checkpointing": args.gradient_checkpointing,
|
| 347 |
+
"batch_schedule_sha256": schedule.fingerprint(),
|
| 348 |
+
"initial_block_parameter_norm": initial_block_norm,
|
| 349 |
+
"trainable_parameters": sum(
|
| 350 |
+
parameter.numel() for parameter in model.parameters()
|
| 351 |
+
),
|
| 352 |
+
}
|
| 353 |
+
atomic_json(run_dir / "config.json", config)
|
| 354 |
+
print(
|
| 355 |
+
f"[run] {name}: kind={config['pair_kind']} start={start_step}", flush=True
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
if not evaluations:
|
| 359 |
+
initial_eval = evaluate(
|
| 360 |
+
model,
|
| 361 |
+
store,
|
| 362 |
+
pair,
|
| 363 |
+
val_prompt_ids,
|
| 364 |
+
args.eval_batch_size,
|
| 365 |
+
teacher,
|
| 366 |
+
device,
|
| 367 |
+
args.hidden_weight,
|
| 368 |
+
args.flow_weight,
|
| 369 |
+
)
|
| 370 |
+
evaluations.append({"step": 0, **initial_eval})
|
| 371 |
+
print(
|
| 372 |
+
f"[eval] {name} step=0 flow={initial_eval['flow_mse']:.8f} "
|
| 373 |
+
f"hidden={initial_eval['hidden_mse']:.8f}",
|
| 374 |
+
flush=True,
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
optimizer.zero_grad(set_to_none=True)
|
| 378 |
+
model.train()
|
| 379 |
+
run_started = time.perf_counter()
|
| 380 |
+
for step in range(start_step, args.max_steps):
|
| 381 |
+
block_enabled = step >= args.block_freeze_steps
|
| 382 |
+
if any(
|
| 383 |
+
parameter.requires_grad != block_enabled
|
| 384 |
+
for parameter in block_parameters
|
| 385 |
+
):
|
| 386 |
+
model.set_blocks_trainable(block_enabled)
|
| 387 |
+
|
| 388 |
+
fusion_lr, block_lr = lr_values(
|
| 389 |
+
step,
|
| 390 |
+
args.max_steps,
|
| 391 |
+
args.fusion_lr,
|
| 392 |
+
args.block_lr,
|
| 393 |
+
args.fusion_warmup_steps,
|
| 394 |
+
args.block_freeze_steps,
|
| 395 |
+
args.block_warmup_steps,
|
| 396 |
+
)
|
| 397 |
+
optimizer.param_groups[0]["lr"] = fusion_lr
|
| 398 |
+
optimizer.param_groups[1]["lr"] = block_lr
|
| 399 |
+
|
| 400 |
+
chunk, target_step, prompt_ids = schedule.entries[step]
|
| 401 |
+
batch = move_batch(
|
| 402 |
+
store.batch_layers(prompt_ids, chunk, target_step, pair), device
|
| 403 |
+
)
|
| 404 |
+
step_started = time.perf_counter()
|
| 405 |
+
pred_hidden, pred_flow = forward_predictor(model, batch, teacher, device)
|
| 406 |
+
hidden_loss = F.mse_loss(
|
| 407 |
+
pred_hidden.float(), batch["target_hidden"].float()
|
| 408 |
+
)
|
| 409 |
+
flow_loss = F.mse_loss(pred_flow.float(), batch["target_flow"].float())
|
| 410 |
+
loss = args.hidden_weight * hidden_loss + args.flow_weight * flow_loss
|
| 411 |
+
loss.backward()
|
| 412 |
+
fusion_grad_norm = gradient_norm(fusion_parameters)
|
| 413 |
+
block_grad_norm = gradient_norm(block_parameters)
|
| 414 |
+
block_0_grad_norm = gradient_norm(list(model.blocks[0].parameters()))
|
| 415 |
+
block_1_grad_norm = gradient_norm(list(model.blocks[1].parameters()))
|
| 416 |
+
total_grad_norm = torch.nn.utils.clip_grad_norm_(
|
| 417 |
+
model.parameters(), args.grad_clip
|
| 418 |
+
)
|
| 419 |
+
optimizer.step()
|
| 420 |
+
optimizer.zero_grad(set_to_none=True)
|
| 421 |
+
completed_step = step + 1
|
| 422 |
+
|
| 423 |
+
if completed_step == 1 or completed_step % args.log_every == 0:
|
| 424 |
+
torch.cuda.synchronize()
|
| 425 |
+
record = {
|
| 426 |
+
"step": completed_step,
|
| 427 |
+
"train_total_loss": float(loss.detach()),
|
| 428 |
+
"train_hidden_mse": float(hidden_loss.detach()),
|
| 429 |
+
"train_flow_mse": float(flow_loss.detach()),
|
| 430 |
+
"fusion_grad_norm": fusion_grad_norm,
|
| 431 |
+
"block_grad_norm": block_grad_norm,
|
| 432 |
+
"block_0_grad_norm": block_0_grad_norm,
|
| 433 |
+
"block_1_grad_norm": block_1_grad_norm,
|
| 434 |
+
"total_grad_norm_before_clip": float(total_grad_norm),
|
| 435 |
+
"fusion_lr": fusion_lr,
|
| 436 |
+
"block_lr": block_lr,
|
| 437 |
+
"chunk": chunk,
|
| 438 |
+
"target_step": target_step,
|
| 439 |
+
"step_time_s": time.perf_counter() - step_started,
|
| 440 |
+
"peak_gpu_gib": torch.cuda.max_memory_allocated() / (1024**3),
|
| 441 |
+
}
|
| 442 |
+
append_jsonl(log_path, record)
|
| 443 |
+
print(
|
| 444 |
+
f"[train] {name} {completed_step}/{args.max_steps} "
|
| 445 |
+
f"flow={record['train_flow_mse']:.8f} "
|
| 446 |
+
f"time={record['step_time_s']:.2f}s "
|
| 447 |
+
f"mem={record['peak_gpu_gib']:.1f}G",
|
| 448 |
+
flush=True,
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
if completed_step % args.eval_every == 0 or completed_step == args.max_steps:
|
| 452 |
+
validation = evaluate(
|
| 453 |
+
model,
|
| 454 |
+
store,
|
| 455 |
+
pair,
|
| 456 |
+
val_prompt_ids,
|
| 457 |
+
args.eval_batch_size,
|
| 458 |
+
teacher,
|
| 459 |
+
device,
|
| 460 |
+
args.hidden_weight,
|
| 461 |
+
args.flow_weight,
|
| 462 |
+
)
|
| 463 |
+
evaluations.append({"step": completed_step, **validation})
|
| 464 |
+
print(
|
| 465 |
+
f"[eval] {name} step={completed_step} "
|
| 466 |
+
f"flow={validation['flow_mse']:.8f} "
|
| 467 |
+
f"hidden={validation['hidden_mse']:.8f}",
|
| 468 |
+
flush=True,
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
if completed_step % args.save_every == 0 or completed_step == args.max_steps:
|
| 472 |
+
temporary = latest_path.with_suffix(".pt.tmp")
|
| 473 |
+
torch.save(
|
| 474 |
+
{
|
| 475 |
+
"model": {
|
| 476 |
+
key: value.detach().cpu()
|
| 477 |
+
for key, value in model.state_dict().items()
|
| 478 |
+
},
|
| 479 |
+
"optimizer": optimizer.state_dict(),
|
| 480 |
+
"evaluations": evaluations,
|
| 481 |
+
"step": completed_step,
|
| 482 |
+
},
|
| 483 |
+
temporary,
|
| 484 |
+
)
|
| 485 |
+
os.replace(temporary, latest_path)
|
| 486 |
+
|
| 487 |
+
del batch, pred_hidden, pred_flow, hidden_loss, flow_loss, loss
|
| 488 |
+
del total_grad_norm
|
| 489 |
+
|
| 490 |
+
final = evaluations[-1]
|
| 491 |
+
result = {
|
| 492 |
+
"status": "complete",
|
| 493 |
+
**config,
|
| 494 |
+
"final_val_hidden_mse": final["hidden_mse"],
|
| 495 |
+
"final_val_flow_mse": final["flow_mse"],
|
| 496 |
+
"final_val_total_loss": final["total_loss"],
|
| 497 |
+
"val_hidden_mse_auc": normalized_auc(
|
| 498 |
+
evaluations, "hidden_mse", args.max_steps
|
| 499 |
+
),
|
| 500 |
+
"val_flow_mse_auc": normalized_auc(
|
| 501 |
+
evaluations, "flow_mse", args.max_steps
|
| 502 |
+
),
|
| 503 |
+
"val_total_loss_auc": normalized_auc(
|
| 504 |
+
evaluations, "total_loss", args.max_steps
|
| 505 |
+
),
|
| 506 |
+
"evaluations": evaluations,
|
| 507 |
+
"training_time_s": time.perf_counter() - run_started,
|
| 508 |
+
"final_block_parameter_norm": parameter_norm(block_parameters),
|
| 509 |
+
}
|
| 510 |
+
if args.save_final_weights:
|
| 511 |
+
save_predictor_weights(model, run_dir / "predictor_final.safetensors")
|
| 512 |
+
atomic_json(metrics_path, result)
|
| 513 |
+
if latest_path.exists():
|
| 514 |
+
latest_path.unlink()
|
| 515 |
+
del model, optimizer
|
| 516 |
+
torch.cuda.empty_cache()
|
| 517 |
+
return result
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
def write_summary(output_dir: Path, results: list[dict[str, Any]]) -> None:
|
| 521 |
+
rows = [
|
| 522 |
+
{
|
| 523 |
+
"name": result["name"],
|
| 524 |
+
"source_layer_1": result["source_layers"][0],
|
| 525 |
+
"source_layer_2": result["source_layers"][1],
|
| 526 |
+
"pair_kind": result["pair_kind"],
|
| 527 |
+
"final_val_flow_mse": result["final_val_flow_mse"],
|
| 528 |
+
"final_val_hidden_mse": result["final_val_hidden_mse"],
|
| 529 |
+
"final_val_total_loss": result["final_val_total_loss"],
|
| 530 |
+
"val_flow_mse_auc": result["val_flow_mse_auc"],
|
| 531 |
+
"val_hidden_mse_auc": result["val_hidden_mse_auc"],
|
| 532 |
+
"val_total_loss_auc": result["val_total_loss_auc"],
|
| 533 |
+
"training_time_s": result["training_time_s"],
|
| 534 |
+
}
|
| 535 |
+
for result in results
|
| 536 |
+
]
|
| 537 |
+
rows.sort(key=lambda row: float(row["final_val_flow_mse"]))
|
| 538 |
+
if not rows:
|
| 539 |
+
return
|
| 540 |
+
destination = output_dir / "summary.csv"
|
| 541 |
+
temporary = destination.with_suffix(".csv.tmp")
|
| 542 |
+
with temporary.open("w", encoding="utf-8", newline="") as handle:
|
| 543 |
+
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
|
| 544 |
+
writer.writeheader()
|
| 545 |
+
writer.writerows(rows)
|
| 546 |
+
os.replace(temporary, destination)
|
| 547 |
+
atomic_json(output_dir / "summary.json", rows)
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
def main() -> None:
|
| 551 |
+
args = parse_args()
|
| 552 |
+
args.dataset_root = resolve(args.dataset_root)
|
| 553 |
+
args.checkpoint_path = resolve(args.checkpoint_path)
|
| 554 |
+
args.config_path = resolve(args.config_path)
|
| 555 |
+
args.output_dir = resolve(args.output_dir)
|
| 556 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 557 |
+
|
| 558 |
+
pairs = default_pairs() if args.pairs is None else list(dict.fromkeys(args.pairs))
|
| 559 |
+
if args.max_pairs is not None:
|
| 560 |
+
pairs = pairs[: args.max_pairs]
|
| 561 |
+
train_prompt_ids = list(range(args.train_prompts))
|
| 562 |
+
val_prompt_ids = list(
|
| 563 |
+
range(args.train_prompts, args.train_prompts + args.val_prompts)
|
| 564 |
+
)
|
| 565 |
+
all_prompt_ids = train_prompt_ids + val_prompt_ids
|
| 566 |
+
device = torch.device("cuda")
|
| 567 |
+
set_seed(args.seed)
|
| 568 |
+
torch.set_grad_enabled(True)
|
| 569 |
+
|
| 570 |
+
print("[setup] loading frozen generator_ema", flush=True)
|
| 571 |
+
teacher = load_teacher(args.checkpoint_path, args.config_path, device)
|
| 572 |
+
print("[setup] loading common offline trajectories into RAM", flush=True)
|
| 573 |
+
store = OfflinePredictorStore(args.dataset_root, all_prompt_ids)
|
| 574 |
+
schedule = BatchSchedule(
|
| 575 |
+
train_prompt_ids, args.batch_size, args.max_steps, args.seed
|
| 576 |
+
)
|
| 577 |
+
shared_nonblock_state = build_shared_nonblock_state(args.seed)
|
| 578 |
+
manifest = {
|
| 579 |
+
"status": "running",
|
| 580 |
+
"pairs": [list(pair) for pair in pairs],
|
| 581 |
+
"num_pairs": len(pairs),
|
| 582 |
+
"mirror_pairs": 15,
|
| 583 |
+
"consecutive_pairs": 29,
|
| 584 |
+
"deduplicated_overlap": [14, 15],
|
| 585 |
+
"max_steps": args.max_steps,
|
| 586 |
+
"seed": args.seed,
|
| 587 |
+
"train_prompt_ids": train_prompt_ids,
|
| 588 |
+
"val_prompt_ids": val_prompt_ids,
|
| 589 |
+
"batch_schedule_sha256": schedule.fingerprint(),
|
| 590 |
+
}
|
| 591 |
+
atomic_json(args.output_dir / "sweep_manifest.json", manifest)
|
| 592 |
+
|
| 593 |
+
results: list[dict[str, Any]] = []
|
| 594 |
+
for index, pair in enumerate(pairs, start=1):
|
| 595 |
+
name = f"pair_{pair[0]:02d}_{pair[1]:02d}"
|
| 596 |
+
metrics_path = args.output_dir / name / "metrics.json"
|
| 597 |
+
if metrics_path.exists():
|
| 598 |
+
existing = json.loads(metrics_path.read_text(encoding="utf-8"))
|
| 599 |
+
if existing.get("status") == "complete":
|
| 600 |
+
print(f"[sweep] {index}/{len(pairs)} skip {name}", flush=True)
|
| 601 |
+
results.append(existing)
|
| 602 |
+
continue
|
| 603 |
+
print(
|
| 604 |
+
f"[data] {index}/{len(pairs)} loading pair {pair[0]},{pair[1]}",
|
| 605 |
+
flush=True,
|
| 606 |
+
)
|
| 607 |
+
store.load_layer_caches(pair, teacher, device)
|
| 608 |
+
result = run_experiment(
|
| 609 |
+
pair=pair,
|
| 610 |
+
args=args,
|
| 611 |
+
store=store,
|
| 612 |
+
teacher=teacher,
|
| 613 |
+
train_prompt_ids=train_prompt_ids,
|
| 614 |
+
val_prompt_ids=val_prompt_ids,
|
| 615 |
+
schedule=schedule,
|
| 616 |
+
shared_nonblock_state=shared_nonblock_state,
|
| 617 |
+
device=device,
|
| 618 |
+
)
|
| 619 |
+
results.append(result)
|
| 620 |
+
write_summary(args.output_dir, results)
|
| 621 |
+
|
| 622 |
+
manifest["status"] = "complete"
|
| 623 |
+
atomic_json(args.output_dir / "sweep_manifest.json", manifest)
|
| 624 |
+
write_summary(args.output_dir, results)
|
| 625 |
+
print(
|
| 626 |
+
f"[complete] {len(results)} pairs -> {args.output_dir / 'summary.csv'}",
|
| 627 |
+
flush=True,
|
| 628 |
+
)
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
if __name__ == "__main__":
|
| 632 |
+
main()
|
scripts/run_wan14b_timestep_chunk_cosine.py
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run native Wan2.1-T2V-14B/50-step feature cosine analysis.
|
| 3 |
+
|
| 4 |
+
Wan14B evaluates the whole 21-latent video in one forward. The token sequence
|
| 5 |
+
is split into seven three-latent temporal slices. Consequently ``cross_same``
|
| 6 |
+
means adjacent full-video temporal slices, not a previously computed AR chunk.
|
| 7 |
+
Only the conditional CFG stream is recorded.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import csv
|
| 14 |
+
import gc
|
| 15 |
+
import json
|
| 16 |
+
import math
|
| 17 |
+
import os
|
| 18 |
+
import sys
|
| 19 |
+
import time
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
from typing import Any
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _preparse_gpu() -> str:
|
| 25 |
+
parser = argparse.ArgumentParser(add_help=False)
|
| 26 |
+
parser.add_argument("--gpu", default="0")
|
| 27 |
+
args, _ = parser.parse_known_args()
|
| 28 |
+
os.environ["CUDA_VISIBLE_DEVICES"] = str(args.gpu)
|
| 29 |
+
return str(args.gpu)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
PHYSICAL_GPU = _preparse_gpu()
|
| 33 |
+
|
| 34 |
+
import torch
|
| 35 |
+
import torch.nn.functional as F
|
| 36 |
+
|
| 37 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 38 |
+
if str(ROOT) not in sys.path:
|
| 39 |
+
sys.path.insert(0, str(ROOT))
|
| 40 |
+
|
| 41 |
+
from utils.misc import set_seed
|
| 42 |
+
from utils.wan_wrapper import WanTextEncoder
|
| 43 |
+
from wan.modules.model import WanModel
|
| 44 |
+
from wan.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def parse_args() -> argparse.Namespace:
|
| 48 |
+
parser = argparse.ArgumentParser()
|
| 49 |
+
parser.add_argument("--gpu", default=PHYSICAL_GPU)
|
| 50 |
+
parser.add_argument(
|
| 51 |
+
"--checkpoint_dir", type=Path, default=Path("wan_models/Wan2.1-T2V-14B")
|
| 52 |
+
)
|
| 53 |
+
parser.add_argument(
|
| 54 |
+
"--prompt_path", type=Path, default=Path("prompts/MovieGenVideoBench_extended.txt")
|
| 55 |
+
)
|
| 56 |
+
parser.add_argument("--output_dir", type=Path, required=True)
|
| 57 |
+
parser.add_argument("--num_prompts", type=int, default=10)
|
| 58 |
+
parser.add_argument(
|
| 59 |
+
"--prompt_ids",
|
| 60 |
+
default="",
|
| 61 |
+
help="Optional comma-separated global prompt ids for multi-GPU sharding.",
|
| 62 |
+
)
|
| 63 |
+
parser.add_argument("--sampling_steps", type=int, default=50)
|
| 64 |
+
parser.add_argument("--layers", type=int, nargs="+", default=[9, 19, 29, 39])
|
| 65 |
+
parser.add_argument("--max_tokens", type=int, default=240)
|
| 66 |
+
parser.add_argument(
|
| 67 |
+
"--chunk_pairing",
|
| 68 |
+
choices=["matched_slot", "boundary_to_all"],
|
| 69 |
+
default="matched_slot",
|
| 70 |
+
help=(
|
| 71 |
+
"Cross-chunk token pairing. boundary_to_all compares all current "
|
| 72 |
+
"temporal slots with the previous slice's last slot."
|
| 73 |
+
),
|
| 74 |
+
)
|
| 75 |
+
parser.add_argument("--seed", type=int, default=0)
|
| 76 |
+
parser.add_argument("--guide_scale", type=float, default=5.0)
|
| 77 |
+
parser.add_argument("--shift", type=float, default=5.0)
|
| 78 |
+
parser.add_argument("--overwrite", action="store_true")
|
| 79 |
+
return parser.parse_args()
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def resolve(path: Path) -> Path:
|
| 83 |
+
return path if path.is_absolute() else (ROOT / path).resolve()
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
|
| 87 |
+
if not rows:
|
| 88 |
+
return
|
| 89 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 90 |
+
fields: list[str] = []
|
| 91 |
+
for row in rows:
|
| 92 |
+
for key in row:
|
| 93 |
+
if key not in fields:
|
| 94 |
+
fields.append(key)
|
| 95 |
+
with path.open("w", newline="", encoding="utf-8") as handle:
|
| 96 |
+
writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore")
|
| 97 |
+
writer.writeheader()
|
| 98 |
+
writer.writerows(rows)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def cosine_metrics(reference: torch.Tensor, target: torch.Tensor) -> dict[str, float]:
|
| 102 |
+
x = reference.float()
|
| 103 |
+
y = target.float()
|
| 104 |
+
values = F.cosine_similarity(x, y, dim=-1, eps=1e-8)
|
| 105 |
+
quantiles = torch.quantile(values, torch.tensor([0.1, 0.5, 0.9]))
|
| 106 |
+
return {
|
| 107 |
+
"token_cosine_mean": float(values.mean()),
|
| 108 |
+
"token_cosine_p10": float(quantiles[0]),
|
| 109 |
+
"token_cosine_p50": float(quantiles[1]),
|
| 110 |
+
"token_cosine_p90": float(quantiles[2]),
|
| 111 |
+
"global_cosine": float(F.cosine_similarity(x.flatten()[None], y.flatten()[None])),
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class NativeWanRecorder:
|
| 116 |
+
def __init__(
|
| 117 |
+
self,
|
| 118 |
+
model: WanModel,
|
| 119 |
+
layers: list[int],
|
| 120 |
+
max_tokens: int,
|
| 121 |
+
chunk_pairing: str = "matched_slot",
|
| 122 |
+
):
|
| 123 |
+
self.model = model
|
| 124 |
+
self.layers = sorted(set(layers))
|
| 125 |
+
self.max_tokens = int(max_tokens)
|
| 126 |
+
self.chunk_pairing = str(chunk_pairing)
|
| 127 |
+
self.handles = []
|
| 128 |
+
self.active = False
|
| 129 |
+
self.prompt_id = -1
|
| 130 |
+
self.step = -1
|
| 131 |
+
self.timestep = 0.0
|
| 132 |
+
self.rows: list[dict[str, Any]] = []
|
| 133 |
+
self.previous: dict[tuple[int, int], torch.Tensor] = {}
|
| 134 |
+
self.previous_timestep: float | None = None
|
| 135 |
+
self.matched_pairs = {12: 0, 25: 12, 37: 25}
|
| 136 |
+
self.matched_history: dict[
|
| 137 |
+
tuple[int, int, int], tuple[torch.Tensor, float]
|
| 138 |
+
] = {}
|
| 139 |
+
for layer in self.layers:
|
| 140 |
+
if not 0 <= layer < len(model.blocks):
|
| 141 |
+
raise ValueError(f"Layer {layer} outside 0..{len(model.blocks)-1}")
|
| 142 |
+
self.handles.append(model.blocks[layer].register_forward_hook(self._hook(layer)))
|
| 143 |
+
|
| 144 |
+
def reset(self, prompt_id: int) -> None:
|
| 145 |
+
self.active = False
|
| 146 |
+
self.prompt_id = int(prompt_id)
|
| 147 |
+
self.step = -1
|
| 148 |
+
self.timestep = 0.0
|
| 149 |
+
self.rows = []
|
| 150 |
+
self.previous = {}
|
| 151 |
+
self.previous_timestep = None
|
| 152 |
+
self.matched_history = {}
|
| 153 |
+
|
| 154 |
+
def begin(self, step: int, timestep: float) -> None:
|
| 155 |
+
self.step = int(step)
|
| 156 |
+
self.timestep = float(timestep)
|
| 157 |
+
self.active = True
|
| 158 |
+
|
| 159 |
+
def end(self) -> None:
|
| 160 |
+
self.active = False
|
| 161 |
+
self.previous_timestep = self.timestep
|
| 162 |
+
|
| 163 |
+
def close(self) -> None:
|
| 164 |
+
for handle in self.handles:
|
| 165 |
+
handle.remove()
|
| 166 |
+
self.handles.clear()
|
| 167 |
+
|
| 168 |
+
def _sample(self, tokens: torch.Tensor) -> torch.Tensor:
|
| 169 |
+
if self.chunk_pairing == "boundary_to_all":
|
| 170 |
+
if tokens.shape[0] != 3 * 30 * 52:
|
| 171 |
+
raise ValueError(f"Expected 3x30x52 tokens, got {tokens.shape[0]}")
|
| 172 |
+
spatial_count = max(1, self.max_tokens // 3)
|
| 173 |
+
if 30 * 52 <= spatial_count:
|
| 174 |
+
indices = torch.arange(30 * 52, device=tokens.device)
|
| 175 |
+
else:
|
| 176 |
+
indices = torch.linspace(
|
| 177 |
+
0, 30 * 52 - 1, spatial_count, device=tokens.device
|
| 178 |
+
).round().long().unique()
|
| 179 |
+
return (
|
| 180 |
+
tokens.reshape(3, 30 * 52, -1)
|
| 181 |
+
.index_select(1, indices)
|
| 182 |
+
.detach()
|
| 183 |
+
.to("cpu", torch.float16)
|
| 184 |
+
)
|
| 185 |
+
if tokens.shape[0] <= self.max_tokens:
|
| 186 |
+
indices = torch.arange(tokens.shape[0], device=tokens.device)
|
| 187 |
+
else:
|
| 188 |
+
indices = torch.linspace(
|
| 189 |
+
0, tokens.shape[0] - 1, self.max_tokens, device=tokens.device
|
| 190 |
+
).round().long().unique()
|
| 191 |
+
return tokens.index_select(0, indices).detach().to("cpu", torch.float16)
|
| 192 |
+
|
| 193 |
+
def _hook(self, layer: int):
|
| 194 |
+
def hook(_module, _inputs, output) -> None:
|
| 195 |
+
if not self.active:
|
| 196 |
+
return
|
| 197 |
+
feature = output[0] if isinstance(output, (tuple, list)) else output
|
| 198 |
+
if not isinstance(feature, torch.Tensor) or feature.ndim != 3:
|
| 199 |
+
raise ValueError(f"Unexpected layer {layer} output: {type(feature)}")
|
| 200 |
+
# 21 latent frames x (30*52) tokens, grouped into 7 slices of 3 frames.
|
| 201 |
+
per_frame = 30 * 52
|
| 202 |
+
expected = 21 * per_frame
|
| 203 |
+
if feature.shape[1] < expected:
|
| 204 |
+
raise ValueError(f"Expected at least {expected} tokens, got {feature.shape[1]}")
|
| 205 |
+
current: list[torch.Tensor] = []
|
| 206 |
+
for chunk in range(7):
|
| 207 |
+
start = chunk * 3 * per_frame
|
| 208 |
+
stop = start + 3 * per_frame
|
| 209 |
+
current.append(self._sample(feature[0, start:stop]))
|
| 210 |
+
for chunk, sampled in enumerate(current):
|
| 211 |
+
base = {
|
| 212 |
+
"model_family": "self_forcing",
|
| 213 |
+
"model_variant": "wan14b_50",
|
| 214 |
+
"chunk_semantics": "full_video_temporal_slice",
|
| 215 |
+
"prompt_id": self.prompt_id,
|
| 216 |
+
"layer_index": layer,
|
| 217 |
+
"chunk": chunk,
|
| 218 |
+
"target_step": self.step,
|
| 219 |
+
"target_timestep": self.timestep,
|
| 220 |
+
"sample_tokens": int(sampled.numel() // sampled.shape[-1]),
|
| 221 |
+
"feature_dim": int(sampled.shape[1]),
|
| 222 |
+
}
|
| 223 |
+
within = self.previous.get((layer, chunk))
|
| 224 |
+
if within is not None:
|
| 225 |
+
self.rows.append({
|
| 226 |
+
**base,
|
| 227 |
+
"comparison": "within_adjacent",
|
| 228 |
+
"reference_chunk": chunk,
|
| 229 |
+
"reference_step": self.step - 1,
|
| 230 |
+
"reference_timestep": self.previous_timestep,
|
| 231 |
+
**cosine_metrics(within, sampled),
|
| 232 |
+
})
|
| 233 |
+
matched_reference_step = self.matched_pairs.get(self.step)
|
| 234 |
+
matched = (
|
| 235 |
+
None
|
| 236 |
+
if matched_reference_step is None
|
| 237 |
+
else self.matched_history.get((layer, chunk, matched_reference_step))
|
| 238 |
+
)
|
| 239 |
+
if matched is not None:
|
| 240 |
+
matched_feature, matched_timestep = matched
|
| 241 |
+
self.rows.append({
|
| 242 |
+
**base,
|
| 243 |
+
"comparison": "within_matched_gap",
|
| 244 |
+
"reference_chunk": chunk,
|
| 245 |
+
"reference_step": matched_reference_step,
|
| 246 |
+
"reference_timestep": matched_timestep,
|
| 247 |
+
**cosine_metrics(matched_feature, sampled),
|
| 248 |
+
})
|
| 249 |
+
if chunk > 0:
|
| 250 |
+
cross_reference = (
|
| 251 |
+
current[chunk - 1][-1:].expand_as(sampled)
|
| 252 |
+
if self.chunk_pairing == "boundary_to_all"
|
| 253 |
+
else current[chunk - 1]
|
| 254 |
+
)
|
| 255 |
+
self.rows.append({
|
| 256 |
+
**base,
|
| 257 |
+
"comparison": (
|
| 258 |
+
"cross_boundary_to_all"
|
| 259 |
+
if self.chunk_pairing == "boundary_to_all"
|
| 260 |
+
else "cross_same"
|
| 261 |
+
),
|
| 262 |
+
"reference_chunk": chunk - 1,
|
| 263 |
+
"reference_step": self.step,
|
| 264 |
+
"reference_timestep": self.timestep,
|
| 265 |
+
**cosine_metrics(cross_reference, sampled),
|
| 266 |
+
})
|
| 267 |
+
self.previous[(layer, chunk)] = sampled
|
| 268 |
+
if self.step in {0, 12, 25, 37}:
|
| 269 |
+
self.matched_history[(layer, chunk, self.step)] = (
|
| 270 |
+
sampled,
|
| 271 |
+
self.timestep,
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
return hook
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
@torch.inference_mode()
|
| 278 |
+
def encode_prompts(prompts: list[str]) -> tuple[torch.Tensor, torch.Tensor]:
|
| 279 |
+
print("[load] text encoder", flush=True)
|
| 280 |
+
encoder = WanTextEncoder().to(device="cuda", dtype=torch.bfloat16).eval()
|
| 281 |
+
conditional = encoder(text_prompts=prompts)["prompt_embeds"].detach().to("cpu", torch.bfloat16)
|
| 282 |
+
negative = encoder(text_prompts=[
|
| 283 |
+
"色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,"
|
| 284 |
+
"整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,"
|
| 285 |
+
"画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合"
|
| 286 |
+
])["prompt_embeds"].detach().to("cpu", torch.bfloat16)
|
| 287 |
+
del encoder
|
| 288 |
+
gc.collect()
|
| 289 |
+
torch.cuda.empty_cache()
|
| 290 |
+
return conditional, negative
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
@torch.inference_mode()
|
| 294 |
+
def main() -> None:
|
| 295 |
+
args = parse_args()
|
| 296 |
+
checkpoint_dir = resolve(args.checkpoint_dir)
|
| 297 |
+
prompt_path = resolve(args.prompt_path)
|
| 298 |
+
args.output_dir = resolve(args.output_dir)
|
| 299 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 300 |
+
all_prompts = [line.strip() for line in prompt_path.read_text(encoding="utf-8").splitlines() if line.strip()]
|
| 301 |
+
prompt_ids = (
|
| 302 |
+
[int(value) for value in args.prompt_ids.split(",") if value.strip()]
|
| 303 |
+
if args.prompt_ids
|
| 304 |
+
else list(range(args.num_prompts))
|
| 305 |
+
)
|
| 306 |
+
if not prompt_ids or min(prompt_ids) < 0 or max(prompt_ids) >= len(all_prompts):
|
| 307 |
+
raise ValueError(f"Invalid prompt ids {prompt_ids} for {len(all_prompts)} prompts")
|
| 308 |
+
prompts = [all_prompts[index] for index in prompt_ids]
|
| 309 |
+
conditional, negative = encode_prompts(prompts)
|
| 310 |
+
print("[load] Wan14B in bfloat16", flush=True)
|
| 311 |
+
model = WanModel.from_pretrained(
|
| 312 |
+
checkpoint_dir,
|
| 313 |
+
torch_dtype=torch.bfloat16,
|
| 314 |
+
low_cpu_mem_usage=True,
|
| 315 |
+
).eval().requires_grad_(False).to("cuda")
|
| 316 |
+
recorder = NativeWanRecorder(
|
| 317 |
+
model,
|
| 318 |
+
args.layers,
|
| 319 |
+
args.max_tokens,
|
| 320 |
+
chunk_pairing=args.chunk_pairing,
|
| 321 |
+
)
|
| 322 |
+
scheduler = FlowUniPCMultistepScheduler(
|
| 323 |
+
num_train_timesteps=1000, shift=1, use_dynamic_shifting=False
|
| 324 |
+
)
|
| 325 |
+
scheduler.set_timesteps(args.sampling_steps, device="cuda", shift=args.shift)
|
| 326 |
+
timesteps = scheduler.timesteps
|
| 327 |
+
manifest = {
|
| 328 |
+
"model_family": "self_forcing",
|
| 329 |
+
"model_variant": "wan14b_50",
|
| 330 |
+
"chunk_semantics": "full_video_temporal_slice",
|
| 331 |
+
"checkpoint_dir": str(checkpoint_dir),
|
| 332 |
+
"prompt_path": str(prompt_path),
|
| 333 |
+
"num_prompts": len(prompts),
|
| 334 |
+
"prompt_ids": prompt_ids,
|
| 335 |
+
"num_frames": 21,
|
| 336 |
+
"num_chunks": 7,
|
| 337 |
+
"sampling_steps": args.sampling_steps,
|
| 338 |
+
"timesteps": [float(value) for value in timesteps.detach().cpu()],
|
| 339 |
+
"layers": args.layers,
|
| 340 |
+
"layer_roles": ["early", "middle", "late", "final"],
|
| 341 |
+
"max_tokens": args.max_tokens,
|
| 342 |
+
"chunk_pairing": args.chunk_pairing,
|
| 343 |
+
"seed": args.seed,
|
| 344 |
+
"guide_scale": args.guide_scale,
|
| 345 |
+
"shift": args.shift,
|
| 346 |
+
"physical_gpu": args.gpu,
|
| 347 |
+
"conditional_stream_only": True,
|
| 348 |
+
}
|
| 349 |
+
(args.output_dir / "run_manifest.json").write_text(
|
| 350 |
+
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
| 351 |
+
)
|
| 352 |
+
all_rows: list[dict[str, Any]] = []
|
| 353 |
+
seq_len = 21 * 30 * 52
|
| 354 |
+
try:
|
| 355 |
+
for local_index, (prompt_id, prompt) in enumerate(zip(prompt_ids, prompts)):
|
| 356 |
+
run_dir = args.output_dir / "runs" / f"prompt_{prompt_id:04d}"
|
| 357 |
+
pair_path = run_dir / "feature_pair_metrics.csv"
|
| 358 |
+
if pair_path.exists() and not args.overwrite:
|
| 359 |
+
with pair_path.open(newline="", encoding="utf-8") as handle:
|
| 360 |
+
all_rows.extend(csv.DictReader(handle))
|
| 361 |
+
print(f"[skip] prompt {prompt_id}", flush=True)
|
| 362 |
+
continue
|
| 363 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 364 |
+
set_seed(args.seed)
|
| 365 |
+
generator = torch.Generator(device="cuda").manual_seed(args.seed)
|
| 366 |
+
latents = [torch.randn(
|
| 367 |
+
16, 21, 60, 104,
|
| 368 |
+
dtype=torch.float32, device="cuda", generator=generator,
|
| 369 |
+
)]
|
| 370 |
+
cond = conditional[local_index].to("cuda")
|
| 371 |
+
uncond = negative[0].to("cuda")
|
| 372 |
+
recorder.reset(prompt_id)
|
| 373 |
+
scheduler.set_timesteps(args.sampling_steps, device="cuda", shift=args.shift)
|
| 374 |
+
torch.cuda.reset_peak_memory_stats()
|
| 375 |
+
torch.cuda.synchronize()
|
| 376 |
+
started = time.perf_counter()
|
| 377 |
+
with torch.amp.autocast("cuda", dtype=torch.bfloat16):
|
| 378 |
+
for step, timestep in enumerate(scheduler.timesteps):
|
| 379 |
+
t = torch.stack([timestep])
|
| 380 |
+
recorder.begin(step, float(timestep))
|
| 381 |
+
pred_cond = model(latents, t=t, context=[cond], seq_len=seq_len)[0]
|
| 382 |
+
recorder.end()
|
| 383 |
+
pred_uncond = model(latents, t=t, context=[uncond], seq_len=seq_len)[0]
|
| 384 |
+
prediction = pred_uncond + args.guide_scale * (pred_cond - pred_uncond)
|
| 385 |
+
next_latent = scheduler.step(
|
| 386 |
+
prediction.unsqueeze(0),
|
| 387 |
+
timestep,
|
| 388 |
+
latents[0].unsqueeze(0),
|
| 389 |
+
return_dict=False,
|
| 390 |
+
generator=generator,
|
| 391 |
+
)[0]
|
| 392 |
+
latents = [next_latent.squeeze(0)]
|
| 393 |
+
torch.cuda.synchronize()
|
| 394 |
+
elapsed = time.perf_counter() - started
|
| 395 |
+
rows = list(recorder.rows)
|
| 396 |
+
write_csv(pair_path, rows)
|
| 397 |
+
metadata = {
|
| 398 |
+
"prompt_id": prompt_id,
|
| 399 |
+
"prompt": prompt,
|
| 400 |
+
"elapsed_s": elapsed,
|
| 401 |
+
"peak_gpu_gib": torch.cuda.max_memory_allocated() / 1024**3,
|
| 402 |
+
"pair_rows": len(rows),
|
| 403 |
+
}
|
| 404 |
+
(run_dir / "run_metadata.json").write_text(
|
| 405 |
+
json.dumps(metadata, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
| 406 |
+
)
|
| 407 |
+
all_rows.extend(rows)
|
| 408 |
+
print(
|
| 409 |
+
f"[done] prompt={prompt_id} rows={len(rows)} elapsed={elapsed:.1f}s "
|
| 410 |
+
f"peak={metadata['peak_gpu_gib']:.1f}GiB",
|
| 411 |
+
flush=True,
|
| 412 |
+
)
|
| 413 |
+
del latents, cond, uncond
|
| 414 |
+
torch.cuda.empty_cache()
|
| 415 |
+
finally:
|
| 416 |
+
recorder.close()
|
| 417 |
+
write_csv(args.output_dir / "feature_pair_metrics.csv", all_rows)
|
| 418 |
+
print(f"[complete] {args.output_dir}", flush=True)
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
if __name__ == "__main__":
|
| 422 |
+
main()
|
scripts/summarize_boundary_to_all_cosine.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Summarize matched-step and previous-boundary-to-all cosine consistently."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
import json
|
| 9 |
+
from collections import defaultdict
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
import torch.nn.functional as F
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
ROLE_MAP = {
|
| 19 |
+
("self_forcing", "wan14b50"): {9: "early", 19: "middle", 29: "late", 39: "final"},
|
| 20 |
+
("self_forcing", "dmd4"): {7: "early", 14: "middle", 22: "late", 29: "final"},
|
| 21 |
+
("causal_forcing", "ar50"): {7: "early", 14: "middle", 22: "late", 29: "final"},
|
| 22 |
+
("causal_forcing", "dmd4"): {7: "early", 14: "middle", 22: "late", 29: "final"},
|
| 23 |
+
}
|
| 24 |
+
ORDER = [
|
| 25 |
+
("self_forcing", "wan14b50"),
|
| 26 |
+
("self_forcing", "dmd4"),
|
| 27 |
+
("causal_forcing", "ar50"),
|
| 28 |
+
("causal_forcing", "dmd4"),
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def parse_args() -> argparse.Namespace:
|
| 33 |
+
parser = argparse.ArgumentParser()
|
| 34 |
+
parser.add_argument("--self_dmd_root", type=Path, required=True)
|
| 35 |
+
parser.add_argument("--self_wan_root", type=Path, required=True)
|
| 36 |
+
parser.add_argument("--causal_ar_root", type=Path, required=True)
|
| 37 |
+
parser.add_argument("--causal_dmd_root", type=Path, required=True)
|
| 38 |
+
parser.add_argument("--output_root", type=Path, required=True)
|
| 39 |
+
return parser.parse_args()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
|
| 43 |
+
if not rows:
|
| 44 |
+
return
|
| 45 |
+
fields: list[str] = []
|
| 46 |
+
for row in rows:
|
| 47 |
+
for key in row:
|
| 48 |
+
if key not in fields:
|
| 49 |
+
fields.append(key)
|
| 50 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 51 |
+
with path.open("w", newline="", encoding="utf-8") as handle:
|
| 52 |
+
writer = csv.DictWriter(handle, fieldnames=fields)
|
| 53 |
+
writer.writeheader()
|
| 54 |
+
writer.writerows(rows)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def self_dmd_rows(root: Path) -> list[dict[str, Any]]:
|
| 58 |
+
rows = []
|
| 59 |
+
role_map = ROLE_MAP[("self_forcing", "dmd4")]
|
| 60 |
+
for path in sorted((root / "runs").glob("prompt_*.pt")):
|
| 61 |
+
state = torch.load(path, map_location="cpu", weights_only=False)
|
| 62 |
+
prompt_id = int(state["run_index"])
|
| 63 |
+
coords = state["sample_coords"]["hidden"]
|
| 64 |
+
spatial_counts = [int((coords[:, 0] == slot).sum()) for slot in range(3)]
|
| 65 |
+
if len(set(spatial_counts)) != 1:
|
| 66 |
+
raise ValueError(f"Unbalanced temporal sampling in {path}: {spatial_counts}")
|
| 67 |
+
spatial_count = spatial_counts[0]
|
| 68 |
+
for layer, role in role_map.items():
|
| 69 |
+
records = state["records"][f"block_{layer}_hidden"]
|
| 70 |
+
for chunk in range(1, 7):
|
| 71 |
+
for step in range(1, 4):
|
| 72 |
+
target = records[f"{chunk}:{step}"].float().reshape(3, spatial_count, -1)
|
| 73 |
+
previous_step = records[f"{chunk}:{step - 1}"].float().reshape(
|
| 74 |
+
3, spatial_count, -1
|
| 75 |
+
)
|
| 76 |
+
previous_chunk = records[f"{chunk - 1}:{step}"].float().reshape(
|
| 77 |
+
3, spatial_count, -1
|
| 78 |
+
)
|
| 79 |
+
for comparison, reference in (
|
| 80 |
+
("within_adjacent", previous_step),
|
| 81 |
+
("cross_boundary_to_all", previous_chunk[-1:].expand_as(target)),
|
| 82 |
+
):
|
| 83 |
+
rows.append({
|
| 84 |
+
"model_family": "self_forcing",
|
| 85 |
+
"model_variant": "dmd4",
|
| 86 |
+
"layer_role": role,
|
| 87 |
+
"layer_index": layer,
|
| 88 |
+
"prompt_id": prompt_id,
|
| 89 |
+
"comparison": comparison,
|
| 90 |
+
"target_chunk": chunk,
|
| 91 |
+
"target_step": step,
|
| 92 |
+
"token_cosine_mean": float(
|
| 93 |
+
F.cosine_similarity(target, reference, dim=-1).mean()
|
| 94 |
+
),
|
| 95 |
+
})
|
| 96 |
+
return rows
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def direct_rows(root: Path, family: str, variant: str) -> list[dict[str, Any]]:
|
| 100 |
+
rows = []
|
| 101 |
+
role_map = ROLE_MAP[(family, variant)]
|
| 102 |
+
paths = sorted((root / "runs").glob("prompt_*/feature_pair_metrics.csv"))
|
| 103 |
+
if not paths:
|
| 104 |
+
paths = sorted(root.glob("shard_*/runs/prompt_*/feature_pair_metrics.csv"))
|
| 105 |
+
for path in paths:
|
| 106 |
+
with path.open(newline="", encoding="utf-8") as handle:
|
| 107 |
+
for raw in csv.DictReader(handle):
|
| 108 |
+
comparison = raw["comparison"]
|
| 109 |
+
if comparison not in {"within_adjacent", "cross_boundary_to_all"}:
|
| 110 |
+
continue
|
| 111 |
+
step = int(raw["target_step"])
|
| 112 |
+
if step < 1:
|
| 113 |
+
continue
|
| 114 |
+
layer = int(raw["layer_index"])
|
| 115 |
+
rows.append({
|
| 116 |
+
"model_family": family,
|
| 117 |
+
"model_variant": variant,
|
| 118 |
+
"layer_role": role_map[layer],
|
| 119 |
+
"layer_index": layer,
|
| 120 |
+
"prompt_id": int(raw["prompt_id"]),
|
| 121 |
+
"comparison": comparison,
|
| 122 |
+
"target_chunk": int(raw["chunk"]),
|
| 123 |
+
"target_step": step,
|
| 124 |
+
"token_cosine_mean": float(raw["token_cosine_mean"]),
|
| 125 |
+
})
|
| 126 |
+
return rows
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def bootstrap(values: list[float], seed: int, rounds: int = 10000) -> tuple[float, float, float]:
|
| 130 |
+
array = np.asarray(values, dtype=np.float64)
|
| 131 |
+
generator = np.random.default_rng(seed)
|
| 132 |
+
indices = generator.integers(0, len(array), size=(rounds, len(array)))
|
| 133 |
+
means = array[indices].mean(axis=1)
|
| 134 |
+
return float(array.mean()), float(np.quantile(means, 0.025)), float(np.quantile(means, 0.975))
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def main() -> None:
|
| 138 |
+
args = parse_args()
|
| 139 |
+
output = args.output_root.resolve()
|
| 140 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 141 |
+
rows = self_dmd_rows(args.self_dmd_root.resolve())
|
| 142 |
+
rows += direct_rows(args.self_wan_root.resolve(), "self_forcing", "wan14b50")
|
| 143 |
+
rows += direct_rows(args.causal_ar_root.resolve(), "causal_forcing", "ar50")
|
| 144 |
+
rows += direct_rows(args.causal_dmd_root.resolve(), "causal_forcing", "dmd4")
|
| 145 |
+
|
| 146 |
+
prompt_groups: dict[tuple[Any, ...], list[float]] = defaultdict(list)
|
| 147 |
+
for row in rows:
|
| 148 |
+
key = (
|
| 149 |
+
row["model_family"], row["model_variant"], row["layer_role"],
|
| 150 |
+
row["layer_index"], row["prompt_id"], row["comparison"],
|
| 151 |
+
)
|
| 152 |
+
prompt_groups[key].append(float(row["token_cosine_mean"]))
|
| 153 |
+
prompt_rows = []
|
| 154 |
+
for key, values in prompt_groups.items():
|
| 155 |
+
prompt_rows.append({
|
| 156 |
+
**dict(zip(
|
| 157 |
+
["model_family", "model_variant", "layer_role", "layer_index", "prompt_id", "comparison"],
|
| 158 |
+
key,
|
| 159 |
+
)),
|
| 160 |
+
"pair_count": len(values),
|
| 161 |
+
"token_cosine_mean": float(np.mean(values)),
|
| 162 |
+
})
|
| 163 |
+
|
| 164 |
+
groups: dict[tuple[Any, ...], list[float]] = defaultdict(list)
|
| 165 |
+
for row in prompt_rows:
|
| 166 |
+
key = (row["model_family"], row["model_variant"], row["layer_role"], row["comparison"])
|
| 167 |
+
groups[key].append(float(row["token_cosine_mean"]))
|
| 168 |
+
summary = []
|
| 169 |
+
for key, values in groups.items():
|
| 170 |
+
avg, low, high = bootstrap(values, 20260828 + sum(map(ord, "".join(map(str, key)))))
|
| 171 |
+
summary.append({
|
| 172 |
+
**dict(zip(["model_family", "model_variant", "layer_role", "comparison"], key)),
|
| 173 |
+
"prompt_count": len(values),
|
| 174 |
+
"token_cosine_mean": avg,
|
| 175 |
+
"ci95_low": low,
|
| 176 |
+
"ci95_high": high,
|
| 177 |
+
})
|
| 178 |
+
|
| 179 |
+
lookup = {
|
| 180 |
+
(row["model_family"], row["model_variant"], row["layer_role"], row["comparison"]): row
|
| 181 |
+
for row in summary
|
| 182 |
+
}
|
| 183 |
+
table_rows = []
|
| 184 |
+
for family, variant in ORDER:
|
| 185 |
+
item: dict[str, Any] = {"model_family": family, "model_variant": variant}
|
| 186 |
+
for role in ("early", "middle", "late", "final"):
|
| 187 |
+
step = lookup[(family, variant, role, "within_adjacent")]
|
| 188 |
+
chunk = lookup[(family, variant, role, "cross_boundary_to_all")]
|
| 189 |
+
item[f"{role}_step"] = step["token_cosine_mean"]
|
| 190 |
+
item[f"{role}_chunk"] = chunk["token_cosine_mean"]
|
| 191 |
+
table_rows.append(item)
|
| 192 |
+
|
| 193 |
+
write_csv(output / "boundary_pair_metrics.csv", rows)
|
| 194 |
+
write_csv(output / "boundary_prompt_summary.csv", prompt_rows)
|
| 195 |
+
write_csv(output / "boundary_cosine_summary.csv", summary)
|
| 196 |
+
write_csv(output / "boundary_cosine_table.csv", table_rows)
|
| 197 |
+
config = {
|
| 198 |
+
"pairing": "current slot s vs previous chunk last slot; matched spatial coordinate",
|
| 199 |
+
"support": "target_chunk>=1, target_step>=1",
|
| 200 |
+
"prompt_first_aggregation": True,
|
| 201 |
+
"prompt_count_expected": 10,
|
| 202 |
+
"row_count": len(rows),
|
| 203 |
+
}
|
| 204 |
+
(output / "config.json").write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
| 205 |
+
print(f"[complete] {output}: rows={len(rows)}", flush=True)
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
main()
|
scripts/summarize_conditional_probe.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Summarize Linear/Ridge and nonlinear conditional probe fold results."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
import itertools
|
| 9 |
+
import json
|
| 10 |
+
import math
|
| 11 |
+
from collections import defaultdict
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
import matplotlib
|
| 16 |
+
|
| 17 |
+
matplotlib.use("Agg")
|
| 18 |
+
import matplotlib.pyplot as plt
|
| 19 |
+
import numpy as np
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
FAMILIES = ("self_forcing", "causal_forcing", "hy_worldplay")
|
| 23 |
+
ROLES = ("early", "middle", "late", "final")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def read_csv(path: Path) -> list[dict[str, Any]]:
|
| 27 |
+
with path.open(newline="", encoding="utf-8") as handle:
|
| 28 |
+
rows = list(csv.DictReader(handle))
|
| 29 |
+
for row in rows:
|
| 30 |
+
for key in ("target_step", "held_out_prompt", "seed", "layer_index", "test_tokens"):
|
| 31 |
+
if key in row:
|
| 32 |
+
row[key] = int(row[key])
|
| 33 |
+
for key in ("mse", "nMSE", "nRMSE", "r2", "cosine"):
|
| 34 |
+
row[key] = float(row[key])
|
| 35 |
+
return rows
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
|
| 39 |
+
if not rows:
|
| 40 |
+
return
|
| 41 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 42 |
+
fields: list[str] = []
|
| 43 |
+
for row in rows:
|
| 44 |
+
for key in row:
|
| 45 |
+
if key not in fields:
|
| 46 |
+
fields.append(key)
|
| 47 |
+
with path.open("w", newline="", encoding="utf-8") as handle:
|
| 48 |
+
writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore")
|
| 49 |
+
writer.writeheader()
|
| 50 |
+
writer.writerows(rows)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def bootstrap(values: list[float], seed: int, rounds: int = 10000):
|
| 54 |
+
values = np.asarray(values, dtype=np.float64)
|
| 55 |
+
rng = np.random.default_rng(seed)
|
| 56 |
+
if values.size == 0:
|
| 57 |
+
return float("nan"), float("nan"), float("nan")
|
| 58 |
+
indices = rng.integers(0, values.size, size=(rounds, values.size))
|
| 59 |
+
means = values[indices].mean(axis=1)
|
| 60 |
+
return float(values.mean()), float(np.quantile(means, 0.025)), float(np.quantile(means, 0.975))
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def exact_signflip(values: list[float]) -> float:
|
| 64 |
+
values = np.asarray(values, dtype=np.float64)
|
| 65 |
+
values = values[np.isfinite(values)]
|
| 66 |
+
if not values.size:
|
| 67 |
+
return float("nan")
|
| 68 |
+
observed = abs(float(values.mean()))
|
| 69 |
+
exceed = 0
|
| 70 |
+
total = 1 << int(values.size)
|
| 71 |
+
for mask in range(total):
|
| 72 |
+
signed = np.asarray(
|
| 73 |
+
[value if (mask >> index) & 1 else -value for index, value in enumerate(values)]
|
| 74 |
+
)
|
| 75 |
+
if abs(float(signed.mean())) >= observed - 1e-15:
|
| 76 |
+
exceed += 1
|
| 77 |
+
return float((exceed + 1) / (total + 1))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def prompt_metric_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 81 |
+
"""Average seeds within each prompt, keeping prompt as statistical unit."""
|
| 82 |
+
grouped: dict[tuple, list[dict[str, Any]]] = defaultdict(list)
|
| 83 |
+
for row in rows:
|
| 84 |
+
grouped[
|
| 85 |
+
(
|
| 86 |
+
row["method"], row["model_family"], row["layer_role"],
|
| 87 |
+
row["target_step"], row["probe"], row["held_out_prompt"],
|
| 88 |
+
)
|
| 89 |
+
].append(row)
|
| 90 |
+
result = []
|
| 91 |
+
for key, values in sorted(grouped.items(), key=lambda item: tuple(map(str, item[0]))):
|
| 92 |
+
method, family, role, step, probe, prompt = key
|
| 93 |
+
item = {
|
| 94 |
+
"method": method,
|
| 95 |
+
"model_family": family,
|
| 96 |
+
"layer_role": role,
|
| 97 |
+
"target_step": step,
|
| 98 |
+
"probe": probe,
|
| 99 |
+
"held_out_prompt": prompt,
|
| 100 |
+
"seed_count": len(values),
|
| 101 |
+
}
|
| 102 |
+
for metric in ("mse", "nMSE", "nRMSE", "r2", "cosine"):
|
| 103 |
+
item[metric] = float(np.mean([float(value[metric]) for value in values]))
|
| 104 |
+
result.append(item)
|
| 105 |
+
return result
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def gain_rows(prompt_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 109 |
+
baseline_name = {"linear": "within_affine", "nonlinear": "step_only"}
|
| 110 |
+
grouped = defaultdict(dict)
|
| 111 |
+
for row in prompt_rows:
|
| 112 |
+
grouped[
|
| 113 |
+
(row["method"], row["model_family"], row["layer_role"], row["target_step"], row["held_out_prompt"])
|
| 114 |
+
][row["probe"]] = row
|
| 115 |
+
result = []
|
| 116 |
+
for key, probes in sorted(grouped.items(), key=lambda item: tuple(map(str, item[0]))):
|
| 117 |
+
method, family, role, step, prompt = key
|
| 118 |
+
baseline = probes.get(baseline_name[method])
|
| 119 |
+
if baseline is None:
|
| 120 |
+
continue
|
| 121 |
+
for probe, current in probes.items():
|
| 122 |
+
reference_mse = float(baseline["mse"])
|
| 123 |
+
current_mse = float(current["mse"])
|
| 124 |
+
result.append({
|
| 125 |
+
"method": method,
|
| 126 |
+
"model_family": family,
|
| 127 |
+
"layer_role": role,
|
| 128 |
+
"target_step": step,
|
| 129 |
+
"held_out_prompt": prompt,
|
| 130 |
+
"baseline_probe": baseline_name[method],
|
| 131 |
+
"probe": probe,
|
| 132 |
+
"baseline_mse": reference_mse,
|
| 133 |
+
"probe_mse": current_mse,
|
| 134 |
+
"gain": (reference_mse - current_mse) / max(reference_mse, 1e-12),
|
| 135 |
+
"delta_r2": float(current["r2"]) - float(baseline["r2"]),
|
| 136 |
+
})
|
| 137 |
+
return result
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def summarize_gains(gains: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 141 |
+
grouped = defaultdict(list)
|
| 142 |
+
for row in gains:
|
| 143 |
+
grouped[(row["method"], row["model_family"], row["layer_role"], row["target_step"], row["probe"])].append(row)
|
| 144 |
+
result = []
|
| 145 |
+
for key, values in sorted(grouped.items(), key=lambda item: tuple(map(str, item[0]))):
|
| 146 |
+
method, family, role, step, probe = key
|
| 147 |
+
values = sorted(values, key=lambda row: row["held_out_prompt"])
|
| 148 |
+
numbers = [float(row["gain"]) for row in values]
|
| 149 |
+
mean, low, high = bootstrap(numbers, 1000 + sum(ord(ch) for ch in str(key)))
|
| 150 |
+
result.append({
|
| 151 |
+
"method": method,
|
| 152 |
+
"model_family": family,
|
| 153 |
+
"layer_role": role,
|
| 154 |
+
"target_step": step,
|
| 155 |
+
"probe": probe,
|
| 156 |
+
"baseline_probe": values[0]["baseline_probe"],
|
| 157 |
+
"prompt_count": len(numbers),
|
| 158 |
+
"gain_mean": mean,
|
| 159 |
+
"gain_ci95_low": low,
|
| 160 |
+
"gain_ci95_high": high,
|
| 161 |
+
"wins": int(sum(number > 0 for number in numbers)),
|
| 162 |
+
"signflip_p": exact_signflip(numbers),
|
| 163 |
+
})
|
| 164 |
+
return result
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def summarize_layer_gains(gains: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 168 |
+
"""Average target steps inside each prompt, then summarize across prompts."""
|
| 169 |
+
prompt_groups = defaultdict(list)
|
| 170 |
+
for row in gains:
|
| 171 |
+
prompt_groups[
|
| 172 |
+
(
|
| 173 |
+
row["method"], row["model_family"], row["layer_role"],
|
| 174 |
+
row["probe"], row["held_out_prompt"], row["baseline_probe"],
|
| 175 |
+
)
|
| 176 |
+
].append(float(row["gain"]))
|
| 177 |
+
groups = defaultdict(list)
|
| 178 |
+
for key, values in prompt_groups.items():
|
| 179 |
+
method, family, role, probe, _prompt, baseline = key
|
| 180 |
+
groups[(method, family, role, probe, baseline)].append(float(np.mean(values)))
|
| 181 |
+
result = []
|
| 182 |
+
for key, values in sorted(groups.items(), key=lambda item: tuple(map(str, item[0]))):
|
| 183 |
+
method, family, role, probe, baseline = key
|
| 184 |
+
mean, low, high = bootstrap(values, 17000 + sum(ord(ch) for ch in str(key)))
|
| 185 |
+
result.append({
|
| 186 |
+
"method": method,
|
| 187 |
+
"model_family": family,
|
| 188 |
+
"layer_role": role,
|
| 189 |
+
"probe": probe,
|
| 190 |
+
"baseline_probe": baseline,
|
| 191 |
+
"target_step_count": 3,
|
| 192 |
+
"prompt_count": len(values),
|
| 193 |
+
"gain_mean": mean,
|
| 194 |
+
"gain_ci95_low": low,
|
| 195 |
+
"gain_ci95_high": high,
|
| 196 |
+
"wins": int(sum(value > 0 for value in values)),
|
| 197 |
+
"signflip_p": exact_signflip(values),
|
| 198 |
+
})
|
| 199 |
+
return result
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def summarize_metrics(prompt_rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 203 |
+
grouped = defaultdict(list)
|
| 204 |
+
for row in prompt_rows:
|
| 205 |
+
grouped[(row["method"], row["model_family"], row["layer_role"], row["target_step"], row["probe"])].append(row)
|
| 206 |
+
result = []
|
| 207 |
+
for key, values in sorted(grouped.items(), key=lambda item: tuple(map(str, item[0]))):
|
| 208 |
+
method, family, role, step, probe = key
|
| 209 |
+
item = {
|
| 210 |
+
"method": method,
|
| 211 |
+
"model_family": family,
|
| 212 |
+
"layer_role": role,
|
| 213 |
+
"target_step": step,
|
| 214 |
+
"probe": probe,
|
| 215 |
+
"prompt_count": len(values),
|
| 216 |
+
}
|
| 217 |
+
for metric_index, metric in enumerate(("mse", "nMSE", "nRMSE", "r2", "cosine")):
|
| 218 |
+
numbers = [float(row[metric]) for row in values]
|
| 219 |
+
mean, low, high = bootstrap(numbers, 9000 + metric_index + sum(ord(ch) for ch in str(key)))
|
| 220 |
+
item[f"{metric}_mean"] = mean
|
| 221 |
+
item[f"{metric}_ci95_low"] = low
|
| 222 |
+
item[f"{metric}_ci95_high"] = high
|
| 223 |
+
result.append(item)
|
| 224 |
+
return result
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def plot_primary(gains: list[dict[str, Any]], output: Path) -> None:
|
| 228 |
+
fig, axes = plt.subplots(1, 2, figsize=(14, 5), sharey=True)
|
| 229 |
+
for ax, method, title in zip(axes, ("linear", "nonlinear"), ("Linear/Ridge", "Nonlinear MLP")):
|
| 230 |
+
selected = [
|
| 231 |
+
row for row in gains
|
| 232 |
+
if row["method"] == method and row["probe"] in ({"fusion_same"} if method == "linear" else {"both_correct"})
|
| 233 |
+
]
|
| 234 |
+
x = np.arange(len(ROLES))
|
| 235 |
+
width = 0.24
|
| 236 |
+
for family_index, family in enumerate(FAMILIES):
|
| 237 |
+
values = []
|
| 238 |
+
lows = []
|
| 239 |
+
highs = []
|
| 240 |
+
for role in ROLES:
|
| 241 |
+
group = [row for row in selected if row["model_family"] == family and row["layer_role"] == role]
|
| 242 |
+
nums = [float(row["gain"]) for row in group]
|
| 243 |
+
mean, low, high = bootstrap(nums, 1234 + family_index * 100 + ROLES.index(role))
|
| 244 |
+
values.append(mean)
|
| 245 |
+
lows.append(mean - low)
|
| 246 |
+
highs.append(high - mean)
|
| 247 |
+
pos = x + (family_index - (len(FAMILIES) - 1) / 2) * width
|
| 248 |
+
ax.bar(pos, values, width, yerr=[lows, highs], capsize=3, label=family)
|
| 249 |
+
ax.axhline(0, color="black", linewidth=0.8)
|
| 250 |
+
ax.set_xticks(x, ROLES)
|
| 251 |
+
ax.set_ylabel("Gain vs step-only baseline" if method == "nonlinear" else "Gain vs within-affine baseline")
|
| 252 |
+
ax.set_title(title)
|
| 253 |
+
ax.grid(axis="y", alpha=0.25)
|
| 254 |
+
axes[1].legend(fontsize=9)
|
| 255 |
+
fig.tight_layout()
|
| 256 |
+
fig.savefig(output, dpi=180)
|
| 257 |
+
plt.close(fig)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def plot_controls(gains: list[dict[str, Any]], output: Path) -> None:
|
| 261 |
+
probes = ["fusion_same", "fusion_step_duplicate", "fusion_wrong_step", "fusion_distant", "fusion_batch_shuffle", "fusion_zero", "fusion_noise"]
|
| 262 |
+
labels = {
|
| 263 |
+
"fusion_same": "correct",
|
| 264 |
+
"fusion_step_duplicate": "step duplicate",
|
| 265 |
+
"fusion_wrong_step": "wrong step",
|
| 266 |
+
"fusion_distant": "distant",
|
| 267 |
+
"fusion_batch_shuffle": "other video",
|
| 268 |
+
"fusion_zero": "zero",
|
| 269 |
+
"fusion_noise": "noise",
|
| 270 |
+
}
|
| 271 |
+
fig, axes = plt.subplots(
|
| 272 |
+
1,
|
| 273 |
+
len(FAMILIES),
|
| 274 |
+
figsize=(5.7 * len(FAMILIES), 5),
|
| 275 |
+
sharey=True,
|
| 276 |
+
)
|
| 277 |
+
axes = np.atleast_1d(axes)
|
| 278 |
+
for ax, family in zip(axes, FAMILIES):
|
| 279 |
+
values = []
|
| 280 |
+
errors = []
|
| 281 |
+
for probe in probes:
|
| 282 |
+
group = [row for row in gains if row["method"] == "linear" and row["model_family"] == family and row["layer_role"] == "final" and row["probe"] == probe]
|
| 283 |
+
nums = [float(row["gain"]) for row in group]
|
| 284 |
+
mean, low, high = bootstrap(nums, 4000 + probes.index(probe))
|
| 285 |
+
values.append(mean)
|
| 286 |
+
errors.append((mean - low, high - mean))
|
| 287 |
+
y = np.arange(len(probes))
|
| 288 |
+
ax.errorbar(values, y, xerr=np.asarray(errors).T, fmt="o", capsize=3)
|
| 289 |
+
ax.axvline(0, color="black", linewidth=0.8)
|
| 290 |
+
ax.set_yticks(y, [labels[p] for p in probes])
|
| 291 |
+
ax.set_title(family)
|
| 292 |
+
ax.grid(axis="x", alpha=0.25)
|
| 293 |
+
axes[0].set_xlabel("Linear MSE gain")
|
| 294 |
+
fig.tight_layout()
|
| 295 |
+
fig.savefig(output, dpi=180)
|
| 296 |
+
plt.close(fig)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def build_report(metrics: list[dict[str, Any]], gains: list[dict[str, Any]], output: Path, config: dict[str, Any]) -> None:
|
| 300 |
+
lines = [
|
| 301 |
+
"# Conditional prediction and incremental chunk information",
|
| 302 |
+
"",
|
| 303 |
+
"This report uses 10 prompt-grouped held-out folds. The test prompt and its",
|
| 304 |
+
"other-video donor are excluded from training; nonlinear seeds are averaged",
|
| 305 |
+
"within prompt before confidence intervals are computed.",
|
| 306 |
+
"",
|
| 307 |
+
"The primary endpoint is `fusion_same` vs `within_affine` for Linear/Ridge",
|
| 308 |
+
"and `both_correct` vs `step_only` for the nonlinear MLP.",
|
| 309 |
+
"",
|
| 310 |
+
"| method | family | role | step | probe | gain | 95% CI | wins | sign-flip p |",
|
| 311 |
+
"|---|---|---|---:|---|---:|---|---:|---:|",
|
| 312 |
+
]
|
| 313 |
+
primary = [
|
| 314 |
+
row for row in gains
|
| 315 |
+
if (row["method"] == "linear" and row["probe"] == "fusion_same")
|
| 316 |
+
or (row["method"] == "nonlinear" and row["probe"] == "both_correct")
|
| 317 |
+
]
|
| 318 |
+
for row in primary:
|
| 319 |
+
lines.append(
|
| 320 |
+
f"| {row['method']} | {row['model_family']} | {row['layer_role']} | {row['target_step']} | "
|
| 321 |
+
f"{row['probe']} | {row['gain_mean']:.4f} | [{row['gain_ci95_low']:.4f}, {row['gain_ci95_high']:.4f}] | "
|
| 322 |
+
f"{row['wins']}/{row['prompt_count']} | {row['signflip_p']:.4f} |"
|
| 323 |
+
)
|
| 324 |
+
lines += [
|
| 325 |
+
"",
|
| 326 |
+
"Interpretation: positive gain means that adding the auxiliary feature lowers held-out MSE.",
|
| 327 |
+
"The exact sign-flip test treats prompt, not token, as the independent unit.",
|
| 328 |
+
"Absolute MSE is not compared across model families because feature dimensions and",
|
| 329 |
+
"conditioning paths differ.",
|
| 330 |
+
"",
|
| 331 |
+
"## Files",
|
| 332 |
+
"",
|
| 333 |
+
"- `probe_folds_unified.csv`",
|
| 334 |
+
"- `probe_prompt_averaged.csv`",
|
| 335 |
+
"- `probe_metrics_summary.csv`",
|
| 336 |
+
"- `probe_gain_summary.csv`",
|
| 337 |
+
"- `conditional_gain_by_layer.png`",
|
| 338 |
+
"- `conditional_control_comparison.png`",
|
| 339 |
+
"",
|
| 340 |
+
"```json",
|
| 341 |
+
json.dumps(config, indent=2, ensure_ascii=False),
|
| 342 |
+
"```",
|
| 343 |
+
]
|
| 344 |
+
output.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def main() -> None:
|
| 348 |
+
global FAMILIES
|
| 349 |
+
parser = argparse.ArgumentParser()
|
| 350 |
+
parser.add_argument("--linear_csv", type=Path, required=True)
|
| 351 |
+
parser.add_argument("--nonlinear_dir", type=Path, required=True)
|
| 352 |
+
parser.add_argument("--output_dir", type=Path, required=True)
|
| 353 |
+
parser.add_argument("--families", default=",".join(FAMILIES))
|
| 354 |
+
parser.add_argument(
|
| 355 |
+
"--chunk_pairing",
|
| 356 |
+
choices=("matched_slot", "boundary_to_all"),
|
| 357 |
+
default="matched_slot",
|
| 358 |
+
)
|
| 359 |
+
args = parser.parse_args()
|
| 360 |
+
requested_families = tuple(
|
| 361 |
+
value.strip() for value in args.families.split(",") if value.strip()
|
| 362 |
+
)
|
| 363 |
+
unknown = set(requested_families) - set(FAMILIES)
|
| 364 |
+
if not requested_families or unknown:
|
| 365 |
+
raise ValueError(f"Invalid families: {requested_families}; unknown={sorted(unknown)}")
|
| 366 |
+
FAMILIES = requested_families
|
| 367 |
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
| 368 |
+
|
| 369 |
+
linear = read_csv(args.linear_csv)
|
| 370 |
+
linear = [row for row in linear if row["model_family"] in FAMILIES]
|
| 371 |
+
linear = [{**row, "method": "linear"} for row in linear]
|
| 372 |
+
nonlinear = []
|
| 373 |
+
for family in FAMILIES:
|
| 374 |
+
path = args.nonlinear_dir / f"nonlinear_probe_{family}_folds.csv"
|
| 375 |
+
rows = read_csv(path)
|
| 376 |
+
nonlinear.extend({**row, "method": "nonlinear"} for row in rows)
|
| 377 |
+
expected_linear = 1320 * len(FAMILIES)
|
| 378 |
+
expected_nonlinear = 2160 * len(FAMILIES)
|
| 379 |
+
if len(linear) != expected_linear:
|
| 380 |
+
raise ValueError(f"Expected {expected_linear} linear rows, found {len(linear)}")
|
| 381 |
+
if len(nonlinear) != expected_nonlinear:
|
| 382 |
+
raise ValueError(f"Expected {expected_nonlinear} nonlinear rows, found {len(nonlinear)}")
|
| 383 |
+
unified = linear + nonlinear
|
| 384 |
+
prompt_rows = prompt_metric_rows(unified)
|
| 385 |
+
gains = gain_rows(prompt_rows)
|
| 386 |
+
metrics = summarize_metrics(prompt_rows)
|
| 387 |
+
gain_summary = summarize_gains(gains)
|
| 388 |
+
layer_gain_summary = summarize_layer_gains(gains)
|
| 389 |
+
write_csv(args.output_dir / "probe_folds_unified.csv", unified)
|
| 390 |
+
write_csv(args.output_dir / "probe_prompt_averaged.csv", prompt_rows)
|
| 391 |
+
write_csv(args.output_dir / "probe_metrics_summary.csv", metrics)
|
| 392 |
+
write_csv(args.output_dir / "probe_gain_by_prompt.csv", gains)
|
| 393 |
+
write_csv(args.output_dir / "probe_gain_summary.csv", gain_summary)
|
| 394 |
+
write_csv(args.output_dir / "probe_layer_gain_summary.csv", layer_gain_summary)
|
| 395 |
+
plot_primary(gains, args.output_dir / "conditional_gain_by_layer.png")
|
| 396 |
+
plot_controls(gains, args.output_dir / "conditional_control_comparison.png")
|
| 397 |
+
config = {
|
| 398 |
+
"linear_rows": len(linear),
|
| 399 |
+
"nonlinear_rows": len(nonlinear),
|
| 400 |
+
"families": list(FAMILIES),
|
| 401 |
+
"chunk_pairing": args.chunk_pairing,
|
| 402 |
+
"prompt_averaged_rows": len(prompt_rows),
|
| 403 |
+
"gain_rows": len(gains),
|
| 404 |
+
"prompt_count": 10,
|
| 405 |
+
"target_chunks": [2, 3],
|
| 406 |
+
"target_steps": [1, 2, 3],
|
| 407 |
+
"linear_baseline": "within_affine",
|
| 408 |
+
"nonlinear_baseline": "step_only",
|
| 409 |
+
"nonlinear_seed_aggregation": "mean within held-out prompt",
|
| 410 |
+
"outer_split": "held-out prompt plus its cyclic other-video donor",
|
| 411 |
+
}
|
| 412 |
+
(args.output_dir / "config.json").write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
| 413 |
+
build_report(metrics, gain_summary, args.output_dir / "REPORT.md", config)
|
| 414 |
+
print(f"[complete] {args.output_dir} unified={len(unified)} gains={len(gains)}", flush=True)
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
if __name__ == "__main__":
|
| 418 |
+
main()
|
scripts/summarize_confidence_stage1_experiments.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Create a compact report for the Stage-1 confidence-head rerun."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def csv_rows(path: Path) -> list[dict[str, str]]:
|
| 13 |
+
with path.open(encoding="utf-8") as handle:
|
| 14 |
+
return list(csv.DictReader(handle))
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def selected(root: Path) -> list[dict]:
|
| 18 |
+
return json.loads((root / "validation/selected.json").read_text())["selected_dynamic"]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def main() -> None:
|
| 22 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 23 |
+
parser.add_argument("--root", type=Path, required=True)
|
| 24 |
+
args = parser.parse_args()
|
| 25 |
+
root = args.root.resolve()
|
| 26 |
+
step12_manifest = json.loads(
|
| 27 |
+
(root / "confidence_step12/manifest.json").read_text()
|
| 28 |
+
)
|
| 29 |
+
report = {
|
| 30 |
+
"status": "complete",
|
| 31 |
+
"predictor": step12_manifest["predictor_weights"],
|
| 32 |
+
}
|
| 33 |
+
for name in ("step12", "step123"):
|
| 34 |
+
report[f"confidence_{name}"] = json.loads(
|
| 35 |
+
(root / f"confidence_{name}/metrics.json").read_text()
|
| 36 |
+
)
|
| 37 |
+
dynamic_root = root / f"dynamic_{name}"
|
| 38 |
+
test = {row["config_name"]: row for row in csv_rows(dynamic_root / "test/summary.csv")}
|
| 39 |
+
chosen = []
|
| 40 |
+
for row in selected(dynamic_root):
|
| 41 |
+
item = test[str(row["config_name"])].copy()
|
| 42 |
+
item["validation_beta"] = row["beta"]
|
| 43 |
+
item["validation_threshold"] = row["threshold"]
|
| 44 |
+
chosen.append(item)
|
| 45 |
+
report[f"dynamic_{name}"] = chosen
|
| 46 |
+
report[f"baselines_{name}"] = {
|
| 47 |
+
key: test[key] for key in ("ffff", "fppf" if name == "step12" else "fppp")
|
| 48 |
+
}
|
| 49 |
+
vbench_path = root / "vbench/summary.csv"
|
| 50 |
+
if vbench_path.exists():
|
| 51 |
+
vbench_rows = csv_rows(vbench_path)
|
| 52 |
+
high_speed_path = root / "vbench_high_speed/summary.csv"
|
| 53 |
+
if high_speed_path.exists():
|
| 54 |
+
by_condition = {row["condition"]: row for row in vbench_rows}
|
| 55 |
+
by_condition.update(
|
| 56 |
+
{row["condition"]: row for row in csv_rows(high_speed_path)}
|
| 57 |
+
)
|
| 58 |
+
vbench_rows = [by_condition[name] for name in sorted(by_condition)]
|
| 59 |
+
report["vbench"] = vbench_rows
|
| 60 |
+
(root / "summary.json").write_text(
|
| 61 |
+
json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
| 62 |
+
)
|
| 63 |
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
main()
|
scripts/summarize_layer17_dynamic_gate.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Summarize validation-selected Layer-17 dynamic-gating test results."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import argparse
|
| 7 |
+
import csv
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
import matplotlib.pyplot as plt
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
TARGETS = (4, 6, 8, 10)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def read_csv(path: Path) -> list[dict[str, str]]:
|
| 19 |
+
with path.open(encoding="utf-8") as handle:
|
| 20 |
+
return list(csv.DictReader(handle))
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
|
| 24 |
+
fields = list(rows[0])
|
| 25 |
+
with path.open("w", encoding="utf-8", newline="") as handle:
|
| 26 |
+
writer = csv.DictWriter(handle, fieldnames=fields)
|
| 27 |
+
writer.writeheader()
|
| 28 |
+
writer.writerows(rows)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main() -> None:
|
| 32 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 33 |
+
parser.add_argument(
|
| 34 |
+
"--root", type=Path,
|
| 35 |
+
default=Path("outputs/layer17_dynamic_gate_20260830"),
|
| 36 |
+
)
|
| 37 |
+
args = parser.parse_args()
|
| 38 |
+
root = args.root.resolve()
|
| 39 |
+
test_dir = root / "test"
|
| 40 |
+
summary_rows = read_csv(test_dir / "summary.csv")
|
| 41 |
+
summary = {row["config_name"]: row for row in summary_rows}
|
| 42 |
+
selected = json.loads((root / "validation" / "selected.json").read_text())
|
| 43 |
+
selected_by_target = {
|
| 44 |
+
int(row["target_accepts"]): row for row in selected["selected_dynamic"]
|
| 45 |
+
}
|
| 46 |
+
ffff_time = float(summary["ffff"]["generation_time_s"])
|
| 47 |
+
|
| 48 |
+
comparisons: list[dict[str, Any]] = []
|
| 49 |
+
acceptance_rows: list[dict[str, Any]] = []
|
| 50 |
+
for target in TARGETS:
|
| 51 |
+
selected_row = selected_by_target[target]
|
| 52 |
+
dynamic_name = selected_row["config_name"]
|
| 53 |
+
dynamic = summary[dynamic_name]
|
| 54 |
+
static = summary[f"static_late_k{target:02d}"]
|
| 55 |
+
dynamic_lpips = float(dynamic["tail_lpips"])
|
| 56 |
+
static_lpips = float(static["tail_lpips"])
|
| 57 |
+
comparisons.append(
|
| 58 |
+
{
|
| 59 |
+
"target_accepts": target,
|
| 60 |
+
"dynamic_config": dynamic_name,
|
| 61 |
+
"beta": float(dynamic["beta"]),
|
| 62 |
+
"threshold": float(dynamic["threshold"]),
|
| 63 |
+
"dynamic_actual_accepts": float(dynamic["accepted_predictor_calls"]),
|
| 64 |
+
"static_actual_accepts": float(static["accepted_predictor_calls"]),
|
| 65 |
+
"dynamic_full_calls": float(dynamic["full_calls"]),
|
| 66 |
+
"static_full_calls": float(static["full_calls"]),
|
| 67 |
+
"dynamic_tail_lpips": dynamic_lpips,
|
| 68 |
+
"static_tail_lpips": static_lpips,
|
| 69 |
+
"tail_lpips_reduction_percent": 100.0 * (static_lpips - dynamic_lpips) / static_lpips,
|
| 70 |
+
"dynamic_generation_time_s": float(dynamic["generation_time_s"]),
|
| 71 |
+
"static_generation_time_s": float(static["generation_time_s"]),
|
| 72 |
+
"dynamic_overhead_vs_static_percent": 100.0 * (
|
| 73 |
+
float(dynamic["generation_time_s"])
|
| 74 |
+
/ float(static["generation_time_s"])
|
| 75 |
+
- 1.0
|
| 76 |
+
),
|
| 77 |
+
"dynamic_speedup_vs_ffff_percent": 100.0 * (
|
| 78 |
+
1.0 - float(dynamic["generation_time_s"]) / ffff_time
|
| 79 |
+
),
|
| 80 |
+
"dynamic_lpips": float(dynamic["lpips"]),
|
| 81 |
+
"static_lpips": float(static["lpips"]),
|
| 82 |
+
"dynamic_latent_tail_nrmse": float(dynamic["latent_tail_nrmse"]),
|
| 83 |
+
"static_latent_tail_nrmse": float(static["latent_tail_nrmse"]),
|
| 84 |
+
}
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
decision_files = sorted((test_dir / "per_run" / dynamic_name).glob("*.json"))
|
| 88 |
+
decisions = [
|
| 89 |
+
decision
|
| 90 |
+
for path in decision_files
|
| 91 |
+
for decision in json.loads(path.read_text())["decisions"]
|
| 92 |
+
]
|
| 93 |
+
for chunk in range(1, 7):
|
| 94 |
+
for step in (1, 2):
|
| 95 |
+
cell = [
|
| 96 |
+
row for row in decisions
|
| 97 |
+
if int(row["chunk"]) == chunk and int(row["step"]) == step
|
| 98 |
+
]
|
| 99 |
+
acceptance_rows.append(
|
| 100 |
+
{
|
| 101 |
+
"target_accepts": target,
|
| 102 |
+
"dynamic_config": dynamic_name,
|
| 103 |
+
"chunk": chunk,
|
| 104 |
+
"step": step,
|
| 105 |
+
"acceptance_ratio": sum(bool(row["accepted"]) for row in cell)
|
| 106 |
+
/ len(cell),
|
| 107 |
+
}
|
| 108 |
+
)
|
| 109 |
+
write_csv(test_dir / "dynamic_vs_static.csv", comparisons)
|
| 110 |
+
write_csv(test_dir / "acceptance_by_chunk_step.csv", acceptance_rows)
|
| 111 |
+
|
| 112 |
+
fig, axes = plt.subplots(1, 2, figsize=(11, 4.2))
|
| 113 |
+
dynamic_rows = [summary[selected_by_target[target]["config_name"]] for target in TARGETS]
|
| 114 |
+
static_rows = [summary[f"static_late_k{target:02d}"] for target in TARGETS]
|
| 115 |
+
for axis, x_field, label in (
|
| 116 |
+
(axes[0], "full_calls", "Mean Full calls"),
|
| 117 |
+
(axes[1], "generation_time_s", "Generation time (s)"),
|
| 118 |
+
):
|
| 119 |
+
axis.plot(
|
| 120 |
+
[float(row[x_field]) for row in dynamic_rows],
|
| 121 |
+
[float(row["tail_lpips"]) for row in dynamic_rows],
|
| 122 |
+
"o-", label="Dynamic confidence", color="#d64b40", linewidth=2,
|
| 123 |
+
)
|
| 124 |
+
axis.plot(
|
| 125 |
+
[float(row[x_field]) for row in static_rows],
|
| 126 |
+
[float(row["tail_lpips"]) for row in static_rows],
|
| 127 |
+
"s--", label="Static late-first", color="#3977b8", linewidth=2,
|
| 128 |
+
)
|
| 129 |
+
axis.scatter(
|
| 130 |
+
[float(summary["ffff"][x_field])],
|
| 131 |
+
[float(summary["ffff"]["tail_lpips"])],
|
| 132 |
+
marker="*", s=100, color="#333333", label="FFFF",
|
| 133 |
+
)
|
| 134 |
+
axis.scatter(
|
| 135 |
+
[float(summary["fppf"][x_field])],
|
| 136 |
+
[float(summary["fppf"]["tail_lpips"])],
|
| 137 |
+
marker="X", s=80, color="#777777", label="FPPF",
|
| 138 |
+
)
|
| 139 |
+
axis.set_xlabel(label)
|
| 140 |
+
axis.set_ylabel("Tail LPIPS")
|
| 141 |
+
axis.grid(alpha=0.25)
|
| 142 |
+
axes[0].legend(frameon=False)
|
| 143 |
+
fig.suptitle("Layer-17 Predictor: quality-compute frontier on prompts 90–99")
|
| 144 |
+
fig.tight_layout()
|
| 145 |
+
fig.savefig(root / "quality_compute_pareto.png", dpi=180)
|
| 146 |
+
plt.close(fig)
|
| 147 |
+
|
| 148 |
+
report = [
|
| 149 |
+
"# Layer-17 dynamic confidence gating",
|
| 150 |
+
"",
|
| 151 |
+
"Thresholds and beta were selected only on prompts 80–89. The table below "
|
| 152 |
+
"reports the frozen configurations on prompts 90–99.",
|
| 153 |
+
"",
|
| 154 |
+
"| Target P | Beta | Actual P | Full | Tail LPIPS dynamic | Static | Reduction | Gen speedup vs FFFF |",
|
| 155 |
+
"|---:|---:|---:|---:|---:|---:|---:|---:|",
|
| 156 |
+
]
|
| 157 |
+
for row in comparisons:
|
| 158 |
+
report.append(
|
| 159 |
+
f"| {row['target_accepts']} | {row['beta']:.1f} | "
|
| 160 |
+
f"{row['dynamic_actual_accepts']:.1f} | {row['dynamic_full_calls']:.1f} | "
|
| 161 |
+
f"{row['dynamic_tail_lpips']:.5f} | {row['static_tail_lpips']:.5f} | "
|
| 162 |
+
f"{row['tail_lpips_reduction_percent']:.1f}% | "
|
| 163 |
+
f"{row['dynamic_speedup_vs_ffff_percent']:.1f}% |"
|
| 164 |
+
)
|
| 165 |
+
report.extend(
|
| 166 |
+
[
|
| 167 |
+
"",
|
| 168 |
+
f"FFFF generation time: {float(summary['ffff']['generation_time_s']):.3f}s. "
|
| 169 |
+
f"FPPF generation time: {float(summary['fppf']['generation_time_s']):.3f}s; "
|
| 170 |
+
f"tail LPIPS: {float(summary['fppf']['tail_lpips']):.5f}.",
|
| 171 |
+
"",
|
| 172 |
+
"Dynamic gating evaluates the Predictor at all 12 candidate decisions, "
|
| 173 |
+
"including rejected calls. Its generation-time overhead relative to the "
|
| 174 |
+
"budget-matched static policies is 0–4.4%, and is included in the table/plot.",
|
| 175 |
+
"",
|
| 176 |
+
"The K≈6 point is the recommended balanced operating point: beta=1.0, "
|
| 177 |
+
"threshold=0.333097, 5.8 accepted Predictor calls, 22.2 Full calls, "
|
| 178 |
+
"tail LPIPS 0.03449, and 16.3% generation speedup versus FFFF.",
|
| 179 |
+
]
|
| 180 |
+
)
|
| 181 |
+
(root / "REPORT.md").write_text("\n".join(report) + "\n", encoding="utf-8")
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
if __name__ == "__main__":
|
| 185 |
+
main()
|