Diffusers
Safetensors
Cccccz commited on
Commit
74da989
·
verified ·
1 Parent(s): 5f0e4a2

Upload batch 65: 500 files (0.01 GiB)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. tools/run_confidence_validation25_reference_single.sh +143 -0
  2. tools/run_offline_ffff_targets_validation25_evaluation.sh +123 -0
  3. tools/run_oldenv_validation_reruns_sequential.sh +76 -0
  4. tools/run_predictor_confidence_experiment.sh +55 -0
  5. tools/run_predictor_v4_atc_full_experiment.sh +24 -0
  6. tools/run_predictor_v4_atc_global_sequential_8gpu.sh +14 -0
  7. tools/run_predictor_v4_detached_timestep_rollout_offline_targets_overfit8.sh +116 -0
  8. tools/run_predictor_v4_grouped_teacher_forcing_overfit8.sh +99 -0
  9. tools/run_predictor_v4_timestep_rollout_detached_overfit8_experiments.sh +11 -0
  10. tools/run_predictor_v4_timestep_rollout_overfit8_experiments.sh +115 -0
  11. tools/run_predictor_v4_validation5_rollout_comparison.sh +176 -0
  12. tools/run_stage1_continued_validation25_evaluation.sh +123 -0
  13. tools/run_stage1_previous_condition_ablation.sh +84 -0
  14. tools/run_stage1_previous_condition_retrain_validation.sh +66 -0
  15. tools/run_vbench_atc_validation25.sh +136 -0
  16. tools/run_vbench_c1_threshold_validation25.sh +92 -0
  17. tools/run_vbench_previous_condition_none_step0300_validation25.sh +110 -0
  18. tools/run_vbench_previous_condition_validation25.sh +137 -0
  19. tools/run_vbench_token_gate_validation25.sh +83 -0
  20. tools/run_vbench_validation25_chunk_timestep_step2000.sh +108 -0
  21. tools/run_vbench_validation25_core.sh +111 -0
  22. tools/run_vbench_validation25_groups.sh +75 -0
  23. tools/run_wide_c1_r33_validation25_reference.sh +84 -0
  24. tools/run_wide_c2_hybrid_validation25_reference.sh +116 -0
  25. tools/summarize_atc_rollout_ckpt_sweep.py +37 -0
  26. tools/summarize_c1_threshold_validation25.py +167 -0
  27. tools/summarize_predictor_timestep_rollout_overfit8.py +71 -0
  28. tools/summarize_vbench_validation25_chunk_timestep_step2000.py +205 -0
  29. tools/summarize_vbench_validation25_core.py +243 -0
  30. tools/summarize_vbench_validation25_pair.py +211 -0
  31. tools/supervise_predictor_v4_fp32master_training.sh +34 -0
  32. tools/supervise_predictor_v4_rollout_fppf_overfit8_training.sh +34 -0
  33. tools/supervise_predictor_v4_rollout_overfit8_training.sh +34 -0
  34. tools/supervise_predictor_v4_rollout_training.sh +34 -0
  35. tools/supervise_previous_vbench_validation_predictors.sh +50 -0
  36. tools/supervise_training_tests_then_helios.sh +244 -0
  37. tools/sync_predictor_prefeature_to_s3.sh +44 -0
  38. tools/train_evaluate_predictor_confidence.py +281 -0
  39. tools/validate_predictor_dataset.py +52 -0
  40. tools/validate_predictor_prefeature_dataset.py +80 -0
  41. tools/validate_predictor_text_kv_all54.py +58 -0
  42. tools/validate_predictor_v2_dataset.py +97 -0
  43. tools/video_review_server.py +1031 -0
  44. train.md +592 -0
  45. train_predictor.py +271 -0
  46. train_predictor_disca.py +42 -0
  47. train_predictor_v2.py +374 -0
  48. train_predictor_v3.py +14 -0
  49. train_predictor_v4.py +1030 -0
  50. train_predictor_v4_grouped_teacher_forcing.py +825 -0
tools/run_confidence_validation25_reference_single.sh ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [[ "$#" -ne 5 ]]; then
5
+ echo "Usage: $0 EVAL_NAME CONFIDENCE_CHECKPOINT REPLACE_STEPS THRESHOLD_MODE THRESHOLD" >&2
6
+ echo "THRESHOLD_MODE must be global or per_step." >&2
7
+ exit 2
8
+ fi
9
+
10
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
11
+ eval_name="$1"
12
+ confidence_checkpoint="$2"
13
+ replace_steps="$3"
14
+ threshold_mode="$4"
15
+ threshold="$5"
16
+
17
+ python_bin="${PREDICTOR_PYTHON_BIN:-/mnt/local_nvme/cz/envs/hy_worldplay_july20260720/bin/python}"
18
+ dataset_name="hyworldplay_predictor_vbench_val25_test50"
19
+ dataset_root="${PREDICTOR_HOLDOUT_NVME_ROOT:-${repo_root}/datasets/${dataset_name}}"
20
+ validation_root="${dataset_root}/validation"
21
+ eval_root="${dataset_root}/evaluations/${eval_name}"
22
+ log_dir="${eval_root}/logs"
23
+ base_model="${PREDICTOR_BASE_MODEL:-${repo_root}/outputs/hf_snapshot_repaired_9b49404}"
24
+ action_ckpt="${PREDICTOR_ACTION_CKPT:-/mnt/local_nvme/cz/checkpoints/hy_worldplay/huggingface/hub/models--tencent--HY-WorldPlay/snapshots/f4c29235647707b571479a69b569e4166f9f5bf8/ar_distilled_action_model/diffusion_pytorch_model.safetensors}"
25
+ weights="${repo_root}/checkpoints/predictor_v4_prefeature_blocks1-52_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab/predictor_step_02000.safetensors"
26
+
27
+ if [[ ! "${eval_name}" =~ ^[A-Za-z0-9._-]+$ ]]; then
28
+ echo "Invalid eval name: ${eval_name}" >&2
29
+ exit 2
30
+ fi
31
+ if [[ "${replace_steps}" != "1,2" && "${replace_steps}" != "1,2,3" ]]; then
32
+ echo "replace_steps must be 1,2 or 1,2,3" >&2
33
+ exit 2
34
+ fi
35
+ if [[ "${threshold_mode}" != "global" && "${threshold_mode}" != "per_step" ]]; then
36
+ echo "threshold mode must be global or per_step" >&2
37
+ exit 2
38
+ fi
39
+
40
+ [[ -x "${python_bin}" ]]
41
+ [[ -d "${base_model}" ]]
42
+ [[ -f "${action_ckpt}" ]]
43
+ [[ -f "${weights}" ]]
44
+ [[ -f "${confidence_checkpoint}" ]]
45
+ [[ -f "${validation_root}/cases.jsonl" ]]
46
+ [[ $(find "${validation_root}/input_images" -type f -name '*.jpg' | wc -l) -eq 25 ]]
47
+ [[ $(find "${validation_root}/full" -type f -name '*.mp4' | wc -l) -eq 100 ]]
48
+
49
+ mkdir -p "${log_dir}"
50
+ export PYTHONPATH="${repo_root}"
51
+ export HY_PROFILE_TIMING=1
52
+
53
+ confidence_args=(--confidence_checkpoint "${confidence_checkpoint}")
54
+ if [[ "${threshold_mode}" == "global" ]]; then
55
+ confidence_args+=(--confidence_threshold "${threshold}")
56
+ else
57
+ confidence_args+=(--confidence_thresholds "${threshold}")
58
+ fi
59
+
60
+ run_workers() {
61
+ local phase="$1"
62
+ shift
63
+ local -a pids=()
64
+ local worker_id pid failed=0
65
+ for worker_id in $(seq 0 7); do
66
+ CUDA_VISIBLE_DEVICES="${worker_id}" "${python_bin}" -u "$@" \
67
+ --worker_id "${worker_id}" --num_workers 8 \
68
+ >"${log_dir}/${phase}_worker_${worker_id}.log" 2>&1 &
69
+ pid="$!"
70
+ pids+=("${pid}")
71
+ echo "${pid}" >"${log_dir}/${phase}_worker_${worker_id}.pid"
72
+ done
73
+ for pid in "${pids[@]}"; do
74
+ if ! wait "${pid}"; then failed=1; fi
75
+ done
76
+ if [[ "${failed}" -ne 0 ]]; then
77
+ echo "${phase} worker failure; inspect ${log_dir}/${phase}_worker_*.log" >&2
78
+ return 1
79
+ fi
80
+ }
81
+
82
+ if [[ ! -f "${eval_root}/generation.completed" ]] \
83
+ || [[ $(find "${eval_root}/predictor" -type f -name '*.mp4' 2>/dev/null | wc -l) -ne 100 ]]; then
84
+ date -u +%FT%TZ >"${eval_root}/generation.started"
85
+ run_workers generate \
86
+ "${repo_root}/tools/generate_predictor_v4_train_case_eval.py" \
87
+ --dataset_root "${validation_root}" \
88
+ --output_root "${eval_root}" \
89
+ --base_model "${base_model}" \
90
+ --action_ckpt "${action_ckpt}" \
91
+ --weights "${weights}" \
92
+ --source_blocks 1,52 \
93
+ --predictor_variant v4 \
94
+ --previous_condition_mode same_step \
95
+ --replace_steps "${replace_steps}" \
96
+ --modes predictor \
97
+ "${confidence_args[@]}"
98
+ [[ $(find "${eval_root}/predictor" -type f -name '*.mp4' | wc -l) -eq 100 ]]
99
+ date -u +%FT%TZ >"${eval_root}/generation.completed"
100
+ else
101
+ echo "[skip] generation already complete: ${eval_name}"
102
+ fi
103
+
104
+ metric_records=0
105
+ if [[ -f "${eval_root}/metrics.json" ]]; then
106
+ metric_records=$("${python_bin}" -c \
107
+ 'import json,sys; print(len(json.load(open(sys.argv[1]))["records"]))' \
108
+ "${eval_root}/metrics.json")
109
+ fi
110
+ if [[ ! -f "${eval_root}/metrics.completed" || "${metric_records}" -ne 100 ]]; then
111
+ date -u +%FT%TZ >"${eval_root}/metrics.started"
112
+ run_workers metrics \
113
+ "${repo_root}/tools/compute_predictor_v4_train_case_metrics.py" \
114
+ --dataset_root "${validation_root}" \
115
+ --output_root "${eval_root}" \
116
+ --num_cases 25 \
117
+ --reference_subdir full \
118
+ --modes predictor \
119
+ --metric_batch_size 4
120
+ "${python_bin}" "${repo_root}/tools/merge_predictor_v4_train_case_metrics.py" \
121
+ --output_root "${eval_root}" \
122
+ --num_workers 8 \
123
+ --num_cases 25 \
124
+ --modes predictor \
125
+ --replace_steps "${replace_steps}" \
126
+ --reference_label "Full-DiT validation videos" \
127
+ >"${log_dir}/metrics_merge.log" 2>&1
128
+ [[ $("${python_bin}" -c \
129
+ 'import json,sys; print(len(json.load(open(sys.argv[1]))["records"]))' \
130
+ "${eval_root}/metrics.json") -eq 100 ]]
131
+ date -u +%FT%TZ >"${eval_root}/metrics.completed"
132
+ else
133
+ echo "[skip] metrics already complete: ${eval_name}"
134
+ fi
135
+
136
+ date -u +%FT%TZ >"${eval_root}/sync.completed"
137
+ if [[ "${PREDICTOR_SKIP_SYNC:-1}" != "1" ]]; then
138
+ bucket="${BUCKET:-s3-us-west2-default}"
139
+ durable_eval_uri="s3://${bucket}/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/datasets/${dataset_name}/evaluations/${eval_name}"
140
+ aws s3 sync "${eval_root}" "${durable_eval_uri}" --only-show-errors
141
+ fi
142
+
143
+ echo "Validation evaluation complete: ${eval_root}"
tools/run_offline_ffff_targets_validation25_evaluation.sh ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ vbench_repo="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/VBench"
6
+ dataset_name="hyworldplay_predictor_vbench_val25_test50"
7
+ dataset_root="${PREDICTOR_HOLDOUT_NVME_ROOT:-/mnt/local_nvme/zoubin/cz/${dataset_name}}"
8
+ python_bin="/mnt/s3files/s3-us-west2-default/zoubin/cz/envs/hy_worldplay/bin/python"
9
+ weights="${repo_root}/checkpoints/predictor_v4_detached-chunk-timestep-rollout-offline-ffff-targets-fppp_blocks1-52_fulltrain100_seed20260721_fp32master_8gpu_flr5e-5_blr5e-6_schedsteps2000_swarmup100_bwarmup100linear_bs1_swanlab/predictor_step_02000.safetensors"
10
+ eval_prefix="eval_validation25_detached-chunk-timestep-rollout-offline-ffff-targets-fppp-fulltrain100"
11
+ eval_suffix="blocks1-52_ckptstep2000_flr5e-5_blr5e-6_schedsteps2000_swarmup100_bwarmup100linear_bs1"
12
+ fppf_name="${eval_prefix}_schedule-fppf_${eval_suffix}"
13
+ fppp_name="${eval_prefix}_schedule-fppp_${eval_suffix}"
14
+ run_name="validation25_offline_ffff_targets_step2000"
15
+ run_root="${dataset_root}/vbench/${run_name}"
16
+ staging_root="${dataset_root}/vbench_staging/${run_name}"
17
+ durable_uri="s3://s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/datasets/${dataset_name}/vbench/${run_name}"
18
+
19
+ export PYTHONPATH="${repo_root}"
20
+
21
+ bash "${repo_root}/tools/launch_vbench_validation_predictor.sh" \
22
+ "${fppf_name}" "${weights}" "1,52" v4 "1,2"
23
+ bash "${repo_root}/tools/launch_vbench_validation_predictor.sh" \
24
+ "${fppp_name}" "${weights}" "1,52" v4 "1,2,3"
25
+
26
+ export PYTHONPATH="${vbench_repo}:${repo_root}"
27
+ export VBENCH_CACHE_DIR="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/vbench"
28
+ export TORCH_HOME="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/torch"
29
+ export HF_HOME="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/huggingface"
30
+
31
+ mkdir -p "${run_root}/raw" "${run_root}/logs" "${staging_root}"
32
+
33
+ group_fppf="offline_ffff_targets_step2000_fppf"
34
+ group_fppp="offline_ffff_targets_step2000_fppp"
35
+ declare -a groups=(
36
+ "${group_fppf}|${dataset_root}/evaluations/${fppf_name}/predictor"
37
+ "${group_fppp}|${dataset_root}/evaluations/${fppp_name}/predictor"
38
+ )
39
+
40
+ find "${staging_root}" -maxdepth 1 -type l -delete
41
+ for spec in "${groups[@]}"; do
42
+ IFS='|' read -r label source_dir <<<"${spec}"
43
+ video_count=$(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | wc -l)
44
+ if [[ "${video_count}" -ne 100 ]]; then
45
+ echo "${label}: expected 100 videos, found ${video_count}: ${source_dir}" >&2
46
+ exit 1
47
+ fi
48
+ while IFS= read -r video_path; do
49
+ ln -s "${video_path}" "${staging_root}/${label}__$(basename "${video_path}")"
50
+ done < <(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | sort)
51
+ done
52
+
53
+ staged_count=$(find "${staging_root}" -maxdepth 1 -type l | wc -l)
54
+ if [[ "${staged_count}" -ne 200 ]]; then
55
+ echo "Expected 200 staged videos, found ${staged_count}" >&2
56
+ exit 1
57
+ fi
58
+
59
+ prompt_file="${run_root}/validation25_prompt_map.json"
60
+ "${python_bin}" "${repo_root}/tools/build_vbench_validation25_prompt_map.py" \
61
+ --cases_jsonl "${dataset_root}/validation/cases.jsonl" \
62
+ --staging_root "${staging_root}" \
63
+ --output "${prompt_file}" \
64
+ --expected_count 200
65
+
66
+ dimensions=(
67
+ subject_consistency
68
+ background_consistency
69
+ temporal_flickering
70
+ motion_smoothness
71
+ dynamic_degree
72
+ aesthetic_quality
73
+ imaging_quality
74
+ overall_consistency
75
+ )
76
+
77
+ for dimension in "${dimensions[@]}"; do
78
+ output_dir="${run_root}/raw/${dimension}"
79
+ marker="${run_root}/${dimension}.completed"
80
+ mkdir -p "${output_dir}"
81
+ result_count=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json' | wc -l)
82
+ if [[ -f "${marker}" && "${result_count}" -eq 1 ]]; then
83
+ echo "[skip] ${dimension} already complete"
84
+ continue
85
+ fi
86
+ find "${output_dir}" -maxdepth 1 -type f -delete
87
+ echo "[vbench] ${dimension} start $(date -u +%FT%TZ)"
88
+ "${python_bin}" -m torch.distributed.run \
89
+ --standalone \
90
+ --nproc_per_node=8 \
91
+ "${vbench_repo}/evaluate.py" \
92
+ --videos_path "${staging_root}" \
93
+ --dimension "${dimension}" \
94
+ --mode custom_input \
95
+ --prompt_file "${prompt_file}" \
96
+ --load_ckpt_from_local True \
97
+ --imaging_quality_preprocessing_mode longer \
98
+ --output_path "${output_dir}" \
99
+ >"${run_root}/logs/${dimension}.log" 2>&1
100
+ result_json=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json')
101
+ "${python_bin}" - "${result_json}" "${dimension}" <<'PY'
102
+ import json
103
+ import sys
104
+
105
+ path, dimension = sys.argv[1:]
106
+ payload = json.load(open(path))
107
+ records = payload[dimension][1]
108
+ assert len(records) == 200, (dimension, len(records))
109
+ print(dimension, len(records), payload[dimension][0])
110
+ PY
111
+ date -u +%FT%TZ >"${marker}"
112
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
113
+ echo "[vbench] ${dimension} complete $(date -u +%FT%TZ)"
114
+ done
115
+
116
+ "${python_bin}" "${repo_root}/tools/summarize_vbench_validation25_pair.py" \
117
+ --run_root "${run_root}" \
118
+ --group "${group_fppf}" \
119
+ --group "${group_fppp}" \
120
+ >"${run_root}/logs/summary.log" 2>&1
121
+ date -u +%FT%TZ >"${run_root}/pipeline.completed"
122
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
123
+ echo "[validation] all standard and VBench metrics complete: ${run_root}"
tools/run_oldenv_validation_reruns_sequential.sh ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ python_bin="${PREDICTOR_PYTHON_BIN:-/mnt/local_nvme/cz/envs/hy_worldplay_july20260720/bin/python}"
6
+ base_model="${PREDICTOR_BASE_MODEL:-${repo_root}/outputs/hf_snapshot_repaired_9b49404}"
7
+ run_tag="${OLDENV_REVAL_TAG:-oldenv_reval_20260821}"
8
+ run_root="${repo_root}/logs/${run_tag}"
9
+ master_log="${run_root}/sequential.log"
10
+ status_file="${run_root}/status.tsv"
11
+
12
+ mkdir -p "${run_root}"
13
+ exec > >(tee -a "${master_log}") 2>&1
14
+
15
+ export PREDICTOR_PYTHON_BIN="${python_bin}"
16
+ export PREDICTOR_BASE_MODEL="${base_model}"
17
+ export PREDICTOR_SKIP_SYNC="${PREDICTOR_SKIP_SYNC:-0}"
18
+ export PYTHONPATH="${repo_root}"
19
+
20
+ on_error() {
21
+ local exit_code="$?"
22
+ printf 'failed\t%s\tline=%s\texit=%s\n' "$(date -u +%FT%TZ)" "${BASH_LINENO[0]}" "${exit_code}" >>"${status_file}"
23
+ exit "${exit_code}"
24
+ }
25
+ trap on_error ERR
26
+
27
+ record_status() {
28
+ printf '%s\t%s\t%s\n' "$1" "$(date -u +%FT%TZ)" "$2" >>"${status_file}"
29
+ }
30
+
31
+ run_confidence_eval() {
32
+ local short_name="$1" checkpoint="$2" replace_steps="$3" threshold_mode="$4" threshold="$5"
33
+ local eval_name="eval_validation25_${short_name}_${run_tag}"
34
+ record_status start "${eval_name}"
35
+ bash "${repo_root}/tools/run_confidence_validation25_reference_single.sh" \
36
+ "${eval_name}" "${checkpoint}" "${replace_steps}" "${threshold_mode}" "${threshold}"
37
+ record_status complete "${eval_name}"
38
+ }
39
+
40
+ echo "[preflight] fixed environment"
41
+ "${python_bin}" - <<'PY'
42
+ import importlib.util
43
+ import torch
44
+ assert torch.__version__ == "2.6.0+cu124", torch.__version__
45
+ assert importlib.util.find_spec("flash_attn") is None, "flash_attn must be absent"
46
+ print(f"torch={torch.__version__} cuda={torch.version.cuda} flash_attn=False")
47
+ PY
48
+ [[ -d "${base_model}" ]]
49
+ [[ $(find "${repo_root}/datasets/hyworldplay_predictor_vbench_val25_test50/validation/full" -type f -name '*.mp4' | wc -l) -eq 100 ]]
50
+ record_status preflight "torch=2.6.0+cu124; flash_attn=False"
51
+
52
+ small_c1="${repo_root}/confidence_validation/checkpoints/confidence_c1.pt"
53
+ wide_c1="${repo_root}/confidence_validation_wide4096x2_c1/checkpoints/confidence_c1.pt"
54
+ wide_c2="${repo_root}/confidence_validation_wide4096x2_c2/checkpoints/confidence_c2.pt"
55
+
56
+ # Keep the trained thresholds unchanged. The fixed-environment run records the
57
+ # actual fallback count, so distribution drift remains visible in the result.
58
+ run_confidence_eval stage1-c1-threshold-r10 "${small_c1}" 1,2,3 global -3.523588657
59
+ run_confidence_eval stage1-c1-threshold-r25 "${small_c1}" 1,2,3 global -3.915276289
60
+ run_confidence_eval stage1-c1-threshold-r33 "${small_c1}" 1,2,3 global -4.329442501
61
+ run_confidence_eval stage1-c1-threshold-r50 "${small_c1}" 1,2,3 global -4.798926830
62
+ run_confidence_eval stage1-wide4096x2-c1-threshold-r33 "${wide_c1}" 1,2,3 global -4.308430990
63
+ run_confidence_eval stage1-wide4096x2-c2-hybrid-p3full-p12r10 "${wide_c2}" 1,2 per_step 1:-4.136674309,2:-3.798280764
64
+ run_confidence_eval stage1-wide4096x2-c2-hybrid-p3full-p12r25 "${wide_c2}" 1,2 per_step 1:-4.400576830,2:-4.049407601
65
+
66
+ record_status complete "all validation reruns"
67
+ date -u +%FT%TZ >"${run_root}/all.completed"
68
+
69
+ if [[ "${PREDICTOR_SKIP_SYNC}" != "1" ]]; then
70
+ bucket="${BUCKET:-s3-us-west2-default}"
71
+ aws s3 sync "${run_root}" \
72
+ "s3://${bucket}/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/logs/${run_tag}" \
73
+ --only-show-errors
74
+ fi
75
+
76
+ echo "[complete] all fixed-environment validation reruns"
tools/run_predictor_confidence_experiment.sh ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT"
6
+ PYTHON_BIN="${PYTHON_BIN:-/mnt/local_nvme/cz/envs/hy_worldplay_runtime/bin/python}"
7
+ GPU_IDS="${GPU_IDS:-0,1,2,3,4,5,6,7}"
8
+ NPROC="${NPROC:-8}"
9
+ TRAIN_ROOT="${TRAIN_ROOT:-$ROOT/datasets/hyworldplay_predictor_prefeature_v4}"
10
+ VAL_ROOT="${VAL_ROOT:-$ROOT/datasets/hyworldplay_predictor_confidence_validation25_prefeature}"
11
+ FEATURE_ROOT="${FEATURE_ROOT:-$ROOT/confidence_validation/features}"
12
+ OUTPUT_ROOT="${OUTPUT_ROOT:-$ROOT/confidence_validation}"
13
+ PREDICTOR="${PREDICTOR:-$ROOT/checkpoints/predictor_v4_prefeature_blocks1-52_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab/predictor_step_02000.safetensors}"
14
+ BASE_MODEL="${BASE_MODEL:-$ROOT/outputs/model_cache_repaired/HunyuanVideo-1.5-9b49404-v2}"
15
+ TEACHER="${TEACHER:-/mnt/local_nvme/cz/checkpoints/hy_worldplay/huggingface/hub/models--tencent--HY-WorldPlay/snapshots/f4c29235647707b571479a69b569e4166f9f5bf8/ar_distilled_action_model/diffusion_pytorch_model.safetensors}"
16
+ export PYTHONPATH="$ROOT"
17
+
18
+ if [[ ! -f "$VAL_ROOT/train_eval_manifest.jsonl" ]]; then
19
+ "$PYTHON_BIN" tools/prepare_confidence_validation_prefeature.py --output_root "$VAL_ROOT"
20
+ mkdir -p "$VAL_ROOT/logs"
21
+ pids=()
22
+ for worker in $(seq 0 $((NPROC - 1))); do
23
+ CUDA_VISIBLE_DEVICES="${worker}" "$PYTHON_BIN" -u tools/build_predictor_prefeature_dataset.py \
24
+ --dataset_root "$VAL_ROOT" --base_model "$BASE_MODEL" --action_ckpt "$TEACHER" \
25
+ --worker_id "$worker" --num_workers "$NPROC" \
26
+ >"$VAL_ROOT/logs/worker_${worker}.log" 2>&1 &
27
+ pids+=("$!")
28
+ done
29
+ failed=0
30
+ for worker in "${!pids[@]}"; do
31
+ wait "${pids[$worker]}" || { echo "validation trajectory worker $worker failed" >&2; failed=1; }
32
+ done
33
+ (( failed == 0 )) || exit 1
34
+ "$PYTHON_BIN" tools/merge_predictor_prefeature_manifests.py \
35
+ --dataset_root "$VAL_ROOT" --expected_cases 25 --expected_actions 4 --expected_chunks 8
36
+ fi
37
+
38
+ extract() {
39
+ local split="$1" manifest="$2" output="$FEATURE_ROOT/$1"
40
+ mkdir -p "$output"
41
+ if [[ "$(find "$output" -maxdepth 1 -name 'features_rank_*.pt' | wc -l)" -ne "$NPROC" ]]; then
42
+ rm -f "$output"/features_rank_*.pt
43
+ CUDA_VISIBLE_DEVICES="$GPU_IDS" "$PYTHON_BIN" -m torch.distributed.run \
44
+ --standalone --nproc_per_node="$NPROC" tools/extract_predictor_confidence_features.py \
45
+ --manifest "$manifest" --predictor_checkpoint "$PREDICTOR" \
46
+ --teacher_checkpoint "$TEACHER" --output_dir "$output" --batch_size 8
47
+ fi
48
+ }
49
+
50
+ extract train "$TRAIN_ROOT/train_eval_manifest.jsonl"
51
+ extract validation "$VAL_ROOT/train_eval_manifest.jsonl"
52
+ CUDA_VISIBLE_DEVICES="$GPU_IDS" "$PYTHON_BIN" -m torch.distributed.run \
53
+ --standalone --nproc_per_node="$NPROC" tools/train_evaluate_predictor_confidence.py \
54
+ --train_features "$FEATURE_ROOT/train" --validation_features "$FEATURE_ROOT/validation" \
55
+ --output_dir "$OUTPUT_ROOT" --epochs 40 --batch_size 256 --seed 0
tools/run_predictor_v4_atc_full_experiment.sh ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ max_steps="${ATC_MAX_STEPS:-2000}"
6
+ variants=(full anchor_only raw_prev no_gate no_distance)
7
+
8
+ for variant in "${variants[@]}"; do
9
+ bash "${repo_root}/tools/launch_predictor_v4_atc_stage1_8gpu.sh" \
10
+ "${variant}" "${max_steps}"
11
+ done
12
+
13
+ for variant in "${variants[@]}"; do
14
+ checkpoint_dir="${repo_root}/checkpoints/predictor_v4_atc-${variant}_blocks1-52_fulltrain100_fp32master_8gpu_flr1e-4_blr1e-5_steps${max_steps}"
15
+ weights="${checkpoint_dir}/predictor_step_$(printf '%05d' "${max_steps}").safetensors"
16
+ for schedule in fppf fppp; do
17
+ replace_steps="1,2"
18
+ [[ "${schedule}" == "fppp" ]] && replace_steps="1,2,3"
19
+ eval_name="eval_validation25_atc-${variant}_schedule-${schedule}_blocks1-52_step${max_steps}_oldenv"
20
+ bash "${repo_root}/tools/launch_vbench_validation_predictor.sh" \
21
+ "${eval_name}" "${weights}" 1,52 v4 "${replace_steps}" \
22
+ same_step concat atc "${variant}"
23
+ done
24
+ done
tools/run_predictor_v4_atc_global_sequential_8gpu.sh ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Train the global-transport ATC variants one after another on 8 GPUs:
3
+ # per-rank batch 8, no gradient accumulation (global batch 64), 2000 steps each.
4
+ set -euo pipefail
5
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ max_steps="${ATC_MAX_STEPS:-2000}"
7
+ scopes=("${@:-}")
8
+ [[ -n "${scopes[0]}" ]] || scopes=(chunk last_frame)
9
+ for scope in "${scopes[@]}"; do
10
+ echo "[$(date -Is)] start scope=${scope}"
11
+ PREDICTOR_RDZV_PORT=29501 bash "${repo_root}/tools/launch_predictor_v4_atc_global_stage1.sh" \
12
+ "${scope}" 0,1,2,3,4,5,6,7 8 1 "${max_steps}"
13
+ echo "[$(date -Is)] done scope=${scope}"
14
+ done
tools/run_predictor_v4_detached_timestep_rollout_offline_targets_overfit8.sh ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ python_bin="/mnt/s3files/s3-us-west2-default/zoubin/cz/envs/hy_worldplay/bin/python"
6
+ config_tag="flr1e-4_blr1e-5_schedsteps1000_swarmup8_bwarmup8"
7
+ run_name="detached-timestep-rollout-offline-ffff-targets_blocks1-52_overfit8_seed20260721_fp32master_8gpu_${config_tag}"
8
+ checkpoint_dir="${repo_root}/checkpoints/predictor_v4_${run_name}"
9
+ eval_prefix="detached-timestep-rollout-offline-ffff-targets-overfit8"
10
+ summary_name="summary_${eval_prefix}_ckptstep1000_${config_tag}"
11
+ summary_root="/mnt/local_nvme/zoubin/cz/hyworldplay_${summary_name}"
12
+ final_weights="${checkpoint_dir}/predictor_step_01000.safetensors"
13
+ mkdir -p "${checkpoint_dir}" "${summary_root}"
14
+
15
+ export BUCKET="${BUCKET:-s3-us-west2-default}"
16
+ export FS_ID="${FS_ID:-fs-05c60be7352499179}"
17
+ export MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files/s3-us-west2-default}"
18
+ export PYTHONPATH="${repo_root}"
19
+
20
+ exec 9>"${summary_root}/pipeline.lock"
21
+ if ! flock -n 9; then
22
+ echo "[pipeline] detached offline-target rollout pipeline is already active"
23
+ exit 0
24
+ fi
25
+
26
+ run_with_retries() {
27
+ local stage="$1"
28
+ shift
29
+ local attempt=1
30
+ while true; do
31
+ echo "[pipeline] stage=${stage} attempt=${attempt} start=$(date -u +%FT%TZ)"
32
+ if "$@"; then
33
+ echo "[pipeline] stage=${stage} complete=$(date -u +%FT%TZ)"
34
+ return 0
35
+ fi
36
+ if [[ "${attempt}" -ge 5 ]]; then
37
+ echo "[pipeline] stage=${stage} failed after ${attempt} attempts" >&2
38
+ return 1
39
+ fi
40
+ attempt=$((attempt + 1))
41
+ sleep 30
42
+ done
43
+ }
44
+
45
+ train_model() {
46
+ if [[ -f "${final_weights}" ]]; then
47
+ echo "[pipeline] step 1000 already exists"
48
+ return 0
49
+ fi
50
+ local resume_path=""
51
+ if [[ -f "${checkpoint_dir}/training_latest.pt" ]]; then
52
+ resume_path="${checkpoint_dir}/training_latest.pt"
53
+ fi
54
+ bash "${repo_root}/tools/launch_predictor_v4_detached_timestep_rollout_offline_targets_overfit8_training.sh" \
55
+ "${checkpoint_dir}" "predictor-v4-${run_name//_/-}" "${resume_path}" 1000
56
+ [[ -f "${final_weights}" ]]
57
+ }
58
+
59
+ evaluate_model() {
60
+ local schedule="$1"
61
+ local replace_steps="$2"
62
+ local eval_root="/mnt/local_nvme/zoubin/cz/hyworldplay_eval_v4_${eval_prefix}_schedule-${schedule}_blocks1-52_ckptstep1000_${config_tag}"
63
+ local durable_name="eval_${eval_prefix}_schedule-${schedule}_blocks1-52_ckptstep1000_${config_tag}"
64
+ if [[ -f "${eval_root}/metrics.completed" && -f "${eval_root}/sync.completed" ]]; then
65
+ echo "[pipeline] ${schedule} step-1000 evaluation already complete"
66
+ return 0
67
+ fi
68
+ bash "${repo_root}/tools/launch_predictor_v4_rollout_overfit8_eval.sh" \
69
+ "${eval_root}" "${final_weights}" "${durable_name}" "${replace_steps}"
70
+ [[ -f "${eval_root}/metrics.json" && -f "${eval_root}/sync.completed" ]]
71
+ }
72
+
73
+ compare_to() {
74
+ local baseline_label="$1"
75
+ local baseline_fppf="$2"
76
+ local baseline_fppp="$3"
77
+ local output_name="$4"
78
+ local candidate_prefix="/mnt/local_nvme/zoubin/cz/hyworldplay_eval_v4_${eval_prefix}"
79
+ "${python_bin}" "${repo_root}/tools/compare_predictor_overfit8_metrics.py" \
80
+ --baseline_label "${baseline_label}" \
81
+ --baseline_fppf "${baseline_fppf}" \
82
+ --baseline_fppp "${baseline_fppp}" \
83
+ --candidate_label "detached-timestep-rollout-offline-ffff-targets-step1000" \
84
+ --candidate_fppf "${candidate_prefix}_schedule-fppf_blocks1-52_ckptstep1000_${config_tag}/metrics.json" \
85
+ --candidate_fppp "${candidate_prefix}_schedule-fppp_blocks1-52_ckptstep1000_${config_tag}/metrics.json" \
86
+ --output "${summary_root}/${output_name}.json" \
87
+ >"${summary_root}/${output_name}.log" 2>&1
88
+ }
89
+
90
+ run_with_retries dataset \
91
+ bash "${repo_root}/tools/ensure_predictor_prefeature_nvme.sh"
92
+ run_with_retries train_step1000 train_model
93
+ run_with_retries eval_step1000_fppf evaluate_model fppf "1,2"
94
+ run_with_retries eval_step1000_fppp evaluate_model fppp "1,2,3"
95
+
96
+ run_with_retries compare_stage1 compare_to \
97
+ "stage1-step2000" \
98
+ "${repo_root}/outputs/predictor_v4/eval_stage1_overfit8_schedule-fppf_blocks1-52_ckptstep2000_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100/metrics.json" \
99
+ "${repo_root}/outputs/predictor_v4/eval_stage1_overfit8_schedule-fppp_blocks1-52_ckptstep2000_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100/metrics.json" \
100
+ comparison_vs_stage1
101
+ run_with_retries compare_grouped_offline compare_to \
102
+ "grouped-teacher-forcing-offline-prefeature-step1000" \
103
+ "${repo_root}/outputs/predictor_v4/eval_grouped-teacher-forcing-offline-prefeature-overfit8_schedule-fppf_blocks1-52_ckptstep1000_${config_tag}/metrics.json" \
104
+ "${repo_root}/outputs/predictor_v4/eval_grouped-teacher-forcing-offline-prefeature-overfit8_schedule-fppp_blocks1-52_ckptstep1000_${config_tag}/metrics.json" \
105
+ comparison_vs_grouped_offline
106
+ run_with_retries compare_detached_online compare_to \
107
+ "detached-timestep-only-online-same-state-step1000" \
108
+ "${repo_root}/outputs/predictor_v4/eval_timestep-rollout-detached-timestepfp32-overfit8-pure_schedule-fppf_blocks1-52_ckptstep1000_${config_tag}/metrics.json" \
109
+ "${repo_root}/outputs/predictor_v4/eval_timestep-rollout-detached-timestepfp32-overfit8-pure_schedule-fppp_blocks1-52_ckptstep1000_${config_tag}/metrics.json" \
110
+ comparison_vs_detached_online_same_state
111
+
112
+ date -u +%FT%TZ >"${summary_root}/pipeline.completed"
113
+ aws s3 sync "${summary_root}" \
114
+ "s3://${BUCKET}/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/outputs/predictor_v4/${summary_name}" \
115
+ --only-show-errors
116
+ echo "[pipeline] training, final evaluations, and three comparisons complete"
tools/run_predictor_v4_grouped_teacher_forcing_overfit8.sh ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ python_bin="/mnt/s3files/s3-us-west2-default/zoubin/cz/envs/hy_worldplay/bin/python"
6
+ config_tag="flr1e-4_blr1e-5_schedsteps1000_swarmup8_bwarmup8"
7
+ run_name="grouped-teacher-forcing-offline-prefeature_blocks1-52_overfit8_seed20260721_fp32master_8gpu_${config_tag}"
8
+ checkpoint_dir="${repo_root}/checkpoints/predictor_v4_${run_name}"
9
+ eval_prefix="grouped-teacher-forcing-offline-prefeature-overfit8"
10
+ summary_name="summary_grouped-teacher-forcing-offline-prefeature-vs-detached-timestepfp32-overfit8_ckptstep1000_${config_tag}"
11
+ summary_root="/mnt/local_nvme/zoubin/cz/hyworldplay_${summary_name}"
12
+ final_weights="${checkpoint_dir}/predictor_step_01000.safetensors"
13
+ mkdir -p "${checkpoint_dir}" "${summary_root}"
14
+
15
+ export BUCKET="${BUCKET:-s3-us-west2-default}"
16
+ export FS_ID="${FS_ID:-fs-05c60be7352499179}"
17
+ export MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files/s3-us-west2-default}"
18
+ export PYTHONPATH="${repo_root}"
19
+
20
+ exec 9>"${summary_root}/pipeline.lock"
21
+ if ! flock -n 9; then
22
+ echo "[pipeline] grouped teacher-forcing pipeline is already active"
23
+ exit 0
24
+ fi
25
+
26
+ run_with_retries() {
27
+ local stage="$1"
28
+ shift
29
+ local attempt=1
30
+ while true; do
31
+ echo "[pipeline] stage=${stage} attempt=${attempt} start=$(date -u +%FT%TZ)"
32
+ if "$@"; then
33
+ echo "[pipeline] stage=${stage} complete=$(date -u +%FT%TZ)"
34
+ return 0
35
+ fi
36
+ if [[ "${attempt}" -ge 5 ]]; then
37
+ echo "[pipeline] stage=${stage} failed after ${attempt} attempts" >&2
38
+ return 1
39
+ fi
40
+ attempt=$((attempt + 1))
41
+ sleep 30
42
+ done
43
+ }
44
+
45
+ train_grouped() {
46
+ if [[ -f "${final_weights}" ]]; then
47
+ echo "[pipeline] grouped teacher-forcing step 1000 already exists"
48
+ return 0
49
+ fi
50
+ local resume_path=""
51
+ if [[ -f "${checkpoint_dir}/training_latest.pt" ]]; then
52
+ resume_path="${checkpoint_dir}/training_latest.pt"
53
+ fi
54
+ bash "${repo_root}/tools/launch_predictor_v4_grouped_teacher_forcing_overfit8_training.sh" \
55
+ "${checkpoint_dir}" \
56
+ "predictor-v4-${run_name//_/-}" \
57
+ "${resume_path}" 1000
58
+ [[ -f "${final_weights}" ]]
59
+ }
60
+
61
+ evaluate_grouped() {
62
+ local schedule="$1"
63
+ local replace_steps="$2"
64
+ local eval_root="/mnt/local_nvme/zoubin/cz/hyworldplay_eval_v4_${eval_prefix}_schedule-${schedule}_blocks1-52_ckptstep1000_${config_tag}"
65
+ local durable_name="eval_${eval_prefix}_schedule-${schedule}_blocks1-52_ckptstep1000_${config_tag}"
66
+ if [[ -f "${eval_root}/metrics.completed" && -f "${eval_root}/sync.completed" ]]; then
67
+ echo "[pipeline] ${schedule} step-1000 evaluation already complete"
68
+ return 0
69
+ fi
70
+ bash "${repo_root}/tools/launch_predictor_v4_rollout_overfit8_eval.sh" \
71
+ "${eval_root}" "${final_weights}" "${durable_name}" "${replace_steps}"
72
+ [[ -f "${eval_root}/metrics.json" && -f "${eval_root}/sync.completed" ]]
73
+ }
74
+
75
+ run_with_retries dataset \
76
+ bash "${repo_root}/tools/ensure_predictor_prefeature_nvme.sh"
77
+ run_with_retries train_step1000 train_grouped
78
+
79
+ # Only the final step-1000 checkpoint is evaluated.
80
+ run_with_retries eval_step1000_fppf evaluate_grouped fppf "1,2"
81
+ run_with_retries eval_step1000_fppp evaluate_grouped fppp "1,2,3"
82
+
83
+ baseline_prefix="${repo_root}/outputs/predictor_v4/eval_timestep-rollout-detached-timestepfp32-overfit8-pure"
84
+ baseline_suffix="blocks1-52_ckptstep1000_${config_tag}/metrics.json"
85
+ candidate_prefix="/mnt/local_nvme/zoubin/cz/hyworldplay_eval_v4_${eval_prefix}"
86
+ "${python_bin}" "${repo_root}/tools/compare_predictor_overfit8_metrics.py" \
87
+ --baseline_label "detached-timestep-only-step1000" \
88
+ --baseline_fppf "${baseline_prefix}_schedule-fppf_${baseline_suffix}" \
89
+ --baseline_fppp "${baseline_prefix}_schedule-fppp_${baseline_suffix}" \
90
+ --candidate_label "grouped-teacher-forcing-offline-prefeature-step1000" \
91
+ --candidate_fppf "${candidate_prefix}_schedule-fppf_blocks1-52_ckptstep1000_${config_tag}/metrics.json" \
92
+ --candidate_fppp "${candidate_prefix}_schedule-fppp_blocks1-52_ckptstep1000_${config_tag}/metrics.json" \
93
+ --output "${summary_root}/comparison.json" \
94
+ >"${summary_root}/comparison.log" 2>&1
95
+ date -u +%FT%TZ >"${summary_root}/pipeline.completed"
96
+ aws s3 sync "${summary_root}" \
97
+ "s3://${BUCKET}/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/outputs/predictor_v4/${summary_name}" \
98
+ --only-show-errors
99
+ echo "[pipeline] grouped teacher forcing, final evaluations, and comparison complete"
tools/run_predictor_v4_timestep_rollout_detached_overfit8_experiments.sh ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ export TIMESTEP_ROLLOUT_SUITE_NAME="timestep_rollout_detached_timestepfp32_blocks1-52_overfit8_seed20260721_fp32master_8gpu"
6
+ export TIMESTEP_ROLLOUT_EVAL_SUITE_NAME="timestep-rollout-detached-timestepfp32-overfit8"
7
+ export TIMESTEP_ROLLOUT_DETACH_BETWEEN_TIMESTEPS="true"
8
+ export TIMESTEP_ROLLOUT_VARIANTS="pure:0.0"
9
+ export TIMESTEP_ROLLOUT_MAX_STEPS="1000"
10
+
11
+ exec bash "${repo_root}/tools/run_predictor_v4_timestep_rollout_overfit8_experiments.sh"
tools/run_predictor_v4_timestep_rollout_overfit8_experiments.sh ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ python_bin="/mnt/s3files/s3-us-west2-default/zoubin/cz/envs/hy_worldplay/bin/python"
6
+ suite_name="${TIMESTEP_ROLLOUT_SUITE_NAME:-timestep_rollout_timestepfp32_blocks1-52_overfit8_seed20260721_fp32master_8gpu}"
7
+ eval_suite_name="${TIMESTEP_ROLLOUT_EVAL_SUITE_NAME:-timestep-rollout-timestepfp32-overfit8}"
8
+ detach_between_timesteps="${TIMESTEP_ROLLOUT_DETACH_BETWEEN_TIMESTEPS:-false}"
9
+ variant_specs="${TIMESTEP_ROLLOUT_VARIANTS:-replay0:0.0 replay0p2:0.2 replay0p4:0.4}"
10
+ max_steps="${TIMESTEP_ROLLOUT_MAX_STEPS:-200}"
11
+ stop_after_steps="${TIMESTEP_ROLLOUT_STOP_AFTER_STEPS:-${max_steps}}"
12
+ checkpoint_parent="${repo_root}/checkpoints/predictor_v4_${suite_name}"
13
+ config_tag="flr1e-4_blr1e-5_schedsteps${max_steps}_swarmup8_bwarmup8"
14
+ checkpoint_parent="${checkpoint_parent}_${config_tag}"
15
+ eval_parent="/mnt/local_nvme/zoubin/cz/hyworldplay_eval_v4_${eval_suite_name}_blocks1-52_ckptstep${stop_after_steps}_${config_tag}"
16
+ pipeline_root="/mnt/local_nvme/zoubin/cz/hyworldplay_summary_v4_${eval_suite_name}_ckptstep${stop_after_steps}_${config_tag}"
17
+ mkdir -p "${checkpoint_parent}" "${eval_parent}" "${pipeline_root}"
18
+
19
+ export BUCKET="${BUCKET:-s3-us-west2-default}"
20
+ export FS_ID="${FS_ID:-fs-05c60be7352499179}"
21
+ export MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files/s3-us-west2-default}"
22
+ export PYTHONPATH="${repo_root}"
23
+
24
+ exec 9>"${pipeline_root}/pipeline.lock"
25
+ if ! flock -n 9; then
26
+ echo "[pipeline] another timestep-rollout sweep is already active"
27
+ exit 0
28
+ fi
29
+
30
+ run_with_retries() {
31
+ local stage="$1"
32
+ shift
33
+ local attempt=1
34
+ while true; do
35
+ echo "[pipeline] stage=${stage} attempt=${attempt} start=$(date -u +%FT%TZ)"
36
+ if "$@"; then
37
+ echo "[pipeline] stage=${stage} complete=$(date -u +%FT%TZ)"
38
+ return 0
39
+ fi
40
+ if [[ "${attempt}" -ge 5 ]]; then
41
+ echo "[pipeline] stage=${stage} failed after ${attempt} attempts" >&2
42
+ return 1
43
+ fi
44
+ attempt=$((attempt + 1))
45
+ sleep 30
46
+ done
47
+ }
48
+
49
+ train_variant() {
50
+ local label="$1"
51
+ local replay_weight="$2"
52
+ local output_dir="${checkpoint_parent}/${label}"
53
+ local final_step
54
+ printf -v final_step "%05d" "${stop_after_steps}"
55
+ local final_weights="${output_dir}/predictor_step_${final_step}.safetensors"
56
+ if [[ -f "${final_weights}" ]]; then
57
+ echo "[pipeline] training ${label} already complete"
58
+ return 0
59
+ fi
60
+ local resume_path=""
61
+ if [[ -f "${output_dir}/training_latest.pt" ]]; then
62
+ resume_path="${output_dir}/training_latest.pt"
63
+ fi
64
+ bash "${repo_root}/tools/launch_predictor_v4_timestep_rollout_overfit8_training.sh" \
65
+ "${output_dir}" "${replay_weight}" \
66
+ "predictor-v4-${suite_name}-${label}-${config_tag//_/-}" \
67
+ "${resume_path}" "${detach_between_timesteps}" "${max_steps}" \
68
+ "${stop_after_steps}"
69
+ [[ -f "${final_weights}" ]]
70
+ }
71
+
72
+ evaluate_variant() {
73
+ local label="$1"
74
+ local schedule="$2"
75
+ local replace_steps="$3"
76
+ local final_step
77
+ printf -v final_step "%05d" "${stop_after_steps}"
78
+ local weights="${checkpoint_parent}/${label}/predictor_step_${final_step}.safetensors"
79
+ local eval_root="${eval_parent}/${label}_${schedule}"
80
+ local durable_name="eval_${eval_suite_name}_${label}_schedule-${schedule}_blocks1-52_ckptstep${stop_after_steps}_${config_tag}"
81
+ if [[ -f "${eval_root}/metrics.completed" && -f "${eval_root}/sync.completed" ]]; then
82
+ echo "[pipeline] evaluation ${label}/${schedule} already complete"
83
+ return 0
84
+ fi
85
+ bash "${repo_root}/tools/launch_predictor_v4_rollout_overfit8_eval.sh" \
86
+ "${eval_root}" "${weights}" "${durable_name}" "${replace_steps}"
87
+ [[ -f "${eval_root}/metrics.json" && -f "${eval_root}/sync.completed" ]]
88
+ }
89
+
90
+ run_with_retries sidecar \
91
+ bash "${repo_root}/tools/ensure_predictor_timestep_rollout_overfit8_sidecar.sh"
92
+
93
+ for spec in ${variant_specs}; do
94
+ label="${spec%%:*}"
95
+ replay_weight="${spec##*:}"
96
+ run_with_retries "train_${label}" train_variant "${label}" "${replay_weight}"
97
+ done
98
+
99
+ for spec in ${variant_specs}; do
100
+ label="${spec%%:*}"
101
+ run_with_retries "eval_${label}_fppp" evaluate_variant "${label}" fppp "1,2,3"
102
+ run_with_retries "eval_${label}_fppf" evaluate_variant "${label}" fppf "1,2"
103
+ done
104
+
105
+ summary_path="${pipeline_root}/summary.json"
106
+ "${python_bin}" "${repo_root}/tools/summarize_predictor_timestep_rollout_overfit8.py" \
107
+ --eval_parent "${eval_parent}" --output "${summary_path}" \
108
+ --experiment "v4 ${suite_name}; scheduler_max_steps=${max_steps}; stop_after_steps=${stop_after_steps}; detach_between_timesteps=${detach_between_timesteps}" \
109
+ --variants "${variant_specs}" \
110
+ >"${pipeline_root}/summary.log" 2>&1
111
+ date -u +%FT%TZ >"${pipeline_root}/pipeline.completed"
112
+ aws s3 sync "${pipeline_root}" \
113
+ "s3://${BUCKET}/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/outputs/predictor_v4/summary_${eval_suite_name}_ckptstep${stop_after_steps}_${config_tag}" \
114
+ --only-show-errors
115
+ echo "[pipeline] all training and evaluation stages complete"
tools/run_predictor_v4_validation5_rollout_comparison.sh ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ dataset_name="hyworldplay_predictor_vbench_val25_test50"
6
+ dataset_root="${PREDICTOR_HOLDOUT_NVME_ROOT:-/mnt/local_nvme/zoubin/cz/${dataset_name}}"
7
+ validation_root="${dataset_root}/validation"
8
+ python_bin="/mnt/s3files/s3-us-west2-default/zoubin/cz/envs/hy_worldplay/bin/python"
9
+ case_ids="0,8,15,19,21"
10
+ subset_tag="validation5_seed20260722_cases0-8-15-19-21"
11
+ config_tag="flr1e-4_blr1e-5_schedsteps1000_swarmup8_bwarmup8"
12
+ summary_root="${dataset_root}/evaluations/summary_${subset_tag}_three-stage2-variants"
13
+ pipeline_log_dir="${summary_root}/logs"
14
+ mkdir -p "${pipeline_log_dir}"
15
+
16
+ export BUCKET="${BUCKET:-s3-us-west2-default}"
17
+ export FS_ID="${FS_ID:-fs-05c60be7352499179}"
18
+ export MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files/s3-us-west2-default}"
19
+ export PYTHONPATH="${repo_root}"
20
+ export HY_PROFILE_TIMING=1
21
+
22
+ durable_dataset_uri="s3://${BUCKET}/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/datasets/${dataset_name}"
23
+ durable_eval_uri="${durable_dataset_uri}/evaluations"
24
+
25
+ exec 9>"${summary_root}/pipeline.lock"
26
+ if ! flock -n 9; then
27
+ echo "[pipeline] validation5 comparison is already active"
28
+ exit 0
29
+ fi
30
+
31
+ if [[ ! -f "${validation_root}/cases.jsonl" ]] \
32
+ || [[ $(find "${validation_root}/input_images" -type f -name '*.jpg' 2>/dev/null | wc -l) -ne 25 ]] \
33
+ || [[ $(find "${validation_root}/full" -type f -name '*.mp4' 2>/dev/null | wc -l) -ne 100 ]]; then
34
+ echo "[dataset] restoring validation cache from ${durable_dataset_uri}"
35
+ mkdir -p "${dataset_root}"
36
+ aws s3 sync "${durable_dataset_uri}" "${dataset_root}" --only-show-errors
37
+ fi
38
+
39
+ [[ -f "${validation_root}/cases.jsonl" ]]
40
+ [[ $(find "${validation_root}/input_images" -type f -name '*.jpg' | wc -l) -eq 25 ]]
41
+ [[ $(find "${validation_root}/full" -type f -name '*.mp4' | wc -l) -eq 100 ]]
42
+ cp "${repo_root}/configs/validation5_seed20260722.json" "${summary_root}/selection.json"
43
+
44
+ declare -a variants=(
45
+ "online-same-state|${repo_root}/checkpoints/predictor_v4_timestep_rollout_detached_timestepfp32_blocks1-52_overfit8_seed20260721_fp32master_8gpu_${config_tag}/pure/predictor_step_01000.safetensors"
46
+ "grouped-offline-teacher-forcing|${repo_root}/checkpoints/predictor_v4_grouped-teacher-forcing-offline-prefeature_blocks1-52_overfit8_seed20260721_fp32master_8gpu_${config_tag}/predictor_step_01000.safetensors"
47
+ "offline-ffff-target-rollout|${repo_root}/checkpoints/predictor_v4_detached-timestep-rollout-offline-ffff-targets_blocks1-52_overfit8_seed20260721_fp32master_8gpu_${config_tag}/predictor_step_01000.safetensors"
48
+ )
49
+
50
+ run_workers() {
51
+ local log_dir="$1"
52
+ local phase="$2"
53
+ shift 2
54
+ local pids=()
55
+ local worker_id worker_pid failed=0
56
+ mkdir -p "${log_dir}"
57
+ for worker_id in $(seq 0 7); do
58
+ CUDA_VISIBLE_DEVICES="${worker_id}" \
59
+ "${python_bin}" -u "$@" \
60
+ --worker_id "${worker_id}" --num_workers 8 \
61
+ >"${log_dir}/${phase}_worker_${worker_id}.log" 2>&1 &
62
+ worker_pid=$!
63
+ pids+=("${worker_pid}")
64
+ echo "${worker_pid}" >"${log_dir}/${phase}_worker_${worker_id}.pid"
65
+ done
66
+ for worker_pid in "${pids[@]}"; do
67
+ if ! wait "${worker_pid}"; then
68
+ failed=1
69
+ fi
70
+ done
71
+ if [[ "${failed}" -ne 0 ]]; then
72
+ echo "${phase} worker failure; inspect ${log_dir}/${phase}_worker_*.log" >&2
73
+ return 1
74
+ fi
75
+ }
76
+
77
+ run_with_retries() {
78
+ local stage="$1"
79
+ shift
80
+ local attempt=1
81
+ while true; do
82
+ echo "[pipeline] stage=${stage} attempt=${attempt} start=$(date -u +%FT%TZ)"
83
+ if "$@"; then
84
+ echo "[pipeline] stage=${stage} complete=$(date -u +%FT%TZ)"
85
+ return 0
86
+ fi
87
+ if [[ "${attempt}" -ge 5 ]]; then
88
+ echo "[pipeline] stage=${stage} failed after ${attempt} attempts" >&2
89
+ return 1
90
+ fi
91
+ attempt=$((attempt + 1))
92
+ sleep 30
93
+ done
94
+ }
95
+
96
+ evaluate_one() {
97
+ local variant="$1"
98
+ local weights="$2"
99
+ local schedule="$3"
100
+ local replace_steps="$4"
101
+ local eval_name="eval_${subset_tag}_${variant}_schedule-${schedule}_blocks1-52_ckptstep1000_${config_tag}"
102
+ local eval_root="${dataset_root}/evaluations/${eval_name}"
103
+ local log_dir="${eval_root}/logs"
104
+ mkdir -p "${log_dir}"
105
+ [[ -f "${weights}" ]]
106
+
107
+ local video_count=0
108
+ if [[ -d "${eval_root}/predictor" ]]; then
109
+ video_count=$(find "${eval_root}/predictor" -type f -name '*.mp4' | wc -l)
110
+ fi
111
+ if [[ ! -f "${eval_root}/generation.completed" || "${video_count}" -ne 20 ]]; then
112
+ date -u +%FT%TZ >"${eval_root}/generation.started"
113
+ run_workers "${log_dir}" generation \
114
+ "${repo_root}/tools/generate_predictor_v4_train_case_eval.py" \
115
+ --dataset_root "${validation_root}" \
116
+ --output_root "${eval_root}" \
117
+ --weights "${weights}" \
118
+ --source_blocks 1,52 \
119
+ --predictor_variant v4 \
120
+ --case_ids "${case_ids}" \
121
+ --modes predictor \
122
+ --replace_steps "${replace_steps}"
123
+ [[ $(find "${eval_root}/predictor" -type f -name '*.mp4' | wc -l) -eq 20 ]]
124
+ date -u +%FT%TZ >"${eval_root}/generation.completed"
125
+ else
126
+ echo "[skip] generation complete: ${eval_name}"
127
+ fi
128
+
129
+ local metric_records=0
130
+ if [[ -f "${eval_root}/metrics.json" ]]; then
131
+ metric_records=$("${python_bin}" -c \
132
+ 'import json,sys; print(len(json.load(open(sys.argv[1]))["records"]))' \
133
+ "${eval_root}/metrics.json")
134
+ fi
135
+ if [[ ! -f "${eval_root}/metrics.completed" || "${metric_records}" -ne 20 ]]; then
136
+ date -u +%FT%TZ >"${eval_root}/metrics.started"
137
+ run_workers "${log_dir}" metrics \
138
+ "${repo_root}/tools/compute_predictor_v4_train_case_metrics.py" \
139
+ --dataset_root "${validation_root}" \
140
+ --output_root "${eval_root}" \
141
+ --num_cases 25 \
142
+ --case_ids "${case_ids}" \
143
+ --reference_subdir full \
144
+ --modes predictor \
145
+ --metric_batch_size 4
146
+ "${python_bin}" "${repo_root}/tools/merge_predictor_v4_train_case_metrics.py" \
147
+ --output_root "${eval_root}" \
148
+ --num_workers 8 \
149
+ --num_cases 5 \
150
+ --modes predictor \
151
+ --replace_steps "${replace_steps}" \
152
+ --reference_label "Full-DiT validation videos; ${subset_tag}" \
153
+ >"${log_dir}/metrics_merge.log" 2>&1
154
+ [[ $("${python_bin}" -c \
155
+ 'import json,sys; print(len(json.load(open(sys.argv[1]))["records"]))' \
156
+ "${eval_root}/metrics.json") -eq 20 ]]
157
+ date -u +%FT%TZ >"${eval_root}/metrics.completed"
158
+ else
159
+ echo "[skip] metrics complete: ${eval_name}"
160
+ fi
161
+
162
+ aws s3 sync "${eval_root}" "${durable_eval_uri}/${eval_name}" --only-show-errors
163
+ date -u +%FT%TZ >"${eval_root}/sync.completed"
164
+ aws s3 cp "${eval_root}/sync.completed" \
165
+ "${durable_eval_uri}/${eval_name}/sync.completed" --only-show-errors
166
+ }
167
+
168
+ for spec in "${variants[@]}"; do
169
+ IFS='|' read -r variant weights <<<"${spec}"
170
+ run_with_retries "${variant}_fppf" evaluate_one "${variant}" "${weights}" fppf "1,2"
171
+ run_with_retries "${variant}_fppp" evaluate_one "${variant}" "${weights}" fppp "1,2,3"
172
+ done
173
+
174
+ date -u +%FT%TZ >"${summary_root}/pipeline.completed"
175
+ aws s3 sync "${summary_root}" "${durable_eval_uri}/$(basename "${summary_root}")" --only-show-errors
176
+ echo "[pipeline] all validation5 evaluations completed"
tools/run_stage1_continued_validation25_evaluation.sh ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ vbench_repo="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/VBench"
6
+ dataset_name="hyworldplay_predictor_vbench_val25_test50"
7
+ dataset_root="${PREDICTOR_HOLDOUT_NVME_ROOT:-/mnt/local_nvme/zoubin/cz/${dataset_name}}"
8
+ python_bin="/mnt/s3files/s3-us-west2-default/zoubin/cz/envs/hy_worldplay/bin/python"
9
+ weights="${repo_root}/checkpoints/predictor_v4_prefeature-continued-from-stage1step2000_blocks1-52_fulltrain100_fp32master_8gpu_flr5e-5_blr5e-6_schedsteps2000_swarmup100_bwarmup100linear_bs16_swanlab/predictor_step_02000.safetensors"
10
+ eval_prefix="eval_validation25_stage1-continued-offline-tf"
11
+ eval_suffix="blocks1-52_ckptstep2000-initstage1step2000_flr5e-5_blr5e-6_schedsteps2000_swarmup100_bwarmup100linear_bs16"
12
+ fppf_name="${eval_prefix}_schedule-fppf_${eval_suffix}"
13
+ fppp_name="${eval_prefix}_schedule-fppp_${eval_suffix}"
14
+ run_name="validation25_stage1_continued_offline_tf_step2000"
15
+ run_root="${dataset_root}/vbench/${run_name}"
16
+ staging_root="${dataset_root}/vbench_staging/${run_name}"
17
+ durable_uri="s3://s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/datasets/${dataset_name}/vbench/${run_name}"
18
+
19
+ export PYTHONPATH="${repo_root}"
20
+
21
+ bash "${repo_root}/tools/launch_vbench_validation_predictor.sh" \
22
+ "${fppf_name}" "${weights}" "1,52" v4 "1,2"
23
+ bash "${repo_root}/tools/launch_vbench_validation_predictor.sh" \
24
+ "${fppp_name}" "${weights}" "1,52" v4 "1,2,3"
25
+
26
+ export PYTHONPATH="${vbench_repo}:${repo_root}"
27
+ export VBENCH_CACHE_DIR="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/vbench"
28
+ export TORCH_HOME="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/torch"
29
+ export HF_HOME="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/huggingface"
30
+
31
+ mkdir -p "${run_root}/raw" "${run_root}/logs" "${staging_root}"
32
+
33
+ group_fppf="stage1_continued_offline_tf_fppf"
34
+ group_fppp="stage1_continued_offline_tf_fppp"
35
+ declare -a groups=(
36
+ "${group_fppf}|${dataset_root}/evaluations/${fppf_name}/predictor"
37
+ "${group_fppp}|${dataset_root}/evaluations/${fppp_name}/predictor"
38
+ )
39
+
40
+ find "${staging_root}" -maxdepth 1 -type l -delete
41
+ for spec in "${groups[@]}"; do
42
+ IFS='|' read -r label source_dir <<<"${spec}"
43
+ video_count=$(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | wc -l)
44
+ if [[ "${video_count}" -ne 100 ]]; then
45
+ echo "${label}: expected 100 videos, found ${video_count}: ${source_dir}" >&2
46
+ exit 1
47
+ fi
48
+ while IFS= read -r video_path; do
49
+ ln -s "${video_path}" "${staging_root}/${label}__$(basename "${video_path}")"
50
+ done < <(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | sort)
51
+ done
52
+
53
+ staged_count=$(find "${staging_root}" -maxdepth 1 -type l | wc -l)
54
+ if [[ "${staged_count}" -ne 200 ]]; then
55
+ echo "Expected 200 staged videos, found ${staged_count}" >&2
56
+ exit 1
57
+ fi
58
+
59
+ prompt_file="${run_root}/validation25_prompt_map.json"
60
+ "${python_bin}" "${repo_root}/tools/build_vbench_validation25_prompt_map.py" \
61
+ --cases_jsonl "${dataset_root}/validation/cases.jsonl" \
62
+ --staging_root "${staging_root}" \
63
+ --output "${prompt_file}" \
64
+ --expected_count 200
65
+
66
+ dimensions=(
67
+ subject_consistency
68
+ background_consistency
69
+ temporal_flickering
70
+ motion_smoothness
71
+ dynamic_degree
72
+ aesthetic_quality
73
+ imaging_quality
74
+ overall_consistency
75
+ )
76
+
77
+ for dimension in "${dimensions[@]}"; do
78
+ output_dir="${run_root}/raw/${dimension}"
79
+ marker="${run_root}/${dimension}.completed"
80
+ mkdir -p "${output_dir}"
81
+ result_count=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json' | wc -l)
82
+ if [[ -f "${marker}" && "${result_count}" -eq 1 ]]; then
83
+ echo "[skip] ${dimension} already complete"
84
+ continue
85
+ fi
86
+ find "${output_dir}" -maxdepth 1 -type f -delete
87
+ echo "[vbench] ${dimension} start $(date -u +%FT%TZ)"
88
+ "${python_bin}" -m torch.distributed.run \
89
+ --standalone \
90
+ --nproc_per_node=8 \
91
+ "${vbench_repo}/evaluate.py" \
92
+ --videos_path "${staging_root}" \
93
+ --dimension "${dimension}" \
94
+ --mode custom_input \
95
+ --prompt_file "${prompt_file}" \
96
+ --load_ckpt_from_local True \
97
+ --imaging_quality_preprocessing_mode longer \
98
+ --output_path "${output_dir}" \
99
+ >"${run_root}/logs/${dimension}.log" 2>&1
100
+ result_json=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json')
101
+ "${python_bin}" - "${result_json}" "${dimension}" <<'PY'
102
+ import json
103
+ import sys
104
+
105
+ path, dimension = sys.argv[1:]
106
+ payload = json.load(open(path))
107
+ records = payload[dimension][1]
108
+ assert len(records) == 200, (dimension, len(records))
109
+ print(dimension, len(records), payload[dimension][0])
110
+ PY
111
+ date -u +%FT%TZ >"${marker}"
112
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
113
+ echo "[vbench] ${dimension} complete $(date -u +%FT%TZ)"
114
+ done
115
+
116
+ "${python_bin}" "${repo_root}/tools/summarize_vbench_validation25_pair.py" \
117
+ --run_root "${run_root}" \
118
+ --group "${group_fppf}" \
119
+ --group "${group_fppp}" \
120
+ >"${run_root}/logs/summary.log" 2>&1
121
+ date -u +%FT%TZ >"${run_root}/pipeline.completed"
122
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
123
+ echo "[validation] all standard and VBench metrics complete: ${run_root}"
tools/run_stage1_previous_condition_ablation.sh ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root=/mnt/local_nvme/cz/projects/HY-WorldPlay-DEV-Predictor
5
+ predictor_env=/mnt/local_nvme/cz/envs/hy_worldplay_july20260720
6
+ python_bin="$predictor_env/bin/python"
7
+ manifest="$repo_root/datasets/hyworldplay_predictor_prefeature_v4/train_eval_manifest.jsonl"
8
+ teacher=/mnt/local_nvme/cz/checkpoints/hy_worldplay/huggingface/hub/models--tencent--HY-WorldPlay/snapshots/f4c29235647707b571479a69b569e4166f9f5bf8/ar_distilled_action_model/diffusion_pytorch_model.safetensors
9
+ suite_root="$repo_root/checkpoints/predictor_v4_stage1_previous_condition_ablation_seed0"
10
+ log_root="$repo_root/logs/predictor_v4_stage1_previous_condition_ablation_seed0"
11
+ s3_root=s3://s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor
12
+ modes=(none previous_step both)
13
+
14
+ export PATH="$predictor_env/bin:$PATH"
15
+ export PYTHONPATH="$repo_root"
16
+ export OMP_NUM_THREADS=4
17
+ export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
18
+ export TORCH_NCCL_ASYNC_ERROR_HANDLING=1
19
+ export SSL_CERT_FILE="$predictor_env/lib/python3.10/site-packages/certifi/cacert.pem"
20
+ export REQUESTS_CA_BUNDLE="$SSL_CERT_FILE"
21
+
22
+ mkdir -p "$suite_root" "$log_root"
23
+
24
+ "$python_bin" - <<'PY' | tee "$log_root/environment.log"
25
+ import importlib.util
26
+ import torch
27
+
28
+ assert torch.__version__ == "2.6.0+cu124", torch.__version__
29
+ assert torch.version.cuda == "12.4", torch.version.cuda
30
+ assert importlib.util.find_spec("flash_attn") is None, "flash_attn must not be installed"
31
+ print(f"torch={torch.__version__} cuda={torch.version.cuda} flash_attn=False")
32
+ PY
33
+
34
+ for mode in "${modes[@]}"; do
35
+ output_dir="$suite_root/$mode"
36
+ log_file="$log_root/${mode}.log"
37
+ complete_marker="$output_dir/training.complete"
38
+ if [[ -f "$complete_marker" && -f "$output_dir/predictor_step_02000.safetensors" ]]; then
39
+ echo "[skip] $mode already complete"
40
+ continue
41
+ fi
42
+ mkdir -p "$output_dir"
43
+ resume_args=()
44
+ if [[ -f "$output_dir/training_latest.pt" ]]; then
45
+ resume_args+=(--resume "$output_dir/training_latest.pt")
46
+ fi
47
+ echo "[start] mode=$mode $(date -u +%FT%TZ)" | tee -a "$log_file"
48
+ "$python_bin" -m torch.distributed.run \
49
+ --standalone --nproc_per_node=8 \
50
+ "$repo_root/train_predictor_v4.py" \
51
+ --manifest "$manifest" \
52
+ --teacher_checkpoint "$teacher" \
53
+ --source_blocks 1,52 \
54
+ --previous_condition_mode "$mode" \
55
+ --output_dir "$output_dir" \
56
+ --batch_size 16 \
57
+ --gradient_accumulation_steps 1 \
58
+ --max_steps 2000 \
59
+ --seed 0 \
60
+ --fusion_lr 1e-4 \
61
+ --blocks_lr 1e-5 \
62
+ --scheduler_warmup_steps 100 \
63
+ --block_warmup_steps 100 \
64
+ --block_warmup_mode delayed \
65
+ --fp32_trainable_params \
66
+ --gradient_checkpointing \
67
+ --bucket_by_context \
68
+ --save_every 100 \
69
+ --keep_snapshots 20 \
70
+ --log_every 10 \
71
+ --swanlab_mode cloud \
72
+ --swanlab_experiment "predictor-v4-stage1-previous-${mode}-seed0-blocks1-52-fp32master-8gpu-torch26-noflash" \
73
+ "${resume_args[@]}" \
74
+ 2>&1 | tee -a "$log_file"
75
+ test -f "$output_dir/predictor_step_02000.safetensors"
76
+ date -u +%FT%TZ >"$complete_marker"
77
+ aws s3 sync "$output_dir" "$s3_root/checkpoints/predictor_v4_stage1_previous_condition_ablation_seed0/$mode" --only-show-errors
78
+ aws s3 sync "$log_root" "$s3_root/logs/predictor_v4_stage1_previous_condition_ablation_seed0" --only-show-errors
79
+ echo "[complete] mode=$mode $(date -u +%FT%TZ)" | tee -a "$log_file"
80
+ done
81
+
82
+ date -u +%FT%TZ >"$suite_root/suite.complete"
83
+ aws s3 cp "$suite_root/suite.complete" "$s3_root/checkpoints/predictor_v4_stage1_previous_condition_ablation_seed0/suite.complete" --only-show-errors
84
+ echo "[complete] all modes $(date -u +%FT%TZ)"
tools/run_stage1_previous_condition_retrain_validation.sh ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -Eeuo pipefail
3
+
4
+ repo_root=/mnt/local_nvme/cz/projects/HY-WorldPlay-DEV-Predictor
5
+ predictor_env=/mnt/local_nvme/cz/envs/hy_worldplay_july20260720
6
+ python_bin="$predictor_env/bin/python"
7
+ base_model="$repo_root/outputs/hf_snapshot_repaired_9b49404"
8
+ checkpoint_root="$repo_root/checkpoints/predictor_v4_stage1_previous_condition_ablation_seed0"
9
+ run_tag=torch26-retrain_20260824
10
+ run_root="$repo_root/logs/stage1_previous_condition_validation_${run_tag}"
11
+ status_file="$run_root/status.tsv"
12
+
13
+ export PATH="$predictor_env/bin:$PATH"
14
+ export PYTHONPATH="$repo_root"
15
+ export PREDICTOR_PYTHON_BIN="$python_bin"
16
+ export PREDICTOR_BASE_MODEL="$base_model"
17
+ export PREDICTOR_SKIP_SYNC=0
18
+
19
+ mkdir -p "$run_root"
20
+ exec > >(tee -a "$run_root/sequential.log") 2>&1
21
+
22
+ record_status() {
23
+ printf '%s\t%s\t%s\n' "$1" "$(date -u +%FT%TZ)" "$2" >>"$status_file"
24
+ }
25
+
26
+ on_error() {
27
+ local exit_code="$?"
28
+ record_status failed "line=${BASH_LINENO[0]} exit=${exit_code}"
29
+ exit "$exit_code"
30
+ }
31
+ trap on_error ERR
32
+
33
+ "$python_bin" - <<'PY'
34
+ import importlib.util
35
+ import torch
36
+
37
+ assert torch.__version__ == "2.6.0+cu124", torch.__version__
38
+ assert torch.version.cuda == "12.4", torch.version.cuda
39
+ assert importlib.util.find_spec("flash_attn") is None, "flash_attn must not be installed"
40
+ print(f"torch={torch.__version__} cuda={torch.version.cuda} flash_attn=False")
41
+ PY
42
+ test -d "$base_model"
43
+ record_status preflight "torch=2.6.0+cu124 cuda=12.4 flash_attn=False"
44
+
45
+ run_eval() {
46
+ local label="$1"
47
+ local mode="$2"
48
+ local checkpoint="$checkpoint_root/$mode/predictor_step_02000.safetensors"
49
+ local eval_name="eval_validation25_previous-condition-${label}_${run_tag}"
50
+ test -f "$checkpoint"
51
+ record_status start "$eval_name"
52
+ bash "$repo_root/tools/launch_vbench_validation_predictor.sh" \
53
+ "$eval_name" "$checkpoint" 1,52 v4 1,2 "$mode" concat
54
+ record_status complete "$eval_name"
55
+ }
56
+
57
+ run_eval none none
58
+ run_eval previous-step previous_step
59
+ run_eval both both
60
+
61
+ record_status complete all
62
+ date -u +%FT%TZ >"$run_root/all.completed"
63
+ aws s3 sync "$run_root" \
64
+ "s3://s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/logs/stage1_previous_condition_validation_${run_tag}" \
65
+ --only-show-errors
66
+ echo "[complete] all previous-condition validation evaluations"
tools/run_vbench_atc_validation25.sh ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ vbench_repo="${VBENCH_REPO:-/mnt/local_nvme/cz/projects/VBench}"
6
+ python_bin="${PREDICTOR_PYTHON_BIN:-/mnt/local_nvme/cz/envs/hy_worldplay_july20260720/bin/python}"
7
+ vbench_site_packages="${VBENCH_SITE_PACKAGES:-/mnt/local_nvme/cz/envs/vbench_eval/lib/python3.10/site-packages}"
8
+ dataset_name="hyworldplay_predictor_vbench_val25_test50"
9
+ dataset_root="${PREDICTOR_HOLDOUT_NVME_ROOT:-${repo_root}/datasets/${dataset_name}}"
10
+ run_name="validation25_atc_ablation_oldenv"
11
+ run_root="${dataset_root}/vbench/${run_name}"
12
+ staging_root="${dataset_root}/vbench_staging/${run_name}"
13
+ durable_uri="s3://s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/datasets/${dataset_name}/vbench/${run_name}"
14
+ overlay_root="${run_root}/site_overlay"
15
+ self_forcing_site_packages="${SELF_FORCING_SITE_PACKAGES:-/mnt/local_nvme/cz/envs/self_forcing/lib/python3.10/site-packages}"
16
+
17
+ mkdir -p "${overlay_root}"
18
+ for package in easydict timm open_clip; do
19
+ [[ -d "${self_forcing_site_packages}/${package}" ]]
20
+ ln -sfn "${self_forcing_site_packages}/${package}" "${overlay_root}/${package}"
21
+ done
22
+
23
+ export PYTHONPATH="${overlay_root}:${vbench_site_packages}:${vbench_repo}:${repo_root}"
24
+ export VBENCH_CACHE_DIR="${VBENCH_CACHE_DIR:-/mnt/local_nvme/cz/.cache/vbench}"
25
+ export TORCH_HOME="${TORCH_HOME:-/mnt/local_nvme/cz/.cache/torch}"
26
+ export HF_HOME="${HF_HOME:-/mnt/local_nvme/cz/.cache/huggingface}"
27
+ export OMP_NUM_THREADS="${OMP_NUM_THREADS:-4}"
28
+
29
+ [[ -x "${python_bin}" ]]
30
+ [[ -f "${vbench_repo}/evaluate.py" ]]
31
+ [[ -f "${dataset_root}/validation/cases.jsonl" ]]
32
+ mkdir -p "${run_root}/raw" "${run_root}/logs" "${staging_root}"
33
+
34
+ groups=()
35
+ group_names=()
36
+ for variant in full anchor_only raw_prev no_gate no_distance; do
37
+ for schedule in fppf fppp; do
38
+ group="atc_${variant}_${schedule}"
39
+ eval_name="eval_validation25_atc-${variant}_schedule-${schedule}_blocks1-52_step2000_oldenv"
40
+ groups+=("${group}|${dataset_root}/evaluations/${eval_name}/predictor")
41
+ group_names+=("${group}")
42
+ done
43
+ done
44
+
45
+ find "${staging_root}" -maxdepth 1 -type l -delete
46
+ for spec in "${groups[@]}"; do
47
+ IFS='|' read -r label source_dir <<<"${spec}"
48
+ video_count=$(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | wc -l)
49
+ if [[ "${video_count}" -ne 100 ]]; then
50
+ echo "${label}: expected 100 videos, found ${video_count}: ${source_dir}" >&2
51
+ exit 1
52
+ fi
53
+ while IFS= read -r video_path; do
54
+ ln -s "${video_path}" "${staging_root}/${label}__$(basename "${video_path}")"
55
+ done < <(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | sort)
56
+ done
57
+
58
+ expected_records=1000
59
+ staged_count=$(find "${staging_root}" -maxdepth 1 -type l | wc -l)
60
+ if [[ "${staged_count}" -ne "${expected_records}" ]]; then
61
+ echo "Expected ${expected_records} staged videos, found ${staged_count}" >&2
62
+ exit 1
63
+ fi
64
+
65
+ prompt_file="${run_root}/validation25_prompt_map.json"
66
+ "${python_bin}" "${repo_root}/tools/build_vbench_validation25_prompt_map.py" \
67
+ --cases_jsonl "${dataset_root}/validation/cases.jsonl" \
68
+ --staging_root "${staging_root}" \
69
+ --output "${prompt_file}" \
70
+ --expected_count "${expected_records}"
71
+
72
+ dimensions=(
73
+ subject_consistency
74
+ background_consistency
75
+ temporal_flickering
76
+ motion_smoothness
77
+ dynamic_degree
78
+ aesthetic_quality
79
+ imaging_quality
80
+ overall_consistency
81
+ )
82
+
83
+ for dimension in "${dimensions[@]}"; do
84
+ output_dir="${run_root}/raw/${dimension}"
85
+ marker="${run_root}/${dimension}.completed"
86
+ mkdir -p "${output_dir}"
87
+ result_count=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json' | wc -l)
88
+ if [[ -f "${marker}" && "${result_count}" -eq 1 ]]; then
89
+ echo "[skip] ${dimension} already complete"
90
+ continue
91
+ fi
92
+ find "${output_dir}" -maxdepth 1 -type f -delete
93
+ echo "[vbench] ${dimension} start $(date -u +%FT%TZ)"
94
+ "${python_bin}" -m torch.distributed.run \
95
+ --standalone \
96
+ --nproc_per_node=8 \
97
+ "${vbench_repo}/evaluate.py" \
98
+ --videos_path "${staging_root}" \
99
+ --dimension "${dimension}" \
100
+ --mode custom_input \
101
+ --prompt_file "${prompt_file}" \
102
+ --load_ckpt_from_local True \
103
+ --imaging_quality_preprocessing_mode longer \
104
+ --output_path "${output_dir}" \
105
+ >"${run_root}/logs/${dimension}.log" 2>&1
106
+ result_json=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json')
107
+ "${python_bin}" - "${result_json}" "${dimension}" "${expected_records}" <<'PY'
108
+ import json
109
+ import sys
110
+
111
+ path, dimension, expected_records = sys.argv[1:]
112
+ payload = json.load(open(path))
113
+ records = payload[dimension][1]
114
+ assert len(records) == int(expected_records), (dimension, len(records))
115
+ print(dimension, len(records), payload[dimension][0])
116
+ PY
117
+ date -u +%FT%TZ >"${marker}"
118
+ if [[ "${PREDICTOR_SKIP_SYNC:-0}" != "1" ]]; then
119
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
120
+ fi
121
+ echo "[vbench] ${dimension} complete $(date -u +%FT%TZ)"
122
+ done
123
+
124
+ summary_args=()
125
+ for group in "${group_names[@]}"; do
126
+ summary_args+=(--group "${group}")
127
+ done
128
+ "${python_bin}" "${repo_root}/tools/summarize_vbench_validation25_pair.py" \
129
+ --run_root "${run_root}" \
130
+ "${summary_args[@]}" \
131
+ >"${run_root}/logs/summary.log" 2>&1
132
+ date -u +%FT%TZ >"${run_root}/pipeline.completed"
133
+ if [[ "${PREDICTOR_SKIP_SYNC:-0}" != "1" ]]; then
134
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
135
+ fi
136
+ echo "[vbench] all ATC dimensions complete: ${run_root}"
tools/run_vbench_c1_threshold_validation25.sh ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ vbench_repo="${VBENCH_REPO:-/mnt/local_nvme/cz/projects/VBench}"
6
+ python_bin="${PREDICTOR_PYTHON_BIN:-/mnt/local_nvme/cz/envs/self_forcing/bin/python3.10}"
7
+ vbench_site_packages="${VBENCH_SITE_PACKAGES:-/mnt/local_nvme/cz/envs/vbench_eval/lib/python3.10/site-packages}"
8
+ dataset_root="${repo_root}/datasets/hyworldplay_predictor_vbench_val25_test50"
9
+ run_root="${dataset_root}/vbench/validation25_c1_thresholds"
10
+ staging_root="${dataset_root}/vbench_staging/validation25_c1_thresholds"
11
+
12
+ export PYTHONPATH="${vbench_site_packages}:${vbench_repo}:${repo_root}"
13
+ export VBENCH_CACHE_DIR="${VBENCH_CACHE_DIR:-/mnt/local_nvme/cz/.cache/vbench}"
14
+ export TORCH_HOME="${TORCH_HOME:-/mnt/local_nvme/cz/.cache/torch}"
15
+ export HF_HOME="${HF_HOME:-/mnt/local_nvme/cz/.cache/huggingface}"
16
+
17
+ mkdir -p "${run_root}/raw" "${run_root}/logs" "${staging_root}"
18
+
19
+ groups=(
20
+ "c1_r10|${dataset_root}/evaluations/eval_validation25_stage1-c1-threshold-r10_blocks1-52_fp32master_step2000/predictor"
21
+ "c1_r25|${dataset_root}/evaluations/eval_validation25_stage1-c1-threshold-r25_blocks1-52_fp32master_step2000/predictor"
22
+ "c1_r33|${dataset_root}/evaluations/eval_validation25_stage1-c1-threshold-r33_blocks1-52_fp32master_step2000/predictor"
23
+ "c1_r50|${dataset_root}/evaluations/eval_validation25_stage1-c1-threshold-r50_blocks1-52_fp32master_step2000/predictor"
24
+ )
25
+
26
+ find "${staging_root}" -maxdepth 1 -type l -delete
27
+ for spec in "${groups[@]}"; do
28
+ IFS='|' read -r label source_dir <<<"${spec}"
29
+ [[ $(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | wc -l) -eq 100 ]]
30
+ while IFS= read -r video_path; do
31
+ ln -s "${video_path}" "${staging_root}/${label}__$(basename "${video_path}")"
32
+ done < <(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | sort)
33
+ done
34
+ [[ $(find "${staging_root}" -maxdepth 1 -type l | wc -l) -eq 400 ]]
35
+
36
+ prompt_file="${run_root}/validation25_prompt_map.json"
37
+ "${python_bin}" "${repo_root}/tools/build_vbench_validation25_prompt_map.py" \
38
+ --cases_jsonl "${dataset_root}/validation/cases.jsonl" \
39
+ --staging_root "${staging_root}" \
40
+ --output "${prompt_file}" \
41
+ --expected_count 400
42
+
43
+ dimensions=(
44
+ subject_consistency
45
+ background_consistency
46
+ temporal_flickering
47
+ motion_smoothness
48
+ dynamic_degree
49
+ aesthetic_quality
50
+ imaging_quality
51
+ overall_consistency
52
+ )
53
+ for dimension in "${dimensions[@]}"; do
54
+ output_dir="${run_root}/raw/${dimension}"
55
+ marker="${run_root}/${dimension}.completed"
56
+ mkdir -p "${output_dir}"
57
+ if [[ -f "${marker}" ]] \
58
+ && [[ $(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json' | wc -l) -eq 1 ]]; then
59
+ echo "[skip] ${dimension}"
60
+ continue
61
+ fi
62
+ find "${output_dir}" -maxdepth 1 -type f -delete
63
+ echo "[vbench] ${dimension} start $(date -u +%FT%TZ)"
64
+ "${python_bin}" -m torch.distributed.run \
65
+ --standalone \
66
+ --nproc_per_node=8 \
67
+ "${vbench_repo}/evaluate.py" \
68
+ --videos_path "${staging_root}" \
69
+ --dimension "${dimension}" \
70
+ --mode custom_input \
71
+ --prompt_file "${prompt_file}" \
72
+ --load_ckpt_from_local True \
73
+ --imaging_quality_preprocessing_mode longer \
74
+ --output_path "${output_dir}" \
75
+ >"${run_root}/logs/${dimension}.log" 2>&1
76
+ result_json=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json')
77
+ "${python_bin}" - "${result_json}" "${dimension}" <<'PY'
78
+ import json, sys
79
+ path, dimension = sys.argv[1:]
80
+ records = json.load(open(path))[dimension][1]
81
+ assert len(records) == 400, (dimension, len(records))
82
+ print(dimension, len(records))
83
+ PY
84
+ date -u +%FT%TZ >"${marker}"
85
+ done
86
+
87
+ "${python_bin}" "${repo_root}/tools/summarize_vbench_validation25_pair.py" \
88
+ --run_root "${run_root}" \
89
+ --group c1_r10 --group c1_r25 --group c1_r33 --group c1_r50 \
90
+ >"${run_root}/logs/summary.log" 2>&1
91
+ date -u +%FT%TZ >"${run_root}/pipeline.completed"
92
+ echo "[vbench] all C1 threshold dimensions complete: ${run_root}"
tools/run_vbench_previous_condition_none_step0300_validation25.sh ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ vbench_repo="${VBENCH_REPO:-/mnt/local_nvme/cz/projects/VBench}"
6
+ python_bin="${PREDICTOR_PYTHON_BIN:-/mnt/local_nvme/cz/envs/hy_worldplay_july20260720/bin/python}"
7
+ vbench_site_packages="${VBENCH_SITE_PACKAGES:-/mnt/local_nvme/cz/envs/vbench_eval/lib/python3.10/site-packages}"
8
+ self_forcing_site_packages="${SELF_FORCING_SITE_PACKAGES:-/mnt/local_nvme/cz/envs/self_forcing/lib/python3.10/site-packages}"
9
+ dataset_name="hyworldplay_predictor_vbench_val25_test50"
10
+ dataset_root="${PREDICTOR_HOLDOUT_NVME_ROOT:-${repo_root}/datasets/${dataset_name}}"
11
+ run_name="validation25_previous_condition_none_step0300_torch26_20260825"
12
+ run_root="${dataset_root}/vbench/${run_name}"
13
+ staging_root="${dataset_root}/vbench_staging/${run_name}"
14
+ source_dir="${dataset_root}/evaluations/eval_validation25_previous-condition-none-step0300_force-first-chunk-ffff_torch26_20260824/predictor"
15
+ group="none_step0300_first_chunk_ffff"
16
+ durable_uri="s3://s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/datasets/${dataset_name}/vbench/${run_name}"
17
+ overlay_root="${run_root}/site_overlay"
18
+
19
+ mkdir -p "${overlay_root}"
20
+ for package in easydict timm open_clip; do
21
+ [[ -d "${self_forcing_site_packages}/${package}" ]]
22
+ ln -sfn "${self_forcing_site_packages}/${package}" "${overlay_root}/${package}"
23
+ done
24
+
25
+ export PYTHONPATH="${overlay_root}:${vbench_site_packages}:${vbench_repo}:${repo_root}"
26
+ export VBENCH_CACHE_DIR="${VBENCH_CACHE_DIR:-/mnt/local_nvme/cz/.cache/vbench}"
27
+ export TORCH_HOME="${TORCH_HOME:-/mnt/local_nvme/cz/.cache/torch}"
28
+ export HF_HOME="${HF_HOME:-/mnt/local_nvme/cz/.cache/huggingface}"
29
+ export OMP_NUM_THREADS="${OMP_NUM_THREADS:-4}"
30
+
31
+ [[ -x "${python_bin}" ]]
32
+ [[ -f "${vbench_repo}/evaluate.py" ]]
33
+ [[ -f "${dataset_root}/validation/cases.jsonl" ]]
34
+ [[ $(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | wc -l) -eq 100 ]]
35
+ mkdir -p "${run_root}/raw" "${run_root}/logs" "${staging_root}"
36
+
37
+ find "${staging_root}" -maxdepth 1 -type l -delete
38
+ while IFS= read -r video_path; do
39
+ ln -s "${video_path}" "${staging_root}/${group}__$(basename "${video_path}")"
40
+ done < <(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | sort)
41
+ [[ $(find "${staging_root}" -maxdepth 1 -type l | wc -l) -eq 100 ]]
42
+
43
+ prompt_file="${run_root}/validation25_prompt_map.json"
44
+ "${python_bin}" "${repo_root}/tools/build_vbench_validation25_prompt_map.py" \
45
+ --cases_jsonl "${dataset_root}/validation/cases.jsonl" \
46
+ --staging_root "${staging_root}" \
47
+ --output "${prompt_file}" \
48
+ --expected_count 100
49
+
50
+ dimensions=(
51
+ subject_consistency
52
+ background_consistency
53
+ temporal_flickering
54
+ motion_smoothness
55
+ dynamic_degree
56
+ aesthetic_quality
57
+ imaging_quality
58
+ overall_consistency
59
+ )
60
+
61
+ for dimension in "${dimensions[@]}"; do
62
+ output_dir="${run_root}/raw/${dimension}"
63
+ marker="${run_root}/${dimension}.completed"
64
+ mkdir -p "${output_dir}"
65
+ result_count=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json' | wc -l)
66
+ if [[ -f "${marker}" && "${result_count}" -eq 1 ]]; then
67
+ echo "[skip] ${dimension} already complete"
68
+ continue
69
+ fi
70
+ find "${output_dir}" -maxdepth 1 -type f -delete
71
+ echo "[vbench] ${dimension} start $(date -u +%FT%TZ)"
72
+ "${python_bin}" -m torch.distributed.run \
73
+ --standalone \
74
+ --nproc_per_node=8 \
75
+ "${vbench_repo}/evaluate.py" \
76
+ --videos_path "${staging_root}" \
77
+ --dimension "${dimension}" \
78
+ --mode custom_input \
79
+ --prompt_file "${prompt_file}" \
80
+ --load_ckpt_from_local True \
81
+ --imaging_quality_preprocessing_mode longer \
82
+ --output_path "${output_dir}" \
83
+ >"${run_root}/logs/${dimension}.log" 2>&1
84
+ result_json=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json')
85
+ "${python_bin}" - "${result_json}" "${dimension}" <<'PY'
86
+ import json
87
+ import sys
88
+
89
+ path, dimension = sys.argv[1:]
90
+ payload = json.load(open(path))
91
+ records = payload[dimension][1]
92
+ assert len(records) == 100, (dimension, len(records))
93
+ print(dimension, len(records), payload[dimension][0])
94
+ PY
95
+ date -u +%FT%TZ >"${marker}"
96
+ if [[ "${PREDICTOR_SKIP_SYNC:-0}" != "1" ]]; then
97
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
98
+ fi
99
+ echo "[vbench] ${dimension} complete $(date -u +%FT%TZ)"
100
+ done
101
+
102
+ "${python_bin}" "${repo_root}/tools/summarize_vbench_validation25_pair.py" \
103
+ --run_root "${run_root}" \
104
+ --group "${group}" \
105
+ >"${run_root}/logs/summary.log" 2>&1
106
+ date -u +%FT%TZ >"${run_root}/pipeline.completed"
107
+ if [[ "${PREDICTOR_SKIP_SYNC:-0}" != "1" ]]; then
108
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
109
+ fi
110
+ echo "[vbench] none step0300 complete: ${run_root}"
tools/run_vbench_previous_condition_validation25.sh ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ vbench_repo="${VBENCH_REPO:-/mnt/local_nvme/cz/projects/VBench}"
6
+ python_bin="${PREDICTOR_PYTHON_BIN:-/mnt/local_nvme/cz/envs/hy_worldplay_july20260720/bin/python}"
7
+ vbench_site_packages="${VBENCH_SITE_PACKAGES:-/mnt/local_nvme/cz/envs/vbench_eval/lib/python3.10/site-packages}"
8
+ self_forcing_site_packages="${SELF_FORCING_SITE_PACKAGES:-/mnt/local_nvme/cz/envs/self_forcing/lib/python3.10/site-packages}"
9
+ dataset_name="hyworldplay_predictor_vbench_val25_test50"
10
+ dataset_root="${PREDICTOR_HOLDOUT_NVME_ROOT:-${repo_root}/datasets/${dataset_name}}"
11
+ run_name="validation25_previous_condition_torch26_20260824"
12
+ run_root="${dataset_root}/vbench/${run_name}"
13
+ staging_root="${dataset_root}/vbench_staging/${run_name}"
14
+ durable_uri="s3://s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/datasets/${dataset_name}/vbench/${run_name}"
15
+ overlay_root="${run_root}/site_overlay"
16
+
17
+ mkdir -p "${overlay_root}"
18
+ for package in easydict timm open_clip; do
19
+ [[ -d "${self_forcing_site_packages}/${package}" ]]
20
+ ln -sfn "${self_forcing_site_packages}/${package}" "${overlay_root}/${package}"
21
+ done
22
+
23
+ export PYTHONPATH="${overlay_root}:${vbench_site_packages}:${vbench_repo}:${repo_root}"
24
+ export VBENCH_CACHE_DIR="${VBENCH_CACHE_DIR:-/mnt/local_nvme/cz/.cache/vbench}"
25
+ export TORCH_HOME="${TORCH_HOME:-/mnt/local_nvme/cz/.cache/torch}"
26
+ export HF_HOME="${HF_HOME:-/mnt/local_nvme/cz/.cache/huggingface}"
27
+ export OMP_NUM_THREADS="${OMP_NUM_THREADS:-4}"
28
+
29
+ [[ -x "${python_bin}" ]]
30
+ [[ -f "${vbench_repo}/evaluate.py" ]]
31
+ [[ -f "${dataset_root}/validation/cases.jsonl" ]]
32
+ mkdir -p "${run_root}/raw" "${run_root}/logs" "${staging_root}"
33
+
34
+ groups=(
35
+ "same_step_step2000|${dataset_root}/evaluations/eval_validation25_v4_blocks1-52_fp32master_ckptstep2000_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100/predictor"
36
+ "previous_step_step2000|${dataset_root}/evaluations/eval_validation25_previous-condition-previous-step_torch26-retrain_20260824/predictor"
37
+ "both_step2000|${dataset_root}/evaluations/eval_validation25_previous-condition-both_torch26-retrain_20260824/predictor"
38
+ "none_step2000_first_chunk_fppf|${dataset_root}/evaluations/eval_validation25_previous-condition-none_torch26-retrain_20260824/predictor"
39
+ "none_step0500_first_chunk_ffff|${dataset_root}/evaluations/eval_validation25_previous-condition-none-step0500_force-first-chunk-ffff_torch26_20260824/predictor"
40
+ "none_step1000_first_chunk_ffff|${dataset_root}/evaluations/eval_validation25_previous-condition-none-step1000_force-first-chunk-ffff_torch26_20260824/predictor"
41
+ "none_step2000_first_chunk_ffff|${dataset_root}/evaluations/eval_validation25_previous-condition-none_force-first-chunk-ffff_torch26_20260824/predictor"
42
+ )
43
+ group_names=()
44
+
45
+ find "${staging_root}" -maxdepth 1 -type l -delete
46
+ for spec in "${groups[@]}"; do
47
+ IFS='|' read -r label source_dir <<<"${spec}"
48
+ group_names+=("${label}")
49
+ video_count=$(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | wc -l)
50
+ if [[ "${video_count}" -ne 100 ]]; then
51
+ echo "${label}: expected 100 videos, found ${video_count}: ${source_dir}" >&2
52
+ exit 1
53
+ fi
54
+ while IFS= read -r video_path; do
55
+ ln -s "${video_path}" "${staging_root}/${label}__$(basename "${video_path}")"
56
+ done < <(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | sort)
57
+ done
58
+
59
+ expected_records=700
60
+ staged_count=$(find "${staging_root}" -maxdepth 1 -type l | wc -l)
61
+ if [[ "${staged_count}" -ne "${expected_records}" ]]; then
62
+ echo "Expected ${expected_records} staged videos, found ${staged_count}" >&2
63
+ exit 1
64
+ fi
65
+
66
+ prompt_file="${run_root}/validation25_prompt_map.json"
67
+ "${python_bin}" "${repo_root}/tools/build_vbench_validation25_prompt_map.py" \
68
+ --cases_jsonl "${dataset_root}/validation/cases.jsonl" \
69
+ --staging_root "${staging_root}" \
70
+ --output "${prompt_file}" \
71
+ --expected_count "${expected_records}"
72
+
73
+ dimensions=(
74
+ subject_consistency
75
+ background_consistency
76
+ temporal_flickering
77
+ motion_smoothness
78
+ dynamic_degree
79
+ aesthetic_quality
80
+ imaging_quality
81
+ overall_consistency
82
+ )
83
+
84
+ for dimension in "${dimensions[@]}"; do
85
+ output_dir="${run_root}/raw/${dimension}"
86
+ marker="${run_root}/${dimension}.completed"
87
+ mkdir -p "${output_dir}"
88
+ result_count=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json' | wc -l)
89
+ if [[ -f "${marker}" && "${result_count}" -eq 1 ]]; then
90
+ echo "[skip] ${dimension} already complete"
91
+ continue
92
+ fi
93
+ find "${output_dir}" -maxdepth 1 -type f -delete
94
+ echo "[vbench] ${dimension} start $(date -u +%FT%TZ)"
95
+ "${python_bin}" -m torch.distributed.run \
96
+ --standalone \
97
+ --nproc_per_node=8 \
98
+ "${vbench_repo}/evaluate.py" \
99
+ --videos_path "${staging_root}" \
100
+ --dimension "${dimension}" \
101
+ --mode custom_input \
102
+ --prompt_file "${prompt_file}" \
103
+ --load_ckpt_from_local True \
104
+ --imaging_quality_preprocessing_mode longer \
105
+ --output_path "${output_dir}" \
106
+ >"${run_root}/logs/${dimension}.log" 2>&1
107
+ result_json=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json')
108
+ "${python_bin}" - "${result_json}" "${dimension}" "${expected_records}" <<'PY'
109
+ import json
110
+ import sys
111
+
112
+ path, dimension, expected_records = sys.argv[1:]
113
+ payload = json.load(open(path))
114
+ records = payload[dimension][1]
115
+ assert len(records) == int(expected_records), (dimension, len(records))
116
+ print(dimension, len(records), payload[dimension][0])
117
+ PY
118
+ date -u +%FT%TZ >"${marker}"
119
+ if [[ "${PREDICTOR_SKIP_SYNC:-0}" != "1" ]]; then
120
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
121
+ fi
122
+ echo "[vbench] ${dimension} complete $(date -u +%FT%TZ)"
123
+ done
124
+
125
+ summary_args=()
126
+ for group in "${group_names[@]}"; do
127
+ summary_args+=(--group "${group}")
128
+ done
129
+ "${python_bin}" "${repo_root}/tools/summarize_vbench_validation25_pair.py" \
130
+ --run_root "${run_root}" \
131
+ "${summary_args[@]}" \
132
+ >"${run_root}/logs/summary.log" 2>&1
133
+ date -u +%FT%TZ >"${run_root}/pipeline.completed"
134
+ if [[ "${PREDICTOR_SKIP_SYNC:-0}" != "1" ]]; then
135
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
136
+ fi
137
+ echo "[vbench] all previous-condition dimensions complete: ${run_root}"
tools/run_vbench_token_gate_validation25.sh ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ vbench_repo="${VBENCH_REPO:-/mnt/local_nvme/cz/projects/VBench}"
6
+ python_bin="${PREDICTOR_PYTHON_BIN:-/mnt/local_nvme/cz/envs/self_forcing/bin/python3.10}"
7
+ vbench_site_packages="${VBENCH_SITE_PACKAGES:-/mnt/local_nvme/cz/envs/vbench_eval/lib/python3.10/site-packages}"
8
+ dataset_root="${repo_root}/datasets/hyworldplay_predictor_vbench_val25_test50"
9
+ source_dir="${dataset_root}/evaluations/eval_validation25_stage1-token-gate-prev_schedule-fppf_blocks1-52_ckptstep2000_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100/predictor"
10
+ run_root="${dataset_root}/vbench/validation25_token_gate_prev_fppf"
11
+ staging_root="${dataset_root}/vbench_staging/validation25_token_gate_prev_fppf"
12
+ group=token_gate_prev_fppf
13
+
14
+ export PYTHONPATH="${vbench_site_packages}:${vbench_repo}:${repo_root}"
15
+ export VBENCH_CACHE_DIR="${VBENCH_CACHE_DIR:-/mnt/local_nvme/cz/.cache/vbench}"
16
+ export TORCH_HOME="${TORCH_HOME:-/mnt/local_nvme/cz/.cache/torch}"
17
+ export HF_HOME="${HF_HOME:-/mnt/local_nvme/cz/.cache/huggingface}"
18
+
19
+ mkdir -p "${run_root}/raw" "${run_root}/logs" "${staging_root}"
20
+ find "${staging_root}" -maxdepth 1 -type l -delete
21
+ [[ $(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | wc -l) -eq 100 ]]
22
+ while IFS= read -r video_path; do
23
+ ln -s "${video_path}" "${staging_root}/${group}__$(basename "${video_path}")"
24
+ done < <(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | sort)
25
+ [[ $(find "${staging_root}" -maxdepth 1 -type l | wc -l) -eq 100 ]]
26
+
27
+ prompt_file="${run_root}/validation25_prompt_map.json"
28
+ "${python_bin}" "${repo_root}/tools/build_vbench_validation25_prompt_map.py" \
29
+ --cases_jsonl "${dataset_root}/validation/cases.jsonl" \
30
+ --staging_root "${staging_root}" \
31
+ --output "${prompt_file}" \
32
+ --expected_count 100
33
+
34
+ dimensions=(
35
+ subject_consistency
36
+ background_consistency
37
+ temporal_flickering
38
+ motion_smoothness
39
+ dynamic_degree
40
+ aesthetic_quality
41
+ imaging_quality
42
+ overall_consistency
43
+ )
44
+ for dimension in "${dimensions[@]}"; do
45
+ output_dir="${run_root}/raw/${dimension}"
46
+ marker="${run_root}/${dimension}.completed"
47
+ mkdir -p "${output_dir}"
48
+ if [[ -f "${marker}" ]] \
49
+ && [[ $(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json' | wc -l) -eq 1 ]]; then
50
+ echo "[skip] ${dimension}"
51
+ continue
52
+ fi
53
+ find "${output_dir}" -maxdepth 1 -type f -delete
54
+ echo "[vbench] ${dimension} start $(date -u +%FT%TZ)"
55
+ "${python_bin}" -m torch.distributed.run \
56
+ --standalone \
57
+ --nproc_per_node=8 \
58
+ "${vbench_repo}/evaluate.py" \
59
+ --videos_path "${staging_root}" \
60
+ --dimension "${dimension}" \
61
+ --mode custom_input \
62
+ --prompt_file "${prompt_file}" \
63
+ --load_ckpt_from_local True \
64
+ --imaging_quality_preprocessing_mode longer \
65
+ --output_path "${output_dir}" \
66
+ >"${run_root}/logs/${dimension}.log" 2>&1
67
+ result_json=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json')
68
+ "${python_bin}" - "${result_json}" "${dimension}" <<'PY'
69
+ import json, sys
70
+ path, dimension = sys.argv[1:]
71
+ records = json.load(open(path))[dimension][1]
72
+ assert len(records) == 100, (dimension, len(records))
73
+ print(dimension, len(records))
74
+ PY
75
+ date -u +%FT%TZ >"${marker}"
76
+ done
77
+
78
+ "${python_bin}" "${repo_root}/tools/summarize_vbench_validation25_pair.py" \
79
+ --run_root "${run_root}" \
80
+ --group "${group}" \
81
+ >"${run_root}/logs/summary.log" 2>&1
82
+ date -u +%FT%TZ >"${run_root}/pipeline.completed"
83
+ echo "[vbench] token-gate validation complete: ${run_root}"
tools/run_vbench_validation25_chunk_timestep_step2000.sh ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ vbench_repo="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/VBench"
6
+ dataset_name="hyworldplay_predictor_vbench_val25_test50"
7
+ dataset_root="${PREDICTOR_HOLDOUT_NVME_ROOT:-/mnt/local_nvme/zoubin/cz/${dataset_name}}"
8
+ run_name="validation25_chunk_timestep_step2000"
9
+ run_root="${dataset_root}/vbench/${run_name}"
10
+ staging_root="${dataset_root}/vbench_staging/${run_name}"
11
+ python_bin="/mnt/s3files/s3-us-west2-default/zoubin/cz/envs/hy_worldplay/bin/python"
12
+ durable_uri="s3://s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/datasets/${dataset_name}/vbench/${run_name}"
13
+
14
+ export PYTHONPATH="${vbench_repo}:${repo_root}"
15
+ export VBENCH_CACHE_DIR="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/vbench"
16
+ export TORCH_HOME="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/torch"
17
+ export HF_HOME="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/huggingface"
18
+
19
+ mkdir -p "${run_root}/raw" "${run_root}/logs" "${staging_root}"
20
+
21
+ declare -a groups=(
22
+ "chunk_timestep_step2000_fppf|${dataset_root}/evaluations/eval_validation25_detached-chunk-timestep-rollout-onpolicy-fppp-fulltrain100_schedule-fppf_blocks1-52_ckptstep2000_flr5e-5_blr5e-6_schedsteps2000_swarmup100_bwarmup100linear_bs1/predictor"
23
+ "chunk_timestep_step2000_fppp|${dataset_root}/evaluations/eval_validation25_detached-chunk-timestep-rollout-onpolicy-fppp-fulltrain100_schedule-fppp_blocks1-52_ckptstep2000_flr5e-5_blr5e-6_schedsteps2000_swarmup100_bwarmup100linear_bs1/predictor"
24
+ )
25
+
26
+ find "${staging_root}" -maxdepth 1 -type l -delete
27
+ for spec in "${groups[@]}"; do
28
+ IFS='|' read -r label source_dir <<<"${spec}"
29
+ video_count=$(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | wc -l)
30
+ if [[ "${video_count}" -ne 100 ]]; then
31
+ echo "${label}: expected 100 videos, found ${video_count}: ${source_dir}" >&2
32
+ exit 1
33
+ fi
34
+ while IFS= read -r video_path; do
35
+ ln -s "${video_path}" "${staging_root}/${label}__$(basename "${video_path}")"
36
+ done < <(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | sort)
37
+ done
38
+
39
+ staged_count=$(find "${staging_root}" -maxdepth 1 -type l | wc -l)
40
+ if [[ "${staged_count}" -ne 200 ]]; then
41
+ echo "Expected 200 staged videos, found ${staged_count}" >&2
42
+ exit 1
43
+ fi
44
+
45
+ prompt_file="${run_root}/validation25_prompt_map.json"
46
+ "${python_bin}" "${repo_root}/tools/build_vbench_validation25_prompt_map.py" \
47
+ --cases_jsonl "${dataset_root}/validation/cases.jsonl" \
48
+ --staging_root "${staging_root}" \
49
+ --output "${prompt_file}" \
50
+ --expected_count 200
51
+
52
+ dimensions=(
53
+ subject_consistency
54
+ background_consistency
55
+ temporal_flickering
56
+ motion_smoothness
57
+ dynamic_degree
58
+ aesthetic_quality
59
+ imaging_quality
60
+ overall_consistency
61
+ )
62
+
63
+ for dimension in "${dimensions[@]}"; do
64
+ output_dir="${run_root}/raw/${dimension}"
65
+ marker="${run_root}/${dimension}.completed"
66
+ mkdir -p "${output_dir}"
67
+ result_count=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json' | wc -l)
68
+ if [[ -f "${marker}" && "${result_count}" -eq 1 ]]; then
69
+ echo "[skip] ${dimension} already complete"
70
+ continue
71
+ fi
72
+ find "${output_dir}" -maxdepth 1 -type f -delete
73
+ echo "[vbench] ${dimension} start $(date -u +%FT%TZ)"
74
+ "${python_bin}" -m torch.distributed.run \
75
+ --standalone \
76
+ --nproc_per_node=8 \
77
+ "${vbench_repo}/evaluate.py" \
78
+ --videos_path "${staging_root}" \
79
+ --dimension "${dimension}" \
80
+ --mode custom_input \
81
+ --prompt_file "${prompt_file}" \
82
+ --load_ckpt_from_local True \
83
+ --imaging_quality_preprocessing_mode longer \
84
+ --output_path "${output_dir}" \
85
+ >"${run_root}/logs/${dimension}.log" 2>&1
86
+ result_json=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json')
87
+ "${python_bin}" - "${result_json}" "${dimension}" <<'PY'
88
+ import json
89
+ import sys
90
+
91
+ path, dimension = sys.argv[1:]
92
+ payload = json.load(open(path))
93
+ records = payload[dimension][1]
94
+ assert len(records) == 200, (dimension, len(records))
95
+ print(dimension, len(records), payload[dimension][0])
96
+ PY
97
+ date -u +%FT%TZ >"${marker}"
98
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
99
+ echo "[vbench] ${dimension} complete $(date -u +%FT%TZ)"
100
+ done
101
+
102
+ "${python_bin}" \
103
+ "${repo_root}/tools/summarize_vbench_validation25_chunk_timestep_step2000.py" \
104
+ --run_root "${run_root}" \
105
+ >"${run_root}/logs/summary.log" 2>&1
106
+ date -u +%FT%TZ >"${run_root}/pipeline.completed"
107
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
108
+ echo "[vbench] all dimensions complete: ${run_root}"
tools/run_vbench_validation25_core.sh ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ vbench_repo="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/VBench"
6
+ dataset_name="hyworldplay_predictor_vbench_val25_test50"
7
+ dataset_root="${PREDICTOR_HOLDOUT_NVME_ROOT:-/mnt/local_nvme/zoubin/cz/${dataset_name}}"
8
+ run_root="${dataset_root}/vbench/validation25_core8"
9
+ staging_root="${dataset_root}/vbench_staging/validation25_core8"
10
+ python_bin="/mnt/s3files/s3-us-west2-default/zoubin/cz/envs/hy_worldplay/bin/python"
11
+ durable_uri="s3://s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/datasets/${dataset_name}/vbench/validation25_core8"
12
+
13
+ export PYTHONPATH="${vbench_repo}:${repo_root}"
14
+ export VBENCH_CACHE_DIR="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/vbench"
15
+ export TORCH_HOME="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/torch"
16
+ export HF_HOME="/mnt/s3files/s3-us-west2-default/zoubin/cz/.cache/huggingface"
17
+
18
+ mkdir -p "${run_root}/raw" "${run_root}/logs" "${staging_root}"
19
+
20
+ declare -a groups=(
21
+ "full_ffff|${dataset_root}/validation/full"
22
+ "reuse_frrf|${dataset_root}/validation/reuse"
23
+ "stage1_fppf|${dataset_root}/evaluations/eval_validation25_v4_blocks1-52_fp32master_ckptstep2000_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100/predictor"
24
+ "stage1_fppp|${dataset_root}/evaluations/eval_validation25_stage1-v4_schedule-fppp_blocks1-52_fp32master_ckptstep2000_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100/predictor"
25
+ "timestep_onpolicy_fppf|${dataset_root}/evaluations/eval_validation25_detached-timestep-rollout-onpolicy-fulltrain100_schedule-fppf_blocks1-52_ckptstep1000_flr5e-5_blr5e-6_schedsteps2000_swarmup100_bwarmup100linear_bs1/predictor"
26
+ "timestep_onpolicy_fppp|${dataset_root}/evaluations/eval_validation25_detached-timestep-rollout-onpolicy-fulltrain100_schedule-fppp_blocks1-52_ckptstep1000_flr5e-5_blr5e-6_schedsteps2000_swarmup100_bwarmup100linear_bs1/predictor"
27
+ "chunk_timestep_fppf|${dataset_root}/evaluations/eval_validation25_detached-chunk-timestep-rollout-onpolicy-fppp-fulltrain100_schedule-fppf_blocks1-52_ckptstep1000_flr5e-5_blr5e-6_schedsteps2000_swarmup100_bwarmup100linear_bs1/predictor"
28
+ "chunk_timestep_fppp|${dataset_root}/evaluations/eval_validation25_detached-chunk-timestep-rollout-onpolicy-fppp-fulltrain100_schedule-fppp_blocks1-52_ckptstep1000_flr5e-5_blr5e-6_schedsteps2000_swarmup100_bwarmup100linear_bs1/predictor"
29
+ )
30
+
31
+ find "${staging_root}" -maxdepth 1 -type l -delete
32
+ for spec in "${groups[@]}"; do
33
+ IFS='|' read -r label source_dir <<<"${spec}"
34
+ video_count=$(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | wc -l)
35
+ if [[ "${video_count}" -ne 100 ]]; then
36
+ echo "${label}: expected 100 videos, found ${video_count}: ${source_dir}" >&2
37
+ exit 1
38
+ fi
39
+ while IFS= read -r video_path; do
40
+ ln -s "${video_path}" "${staging_root}/${label}__$(basename "${video_path}")"
41
+ done < <(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | sort)
42
+ done
43
+
44
+ staged_count=$(find "${staging_root}" -maxdepth 1 -type l | wc -l)
45
+ if [[ "${staged_count}" -ne 800 ]]; then
46
+ echo "Expected 800 staged videos, found ${staged_count}" >&2
47
+ exit 1
48
+ fi
49
+
50
+ prompt_file="${run_root}/validation25_prompt_map.json"
51
+ "${python_bin}" "${repo_root}/tools/build_vbench_validation25_prompt_map.py" \
52
+ --cases_jsonl "${dataset_root}/validation/cases.jsonl" \
53
+ --staging_root "${staging_root}" \
54
+ --output "${prompt_file}"
55
+
56
+ dimensions=(
57
+ subject_consistency
58
+ background_consistency
59
+ temporal_flickering
60
+ motion_smoothness
61
+ dynamic_degree
62
+ aesthetic_quality
63
+ imaging_quality
64
+ overall_consistency
65
+ )
66
+
67
+ for dimension in "${dimensions[@]}"; do
68
+ output_dir="${run_root}/raw/${dimension}"
69
+ marker="${run_root}/${dimension}.completed"
70
+ mkdir -p "${output_dir}"
71
+ result_count=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json' | wc -l)
72
+ if [[ -f "${marker}" && "${result_count}" -eq 1 ]]; then
73
+ echo "[skip] ${dimension} already complete"
74
+ continue
75
+ fi
76
+ find "${output_dir}" -maxdepth 1 -type f -delete
77
+ echo "[vbench] ${dimension} start $(date -u +%FT%TZ)"
78
+ "${python_bin}" -m torch.distributed.run \
79
+ --standalone \
80
+ --nproc_per_node=8 \
81
+ "${vbench_repo}/evaluate.py" \
82
+ --videos_path "${staging_root}" \
83
+ --dimension "${dimension}" \
84
+ --mode custom_input \
85
+ --prompt_file "${prompt_file}" \
86
+ --load_ckpt_from_local True \
87
+ --imaging_quality_preprocessing_mode longer \
88
+ --output_path "${output_dir}" \
89
+ >"${run_root}/logs/${dimension}.log" 2>&1
90
+ result_json=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json')
91
+ "${python_bin}" - "${result_json}" "${dimension}" <<'PY'
92
+ import json
93
+ import sys
94
+
95
+ path, dimension = sys.argv[1:]
96
+ payload = json.load(open(path))
97
+ records = payload[dimension][1]
98
+ assert len(records) == 800, (dimension, len(records))
99
+ print(dimension, len(records), payload[dimension][0])
100
+ PY
101
+ date -u +%FT%TZ >"${marker}"
102
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
103
+ echo "[vbench] ${dimension} complete $(date -u +%FT%TZ)"
104
+ done
105
+
106
+ "${python_bin}" "${repo_root}/tools/summarize_vbench_validation25_core.py" \
107
+ --run_root "${run_root}" \
108
+ >"${run_root}/logs/summary.log" 2>&1
109
+ date -u +%FT%TZ >"${run_root}/pipeline.completed"
110
+ aws s3 sync "${run_root}" "${durable_uri}" --only-show-errors
111
+ echo "[vbench] all dimensions complete: ${run_root}"
tools/run_vbench_validation25_groups.sh ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # Generic VBench (8 dimensions) over staged validation25 groups.
3
+ # RUN_NAME=<name> NPROC=4 VBENCH_GROUPS="label|dir ..." bash tools/run_vbench_validation25_groups.sh
4
+ # VBENCH_GROUPS entries are "label|<eval_root>/predictor" separated by spaces; each dir
5
+ # must hold exactly 100 mp4s. Local paths; no S3 sync.
6
+ set -euo pipefail
7
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8
+ vbench_repo="${VBENCH_REPO:-/local/zoubin/cz/projects/VBench}"
9
+ python_bin="${PREDICTOR_PYTHON_BIN:-/local/zoubin/cz/envs/hy_worldplay_july20260720/bin/python}"
10
+ vbench_site_packages="${VBENCH_SITE_PACKAGES:-/local/zoubin/cz/envs/vbench_eval/lib/python3.10/site-packages}"
11
+ self_forcing_site_packages="${SELF_FORCING_SITE_PACKAGES:-/local/zoubin/cz/envs/self_forcing/lib/python3.10/site-packages}"
12
+ dataset_root="${PREDICTOR_HOLDOUT_NVME_ROOT:-${repo_root}/datasets/hyworldplay_predictor_vbench_val25_test50}"
13
+ run_name="${RUN_NAME:?set RUN_NAME}"
14
+ nproc="${NPROC:-8}"
15
+ run_root="${dataset_root}/vbench/${run_name}"
16
+ staging_root="${dataset_root}/vbench_staging/${run_name}"
17
+ overlay_root="${run_root}/site_overlay"
18
+ mkdir -p "${overlay_root}" "${run_root}/raw" "${run_root}/logs" "${staging_root}"
19
+ for package in easydict timm open_clip; do
20
+ [[ -d "${self_forcing_site_packages}/${package}" ]]
21
+ ln -sfn "${self_forcing_site_packages}/${package}" "${overlay_root}/${package}"
22
+ done
23
+ export PYTHONPATH="${overlay_root}:${vbench_site_packages}:${vbench_repo}:${repo_root}"
24
+ export VBENCH_CACHE_DIR="/local/zoubin/cz/.cache/vbench"
25
+ export TORCH_HOME="/local/zoubin/cz/.cache/torch"
26
+ export TORCH_HUB="${TORCH_HOME}/hub" # torch.hub reads TORCH_HUB first; the login shell sets it to /mnt/local_nvme
27
+ export HF_HOME="/local/zoubin/cz/.cache/huggingface"
28
+ export OMP_NUM_THREADS="${OMP_NUM_THREADS:-4}"
29
+ [[ -x "${python_bin}" && -f "${vbench_repo}/evaluate.py" && -f "${dataset_root}/validation/cases.jsonl" ]]
30
+ read -r -a group_specs <<< "${VBENCH_GROUPS:?set VBENCH_GROUPS}"
31
+ group_names=()
32
+ find "${staging_root}" -maxdepth 1 -type l -delete
33
+ for spec in "${group_specs[@]}"; do
34
+ IFS='|' read -r label source_dir <<<"${spec}"
35
+ count=$(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | wc -l)
36
+ [[ "${count}" -eq 100 ]] || { echo "${label}: expected 100 videos, found ${count}: ${source_dir}" >&2; exit 1; }
37
+ while IFS= read -r video_path; do
38
+ ln -s "${video_path}" "${staging_root}/${label}__$(basename "${video_path}")"
39
+ done < <(find "${source_dir}" -maxdepth 1 -type f -name '*.mp4' | sort)
40
+ group_names+=("${label}")
41
+ done
42
+ expected_records=$(( ${#group_names[@]} * 100 ))
43
+ [[ $(find "${staging_root}" -maxdepth 1 -type l | wc -l) -eq "${expected_records}" ]]
44
+ prompt_file="${run_root}/validation25_prompt_map.json"
45
+ "${python_bin}" "${repo_root}/tools/build_vbench_validation25_prompt_map.py" \
46
+ --cases_jsonl "${dataset_root}/validation/cases.jsonl" --staging_root "${staging_root}" \
47
+ --output "${prompt_file}" --expected_count "${expected_records}"
48
+ for dimension in subject_consistency background_consistency temporal_flickering motion_smoothness dynamic_degree aesthetic_quality imaging_quality overall_consistency; do
49
+ output_dir="${run_root}/raw/${dimension}"; marker="${run_root}/${dimension}.completed"
50
+ mkdir -p "${output_dir}"
51
+ if [[ -f "${marker}" && $(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json' | wc -l) -eq 1 ]]; then
52
+ echo "[skip] ${dimension} already complete"; continue
53
+ fi
54
+ find "${output_dir}" -maxdepth 1 -type f -delete
55
+ echo "[vbench] ${dimension} start $(date -u +%FT%TZ)"
56
+ "${python_bin}" -m torch.distributed.run --standalone --nproc_per_node="${nproc}" \
57
+ "${vbench_repo}/evaluate.py" --videos_path "${staging_root}" --dimension "${dimension}" \
58
+ --mode custom_input --prompt_file "${prompt_file}" --load_ckpt_from_local True \
59
+ --imaging_quality_preprocessing_mode longer --output_path "${output_dir}" \
60
+ >"${run_root}/logs/${dimension}.log" 2>&1
61
+ result_json=$(find "${output_dir}" -maxdepth 1 -type f -name '*_eval_results.json')
62
+ "${python_bin}" - "${result_json}" "${dimension}" "${expected_records}" <<'PY'
63
+ import json, sys
64
+ path, dimension, expected = sys.argv[1:]
65
+ payload = json.load(open(path)); records = payload[dimension][1]
66
+ assert len(records) == int(expected), (dimension, len(records))
67
+ print(dimension, len(records), payload[dimension][0])
68
+ PY
69
+ date -u +%FT%TZ >"${marker}"
70
+ echo "[vbench] ${dimension} complete $(date -u +%FT%TZ)"
71
+ done
72
+ summary_args=(); for group in "${group_names[@]}"; do summary_args+=(--group "${group}"); done
73
+ "${python_bin}" "${repo_root}/tools/summarize_vbench_validation25_pair.py" --run_root "${run_root}" "${summary_args[@]}" >"${run_root}/logs/summary.log" 2>&1
74
+ date -u +%FT%TZ >"${run_root}/pipeline.completed"
75
+ echo "[vbench] complete: ${run_root}"
tools/run_wide_c1_r33_validation25_reference.sh ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ python_bin="${PREDICTOR_PYTHON_BIN:-/mnt/local_nvme/cz/envs/hy_worldplay/bin/python3.10}"
6
+ dataset_root="${repo_root}/datasets/hyworldplay_predictor_vbench_val25_test50"
7
+ validation_root="${dataset_root}/validation"
8
+ base_model="${repo_root}/outputs/hf_snapshot_repaired_9b49404"
9
+ action_ckpt="/mnt/local_nvme/cz/checkpoints/hy_worldplay/huggingface/hub/models--tencent--HY-WorldPlay/snapshots/f4c29235647707b571479a69b569e4166f9f5bf8/ar_distilled_action_model/diffusion_pytorch_model.safetensors"
10
+ weights="${repo_root}/checkpoints/predictor_v4_prefeature_blocks1-52_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab/predictor_step_02000.safetensors"
11
+ confidence_checkpoint="${repo_root}/confidence_validation_wide4096x2_c1/checkpoints/confidence_c1.pt"
12
+ confidence_threshold="-4.308430990"
13
+ eval_name="eval_validation25_stage1-wide4096x2-c1-threshold-r33_blocks1-52_fp32master_step2000"
14
+ eval_root="${dataset_root}/evaluations/${eval_name}"
15
+ log_dir="${eval_root}/logs"
16
+
17
+ export PYTHONPATH="${repo_root}"
18
+ export HY_PROFILE_TIMING=1
19
+ mkdir -p "${log_dir}"
20
+
21
+ run_workers() {
22
+ local phase="$1"
23
+ shift
24
+ local -a pids=()
25
+ local worker_id
26
+ for worker_id in $(seq 0 7); do
27
+ CUDA_VISIBLE_DEVICES="${worker_id}" "${python_bin}" -u "$@" \
28
+ --worker_id "${worker_id}" --num_workers 8 \
29
+ >"${log_dir}/${phase}_worker_${worker_id}.log" 2>&1 &
30
+ pids+=("$!")
31
+ done
32
+ local failed=0 pid
33
+ for pid in "${pids[@]}"; do
34
+ if ! wait "${pid}"; then failed=1; fi
35
+ done
36
+ if [[ "${failed}" -ne 0 ]]; then
37
+ echo "${phase} worker failure; inspect ${log_dir}/${phase}_worker_*.log" >&2
38
+ return 1
39
+ fi
40
+ }
41
+
42
+ if [[ ! -f "${eval_root}/generation.completed" ]] \
43
+ || [[ $(find "${eval_root}/predictor" -type f -name '*.mp4' 2>/dev/null | wc -l) -ne 100 ]]; then
44
+ date -u +%FT%TZ >"${eval_root}/generation.started"
45
+ run_workers generate \
46
+ "${repo_root}/tools/generate_predictor_v4_train_case_eval.py" \
47
+ --dataset_root "${validation_root}" \
48
+ --output_root "${eval_root}" \
49
+ --base_model "${base_model}" \
50
+ --action_ckpt "${action_ckpt}" \
51
+ --weights "${weights}" \
52
+ --source_blocks 1,52 \
53
+ --predictor_variant v4 \
54
+ --previous_condition_mode same_step \
55
+ --replace_steps 1,2,3 \
56
+ --modes predictor \
57
+ --confidence_checkpoint "${confidence_checkpoint}" \
58
+ --confidence_threshold "${confidence_threshold}"
59
+ [[ $(find "${eval_root}/predictor" -type f -name '*.mp4' | wc -l) -eq 100 ]]
60
+ date -u +%FT%TZ >"${eval_root}/generation.completed"
61
+ fi
62
+
63
+ if [[ ! -f "${eval_root}/metrics.completed" ]]; then
64
+ date -u +%FT%TZ >"${eval_root}/metrics.started"
65
+ run_workers metrics \
66
+ "${repo_root}/tools/compute_predictor_v4_train_case_metrics.py" \
67
+ --dataset_root "${validation_root}" \
68
+ --output_root "${eval_root}" \
69
+ --num_cases 25 \
70
+ --reference_subdir full \
71
+ --modes predictor \
72
+ --metric_batch_size 4
73
+ "${python_bin}" "${repo_root}/tools/merge_predictor_v4_train_case_metrics.py" \
74
+ --output_root "${eval_root}" \
75
+ --num_workers 8 \
76
+ --num_cases 25 \
77
+ --modes predictor \
78
+ --replace_steps 1,2,3 \
79
+ --reference_label "Full-DiT validation videos" \
80
+ >"${log_dir}/metrics_merge.log" 2>&1
81
+ date -u +%FT%TZ >"${eval_root}/metrics.completed"
82
+ fi
83
+
84
+ echo "Validation evaluation complete: ${eval_root}"
tools/run_wide_c2_hybrid_validation25_reference.sh ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ python_bin="${PREDICTOR_PYTHON_BIN:-/mnt/local_nvme/cz/envs/hy_worldplay/bin/python3.10}"
6
+ dataset_root="${repo_root}/datasets/hyworldplay_predictor_vbench_val25_test50"
7
+ validation_root="${dataset_root}/validation"
8
+ base_model="${repo_root}/outputs/hf_snapshot_repaired_9b49404"
9
+ action_ckpt="/mnt/local_nvme/cz/checkpoints/hy_worldplay/huggingface/hub/models--tencent--HY-WorldPlay/snapshots/f4c29235647707b571479a69b569e4166f9f5bf8/ar_distilled_action_model/diffusion_pytorch_model.safetensors"
10
+ weights="${repo_root}/checkpoints/predictor_v4_prefeature_blocks1-52_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab/predictor_step_02000.safetensors"
11
+ confidence_checkpoint="${repo_root}/confidence_validation_wide4096x2_c2/checkpoints/confidence_c2.pt"
12
+
13
+ tags=(p12r10 p12r25)
14
+ thresholds=(
15
+ "1:-4.136674309,2:-3.798280764"
16
+ "1:-4.400576830,2:-4.049407601"
17
+ )
18
+ gpu_groups=("0 1 2 3" "4 5 6 7")
19
+
20
+ export PYTHONPATH="${repo_root}"
21
+ export HY_PROFILE_TIMING=1
22
+
23
+ run_group() {
24
+ local phase="$1" group_index="$2"
25
+ local tag="${tags[$group_index]}"
26
+ local threshold_spec="${thresholds[$group_index]}"
27
+ local eval_name="eval_validation25_stage1-wide4096x2-c2-hybrid-p3full-${tag}_blocks1-52_fp32master_step2000"
28
+ local eval_root="${dataset_root}/evaluations/${eval_name}"
29
+ local log_dir="${eval_root}/logs"
30
+ local -a gpus=(${gpu_groups[$group_index]})
31
+ mkdir -p "${log_dir}"
32
+
33
+ if [[ "${phase}" == "generate" ]] \
34
+ && [[ -f "${eval_root}/generation.completed" ]] \
35
+ && [[ $(find "${eval_root}/predictor" -type f -name '*.mp4' 2>/dev/null | wc -l) -eq 100 ]]; then
36
+ return
37
+ fi
38
+ if [[ "${phase}" == "metrics" && -f "${eval_root}/metrics.completed" ]]; then
39
+ return
40
+ fi
41
+
42
+ local worker_id gpu failed=0 pid
43
+ local -a pids=()
44
+ for worker_id in "${!gpus[@]}"; do
45
+ gpu="${gpus[$worker_id]}"
46
+ if [[ "${phase}" == "generate" ]]; then
47
+ CUDA_VISIBLE_DEVICES="${gpu}" "${python_bin}" -u \
48
+ "${repo_root}/tools/generate_predictor_v4_train_case_eval.py" \
49
+ --dataset_root "${validation_root}" \
50
+ --output_root "${eval_root}" \
51
+ --base_model "${base_model}" \
52
+ --action_ckpt "${action_ckpt}" \
53
+ --weights "${weights}" \
54
+ --source_blocks 1,52 \
55
+ --predictor_variant v4 \
56
+ --previous_condition_mode same_step \
57
+ --replace_steps 1,2 \
58
+ --modes predictor \
59
+ --confidence_checkpoint "${confidence_checkpoint}" \
60
+ --confidence_thresholds "${threshold_spec}" \
61
+ --worker_id "${worker_id}" --num_workers "${#gpus[@]}" \
62
+ >"${log_dir}/generate_worker_${worker_id}.log" 2>&1 &
63
+ else
64
+ CUDA_VISIBLE_DEVICES="${gpu}" "${python_bin}" -u \
65
+ "${repo_root}/tools/compute_predictor_v4_train_case_metrics.py" \
66
+ --dataset_root "${validation_root}" \
67
+ --output_root "${eval_root}" \
68
+ --num_cases 25 \
69
+ --reference_subdir full \
70
+ --modes predictor \
71
+ --metric_batch_size 4 \
72
+ --worker_id "${worker_id}" --num_workers "${#gpus[@]}" \
73
+ >"${log_dir}/metrics_worker_${worker_id}.log" 2>&1 &
74
+ fi
75
+ pids+=("$!")
76
+ done
77
+ for pid in "${pids[@]}"; do
78
+ if ! wait "${pid}"; then failed=1; fi
79
+ done
80
+ if [[ "${failed}" -ne 0 ]]; then
81
+ echo "${phase} failed for ${tag}; inspect ${log_dir}" >&2
82
+ return 1
83
+ fi
84
+
85
+ if [[ "${phase}" == "generate" ]]; then
86
+ [[ $(find "${eval_root}/predictor" -type f -name '*.mp4' | wc -l) -eq 100 ]]
87
+ date -u +%FT%TZ >"${eval_root}/generation.completed"
88
+ else
89
+ "${python_bin}" "${repo_root}/tools/merge_predictor_v4_train_case_metrics.py" \
90
+ --output_root "${eval_root}" \
91
+ --num_workers "${#gpus[@]}" \
92
+ --num_cases 25 \
93
+ --modes predictor \
94
+ --replace_steps 1,2 \
95
+ --reference_label "Full-DiT validation videos" \
96
+ >"${log_dir}/metrics_merge.log" 2>&1
97
+ date -u +%FT%TZ >"${eval_root}/metrics.completed"
98
+ fi
99
+ }
100
+
101
+ run_phase() {
102
+ local phase="$1" failed=0 pid group_index
103
+ local -a pids=()
104
+ for group_index in "${!tags[@]}"; do
105
+ run_group "${phase}" "${group_index}" &
106
+ pids+=("$!")
107
+ done
108
+ for pid in "${pids[@]}"; do
109
+ if ! wait "${pid}"; then failed=1; fi
110
+ done
111
+ [[ "${failed}" -eq 0 ]]
112
+ }
113
+
114
+ run_phase generate
115
+ run_phase metrics
116
+ echo "Wide C2 hybrid validation25 evaluation complete"
tools/summarize_atc_rollout_ckpt_sweep.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Full-reference metrics + VBench Core5 table for the ATC rollout checkpoint sweep."""
3
+ from __future__ import annotations
4
+ import argparse, json
5
+ from pathlib import Path
6
+
7
+ CORE5 = ("subject_consistency", "background_consistency", "motion_smoothness", "aesthetic_quality", "imaging_quality")
8
+
9
+
10
+ def main() -> None:
11
+ p = argparse.ArgumentParser(description=__doc__)
12
+ p.add_argument("--dataset_root", type=Path, required=True)
13
+ p.add_argument("--vbench_run", required=True)
14
+ p.add_argument("--group", action="append", required=True, help="label|eval_dir_name")
15
+ p.add_argument("--output", type=Path, required=True)
16
+ args = p.parse_args()
17
+ vb = json.load(open(args.dataset_root / "vbench" / args.vbench_run / "summary.json"))
18
+ groups = vb.get("groups", vb)
19
+ rows = []
20
+ for spec in args.group:
21
+ label, eval_name = spec.split("|", 1)
22
+ m = json.load(open(args.dataset_root / "evaluations" / eval_name / "metrics.json"))["summary_mean"]["predictor"]
23
+ g = groups.get(label, {}).get("mean", {})
24
+ core5 = sum(g[d] / (100.0 if d == "imaging_quality" and g[d] > 1.5 else 1.0) for d in CORE5) / 5 if all(d in g for d in CORE5) else float("nan")
25
+ rows.append({"label": label, "psnr": m["psnr_db"], "frame_psnr": m["frame_psnr_mean_db"], "ssim": m["ssim"],
26
+ "lpips": m["lpips_alex"], "mse": m["pixel_mse"], "temporal_mse": m["temporal_delta_mse"],
27
+ "vbench_core5": core5, **{f"vbench_{d}": g.get(d, float("nan")) for d in g}})
28
+ lines = ["| 组 | PSNR ↑ | Frame PSNR ↑ | SSIM ↑ | LPIPS ↓ | MSE ↓ | Temporal MSE ↓ | VBench Core5 ↑ |", "|---|---:|---:|---:|---:|---:|---:|---:|"]
29
+ for r in rows:
30
+ lines.append(f"| {r['label']} | {r['psnr']:.4f} | {r['frame_psnr']:.4f} | {r['ssim']:.5f} | {r['lpips']:.5f} | {r['mse']:.6f} | {r['temporal_mse']:.6f} | {r['vbench_core5']:.6f} |")
31
+ args.output.with_suffix(".json").write_text(json.dumps(rows, indent=2) + "\n")
32
+ args.output.write_text("\n".join(lines) + "\n")
33
+ print("\n".join(lines))
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
tools/summarize_c1_threshold_validation25.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Summarize Stage-1 C1-threshold validation25 reference metrics and routing."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ from pathlib import Path
8
+
9
+
10
+ ROOT = Path(__file__).resolve().parents[1]
11
+ DATASET = ROOT / "datasets/hyworldplay_predictor_vbench_val25_test50"
12
+ EVALUATIONS = DATASET / "evaluations"
13
+ OUTPUT = ROOT / "confidence_validation/c1_threshold_validation25"
14
+ BASELINES = {
15
+ "FFFF": None,
16
+ "FPPF": "eval_validation25_v4_blocks1-52_fp32master_ckptstep2000_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100",
17
+ "FPPP": "eval_validation25_stage1-v4_schedule-fppp_blocks1-52_fp32master_ckptstep2000_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100",
18
+ }
19
+ THRESHOLDS = {
20
+ "C1-r10": ("eval_validation25_stage1-c1-threshold-r10_blocks1-52_fp32master_step2000", -3.523588657),
21
+ "C1-r25": ("eval_validation25_stage1-c1-threshold-r25_blocks1-52_fp32master_step2000", -3.915276289),
22
+ "C1-r33": ("eval_validation25_stage1-c1-threshold-r33_blocks1-52_fp32master_step2000", -4.329442501),
23
+ "C1-r50": ("eval_validation25_stage1-c1-threshold-r50_blocks1-52_fp32master_step2000", -4.798926830),
24
+ }
25
+ METRICS = (
26
+ "psnr_db", "frame_psnr_mean_db", "ssim", "lpips_alex", "pixel_mae",
27
+ "pixel_mse", "pixel_rmse", "temporal_delta_mse",
28
+ )
29
+ VBENCH_DIMENSIONS = (
30
+ "subject_consistency", "background_consistency", "temporal_flickering",
31
+ "motion_smoothness", "dynamic_degree", "aesthetic_quality",
32
+ "imaging_quality", "overall_consistency",
33
+ )
34
+ VBENCH_CORE5 = (
35
+ "subject_consistency", "background_consistency", "motion_smoothness",
36
+ "aesthetic_quality", "imaging_quality",
37
+ )
38
+
39
+
40
+ def read_metrics(eval_name: str) -> dict:
41
+ payload = json.loads((EVALUATIONS / eval_name / "metrics.json").read_text())
42
+ return payload["summary_mean"]["predictor"]
43
+
44
+
45
+ def read_decisions(eval_name: str) -> tuple[list[dict], list[float]]:
46
+ root = EVALUATIONS / eval_name
47
+ decisions = []
48
+ for path in sorted((root / "confidence_decisions").glob("worker_*.jsonl")):
49
+ decisions.extend(json.loads(line) for line in path.read_text().splitlines())
50
+ elapsed = []
51
+ for path in sorted((root / "logs").glob("worker_*.jsonl")):
52
+ elapsed.extend(json.loads(line)["elapsed_s"] for line in path.read_text().splitlines())
53
+ return decisions, elapsed
54
+
55
+
56
+ def main() -> None:
57
+ rows = []
58
+ rows.append({
59
+ "method": "FFFF", "threshold_log_c1": None, "fallback_rate": 1.0,
60
+ **{name: (float("inf") if "psnr" in name else 1.0 if name == "ssim" else 0.0) for name in METRICS},
61
+ })
62
+ for method in ("FPPF", "FPPP"):
63
+ fallback = 1 / 3 if method == "FPPF" else 0.0
64
+ rows.append({
65
+ "method": method, "threshold_log_c1": None,
66
+ "fallback_rate": fallback, **read_metrics(BASELINES[method]),
67
+ })
68
+ for method, (eval_name, threshold) in THRESHOLDS.items():
69
+ decisions, elapsed = read_decisions(eval_name)
70
+ fallback = [item for item in decisions if item["fallback_to_full"]]
71
+ by_step = {
72
+ f"p{step}_fallback_rate": sum(
73
+ item["fallback_to_full"] for item in decisions
74
+ if item["denoise_step"] == step
75
+ ) / sum(item["denoise_step"] == step for item in decisions)
76
+ for step in (1, 2, 3)
77
+ }
78
+ rows.append({
79
+ "method": method,
80
+ "threshold_log_c1": threshold,
81
+ "fallback_rate": len(fallback) / len(decisions),
82
+ **by_step,
83
+ "mean_end_to_end_s": sum(elapsed) / len(elapsed),
84
+ **read_metrics(eval_name),
85
+ })
86
+ core_vbench = json.loads(
87
+ (DATASET / "vbench/validation25_core8/summary.json").read_text()
88
+ )["groups"]
89
+ threshold_vbench = json.loads(
90
+ (DATASET / "vbench/validation25_c1_thresholds/summary.json").read_text()
91
+ )["groups"]
92
+ vbench_groups = {
93
+ "FFFF": core_vbench["full_ffff"]["mean"],
94
+ "FPPF": core_vbench["stage1_fppf"]["mean"],
95
+ "FPPP": core_vbench["stage1_fppp"]["mean"],
96
+ "C1-r10": threshold_vbench["c1_r10"]["mean"],
97
+ "C1-r25": threshold_vbench["c1_r25"]["mean"],
98
+ "C1-r33": threshold_vbench["c1_r33"]["mean"],
99
+ "C1-r50": threshold_vbench["c1_r50"]["mean"],
100
+ }
101
+ for row in rows:
102
+ scores = vbench_groups[row["method"]]
103
+ row.update({f"vbench_{name}": scores[name] for name in VBENCH_DIMENSIONS})
104
+ row["vbench_core5_mean"] = sum(scores[name] for name in VBENCH_CORE5) / len(VBENCH_CORE5)
105
+ by_method = {row["method"]: row for row in rows}
106
+ comparison_metrics = (
107
+ "psnr_db", "ssim", "lpips_alex", "pixel_mse", "temporal_delta_mse",
108
+ *[f"vbench_{name}" for name in VBENCH_DIMENSIONS], "vbench_core5_mean",
109
+ )
110
+ comparisons = {}
111
+ for candidate in ("C1-r33", "C1-r50"):
112
+ comparisons[f"{candidate}_vs_FPPF"] = {
113
+ name: by_method[candidate][name] - by_method["FPPF"][name]
114
+ for name in comparison_metrics
115
+ }
116
+ OUTPUT.mkdir(parents=True, exist_ok=True)
117
+ payload = {
118
+ "protocol": "validation25, seed 0, Stage-1 v4 FP32-master step 2000",
119
+ "rows": rows,
120
+ "comparisons": comparisons,
121
+ }
122
+ (OUTPUT / "summary.json").write_text(json.dumps(payload, indent=2, allow_nan=True) + "\n")
123
+ columns = [
124
+ "method", "threshold_log_c1", "fallback_rate", "p1_fallback_rate",
125
+ "p2_fallback_rate", "p3_fallback_rate", "mean_end_to_end_s", *METRICS,
126
+ *[f"vbench_{name}" for name in VBENCH_DIMENSIONS], "vbench_core5_mean",
127
+ ]
128
+ lines = [",".join(columns)]
129
+ for row in rows:
130
+ lines.append(",".join(str(row.get(column, "")) for column in columns))
131
+ (OUTPUT / "summary.csv").write_text("\n".join(lines) + "\n")
132
+ reference_lines = [
133
+ "# C1 threshold validation25 comparison",
134
+ "",
135
+ "## Reference metrics against FFFF",
136
+ "",
137
+ "| Method | Fallback | P1/P2/P3 fallback | PSNR | SSIM | LPIPS | Pixel MSE | Temporal MSE |",
138
+ "| --- | ---: | --- | ---: | ---: | ---: | ---: | ---: |",
139
+ ]
140
+ for row in rows:
141
+ routing = "-" if "p1_fallback_rate" not in row else "/".join(
142
+ f"{100 * row[f'p{step}_fallback_rate']:.1f}%" for step in (1, 2, 3)
143
+ )
144
+ psnr = "∞" if row["psnr_db"] == float("inf") else f"{row['psnr_db']:.4f}"
145
+ reference_lines.append(
146
+ f"| {row['method']} | {100 * row['fallback_rate']:.1f}% | {routing} | "
147
+ f"{psnr} | {row['ssim']:.5f} | {row['lpips_alex']:.5f} | "
148
+ f"{row['pixel_mse']:.6f} | {row['temporal_delta_mse']:.6f} |"
149
+ )
150
+ reference_lines.extend([
151
+ "", "## VBench", "",
152
+ "| Method | Subject | Background | Flicker | Motion | Dynamic | Aesthetic | Imaging | Overall | Core5 mean |",
153
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
154
+ ])
155
+ for row in rows:
156
+ values = " | ".join(
157
+ f"{row[f'vbench_{name}']:.6f}" for name in VBENCH_DIMENSIONS
158
+ )
159
+ reference_lines.append(
160
+ f"| {row['method']} | {values} | {row['vbench_core5_mean']:.6f} |"
161
+ )
162
+ (OUTPUT / "report.md").write_text("\n".join(reference_lines) + "\n")
163
+ print(json.dumps(payload, indent=2, allow_nan=True))
164
+
165
+
166
+ if __name__ == "__main__":
167
+ main()
tools/summarize_predictor_timestep_rollout_overfit8.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Summarize the six FPPP/FPPF evaluations from the replay sweep."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+
10
+
11
+ SCHEDULES = ("fppp", "fppf")
12
+ METRICS = (
13
+ "psnr_db", "frame_psnr_mean_db", "ssim", "lpips_alex",
14
+ "pixel_mse", "pixel_rmse", "pixel_mae", "temporal_delta_mse",
15
+ )
16
+
17
+
18
+ def main() -> None:
19
+ parser = argparse.ArgumentParser(description=__doc__)
20
+ parser.add_argument("--eval_parent", type=Path, required=True)
21
+ parser.add_argument("--output", type=Path, required=True)
22
+ parser.add_argument(
23
+ "--experiment",
24
+ default="v4 timestep-only FPPP rollout overfit8, 200 optimizer steps",
25
+ )
26
+ parser.add_argument(
27
+ "--variants",
28
+ default="replay0:0.0 replay0p2:0.2 replay0p4:0.4",
29
+ )
30
+ args = parser.parse_args()
31
+ variants = tuple(
32
+ (spec.split(":", 1)[0], float(spec.split(":", 1)[1]))
33
+ for spec in args.variants.split()
34
+ )
35
+ rows = []
36
+ for label, replay_weight in variants:
37
+ for schedule in SCHEDULES:
38
+ path = args.eval_parent / f"{label}_{schedule}" / "metrics.json"
39
+ with path.open("r", encoding="utf-8") as handle:
40
+ payload = json.load(handle)
41
+ summary = payload["summary_mean"]["predictor"]
42
+ rows.append(
43
+ {
44
+ "variant": label,
45
+ "replay_weight": replay_weight,
46
+ "evaluation_schedule": schedule.upper(),
47
+ "replace_steps": payload["config"]["replace_steps"],
48
+ "videos": payload["config"]["videos_per_mode"],
49
+ **{name: summary[name] for name in METRICS},
50
+ "metrics_path": str(path),
51
+ }
52
+ )
53
+ result = {
54
+ "experiment": args.experiment,
55
+ "training_variants": [
56
+ {"variant": label, "replay_weight": weight} for label, weight in variants
57
+ ],
58
+ "evaluations_per_variant": ["FPPP", "FPPF"],
59
+ "videos_per_evaluation": 32,
60
+ "rows": rows,
61
+ }
62
+ args.output.parent.mkdir(parents=True, exist_ok=True)
63
+ args.output.write_text(
64
+ json.dumps(result, indent=2, ensure_ascii=False, sort_keys=True) + "\n",
65
+ encoding="utf-8",
66
+ )
67
+ print(json.dumps(result, indent=2, ensure_ascii=False, sort_keys=True))
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
tools/summarize_vbench_validation25_chunk_timestep_step2000.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Summarize VBench results for chunk+timestep step-2000 FPPF/FPPP videos."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import csv
8
+ import json
9
+ import re
10
+ from collections import defaultdict
11
+ from pathlib import Path
12
+ from statistics import fmean
13
+
14
+
15
+ DIMENSIONS = [
16
+ "subject_consistency",
17
+ "background_consistency",
18
+ "temporal_flickering",
19
+ "motion_smoothness",
20
+ "dynamic_degree",
21
+ "aesthetic_quality",
22
+ "imaging_quality",
23
+ "overall_consistency",
24
+ ]
25
+
26
+ GROUP_ORDER = [
27
+ "chunk_timestep_step2000_fppf",
28
+ "chunk_timestep_step2000_fppp",
29
+ ]
30
+
31
+ ACTION_RE = re.compile(r"_action_(\d{2})$")
32
+
33
+
34
+ def load_dimension_result(run_root: Path, dimension: str) -> dict:
35
+ candidates = sorted((run_root / "raw" / dimension).glob("*_eval_results.json"))
36
+ if len(candidates) != 1:
37
+ raise RuntimeError(
38
+ f"Expected exactly one result JSON for {dimension}, got {len(candidates)}"
39
+ )
40
+ payload = json.loads(candidates[0].read_text())
41
+ if dimension not in payload:
42
+ raise KeyError(f"{dimension} missing from {candidates[0]}")
43
+ overall, records = payload[dimension]
44
+ if len(records) != 200:
45
+ raise RuntimeError(f"{dimension}: expected 200 records, got {len(records)}")
46
+ return {
47
+ "source": str(candidates[0]),
48
+ "official_overall": float(overall),
49
+ "records": records,
50
+ }
51
+
52
+
53
+ def normalize_score(dimension: str, value: float) -> float:
54
+ if dimension == "imaging_quality":
55
+ return float(value) / 100.0
56
+ return float(value)
57
+
58
+
59
+ def parse_record(record: dict, dimension: str) -> dict:
60
+ video_path = Path(record["video_path"])
61
+ stem = video_path.stem
62
+ if "__" not in stem:
63
+ raise ValueError(f"Missing group prefix in staged video name: {video_path}")
64
+ group, original_stem = stem.split("__", 1)
65
+ if group not in GROUP_ORDER:
66
+ raise ValueError(f"Unknown VBench group {group}: {video_path}")
67
+ action_match = ACTION_RE.search(original_stem)
68
+ if action_match is None:
69
+ raise ValueError(f"Cannot parse action id from {video_path}")
70
+ return {
71
+ "group": group,
72
+ "video_id": original_stem,
73
+ "action_id": int(action_match.group(1)),
74
+ "video_path": str(video_path),
75
+ "score": normalize_score(dimension, record["video_results"]),
76
+ }
77
+
78
+
79
+ def main() -> None:
80
+ parser = argparse.ArgumentParser(description=__doc__)
81
+ parser.add_argument("--run_root", type=Path, required=True)
82
+ args = parser.parse_args()
83
+ run_root = args.run_root.resolve()
84
+
85
+ parsed_by_dimension: dict[str, list[dict]] = {}
86
+ official_overall: dict[str, float] = {}
87
+ sources: dict[str, str] = {}
88
+ for dimension in DIMENSIONS:
89
+ result = load_dimension_result(run_root, dimension)
90
+ parsed_by_dimension[dimension] = [
91
+ parse_record(record, dimension) for record in result["records"]
92
+ ]
93
+ official_overall[dimension] = result["official_overall"]
94
+ sources[dimension] = result["source"]
95
+
96
+ per_video: dict[str, dict[str, dict]] = defaultdict(dict)
97
+ for dimension, records in parsed_by_dimension.items():
98
+ counts = defaultdict(int)
99
+ for record in records:
100
+ counts[record["group"]] += 1
101
+ item = per_video[record["group"]].setdefault(
102
+ record["video_id"],
103
+ {
104
+ "action_id": record["action_id"],
105
+ "video_id": record["video_id"],
106
+ "scores": {},
107
+ },
108
+ )
109
+ item["scores"][dimension] = record["score"]
110
+ for group in GROUP_ORDER:
111
+ if counts[group] != 100:
112
+ raise RuntimeError(
113
+ f"{dimension}/{group}: expected 100 records, got {counts[group]}"
114
+ )
115
+
116
+ group_summary = {}
117
+ for group in GROUP_ORDER:
118
+ items = list(per_video[group].values())
119
+ if len(items) != 100:
120
+ raise RuntimeError(f"{group}: expected 100 paired videos, got {len(items)}")
121
+ mean = {
122
+ dimension: fmean(item["scores"][dimension] for item in items)
123
+ for dimension in DIMENSIONS
124
+ }
125
+ mean_by_action_id = {}
126
+ for action_id in range(4):
127
+ action_items = [item for item in items if item["action_id"] == action_id]
128
+ if len(action_items) != 25:
129
+ raise RuntimeError(
130
+ f"{group}/action{action_id}: expected 25 videos, "
131
+ f"got {len(action_items)}"
132
+ )
133
+ mean_by_action_id[str(action_id)] = {
134
+ dimension: fmean(
135
+ item["scores"][dimension] for item in action_items
136
+ )
137
+ for dimension in DIMENSIONS
138
+ }
139
+ group_summary[group] = {
140
+ "record_count": len(items),
141
+ "mean": mean,
142
+ "mean_by_action_id": mean_by_action_id,
143
+ }
144
+
145
+ payload = {
146
+ "dimensions": DIMENSIONS,
147
+ "group_order": GROUP_ORDER,
148
+ "official_overall_all_200_videos": official_overall,
149
+ "raw_sources": sources,
150
+ "groups": group_summary,
151
+ }
152
+ (run_root / "summary.json").write_text(
153
+ json.dumps(payload, indent=2, sort_keys=True) + "\n"
154
+ )
155
+
156
+ with (run_root / "summary.csv").open("w", newline="") as handle:
157
+ writer = csv.writer(handle)
158
+ writer.writerow(["group", "video_count", *DIMENSIONS])
159
+ for group in GROUP_ORDER:
160
+ writer.writerow(
161
+ [
162
+ group,
163
+ group_summary[group]["record_count"],
164
+ *[
165
+ f"{group_summary[group]['mean'][dimension]:.8f}"
166
+ for dimension in DIMENSIONS
167
+ ],
168
+ ]
169
+ )
170
+
171
+ header = (
172
+ "| 版本 | 视频数 | Subject ↑ | Background ↑ | Flickering ↑ | "
173
+ "Motion smoothness ↑ | Dynamic degree ↑ | Aesthetic ↑ | Imaging ↑ | "
174
+ "Overall consistency ↑ |"
175
+ )
176
+ divider = (
177
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |"
178
+ )
179
+ rows = [header, divider]
180
+ for group in GROUP_ORDER:
181
+ mean = group_summary[group]["mean"]
182
+ rows.append(
183
+ "| "
184
+ + " | ".join(
185
+ [
186
+ group,
187
+ str(group_summary[group]["record_count"]),
188
+ f"{mean['subject_consistency']:.6f}",
189
+ f"{mean['background_consistency']:.6f}",
190
+ f"{mean['temporal_flickering']:.6f}",
191
+ f"{mean['motion_smoothness']:.6f}",
192
+ f"{mean['dynamic_degree']:.6f}",
193
+ f"{mean['aesthetic_quality']:.6f}",
194
+ f"{mean['imaging_quality']:.6f}",
195
+ f"{mean['overall_consistency']:.6f}",
196
+ ]
197
+ )
198
+ + " |"
199
+ )
200
+ (run_root / "summary.md").write_text("\n".join(rows) + "\n")
201
+ print(json.dumps(payload, indent=2, sort_keys=True))
202
+
203
+
204
+ if __name__ == "__main__":
205
+ main()
tools/summarize_vbench_validation25_core.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Summarize the five VBench metrics for the validation25 core comparison."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import csv
8
+ import json
9
+ import re
10
+ from collections import defaultdict
11
+ from pathlib import Path
12
+ from statistics import fmean
13
+
14
+
15
+ DIMENSIONS = [
16
+ "subject_consistency",
17
+ "background_consistency",
18
+ "temporal_flickering",
19
+ "motion_smoothness",
20
+ "dynamic_degree",
21
+ "aesthetic_quality",
22
+ "imaging_quality",
23
+ "overall_consistency",
24
+ ]
25
+
26
+ GROUP_ORDER = [
27
+ "full_ffff",
28
+ "reuse_frrf",
29
+ "stage1_fppf",
30
+ "stage1_fppp",
31
+ "timestep_onpolicy_fppf",
32
+ "timestep_onpolicy_fppp",
33
+ "chunk_timestep_fppf",
34
+ "chunk_timestep_fppp",
35
+ ]
36
+
37
+ COMPARISONS = [
38
+ ("timestep_onpolicy_fppf", "stage1_fppf"),
39
+ ("timestep_onpolicy_fppp", "stage1_fppp"),
40
+ ("chunk_timestep_fppf", "stage1_fppf"),
41
+ ("chunk_timestep_fppp", "stage1_fppp"),
42
+ ]
43
+
44
+ ACTION_RE = re.compile(r"_action_(\d{2})$")
45
+
46
+
47
+ def load_dimension_result(run_root: Path, dimension: str) -> dict:
48
+ candidates = sorted((run_root / "raw" / dimension).glob("*_eval_results.json"))
49
+ if len(candidates) != 1:
50
+ raise RuntimeError(
51
+ f"Expected exactly one result JSON for {dimension}, got {len(candidates)}"
52
+ )
53
+ payload = json.loads(candidates[0].read_text())
54
+ if dimension not in payload:
55
+ raise KeyError(f"{dimension} missing from {candidates[0]}")
56
+ overall, records = payload[dimension]
57
+ if len(records) != 800:
58
+ raise RuntimeError(f"{dimension}: expected 800 records, got {len(records)}")
59
+ return {
60
+ "source": str(candidates[0]),
61
+ "official_overall": float(overall),
62
+ "records": records,
63
+ }
64
+
65
+
66
+ def normalize_score(dimension: str, value: float) -> float:
67
+ # VBench stores per-video MUSIQ scores on the 0..100 scale but divides its
68
+ # aggregate imaging_quality result by 100.
69
+ if dimension == "imaging_quality":
70
+ return float(value) / 100.0
71
+ return float(value)
72
+
73
+
74
+ def parse_record(record: dict, dimension: str) -> dict:
75
+ video_path = Path(record["video_path"])
76
+ stem = video_path.stem
77
+ if "__" not in stem:
78
+ raise ValueError(f"Missing group prefix in staged video name: {video_path}")
79
+ group, original_stem = stem.split("__", 1)
80
+ if group not in GROUP_ORDER:
81
+ raise ValueError(f"Unknown VBench group {group}: {video_path}")
82
+ action_match = ACTION_RE.search(original_stem)
83
+ if action_match is None:
84
+ raise ValueError(f"Cannot parse action id from {video_path}")
85
+ return {
86
+ "group": group,
87
+ "video_id": original_stem,
88
+ "action_id": int(action_match.group(1)),
89
+ "video_path": str(video_path),
90
+ "score": normalize_score(dimension, record["video_results"]),
91
+ }
92
+
93
+
94
+ def main() -> None:
95
+ parser = argparse.ArgumentParser(description=__doc__)
96
+ parser.add_argument("--run_root", type=Path, required=True)
97
+ args = parser.parse_args()
98
+ run_root = args.run_root.resolve()
99
+
100
+ parsed_by_dimension: dict[str, list[dict]] = {}
101
+ official_overall: dict[str, float] = {}
102
+ sources: dict[str, str] = {}
103
+ for dimension in DIMENSIONS:
104
+ result = load_dimension_result(run_root, dimension)
105
+ parsed = [parse_record(record, dimension) for record in result["records"]]
106
+ parsed_by_dimension[dimension] = parsed
107
+ official_overall[dimension] = result["official_overall"]
108
+ sources[dimension] = result["source"]
109
+
110
+ per_video: dict[str, dict[str, dict]] = defaultdict(dict)
111
+ for dimension, records in parsed_by_dimension.items():
112
+ counts = defaultdict(int)
113
+ for record in records:
114
+ counts[record["group"]] += 1
115
+ item = per_video[record["group"]].setdefault(
116
+ record["video_id"],
117
+ {
118
+ "action_id": record["action_id"],
119
+ "video_id": record["video_id"],
120
+ "scores": {},
121
+ },
122
+ )
123
+ item["scores"][dimension] = record["score"]
124
+ for group in GROUP_ORDER:
125
+ if counts[group] != 100:
126
+ raise RuntimeError(
127
+ f"{dimension}/{group}: expected 100 records, got {counts[group]}"
128
+ )
129
+
130
+ group_summary = {}
131
+ for group in GROUP_ORDER:
132
+ items = list(per_video[group].values())
133
+ if len(items) != 100:
134
+ raise RuntimeError(f"{group}: expected 100 paired videos, got {len(items)}")
135
+ mean = {
136
+ dimension: fmean(item["scores"][dimension] for item in items)
137
+ for dimension in DIMENSIONS
138
+ }
139
+ mean_by_action_id = {}
140
+ for action_id in range(4):
141
+ action_items = [item for item in items if item["action_id"] == action_id]
142
+ if len(action_items) != 25:
143
+ raise RuntimeError(
144
+ f"{group}/action{action_id}: expected 25 videos, got {len(action_items)}"
145
+ )
146
+ mean_by_action_id[str(action_id)] = {
147
+ dimension: fmean(
148
+ item["scores"][dimension] for item in action_items
149
+ )
150
+ for dimension in DIMENSIONS
151
+ }
152
+ group_summary[group] = {
153
+ "record_count": len(items),
154
+ "mean": mean,
155
+ "mean_by_action_id": mean_by_action_id,
156
+ }
157
+
158
+ comparisons = {}
159
+ for candidate, baseline in COMPARISONS:
160
+ candidate_items = per_video[candidate]
161
+ baseline_items = per_video[baseline]
162
+ if set(candidate_items) != set(baseline_items):
163
+ raise RuntimeError(f"Pairing mismatch: {candidate} vs {baseline}")
164
+ key = f"{candidate}_vs_{baseline}"
165
+ comparisons[key] = {
166
+ "candidate": candidate,
167
+ "baseline": baseline,
168
+ "delta": {},
169
+ "wins_out_of_100": {},
170
+ }
171
+ for dimension in DIMENSIONS:
172
+ deltas = [
173
+ candidate_items[video_id]["scores"][dimension]
174
+ - baseline_items[video_id]["scores"][dimension]
175
+ for video_id in sorted(candidate_items)
176
+ ]
177
+ comparisons[key]["delta"][dimension] = fmean(deltas)
178
+ comparisons[key]["wins_out_of_100"][dimension] = sum(
179
+ delta > 0 for delta in deltas
180
+ )
181
+
182
+ payload = {
183
+ "dimensions": DIMENSIONS,
184
+ "group_order": GROUP_ORDER,
185
+ "official_overall_all_800_videos": official_overall,
186
+ "raw_sources": sources,
187
+ "groups": group_summary,
188
+ "comparisons": comparisons,
189
+ }
190
+ (run_root / "summary.json").write_text(
191
+ json.dumps(payload, indent=2, sort_keys=True) + "\n"
192
+ )
193
+
194
+ with (run_root / "summary.csv").open("w", newline="") as handle:
195
+ writer = csv.writer(handle)
196
+ writer.writerow(["group", "video_count", *DIMENSIONS])
197
+ for group in GROUP_ORDER:
198
+ writer.writerow(
199
+ [
200
+ group,
201
+ group_summary[group]["record_count"],
202
+ *[
203
+ f"{group_summary[group]['mean'][dimension]:.8f}"
204
+ for dimension in DIMENSIONS
205
+ ],
206
+ ]
207
+ )
208
+
209
+ header = (
210
+ "| 版本 | 视频数 | Subject ↑ | Background ↑ | Flickering ↑ | "
211
+ "Motion smoothness ↑ | Dynamic degree ↑ | Aesthetic ↑ | Imaging ↑ | "
212
+ "Overall consistency ↑ |"
213
+ )
214
+ divider = (
215
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |"
216
+ )
217
+ rows = [header, divider]
218
+ for group in GROUP_ORDER:
219
+ mean = group_summary[group]["mean"]
220
+ rows.append(
221
+ "| "
222
+ + " | ".join(
223
+ [
224
+ group,
225
+ str(group_summary[group]["record_count"]),
226
+ f"{mean['subject_consistency']:.6f}",
227
+ f"{mean['background_consistency']:.6f}",
228
+ f"{mean['temporal_flickering']:.6f}",
229
+ f"{mean['motion_smoothness']:.6f}",
230
+ f"{mean['dynamic_degree']:.6f}",
231
+ f"{mean['aesthetic_quality']:.6f}",
232
+ f"{mean['imaging_quality']:.6f}",
233
+ f"{mean['overall_consistency']:.6f}",
234
+ ]
235
+ )
236
+ + " |"
237
+ )
238
+ (run_root / "summary.md").write_text("\n".join(rows) + "\n")
239
+ print(json.dumps(payload, indent=2, sort_keys=True))
240
+
241
+
242
+ if __name__ == "__main__":
243
+ main()
tools/summarize_vbench_validation25_pair.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Summarize eight VBench dimensions for validation25 video groups."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import csv
8
+ import json
9
+ import re
10
+ from collections import defaultdict
11
+ from pathlib import Path
12
+ from statistics import fmean
13
+
14
+
15
+ DIMENSIONS = [
16
+ "subject_consistency",
17
+ "background_consistency",
18
+ "temporal_flickering",
19
+ "motion_smoothness",
20
+ "dynamic_degree",
21
+ "aesthetic_quality",
22
+ "imaging_quality",
23
+ "overall_consistency",
24
+ ]
25
+
26
+ ACTION_RE = re.compile(r"_action_(\d{2})$")
27
+
28
+
29
+ def load_dimension_result(
30
+ run_root: Path, dimension: str, expected_records: int
31
+ ) -> dict:
32
+ candidates = sorted((run_root / "raw" / dimension).glob("*_eval_results.json"))
33
+ if len(candidates) != 1:
34
+ raise RuntimeError(
35
+ f"Expected exactly one result JSON for {dimension}, got {len(candidates)}"
36
+ )
37
+ payload = json.loads(candidates[0].read_text())
38
+ if dimension not in payload:
39
+ raise KeyError(f"{dimension} missing from {candidates[0]}")
40
+ overall, records = payload[dimension]
41
+ if len(records) != expected_records:
42
+ raise RuntimeError(
43
+ f"{dimension}: expected {expected_records} records, got {len(records)}"
44
+ )
45
+ return {
46
+ "source": str(candidates[0]),
47
+ "official_overall": float(overall),
48
+ "records": records,
49
+ }
50
+
51
+
52
+ def normalize_score(dimension: str, value: float) -> float:
53
+ if dimension == "imaging_quality":
54
+ return float(value) / 100.0
55
+ return float(value)
56
+
57
+
58
+ def parse_record(record: dict, dimension: str, group_order: list[str]) -> dict:
59
+ video_path = Path(record["video_path"])
60
+ stem = video_path.stem
61
+ if "__" not in stem:
62
+ raise ValueError(f"Missing group prefix in staged video name: {video_path}")
63
+ group, original_stem = stem.split("__", 1)
64
+ if group not in group_order:
65
+ raise ValueError(f"Unknown VBench group {group}: {video_path}")
66
+ action_match = ACTION_RE.search(original_stem)
67
+ if action_match is None:
68
+ raise ValueError(f"Cannot parse action id from {video_path}")
69
+ return {
70
+ "group": group,
71
+ "video_id": original_stem,
72
+ "action_id": int(action_match.group(1)),
73
+ "video_path": str(video_path),
74
+ "score": normalize_score(dimension, record["video_results"]),
75
+ }
76
+
77
+
78
+ def main() -> None:
79
+ parser = argparse.ArgumentParser(description=__doc__)
80
+ parser.add_argument("--run_root", type=Path, required=True)
81
+ parser.add_argument("--group", action="append", required=True)
82
+ parser.add_argument("--videos_per_group", type=int, default=100)
83
+ args = parser.parse_args()
84
+ run_root = args.run_root.resolve()
85
+ group_order = list(args.group)
86
+ if not group_order or len(set(group_order)) != len(group_order):
87
+ raise ValueError("Pass one or more distinct --group values")
88
+ expected_records = len(group_order) * args.videos_per_group
89
+
90
+ parsed_by_dimension: dict[str, list[dict]] = {}
91
+ official_overall: dict[str, float] = {}
92
+ sources: dict[str, str] = {}
93
+ for dimension in DIMENSIONS:
94
+ result = load_dimension_result(run_root, dimension, expected_records)
95
+ parsed_by_dimension[dimension] = [
96
+ parse_record(record, dimension, group_order)
97
+ for record in result["records"]
98
+ ]
99
+ official_overall[dimension] = result["official_overall"]
100
+ sources[dimension] = result["source"]
101
+
102
+ per_video: dict[str, dict[str, dict]] = defaultdict(dict)
103
+ for dimension, records in parsed_by_dimension.items():
104
+ counts = defaultdict(int)
105
+ for record in records:
106
+ counts[record["group"]] += 1
107
+ item = per_video[record["group"]].setdefault(
108
+ record["video_id"],
109
+ {
110
+ "action_id": record["action_id"],
111
+ "video_id": record["video_id"],
112
+ "scores": {},
113
+ },
114
+ )
115
+ item["scores"][dimension] = record["score"]
116
+ for group in group_order:
117
+ if counts[group] != args.videos_per_group:
118
+ raise RuntimeError(
119
+ f"{dimension}/{group}: expected {args.videos_per_group} "
120
+ f"records, got {counts[group]}"
121
+ )
122
+
123
+ group_summary = {}
124
+ for group in group_order:
125
+ items = list(per_video[group].values())
126
+ if len(items) != args.videos_per_group:
127
+ raise RuntimeError(
128
+ f"{group}: expected {args.videos_per_group} videos, got {len(items)}"
129
+ )
130
+ mean = {
131
+ dimension: fmean(item["scores"][dimension] for item in items)
132
+ for dimension in DIMENSIONS
133
+ }
134
+ mean_by_action_id = {}
135
+ for action_id in range(4):
136
+ action_items = [item for item in items if item["action_id"] == action_id]
137
+ if len(action_items) * 4 != args.videos_per_group:
138
+ raise RuntimeError(
139
+ f"{group}/action{action_id}: uneven validation action count "
140
+ f"{len(action_items)}"
141
+ )
142
+ mean_by_action_id[str(action_id)] = {
143
+ dimension: fmean(item["scores"][dimension] for item in action_items)
144
+ for dimension in DIMENSIONS
145
+ }
146
+ group_summary[group] = {
147
+ "record_count": len(items),
148
+ "mean": mean,
149
+ "mean_by_action_id": mean_by_action_id,
150
+ }
151
+
152
+ summary = {
153
+ "dimensions": DIMENSIONS,
154
+ "group_order": group_order,
155
+ "official_overall_all_videos": official_overall,
156
+ "raw_sources": sources,
157
+ "groups": group_summary,
158
+ }
159
+ (run_root / "summary.json").write_text(
160
+ json.dumps(summary, indent=2, sort_keys=True) + "\n"
161
+ )
162
+
163
+ with (run_root / "summary.csv").open("w", newline="") as handle:
164
+ writer = csv.writer(handle)
165
+ writer.writerow(["group", "video_count", *DIMENSIONS])
166
+ for group in group_order:
167
+ writer.writerow(
168
+ [
169
+ group,
170
+ group_summary[group]["record_count"],
171
+ *[
172
+ f"{group_summary[group]['mean'][dimension]:.8f}"
173
+ for dimension in DIMENSIONS
174
+ ],
175
+ ]
176
+ )
177
+
178
+ rows = [
179
+ (
180
+ "| 版本 | 视频数 | Subject ↑ | Background ↑ | Flickering ↑ | "
181
+ "Motion smoothness ↑ | Dynamic degree ↑ | Aesthetic ↑ | Imaging ↑ | "
182
+ "Overall consistency ↑ |"
183
+ ),
184
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
185
+ ]
186
+ for group in group_order:
187
+ mean = group_summary[group]["mean"]
188
+ rows.append(
189
+ "| "
190
+ + " | ".join(
191
+ [
192
+ group,
193
+ str(group_summary[group]["record_count"]),
194
+ f"{mean['subject_consistency']:.6f}",
195
+ f"{mean['background_consistency']:.6f}",
196
+ f"{mean['temporal_flickering']:.6f}",
197
+ f"{mean['motion_smoothness']:.6f}",
198
+ f"{mean['dynamic_degree']:.6f}",
199
+ f"{mean['aesthetic_quality']:.6f}",
200
+ f"{mean['imaging_quality']:.6f}",
201
+ f"{mean['overall_consistency']:.6f}",
202
+ ]
203
+ )
204
+ + " |"
205
+ )
206
+ (run_root / "summary.md").write_text("\n".join(rows) + "\n")
207
+ print(json.dumps(summary, indent=2, sort_keys=True))
208
+
209
+
210
+ if __name__ == "__main__":
211
+ main()
tools/supervise_predictor_v4_fp32master_training.sh ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ output_dir="${repo_root}/checkpoints/predictor_v4_prefeature_blocks1-52_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab"
6
+ final_weights="${output_dir}/predictor_step_02000.safetensors"
7
+ resume_path="${output_dir}/training_latest.pt"
8
+ lock_path="${output_dir}/supervisor.lock"
9
+ mkdir -p "${output_dir}"
10
+
11
+ exec 9>"${lock_path}"
12
+ if ! flock -n 9; then
13
+ echo "[supervisor] another FP32-master v4 supervisor is already active"
14
+ exit 0
15
+ fi
16
+
17
+ while [[ ! -f "${final_weights}" ]]; do
18
+ if pgrep -f "python -u ${repo_root}/train_predictor_v4.py .*--output_dir ${output_dir}" >/dev/null; then
19
+ sleep 60
20
+ continue
21
+ fi
22
+
23
+ echo "[supervisor] FP32-master v4 worker is absent; restarting at $(date -u +%FT%TZ)"
24
+ if [[ -f "${resume_path}" ]]; then
25
+ bash "${repo_root}/tools/launch_predictor_v4_fp32master_training.sh" \
26
+ "${output_dir}" "${resume_path}" || true
27
+ else
28
+ bash "${repo_root}/tools/launch_predictor_v4_fp32master_training.sh" \
29
+ "${output_dir}" || true
30
+ fi
31
+ sleep 30
32
+ done
33
+
34
+ echo "[supervisor] step 2000 checkpoint is complete: ${final_weights}"
tools/supervise_predictor_v4_rollout_fppf_overfit8_training.sh ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ output_dir="${repo_root}/checkpoints/predictor_v4_rollout_fppf_blocks1-52_overfit8_seed20260721_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps300_swarmup8_bwarmup8_swanlab"
6
+ final_weights="${output_dir}/predictor_step_00300.safetensors"
7
+ resume_path="${output_dir}/training_latest.pt"
8
+ lock_path="${output_dir}/supervisor.lock"
9
+ mkdir -p "${output_dir}"
10
+
11
+ exec 9>"${lock_path}"
12
+ if ! flock -n 9; then
13
+ echo "[supervisor] another FPPF rollout overfit supervisor is already active"
14
+ exit 0
15
+ fi
16
+
17
+ while [[ ! -f "${final_weights}" ]]; do
18
+ if pgrep -f "train_predictor_v4_rollout.py .*--output_dir ${output_dir} .*--rollout_schedule fppf" >/dev/null; then
19
+ sleep 60
20
+ continue
21
+ fi
22
+
23
+ echo "[supervisor] FPPF rollout worker is absent; restarting at $(date -u +%FT%TZ)"
24
+ if [[ -f "${resume_path}" ]]; then
25
+ bash "${repo_root}/tools/launch_predictor_v4_rollout_fppf_overfit8_training.sh" \
26
+ "${output_dir}" "${resume_path}" 300 || true
27
+ else
28
+ bash "${repo_root}/tools/launch_predictor_v4_rollout_fppf_overfit8_training.sh" \
29
+ "${output_dir}" "" 300 || true
30
+ fi
31
+ sleep 30
32
+ done
33
+
34
+ echo "[supervisor] FPPF step 300 checkpoint is complete: ${final_weights}"
tools/supervise_predictor_v4_rollout_overfit8_training.sh ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ output_dir="${repo_root}/checkpoints/predictor_v4_rollout_fppp_blocks1-52_overfit8_seed20260721_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup8_bwarmup8_swanlab"
6
+ final_weights="${output_dir}/predictor_step_02000.safetensors"
7
+ resume_path="${output_dir}/training_latest.pt"
8
+ lock_path="${output_dir}/supervisor.lock"
9
+ mkdir -p "${output_dir}"
10
+
11
+ exec 9>"${lock_path}"
12
+ if ! flock -n 9; then
13
+ echo "[supervisor] another rollout overfit supervisor is already active"
14
+ exit 0
15
+ fi
16
+
17
+ while [[ ! -f "${final_weights}" ]]; do
18
+ if pgrep -f "python -u ${repo_root}/train_predictor_v4_rollout.py .*--output_dir ${output_dir}" >/dev/null; then
19
+ sleep 60
20
+ continue
21
+ fi
22
+
23
+ echo "[supervisor] rollout overfit worker is absent; restarting at $(date -u +%FT%TZ)"
24
+ if [[ -f "${resume_path}" ]]; then
25
+ bash "${repo_root}/tools/launch_predictor_v4_rollout_overfit8_training.sh" \
26
+ "${output_dir}" "${resume_path}" || true
27
+ else
28
+ bash "${repo_root}/tools/launch_predictor_v4_rollout_overfit8_training.sh" \
29
+ "${output_dir}" || true
30
+ fi
31
+ sleep 30
32
+ done
33
+
34
+ echo "[supervisor] step 2000 checkpoint is complete: ${final_weights}"
tools/supervise_predictor_v4_rollout_training.sh ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ output_dir="${repo_root}/checkpoints/predictor_v4_rollout_fppp_blocks1-52_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab"
6
+ final_weights="${output_dir}/predictor_step_02000.safetensors"
7
+ resume_path="${output_dir}/training_latest.pt"
8
+ lock_path="${output_dir}/supervisor.lock"
9
+ mkdir -p "${output_dir}"
10
+
11
+ exec 9>"${lock_path}"
12
+ if ! flock -n 9; then
13
+ echo "[supervisor] another rollout supervisor is already active"
14
+ exit 0
15
+ fi
16
+
17
+ while [[ ! -f "${final_weights}" ]]; do
18
+ if pgrep -f "python -u ${repo_root}/train_predictor_v4_rollout.py .*--output_dir ${output_dir}" >/dev/null; then
19
+ sleep 60
20
+ continue
21
+ fi
22
+
23
+ echo "[supervisor] rollout worker is absent; restarting at $(date -u +%FT%TZ)"
24
+ if [[ -f "${resume_path}" ]]; then
25
+ bash "${repo_root}/tools/launch_predictor_v4_rollout_training.sh" \
26
+ "${output_dir}" "${resume_path}" || true
27
+ else
28
+ bash "${repo_root}/tools/launch_predictor_v4_rollout_training.sh" \
29
+ "${output_dir}" || true
30
+ fi
31
+ sleep 30
32
+ done
33
+
34
+ echo "[supervisor] step 2000 checkpoint is complete: ${final_weights}"
tools/supervise_previous_vbench_validation_predictors.sh ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ repo_root="/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor"
5
+ dataset_root="${PREDICTOR_HOLDOUT_NVME_ROOT:-/mnt/local_nvme/zoubin/cz/hyworldplay_predictor_vbench_val25_test50}"
6
+ eval_base="${dataset_root}/evaluations"
7
+ lock_path="${eval_base}/previous_versions_validation.lock"
8
+ complete_path="${eval_base}/previous_versions_validation.completed"
9
+ mkdir -p "${eval_base}"
10
+
11
+ exec 9>"${lock_path}"
12
+ if ! flock -n 9; then
13
+ echo "[supervisor] another previous-version validation supervisor is active"
14
+ exit 0
15
+ fi
16
+
17
+ run_until_complete() {
18
+ local eval_name="$1"
19
+ local weights="$2"
20
+ local source_blocks="$3"
21
+ local predictor_variant="$4"
22
+ local marker="${eval_base}/${eval_name}/sync.completed"
23
+ while [[ ! -f "${marker}" ]]; do
24
+ echo "[supervisor] starting ${eval_name} at $(date -u +%FT%TZ)"
25
+ if ! bash "${repo_root}/tools/launch_vbench_validation_predictor.sh" \
26
+ "${eval_name}" "${weights}" "${source_blocks}" "${predictor_variant}"; then
27
+ echo "[supervisor] ${eval_name} failed; retrying in 30 seconds"
28
+ sleep 30
29
+ fi
30
+ done
31
+ echo "[supervisor] completed ${eval_name} at $(date -u +%FT%TZ)"
32
+ }
33
+
34
+ run_until_complete \
35
+ eval_validation25_v4_blocks0-53_bf16_ckptstep2000_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100 \
36
+ "${repo_root}/checkpoints/predictor_v4_prefeature_blocks0-53_bf16_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab/predictor_step_02000.safetensors" \
37
+ 0,53 v4
38
+
39
+ run_until_complete \
40
+ eval_validation25_v4_blocks1-52_bf16_ckptstep2000_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100 \
41
+ "${repo_root}/checkpoints/predictor_v4_prefeature_blocks1-52_bf16_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab/predictor_step_02000.safetensors" \
42
+ 1,52 v4
43
+
44
+ run_until_complete \
45
+ eval_validation25_disca_blocks0-53_bf16_ckptstep2000_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100 \
46
+ "${repo_root}/checkpoints/predictor_disca_blocks0-53_bf16_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab/predictor_step_02000.safetensors" \
47
+ 0,53 disca
48
+
49
+ date -u +%FT%TZ >"${complete_path}"
50
+ echo "[supervisor] all previous-version validation evaluations are complete"
tools/supervise_training_tests_then_helios.sh ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -uo pipefail
3
+
4
+ ROOT=/data3/chenzhuo/workspace/HY-WorldPlay-DEV-Predictor
5
+ HELIOS=/data3/chenzhuo/workspace/Helios
6
+ PYTHON=/data3/chenzhuo/anaconda3/envs/worldplay/bin/python
7
+ SUPERVISOR_LOG="$ROOT/outputs/supervise_training_tests_then_helios.log"
8
+
9
+ V2_TRAIN_PID=4073736
10
+ V3_TRAIN_PID=3092818
11
+ V2_WEIGHTS="$ROOT/checkpoints/predictor_v2_1_52/predictor_step_02000.safetensors"
12
+ V3_WEIGHTS="$ROOT/checkpoints/predictor_v3_0_53/predictor_step_02000.safetensors"
13
+
14
+ log() {
15
+ printf '[%s] %s\n' "$(date '+%F %T')" "$*"
16
+ }
17
+
18
+ latest_step() {
19
+ local path=$1
20
+ if [[ -s "$path" ]]; then
21
+ tail -n 1 "$path" | sed -n 's/.*"global_step"[ ]*:[ ]*\([0-9]*\).*/\1/p'
22
+ else
23
+ printf '?'
24
+ fi
25
+ }
26
+
27
+ wait_for_training() {
28
+ local label=$1
29
+ local pid=$2
30
+ local train_log=$3
31
+ local weights=$4
32
+ while kill -0 "$pid" 2>/dev/null; do
33
+ log "$label is active: step=$(latest_step "$train_log")/2000 pid=$pid"
34
+ sleep 60
35
+ done
36
+ if [[ ! -s "$weights" ]]; then
37
+ log "ERROR: $label exited without final weights: $weights"
38
+ return 1
39
+ fi
40
+ log "$label completed and final weights exist: $weights"
41
+ }
42
+
43
+ validate_eval() {
44
+ local output_dir=$1
45
+ local expected_runs=$2
46
+ "$PYTHON" - "$output_dir" "$expected_runs" <<'PY'
47
+ import json
48
+ import sys
49
+ from pathlib import Path
50
+
51
+ root = Path(sys.argv[1])
52
+ expected = int(sys.argv[2])
53
+ payload = json.loads((root / "metrics.json").read_text())
54
+ runs = payload.get("runs", [])
55
+ if len(runs) != expected:
56
+ raise SystemExit(f"expected {expected} runs, found {len(runs)}")
57
+ missing = [run.get("video_path") for run in runs if not Path(run["video_path"]).is_file()]
58
+ if missing:
59
+ raise SystemExit(f"missing videos: {missing[:3]}")
60
+ print(f"validated {len(runs)} runs and videos in {root}")
61
+ PY
62
+ }
63
+
64
+ adopt_or_validate_eval() {
65
+ local label=$1
66
+ local output_dir=$2
67
+ local expected_runs=$3
68
+ if validate_eval "$output_dir" "$expected_runs" >/dev/null 2>&1; then
69
+ log "$label is already complete: $output_dir"
70
+ return 0
71
+ fi
72
+
73
+ local pid
74
+ pid=$(pgrep -f "eval_predictor_v2_rollout.py.*--output_dir $output_dir" | head -n 1 || true)
75
+ if [[ -z "$pid" ]]; then
76
+ return 1
77
+ fi
78
+ log "adopting active $label process: pid=$pid output=$output_dir"
79
+ while kill -0 "$pid" 2>/dev/null; do
80
+ log "$label is active: pid=$pid"
81
+ sleep 60
82
+ done
83
+ validate_eval "$output_dir" "$expected_runs" || return 1
84
+ log "$label completed: $output_dir"
85
+ }
86
+
87
+ run_v2_test_after_training() {
88
+ wait_for_training \
89
+ 'v2 [1,52] GPU4 training' \
90
+ "$V2_TRAIN_PID" \
91
+ "$ROOT/checkpoints/predictor_v2_1_52/train_log.jsonl" \
92
+ "$V2_WEIGHTS" || return 1
93
+
94
+ local output_dir="$ROOT/outputs/predictor_v2/online_1_52_step2000"
95
+ local eval_log="$ROOT/outputs/predictor_v2_online_1_52_step2000.log"
96
+ if adopt_or_validate_eval 'v2 [1,52] test' "$output_dir" 30; then
97
+ return 0
98
+ fi
99
+ log "starting v2 [1,52] 10-case Full/Reuse/Predictor test on GPU4"
100
+ (
101
+ cd "$ROOT" || exit 1
102
+ unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
103
+ export CUDA_VISIBLE_DEVICES=4 PYTHONPATH=.
104
+ "$PYTHON" -u eval_predictor_v2_rollout.py \
105
+ --predictor_version v2 \
106
+ --weights_1_52 "$V2_WEIGHTS" \
107
+ --modes full,reuse,predictor_1_52 \
108
+ --case_ids 1-10 \
109
+ --video_frames 253 \
110
+ --output_dir "$output_dir"
111
+ ) >"$eval_log" 2>&1
112
+ local status=$?
113
+ if (( status != 0 )); then
114
+ log "ERROR: v2 [1,52] test failed with status $status; log=$eval_log"
115
+ return "$status"
116
+ fi
117
+ validate_eval "$output_dir" 30 || return 1
118
+ log "v2 [1,52] test completed: $output_dir"
119
+ }
120
+
121
+ run_v3_test_after_training() {
122
+ wait_for_training \
123
+ 'v3 [0,53] GPU5 training' \
124
+ "$V3_TRAIN_PID" \
125
+ "$ROOT/checkpoints/predictor_v3_0_53/train_log.jsonl" \
126
+ "$V3_WEIGHTS" || return 1
127
+
128
+ local smoke_dir="$ROOT/outputs/predictor_v3/online_0_53_step2000_smoke"
129
+ local smoke_log="$ROOT/outputs/predictor_v3_online_0_53_step2000_smoke.log"
130
+ local status=0
131
+ if ! adopt_or_validate_eval 'v3 [0,53] smoke' "$smoke_dir" 2; then
132
+ log "starting v3 [0,53] two-chunk online smoke on GPU5"
133
+ (
134
+ cd "$ROOT" || exit 1
135
+ unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
136
+ export CUDA_VISIBLE_DEVICES=5 PYTHONPATH=.
137
+ "$PYTHON" -u eval_predictor_v2_rollout.py \
138
+ --predictor_version v3 \
139
+ --weights_0_53 "$V3_WEIGHTS" \
140
+ --modes full,predictor_0_53 \
141
+ --case_ids 1 \
142
+ --video_frames 29 \
143
+ --output_dir "$smoke_dir"
144
+ ) >"$smoke_log" 2>&1
145
+ status=$?
146
+ if (( status != 0 )); then
147
+ log "ERROR: v3 [0,53] smoke failed with status $status; log=$smoke_log"
148
+ return "$status"
149
+ fi
150
+ validate_eval "$smoke_dir" 2 || return 1
151
+ fi
152
+ log "v3 smoke passed; starting 10-case Full/Reuse/Predictor test on GPU5"
153
+
154
+ local output_dir="$ROOT/outputs/predictor_v3/online_0_53_step2000"
155
+ local eval_log="$ROOT/outputs/predictor_v3_online_0_53_step2000.log"
156
+ if adopt_or_validate_eval 'v3 [0,53] full test' "$output_dir" 30; then
157
+ return 0
158
+ fi
159
+ (
160
+ cd "$ROOT" || exit 1
161
+ unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
162
+ export CUDA_VISIBLE_DEVICES=5 PYTHONPATH=.
163
+ "$PYTHON" -u eval_predictor_v2_rollout.py \
164
+ --predictor_version v3 \
165
+ --weights_0_53 "$V3_WEIGHTS" \
166
+ --modes full,reuse,predictor_0_53 \
167
+ --case_ids 1-10 \
168
+ --video_frames 253 \
169
+ --output_dir "$output_dir"
170
+ ) >"$eval_log" 2>&1
171
+ status=$?
172
+ if (( status != 0 )); then
173
+ log "ERROR: v3 [0,53] test failed with status $status; log=$eval_log"
174
+ return "$status"
175
+ fi
176
+ validate_eval "$output_dir" 30 || return 1
177
+ log "v3 [0,53] test completed: $output_dir"
178
+ }
179
+
180
+ wait_for_existing_v2_0_53_test() {
181
+ local pid=1215665
182
+ local output_dir="$ROOT/outputs/predictor_v2/online_0_53_step2000"
183
+ while kill -0 "$pid" 2>/dev/null; do
184
+ log "v2 [0,53] GPU3 test is active: pid=$pid"
185
+ sleep 60
186
+ done
187
+ validate_eval "$output_dir" 30 || {
188
+ log "ERROR: v2 [0,53] GPU3 test did not complete successfully"
189
+ return 1
190
+ }
191
+ log "v2 [0,53] GPU3 test completed"
192
+ }
193
+
194
+ launch_helios() {
195
+ local gpu=$1
196
+ local stamp=$2
197
+ local output_root="$HELIOS/output_helios/moviebench_990_gpu${gpu}_post_predictor_${stamp}"
198
+ local helios_log="$HELIOS/logs/run_gpu${gpu}_post_predictor_${stamp}.log"
199
+ (
200
+ cd "$HELIOS" || exit 1
201
+ unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
202
+ export GPU="$gpu" OUTPUT_ROOT="$output_root"
203
+ exec bash "$HELIOS/run.sh"
204
+ ) >"$helios_log" 2>&1 < /dev/null &
205
+ local pid=$!
206
+ log "launched Helios/run.sh on GPU$gpu: pid=$pid log=$helios_log" >&2
207
+ printf '%s' "$pid"
208
+ }
209
+
210
+ main() {
211
+ mkdir -p "$ROOT/outputs" "$HELIOS/logs"
212
+ log "supervisor started"
213
+
214
+ run_v2_test_after_training &
215
+ local worker4=$!
216
+ run_v3_test_after_training &
217
+ local worker5=$!
218
+
219
+ wait "$worker4"
220
+ local status4=$?
221
+ wait "$worker5"
222
+ local status5=$?
223
+ wait_for_existing_v2_0_53_test
224
+ local status3=$?
225
+
226
+ if (( status3 != 0 || status4 != 0 || status5 != 0 )); then
227
+ log "ERROR: at least one required test failed; Helios will not be launched (gpu3=$status3 gpu4=$status4 gpu5=$status5)"
228
+ return 1
229
+ fi
230
+
231
+ local stamp
232
+ stamp=$(date '+%Y%m%d_%H%M%S')
233
+ local helios4 helios5
234
+ helios4=$(launch_helios 4 "$stamp")
235
+ helios5=$(launch_helios 5 "$stamp")
236
+ sleep 60
237
+ if ! kill -0 "$helios4" 2>/dev/null || ! kill -0 "$helios5" 2>/dev/null; then
238
+ log "ERROR: a Helios process exited during startup (gpu4=$helios4 gpu5=$helios5)"
239
+ return 1
240
+ fi
241
+ log "Helios processes are active on GPU4/GPU5; supervisor finished successfully"
242
+ }
243
+
244
+ main >>"$SUPERVISOR_LOG" 2>&1
tools/sync_predictor_prefeature_to_s3.sh ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ export BUCKET="${BUCKET:-s3-us-west2-default}"
5
+ export FS_ID="${FS_ID:-fs-05c60be7352499179}"
6
+ export MOUNT_POINT="${MOUNT_POINT:-/mnt/s3files/s3-us-west2-default}"
7
+
8
+ dataset_name="hyworldplay_predictor_prefeature_v4"
9
+ nvme_root="${PREDICTOR_DATA_NVME_ROOT:-/mnt/local_nvme/zoubin/cz/${dataset_name}}"
10
+ s3_uri="${PREDICTOR_DATA_S3_URI:-s3://${BUCKET}/zoubin/cz/projects/HY-WorldPlay-DEV-Predictor/datasets/${dataset_name}}"
11
+
12
+ [[ -f "${nvme_root}/manifest.jsonl" ]] || {
13
+ echo "ERROR: missing merged manifest in ${nvme_root}" >&2
14
+ exit 1
15
+ }
16
+ [[ $(wc -l < "${nvme_root}/manifest.jsonl") -eq 3200 ]] || {
17
+ echo "ERROR: manifest.jsonl must contain 3200 records" >&2
18
+ exit 1
19
+ }
20
+ [[ $(wc -l < "${nvme_root}/train_eval_manifest.jsonl") -eq 2800 ]] || {
21
+ echo "ERROR: train_eval_manifest.jsonl must contain 2800 records" >&2
22
+ exit 1
23
+ }
24
+ [[ $(find "${nvme_root}/cases" -type f -name '*.safetensors' | wc -l) -eq 100 ]] || {
25
+ echo "ERROR: expected 100 case tensor files" >&2
26
+ exit 1
27
+ }
28
+ [[ $(find "${nvme_root}/steps" -type f -name '*.safetensors' | wc -l) -eq 3200 ]] || {
29
+ echo "ERROR: expected 3200 step tensor files" >&2
30
+ exit 1
31
+ }
32
+ [[ $(find "${nvme_root}/context_prefeature" -type f -name '*.safetensors' | wc -l) -eq 12800 ]] || {
33
+ echo "ERROR: expected 12800 Context prefeature files" >&2
34
+ exit 1
35
+ }
36
+ [[ $(find "${nvme_root}/videos" -type f -name '*.mp4' | wc -l) -eq 400 ]] || {
37
+ echo "ERROR: expected 400 evaluation videos" >&2
38
+ exit 1
39
+ }
40
+
41
+ echo "[dataset] validated complete NVMe build: ${nvme_root}"
42
+ echo "[dataset] syncing ${nvme_root} -> ${s3_uri}"
43
+ aws s3 sync "${nvme_root}" "${s3_uri}" --only-show-errors
44
+ echo "[dataset] durable S3 synchronization completed"
tools/train_evaluate_predictor_confidence.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Train C0/C1/C2 confidence heads and report held-out risk coverage."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import csv
8
+ import json
9
+ import math
10
+ import os
11
+ import random
12
+ from pathlib import Path
13
+
14
+ import matplotlib.pyplot as plt
15
+ import numpy as np
16
+ import torch
17
+ import torch.distributed as dist
18
+ import torch.nn.functional as F
19
+ from scipy.stats import pearsonr, spearmanr
20
+ from torch.nn.parallel import DistributedDataParallel as DDP
21
+ from torch.utils.data import DataLoader, TensorDataset
22
+ from torch.utils.data.distributed import DistributedSampler
23
+
24
+ from models.confidence_head import PredictorConfidenceHead
25
+
26
+
27
+ TARGETS = ("c0", "c1", "c2")
28
+ RATIOS = (0.0, 0.05, 0.1, 0.2, 0.25, 1 / 3, 0.4, 0.5, 0.6, 0.75, 1.0)
29
+
30
+
31
+ def load_features(directory):
32
+ shards = [torch.load(path, map_location="cpu", weights_only=False) for path in sorted(directory.glob("features_rank_*.pt"))]
33
+ if not shards:
34
+ raise FileNotFoundError(f"No feature shards in {directory}")
35
+ names = ("pooled_hidden", "log_residual", "true_c0", "true_c1", "true_c2", "scheduler_coeff", "step_id")
36
+ result = {name: torch.cat([shard[name] for shard in shards]) for name in names}
37
+ result["metadata"] = sum((shard["metadata"] for shard in shards), [])
38
+ order = sorted(range(len(result["metadata"])), key=lambda i: tuple(result["metadata"][i][k] for k in ("case_id", "action_id", "chunk_id", "step_id")))
39
+ index = torch.tensor(order)
40
+ for name in names:
41
+ result[name] = result[name][index]
42
+ result["metadata"] = [result["metadata"][i] for i in order]
43
+ return result
44
+
45
+
46
+ def train_head(
47
+ features, target_name, device, epochs, batch_size, seed, rank, world_size,
48
+ mlp_dims,
49
+ ):
50
+ torch.manual_seed(seed)
51
+ model = PredictorConfidenceHead(
52
+ features["pooled_hidden"].shape[1], mlp_dims=mlp_dims
53
+ ).to(device)
54
+ optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)
55
+ dataset = TensorDataset(
56
+ features["pooled_hidden"].float(), features["step_id"].long(),
57
+ features["log_residual"].float(),
58
+ torch.log(features[f"true_{target_name}"].float() + 1e-8),
59
+ )
60
+ sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank, shuffle=True, seed=seed) if world_size > 1 else None
61
+ loader = DataLoader(dataset, batch_size=batch_size, shuffle=sampler is None, sampler=sampler)
62
+ wrapped = DDP(model, device_ids=[device.index], broadcast_buffers=False) if world_size > 1 else model
63
+ wrapped.train()
64
+ for epoch in range(epochs):
65
+ if sampler is not None:
66
+ sampler.set_epoch(epoch)
67
+ total = count = 0
68
+ for pooled, step, residual, target in loader:
69
+ pooled, step, residual, target = (x.to(device) for x in (pooled, step, residual, target))
70
+ optimizer.zero_grad(set_to_none=True)
71
+ loss = F.smooth_l1_loss(wrapped(pooled, step, residual), target)
72
+ loss.backward()
73
+ optimizer.step()
74
+ total += float(loss.detach()) * len(pooled)
75
+ count += len(pooled)
76
+ if rank == 0 and (epoch == 0 or (epoch + 1) % 10 == 0):
77
+ print(f"{target_name} epoch {epoch + 1}/{epochs} loss={total / count:.6f}", flush=True)
78
+ if world_size > 1:
79
+ dist.barrier()
80
+ return model.eval()
81
+
82
+
83
+ @torch.inference_mode()
84
+ def predict(model, features, device):
85
+ result = []
86
+ for start in range(0, len(features["step_id"]), 512):
87
+ stop = start + 512
88
+ result.append(model(
89
+ features["pooled_hidden"][start:stop].float().to(device),
90
+ features["step_id"][start:stop].long().to(device),
91
+ features["log_residual"][start:stop].float().to(device),
92
+ ).cpu())
93
+ return torch.cat(result).numpy()
94
+
95
+
96
+ def correlation(x, y, function):
97
+ if len(x) < 2 or np.all(x == x[0]) or np.all(y == y[0]):
98
+ return float("nan")
99
+ return float(function(x, y).statistic)
100
+
101
+
102
+ def metric_rows(split, predictions, features):
103
+ rows = []
104
+ steps = features["step_id"].numpy()
105
+ for target, score in predictions.items():
106
+ truth = np.log(features[f"true_{target}"].numpy() + 1e-8)
107
+ groups = [("overall", np.ones(len(steps), dtype=bool))] + [(f"P{s}", steps == s) for s in (1, 2, 3)]
108
+ for label, mask in groups:
109
+ rows.append({
110
+ "model": f"Confidence {target.upper()}", "split": split, "step": label,
111
+ "spearman": correlation(score[mask], truth[mask], spearmanr),
112
+ "pearson": correlation(score[mask], truth[mask], pearsonr),
113
+ "log_mae": float(np.abs(score[mask] - truth[mask]).mean()),
114
+ })
115
+ return rows
116
+
117
+
118
+ def remaining_risk(risk, score, ratio):
119
+ count = min(len(risk), int(round(ratio * len(risk))))
120
+ if count == 0:
121
+ return float(risk.mean())
122
+ selected = np.argpartition(score, len(score) - count)[-count:]
123
+ keep = np.ones(len(risk), dtype=bool)
124
+ keep[selected] = False
125
+ return float(np.where(keep, risk, 0.0).mean())
126
+
127
+
128
+ def risk_rows(train, validation, predictions, seed, risk_target):
129
+ risk = validation[f"true_{risk_target}"].numpy()
130
+ train_steps = train["step_id"].numpy()
131
+ train_risk = train[f"true_{risk_target}"].numpy()
132
+ means = {step: train_risk[train_steps == step].mean() for step in (1, 2, 3)}
133
+ methods = {
134
+ "Timestep-only": np.array([means[int(s)] for s in validation["step_id"]]),
135
+ **{
136
+ f"Confidence {target.upper()}": score
137
+ for target, score in predictions.items()
138
+ },
139
+ "Oracle": risk,
140
+ }
141
+ rows = []
142
+ rng = np.random.default_rng(seed)
143
+ for ratio in RATIOS:
144
+ count = min(len(risk), int(round(ratio * len(risk))))
145
+ random_risks = []
146
+ for _ in range(100):
147
+ chosen = rng.choice(len(risk), count, replace=False) if count else []
148
+ keep = np.ones(len(risk), dtype=bool)
149
+ keep[chosen] = False
150
+ random_risks.append(float(np.where(keep, risk, 0.0).mean()))
151
+ rows.append({"method": "Random", "fallback_ratio": ratio, "remaining_risk": np.mean(random_risks), "std_if_random": np.std(random_risks)})
152
+ for method, score in methods.items():
153
+ rows.append({"method": method, "fallback_ratio": ratio, "remaining_risk": remaining_risk(risk, score, ratio), "std_if_random": ""})
154
+ return rows
155
+
156
+
157
+ def write_csv(path, rows):
158
+ path.parent.mkdir(parents=True, exist_ok=True)
159
+ with path.open("w", newline="", encoding="utf-8") as handle:
160
+ writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
161
+ writer.writeheader()
162
+ writer.writerows(rows)
163
+
164
+
165
+ def make_plots(validation, risks, predictions, output, risk_target):
166
+ output.mkdir(parents=True, exist_ok=True)
167
+ plt.figure(figsize=(7.2, 5.2))
168
+ confidence_method = f"Confidence {risk_target.upper()}"
169
+ for method in ("Random", "Timestep-only", confidence_method, "Oracle"):
170
+ rows = [row for row in risks if row["method"] == method]
171
+ plt.plot([100 * row["fallback_ratio"] for row in rows], [row["remaining_risk"] for row in rows], marker="o", label=method)
172
+ plt.xlabel("Fallback ratio (%)")
173
+ plt.ylabel(f"Remaining {risk_target.upper()} risk")
174
+ plt.legend()
175
+ plt.tight_layout()
176
+ plt.savefig(output / "risk_coverage.png", dpi=180)
177
+ plt.close()
178
+ scores = predictions[risk_target]
179
+ count = int(round(len(scores) / 3))
180
+ chosen = np.argpartition(scores, len(scores) - count)[-count:]
181
+ steps = validation["step_id"].numpy()[chosen]
182
+ plt.figure(figsize=(5.5, 4.4))
183
+ plt.bar(("P1", "P2", "P3"), [float((steps == s).mean()) for s in (1, 2, 3)])
184
+ plt.ylabel("Fraction of selected fallbacks")
185
+ plt.tight_layout()
186
+ plt.savefig(output / "fallback_distribution_33pct.png", dpi=180)
187
+ plt.close()
188
+
189
+
190
+ def main():
191
+ parser = argparse.ArgumentParser(description=__doc__)
192
+ parser.add_argument("--train_features", type=Path, required=True)
193
+ parser.add_argument("--validation_features", type=Path, required=True)
194
+ parser.add_argument("--output_dir", type=Path, required=True)
195
+ parser.add_argument("--epochs", type=int, default=40)
196
+ parser.add_argument("--batch_size", type=int, default=256)
197
+ parser.add_argument("--seed", type=int, default=0)
198
+ parser.add_argument("--targets", nargs="+", choices=TARGETS, default=list(TARGETS))
199
+ parser.add_argument("--risk_target", choices=TARGETS, default="c1")
200
+ parser.add_argument("--mlp_dims", nargs="+", type=int, default=[256])
201
+ args = parser.parse_args()
202
+ if args.risk_target not in args.targets:
203
+ parser.error("--risk_target must be included in --targets")
204
+ if any(dim <= 0 for dim in args.mlp_dims):
205
+ parser.error("--mlp_dims values must be positive")
206
+ targets = tuple(dict.fromkeys(args.targets))
207
+ mlp_dims = tuple(args.mlp_dims)
208
+ random.seed(args.seed); np.random.seed(args.seed); torch.manual_seed(args.seed)
209
+ world_size = int(os.environ.get("WORLD_SIZE", 1)); rank = int(os.environ.get("RANK", 0)); local_rank = int(os.environ.get("LOCAL_RANK", 0))
210
+ if world_size > 1:
211
+ torch.cuda.set_device(local_rank); dist.init_process_group("nccl")
212
+ device = torch.device("cuda", local_rank) if torch.cuda.is_available() else torch.device("cpu")
213
+ train = load_features(args.train_features); validation = load_features(args.validation_features)
214
+ if rank == 0:
215
+ for child in ("checkpoints", "plots", "predictions", "tables"):
216
+ (args.output_dir / child).mkdir(parents=True, exist_ok=True)
217
+ if world_size > 1: dist.barrier()
218
+ train_predictions = {}; validation_predictions = {}
219
+ for target in targets:
220
+ model = train_head(
221
+ train, target, device, args.epochs, args.batch_size, args.seed,
222
+ rank, world_size, mlp_dims,
223
+ )
224
+ if rank == 0:
225
+ torch.save(
226
+ {
227
+ "model": model.state_dict(),
228
+ "target": target,
229
+ "hidden_dim": train["pooled_hidden"].shape[1],
230
+ "timestep_dim": 32,
231
+ "mlp_dims": list(mlp_dims),
232
+ },
233
+ args.output_dir / "checkpoints" / f"confidence_{target}.pt",
234
+ )
235
+ train_predictions[target] = predict(model, train, device)
236
+ validation_predictions[target] = predict(model, validation, device)
237
+ if world_size > 1: dist.barrier()
238
+ if rank != 0:
239
+ dist.destroy_process_group(); return
240
+ metrics = metric_rows("train", train_predictions, train) + metric_rows("validation", validation_predictions, validation)
241
+ risks = risk_rows(
242
+ train, validation, validation_predictions, args.seed, args.risk_target
243
+ )
244
+ write_csv(args.output_dir / "tables" / "confidence_metrics.csv", metrics)
245
+ write_csv(args.output_dir / "tables" / "risk_coverage.csv", risks)
246
+ prediction_rows = [{**item, **{f"pred_{target}": float(validation_predictions[target][i]) for target in targets}, **{f"true_{target}": float(validation[f"true_{target}"][i]) for target in targets}} for i, item in enumerate(validation["metadata"])]
247
+ write_csv(args.output_dir / "predictions" / "validation_predictions.csv", prediction_rows)
248
+ make_plots(
249
+ validation, risks, validation_predictions, args.output_dir / "plots",
250
+ args.risk_target,
251
+ )
252
+ at_33 = {row["method"]: float(row["remaining_risk"]) for row in risks if math.isclose(row["fallback_ratio"], 1 / 3)}
253
+ confidence_method = f"Confidence {args.risk_target.upper()}"
254
+ target_metrics = {row["step"]: row for row in metrics if row["split"] == "validation" and row["model"] == confidence_method}
255
+ scores = validation_predictions[args.risk_target]; count = int(round(len(scores) / 3)); chosen = np.argpartition(scores, len(scores) - count)[-count:]
256
+ chosen_mask = np.zeros(len(scores), dtype=bool); chosen_mask[chosen] = True; steps = validation["step_id"].numpy()
257
+ summary = {
258
+ "protocol": "WorldPlay fulltrain100 (8400 transitions); independent validation25 (2100 transitions); no validation tuning",
259
+ "targets": list(targets), "risk_target": args.risk_target,
260
+ "mlp_dims": list(mlp_dims),
261
+ "train_samples": len(train["step_id"]), "validation_samples": len(validation["step_id"]),
262
+ "risk_target_validation_metrics": target_metrics,
263
+ "remaining_risk_at_33pct": at_33,
264
+ "gain_vs_random": (at_33["Random"] - at_33[confidence_method]) / at_33["Random"],
265
+ "oracle_gap": (at_33[confidence_method] - at_33["Oracle"]) / max(at_33["Oracle"], 1e-12),
266
+ "routing_at_33pct": {f"P{s}": {"selection_fraction": float((steps[chosen_mask] == s).mean()), "within_step_fallback_rate": float(chosen_mask[steps == s].mean())} for s in (1, 2, 3)},
267
+ }
268
+ if args.risk_target == "c1":
269
+ summary["c1_validation_metrics"] = target_metrics
270
+ (args.output_dir / "summary.json").write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n")
271
+ (args.output_dir / "summary.md").write_text(
272
+ "# HY-WorldPlay offline confidence validation\n\n" + summary["protocol"] + "\n\n" +
273
+ f"## {args.risk_target.upper()} validation Spearman\n\n" + "\n".join(f"- {key}: {value['spearman']:.4f}" for key, value in target_metrics.items()) +
274
+ f"\n\n## 33.3% fallback remaining {args.risk_target.upper()} risk\n\n" + "\n".join(f"- {key}: {value:.8g}" for key, value in at_33.items()) + "\n"
275
+ )
276
+ print(json.dumps(summary, indent=2), flush=True)
277
+ if world_size > 1: dist.destroy_process_group()
278
+
279
+
280
+ if __name__ == "__main__":
281
+ main()
tools/validate_predictor_dataset.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Validate Predictor safetensors files without loading the Teacher."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+
10
+ from safetensors.torch import load_file
11
+
12
+ from predictor_data.schema import SCHEMA_VERSION, validate_case_tensors, validate_chunk_tensors
13
+
14
+
15
+ def main() -> None:
16
+ parser = argparse.ArgumentParser(description=__doc__)
17
+ parser.add_argument("--root", default="datasets/predictor_v1")
18
+ parser.add_argument("--expected_cases", type=int, default=None)
19
+ parser.add_argument("--expected_chunks_per_case", type=int, default=None)
20
+ args = parser.parse_args()
21
+
22
+ root = Path(args.root).resolve()
23
+ manifest = root / "manifest.jsonl"
24
+ if not manifest.is_file():
25
+ raise FileNotFoundError(manifest)
26
+ records = [json.loads(line) for line in manifest.read_text(encoding="utf-8").splitlines() if line]
27
+ if len({(r["case_id"], r["seed"], r["chunk_id"]) for r in records}) != len(records):
28
+ raise ValueError("Duplicate manifest keys")
29
+
30
+ seen_cases = set()
31
+ per_case: dict[int, int] = {}
32
+ for record in records:
33
+ if record["schema_version"] != SCHEMA_VERSION:
34
+ raise ValueError(f"Unexpected schema: {record['schema_version']}")
35
+ case_id = int(record["case_id"])
36
+ if case_id not in seen_cases:
37
+ validate_case_tensors(load_file(str(root / record["case_tensor_file"]), device="cpu"))
38
+ seen_cases.add(case_id)
39
+ validate_chunk_tensors(load_file(str(root / record["tensor_file"]), device="cpu"))
40
+ per_case[case_id] = per_case.get(case_id, 0) + 1
41
+
42
+ if args.expected_cases is not None and len(seen_cases) != args.expected_cases:
43
+ raise ValueError(f"Expected {args.expected_cases} cases, found {len(seen_cases)}")
44
+ if args.expected_chunks_per_case is not None:
45
+ bad = {case: count for case, count in per_case.items() if count != args.expected_chunks_per_case}
46
+ if bad:
47
+ raise ValueError(f"Unexpected chunks per case: {bad}")
48
+ print(json.dumps({"records": len(records), "cases": len(seen_cases), "chunks_per_case": per_case}, indent=2))
49
+
50
+
51
+ if __name__ == "__main__":
52
+ main()
tools/validate_predictor_prefeature_dataset.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Validate BF16 prefeature files, targets, task cardinality, and videos."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from collections import Counter
9
+ from pathlib import Path
10
+
11
+ from safetensors.torch import load_file
12
+
13
+ from predictor_data.prefeature_schema import (
14
+ CONTEXT_BLOCK_IDS,
15
+ SCHEMA_VERSION_PREFEATURE,
16
+ validate_case_tensors,
17
+ validate_context_prefeature,
18
+ validate_step_tensors,
19
+ )
20
+
21
+
22
+ def main() -> None:
23
+ parser = argparse.ArgumentParser(description=__doc__)
24
+ parser.add_argument(
25
+ "--manifest",
26
+ type=Path,
27
+ default=Path("/mnt/local_nvme/zoubin/cz/hyworldplay_predictor_prefeature_v4/manifest.jsonl"),
28
+ )
29
+ parser.add_argument("--expected_records", type=int, default=3200)
30
+ parser.add_argument("--check_videos", action=argparse.BooleanOptionalAction, default=True)
31
+ args = parser.parse_args()
32
+ manifest = args.manifest.resolve()
33
+ root = manifest.parent
34
+ with manifest.open("r", encoding="utf-8") as handle:
35
+ records = [json.loads(line) for line in handle if line.strip()]
36
+ if len(records) != args.expected_records:
37
+ raise ValueError(f"Found {len(records)} records, expected {args.expected_records}")
38
+ keys = Counter()
39
+ cases_checked = set()
40
+ frame_histogram = Counter()
41
+ for record in records:
42
+ if record["schema_version"] != SCHEMA_VERSION_PREFEATURE:
43
+ raise ValueError("Schema mismatch")
44
+ key = (int(record["case_id"]), int(record["action_id"]), int(record["chunk_id"]))
45
+ keys[key] += 1
46
+ if key[0] not in cases_checked:
47
+ validate_case_tensors(load_file(str(root / record["case_tensor_file"]), device="cpu"))
48
+ cases_checked.add(key[0])
49
+ validate_step_tensors(load_file(str(root / record["step_tensor_file"]), device="cpu"))
50
+ block_frames = []
51
+ selected = None
52
+ for block_id in CONTEXT_BLOCK_IDS:
53
+ value = load_file(str(root / record["context_tensor_files"][str(block_id)]), device="cpu")
54
+ block_frames.append(validate_context_prefeature(block_id, value))
55
+ current = value["selected_frame_indices"]
56
+ if selected is not None and not current.equal(selected):
57
+ raise ValueError(f"Selected frame mismatch across blocks for {key}")
58
+ selected = current
59
+ if len(set(block_frames)) != 1 or block_frames[0] != int(record["context_frames"]):
60
+ raise ValueError(f"Context frame mismatch for {key}")
61
+ frame_histogram[block_frames[0]] += 1
62
+ duplicates = [key for key, count in keys.items() if count != 1]
63
+ if duplicates:
64
+ raise ValueError(f"Duplicate keys: {duplicates[:10]}")
65
+ if args.check_videos:
66
+ missing_videos = [
67
+ (case_id, action_id)
68
+ for case_id in range(100) for action_id in range(4)
69
+ if not (root / "videos" / f"case_{case_id:04d}_action_{action_id:02d}.mp4").is_file()
70
+ ]
71
+ if missing_videos:
72
+ raise ValueError(f"Missing videos: {missing_videos[:10]}")
73
+ print(json.dumps({
74
+ "status": "ok", "records": len(records), "cases": len(cases_checked),
75
+ "frame_histogram": dict(sorted(frame_histogram.items())),
76
+ }, indent=2))
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
tools/validate_predictor_text_kv_all54.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Validate cardinality, schema, shape, dtype, and provenance of Text K/V cache files."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from pathlib import Path
9
+
10
+ from safetensors import safe_open
11
+ from safetensors.torch import load_file
12
+
13
+ from predictor_data.text_kv_schema import (
14
+ SCHEMA_VERSION_TEXT_KV_ALL54,
15
+ validate_text_kv_all54,
16
+ )
17
+
18
+
19
+ def main() -> None:
20
+ parser = argparse.ArgumentParser(description=__doc__)
21
+ parser.add_argument("--root", type=Path, required=True)
22
+ parser.add_argument("--expected_cases", type=int, default=100)
23
+ args = parser.parse_args()
24
+ root = args.root.resolve()
25
+ files = sorted(root.glob("case_*_text_kv_exact_all54_bf16.safetensors"))
26
+ if len(files) != args.expected_cases:
27
+ raise ValueError(f"Expected {args.expected_cases} files, found {len(files)}")
28
+ total_bytes = 0
29
+ for case_id, path in enumerate(files):
30
+ expected_name = f"case_{case_id:04d}_text_kv_exact_all54_bf16.safetensors"
31
+ if path.name != expected_name:
32
+ raise ValueError(f"Expected {expected_name}, found {path.name}")
33
+ with safe_open(str(path), framework="pt", device="cpu") as handle:
34
+ metadata = handle.metadata() or {}
35
+ if metadata.get("schema_version") != SCHEMA_VERSION_TEXT_KV_ALL54:
36
+ raise ValueError(f"Schema mismatch: {path}")
37
+ if metadata.get("case_id") != str(case_id):
38
+ raise ValueError(f"Case metadata mismatch: {path}")
39
+ validate_text_kv_all54(load_file(str(path), device="cpu"))
40
+ total_bytes += path.stat().st_size
41
+ print(f"[ok] {case_id + 1}/{len(files)} {path.name}", flush=True)
42
+ print(
43
+ json.dumps(
44
+ {
45
+ "status": "ok",
46
+ "schema_version": SCHEMA_VERSION_TEXT_KV_ALL54,
47
+ "cases": len(files),
48
+ "total_bytes": total_bytes,
49
+ "total_gib": total_bytes / 2**30,
50
+ },
51
+ indent=2,
52
+ sort_keys=True,
53
+ )
54
+ )
55
+
56
+
57
+ if __name__ == "__main__":
58
+ main()
tools/validate_predictor_v2_dataset.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Validate Predictor-v2 manifests, BF16 targets, and four-layer context KV."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from collections import Counter, defaultdict
9
+ from pathlib import Path
10
+
11
+ from safetensors.torch import load_file
12
+
13
+ from predictor_data.v2_schema import (
14
+ CONTEXT_BLOCK_IDS,
15
+ SCHEMA_VERSION_V2,
16
+ validate_case_tensors_v2,
17
+ validate_step_tensors_v2,
18
+ validate_vision_context_v2,
19
+ )
20
+
21
+
22
+ def parse_args() -> argparse.Namespace:
23
+ parser = argparse.ArgumentParser(description=__doc__)
24
+ parser.add_argument("--manifest", default="datasets/predictor_v2/manifest.jsonl")
25
+ parser.add_argument("--expected_cases", type=int, default=10)
26
+ parser.add_argument("--expected_chunks_per_case", type=int, default=16)
27
+ return parser.parse_args()
28
+
29
+
30
+ def main() -> None:
31
+ args = parse_args()
32
+ manifest = Path(args.manifest).resolve()
33
+ root = manifest.parent
34
+ with manifest.open("r", encoding="utf-8") as handle:
35
+ records = [json.loads(line) for line in handle if line.strip()]
36
+ if len(records) != args.expected_cases * args.expected_chunks_per_case:
37
+ raise ValueError(f"Unexpected record count: {len(records)}")
38
+
39
+ counts = Counter()
40
+ seen_cases = set()
41
+ frame_histogram: dict[int, Counter] = defaultdict(Counter)
42
+ text_padded_tokens = set()
43
+ text_valid_tokens: dict[int, int] = {}
44
+ context_padded_tokens = set()
45
+ for record in records:
46
+ if record["schema_version"] != SCHEMA_VERSION_V2:
47
+ raise ValueError(f"Unexpected schema: {record['schema_version']}")
48
+ key = (int(record["case_id"]), int(record["seed"]), int(record["chunk_id"]))
49
+ counts[key] += 1
50
+ case_id = key[0]
51
+ if case_id not in seen_cases:
52
+ case = load_file(str(root / record["case_tensor_file"]), device="cpu")
53
+ text_valid_tokens[case_id] = validate_case_tensors_v2(case)
54
+ text_padded_tokens.add(int(case["text_valid_mask"].shape[1]))
55
+ seen_cases.add(case_id)
56
+ steps = load_file(str(root / record["step_tensor_file"]), device="cpu")
57
+ validate_step_tensors_v2(steps)
58
+ frame_counts = []
59
+ for block_id in CONTEXT_BLOCK_IDS:
60
+ context = load_file(
61
+ str(root / record["context_tensor_files"][str(block_id)]),
62
+ device="cpu",
63
+ )
64
+ frames = validate_vision_context_v2(block_id, context)
65
+ context_padded_tokens.add(int(context["context_valid_mask"].shape[1]))
66
+ frame_counts.append(frames)
67
+ frame_histogram[block_id][frames] += 1
68
+ if len(set(frame_counts)) != 1 or frame_counts[0] != int(record["context_frames"]):
69
+ raise ValueError(f"Context frame mismatch for {key}: {frame_counts}")
70
+ duplicates = [key for key, count in counts.items() if count != 1]
71
+ if duplicates:
72
+ raise ValueError(f"Duplicate manifest keys: {duplicates[:5]}")
73
+ if len(text_padded_tokens) != 1:
74
+ raise ValueError(f"Text KV is not uniformly padded: {sorted(text_padded_tokens)}")
75
+ if len(context_padded_tokens) != 1:
76
+ raise ValueError(
77
+ f"Context Vision KV is not uniformly padded: {sorted(context_padded_tokens)}"
78
+ )
79
+ payload = {
80
+ "root": str(root),
81
+ "records": len(records),
82
+ "cases": len(seen_cases),
83
+ "context_blocks": list(CONTEXT_BLOCK_IDS),
84
+ "text_padded_tokens": next(iter(text_padded_tokens)),
85
+ "text_valid_tokens": dict(sorted(text_valid_tokens.items())),
86
+ "context_padded_tokens": next(iter(context_padded_tokens)),
87
+ "context_frame_histogram": {
88
+ str(block_id): dict(sorted(counter.items()))
89
+ for block_id, counter in frame_histogram.items()
90
+ },
91
+ "status": "ok",
92
+ }
93
+ print(json.dumps(payload, indent=2, sort_keys=True))
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
tools/video_review_server.py ADDED
@@ -0,0 +1,1031 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """A zero-dependency video review and download server."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import mimetypes
9
+ import os
10
+ import re
11
+ import secrets
12
+ import threading
13
+ import time
14
+ from http import HTTPStatus
15
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
16
+ from pathlib import Path, PurePosixPath
17
+ from urllib.parse import parse_qs, quote, urlparse
18
+
19
+
20
+ VIDEO_EXTENSIONS = {".mp4", ".webm", ".mov", ".mkv", ".avi", ".m4v"}
21
+ CHUNK_SIZE = 256 * 1024
22
+ DEFAULT_REFRESH_INTERVAL = 30.0
23
+ DEFAULT_SETTLE_SECONDS = 3.0
24
+
25
+ INDEX_HTML = r"""<!doctype html>
26
+ <html lang="zh-CN">
27
+ <head>
28
+ <meta charset="utf-8">
29
+ <meta name="viewport" content="width=device-width,initial-scale=1">
30
+ <title>Projects 视频审阅</title>
31
+ <style>
32
+ :root {
33
+ color-scheme: dark;
34
+ --bg: #0b0d12; --panel: #121620; --line: #262c3a;
35
+ --text: #e8ecf5; --muted: #8d96a8; --accent: #7c9cff;
36
+ }
37
+ * { box-sizing: border-box; }
38
+ body { margin: 0; background: var(--bg); color: var(--text);
39
+ font: 14px/1.45 ui-sans-serif, system-ui, -apple-system, sans-serif; }
40
+ .app { height: 100vh; display: grid;
41
+ grid-template-columns: var(--sidebar-width, 360px) 6px minmax(0, 1fr); }
42
+ aside { min-height: 0; display: flex; flex-direction: column; border-right: 1px solid var(--line);
43
+ background: var(--panel); }
44
+ .resize-handle { position: relative; z-index: 10; background: var(--line); cursor: col-resize;
45
+ touch-action: none; transition: background .15s; }
46
+ .resize-handle::after { content: ""; position: absolute; inset: 0 -4px; }
47
+ .resize-handle:hover, .resize-handle:focus, .resize-handle.dragging {
48
+ background: var(--accent); outline: none; }
49
+ header { padding: 18px; border-bottom: 1px solid var(--line); }
50
+ h1 { margin: 0 0 5px; font-size: 16px; }
51
+ .summary, .hint, .meta { color: var(--muted); font-size: 12px; }
52
+ .catalog-line { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
53
+ .catalog-refresh { flex: none; padding: 3px 7px; border: 1px solid var(--line);
54
+ border-radius: 5px; background: #171c28; color: #cbd2e2; cursor: pointer; font-size: 11px; }
55
+ .catalog-refresh:hover { border-color: var(--accent); color: white; }
56
+ .catalog-refresh:disabled { cursor: wait; opacity: .65; }
57
+ input { width: 100%; margin-top: 14px; padding: 10px 12px; color: var(--text);
58
+ background: #0c1018; border: 1px solid var(--line); border-radius: 8px; outline: none; }
59
+ input:focus { border-color: var(--accent); }
60
+ #tree { overflow: auto; padding: 10px 8px 24px; }
61
+ details details { margin-left: 14px; }
62
+ summary { cursor: pointer; padding: 5px 7px; color: #b8c0d1; white-space: nowrap; }
63
+ .file { width: 100%; border: 0; border-radius: 6px; background: transparent; color: #b7becc;
64
+ padding: 6px 8px 6px 25px; text-align: left; cursor: grab; white-space: nowrap;
65
+ overflow: hidden; text-overflow: ellipsis; }
66
+ .file:active { cursor: grabbing; }
67
+ .file:hover { background: #1b2130; color: white; }
68
+ .file.active { background: #253052; color: white; }
69
+ .search-result { display: block; }
70
+ main { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
71
+ .topbar { min-height: 66px; display: flex; justify-content: space-between; gap: 16px;
72
+ align-items: center; padding: 12px 20px; border-bottom: 1px solid var(--line); }
73
+ .path { min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
74
+ .counter { flex: none; color: var(--muted); }
75
+ .workspace-actions { flex: none; display: flex; align-items: center; gap: 7px; }
76
+ .toolbar-button, .pane-download, .pane-close { border: 1px solid var(--line); border-radius: 6px;
77
+ background: #171c28; color: #cbd2e2; cursor: pointer; }
78
+ .toolbar-button { padding: 6px 10px; }
79
+ .toolbar-button:hover, .pane-download:hover, .pane-close:hover {
80
+ border-color: var(--accent); color: white; }
81
+ .toolbar-button.active { border-color: var(--accent); background: #253052; color: white; }
82
+ .stage { min-height: 0; flex: 1; display: grid;
83
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr));
84
+ grid-auto-rows: minmax(280px, 1fr); align-content: stretch; gap: 8px; padding: 8px;
85
+ overflow: auto;
86
+ background: radial-gradient(circle at 50% 40%, #171c28, #090b0f 65%); }
87
+ .stage.layout-horizontal { grid-template-columns:
88
+ repeat(var(--pane-count, 1), minmax(360px, 1fr)); grid-template-rows: 1fr;
89
+ grid-auto-rows: 1fr; }
90
+ .stage.layout-vertical { grid-template-columns: 1fr; grid-template-rows:
91
+ repeat(var(--pane-count, 1), minmax(280px, 1fr)); grid-auto-rows: minmax(280px, 1fr); }
92
+ .stage.drop-target { box-shadow: inset 0 0 0 2px var(--accent);
93
+ background: #11182a; }
94
+ .pane { min-width: 0; min-height: 280px; display: flex; flex-direction: column;
95
+ border: 1px solid var(--line); background: #090b10; overflow: hidden; }
96
+ .pane.active { border-color: var(--accent); box-shadow: 0 0 0 1px #7c9cff55; }
97
+ .pane-header { min-height: 38px; display: flex; align-items: center; gap: 9px;
98
+ padding: 6px 7px 6px 11px; border-bottom: 1px solid var(--line); background: #151a25; }
99
+ .pane-heading { min-width: 0; flex: 1; }
100
+ .pane-title, .pane-path { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
101
+ .pane-title { font-size: 12px; font-weight: 650; }
102
+ .pane-path { color: var(--muted); font-size: 10px; }
103
+ .pane-download { padding: 4px 9px; text-decoration: none; font-size: 12px; white-space: nowrap; }
104
+ .pane-close { width: 26px; height: 26px; padding: 0; font-size: 17px; }
105
+ .pane video { display: block; width: 100%; min-height: 0; flex: 1; object-fit: contain;
106
+ background: #000; }
107
+ .empty { grid-column: 1 / -1; align-self: center; justify-self: center;
108
+ text-align: center; color: var(--muted); pointer-events: none; }
109
+ .empty strong { display: block; color: var(--text); font-size: 18px; margin-bottom: 8px; }
110
+ .status { min-height: 38px; padding: 10px 20px; border-top: 1px solid var(--line);
111
+ display: flex; justify-content: space-between; color: var(--muted); font-size: 12px; }
112
+ @media (max-width: 760px) {
113
+ .app { grid-template-columns: 1fr; grid-template-rows: 44vh 56vh; }
114
+ .resize-handle { display: none; }
115
+ aside { border-right: 0; border-bottom: 1px solid var(--line); }
116
+ .toolbar-button { padding: 5px 7px; font-size: 11px; }
117
+ .stage { grid-template-columns: 1fr; grid-auto-rows: minmax(240px, 1fr); }
118
+ }
119
+ </style>
120
+ </head>
121
+ <body>
122
+ <div class="app">
123
+ <aside>
124
+ <header>
125
+ <h1>Projects 视频审阅</h1>
126
+ <div class="catalog-line">
127
+ <div class="summary" id="summary">正在读取目录…</div>
128
+ <button class="catalog-refresh" id="refreshCatalog" type="button">立即刷新</button>
129
+ </div>
130
+ <input id="search" type="search" placeholder="按路径或文件名筛选…" autocomplete="off">
131
+ </header>
132
+ <div id="tree"></div>
133
+ </aside>
134
+ <div class="resize-handle" id="resizeHandle" role="separator" aria-label="调整文件栏宽度"
135
+ aria-orientation="vertical" tabindex="0" title="拖动调整宽度,双击恢复默认"></div>
136
+ <main>
137
+ <div class="topbar">
138
+ <div>
139
+ <div class="path" id="currentPath">尚未选择视频</div>
140
+ <div class="meta" id="currentMeta">点击文件在当前窗口打开,或拖入工作区新增窗口</div>
141
+ </div>
142
+ <div class="workspace-actions">
143
+ <button class="toolbar-button layout-button active" data-layout="grid" type="button"
144
+ title="根据空间自动排列">网格</button>
145
+ <button class="toolbar-button layout-button" data-layout="horizontal" type="button"
146
+ title="视频左右排列">左右</button>
147
+ <button class="toolbar-button layout-button" data-layout="vertical" type="button"
148
+ title="视频上下排列">上下</button>
149
+ <button class="toolbar-button" id="playAll" type="button">全部播放</button>
150
+ <button class="toolbar-button" id="pauseAll" type="button">全部暂停</button>
151
+ <button class="toolbar-button" id="resetAll" type="button">全部回零</button>
152
+ <div class="counter" id="counter">0 个窗口</div>
153
+ </div>
154
+ </div>
155
+ <div class="stage" id="workspace">
156
+ <div class="empty" id="empty">
157
+ <strong>拖入视频开始多窗口比较</strong>
158
+ 点击文件使用单窗口;拖动多个文件到这里可并排查看。
159
+ </div>
160
+ </div>
161
+ <div class="status">
162
+ <span id="status">就绪</span>
163
+ <span>拖入新增窗口 · 点击替换活动窗口 · J/K/L 控制活动窗口</span>
164
+ </div>
165
+ </main>
166
+ </div>
167
+ <script>
168
+ const treeEl = document.querySelector('#tree');
169
+ const workspace = document.querySelector('#workspace');
170
+ const empty = document.querySelector('#empty');
171
+ const statusEl = document.querySelector('#status');
172
+ const searchEl = document.querySelector('#search');
173
+ const appEl = document.querySelector('.app');
174
+ const resizeHandle = document.querySelector('#resizeHandle');
175
+ const summaryEl = document.querySelector('#summary');
176
+ const refreshCatalogButton = document.querySelector('#refreshCatalog');
177
+ let videos = [];
178
+ let catalogRevision = 0;
179
+ let catalogPolling = false;
180
+ const panes = new Map();
181
+ let activePaneId = null;
182
+
183
+ function setSidebarWidth(value) {
184
+ const maxWidth = Math.min(1000, window.innerWidth * 0.72);
185
+ const width = Math.max(260, Math.min(maxWidth, value));
186
+ appEl.style.setProperty('--sidebar-width', `${width}px`);
187
+ localStorage.setItem('hy-video-review-sidebar-width', String(width));
188
+ }
189
+
190
+ const savedSidebarWidth = Number(localStorage.getItem('hy-video-review-sidebar-width'));
191
+ if (savedSidebarWidth) setSidebarWidth(savedSidebarWidth);
192
+
193
+ let resizing = false;
194
+ resizeHandle.addEventListener('pointerdown', event => {
195
+ resizing = true;
196
+ resizeHandle.classList.add('dragging');
197
+ resizeHandle.setPointerCapture(event.pointerId);
198
+ document.body.style.userSelect = 'none';
199
+ });
200
+ resizeHandle.addEventListener('pointermove', event => {
201
+ if (resizing) setSidebarWidth(event.clientX);
202
+ });
203
+ resizeHandle.addEventListener('pointerup', event => {
204
+ resizing = false;
205
+ resizeHandle.classList.remove('dragging');
206
+ resizeHandle.releasePointerCapture(event.pointerId);
207
+ document.body.style.userSelect = '';
208
+ });
209
+ resizeHandle.addEventListener('dblclick', () => setSidebarWidth(360));
210
+ resizeHandle.addEventListener('keydown', event => {
211
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
212
+ event.preventDefault();
213
+ const current = document.querySelector('aside').getBoundingClientRect().width;
214
+ setSidebarWidth(current + (event.key === 'ArrowRight' ? 24 : -24));
215
+ });
216
+
217
+ function setWorkspaceLayout(layout) {
218
+ if (!['grid', 'horizontal', 'vertical'].includes(layout)) layout = 'grid';
219
+ workspace.classList.remove('layout-horizontal', 'layout-vertical');
220
+ if (layout !== 'grid') workspace.classList.add(`layout-${layout}`);
221
+ document.querySelectorAll('.layout-button').forEach(button => {
222
+ button.classList.toggle('active', button.dataset.layout === layout);
223
+ });
224
+ localStorage.setItem('hy-video-review-layout', layout);
225
+ }
226
+
227
+ document.querySelectorAll('.layout-button').forEach(button => {
228
+ button.addEventListener('click', () => setWorkspaceLayout(button.dataset.layout));
229
+ });
230
+ setWorkspaceLayout(localStorage.getItem('hy-video-review-layout') || 'grid');
231
+
232
+ const formatBytes = n => {
233
+ const units = ['B', 'KiB', 'MiB', 'GiB'];
234
+ let i = 0, value = n;
235
+ while (value >= 1024 && i < units.length - 1) { value /= 1024; i++; }
236
+ return `${value.toFixed(i ? 1 : 0)} ${units[i]}`;
237
+ };
238
+
239
+ function makeTree(items) {
240
+ const root = {dirs: new Map(), files: []};
241
+ for (const item of items) {
242
+ const parts = item.path.split('/');
243
+ const file = parts.pop();
244
+ let node = root;
245
+ for (const part of parts) {
246
+ if (!node.dirs.has(part)) node.dirs.set(part, {dirs: new Map(), files: []});
247
+ node = node.dirs.get(part);
248
+ }
249
+ node.files.push({...item, name: file});
250
+ }
251
+ return root;
252
+ }
253
+
254
+ function fileButton(item, extraClass = '') {
255
+ const button = document.createElement('button');
256
+ button.className = `file ${extraClass}`;
257
+ button.dataset.path = item.path;
258
+ button.title = `${item.path}\n${formatBytes(item.size)}`;
259
+ button.textContent = item.name || item.path;
260
+ button.draggable = true;
261
+ button.addEventListener('click', () => selectVideo(item.path));
262
+ button.addEventListener('dragstart', event => {
263
+ event.dataTransfer.effectAllowed = 'copy';
264
+ event.dataTransfer.setData('application/x-hy-video-path', item.path);
265
+ event.dataTransfer.setData('text/plain', item.path);
266
+ });
267
+ return button;
268
+ }
269
+
270
+ function renderNode(node, container) {
271
+ const dirs = [...node.dirs].sort((a, b) => a[0].localeCompare(b[0], undefined, {numeric:true}));
272
+ for (const [name, child] of dirs) {
273
+ const details = document.createElement('details');
274
+ const summary = document.createElement('summary');
275
+ summary.textContent = `📁 ${name}`;
276
+ details.append(summary);
277
+ renderNode(child, details);
278
+ container.append(details);
279
+ }
280
+ node.files.sort((a, b) => a.name.localeCompare(b.name, undefined, {numeric:true}));
281
+ for (const item of node.files) container.append(fileButton(item));
282
+ }
283
+
284
+ function render(filter = '') {
285
+ treeEl.replaceChildren();
286
+ const query = filter.trim().toLowerCase();
287
+ if (query) {
288
+ const matches = videos.filter(v => v.path.toLowerCase().includes(query)).slice(0, 500);
289
+ for (const item of matches) {
290
+ const button = fileButton(item, 'search-result');
291
+ button.textContent = item.path;
292
+ treeEl.append(button);
293
+ }
294
+ if (!matches.length) treeEl.textContent = '没有匹配的视频';
295
+ } else {
296
+ renderNode(makeTree(videos), treeEl);
297
+ }
298
+ markActive();
299
+ }
300
+
301
+ function markActive() {
302
+ document.querySelectorAll('.file.active').forEach(el => el.classList.remove('active'));
303
+ const pane = panes.get(activePaneId);
304
+ if (pane?.path) {
305
+ document.querySelectorAll('.file').forEach(el => {
306
+ if (el.dataset.path === pane.path) el.classList.add('active');
307
+ });
308
+ }
309
+ }
310
+
311
+ function paneId() {
312
+ return `pane-${globalThis.crypto?.randomUUID?.() ||
313
+ `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
314
+ }
315
+
316
+ function createPane() {
317
+ const id = paneId();
318
+ const element = document.createElement('article');
319
+ element.className = 'pane';
320
+ element.dataset.slot = id;
321
+
322
+ const header = document.createElement('div');
323
+ header.className = 'pane-header';
324
+ const heading = document.createElement('div');
325
+ heading.className = 'pane-heading';
326
+ const title = document.createElement('div');
327
+ title.className = 'pane-title';
328
+ title.textContent = '正在准���…';
329
+ const pathLabel = document.createElement('div');
330
+ pathLabel.className = 'pane-path';
331
+ const download = document.createElement('a');
332
+ download.className = 'pane-download';
333
+ download.textContent = '下载';
334
+ download.title = '从公网下载这个视频';
335
+ download.hidden = true;
336
+ const close = document.createElement('button');
337
+ close.className = 'pane-close';
338
+ close.type = 'button';
339
+ close.title = '关闭窗口并释放带宽';
340
+ close.setAttribute('aria-label', '关闭视频窗口');
341
+ close.textContent = '×';
342
+ const video = document.createElement('video');
343
+ video.controls = true;
344
+ video.playsInline = true;
345
+ video.preload = 'metadata';
346
+
347
+ heading.append(title, pathLabel);
348
+ header.append(heading, download, close);
349
+ element.append(header, video);
350
+ workspace.append(element);
351
+
352
+ const pane = {id, element, title, pathLabel, download, video, token: '', path: '', serial: 0};
353
+ panes.set(id, pane);
354
+ element.addEventListener('pointerdown', () => setActivePane(id));
355
+ close.addEventListener('click', event => {
356
+ event.stopPropagation();
357
+ closePane(id);
358
+ });
359
+ video.addEventListener('error', () => {
360
+ if (video.error) statusEl.textContent =
361
+ `${title.textContent} 无法播放(错误 ${video.error.code})`;
362
+ });
363
+ video.addEventListener('playing', () => {
364
+ statusEl.textContent = `${panes.size} 个窗口 · 正在播放`;
365
+ });
366
+ setActivePane(id);
367
+ updateWorkspaceUI();
368
+ return pane;
369
+ }
370
+
371
+ function setActivePane(id) {
372
+ if (!panes.has(id)) return;
373
+ activePaneId = id;
374
+ panes.forEach(pane => pane.element.classList.toggle('active', pane.id === id));
375
+ updateWorkspaceUI();
376
+ }
377
+
378
+ function releaseToken(slot, token) {
379
+ if (!token) return;
380
+ fetch('/api/release', {
381
+ method: 'POST',
382
+ headers: {'Content-Type': 'application/json'},
383
+ body: JSON.stringify({slot, token}),
384
+ keepalive: true
385
+ }).catch(() => {});
386
+ }
387
+
388
+ function detachMedia(pane) {
389
+ pane.video.pause();
390
+ pane.video.removeAttribute('src');
391
+ pane.video.load();
392
+ if (pane.token) releaseToken(pane.id, pane.token);
393
+ pane.token = '';
394
+ }
395
+
396
+ function closePane(id) {
397
+ const pane = panes.get(id);
398
+ if (!pane) return;
399
+ pane.serial++;
400
+ detachMedia(pane);
401
+ pane.element.remove();
402
+ panes.delete(id);
403
+ if (activePaneId === id) activePaneId = panes.keys().next().value || null;
404
+ if (activePaneId) panes.get(activePaneId).element.classList.add('active');
405
+ updateWorkspaceUI();
406
+ statusEl.textContent = panes.size ? `剩余 ${panes.size} 个窗口` : '所有视频带宽已释放';
407
+ }
408
+
409
+ function updateWorkspaceUI() {
410
+ empty.style.display = panes.size ? 'none' : '';
411
+ workspace.style.setProperty('--pane-count', String(Math.max(1, panes.size)));
412
+ document.querySelector('#counter').textContent = `${panes.size} 个窗口`;
413
+ const pane = panes.get(activePaneId);
414
+ document.querySelector('#currentPath').textContent = pane?.path || '尚未选择视频';
415
+ document.querySelector('#currentMeta').textContent = pane?.path
416
+ ? `${formatBytes(videos.find(item => item.path === pane.path)?.size || 0)} · 活动窗口`
417
+ : '点击文件在当前窗口打开,或拖入工作区新增窗口';
418
+ markActive();
419
+ }
420
+
421
+ async function loadIntoPane(pane, path) {
422
+ const index = videos.findIndex(v => v.path === path);
423
+ if (index < 0) return;
424
+ const serial = ++pane.serial;
425
+ const item = videos[index];
426
+
427
+ detachMedia(pane);
428
+ pane.path = path;
429
+ pane.title.textContent = path.split('/').pop();
430
+ pane.pathLabel.textContent = path;
431
+ pane.pathLabel.title = path;
432
+ pane.download.href = `/download?path=${encodeURIComponent(path)}`;
433
+ pane.download.setAttribute('download', path.split('/').pop());
434
+ pane.download.hidden = false;
435
+ setActivePane(pane.id);
436
+ statusEl.textContent = '正在申请窗口带宽…';
437
+
438
+ try {
439
+ const response = await fetch('/api/select', {
440
+ method: 'POST',
441
+ headers: {'Content-Type': 'application/json'},
442
+ body: JSON.stringify({path, slot: pane.id})
443
+ });
444
+ if (!response.ok) throw new Error(await response.text());
445
+ const {token} = await response.json();
446
+ if (!panes.has(pane.id) || serial !== pane.serial) {
447
+ releaseToken(pane.id, token);
448
+ return;
449
+ }
450
+
451
+ pane.token = token;
452
+ pane.video.src = `/media?path=${encodeURIComponent(path)}&slot=${encodeURIComponent(pane.id)}` +
453
+ `&token=${encodeURIComponent(token)}`;
454
+ pane.video.load();
455
+ pane.video.play().catch(() => {});
456
+ statusEl.textContent = `${panes.size} 个窗口占用视频带宽`;
457
+ updateWorkspaceUI();
458
+ } catch (error) {
459
+ if (serial !== pane.serial) return;
460
+ statusEl.textContent = `加载失败:${error.message}`;
461
+ }
462
+ }
463
+
464
+ function selectVideo(path) {
465
+ const pane = panes.get(activePaneId) || createPane();
466
+ loadIntoPane(pane, path);
467
+ }
468
+
469
+ function addVideoPane(path) {
470
+ loadIntoPane(createPane(), path);
471
+ }
472
+
473
+ workspace.addEventListener('dragover', event => {
474
+ event.preventDefault();
475
+ event.dataTransfer.dropEffect = 'copy';
476
+ workspace.classList.add('drop-target');
477
+ });
478
+ workspace.addEventListener('dragleave', event => {
479
+ if (!workspace.contains(event.relatedTarget)) workspace.classList.remove('drop-target');
480
+ });
481
+ workspace.addEventListener('drop', event => {
482
+ event.preventDefault();
483
+ workspace.classList.remove('drop-target');
484
+ const path = event.dataTransfer.getData('application/x-hy-video-path') ||
485
+ event.dataTransfer.getData('text/plain');
486
+ if (videos.some(item => item.path === path)) addVideoPane(path);
487
+ });
488
+
489
+ document.querySelector('#playAll').addEventListener('click', () => {
490
+ panes.forEach(pane => pane.video.play().catch(() => {}));
491
+ });
492
+ document.querySelector('#pauseAll').addEventListener('click', () => {
493
+ panes.forEach(pane => pane.video.pause());
494
+ statusEl.textContent = `已暂停 ${panes.size} 个窗口`;
495
+ });
496
+ document.querySelector('#resetAll').addEventListener('click', () => {
497
+ panes.forEach(pane => { pane.video.currentTime = 0; });
498
+ statusEl.textContent = `已将 ${panes.size} 个窗口回零`;
499
+ });
500
+
501
+ searchEl.addEventListener('input', () => render(searchEl.value));
502
+ document.addEventListener('keydown', event => {
503
+ if (event.target === searchEl) return;
504
+ const pane = panes.get(activePaneId);
505
+ if (!pane) return;
506
+ const index = videos.findIndex(item => item.path === pane.path);
507
+ if (event.key.toLowerCase() === 'j' && index > 0) selectVideo(videos[index - 1].path);
508
+ if (event.key.toLowerCase() === 'l' && index + 1 < videos.length) selectVideo(videos[index + 1].path);
509
+ if (event.key.toLowerCase() === 'k') {
510
+ event.preventDefault();
511
+ if (pane.video.src) pane.video.paused ? pane.video.play() : pane.video.pause();
512
+ }
513
+ });
514
+ window.addEventListener('beforeunload', () => {
515
+ panes.forEach(pane => {
516
+ if (!pane.token) return;
517
+ navigator.sendBeacon('/api/release', new Blob(
518
+ [JSON.stringify({slot: pane.id, token: pane.token})],
519
+ {type: 'application/json'}
520
+ ));
521
+ });
522
+ });
523
+
524
+ function updateCatalogSummary(data) {
525
+ const count = data.video_count ?? videos.length;
526
+ const suffix = data.refreshing ? ' · 正在扫描新视频…' : ' · 自动更新';
527
+ summaryEl.textContent =
528
+ `${count.toLocaleString()} 个视频 · ${formatBytes(data.total_size || 0)}${suffix}`;
529
+ refreshCatalogButton.disabled = Boolean(data.refreshing);
530
+ refreshCatalogButton.textContent = data.refreshing ? '扫描中…' : '立即刷新';
531
+ }
532
+
533
+ function applyCatalog(data, announce = false) {
534
+ const previousCount = videos.length;
535
+ videos = data.videos;
536
+ catalogRevision = data.revision;
537
+ updateCatalogSummary(data);
538
+ render(searchEl.value);
539
+ updateWorkspaceUI();
540
+ if (announce && videos.length !== previousCount) {
541
+ const difference = videos.length - previousCount;
542
+ statusEl.textContent = difference > 0
543
+ ? `目录已自动更新:新增 ${difference} 个视频`
544
+ : `目录已自动更新:减少 ${Math.abs(difference)} 个视频`;
545
+ }
546
+ }
547
+
548
+ async function fetchCatalog(announce = false) {
549
+ const response = await fetch('/api/videos', {cache: 'no-store'});
550
+ if (!response.ok) throw new Error(await response.text());
551
+ applyCatalog(await response.json(), announce);
552
+ }
553
+
554
+ async function pollCatalogStatus() {
555
+ if (catalogPolling) return;
556
+ catalogPolling = true;
557
+ try {
558
+ const response = await fetch('/api/status', {cache: 'no-store'});
559
+ if (!response.ok) throw new Error(await response.text());
560
+ const data = await response.json();
561
+ updateCatalogSummary(data);
562
+ if (data.revision !== catalogRevision) await fetchCatalog(true);
563
+ } catch (error) {
564
+ summaryEl.textContent = `自动更新检查失败:${error.message}`;
565
+ } finally {
566
+ catalogPolling = false;
567
+ }
568
+ }
569
+
570
+ refreshCatalogButton.addEventListener('click', async () => {
571
+ refreshCatalogButton.disabled = true;
572
+ refreshCatalogButton.textContent = '扫描中…';
573
+ statusEl.textContent = '正在后台扫描新视频,已打开的窗口不会中断';
574
+ try {
575
+ const response = await fetch('/api/refresh', {method: 'POST'});
576
+ if (!response.ok) throw new Error(await response.text());
577
+ updateCatalogSummary(await response.json());
578
+ } catch (error) {
579
+ statusEl.textContent = `刷新失败:${error.message}`;
580
+ refreshCatalogButton.disabled = false;
581
+ refreshCatalogButton.textContent = '立即刷新';
582
+ }
583
+ });
584
+
585
+ fetchCatalog().catch(error => {
586
+ summaryEl.textContent = `目录读取失败:${error.message}`;
587
+ });
588
+ setInterval(pollCatalogStatus, 5000);
589
+ </script>
590
+ </body>
591
+ </html>
592
+ """
593
+
594
+
595
+ class ReviewState:
596
+ def __init__(
597
+ self,
598
+ root: Path,
599
+ refresh_interval: float = DEFAULT_REFRESH_INTERVAL,
600
+ settle_seconds: float = DEFAULT_SETTLE_SECONDS,
601
+ ):
602
+ self.root = root.resolve()
603
+ self.refresh_interval = max(0.0, refresh_interval)
604
+ self.settle_seconds = max(0.0, settle_seconds)
605
+ self.lease_lock = threading.Lock()
606
+ self.catalog_lock = threading.Lock()
607
+ self.refresh_guard = threading.Lock()
608
+ self.leases: dict[str, tuple[str, str]] = {}
609
+ self.refreshing = False
610
+ self.last_scan_started = time.time()
611
+ initial = self._scan_raw()
612
+ self.videos = [self._public_item(item) for item in initial]
613
+ self.paths = {str(item["path"]) for item in self.videos}
614
+ self.signatures = {
615
+ str(item["path"]): (int(item["size"]), int(item["mtime_ns"]))
616
+ for item in initial
617
+ }
618
+ self.total_size = sum(int(item["size"]) for item in self.videos)
619
+ self.revision = 1
620
+ self.last_scan_finished = time.time()
621
+ if self.refresh_interval > 0:
622
+ threading.Thread(
623
+ target=self._auto_refresh_loop,
624
+ name="video-catalog-auto-refresh",
625
+ daemon=True,
626
+ ).start()
627
+
628
+ @staticmethod
629
+ def _public_item(item: dict[str, object]) -> dict[str, object]:
630
+ return {"path": str(item["path"]), "size": int(item["size"])}
631
+
632
+ def _scan_raw(self) -> list[dict[str, object]]:
633
+ videos = []
634
+ for directory, dirnames, filenames in os.walk(self.root):
635
+ dirnames.sort()
636
+ for filename in sorted(filenames):
637
+ path = Path(directory, filename)
638
+ if path.suffix.lower() not in VIDEO_EXTENSIONS:
639
+ continue
640
+ try:
641
+ stat = path.stat()
642
+ relative = path.relative_to(self.root).as_posix()
643
+ except (OSError, ValueError):
644
+ continue
645
+ videos.append(
646
+ {"path": relative, "size": stat.st_size, "mtime_ns": stat.st_mtime_ns}
647
+ )
648
+ videos.sort(key=lambda item: str(item["path"]))
649
+ return videos
650
+
651
+ def _stable_after_delay(
652
+ self, items: list[dict[str, object]]
653
+ ) -> set[str]:
654
+ if not items:
655
+ return set()
656
+ if self.settle_seconds:
657
+ time.sleep(self.settle_seconds)
658
+ stable = set()
659
+ for item in items:
660
+ relative = str(item["path"])
661
+ path = self.root / PurePosixPath(relative)
662
+ try:
663
+ stat = path.stat()
664
+ except OSError:
665
+ continue
666
+ if (
667
+ stat.st_size == int(item["size"])
668
+ and stat.st_mtime_ns == int(item["mtime_ns"])
669
+ ):
670
+ stable.add(relative)
671
+ return stable
672
+
673
+ def _refresh_worker(self) -> None:
674
+ try:
675
+ with self.catalog_lock:
676
+ self.refreshing = True
677
+ self.last_scan_started = time.time()
678
+ old_items = {str(item["path"]): item for item in self.videos}
679
+ old_signatures = dict(self.signatures)
680
+
681
+ scanned = self._scan_raw()
682
+ changed = [
683
+ item
684
+ for item in scanned
685
+ if old_signatures.get(str(item["path"]))
686
+ != (int(item["size"]), int(item["mtime_ns"]))
687
+ ]
688
+ stable_changed = self._stable_after_delay(changed)
689
+
690
+ accepted = []
691
+ accepted_signatures: dict[str, tuple[int, int]] = {}
692
+ for item in scanned:
693
+ relative = str(item["path"])
694
+ signature = (int(item["size"]), int(item["mtime_ns"]))
695
+ if old_signatures.get(relative) == signature or relative in stable_changed:
696
+ accepted.append(self._public_item(item))
697
+ accepted_signatures[relative] = signature
698
+ elif relative in old_items:
699
+ accepted.append(old_items[relative])
700
+ accepted_signatures[relative] = old_signatures[relative]
701
+
702
+ accepted.sort(key=lambda item: str(item["path"]))
703
+ with self.catalog_lock:
704
+ catalog_changed = accepted != self.videos
705
+ self.videos = accepted
706
+ self.paths = {str(item["path"]) for item in accepted}
707
+ self.signatures = accepted_signatures
708
+ self.total_size = sum(int(item["size"]) for item in accepted)
709
+ if catalog_changed:
710
+ self.revision += 1
711
+ self.last_scan_finished = time.time()
712
+ print(
713
+ f"Catalog refresh complete: {len(self.videos)} videos "
714
+ f"(revision {self.revision})",
715
+ flush=True,
716
+ )
717
+ except Exception as error:
718
+ print(f"Catalog refresh failed: {error}", flush=True)
719
+ finally:
720
+ with self.catalog_lock:
721
+ self.refreshing = False
722
+ self.last_scan_finished = time.time()
723
+ self.refresh_guard.release()
724
+
725
+ def request_refresh(self) -> bool:
726
+ if not self.refresh_guard.acquire(blocking=False):
727
+ return False
728
+ with self.catalog_lock:
729
+ self.refreshing = True
730
+ self.last_scan_started = time.time()
731
+ threading.Thread(
732
+ target=self._refresh_worker,
733
+ name="video-catalog-refresh",
734
+ daemon=True,
735
+ ).start()
736
+ return True
737
+
738
+ def _auto_refresh_loop(self) -> None:
739
+ while True:
740
+ time.sleep(self.refresh_interval)
741
+ self.request_refresh()
742
+
743
+ def status(self) -> dict[str, object]:
744
+ with self.catalog_lock:
745
+ return {
746
+ "revision": self.revision,
747
+ "video_count": len(self.videos),
748
+ "total_size": self.total_size,
749
+ "refreshing": self.refreshing,
750
+ "last_scan_started": self.last_scan_started,
751
+ "last_scan_finished": self.last_scan_finished,
752
+ "refresh_interval": self.refresh_interval,
753
+ }
754
+
755
+ def catalog(self) -> dict[str, object]:
756
+ with self.catalog_lock:
757
+ result = {
758
+ "videos": list(self.videos),
759
+ "revision": self.revision,
760
+ "video_count": len(self.videos),
761
+ "total_size": self.total_size,
762
+ "refreshing": self.refreshing,
763
+ "last_scan_started": self.last_scan_started,
764
+ "last_scan_finished": self.last_scan_finished,
765
+ "refresh_interval": self.refresh_interval,
766
+ }
767
+ return result
768
+
769
+ def resolve_video(self, relative: str) -> Path | None:
770
+ normalized = PurePosixPath(relative).as_posix()
771
+ with self.catalog_lock:
772
+ known = normalized in self.paths
773
+ if not known:
774
+ return None
775
+ candidate = (self.root / normalized).resolve()
776
+ try:
777
+ candidate.relative_to(self.root)
778
+ except ValueError:
779
+ return None
780
+ return candidate if candidate.is_file() else None
781
+
782
+ def select(self, relative: str, slot: str) -> str:
783
+ token = secrets.token_urlsafe(24)
784
+ with self.lease_lock:
785
+ if slot not in self.leases and len(self.leases) >= 256:
786
+ self.leases.pop(next(iter(self.leases)))
787
+ self.leases[slot] = (relative, token)
788
+ return token
789
+
790
+ def release(self, slot: str, token: str) -> bool:
791
+ with self.lease_lock:
792
+ lease = self.leases.get(slot)
793
+ if lease is None or not secrets.compare_digest(token, lease[1]):
794
+ return False
795
+ del self.leases[slot]
796
+ return True
797
+
798
+ def is_active(self, relative: str, slot: str, token: str) -> bool:
799
+ with self.lease_lock:
800
+ lease = self.leases.get(slot)
801
+ return (
802
+ lease is not None
803
+ and relative == lease[0]
804
+ and secrets.compare_digest(token, lease[1])
805
+ )
806
+
807
+
808
+ class VideoReviewHandler(BaseHTTPRequestHandler):
809
+ server_version = "HYVideoReview/1.0"
810
+
811
+ @property
812
+ def state(self) -> ReviewState:
813
+ return self.server.review_state # type: ignore[attr-defined]
814
+
815
+ def log_message(self, fmt: str, *args: object) -> None:
816
+ print(f"{self.address_string()} - {fmt % args}", flush=True)
817
+
818
+ def send_bytes(self, data: bytes, content_type: str, status: int = 200) -> None:
819
+ self.send_response(status)
820
+ self.send_header("Content-Type", content_type)
821
+ self.send_header("Content-Length", str(len(data)))
822
+ self.send_header("Cache-Control", "no-store")
823
+ self.send_header("X-Content-Type-Options", "nosniff")
824
+ self.end_headers()
825
+ if self.command != "HEAD":
826
+ self.wfile.write(data)
827
+
828
+ def send_json(self, value: object, status: int = 200) -> None:
829
+ self.send_bytes(
830
+ json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode(),
831
+ "application/json; charset=utf-8",
832
+ status,
833
+ )
834
+
835
+ def do_GET(self) -> None:
836
+ parsed = urlparse(self.path)
837
+ if parsed.path == "/":
838
+ self.send_bytes(INDEX_HTML.encode(), "text/html; charset=utf-8")
839
+ elif parsed.path == "/api/videos":
840
+ self.send_json(self.state.catalog())
841
+ elif parsed.path == "/api/status":
842
+ self.send_json(self.state.status())
843
+ elif parsed.path == "/media":
844
+ self.serve_media(parsed.query)
845
+ elif parsed.path == "/download":
846
+ self.serve_download(parsed.query)
847
+ elif parsed.path == "/healthz":
848
+ self.send_json({"ok": True, **self.state.status()})
849
+ else:
850
+ self.send_error(HTTPStatus.NOT_FOUND)
851
+
852
+ def do_HEAD(self) -> None:
853
+ parsed = urlparse(self.path)
854
+ if parsed.path == "/media":
855
+ self.serve_media(parsed.query)
856
+ elif parsed.path == "/download":
857
+ self.serve_download(parsed.query)
858
+ elif parsed.path == "/healthz":
859
+ self.send_json({"ok": True, "videos": len(self.state.videos)})
860
+ else:
861
+ self.send_error(HTTPStatus.NOT_FOUND)
862
+
863
+ def do_POST(self) -> None:
864
+ endpoint = urlparse(self.path).path
865
+ if endpoint == "/api/refresh":
866
+ started = self.state.request_refresh()
867
+ self.send_json({"started": started, **self.state.status()}, HTTPStatus.ACCEPTED)
868
+ return
869
+ if endpoint not in {"/api/select", "/api/release"}:
870
+ self.send_error(HTTPStatus.NOT_FOUND)
871
+ return
872
+ try:
873
+ length = int(self.headers.get("Content-Length", "0"))
874
+ if length > 64 * 1024:
875
+ raise ValueError("request too large")
876
+ payload = json.loads(self.rfile.read(length))
877
+ slot = str(payload["slot"])
878
+ if not slot or len(slot) > 128:
879
+ raise ValueError("invalid slot")
880
+ except (ValueError, KeyError, TypeError, json.JSONDecodeError):
881
+ self.send_json({"error": "invalid request"}, HTTPStatus.BAD_REQUEST)
882
+ return
883
+
884
+ if endpoint == "/api/release":
885
+ try:
886
+ token = str(payload["token"])
887
+ except (KeyError, TypeError):
888
+ self.send_json({"error": "invalid request"}, HTTPStatus.BAD_REQUEST)
889
+ return
890
+ self.send_json({"released": self.state.release(slot, token)})
891
+ return
892
+
893
+ try:
894
+ relative = str(payload["path"])
895
+ except (KeyError, TypeError):
896
+ self.send_json({"error": "invalid request"}, HTTPStatus.BAD_REQUEST)
897
+ return
898
+ if self.state.resolve_video(relative) is None:
899
+ self.send_json({"error": "video not found"}, HTTPStatus.NOT_FOUND)
900
+ return
901
+ self.send_json({"token": self.state.select(relative, slot)})
902
+
903
+ def serve_media(self, query: str) -> None:
904
+ params = parse_qs(query)
905
+ relative = params.get("path", [""])[0]
906
+ slot = params.get("slot", [""])[0]
907
+ token = params.get("token", [""])[0]
908
+ path = self.state.resolve_video(relative)
909
+ if path is None:
910
+ self.send_error(HTTPStatus.NOT_FOUND)
911
+ return
912
+ if not self.state.is_active(relative, slot, token):
913
+ self.send_error(HTTPStatus.GONE, "video lease expired")
914
+ return
915
+
916
+ size = path.stat().st_size
917
+ start, end = 0, size - 1
918
+ range_header = self.headers.get("Range")
919
+ if range_header:
920
+ match = re.fullmatch(r"bytes=(\d*)-(\d*)", range_header.strip())
921
+ if not match:
922
+ self.send_error(HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
923
+ return
924
+ first, last = match.groups()
925
+ if first:
926
+ start = int(first)
927
+ end = min(int(last), size - 1) if last else size - 1
928
+ elif last:
929
+ suffix = int(last)
930
+ start = max(0, size - suffix)
931
+ if start >= size or end < start:
932
+ self.send_response(HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
933
+ self.send_header("Content-Range", f"bytes */{size}")
934
+ self.end_headers()
935
+ return
936
+
937
+ length = end - start + 1
938
+ content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
939
+ self.send_response(HTTPStatus.PARTIAL_CONTENT if range_header else HTTPStatus.OK)
940
+ self.send_header("Content-Type", content_type)
941
+ self.send_header("Content-Length", str(length))
942
+ self.send_header("Accept-Ranges", "bytes")
943
+ self.send_header("Cache-Control", "private, no-store")
944
+ if range_header:
945
+ self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
946
+ self.end_headers()
947
+ if self.command == "HEAD":
948
+ return
949
+
950
+ remaining = length
951
+ try:
952
+ with path.open("rb") as source:
953
+ source.seek(start)
954
+ while remaining > 0 and self.state.is_active(relative, slot, token):
955
+ chunk = source.read(min(CHUNK_SIZE, remaining))
956
+ if not chunk:
957
+ break
958
+ self.wfile.write(chunk)
959
+ remaining -= len(chunk)
960
+ except (BrokenPipeError, ConnectionResetError):
961
+ pass
962
+
963
+ def serve_download(self, query: str) -> None:
964
+ relative = parse_qs(query).get("path", [""])[0]
965
+ path = self.state.resolve_video(relative)
966
+ if path is None:
967
+ self.send_error(HTTPStatus.NOT_FOUND)
968
+ return
969
+
970
+ size = path.stat().st_size
971
+ content_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
972
+ fallback = re.sub(r"[^A-Za-z0-9._-]", "_", path.name) or "video"
973
+ encoded = quote(path.name, safe="")
974
+ self.send_response(HTTPStatus.OK)
975
+ self.send_header("Content-Type", content_type)
976
+ self.send_header("Content-Length", str(size))
977
+ self.send_header(
978
+ "Content-Disposition",
979
+ f'attachment; filename="{fallback}"; filename*=UTF-8\'\'{encoded}',
980
+ )
981
+ self.send_header("Cache-Control", "private, no-store")
982
+ self.send_header("X-Content-Type-Options", "nosniff")
983
+ self.end_headers()
984
+ if self.command == "HEAD":
985
+ return
986
+
987
+ try:
988
+ with path.open("rb") as source:
989
+ while chunk := source.read(CHUNK_SIZE):
990
+ self.wfile.write(chunk)
991
+ except (BrokenPipeError, ConnectionResetError):
992
+ pass
993
+
994
+
995
+ class ReviewServer(ThreadingHTTPServer):
996
+ daemon_threads = True
997
+ allow_reuse_address = True
998
+
999
+ def __init__(self, address: tuple[str, int], state: ReviewState):
1000
+ super().__init__(address, VideoReviewHandler)
1001
+ self.review_state = state
1002
+
1003
+
1004
+ def main() -> None:
1005
+ parser = argparse.ArgumentParser(description=__doc__)
1006
+ parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
1007
+ parser.add_argument("--host", default="0.0.0.0")
1008
+ parser.add_argument("--port", type=int, default=8765)
1009
+ parser.add_argument("--refresh-interval", type=float, default=DEFAULT_REFRESH_INTERVAL)
1010
+ parser.add_argument("--settle-seconds", type=float, default=DEFAULT_SETTLE_SECONDS)
1011
+ args = parser.parse_args()
1012
+
1013
+ state = ReviewState(args.root, args.refresh_interval, args.settle_seconds)
1014
+ server = ReviewServer((args.host, args.port), state)
1015
+ print(
1016
+ f"Reviewing {len(state.videos)} videos under {state.root}\n"
1017
+ f"Listening on http://{args.host}:{args.port}\n"
1018
+ f"Auto-refresh every {state.refresh_interval:g}s; "
1019
+ f"new files settle for {state.settle_seconds:g}s",
1020
+ flush=True,
1021
+ )
1022
+ try:
1023
+ server.serve_forever()
1024
+ except KeyboardInterrupt:
1025
+ pass
1026
+ finally:
1027
+ server.server_close()
1028
+
1029
+
1030
+ if __name__ == "__main__":
1031
+ main()
train.md ADDED
@@ -0,0 +1,592 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Train
2
+
3
+ ## 实验命名规范
4
+
5
+ 从 2026-07-22 起,checkpoint 目录、独立日志、SwanLab experiment 和评测输出统一显式记录两组
6
+ 学习率、训练总步数与两种 warmup:
7
+
8
+ ```text
9
+ <experiment>_flr<fusion_lr>_blr<blocks_lr>_schedsteps<max_steps>_swarmup<scheduler_warmup>_bwarmup<block_warmup>
10
+ ```
11
+
12
+ - `flr`:image fusion/residual 参数组的峰值学习率;
13
+ - `blr`:两个 Predictor blocks 参数组的峰值学习率;
14
+ - `schedsteps`:创建 LR scheduler 时使用的训练总步数,不代表某次运行实际走到的步数;
15
+ - `swarmup`:全局 cosine scheduler 的 warmup steps;
16
+ - `bwarmup`:blocks 参数组额外冻结/延迟启用的 warmup steps;旧 rollout 没有该独立参数时写
17
+ `bwarmup-none`;
18
+ - `ckptstep`:仅用于评测输出,表示本次评测实际加载的 checkpoint step。
19
+
20
+ 例如,计划训练 2000 steps、实际使用 step 300 权重进行评测时,训练目录保留
21
+ `schedsteps2000`,评测目录同时写 `ckptstep300` 与 `schedsteps2000`。目录内的 `train_log.jsonl`、
22
+ `torchrun.log`、`train_config.json` 和 `swanlab/` 不重复拼接参数,它们继承父目录的完整命名。
23
+
24
+ 当前主要配置如下:
25
+
26
+ | 实验组 | `flr` | `blr` | `schedsteps` | `swarmup` | `bwarmup` |
27
+ | --- | ---: | ---: | ---: | ---: | ---: |
28
+ | v4 / predictor-disca 离线 Stage 1 | 1e-4 | 1e-5 | 2000 | 100 | 100 |
29
+ | 早期整轨迹 FPPP rollout | 1e-5 | 1e-6 | 2000 | 50 | none |
30
+ | overfit8 FPPP rollout | 1e-4 | 1e-5 | 2000 | 8 | 8 |
31
+ | overfit8 FPPF rollout | 1e-4 | 1e-5 | 300 | 8 | 8 |
32
+ | 旧 timestep-only sweep | 1e-4 | 1e-5 | 200 | 8 | 8 |
33
+ | detached timestep-only | 1e-4 | 1e-5 | 1000 | 8 | 8 |
34
+
35
+ ## 1. 监督目标
36
+
37
+ 当前 v4 数据为每个 chunk 保存四步 Full-DiT Teacher hidden 和 velocity。正式 v4 与
38
+ predictor-disca 使用全部相邻监督 pair:
39
+
40
+ ```text
41
+ step 0 final hidden + step 1 input -> step 1 hidden / velocity
42
+ step 1 final hidden + step 2 input -> step 2 hidden / velocity
43
+ step 2 final hidden + step 3 input -> step 3 hidden / velocity
44
+ ```
45
+
46
+ v1/v2/v3 的历史训练只使用 `0→1` 和 `2→3`,对应 F-P-F-P;从 v4 开始加入 `1→2`,使 Predictor
47
+ 具备连续替代和任意选择后三步的能力。
48
+
49
+ ## 2. Loss
50
+
51
+ 所有版本使用相同的监督损失:
52
+
53
+ ```python
54
+ hidden_loss = mse(pred_hidden.float(), target_hidden.float())
55
+ velocity_loss = mse(pred_velocity.float(), target_velocity.float())
56
+ loss = 0.1 * hidden_loss + 1.0 * velocity_loss
57
+ ```
58
+
59
+ - hidden 和 velocity 的 forward 使用 BF16;
60
+ - MSE 转为 FP32 计算;
61
+ - velocity MSE 是主目标;
62
+ - hidden MSE 是 final-layer 前 feature 的辅助约束;
63
+ - frozen final layer 只冻结参数,forward 不能放进 `no_grad()`,否则 velocity loss 无法回传到
64
+ Predictor hidden。
65
+
66
+ ## 3. 优化方法
67
+
68
+ 当前正式训练使用 AdamW:
69
+
70
+ | 参数组 | 学习率 |
71
+ | --- | ---: |
72
+ | image fusion + residual head | 1e-4 |
73
+ | 两个 Predictor blocks | 1e-5 |
74
+
75
+ 共同配置:
76
+
77
+ ```yaml
78
+ betas: [0.9, 0.95]
79
+ weight_decay: 0.01
80
+ grad_clip: 1.0
81
+ precision: bfloat16
82
+ loss_precision: float32
83
+ max_steps: 2000
84
+ scheduler: cosine
85
+ scheduler_warmup_steps: 100
86
+ block_warmup_steps: 100
87
+ gradient_checkpointing: true
88
+ ```
89
+
90
+ 前 100 optimizer steps 中 Predictor block 参数组的学习率为 0,只训练 fusion/residual;之后 block
91
+ 组按较低学习率参与训练。两个参数组都经过 100-step warmup 和 cosine decay,step 2000 衰减到 0。
92
+
93
+ ## 4. 各版本训练方式
94
+
95
+ ### v1
96
+
97
+ - 入口:`train_predictor.py`;
98
+ - 单 GPU、micro-batch 1;
99
+ - gradient accumulation 4,有效 batch 4;
100
+ - 2000 steps;
101
+ - pairs:`0→1`、`2→3`;
102
+ - source blocks:`[1,52]`;
103
+ - 每 500 steps 保存,默认保留 2 个快照。
104
+
105
+ 旧机器上的 v1 实验结果不再纳入当前文档,这里只保留训练方法。
106
+
107
+ ### v2/v3
108
+
109
+ - 入口:`train_predictor_v2.py --predictor_version v2|v3`;
110
+ - 单 GPU,默认 batch 1、gradient accumulation 4;
111
+ - pairs:`0→1`、`2→3`;
112
+ - source blocks:`[0,53]` 或 `[1,52]`;
113
+ - 2000 steps、100-step block warmup;
114
+ - SwanLab 可选。
115
+
116
+ v3 Dataset 额外加载前一 chunk 的 target-step final hidden,并排除 chunk 0。迁移后的当前目录未保留
117
+ v2/v3 正式 checkpoint,因此这里只记录方法,不将缺失产物写成可复现实验结果。
118
+
119
+ ### v4
120
+
121
+ - 入口:`train_predictor_v4.py`;
122
+ - 启动:`tools/launch_predictor_v4_training.sh`;
123
+ - 8 卡 DDP;
124
+ - 每卡 batch size 16,全局 batch size 128;
125
+ - gradient accumulation 1;
126
+ - 2800 records × 3 transitions = 8400 pairs;
127
+ - 按 Context frames 分桶,减少动态 padding;
128
+ - 2000 steps;
129
+ - 每 100 steps 保存,保留 20 个 safetensors;
130
+ - SwanLab 仅由 rank 0 上报。
131
+
132
+ 已训练 blocks `[0,53]` 和 `[1,52]` 两版。
133
+
134
+ ### predictor-disca
135
+
136
+ - 入口:`train_predictor_disca.py`;
137
+ - 启动:`tools/launch_predictor_disca_training.sh`;
138
+ - 8 卡 DDP、每卡 batch size 16、全局 batch size 128;
139
+ - 8400 pairs、2000 steps;
140
+ - 不加载 Context pre-feature 或 previous-chunk hidden;
141
+ - 不按 Context frames 分桶,使用普通 DistributedSampler;
142
+ - 每 100 steps 保存,保留 20 个 safetensors;
143
+ - 当前正式版使用 blocks `[0,53]`。
144
+
145
+ ## 5. 当前正式训练结果
146
+
147
+ 下表是训练日志在 step 2000 记录窗口内的均值,不是完整数据集 validation loss:
148
+
149
+ | 版本 | blocks | total loss | hidden MSE | velocity MSE | peak memory/GPU | avg micro time |
150
+ | --- | --- | ---: | ---: | ---: | ---: | ---: |
151
+ | v4 | `[0,53]` | 0.011895 | 0.011744 | 0.010721 | 62.82 GiB | 4.47 s |
152
+ | v4 | `[1,52]` | 0.011537 | 0.011732 | 0.010364 | 62.82 GiB | 4.49 s |
153
+ | predictor-disca | `[0,53]` | 0.011053 | 0.011121 | 0.009941 | 31.18 GiB | 1.66 s |
154
+
155
+ 训练 loss 不能直接替代 rollout 视频质量。predictor-disca 的训练 loss 更低,但其训练集视频指标明显
156
+ 低于 v4,说明缺失 AR Context 后更容易拟合单步监督,却在连续在线 rollout 中累积了更大误差。
157
+
158
+ ## 6. Checkpoint 与 resume
159
+
160
+ 正式训练同时保存:
161
+
162
+ ```text
163
+ predictor_step_XXXXX.safetensors # 仅模型推理权重
164
+ training_latest.pt # model、optimizer、scheduler、step、epoch、配置
165
+ train_config.json
166
+ train_log.jsonl
167
+ torchrun.log
168
+ swanlab/
169
+ ```
170
+
171
+ resume 会恢复 model、optimizer、scheduler、global/micro step 和 epoch,并检查 source blocks 与
172
+ Predictor variant 是否匹配。例如:
173
+
174
+ ```bash
175
+ python train_predictor_disca.py \
176
+ --resume checkpoints/predictor_disca_blocks0-53_bf16_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab/training_latest.pt
177
+ ```
178
+
179
+ 正式云端监控使用:
180
+
181
+ ```bash
182
+ export SWANLAB_API_KEY='<token>'
183
+ ```
184
+
185
+ token 只放在进程环境或用户登录缓存中,不写入仓库、配置或日志。
186
+
187
+ ## 7. 在线训练集评测
188
+
189
+ 统一评测 100 个训练 case × 4 actions,共 400 个 125-frame 视频:
190
+
191
+ ```text
192
+ reference: Full-DiT F-F-F-F
193
+ reuse: Full steps 0、3;steps 1、2 reuse
194
+ predictor: Full steps 0、3;steps 1、2 Predictor
195
+ ```
196
+
197
+ 视频指标包括 full-resolution PSNR、frame-mean PSNR、SSIM、AlexNet LPIPS、pixel
198
+ MSE/RMSE/MAE 和 temporal-delta MSE。
199
+
200
+ | 方法 | blocks | PSNR | SSIM | LPIPS | temporal delta MSE |
201
+ | --- | --- | ---: | ---: | ---: | ---: |
202
+ | Direct Reuse | — | 18.4639 | 0.6429 | 0.2083 | 0.01212 |
203
+ | v4 | `[0,53]` | 21.9337 | 0.7606 | 0.1204 | 0.00865 |
204
+ | v4 | `[1,52]` | 22.1090 | 0.7670 | 0.1168 | 0.00832 |
205
+ | predictor-disca | `[0,53]` | 19.4084 | 0.6748 | 0.1816 | 0.01061 |
206
+
207
+ 这些是 train-set/in-sample 指标,只能比较架构在已训练图文 pair 上的拟合和 rollout 行为,不能证明
208
+ 对新图、新 prompt、新 seed 或新动作的泛化。validation/test 应使用 `data.md` 中互斥的 25/50 pair
209
+ holdout。
210
+
211
+ ## 8. v4 递归 F-P-P-P Self-Rollout 微调方案
212
+
213
+ ### 8.1 定位与实现状态
214
+
215
+ 这是一阶段离线 Teacher-forcing 训练之后的二阶段 rollout 微调。初始化应加载已有 v4 Predictor
216
+ 权重,而不是重新从两个 Teacher blocks 开始训练。当前训练集视频结果中 `[1,52]` 最好,因此正式
217
+ 实验优先从以下离线 v4 FP32-master 训练状态中的 `model` 初始化;optimizer/scheduler 不继承,
218
+ rollout 使用全新的优化状态:
219
+
220
+ ```text
221
+ checkpoints/predictor_v4_prefeature_blocks1-52_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab/training_latest.pt
222
+ ```
223
+
224
+ `[0,53]` 可以用同一套流程作为独立对照,但 source blocks 必须与初始 checkpoint 一致。rollout
225
+ 训练已作为独立入口实现,不改变现有离线 `train_predictor_v4.py`:
226
+
227
+ ```text
228
+ train_predictor_v4_rollout.py
229
+ predictor_training/trajectory_dataset.py
230
+ predictor_training/rollout_utils.py
231
+ tools/launch_predictor_v4_rollout_training.sh
232
+ ```
233
+
234
+ 低学习率 `[1,52]` 基线输出到以下目录,并已按实验要求在 step 600 停止:
235
+
236
+ ```text
237
+ checkpoints/predictor_v4_rollout_fppp_blocks1-52_fp32master_8gpu_flr1e-5_blr1e-6_schedsteps2000_swarmup50_bwarmup-none_swanlab
238
+ ```
239
+
240
+ 使用第一阶段学习率的正式入口改用明确 LR 命名,避免用含义模糊的 `stage1lr`:
241
+
242
+ ```text
243
+ checkpoints/predictor_v4_rollout_fppp_blocks1-52_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab
244
+ ```
245
+
246
+ ### 8.2 目标轨迹
247
+
248
+ 每条 item 是一个完整的 `(case_id, action_id)` trajectory,而不是一个独立 transition:
249
+
250
+ ```text
251
+ Chunk 0:F-F-F-F,只建立 self-generated history 和 previous-chunk hidden,不训练
252
+ Chunk 1~7:F-P-P-P,三个 Predictor step 都计算 loss
253
+ ```
254
+
255
+ 在训练 chunk 内:
256
+
257
+ ```text
258
+ initial x0
259
+ -> Full step 0: h0, v0
260
+ -> Euler update: x1
261
+ -> Predictor step 1: pred_h1, pred_v1
262
+ -> differentiable Euler update: x2
263
+ -> Predictor step 2: pred_h2, pred_v2
264
+ -> differentiable Euler update: x3
265
+ -> Predictor step 3: pred_h3, pred_v3
266
+ -> differentiable Euler update: final chunk latent
267
+ ```
268
+
269
+ 同一 chunk 内 `pred_h1 → pred_h2 → pred_h3` 是递归 same-chunk anchor,三个 Predictor 调用保留
270
+ 一张完整计算��。chunk 之间则同时 detach:
271
+
272
+ - final chunk latent detach 后加入 history;
273
+ - `pred_h1/pred_h2/pred_h3` detach 后作为下一 chunk 的同 step previous-chunk hidden。
274
+
275
+ 因此,v4 的 previous-chunk 输入不能只用最终 latent 代替。Chunk 0 虽不训练,也必须保存 Full-DiT
276
+ step 1、2、3 的 final hidden,分别作为 Chunk 1 Predictor step 1、2、3 的 previous-chunk hidden。
277
+
278
+ ### 8.3 轻量样本表与全在线构建
279
+
280
+ Self-rollout 不依赖之前构建的 F-F-F-F tensor 数据集。Dataset 只保存一张轻量样本表,每个 item
281
+ 包含:
282
+
283
+ ```text
284
+ case_id, source_image, caption, action_id, pose, seed
285
+ ```
286
+
287
+ 100 个图文 pair × 4 actions 组成 400 条 trajectory。`__getitem__` 不读取 step tensor、Teacher
288
+ hidden/velocity、Context pre-feature 或 K/V;每个 rank 拿到样本描述后,使用当前正式 WorldPlay
289
+ pipeline 在线完成:
290
+
291
+ 1. 按与数据构建/评测相同的规则将输入图像 resize/crop 到 480×832;
292
+ 2. 使用 `pose_to_input(pose, 32)` 生成完整 8-chunk viewmats、Ks 和 action;
293
+ 3. 用指定 seed 调用 `prepare_latents`,一次生成 `[1,32,32,30,52]` 初始噪声,再按 4 latent
294
+ frames 切成 8 个 chunk;
295
+ 4. 从现有 case tensor 读取静态 `image_condition_latent`,在线构造 condition/mask,避免重复 VAE
296
+ encode;
297
+ 5. 从离线数据集的 `text_kv_exact_all54_bf16/` 读取全部 54 层精确 BF16 Text K/V,并按
298
+ `text_valid_mask` 去掉磁盘 padding;
299
+ 6. 从 Chunk 0 开始在线执行整条 F-F-F-F/F-P-P-P trajectory,并在线生成所有监督 target。
300
+
301
+ 每一步仍按当前 pipeline 语义构造 65 通道输入:32 通道 current noisy latent 加 32 通道 image
302
+ condition 和 1 通道 mask。只有 Chunk 0 的第一个 latent frame 含 image condition;Chunk 1~7 的
303
+ 33 个 condition 通道为零。history prefill 同样拼接对应历史帧的固定 condition,不能只把 32 通道
304
+ self latent 送入 Teacher。
305
+
306
+ 因此 rollout 训练不读取或生成持久化的 `step_1/2/3_noisy_sample`、Teacher target、Context
307
+ pre-feature、previous-step hidden 或全层 Text K/V sidecar。Text K/V 与 Predictor policy 和 trajectory
308
+ 状态无关,按 case 存在主离线数据集内;所有 policy-dependent 状态仍在当前参数、当前 self history 上
309
+ 产生,语义是严格 on-policy。图像/caption/action 样本表不是 Teacher trajectory 数据,可以直接从
310
+ 当前 100 个训练 pair 的来源清单生成;留出集不参与训练。
311
+
312
+ 静态 Text K/V 构建时来自 prompt、vision 和 ByT5 编码,训练主循环不再加载这三个编码器,也不再
313
+ 重复 text prefill。
314
+
315
+ ### 8.4 v4 Context 的准确语义
316
+
317
+ 每个 chunk 使用当前 detached、self-generated history 运行与正式推理相同的
318
+ `select_aligned_memory_frames`。选择结果只由相机轨迹决定;被选中的 latent 内容必须来自当前
319
+ self history。
320
+
321
+ 随后冻结 Full Teacher 对这些 history latent 执行一次 54-layer AR vision prefill,得到精确 Context
322
+ Vision K/V。该 cache:
323
+
324
+ - 与当前 trajectory 的全层 Text K/V 合并;
325
+ - 在当前 chunk 的四个 denoise steps 间只读共享;
326
+ - Teacher target 使用全部 54 层 cache;
327
+ - v4 Predictor 直接读取其 source blocks 对应的同一份 exact K/V;
328
+ - 全部 detach,且不对 Context 建图。
329
+
330
+ 这里不走 `context_prefeature -> frozen projector -> K/V` 路径。pre-feature 是离线 pair 训练的磁盘
331
+ 压缩方案;在线 rollout 已经由 Teacher prefill 持有 exact K/V,行为应与当前推理 pipeline 一致。
332
+
333
+ ### 8.5 单个 chunk 的 Teacher 与 Predictor 顺序
334
+
335
+ Chunk 0 在 `torch.no_grad()` 下执行四次 Full-DiT,并保存 final latent 和 Full hidden-by-step。
336
+ Chunk 1~7 的顺序固定为:
337
+
338
+ 1. 从 detached history 选择 Context,并由 Teacher 生成 exact AR K/V;
339
+ 2. Full Teacher 在 step 0 输入上产生 `h0/v0`,用正式 Euler scheduler 更新到 `x1`;
340
+ 3. 对 step 1~3,先在 `current_student_latent.detach()` 上运行 Full Teacher,获得
341
+ `target_hidden/target_velocity`;
342
+ 4. Predictor 使用相同 noisy latent、condition、相机参数和 Context K/V;
343
+ 5. step 1 的 anchor 是 `h0`,step 2 是 `pred_h1`,step 3 是 `pred_h2`;
344
+ 6. 每个 Predictor velocity 经可微 scheduler 更新到下一 latent;
345
+ 7. 三步 loss 求均值,当前 chunk 立即 backward;
346
+ 8. final latent 与三步 pred hidden detach 后加入下一 chunk 状态。
347
+
348
+ Teacher target 必须来自 Predictor 真正访问的状态。对应当前 Transformer API 的核心调用是:
349
+
350
+ ```python
351
+ with torch.no_grad():
352
+ teacher_model_input = torch.cat(
353
+ [current_student_latent.detach(), current_condition_33ch], dim=1
354
+ )
355
+ teacher_output = full_teacher(
356
+ bi_inference=False,
357
+ ar_txt_inference=False,
358
+ ar_vision_inference=True,
359
+ hidden_states=teacher_model_input,
360
+ timestep=current_timestep,
361
+ timestep_r=None,
362
+ mask_type=task_type,
363
+ action=current_action,
364
+ viewmats=current_viewmats,
365
+ Ks=current_Ks,
366
+ kv_cache=exact_context_kv,
367
+ cache_vision=False,
368
+ rope_temporal_size=rope_temporal_size,
369
+ start_rope_start_idx=start_rope_start_idx,
370
+ return_final_hidden=True,
371
+ )
372
+ target_velocity = teacher_output[0]
373
+ target_hidden = teacher_output[2]
374
+ ```
375
+
376
+ 不能读取原 F-F-F-F trajectory 中相同步的 target。Teacher 只用于构造 stop-gradient label,其输出
377
+ 立即释放。Predictor 使用的 `target_frame_condition` 继续由冻结 Teacher 的
378
+ `time_in(timestep) + action_in(action)` 计算,不从旧 target 文件推断。
379
+
380
+ ### 8.6 完整 timestep BPTT 的代码前置条件
381
+
382
+ 当前 `HYWorldPlayPredictorV2.forward`(v4 继承该实现)使用 `torch.no_grad()` 包裹冻结 `img_in`。
383
+ 这会阻断 `x2/x3 -> img_in -> Predictor` 的输入梯度,不满足完整 timestep BPTT。rollout 实现前必须
384
+ 改为:
385
+
386
+ ```python
387
+ # img_in 参数保持 requires_grad=False,但 forward 不进入 no_grad。
388
+ current_img = self.img_in(target_model_input)
389
+ ```
390
+
391
+ 冻结参数不会产生 parameter gradient;当离线训练输入不需要梯度时也不会额外建图,但 rollout 的
392
+ step 2、3 可以对前一步 scheduler 输出求导。冻结 final layer 同理:参数不训练,forward 仍必须保留
393
+ 对 `pred_hidden` 的梯度。Context projector 保持 `no_grad()`,因为 Context 本来就应 detach。
394
+
395
+ ### 8.7 可微 FlowMatch Euler update
396
+
397
+ 当前项目的 `FlowMatchDiscreteScheduler.step` 在 `solver=euler` 时核心公式已经是 out-of-place 且可微:
398
+
399
+ ```python
400
+ x_next = x.float() + pred_velocity.float() * (
401
+ sigmas[step_index + 1] - sigmas[step_index]
402
+ )
403
+ ```
404
+
405
+ 但正式 pipeline 会把结果原位写回共享 `latents`,不应直接复制该容器逻辑。训练应实现无状态的
406
+ `differentiable_euler_step`:调用相同 `set_timesteps(4)` 后读取同一组 sigmas,每次只接收和返回当前
407
+ chunk local tensor,无 `detach`、`no_grad` 或 inplace slice assignment。其 FP32 update 和输出 dtype
408
+ 必须与正式 scheduler 数值一致。
409
+
410
+ step 2、3 的 loss 应同时通过两类路径影响前面的 Predictor 调用:
411
+
412
+ ```text
413
+ hidden anchor path: pred_h1 -> Predictor step 2 -> pred_h2 -> Predictor step 3
414
+ latent path: pred_v1 -> x2 -> Predictor step 2 -> pred_v2 -> x3 -> Predictor step 3
415
+ ```
416
+
417
+ ### 8.8 Loss 与归一化
418
+
419
+ 每个 Predictor step 继续使用 v4 的 FP32 MSE:
420
+
421
+ ```python
422
+ step_loss = (
423
+ mse(pred_velocity.float(), target_velocity.float())
424
+ + 0.1 * mse(pred_hidden.float(), target_hidden.float())
425
+ )
426
+ chunk_loss = (step1_loss + step2_loss + step3_loss) / 3
427
+ ```
428
+
429
+ 一条 trajectory 有七个训练 chunk,所以每个 chunk 执行:
430
+
431
+ ```python
432
+ (chunk_loss / 7).backward()
433
+ ```
434
+
435
+ 不要再除以 world size,DDP 会对 8 个 rank 自动求平均。若以后额外累计多条 trajectory 才更新一次,
436
+ 还需要再除以 `gradient_accumulation_trajectories`。日志应分别记录 step 1/2/3 hidden MSE、velocity
437
+ MSE、chunk id 均值和整条 trajectory loss,不能只记录合并 total loss。
438
+
439
+ ### 8.9 8 卡 DDP 与同步边界
440
+
441
+ 每个 rank 持有一份冻结的 54-layer Teacher、一份 DDP v4 Predictor 和一条独立 trajectory。只包装
442
+ Predictor:
443
+
444
+ ```python
445
+ predictor = DDP(
446
+ predictor,
447
+ device_ids=[local_rank],
448
+ output_device=local_rank,
449
+ broadcast_buffers=False,
450
+ gradient_as_bucket_view=True,
451
+ find_unused_parameters=False,
452
+ )
453
+ ```
454
+
455
+ 400 条 trajectory 可被 8 个 rank 整除。`DistributedSampler(..., drop_last=True)` 每个 epoch 给每个
456
+ rank 50 条,调用 `sampler.set_epoch(epoch)` 保证 shuffle 一致且不同 rank 不重叠。micro-batch 固定为
457
+ 每卡一条 trajectory,因此每个 optimizer step 的全局 batch 是 8 trajectories。
458
+
459
+ Chunk 1~6 只累计本地梯度,Chunk 7 backward 时执行一次 all-reduce:
460
+
461
+ ```python
462
+ optimizer.zero_grad(set_to_none=True)
463
+
464
+ for chunk_id in range(1, 8):
465
+ sync_ctx = nullcontext() if chunk_id == 7 else predictor.no_sync()
466
+ with sync_ctx: # 必须同时覆盖三个 DDP forward 和当前 backward
467
+ chunk_loss, final_latent, hidden_by_step = train_fppp_chunk(...)
468
+ (chunk_loss / 7).backward()
469
+
470
+ history.append(final_latent.detach())
471
+ previous_hidden_by_step = {
472
+ step: hidden.detach() for step, hidden in hidden_by_step.items()
473
+ }
474
+ del chunk_loss, final_latent, hidden_by_step
475
+
476
+ clip_grad_norm_(predictor.parameters(), 1.0)
477
+ optimizer.step()
478
+ lr_scheduler.step()
479
+ ```
480
+
481
+ 七个 chunk 中间不能 `optimizer.step()` 或 `zero_grad()`。现有 Predictor checkpoint 使用
482
+ `use_reentrant=False`,应继续保留;三个 DDP forward 合并一次 backward 以及七次 chunk backward 的
483
+ 组合必须先通过单卡/DDP 小规模梯度一致性测试。
484
+
485
+ ### 8.10 显存与训练配置
486
+
487
+ 每个 rank 加载 Transformer Teacher 和 Predictor。image-condition latent 直接从 case tensor 读取;
488
+ 全层 Text K/V 从主离线数据集读取并去掉 padding。训练不加载 prompt encoder、vision encoder、
489
+ ByT5、VAE、SR 或视频编码组件。可微 rollout 阶段只让 Teacher、Predictor、当前 Text/Context cache
490
+ 和当前 chunk graph 驻留 GPU。
491
+
492
+ Teacher 全程 eval、`requires_grad_(False)` 和 `no_grad()`;Predictor forward activation 使用 BF16
493
+ autocast,所有可训练 Predictor 参数及 AdamW moments 保持 FP32,loss 使用 FP32,并开启现有
494
+ non-reentrant gradient checkpointing。冻结参数和 Teacher 权重仍为 BF16。每个 chunk backward 后
495
+ 立即释放三步图,history、previous hidden 和 Context K/V 全部 detach。
496
+
497
+ checkpoint 分成两种精度语义:`training_latest.pt` 保留 FP32 trainable master 参数、FP32 optimizer
498
+ 状态和 scheduler,用于无损 resume;`predictor_step_XXXXX.safetensors` 显式转换为 BF16,只用于推理
499
+ 和评测。resume 时会检查 checkpoint 的 FP32-master 配置,禁止把旧 BF16 rollout optimizer 静默载入
500
+ 当前训练。
501
+
502
+ 建议从已有 v4 权重开始一个全新的 optimizer/scheduler,不加载 Teacher-forcing
503
+ `training_latest.pt` 中的 optimizer 状态。rollout checkpoint 自身仍保存 model、optimizer、scheduler、
504
+ global optimizer step、epoch 和 sampler/trajectory 进度,并每 100 optimizer steps 保存一次。
505
+
506
+ 低学习率基线采用 fusion/residual `1e-5`、blocks `1e-6` 和 50-step warmup,运行至 step 600 后,
507
+ 完整 epoch mean loss 只下降约 3%,因此停止。后续正式配置改为与第一阶段相同的 LR 策略:
508
+ fusion/residual 峰值 `1e-4`,blocks 峰值 `1e-5`;fusion 前 100 steps warmup,blocks 前 100 steps
509
+ 学习率为 0,之后两组共同 cosine decay 到 step 2000。这里的一个 step 指完整 8-rank trajectory
510
+ 更新,不是一个 chunk。
511
+
512
+ 每 100 steps 保存一次 checkpoint。训练入口持续断言每步 8 个 rank 的 trajectory 主键互异,并检查
513
+ step 2/3 中间 latent 的梯度非零。
514
+
515
+ 正式启动前的 8 卡完整 8-chunk smoke test 已通过:fresh step 1 的 loss 为 `0.013256`,
516
+ `latent_grad_l1_min=1.63e-4`,峰值显存 `49.13 GiB/GPU`;从该 checkpoint resume 到 step 2 后,
517
+ `latent_grad_l1_min=2.98e-4`,峰值显存 `50.60 GiB/GPU`。训练 checkpoint 中 48 个可训练参数张量及
518
+ Adam moments 均为 FP32,推理 safetensors 中 80 个张量均为 BF16。
519
+
520
+ 为区分学习率问题与梯度/模型容量问题,另固定随机种子 `20260721` 从 100 个训练 case 中选择
521
+ `[4,11,17,28,58,80,94,96]`,每个 case 保留四种 action,共 32 条 trajectory。8 卡每个 epoch
522
+ 恰好 4 steps;临时将 fusion warmup 和 block freeze 都缩短为 8 steps,LR 仍为 `1e-4/1e-5`。
523
+ 截至 epoch 5,完整 overfit mean loss 从 `0.01969` 降至 `0.01836`,下降约 6.8%,证明递归梯度链路
524
+ 和优化器能够学习。该实验输出到:
525
+
526
+ ```text
527
+ checkpoints/predictor_v4_rollout_fppp_blocks1-52_overfit8_seed20260721_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup8_bwarmup8_swanlab
528
+ ```
529
+
530
+ ### 8.11 分阶段验收
531
+
532
+ 实现必须依次通过:
533
+
534
+ 1. Dataset 只有 400 条互异的 `(case_id, action_id)` 元数据,8 个 rank 每步读取不同主键;
535
+ 2. Chunk 0 全部由 Full Teacher 计算、无参数梯度,并产生 step 1/2/3 previous hidden;
536
+ 3. step 2 anchor 与 `pred_h1`、step 3 anchor 与 `pred_h2` 数值和对象来源正确;
537
+ 4. 每个 Teacher target 的输入等于当前 student latent 的 detached copy,不读取任何离线 FFFF 状态;
538
+ 5. self history 生成的 Context latent 正确,selected indices 与正式推理函数一致;
539
+ 6. 训练 Euler update 与正式 `FlowMatchDiscreteScheduler.step` 在四个 step 上误差满足 FP32 容差;
540
+ 7. 只保留 step 3 loss 时,`x2/x3.grad` 非零,且前两次 Predictor 调用对参数有非零贡献;
541
+ 8. Chunk 2 loss 不会回传到 Chunk 1 final latent 或 previous hidden;
542
+ 9. Chunk 1~6 不触发 all-reduce,Chunk 7 每个 optimizer step 只同步一次;
543
+ 10. 单卡小样本中,分 chunk backward 与保留七个 chunk 后一次 backward 的梯度在容差内一致;
544
+ 11. 先完成 1-trajectory forward、1-step backward、8 卡 2-step DDP smoke test,再做 20-step overfit;
545
+ 12. 无 NaN/Inf,step 1/2/3 loss 均能下降,显存峰值有日志记录。
546
+
547
+ 最终评测必须使用 Chunk 0 `F-F-F-F`、Chunk 1~7 `F-P-P-P`,即
548
+ `replace_steps=(1,2,3)`。在同一训练集和 25/50 holdout 上对比:Full-DiT、Direct Reuse、rollout
549
+ 微调前 v4 checkpoint 和微调后 checkpoint,并同时报告视频质量、端到端耗时和组件耗时。只有留出集
550
+ F-P-P-P 优于微调前权重,才能说明 self-rollout 微调带来泛化收益。
551
+
552
+ ## 9. 训练入口速查
553
+
554
+ v4:
555
+
556
+ ```bash
557
+ bash tools/launch_predictor_v4_training.sh
558
+ ```
559
+
560
+ v4 F-P-P-P on-policy rollout:
561
+
562
+ ```bash
563
+ bash tools/launch_predictor_v4_rollout_training.sh
564
+ ```
565
+
566
+ resume:
567
+
568
+ ```bash
569
+ bash tools/launch_predictor_v4_rollout_training.sh \
570
+ checkpoints/predictor_v4_rollout_fppp_blocks1-52_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab \
571
+ checkpoints/predictor_v4_rollout_fppp_blocks1-52_fp32master_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab/training_latest.pt
572
+ ```
573
+
574
+ 8-case overfit:
575
+
576
+ ```bash
577
+ bash tools/launch_predictor_v4_rollout_overfit8_training.sh
578
+ ```
579
+
580
+ predictor-disca `[1,52]`:
581
+
582
+ ```bash
583
+ bash tools/launch_predictor_disca_training.sh
584
+ ```
585
+
586
+ predictor-disca `[0,53]`:
587
+
588
+ ```bash
589
+ bash tools/launch_predictor_disca_training.sh \
590
+ checkpoints/predictor_disca_blocks0-53_bf16_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab \
591
+ 0,53
592
+ ```
train_predictor.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Single-GPU training for the dense image/text HY-WorldPlay Predictor."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import math
9
+ import os
10
+ import random
11
+ import time
12
+ from pathlib import Path
13
+
14
+ import torch
15
+ import torch.nn.functional as F
16
+ from torch.optim import AdamW
17
+ from torch.optim.lr_scheduler import LambdaLR
18
+ from torch.utils.data import DataLoader
19
+
20
+ from hyvideo.commons.infer_state import initialize_infer_state
21
+ from models import HYWorldPlayPredictor
22
+ from predictor_training.checkpoint import (
23
+ load_predictor_weights,
24
+ save_predictor_weights,
25
+ save_training_state,
26
+ trainable_state_dict,
27
+ )
28
+ from predictor_training.dataset import (
29
+ PredictorPairDataset,
30
+ move_pair_to_device,
31
+ predictor_collate,
32
+ )
33
+
34
+
35
+ def parse_args() -> argparse.Namespace:
36
+ parser = argparse.ArgumentParser(description=__doc__)
37
+ parser.add_argument("--manifest", default="datasets/predictor_v1/train_eval_manifest.jsonl")
38
+ parser.add_argument(
39
+ "--teacher_checkpoint",
40
+ default="models-ms/HY-WorldPlay/ar_distilled_action_model/diffusion_pytorch_model.safetensors",
41
+ )
42
+ parser.add_argument(
43
+ "--output_dir",
44
+ default=(
45
+ "checkpoints/predictor_v1_blocks1-52_bf16_1gpu_"
46
+ "flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100"
47
+ ),
48
+ )
49
+ parser.add_argument("--max_steps", type=int, default=2000)
50
+ parser.add_argument("--gradient_accumulation_steps", type=int, default=4)
51
+ parser.add_argument("--warmup_blocks_steps", type=int, default=100)
52
+ parser.add_argument("--scheduler_warmup_steps", type=int, default=100)
53
+ parser.add_argument("--fusion_lr", type=float, default=1e-4)
54
+ parser.add_argument("--blocks_lr", type=float, default=1e-5)
55
+ parser.add_argument("--weight_decay", type=float, default=0.01)
56
+ parser.add_argument("--hidden_weight", type=float, default=0.1)
57
+ parser.add_argument("--velocity_weight", type=float, default=1.0)
58
+ parser.add_argument("--grad_clip", type=float, default=1.0)
59
+ parser.add_argument("--log_every", type=int, default=10)
60
+ parser.add_argument("--save_every", type=int, default=500)
61
+ parser.add_argument("--keep_snapshots", type=int, default=2)
62
+ parser.add_argument("--seed", type=int, default=0)
63
+ parser.add_argument("--num_workers", type=int, default=0)
64
+ parser.add_argument("--max_records", type=int, default=None)
65
+ parser.add_argument("--gradient_checkpointing", action=argparse.BooleanOptionalAction, default=True)
66
+ parser.add_argument("--save_optimizer_state", action=argparse.BooleanOptionalAction, default=True)
67
+ parser.add_argument("--resume", default=None)
68
+ return parser.parse_args()
69
+
70
+
71
+ def initialize_runtime_state() -> None:
72
+ class Args:
73
+ sage_blocks_range = "0-53"
74
+ use_sageattn = False
75
+ enable_torch_compile = False
76
+ use_fp8_gemm = False
77
+ quant_type = "fp8-per-block"
78
+ include_patterns = "double_blocks"
79
+ use_vae_parallel = False
80
+
81
+ initialize_infer_state(Args())
82
+
83
+
84
+ def make_scheduler(optimizer: torch.optim.Optimizer, warmup: int, total: int) -> LambdaLR:
85
+ def factor(step: int) -> float:
86
+ if step < warmup:
87
+ return max(1e-8, float(step + 1) / max(1, warmup))
88
+ progress = min(1.0, float(step - warmup) / max(1, total - warmup))
89
+ return 0.5 * (1.0 + math.cos(math.pi * progress))
90
+
91
+ return LambdaLR(optimizer, factor)
92
+
93
+
94
+ def set_blocks_trainable(model: HYWorldPlayPredictor, enabled: bool) -> None:
95
+ model.double_blocks.requires_grad_(enabled)
96
+
97
+
98
+ def prune_snapshots(output_dir: Path, keep: int) -> None:
99
+ snapshots = sorted(output_dir.glob("predictor_step_*.safetensors"))
100
+ for path in snapshots[:-keep] if keep > 0 else snapshots:
101
+ path.unlink()
102
+
103
+
104
+ def main() -> None:
105
+ args = parse_args()
106
+ if not torch.cuda.is_available():
107
+ raise RuntimeError("CUDA is required")
108
+ random.seed(args.seed)
109
+ torch.manual_seed(args.seed)
110
+ torch.cuda.manual_seed_all(args.seed)
111
+ initialize_runtime_state()
112
+ device = torch.device("cuda:0")
113
+ dtype = torch.bfloat16
114
+ output_dir = Path(args.output_dir).resolve()
115
+ output_dir.mkdir(parents=True, exist_ok=True)
116
+ log_path = output_dir / "train_log.jsonl"
117
+
118
+ dataset = PredictorPairDataset(args.manifest, max_records=args.max_records)
119
+ generator = torch.Generator().manual_seed(args.seed)
120
+ loader = DataLoader(
121
+ dataset,
122
+ batch_size=1,
123
+ shuffle=True,
124
+ num_workers=args.num_workers,
125
+ pin_memory=True,
126
+ collate_fn=predictor_collate,
127
+ generator=generator,
128
+ )
129
+
130
+ model = HYWorldPlayPredictor()
131
+ model.load_teacher_initialization(args.teacher_checkpoint)
132
+ model.to(device=device, dtype=dtype)
133
+ model.enable_gradient_checkpointing(args.gradient_checkpointing)
134
+ model.train()
135
+
136
+ optimizer = AdamW(
137
+ [
138
+ {
139
+ "params": list(model.img_fusion.parameters())
140
+ + list(model.txt_fusion.parameters())
141
+ + list(model.residual_out.parameters()),
142
+ "lr": args.fusion_lr,
143
+ },
144
+ {"params": model.double_blocks.parameters(), "lr": args.blocks_lr},
145
+ ],
146
+ betas=(0.9, 0.95),
147
+ weight_decay=args.weight_decay,
148
+ )
149
+ scheduler = make_scheduler(optimizer, args.scheduler_warmup_steps, args.max_steps)
150
+ global_step = 0
151
+ micro_step = 0
152
+ set_blocks_trainable(model, args.warmup_blocks_steps <= 0)
153
+
154
+ if args.resume:
155
+ state = torch.load(args.resume, map_location="cpu", weights_only=False)
156
+ model.load_state_dict(state["model"], strict=False)
157
+ optimizer.load_state_dict(state["optimizer"])
158
+ scheduler.load_state_dict(state["scheduler"])
159
+ global_step = int(state["global_step"])
160
+ micro_step = int(state.get("micro_step", global_step * args.gradient_accumulation_steps))
161
+ if "torch_rng_state" in state:
162
+ torch.set_rng_state(state["torch_rng_state"])
163
+ if "cuda_rng_state" in state:
164
+ torch.cuda.set_rng_state_all(state["cuda_rng_state"])
165
+ set_blocks_trainable(model, global_step >= args.warmup_blocks_steps)
166
+
167
+ print(
168
+ json.dumps(
169
+ {
170
+ "dataset_pairs": len(dataset),
171
+ "trainable_parameters": model.trainable_parameter_count(),
172
+ "breakdown": model.trainable_parameter_breakdown(),
173
+ "start_step": global_step,
174
+ },
175
+ indent=2,
176
+ ),
177
+ flush=True,
178
+ )
179
+
180
+ optimizer.zero_grad(set_to_none=True)
181
+ running: list[dict[str, float | str]] = []
182
+ epoch = 0
183
+ while global_step < args.max_steps:
184
+ epoch += 1
185
+ for item in loader:
186
+ if global_step >= args.max_steps:
187
+ break
188
+ if global_step == args.warmup_blocks_steps:
189
+ set_blocks_trainable(model, True)
190
+
191
+ inputs, target_hidden, target_velocity = move_pair_to_device(
192
+ item, device=device, dtype=dtype
193
+ )
194
+ started = time.perf_counter()
195
+ with torch.autocast(device_type="cuda", dtype=dtype):
196
+ output = model(**inputs)
197
+ hidden_loss = F.mse_loss(
198
+ output["pred_hidden"].float(), target_hidden.float()
199
+ )
200
+ velocity_loss = F.mse_loss(
201
+ output["pred_velocity"].float(), target_velocity.float()
202
+ )
203
+ loss = (
204
+ args.hidden_weight * hidden_loss
205
+ + args.velocity_weight * velocity_loss
206
+ )
207
+ scaled_loss = loss / args.gradient_accumulation_steps
208
+ scaled_loss.backward()
209
+ micro_step += 1
210
+ running.append(
211
+ {
212
+ "pair_id": item["pair_id"],
213
+ "loss": float(loss.detach()),
214
+ "hidden_mse": float(hidden_loss.detach()),
215
+ "velocity_mse": float(velocity_loss.detach()),
216
+ "micro_time_s": time.perf_counter() - started,
217
+ }
218
+ )
219
+ del output, loss, scaled_loss, hidden_loss, velocity_loss
220
+
221
+ if micro_step % args.gradient_accumulation_steps:
222
+ continue
223
+ grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
224
+ optimizer.step()
225
+ scheduler.step()
226
+ optimizer.zero_grad(set_to_none=True)
227
+ global_step += 1
228
+
229
+ if global_step % args.log_every == 0 or global_step == 1:
230
+ torch.cuda.synchronize()
231
+ count = len(running)
232
+ record = {
233
+ "global_step": global_step,
234
+ "epoch": epoch,
235
+ "loss": sum(float(x["loss"]) for x in running) / count,
236
+ "hidden_mse": sum(float(x["hidden_mse"]) for x in running) / count,
237
+ "velocity_mse": sum(float(x["velocity_mse"]) for x in running) / count,
238
+ "grad_norm": float(grad_norm),
239
+ "lr_fusion": optimizer.param_groups[0]["lr"],
240
+ "lr_blocks": optimizer.param_groups[1]["lr"],
241
+ "peak_memory_gib": torch.cuda.max_memory_allocated() / 2**30,
242
+ "avg_micro_time_s": sum(float(x["micro_time_s"]) for x in running) / count,
243
+ }
244
+ with log_path.open("a", encoding="utf-8") as handle:
245
+ handle.write(json.dumps(record, sort_keys=True) + "\n")
246
+ print(json.dumps(record, sort_keys=True), flush=True)
247
+ running.clear()
248
+
249
+ should_save = global_step % args.save_every == 0 or global_step == args.max_steps
250
+ if should_save:
251
+ weights_path = output_dir / f"predictor_step_{global_step:05d}.safetensors"
252
+ save_predictor_weights(model, weights_path)
253
+ if args.save_optimizer_state:
254
+ latest = {
255
+ "model": trainable_state_dict(model),
256
+ "optimizer": optimizer.state_dict(),
257
+ "scheduler": scheduler.state_dict(),
258
+ "global_step": global_step,
259
+ "micro_step": micro_step,
260
+ "torch_rng_state": torch.get_rng_state(),
261
+ "cuda_rng_state": torch.cuda.get_rng_state_all(),
262
+ "args": vars(args),
263
+ "predictor_config": model.config_dict,
264
+ "weights_path": str(weights_path),
265
+ }
266
+ save_training_state(output_dir / "training_latest.pt", latest)
267
+ prune_snapshots(output_dir, args.keep_snapshots)
268
+
269
+
270
+ if __name__ == "__main__":
271
+ main()
train_predictor_disca.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Eight-GPU training entry point for the project-local Predictor-DisCa."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+
8
+ from train_predictor_v4 import main
9
+
10
+
11
+ def _has_option(name: str) -> bool:
12
+ return any(argument == name or argument.startswith(f"{name}=") for argument in sys.argv[1:])
13
+
14
+
15
+ if __name__ == "__main__":
16
+ defaults = (
17
+ ("--predictor_variant", "disca"),
18
+ (
19
+ "--output_dir",
20
+ (
21
+ "checkpoints/predictor_disca_blocks0-53_bf16_8gpu_"
22
+ "flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab"
23
+ ),
24
+ ),
25
+ ("--source_blocks", "0,53"),
26
+ ("--swanlab_project", "HY-WorldPlay-Predictor-DisCa"),
27
+ (
28
+ "--swanlab_experiment",
29
+ (
30
+ "predictor-disca-blocks0-53-bf16-8gpu-flr1e-4-blr1e-5-"
31
+ "schedsteps2000-swarmup100-bwarmup100"
32
+ ),
33
+ ),
34
+ )
35
+ injected: list[str] = []
36
+ for option, value in defaults:
37
+ if not _has_option(option):
38
+ injected.extend((option, value))
39
+ if not _has_option("--bucket_by_context") and not _has_option("--no-bucket_by_context"):
40
+ injected.append("--no-bucket_by_context")
41
+ sys.argv[1:1] = injected
42
+ main()
train_predictor_v2.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Single-GPU training for AR-context HY-WorldPlay Predictor-v2."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import math
9
+ import os
10
+ import random
11
+ import time
12
+ from pathlib import Path
13
+
14
+ import swanlab
15
+ import torch
16
+ import torch.nn.functional as F
17
+ from torch.optim import AdamW
18
+ from torch.optim.lr_scheduler import LambdaLR
19
+ from torch.utils.data import DataLoader
20
+
21
+ from hyvideo.commons.infer_state import initialize_infer_state
22
+ from models import HYWorldPlayPredictorV2, HYWorldPlayPredictorV3, PredictorV2Config
23
+ from predictor_training.checkpoint import (
24
+ save_predictor_weights,
25
+ save_training_state,
26
+ trainable_state_dict,
27
+ )
28
+ from predictor_training.dataset_v2 import (
29
+ PredictorV2PairDataset,
30
+ move_v2_pair_to_device,
31
+ predictor_v2_collate,
32
+ )
33
+ from predictor_training.dataset_v3 import (
34
+ PredictorV3PairDataset,
35
+ move_v3_pair_to_device,
36
+ predictor_v3_collate,
37
+ )
38
+
39
+
40
+ def parse_source_blocks(value: str) -> tuple[int, int]:
41
+ result = tuple(int(part.strip()) for part in value.split(",") if part.strip())
42
+ if result not in {(0, 53), (1, 52)}:
43
+ raise argparse.ArgumentTypeError("source_blocks must be 0,53 or 1,52")
44
+ return result
45
+
46
+
47
+ def parse_args() -> argparse.Namespace:
48
+ parser = argparse.ArgumentParser(description=__doc__)
49
+ parser.add_argument("--manifest", default="datasets/predictor_v2/train_eval_manifest.jsonl")
50
+ parser.add_argument("--predictor_version", choices=("v2", "v3"), default="v2")
51
+ parser.add_argument("--source_blocks", type=parse_source_blocks, required=True)
52
+ parser.add_argument(
53
+ "--teacher_checkpoint",
54
+ default="models-ms/HY-WorldPlay/ar_distilled_action_model/diffusion_pytorch_model.safetensors",
55
+ )
56
+ parser.add_argument("--output_dir", required=True)
57
+ parser.add_argument("--max_steps", type=int, default=2000)
58
+ parser.add_argument("--batch_size", type=int, default=1)
59
+ parser.add_argument("--gradient_accumulation_steps", type=int, default=4)
60
+ parser.add_argument("--warmup_blocks_steps", type=int, default=100)
61
+ parser.add_argument("--scheduler_warmup_steps", type=int, default=100)
62
+ parser.add_argument("--fusion_lr", type=float, default=1e-4)
63
+ parser.add_argument("--blocks_lr", type=float, default=1e-5)
64
+ parser.add_argument("--weight_decay", type=float, default=0.01)
65
+ parser.add_argument("--hidden_weight", type=float, default=0.1)
66
+ parser.add_argument("--velocity_weight", type=float, default=1.0)
67
+ parser.add_argument("--grad_clip", type=float, default=1.0)
68
+ parser.add_argument("--log_every", type=int, default=10)
69
+ parser.add_argument("--save_every", type=int, default=500)
70
+ parser.add_argument("--keep_snapshots", type=int, default=2)
71
+ parser.add_argument("--seed", type=int, default=0)
72
+ parser.add_argument("--num_workers", type=int, default=0)
73
+ parser.add_argument("--max_records", type=int, default=None)
74
+ parser.add_argument("--gradient_checkpointing", action=argparse.BooleanOptionalAction, default=True)
75
+ parser.add_argument("--save_optimizer_state", action=argparse.BooleanOptionalAction, default=True)
76
+ parser.add_argument("--resume", default=None)
77
+ parser.add_argument("--swanlab_project", default="HY-WorldPlay-Predictor-v2")
78
+ parser.add_argument("--swanlab_workspace", default=None)
79
+ parser.add_argument("--swanlab_experiment", default=None)
80
+ parser.add_argument("--swanlab_mode", choices=("cloud", "offline", "local", "disabled"), default="cloud")
81
+ return parser.parse_args()
82
+
83
+
84
+ def initialize_runtime_state() -> None:
85
+ class Args:
86
+ sage_blocks_range = "0-53"
87
+ use_sageattn = False
88
+ enable_torch_compile = False
89
+ use_fp8_gemm = False
90
+ quant_type = "fp8-per-block"
91
+ include_patterns = "double_blocks"
92
+ use_vae_parallel = False
93
+
94
+ initialize_infer_state(Args())
95
+
96
+
97
+ def make_scheduler(optimizer: torch.optim.Optimizer, warmup: int, total: int) -> LambdaLR:
98
+ def factor(step: int) -> float:
99
+ if step < warmup:
100
+ return max(1e-8, float(step + 1) / max(1, warmup))
101
+ progress = min(1.0, float(step - warmup) / max(1, total - warmup))
102
+ return 0.5 * (1.0 + math.cos(math.pi * progress))
103
+
104
+ return LambdaLR(optimizer, factor)
105
+
106
+
107
+ def set_ar_blocks_trainable(model: HYWorldPlayPredictorV2, enabled: bool) -> None:
108
+ for block in model.double_blocks:
109
+ for name, parameter in block.named_parameters():
110
+ parameter.requires_grad_(enabled and not name.startswith("txt_"))
111
+
112
+
113
+ def prune_snapshots(output_dir: Path, keep: int) -> None:
114
+ snapshots = sorted(output_dir.glob("predictor_step_*.safetensors"))
115
+ for path in snapshots[:-keep] if keep > 0 else snapshots:
116
+ path.unlink()
117
+
118
+
119
+ def init_swanlab(args: argparse.Namespace, config: dict):
120
+ if args.swanlab_mode == "cloud":
121
+ api_key = os.environ.get("SWANLAB_API_KEY")
122
+ swanlab.login(api_key=api_key, save=False) if api_key else swanlab.login()
123
+ fusion_lr = f"{args.fusion_lr:.0e}".replace("e-0", "e-").replace("e+0", "e+")
124
+ blocks_lr = f"{args.blocks_lr:.0e}".replace("e-0", "e-").replace("e+0", "e+")
125
+ experiment = args.swanlab_experiment or (
126
+ f"predictor-{args.predictor_version}-blocks"
127
+ + "-".join(str(value) for value in args.source_blocks)
128
+ + f"-flr{fusion_lr}-blr{blocks_lr}-schedsteps{args.max_steps}"
129
+ + f"-swarmup{args.scheduler_warmup_steps}"
130
+ + f"-bwarmup{args.warmup_blocks_steps}"
131
+ )
132
+ return swanlab.init(
133
+ project=args.swanlab_project,
134
+ workspace=args.swanlab_workspace,
135
+ experiment_name=experiment,
136
+ description=(
137
+ f"HY-WorldPlay {args.predictor_version} AR-context Predictor, "
138
+ "fresh exact 10-case targets"
139
+ ),
140
+ tags=[
141
+ f"predictor-{args.predictor_version}",
142
+ "ar-context-kv",
143
+ f"blocks-{args.source_blocks[0]}-{args.source_blocks[1]}",
144
+ ],
145
+ config=config,
146
+ logdir=str(Path(args.output_dir).resolve() / "swanlab"),
147
+ mode=args.swanlab_mode,
148
+ )
149
+
150
+
151
+ def main() -> None:
152
+ args = parse_args()
153
+ if not torch.cuda.is_available():
154
+ raise RuntimeError("CUDA is required")
155
+ random.seed(args.seed)
156
+ torch.manual_seed(args.seed)
157
+ torch.cuda.manual_seed_all(args.seed)
158
+ initialize_runtime_state()
159
+ device = torch.device("cuda:0")
160
+ dtype = torch.bfloat16
161
+ output_dir = Path(args.output_dir).resolve()
162
+ output_dir.mkdir(parents=True, exist_ok=True)
163
+ log_path = output_dir / "train_log.jsonl"
164
+
165
+ dataset_class = (
166
+ PredictorV3PairDataset if args.predictor_version == "v3" else PredictorV2PairDataset
167
+ )
168
+ collate_fn = (
169
+ predictor_v3_collate if args.predictor_version == "v3" else predictor_v2_collate
170
+ )
171
+ move_pair = (
172
+ move_v3_pair_to_device
173
+ if args.predictor_version == "v3"
174
+ else move_v2_pair_to_device
175
+ )
176
+ dataset = dataset_class(
177
+ args.manifest,
178
+ source_block_ids=args.source_blocks,
179
+ max_records=args.max_records,
180
+ )
181
+ generator = torch.Generator().manual_seed(args.seed)
182
+ loader = DataLoader(
183
+ dataset,
184
+ batch_size=args.batch_size,
185
+ shuffle=True,
186
+ num_workers=args.num_workers,
187
+ pin_memory=False,
188
+ collate_fn=collate_fn,
189
+ generator=generator,
190
+ )
191
+
192
+ model_class = (
193
+ HYWorldPlayPredictorV3 if args.predictor_version == "v3" else HYWorldPlayPredictorV2
194
+ )
195
+ model = model_class(PredictorV2Config(source_block_ids=args.source_blocks))
196
+ model.load_teacher_initialization(args.teacher_checkpoint)
197
+ model.to(device=device, dtype=dtype)
198
+ model.enable_gradient_checkpointing(args.gradient_checkpointing)
199
+ model.train()
200
+
201
+ fusion_parameters = list(model.img_fusion.parameters()) + list(model.residual_out.parameters())
202
+ block_parameters = [
203
+ parameter for parameter in model.double_blocks.parameters() if parameter.requires_grad
204
+ ]
205
+ intended_trainable_parameters = sum(
206
+ parameter.numel() for parameter in fusion_parameters + block_parameters
207
+ )
208
+ intended_parameter_breakdown = model.trainable_parameter_breakdown()
209
+ optimizer = AdamW(
210
+ [
211
+ {"params": fusion_parameters, "lr": args.fusion_lr},
212
+ {"params": block_parameters, "lr": args.blocks_lr},
213
+ ],
214
+ betas=(0.9, 0.95),
215
+ weight_decay=args.weight_decay,
216
+ )
217
+ scheduler = make_scheduler(optimizer, args.scheduler_warmup_steps, args.max_steps)
218
+ global_step = 0
219
+ micro_step = 0
220
+ set_ar_blocks_trainable(model, args.warmup_blocks_steps <= 0)
221
+
222
+ if args.resume:
223
+ state = torch.load(args.resume, map_location="cpu", weights_only=False)
224
+ if tuple(state["predictor_config"]["source_block_ids"]) != args.source_blocks:
225
+ raise ValueError("Resume checkpoint source blocks do not match")
226
+ model.load_state_dict(state["model"], strict=False)
227
+ optimizer.load_state_dict(state["optimizer"])
228
+ scheduler.load_state_dict(state["scheduler"])
229
+ global_step = int(state["global_step"])
230
+ micro_step = int(state.get("micro_step", global_step * args.gradient_accumulation_steps))
231
+ if "torch_rng_state" in state:
232
+ torch.set_rng_state(state["torch_rng_state"])
233
+ if "cuda_rng_state" in state:
234
+ torch.cuda.set_rng_state_all(state["cuda_rng_state"])
235
+ set_ar_blocks_trainable(model, global_step >= args.warmup_blocks_steps)
236
+
237
+ run_config = {
238
+ **vars(args),
239
+ "source_blocks": list(args.source_blocks),
240
+ "manifest": str(Path(args.manifest).resolve()),
241
+ "teacher_checkpoint": str(Path(args.teacher_checkpoint).resolve()),
242
+ "dataset_pairs": len(dataset),
243
+ "effective_batch_size": args.batch_size * args.gradient_accumulation_steps,
244
+ "trainable_parameters": intended_trainable_parameters,
245
+ "parameter_breakdown": intended_parameter_breakdown,
246
+ "dtype": "bfloat16",
247
+ "predictor_version": args.predictor_version,
248
+ "gpu": torch.cuda.get_device_name(device),
249
+ }
250
+ run = init_swanlab(args, run_config)
251
+ print(
252
+ json.dumps(
253
+ {
254
+ "dataset_pairs": len(dataset),
255
+ "source_blocks": args.source_blocks,
256
+ "trainable_parameters": intended_trainable_parameters,
257
+ "breakdown": intended_parameter_breakdown,
258
+ "start_step": global_step,
259
+ "swanlab_run_id": getattr(run, "id", None),
260
+ },
261
+ indent=2,
262
+ ),
263
+ flush=True,
264
+ )
265
+
266
+ optimizer.zero_grad(set_to_none=True)
267
+ running: list[dict[str, float | str]] = []
268
+ epoch = 0
269
+ try:
270
+ while global_step < args.max_steps:
271
+ epoch += 1
272
+ for item in loader:
273
+ if global_step >= args.max_steps:
274
+ break
275
+ if global_step == args.warmup_blocks_steps:
276
+ set_ar_blocks_trainable(model, True)
277
+
278
+ inputs, target_hidden, target_velocity = move_pair(
279
+ item, device=device, dtype=dtype
280
+ )
281
+ started = time.perf_counter()
282
+ with torch.autocast(device_type="cuda", dtype=dtype):
283
+ output = model(**inputs)
284
+ hidden_loss = F.mse_loss(
285
+ output["pred_hidden"].float(), target_hidden.float()
286
+ )
287
+ velocity_loss = F.mse_loss(
288
+ output["pred_velocity"].float(), target_velocity.float()
289
+ )
290
+ loss = args.hidden_weight * hidden_loss + args.velocity_weight * velocity_loss
291
+ scaled_loss = loss / args.gradient_accumulation_steps
292
+ scaled_loss.backward()
293
+ micro_step += 1
294
+ running.append(
295
+ {
296
+ "pair_id": ",".join(item["pair_id"]),
297
+ "loss": float(loss.detach()),
298
+ "hidden_mse": float(hidden_loss.detach()),
299
+ "velocity_mse": float(velocity_loss.detach()),
300
+ "micro_time_s": time.perf_counter() - started,
301
+ "context_frames": sum(item["context_frames"])
302
+ / len(item["context_frames"]),
303
+ }
304
+ )
305
+ del (
306
+ output,
307
+ loss,
308
+ scaled_loss,
309
+ hidden_loss,
310
+ velocity_loss,
311
+ inputs,
312
+ target_hidden,
313
+ target_velocity,
314
+ item,
315
+ )
316
+
317
+ if micro_step % args.gradient_accumulation_steps:
318
+ continue
319
+ grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), args.grad_clip)
320
+ optimizer.step()
321
+ scheduler.step()
322
+ optimizer.zero_grad(set_to_none=True)
323
+ global_step += 1
324
+
325
+ if global_step % args.log_every == 0 or global_step == 1:
326
+ torch.cuda.synchronize()
327
+ count = len(running)
328
+ record = {
329
+ "global_step": global_step,
330
+ "epoch": epoch,
331
+ "loss": sum(float(x["loss"]) for x in running) / count,
332
+ "hidden_mse": sum(float(x["hidden_mse"]) for x in running) / count,
333
+ "velocity_mse": sum(float(x["velocity_mse"]) for x in running) / count,
334
+ "grad_norm": float(grad_norm),
335
+ "lr_fusion": optimizer.param_groups[0]["lr"],
336
+ "lr_blocks": optimizer.param_groups[1]["lr"],
337
+ "peak_memory_gib": torch.cuda.max_memory_allocated() / 2**30,
338
+ "avg_micro_time_s": sum(float(x["micro_time_s"]) for x in running) / count,
339
+ "avg_context_frames": sum(float(x["context_frames"]) for x in running) / count,
340
+ }
341
+ with log_path.open("a", encoding="utf-8") as handle:
342
+ handle.write(json.dumps(record, sort_keys=True) + "\n")
343
+ swanlab.log(record, step=global_step)
344
+ print(json.dumps(record, sort_keys=True), flush=True)
345
+ running.clear()
346
+
347
+ should_save = global_step % args.save_every == 0 or global_step == args.max_steps
348
+ if should_save:
349
+ weights_path = output_dir / f"predictor_step_{global_step:05d}.safetensors"
350
+ save_predictor_weights(model, weights_path)
351
+ if args.save_optimizer_state:
352
+ latest = {
353
+ "model": trainable_state_dict(model),
354
+ "optimizer": optimizer.state_dict(),
355
+ "scheduler": scheduler.state_dict(),
356
+ "global_step": global_step,
357
+ "micro_step": micro_step,
358
+ "torch_rng_state": torch.get_rng_state(),
359
+ "cuda_rng_state": torch.cuda.get_rng_state_all(),
360
+ "args": vars(args),
361
+ "predictor_config": model.config_dict,
362
+ "weights_path": str(weights_path),
363
+ }
364
+ save_training_state(output_dir / "training_latest.pt", latest)
365
+ prune_snapshots(output_dir, args.keep_snapshots)
366
+ except BaseException as error:
367
+ swanlab.finish(error=error)
368
+ raise
369
+ else:
370
+ swanlab.finish()
371
+
372
+
373
+ if __name__ == "__main__":
374
+ main()
train_predictor_v3.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Single-GPU training entry point for previous-chunk Predictor-v3."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import sys
7
+
8
+ from train_predictor_v2 import main
9
+
10
+
11
+ if __name__ == "__main__":
12
+ if "--predictor_version" not in sys.argv:
13
+ sys.argv[1:1] = ["--predictor_version", "v3"]
14
+ main()
train_predictor_v4.py ADDED
@@ -0,0 +1,1030 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Eight-GPU DDP training engine for Predictor-v4 and Predictor-DisCa."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import math
9
+ import os
10
+ import random
11
+ import time
12
+ from pathlib import Path
13
+
14
+ import torch
15
+ import torch.distributed as dist
16
+ import torch.nn.functional as F
17
+ import swanlab
18
+ from torch.nn.parallel import DistributedDataParallel as DDP
19
+ from torch.optim import AdamW
20
+ from torch.optim.lr_scheduler import LambdaLR
21
+ from torch.utils.data import DataLoader
22
+ from torch.utils.data.distributed import DistributedSampler
23
+
24
+ from hyvideo.commons.infer_state import initialize_infer_state
25
+ from hyvideo.commons.parallel_states import initialize_parallel_state
26
+ from models import HYWorldPlayPredictorDisCa, HYWorldPlayPredictorV4, PredictorV2Config
27
+ from predictor_training.bucket_sampler import DistributedContextBucketBatchSampler
28
+ from predictor_training.checkpoint import (
29
+ load_concat_fusion_initialization,
30
+ load_concat_residual_gate_initialization,
31
+ save_predictor_weights,
32
+ save_training_state,
33
+ trainable_state_dict,
34
+ )
35
+ from predictor_training.dataset_prefeature import (
36
+ PredictorPrefeaturePairDataset,
37
+ move_prefeature_pair_to_device,
38
+ predictor_prefeature_collate,
39
+ )
40
+ from predictor_training.dataset_disca import (
41
+ PredictorDisCaPairDataset,
42
+ move_disca_pair_to_device,
43
+ predictor_disca_collate,
44
+ )
45
+
46
+
47
+ DEFAULT_MANIFEST = (
48
+ "/mnt/local_nvme/zoubin/cz/hyworldplay_predictor_prefeature_v4/"
49
+ "train_eval_manifest.jsonl"
50
+ )
51
+ DEFAULT_TEACHER = (
52
+ "/mnt/s3files/s3-us-west2-default/zoubin/cz/checkpoints/hy_worldplay/"
53
+ "huggingface/hub/models--tencent--HY-WorldPlay/snapshots/"
54
+ "f4c29235647707b571479a69b569e4166f9f5bf8/ar_distilled_action_model/"
55
+ "diffusion_pytorch_model.safetensors"
56
+ )
57
+ DEFAULT_OUTPUT = (
58
+ "/mnt/s3files/s3-us-west2-default/zoubin/cz/projects/"
59
+ "HY-WorldPlay-DEV-Predictor/checkpoints/"
60
+ "predictor_v4_prefeature_blocks0-53_bf16_8gpu_flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab"
61
+ )
62
+
63
+
64
+ def parse_source_blocks(value: str) -> tuple[int, int]:
65
+ result = tuple(int(part.strip()) for part in value.split(",") if part.strip())
66
+ if result not in {(0, 53), (1, 52)}:
67
+ raise argparse.ArgumentTypeError("source_blocks must be 0,53 or 1,52")
68
+ return result
69
+
70
+
71
+ def parse_args() -> argparse.Namespace:
72
+ parser = argparse.ArgumentParser(description=__doc__)
73
+ parser.add_argument("--manifest", default=DEFAULT_MANIFEST)
74
+ parser.add_argument("--teacher_checkpoint", default=DEFAULT_TEACHER)
75
+ parser.add_argument("--output_dir", default=DEFAULT_OUTPUT)
76
+ parser.add_argument("--source_blocks", type=parse_source_blocks, default=(0, 53))
77
+ parser.add_argument(
78
+ "--predictor_variant", choices=("v4", "disca"), default="v4",
79
+ help="Use v4 Context/previous-chunk inputs or the Predictor-DisCa ablation",
80
+ )
81
+ parser.add_argument(
82
+ "--previous_condition_mode",
83
+ choices=("none", "same_step", "previous_step", "both", "both_delta_gated"),
84
+ default="same_step",
85
+ help="Explicit previous-chunk hidden condition used by Predictor-v4",
86
+ )
87
+ parser.add_argument(
88
+ "--predictor_fusion_type",
89
+ choices=(
90
+ "concat",
91
+ "projection_add",
92
+ "token_gate_prev",
93
+ "concat_residual_gate_prev",
94
+ ),
95
+ default="concat",
96
+ help="Image-token fusion architecture; non-concat modes require same_step",
97
+ )
98
+ parser.add_argument("--gate_hidden_dim", type=int, default=256)
99
+ parser.add_argument("--gate_initial_probability", type=float, default=0.8)
100
+ parser.add_argument("--predictor_arch", choices=("baseline", "atc"), default="baseline")
101
+ parser.add_argument(
102
+ "--atc_variant",
103
+ choices=("full", "anchor_only", "raw_prev", "no_gate", "no_distance"),
104
+ default="full",
105
+ )
106
+ parser.add_argument("--atc_mlp_hidden_dim", type=int, default=4096)
107
+ parser.add_argument("--atc_gate_hidden_dim", type=int, default=512)
108
+ parser.add_argument("--atc_gate_initial_probability", type=float, default=0.3)
109
+ parser.add_argument(
110
+ "--atc_transport_heads", type=int, default=None,
111
+ help="Transport attention heads; default = Teacher heads so head_dim matches the Teacher RoPE.",
112
+ )
113
+ parser.add_argument(
114
+ "--atc_transport_mode", choices=("global", "local"), default="global",
115
+ help="global: Causal-Forcing RoPE'd global cross-chunk attention; local: legacy 3x3 window.",
116
+ )
117
+ parser.add_argument(
118
+ "--atc_previous_scope", choices=("chunk", "last_frame"), default="chunk",
119
+ help="Keys of the global transport: whole previous chunk or only its last frame.",
120
+ )
121
+ parser.add_argument("--atc_transport_window", type=int, default=3)
122
+ parser.add_argument("--atc_transport_query_chunk_size", type=int, default=128)
123
+ parser.add_argument("--atc_transport_initial_scale", type=float, default=0.1)
124
+ parser.add_argument("--max_steps", type=int, default=2000)
125
+ parser.add_argument("--batch_size", type=int, default=16, help="Per-rank batch size")
126
+ parser.add_argument("--gradient_accumulation_steps", type=int, default=1)
127
+ parser.add_argument("--block_warmup_steps", type=int, default=100)
128
+ parser.add_argument(
129
+ "--block_warmup_mode",
130
+ choices=("delayed", "linear"),
131
+ default="delayed",
132
+ help=(
133
+ "Use the original Stage-1 delayed block start or linearly warm the "
134
+ "block group from the beginning"
135
+ ),
136
+ )
137
+ parser.add_argument("--scheduler_warmup_steps", type=int, default=100)
138
+ parser.add_argument("--fusion_lr", type=float, default=1e-4)
139
+ parser.add_argument("--blocks_lr", type=float, default=1e-5)
140
+ parser.add_argument("--weight_decay", type=float, default=0.01)
141
+ parser.add_argument("--hidden_weight", type=float, default=0.1)
142
+ parser.add_argument("--velocity_weight", type=float, default=1.0)
143
+ parser.add_argument("--grad_clip", type=float, default=1.0)
144
+ parser.add_argument("--log_every", type=int, default=10)
145
+ parser.add_argument("--save_every", type=int, default=100)
146
+ parser.add_argument("--keep_snapshots", type=int, default=20)
147
+ parser.add_argument("--seed", type=int, default=0)
148
+ parser.add_argument("--num_workers", type=int, default=0)
149
+ parser.add_argument("--max_records", type=int, default=None)
150
+ parser.add_argument(
151
+ "--bucket_by_context", action=argparse.BooleanOptionalAction, default=True
152
+ )
153
+ parser.add_argument(
154
+ "--gradient_checkpointing", action=argparse.BooleanOptionalAction, default=True
155
+ )
156
+ parser.add_argument(
157
+ "--fp32_trainable_params",
158
+ action=argparse.BooleanOptionalAction,
159
+ default=False,
160
+ help=(
161
+ "Keep requires_grad=True Predictor parameters and AdamW moments in "
162
+ "FP32 while frozen parameters and forward activations remain BF16"
163
+ ),
164
+ )
165
+ parser.add_argument(
166
+ "--train_delta_only",
167
+ action=argparse.BooleanOptionalAction,
168
+ default=False,
169
+ help=(
170
+ "Freeze the inherited same-step Predictor and optimize only the "
171
+ "both_delta_gated residual branch."
172
+ ),
173
+ )
174
+ parser.add_argument(
175
+ "--train_fusion_residual_only",
176
+ action=argparse.BooleanOptionalAction,
177
+ default=False,
178
+ help=(
179
+ "Freeze the inherited concat fusion and Predictor blocks, and train "
180
+ "only the zero-initialized concat_residual_gate_prev branch."
181
+ ),
182
+ )
183
+ parser.add_argument(
184
+ "--save_optimizer_state", action=argparse.BooleanOptionalAction, default=True
185
+ )
186
+ parser.add_argument(
187
+ "--init_training_state",
188
+ default=None,
189
+ help=(
190
+ "Load only the FP32 Predictor model state from training_latest.pt "
191
+ "while starting a fresh optimizer, scheduler, and step counter"
192
+ ),
193
+ )
194
+ parser.add_argument("--resume", default=None)
195
+ parser.add_argument("--swanlab_project", default="HY-WorldPlay-Predictor-v4")
196
+ parser.add_argument("--swanlab_workspace", default=None)
197
+ parser.add_argument(
198
+ "--swanlab_experiment",
199
+ default=(
200
+ "predictor-v4-prefeature-blocks0-53-bf16-8gpu-"
201
+ "flr1e-4-blr1e-5-schedsteps2000-swarmup100-bwarmup100"
202
+ ),
203
+ )
204
+ parser.add_argument(
205
+ "--swanlab_mode",
206
+ choices=("cloud", "offline", "local", "disabled"),
207
+ default="cloud",
208
+ )
209
+ return parser.parse_args()
210
+
211
+
212
+ def initialize_runtime_state() -> None:
213
+ class Args:
214
+ sage_blocks_range = "0-53"
215
+ use_sageattn = False
216
+ enable_torch_compile = False
217
+ use_fp8_gemm = False
218
+ quant_type = "fp8-per-block"
219
+ include_patterns = "double_blocks"
220
+ use_vae_parallel = False
221
+
222
+ initialize_infer_state(Args())
223
+
224
+
225
+ def cosine_factor(step: int, warmup: int, total: int) -> float:
226
+ if step < warmup:
227
+ return max(1e-8, float(step + 1) / max(1, warmup))
228
+ progress = min(1.0, float(step - warmup) / max(1, total - warmup))
229
+ return 0.5 * (1.0 + math.cos(math.pi * progress))
230
+
231
+
232
+ def make_scheduler(optimizer: torch.optim.Optimizer, args: argparse.Namespace) -> LambdaLR:
233
+ def block_factor(step: int) -> float:
234
+ if args.block_warmup_mode == "linear":
235
+ return cosine_factor(step, args.block_warmup_steps, args.max_steps)
236
+ return (
237
+ 0.0
238
+ if step < args.block_warmup_steps
239
+ else cosine_factor(step, args.scheduler_warmup_steps, args.max_steps)
240
+ )
241
+
242
+ return LambdaLR(
243
+ optimizer,
244
+ [
245
+ lambda step: cosine_factor(step, args.scheduler_warmup_steps, args.max_steps),
246
+ block_factor,
247
+ ],
248
+ )
249
+
250
+
251
+ def prune_snapshots(output_dir: Path, keep: int) -> None:
252
+ snapshots = sorted(output_dir.glob("predictor_step_*.safetensors"))
253
+ for path in snapshots[:-keep] if keep > 0 else snapshots:
254
+ path.unlink()
255
+
256
+
257
+ def distributed_mean(values: torch.Tensor) -> torch.Tensor:
258
+ dist.all_reduce(values, op=dist.ReduceOp.SUM)
259
+ values /= dist.get_world_size()
260
+ return values
261
+
262
+
263
+ def configure_model_precision(
264
+ model: torch.nn.Module,
265
+ *,
266
+ device: torch.device,
267
+ activation_dtype: torch.dtype,
268
+ fp32_trainable_params: bool,
269
+ ) -> None:
270
+ """Place frozen state in BF16 and optionally retain FP32 master parameters."""
271
+ model.to(device=device, dtype=activation_dtype)
272
+ if fp32_trainable_params:
273
+ with torch.no_grad():
274
+ for parameter in model.parameters():
275
+ if parameter.requires_grad:
276
+ parameter.data = parameter.data.to(dtype=torch.float32)
277
+
278
+
279
+ def validate_optimizer_precision(
280
+ optimizer: torch.optim.Optimizer,
281
+ *,
282
+ fp32_trainable_params: bool,
283
+ require_state: bool,
284
+ ) -> None:
285
+ expected = torch.float32 if fp32_trainable_params else torch.bfloat16
286
+ for group in optimizer.param_groups:
287
+ for parameter in group["params"]:
288
+ if parameter.dtype != expected:
289
+ raise RuntimeError(
290
+ f"Optimizer parameter dtype {parameter.dtype} != expected {expected}"
291
+ )
292
+ state = optimizer.state.get(parameter, {})
293
+ for name in ("exp_avg", "exp_avg_sq"):
294
+ value = state.get(name)
295
+ if value is None:
296
+ if require_state:
297
+ raise RuntimeError(f"Optimizer state {name} has not been initialized")
298
+ continue
299
+ if value.dtype != expected:
300
+ raise RuntimeError(
301
+ f"Optimizer state {name} dtype {value.dtype} != expected {expected}"
302
+ )
303
+
304
+
305
+ def configure_delta_only_trainability(model: torch.nn.Module) -> None:
306
+ if getattr(model, "previous_condition_mode", None) != "both_delta_gated":
307
+ raise ValueError("--train_delta_only requires both_delta_gated mode")
308
+ model.requires_grad_(False)
309
+ model.img_fusion.previous_step_norm.requires_grad_(True)
310
+ model.img_fusion.delta_mlp.requires_grad_(True)
311
+ model.img_fusion.delta_gate.requires_grad_(True)
312
+
313
+
314
+ def configure_fusion_residual_only_trainability(model: torch.nn.Module) -> None:
315
+ fusion_type = getattr(
316
+ getattr(model, "predictor_config", None), "predictor_fusion_type", None
317
+ )
318
+ if fusion_type != "concat_residual_gate_prev":
319
+ raise ValueError(
320
+ "--train_fusion_residual_only requires "
321
+ "concat_residual_gate_prev fusion"
322
+ )
323
+ model.requires_grad_(False)
324
+ model.img_fusion.gate_p.requires_grad_(True)
325
+ model.img_fusion.residual_mlp.requires_grad_(True)
326
+
327
+
328
+ def load_initial_training_state(
329
+ model: torch.nn.Module,
330
+ path: Path,
331
+ *,
332
+ source_blocks: tuple[int, int],
333
+ predictor_variant: str,
334
+ previous_condition_mode: str,
335
+ predictor_fusion_type: str,
336
+ fp32_trainable_params: bool,
337
+ ) -> None:
338
+ """Load only a prior FP32 Predictor model state for a fresh training branch."""
339
+ state = torch.load(path, map_location="cpu", weights_only=False)
340
+ if tuple(state["predictor_config"]["source_block_ids"]) != source_blocks:
341
+ raise ValueError("Initialization checkpoint source blocks do not match")
342
+ saved_variant = state.get("args", {}).get("predictor_variant", "v4")
343
+ if saved_variant != predictor_variant:
344
+ raise ValueError("Initialization checkpoint predictor variant does not match")
345
+ saved_mode = state.get("args", {}).get(
346
+ "previous_condition_mode",
347
+ state.get("predictor_config", {}).get("previous_condition_mode", "same_step"),
348
+ )
349
+ delta_from_same_step = (
350
+ saved_mode == "same_step" and previous_condition_mode == "both_delta_gated"
351
+ )
352
+ if saved_mode != previous_condition_mode and not delta_from_same_step:
353
+ raise ValueError(
354
+ "Initialization checkpoint previous-condition mode does not match: "
355
+ f"saved={saved_mode}, requested={previous_condition_mode}"
356
+ )
357
+ saved_fp32_master = bool(
358
+ state.get("args", {}).get("fp32_trainable_params", False)
359
+ )
360
+ if not saved_fp32_master or not fp32_trainable_params:
361
+ raise ValueError(
362
+ "Model-only initialization requires both the checkpoint and current "
363
+ "run to use FP32 trainable parameters"
364
+ )
365
+ saved_fusion_type = state.get("predictor_config", {}).get(
366
+ "predictor_fusion_type",
367
+ state.get("args", {}).get("predictor_fusion_type", "concat"),
368
+ )
369
+ concat_conversion = (
370
+ saved_fusion_type == "concat"
371
+ and predictor_fusion_type in {"projection_add", "token_gate_prev"}
372
+ and saved_mode == previous_condition_mode == "same_step"
373
+ )
374
+ concat_residual_initialization = (
375
+ saved_fusion_type == "concat"
376
+ and predictor_fusion_type == "concat_residual_gate_prev"
377
+ and saved_mode == previous_condition_mode == "same_step"
378
+ )
379
+ if (
380
+ saved_fusion_type != predictor_fusion_type
381
+ and not concat_conversion
382
+ and not concat_residual_initialization
383
+ ):
384
+ raise ValueError(
385
+ "Initialization checkpoint fusion type does not match: "
386
+ f"saved={saved_fusion_type}, requested={predictor_fusion_type}"
387
+ )
388
+ if concat_conversion:
389
+ conversion = load_concat_fusion_initialization(model, state["model"])
390
+ print(
391
+ json.dumps(
392
+ {
393
+ "fusion_initialization": "concat_conversion",
394
+ "discarded_concat_key_count": len(
395
+ conversion["discarded_concat_keys"]
396
+ ),
397
+ "fresh_fusion_key_count": len(conversion["fresh_fusion_keys"]),
398
+ },
399
+ sort_keys=True,
400
+ ),
401
+ flush=True,
402
+ )
403
+ return
404
+ if concat_residual_initialization:
405
+ conversion = load_concat_residual_gate_initialization(
406
+ model, state["model"]
407
+ )
408
+ print(
409
+ json.dumps(
410
+ {
411
+ "fusion_initialization": "concat_residual_gate",
412
+ "fresh_residual_key_count": len(
413
+ conversion["fresh_residual_keys"]
414
+ ),
415
+ },
416
+ sort_keys=True,
417
+ ),
418
+ flush=True,
419
+ )
420
+ return
421
+
422
+ result = model.load_state_dict(state["model"], strict=False)
423
+ trainable_names = {
424
+ name for name, parameter in model.named_parameters() if parameter.requires_grad
425
+ }
426
+ allowed_missing = set()
427
+ if delta_from_same_step:
428
+ allowed_missing = {
429
+ name
430
+ for name in trainable_names
431
+ if name.startswith("img_fusion.previous_step_norm.")
432
+ or name.startswith("img_fusion.delta_mlp.")
433
+ or name == "img_fusion.delta_gate"
434
+ }
435
+ with torch.no_grad():
436
+ model.img_fusion.previous_step_norm.load_state_dict(
437
+ model.img_fusion.previous_norm.state_dict(), strict=True
438
+ )
439
+ missing_trainable = sorted(
440
+ trainable_names.intersection(result.missing_keys) - allowed_missing
441
+ )
442
+ if result.unexpected_keys or missing_trainable:
443
+ raise RuntimeError(
444
+ "Initialization training-state mismatch; "
445
+ f"unexpected={result.unexpected_keys}, "
446
+ f"missing_trainable={missing_trainable}"
447
+ )
448
+
449
+
450
+ def main() -> None:
451
+ args = parse_args()
452
+ if args.predictor_arch == "atc" and (
453
+ args.predictor_variant != "v4" or args.previous_condition_mode != "same_step"
454
+ ):
455
+ raise ValueError("ATC requires predictor_variant=v4 and previous_condition_mode=same_step")
456
+ if args.predictor_variant != "v4" and args.previous_condition_mode != "same_step":
457
+ raise ValueError("previous_condition_mode ablations require predictor_variant=v4")
458
+ if args.predictor_variant != "v4" and args.predictor_fusion_type != "concat":
459
+ raise ValueError("Non-concat fusion types require predictor_variant=v4")
460
+ if (
461
+ args.predictor_fusion_type != "concat"
462
+ and args.previous_condition_mode != "same_step"
463
+ ):
464
+ raise ValueError("Non-concat fusion types require previous_condition_mode=same_step")
465
+ if args.resume and args.init_training_state:
466
+ raise ValueError("--resume and --init_training_state are mutually exclusive")
467
+ if args.train_delta_only and args.train_fusion_residual_only:
468
+ raise ValueError(
469
+ "--train_delta_only and --train_fusion_residual_only are mutually exclusive"
470
+ )
471
+ if args.train_fusion_residual_only and not (
472
+ args.init_training_state or args.resume
473
+ ):
474
+ raise ValueError(
475
+ "--train_fusion_residual_only requires --init_training_state or --resume"
476
+ )
477
+ if not torch.cuda.is_available():
478
+ raise RuntimeError("CUDA is required")
479
+ if not {"RANK", "WORLD_SIZE", "LOCAL_RANK"}.issubset(os.environ):
480
+ raise RuntimeError("Launch Predictor-v4 training with torchrun")
481
+ rank = int(os.environ["RANK"])
482
+ world_size = int(os.environ["WORLD_SIZE"])
483
+ local_rank = int(os.environ["LOCAL_RANK"])
484
+ torch.cuda.set_device(local_rank)
485
+ device = torch.device("cuda", local_rank)
486
+ dist.init_process_group(backend="nccl", device_id=device)
487
+ initialize_parallel_state(sp=1)
488
+ initialize_runtime_state()
489
+ dtype = torch.bfloat16
490
+
491
+ seed = args.seed + rank
492
+ random.seed(seed)
493
+ torch.manual_seed(seed)
494
+ torch.cuda.manual_seed_all(seed)
495
+ output_dir = Path(args.output_dir).resolve()
496
+ if rank == 0:
497
+ output_dir.mkdir(parents=True, exist_ok=True)
498
+ dist.barrier()
499
+ log_path = output_dir / "train_log.jsonl"
500
+
501
+ if args.predictor_variant == "disca":
502
+ dataset = PredictorDisCaPairDataset(
503
+ args.manifest,
504
+ source_block_ids=args.source_blocks,
505
+ max_records=args.max_records,
506
+ )
507
+ collate_fn = predictor_disca_collate
508
+ move_batch = move_disca_pair_to_device
509
+ else:
510
+ dataset = PredictorPrefeaturePairDataset(
511
+ args.manifest,
512
+ source_block_ids=args.source_blocks,
513
+ require_previous_chunk=True,
514
+ previous_condition_mode=args.previous_condition_mode,
515
+ max_records=args.max_records,
516
+ )
517
+ collate_fn = predictor_prefeature_collate
518
+ move_batch = move_prefeature_pair_to_device
519
+ bucket_sampler = None
520
+ distributed_sampler = None
521
+ if args.bucket_by_context:
522
+ bucket_sampler = DistributedContextBucketBatchSampler(
523
+ dataset,
524
+ batch_size=args.batch_size,
525
+ rank=rank,
526
+ world_size=world_size,
527
+ seed=args.seed,
528
+ drop_last=True,
529
+ )
530
+ loader = DataLoader(
531
+ dataset,
532
+ batch_sampler=bucket_sampler,
533
+ num_workers=args.num_workers,
534
+ pin_memory=False,
535
+ collate_fn=collate_fn,
536
+ )
537
+ else:
538
+ distributed_sampler = DistributedSampler(
539
+ dataset,
540
+ num_replicas=world_size,
541
+ rank=rank,
542
+ shuffle=True,
543
+ seed=args.seed,
544
+ drop_last=True,
545
+ )
546
+ loader = DataLoader(
547
+ dataset,
548
+ batch_size=args.batch_size,
549
+ sampler=distributed_sampler,
550
+ drop_last=True,
551
+ num_workers=args.num_workers,
552
+ pin_memory=False,
553
+ collate_fn=collate_fn,
554
+ )
555
+ if len(loader) == 0:
556
+ raise ValueError("Training loader has zero batches")
557
+
558
+ model_class = (
559
+ HYWorldPlayPredictorDisCa
560
+ if args.predictor_variant == "disca"
561
+ else HYWorldPlayPredictorV4
562
+ )
563
+ model = model_class(
564
+ PredictorV2Config(
565
+ source_block_ids=args.source_blocks,
566
+ previous_condition_mode=args.previous_condition_mode,
567
+ predictor_fusion_type=args.predictor_fusion_type,
568
+ gate_hidden_dim=args.gate_hidden_dim,
569
+ gate_initial_probability=args.gate_initial_probability,
570
+ predictor_arch=args.predictor_arch,
571
+ atc_variant=args.atc_variant,
572
+ atc_mlp_hidden_dim=args.atc_mlp_hidden_dim,
573
+ atc_gate_hidden_dim=args.atc_gate_hidden_dim,
574
+ atc_gate_initial_probability=args.atc_gate_initial_probability,
575
+ atc_transport_heads=args.atc_transport_heads,
576
+ atc_transport_window=args.atc_transport_window,
577
+ atc_transport_query_chunk_size=args.atc_transport_query_chunk_size,
578
+ atc_transport_initial_scale=args.atc_transport_initial_scale,
579
+ atc_transport_mode=args.atc_transport_mode,
580
+ atc_previous_scope=args.atc_previous_scope,
581
+ )
582
+ )
583
+ model.load_teacher_initialization(args.teacher_checkpoint)
584
+ if args.train_delta_only:
585
+ configure_delta_only_trainability(model)
586
+ if args.train_fusion_residual_only:
587
+ configure_fusion_residual_only_trainability(model)
588
+ configure_model_precision(
589
+ model,
590
+ device=device,
591
+ activation_dtype=dtype,
592
+ fp32_trainable_params=args.fp32_trainable_params,
593
+ )
594
+ if args.init_training_state:
595
+ initialization_path = Path(args.init_training_state).resolve()
596
+ if not initialization_path.is_file():
597
+ raise FileNotFoundError(initialization_path)
598
+ load_initial_training_state(
599
+ model,
600
+ initialization_path,
601
+ source_blocks=args.source_blocks,
602
+ predictor_variant=args.predictor_variant,
603
+ previous_condition_mode=args.previous_condition_mode,
604
+ predictor_fusion_type=args.predictor_fusion_type,
605
+ fp32_trainable_params=args.fp32_trainable_params,
606
+ )
607
+ model.enable_gradient_checkpointing(args.gradient_checkpointing)
608
+ model.train()
609
+ if args.predictor_variant == "v4" and any(
610
+ parameter.requires_grad for parameter in model.context_projector.parameters()
611
+ ):
612
+ raise RuntimeError("Frozen Context projector unexpectedly has trainable parameters")
613
+
614
+ fusion_parameters = [
615
+ parameter
616
+ for parameter in (
617
+ list(model.img_fusion.parameters())
618
+ + list(model.residual_out.parameters())
619
+ )
620
+ if parameter.requires_grad
621
+ ]
622
+ block_parameters = [
623
+ parameter
624
+ for parameter in model.double_blocks.parameters()
625
+ if parameter.requires_grad
626
+ ]
627
+ optimizer = AdamW(
628
+ [
629
+ {"params": fusion_parameters, "lr": args.fusion_lr},
630
+ {"params": block_parameters, "lr": args.blocks_lr},
631
+ ],
632
+ betas=(0.9, 0.95),
633
+ weight_decay=args.weight_decay,
634
+ )
635
+ validate_optimizer_precision(
636
+ optimizer,
637
+ fp32_trainable_params=args.fp32_trainable_params,
638
+ require_state=False,
639
+ )
640
+ scheduler = make_scheduler(optimizer, args)
641
+ global_step = 0
642
+ micro_step = 0
643
+ epoch = 0
644
+
645
+ if args.resume:
646
+ state = torch.load(args.resume, map_location="cpu", weights_only=False)
647
+ if tuple(state["predictor_config"]["source_block_ids"]) != args.source_blocks:
648
+ raise ValueError("Resume checkpoint source blocks do not match")
649
+ saved_variant = state.get("args", {}).get("predictor_variant", "v4")
650
+ if saved_variant != args.predictor_variant:
651
+ raise ValueError("Resume checkpoint predictor variant does not match")
652
+ saved_arch = state.get("predictor_config", {}).get(
653
+ "predictor_arch", state.get("args", {}).get("predictor_arch", "baseline")
654
+ )
655
+ saved_atc_variant = state.get("predictor_config", {}).get(
656
+ "atc_variant", state.get("args", {}).get("atc_variant", "full")
657
+ )
658
+ if saved_arch != args.predictor_arch:
659
+ raise ValueError(
660
+ f"Resume predictor architecture mismatch: saved={saved_arch}, "
661
+ f"requested={args.predictor_arch}"
662
+ )
663
+ if saved_arch == "atc" and saved_atc_variant != args.atc_variant:
664
+ raise ValueError(
665
+ f"Resume ATC variant mismatch: saved={saved_atc_variant}, "
666
+ f"requested={args.atc_variant}"
667
+ )
668
+ if saved_arch == "atc":
669
+ saved_cfg = state.get("predictor_config", {})
670
+ # Checkpoints written before the transport switch are local-window ATC.
671
+ saved_transport = saved_cfg.get("atc_transport_mode", "local")
672
+ saved_scope = saved_cfg.get("atc_previous_scope", "chunk")
673
+ if (saved_transport, saved_scope) != (args.atc_transport_mode, args.atc_previous_scope):
674
+ raise ValueError(
675
+ f"Resume ATC transport mismatch: saved={saved_transport}/{saved_scope}, "
676
+ f"requested={args.atc_transport_mode}/{args.atc_previous_scope}"
677
+ )
678
+ saved_mode = state.get("args", {}).get(
679
+ "previous_condition_mode",
680
+ state.get("predictor_config", {}).get(
681
+ "previous_condition_mode", "same_step"
682
+ ),
683
+ )
684
+ if saved_mode != args.previous_condition_mode:
685
+ raise ValueError(
686
+ "Resume checkpoint previous-condition mode does not match: "
687
+ f"saved={saved_mode}, requested={args.previous_condition_mode}"
688
+ )
689
+ saved_fusion_type = state.get("predictor_config", {}).get(
690
+ "predictor_fusion_type",
691
+ state.get("args", {}).get("predictor_fusion_type", "concat"),
692
+ )
693
+ if saved_fusion_type != args.predictor_fusion_type:
694
+ raise ValueError(
695
+ "Resume checkpoint fusion type does not match: "
696
+ f"saved={saved_fusion_type}, requested={args.predictor_fusion_type}"
697
+ )
698
+ saved_fp32_master = bool(
699
+ state.get("args", {}).get("fp32_trainable_params", False)
700
+ )
701
+ if saved_fp32_master != args.fp32_trainable_params:
702
+ raise ValueError("Resume checkpoint FP32-master setting does not match")
703
+ result = model.load_state_dict(state["model"], strict=False)
704
+ missing_trainable = [
705
+ key
706
+ for key in result.missing_keys
707
+ if key.startswith(("img_fusion.", "double_blocks.", "residual_out."))
708
+ ]
709
+ if result.unexpected_keys or missing_trainable:
710
+ raise RuntimeError(
711
+ "Resume model mismatch; "
712
+ f"unexpected={result.unexpected_keys}, missing={missing_trainable}"
713
+ )
714
+ optimizer.load_state_dict(state["optimizer"])
715
+ validate_optimizer_precision(
716
+ optimizer,
717
+ fp32_trainable_params=args.fp32_trainable_params,
718
+ require_state=True,
719
+ )
720
+ scheduler.load_state_dict(state["scheduler"])
721
+ global_step = int(state["global_step"])
722
+ micro_step = int(state.get("micro_step", global_step))
723
+ epoch = int(state.get("epoch", 0))
724
+
725
+ ddp = DDP(
726
+ model,
727
+ device_ids=[local_rank],
728
+ output_device=local_rank,
729
+ broadcast_buffers=False,
730
+ gradient_as_bucket_view=True,
731
+ find_unused_parameters=False,
732
+ )
733
+ trainable_parameters = sum(
734
+ parameter.numel() for parameter in model.parameters() if parameter.requires_grad
735
+ )
736
+ frozen_projector_parameters = sum(
737
+ parameter.numel() for parameter in getattr(model, "context_projector", ()).parameters()
738
+ ) if hasattr(model, "context_projector") else 0
739
+ run_config = {
740
+ **vars(args),
741
+ "manifest": str(Path(args.manifest).resolve()),
742
+ "teacher_checkpoint": str(Path(args.teacher_checkpoint).resolve()),
743
+ "output_dir": str(output_dir),
744
+ "source_blocks": list(args.source_blocks),
745
+ "predictor_version": (
746
+ "predictor_disca" if args.predictor_variant == "disca" else "v4_prefeature"
747
+ ),
748
+ "schema_version": (
749
+ "predictor_disca_text_kv_v1"
750
+ if args.predictor_variant == "disca"
751
+ else "predictor_context_prefeature_bf16_v1"
752
+ ),
753
+ "supervision_pairs": [[0, 1], [1, 2], [2, 3]],
754
+ "previous_condition_mode": args.previous_condition_mode,
755
+ "previous_condition_source": "offline_full_teacher_hidden",
756
+ "records": len(dataset.records),
757
+ "dataset_pairs": len(dataset),
758
+ "steps_per_epoch_per_rank": len(loader),
759
+ "world_size": world_size,
760
+ "per_rank_batch_size": args.batch_size,
761
+ "global_batch_size": (
762
+ args.batch_size * world_size * args.gradient_accumulation_steps
763
+ ),
764
+ "trainable_parameters": trainable_parameters,
765
+ "frozen_projector_parameters": frozen_projector_parameters,
766
+ "dtype": "bfloat16",
767
+ "activation_dtype": "bfloat16",
768
+ "trainable_parameter_dtype": (
769
+ "float32" if args.fp32_trainable_params else "bfloat16"
770
+ ),
771
+ "optimizer_moment_dtype": (
772
+ "float32" if args.fp32_trainable_params else "bfloat16"
773
+ ),
774
+ "inference_checkpoint_dtype": "bfloat16",
775
+ "gpu": torch.cuda.get_device_name(device),
776
+ }
777
+ if rank == 0:
778
+ config_path = output_dir / "train_config.json"
779
+ tmp = config_path.with_suffix(".json.tmp")
780
+ tmp.write_text(
781
+ json.dumps(run_config, indent=2, ensure_ascii=False, sort_keys=True) + "\n",
782
+ encoding="utf-8",
783
+ )
784
+ os.replace(tmp, config_path)
785
+ print(json.dumps(run_config, indent=2, default=str), flush=True)
786
+
787
+ swanlab_run = None
788
+ if rank == 0:
789
+ if args.swanlab_mode == "cloud":
790
+ api_key = os.environ.get("SWANLAB_API_KEY")
791
+ # Prefer an explicit ephemeral key, while also supporting the cached
792
+ # machine credential installed during environment setup.
793
+ swanlab.login(api_key=api_key, save=False)
794
+ swanlab_run = swanlab.init(
795
+ project=args.swanlab_project,
796
+ workspace=args.swanlab_workspace,
797
+ experiment_name=args.swanlab_experiment,
798
+ description=(
799
+ f"Predictor-{args.predictor_variant} training; three adjacent "
800
+ "denoise transitions; 8xH100 DDP"
801
+ ),
802
+ tags=[
803
+ f"predictor-{args.predictor_variant}",
804
+ (
805
+ "text-kv-only"
806
+ if args.predictor_variant == "disca"
807
+ else "context-prefeature"
808
+ ),
809
+ f"blocks-{args.source_blocks[0]}-{args.source_blocks[1]}",
810
+ f"fusion-{args.predictor_fusion_type}"[:20],
811
+ f"cond-{args.previous_condition_mode.replace('_step', 'step')}"[:20],
812
+ "batch16x8",
813
+ ],
814
+ config=run_config,
815
+ logdir=str(output_dir / "swanlab"),
816
+ mode=args.swanlab_mode,
817
+ )
818
+ print(
819
+ json.dumps(
820
+ {"swanlab_run_id": getattr(swanlab_run, "id", None)},
821
+ sort_keys=True,
822
+ ),
823
+ flush=True,
824
+ )
825
+
826
+ optimizer.zero_grad(set_to_none=True)
827
+ running = torch.zeros(18, device=device, dtype=torch.float64)
828
+ running_count = 0
829
+ # loss, hidden, velocity, batch_time, context_frames
830
+ try:
831
+ while global_step < args.max_steps:
832
+ epoch += 1
833
+ if bucket_sampler is not None:
834
+ bucket_sampler.set_epoch(epoch)
835
+ if distributed_sampler is not None:
836
+ distributed_sampler.set_epoch(epoch)
837
+ for item in loader:
838
+ if global_step >= args.max_steps:
839
+ break
840
+ inputs, target_hidden, target_velocity = move_batch(
841
+ item, device=device, dtype=dtype
842
+ )
843
+ started = time.perf_counter()
844
+ sync_gradients = (
845
+ (micro_step + 1) % args.gradient_accumulation_steps == 0
846
+ )
847
+ sync_context = (
848
+ torch.enable_grad()
849
+ if sync_gradients
850
+ else ddp.no_sync()
851
+ )
852
+ with sync_context:
853
+ with torch.autocast(device_type="cuda", dtype=dtype):
854
+ output = ddp(**inputs)
855
+ hidden_loss = F.mse_loss(
856
+ output["pred_hidden"].float(), target_hidden.float()
857
+ )
858
+ velocity_loss = F.mse_loss(
859
+ output["pred_velocity"].float(), target_velocity.float()
860
+ )
861
+ loss = (
862
+ args.hidden_weight * hidden_loss
863
+ + args.velocity_weight * velocity_loss
864
+ )
865
+ scaled_loss = loss / args.gradient_accumulation_steps
866
+ scaled_loss.backward()
867
+ micro_step += 1
868
+ gate_stats = output.get("gate_stats", {})
869
+ base_values = torch.tensor(
870
+ [
871
+ float(loss.detach()),
872
+ float(hidden_loss.detach()),
873
+ float(velocity_loss.detach()),
874
+ time.perf_counter() - started,
875
+ sum(item["context_frames"]) / len(item["context_frames"]),
876
+ ],
877
+ device=device,
878
+ dtype=torch.float64,
879
+ )
880
+ gate_names = (
881
+ "gate_p_mean",
882
+ "gate_p_std",
883
+ "gate_p_p10",
884
+ "gate_p_p50",
885
+ "gate_p_p90",
886
+ "gate_p_near_zero",
887
+ "gate_p_near_one",
888
+ )
889
+ gate_values = (
890
+ torch.stack([gate_stats[name] for name in gate_names]).to(
891
+ device=device, dtype=torch.float64
892
+ )
893
+ if gate_stats
894
+ else torch.zeros(7, device=device, dtype=torch.float64)
895
+ )
896
+ atc_stats = output.get("atc_stats", {})
897
+ atc_names = (
898
+ "delta_h_d_norm",
899
+ "delta_h_c_norm",
900
+ "gated_delta_h_c_norm",
901
+ "transport_residual_norm",
902
+ "anchor_distance_mean",
903
+ "transport_attention_entropy",
904
+ )
905
+ atc_values = (
906
+ torch.stack([atc_stats[name] for name in atc_names]).to(
907
+ device=device, dtype=torch.float64
908
+ )
909
+ if atc_stats
910
+ else torch.zeros(6, device=device, dtype=torch.float64)
911
+ )
912
+ running += torch.cat([base_values, gate_values, atc_values])
913
+ running_count += 1
914
+ del output, loss, scaled_loss, hidden_loss, velocity_loss
915
+ del inputs, target_hidden, target_velocity, item
916
+ if not sync_gradients:
917
+ continue
918
+
919
+ grad_norm = torch.nn.utils.clip_grad_norm_(
920
+ model.parameters(), args.grad_clip
921
+ )
922
+ optimizer.step()
923
+ if global_step == 0 or (
924
+ args.resume is not None and global_step == int(state["global_step"])
925
+ ):
926
+ validate_optimizer_precision(
927
+ optimizer,
928
+ fp32_trainable_params=args.fp32_trainable_params,
929
+ require_state=True,
930
+ )
931
+ scheduler.step()
932
+ optimizer.zero_grad(set_to_none=True)
933
+ global_step += 1
934
+
935
+ if global_step % args.log_every == 0 or global_step == 1:
936
+ torch.cuda.synchronize(device)
937
+ averaged = distributed_mean(running.clone())
938
+ averaged /= running_count
939
+ grad_value = torch.tensor(float(grad_norm), device=device)
940
+ dist.all_reduce(grad_value, op=dist.ReduceOp.MAX)
941
+ if rank == 0:
942
+ record = {
943
+ "global_step": global_step,
944
+ "epoch": epoch,
945
+ "loss": float(averaged[0]),
946
+ "hidden_mse": float(averaged[1]),
947
+ "velocity_mse": float(averaged[2]),
948
+ "avg_micro_time_s": float(averaged[3]),
949
+ "avg_context_frames": float(averaged[4]),
950
+ "grad_norm_max": float(grad_value),
951
+ "lr_fusion": optimizer.param_groups[0]["lr"],
952
+ "lr_blocks": optimizer.param_groups[1]["lr"],
953
+ "peak_memory_gib": torch.cuda.max_memory_allocated(device)
954
+ / 2**30,
955
+ }
956
+ if args.predictor_arch == "atc" or args.predictor_fusion_type in {
957
+ "token_gate_prev",
958
+ "concat_residual_gate_prev",
959
+ }:
960
+ record.update(
961
+ {
962
+ "gate_p_mean": float(averaged[5]),
963
+ "gate_p_std": float(averaged[6]),
964
+ "gate_p_p10": float(averaged[7]),
965
+ "gate_p_p50": float(averaged[8]),
966
+ "gate_p_p90": float(averaged[9]),
967
+ "gate_p_near_zero": float(averaged[10]),
968
+ "gate_p_near_one": float(averaged[11]),
969
+ }
970
+ )
971
+ if args.predictor_arch == "atc":
972
+ record.update(
973
+ {
974
+ name: float(averaged[12 + index])
975
+ for index, name in enumerate(atc_names)
976
+ }
977
+ )
978
+ with log_path.open("a", encoding="utf-8") as handle:
979
+ handle.write(json.dumps(record, sort_keys=True) + "\n")
980
+ swanlab.log(record, step=global_step)
981
+ print(json.dumps(record, sort_keys=True), flush=True)
982
+ running.zero_()
983
+ running_count = 0
984
+
985
+ should_save = (
986
+ global_step % args.save_every == 0
987
+ or global_step == args.max_steps
988
+ )
989
+ if should_save:
990
+ dist.barrier()
991
+ if rank == 0:
992
+ weights_path = (
993
+ output_dir
994
+ / f"predictor_step_{global_step:05d}.safetensors"
995
+ )
996
+ save_predictor_weights(
997
+ model,
998
+ weights_path,
999
+ floating_dtype=torch.bfloat16,
1000
+ )
1001
+ if args.save_optimizer_state:
1002
+ save_training_state(
1003
+ output_dir / "training_latest.pt",
1004
+ {
1005
+ "model": trainable_state_dict(model),
1006
+ "optimizer": optimizer.state_dict(),
1007
+ "scheduler": scheduler.state_dict(),
1008
+ "global_step": global_step,
1009
+ "micro_step": micro_step,
1010
+ "epoch": epoch,
1011
+ "args": vars(args),
1012
+ "predictor_config": model.config_dict,
1013
+ "weights_path": str(weights_path),
1014
+ },
1015
+ )
1016
+ prune_snapshots(output_dir, args.keep_snapshots)
1017
+ dist.barrier()
1018
+ except BaseException as error:
1019
+ if rank == 0 and swanlab_run is not None:
1020
+ swanlab.finish(error=str(error))
1021
+ raise
1022
+ else:
1023
+ if rank == 0 and swanlab_run is not None:
1024
+ swanlab.finish()
1025
+ finally:
1026
+ dist.destroy_process_group()
1027
+
1028
+
1029
+ if __name__ == "__main__":
1030
+ main()
train_predictor_v4_grouped_teacher_forcing.py ADDED
@@ -0,0 +1,825 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Eight-GPU grouped teacher forcing using only the offline v4 dataset."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import gc
8
+ import json
9
+ import os
10
+ import random
11
+ import time
12
+ from pathlib import Path
13
+
14
+ import swanlab
15
+ import torch
16
+ import torch.distributed as dist
17
+ import torch.nn.functional as F
18
+ from torch.nn.parallel import DistributedDataParallel as DDP
19
+ from torch.optim import AdamW
20
+
21
+ from hyvideo.commons.parallel_states import initialize_parallel_state
22
+ from models.predictor_v2 import PredictorV2Config
23
+ from models.predictor_v4 import HYWorldPlayPredictorV4
24
+ from predictor_training.checkpoint import (
25
+ TRAINABLE_PREFIXES,
26
+ load_concat_fusion_initialization,
27
+ load_concat_residual_gate_initialization,
28
+ save_predictor_weights,
29
+ save_training_state,
30
+ trainable_state_dict,
31
+ )
32
+ from predictor_training.timestep_rollout_dataset import (
33
+ load_case_ids,
34
+ load_timestep_rollout_record,
35
+ load_timestep_rollout_records,
36
+ move_timestep_rollout_record,
37
+ )
38
+ from train_predictor_v4 import (
39
+ configure_model_precision,
40
+ initialize_runtime_state,
41
+ make_scheduler,
42
+ prune_snapshots,
43
+ validate_optimizer_precision,
44
+ )
45
+
46
+
47
+ DEFAULT_DATA_ROOT = Path("/mnt/local_nvme/zoubin/cz/hyworldplay_predictor_prefeature_v4")
48
+ DEFAULT_CASES = Path("configs/rollout_overfit8_seed20260721_cases.jsonl")
49
+ DEFAULT_TEACHER = Path(
50
+ "/mnt/s3files/s3-us-west2-default/zoubin/cz/checkpoints/hy_worldplay/"
51
+ "huggingface/hub/models--tencent--HY-WorldPlay/snapshots/"
52
+ "f4c29235647707b571479a69b569e4166f9f5bf8/ar_distilled_action_model/"
53
+ "diffusion_pytorch_model.safetensors"
54
+ )
55
+ DEFAULT_INIT_STATE = Path(
56
+ "checkpoints/"
57
+ "predictor_v4_prefeature_blocks1-52_fp32master_8gpu_"
58
+ "flr1e-4_blr1e-5_schedsteps2000_swarmup100_bwarmup100_swanlab/"
59
+ "training_latest.pt"
60
+ )
61
+ SOURCE_BLOCKS = (1, 52)
62
+ EXPECTED_DENOISE_TIMESTEPS = (1000.0, 937.5, 833.3333129882812, 625.0)
63
+ EXPECTED_DENOISE_SIGMAS = (1.0, 0.9375, 0.8333333134651184, 0.625, 0.0)
64
+
65
+
66
+ def parse_args() -> argparse.Namespace:
67
+ parser = argparse.ArgumentParser(description=__doc__)
68
+ parser.add_argument(
69
+ "--training_mode",
70
+ choices=(
71
+ "grouped_teacher_forcing_offline_prefeature",
72
+ "detached_timestep_rollout_offline_ffff_targets",
73
+ ),
74
+ default="grouped_teacher_forcing_offline_prefeature",
75
+ )
76
+ parser.add_argument("--data_root", type=Path, default=DEFAULT_DATA_ROOT)
77
+ parser.add_argument("--cases_path", type=Path, default=DEFAULT_CASES)
78
+ parser.add_argument("--teacher_checkpoint", type=Path, default=DEFAULT_TEACHER)
79
+ parser.add_argument("--init_training_state", type=Path, default=DEFAULT_INIT_STATE)
80
+ parser.add_argument("--output_dir", type=Path, required=True)
81
+ parser.add_argument("--max_steps", type=int, default=1000)
82
+ parser.add_argument("--seed", type=int, default=20260721)
83
+ parser.add_argument("--fusion_lr", type=float, default=1e-4)
84
+ parser.add_argument(
85
+ "--predictor_fusion_type",
86
+ choices=(
87
+ "concat",
88
+ "projection_add",
89
+ "token_gate_prev",
90
+ "concat_residual_gate_prev",
91
+ ),
92
+ default="concat",
93
+ )
94
+ parser.add_argument("--gate_hidden_dim", type=int, default=256)
95
+ parser.add_argument("--gate_initial_probability", type=float, default=0.8)
96
+ parser.add_argument("--blocks_lr", type=float, default=1e-5)
97
+ parser.add_argument("--weight_decay", type=float, default=0.01)
98
+ parser.add_argument("--scheduler_warmup_steps", type=int, default=8)
99
+ parser.add_argument("--block_warmup_steps", type=int, default=8)
100
+ parser.add_argument("--hidden_weight", type=float, default=0.1)
101
+ parser.add_argument("--velocity_weight", type=float, default=1.0)
102
+ parser.add_argument("--grad_clip", type=float, default=1.0)
103
+ parser.add_argument(
104
+ "--gradient_checkpointing", action=argparse.BooleanOptionalAction, default=True
105
+ )
106
+ parser.add_argument(
107
+ "--fp32_trainable_params", action=argparse.BooleanOptionalAction, default=True
108
+ )
109
+ parser.add_argument("--save_every", type=int, default=100)
110
+ parser.add_argument("--keep_snapshots", type=int, default=3)
111
+ parser.add_argument("--log_every", type=int, default=1)
112
+ parser.add_argument("--resume", type=Path, default=None)
113
+ parser.add_argument("--swanlab_project", default="HY-WorldPlay-Predictor-v4")
114
+ parser.add_argument("--swanlab_workspace", default=None)
115
+ parser.add_argument("--swanlab_experiment", required=True)
116
+ parser.add_argument(
117
+ "--swanlab_mode",
118
+ choices=("cloud", "offline", "local", "disabled"),
119
+ default="cloud",
120
+ )
121
+ return parser.parse_args()
122
+
123
+
124
+ def distributed_average(values: torch.Tensor) -> torch.Tensor:
125
+ dist.all_reduce(values, op=dist.ReduceOp.SUM)
126
+ values /= dist.get_world_size()
127
+ return values
128
+
129
+
130
+ def load_stage1_initialization(
131
+ model: HYWorldPlayPredictorV4,
132
+ path: Path,
133
+ ) -> None:
134
+ """Load the Stage-1 FP32 master model while resetting optimizer/LR state."""
135
+ state = torch.load(path, map_location="cpu", weights_only=False)
136
+ if tuple(state["predictor_config"]["source_block_ids"]) != SOURCE_BLOCKS:
137
+ raise ValueError("Stage-1 initialization source blocks do not match")
138
+ saved_fusion_type = state.get("predictor_config", {}).get(
139
+ "predictor_fusion_type",
140
+ state.get("args", {}).get("predictor_fusion_type", "concat"),
141
+ )
142
+ target_fusion_type = model.predictor_config.predictor_fusion_type
143
+ if (
144
+ saved_fusion_type == "concat"
145
+ and target_fusion_type == "concat_residual_gate_prev"
146
+ ):
147
+ report = load_concat_residual_gate_initialization(model, state["model"])
148
+ print(
149
+ json.dumps(
150
+ {
151
+ "fusion_initialization": "concat_residual_gate",
152
+ "fresh_residual_key_count": len(
153
+ report["fresh_residual_keys"]
154
+ ),
155
+ },
156
+ sort_keys=True,
157
+ ),
158
+ flush=True,
159
+ )
160
+ return
161
+ if saved_fusion_type == "concat" and target_fusion_type != "concat":
162
+ report = load_concat_fusion_initialization(model, state["model"])
163
+ print(
164
+ json.dumps(
165
+ {
166
+ "fusion_initialization": "concat_conversion",
167
+ "discarded_concat_key_count": len(
168
+ report["discarded_concat_keys"]
169
+ ),
170
+ "fresh_fusion_key_count": len(report["fresh_fusion_keys"]),
171
+ },
172
+ sort_keys=True,
173
+ ),
174
+ flush=True,
175
+ )
176
+ return
177
+ if saved_fusion_type != target_fusion_type:
178
+ raise ValueError(
179
+ "Stage-1 initialization fusion type does not match: "
180
+ f"saved={saved_fusion_type}, requested={target_fusion_type}"
181
+ )
182
+ result = model.load_state_dict(state["model"], strict=False)
183
+ missing_trainable = [
184
+ key for key in result.missing_keys if key.startswith(TRAINABLE_PREFIXES)
185
+ ]
186
+ if result.unexpected_keys or missing_trainable:
187
+ raise RuntimeError(
188
+ "Stage-1 initialization mismatch; "
189
+ f"unexpected={result.unexpected_keys}, missing={missing_trainable}"
190
+ )
191
+
192
+
193
+ def assert_offline_timesteps(item: dict, device: torch.device) -> None:
194
+ expected = torch.tensor(
195
+ EXPECTED_DENOISE_TIMESTEPS, device=device, dtype=torch.float32
196
+ )
197
+ values = []
198
+ for step_id in range(4):
199
+ value = item["timesteps"][step_id]
200
+ if value.dtype != torch.float32 or value.numel() != 1:
201
+ raise TypeError(f"Offline timestep {step_id} is not one FP32 scalar")
202
+ values.append(value.reshape(1))
203
+ actual = torch.cat(values)
204
+ if not torch.equal(actual, expected):
205
+ raise RuntimeError(
206
+ "Offline denoise timesteps do not match the fixed four-step grid: "
207
+ f"actual={actual.detach().cpu().tolist()}"
208
+ )
209
+
210
+
211
+ @torch.no_grad()
212
+ def rebuild_offline_context_kv(
213
+ model: HYWorldPlayPredictorV4,
214
+ item: dict,
215
+ ) -> dict[int, dict[str, torch.Tensor]]:
216
+ """Run only v4's frozen projectors on saved BF16 Context prefeatures."""
217
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
218
+ rebuilt = model.context_projector(dict(item["context_prefeature"]))
219
+ result = {}
220
+ for block_id in SOURCE_BLOCKS:
221
+ result[block_id] = {
222
+ **dict(item["text_kv"][block_id]),
223
+ **rebuilt[block_id],
224
+ "context_valid_mask": item["context_prefeature"][block_id][
225
+ "context_valid_mask"
226
+ ],
227
+ }
228
+ return result
229
+
230
+
231
+ def model_inputs(
232
+ *,
233
+ pair: dict,
234
+ item: dict,
235
+ context_kv: dict[int, dict[str, torch.Tensor]],
236
+ ) -> dict:
237
+ return {
238
+ "target_model_input": pair["model_input"],
239
+ "anchor_hidden": pair["anchor_hidden"],
240
+ "target_frame_condition": pair["frame_condition"],
241
+ "target_viewmats": item["target_viewmats"],
242
+ "target_Ks": item["target_Ks"],
243
+ "rope_temporal_size": item["rope_temporal_size"],
244
+ "start_rope_start_idx": item["start_rope_start_idx"],
245
+ "context_kv": context_kv,
246
+ "previous_chunk_hidden": pair["previous_chunk_hidden"],
247
+ }
248
+
249
+
250
+ def empty_drift_metrics() -> dict[str, float]:
251
+ return {
252
+ "latent_drift_step2": 0.0,
253
+ "latent_drift_step3": 0.0,
254
+ "anchor_drift_step2": 0.0,
255
+ "anchor_drift_step3": 0.0,
256
+ }
257
+
258
+
259
+ def detached_scheduler_step(
260
+ sample: torch.Tensor,
261
+ velocity: torch.Tensor,
262
+ *,
263
+ step_id: int,
264
+ ) -> torch.Tensor:
265
+ """Apply the exact four-step Euler update and truncate its graph."""
266
+ if step_id not in (1, 2):
267
+ raise ValueError(f"Detached rollout only advances after steps 1/2, got {step_id}")
268
+ sigmas = torch.tensor(
269
+ EXPECTED_DENOISE_SIGMAS,
270
+ device=sample.device,
271
+ dtype=torch.float32,
272
+ )
273
+ with torch.no_grad():
274
+ next_sample = sample.detach().float() + velocity.detach().float() * (
275
+ sigmas[step_id + 1] - sigmas[step_id]
276
+ )
277
+ if next_sample.requires_grad:
278
+ raise RuntimeError("Detached scheduler output unexpectedly requires gradients")
279
+ return next_sample
280
+
281
+
282
+ def assert_offline_ffff_transitions(item: dict) -> None:
283
+ """Account for BF16 storage while checking the saved nominal trajectory."""
284
+ for step_id in (1, 2):
285
+ pair = item["teacher_forcing"][step_id]
286
+ expected = detached_scheduler_step(
287
+ pair["model_input"][:, :32],
288
+ pair["target_velocity"],
289
+ step_id=step_id,
290
+ )
291
+ target = item["teacher_forcing"][step_id + 1]["model_input"][:, :32]
292
+ error = (expected - target.float()).abs()
293
+ if float(error.max()) > 0.02 or float(error.mean()) > 0.002:
294
+ raise RuntimeError(
295
+ f"Offline FFFF transition {step_id}->{step_id + 1} does not "
296
+ f"match the fixed scheduler: max={float(error.max())}, "
297
+ f"mean={float(error.mean())}"
298
+ )
299
+
300
+
301
+ def train_grouped_record(
302
+ *,
303
+ predictor: DDP,
304
+ predictor_model: HYWorldPlayPredictorV4,
305
+ optimizer: torch.optim.Optimizer,
306
+ item: dict,
307
+ args: argparse.Namespace,
308
+ device: torch.device,
309
+ ) -> dict[str, float]:
310
+ started = time.perf_counter()
311
+ assert_offline_timesteps(item, device)
312
+ if tuple(sorted(item["teacher_forcing"])) != (1, 2, 3):
313
+ raise ValueError("Grouped record does not contain all three supervision pairs")
314
+ context_kv = rebuild_offline_context_kv(predictor_model, item)
315
+
316
+ optimizer.zero_grad(set_to_none=True)
317
+ losses: list[torch.Tensor] = []
318
+ hidden_losses: list[torch.Tensor] = []
319
+ velocity_losses: list[torch.Tensor] = []
320
+ for step_id in (1, 2, 3):
321
+ pair = item["teacher_forcing"][step_id]
322
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
323
+ output = predictor(
324
+ **model_inputs(pair=pair, item=item, context_kv=context_kv)
325
+ )
326
+ hidden_loss = F.mse_loss(
327
+ output["pred_hidden"].float(), pair["target_hidden"].float()
328
+ )
329
+ velocity_loss = F.mse_loss(
330
+ output["pred_velocity"].float(), pair["target_velocity"].float()
331
+ )
332
+ step_loss = (
333
+ args.hidden_weight * hidden_loss
334
+ + args.velocity_weight * velocity_loss
335
+ )
336
+ losses.append(step_loss)
337
+ hidden_losses.append(hidden_loss.detach())
338
+ velocity_losses.append(velocity_loss.detach())
339
+ del output, hidden_loss, velocity_loss
340
+
341
+ objective_loss = torch.stack(losses).mean()
342
+ objective_loss.backward()
343
+ if any(parameter.grad is not None for parameter in predictor_model.context_projector.parameters()):
344
+ raise RuntimeError("Frozen Context projector unexpectedly received gradients")
345
+ grad_norm = torch.nn.utils.clip_grad_norm_(
346
+ predictor_model.parameters(), args.grad_clip
347
+ )
348
+ if not torch.isfinite(grad_norm):
349
+ raise FloatingPointError(f"Non-finite grad norm: {float(grad_norm)}")
350
+ optimizer.step()
351
+ result = {
352
+ "loss": float(objective_loss.detach()),
353
+ "hidden_mse_step1": float(hidden_losses[0]),
354
+ "hidden_mse_step2": float(hidden_losses[1]),
355
+ "hidden_mse_step3": float(hidden_losses[2]),
356
+ "velocity_mse_step1": float(velocity_losses[0]),
357
+ "velocity_mse_step2": float(velocity_losses[1]),
358
+ "velocity_mse_step3": float(velocity_losses[2]),
359
+ "grad_norm": float(grad_norm),
360
+ "record_time_s": time.perf_counter() - started,
361
+ **empty_drift_metrics(),
362
+ }
363
+ del context_kv, losses, hidden_losses, velocity_losses, objective_loss
364
+ return result
365
+
366
+
367
+ def train_detached_rollout_record(
368
+ *,
369
+ predictor: DDP,
370
+ predictor_model: HYWorldPlayPredictorV4,
371
+ optimizer: torch.optim.Optimizer,
372
+ item: dict,
373
+ args: argparse.Namespace,
374
+ device: torch.device,
375
+ ) -> dict[str, float]:
376
+ """Roll out Predictor inputs while supervising against fixed offline FFFF outputs."""
377
+ started = time.perf_counter()
378
+ assert_offline_timesteps(item, device)
379
+ if tuple(sorted(item["teacher_forcing"])) != (1, 2, 3):
380
+ raise ValueError("Detached rollout record lacks one of steps 1/2/3")
381
+ assert_offline_ffff_transitions(item)
382
+ context_kv = rebuild_offline_context_kv(predictor_model, item)
383
+
384
+ optimizer.zero_grad(set_to_none=True)
385
+ losses: list[torch.Tensor] = []
386
+ hidden_losses: list[torch.Tensor] = []
387
+ velocity_losses: list[torch.Tensor] = []
388
+ latent_drifts: dict[int, torch.Tensor] = {}
389
+ anchor_drifts: dict[int, torch.Tensor] = {}
390
+
391
+ first_pair = item["teacher_forcing"][1]
392
+ rollout_latent = first_pair["model_input"][:, :32]
393
+ fixed_condition = first_pair["model_input"][:, 32:]
394
+ rollout_anchor = first_pair["anchor_hidden"]
395
+ for step_id in (1, 2, 3):
396
+ target_pair = item["teacher_forcing"][step_id]
397
+ if step_id > 1:
398
+ if rollout_latent.requires_grad or rollout_anchor.requires_grad:
399
+ raise RuntimeError(
400
+ f"Step {step_id} received a gradient-connected rollout state"
401
+ )
402
+ latent_drifts[step_id] = F.mse_loss(
403
+ rollout_latent.float(),
404
+ target_pair["model_input"][:, :32].float(),
405
+ ).detach()
406
+ anchor_drifts[step_id] = F.mse_loss(
407
+ rollout_anchor.float(), target_pair["anchor_hidden"].float()
408
+ ).detach()
409
+ model_input = (
410
+ target_pair["model_input"]
411
+ if step_id == 1
412
+ else torch.cat([rollout_latent, fixed_condition], dim=1)
413
+ )
414
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
415
+ output = predictor(
416
+ **model_inputs(
417
+ pair={
418
+ **target_pair,
419
+ "model_input": model_input,
420
+ "anchor_hidden": rollout_anchor,
421
+ },
422
+ item=item,
423
+ context_kv=context_kv,
424
+ )
425
+ )
426
+ hidden_loss = F.mse_loss(
427
+ output["pred_hidden"].float(), target_pair["target_hidden"].float()
428
+ )
429
+ velocity_loss = F.mse_loss(
430
+ output["pred_velocity"].float(), target_pair["target_velocity"].float()
431
+ )
432
+ step_loss = (
433
+ args.hidden_weight * hidden_loss
434
+ + args.velocity_weight * velocity_loss
435
+ )
436
+ losses.append(step_loss)
437
+ hidden_losses.append(hidden_loss.detach())
438
+ velocity_losses.append(velocity_loss.detach())
439
+ if step_id < 3:
440
+ rollout_anchor = output["pred_hidden"].detach()
441
+ rollout_latent = detached_scheduler_step(
442
+ rollout_latent, output["pred_velocity"], step_id=step_id
443
+ )
444
+ del output, hidden_loss, velocity_loss, model_input
445
+
446
+ objective_loss = torch.stack(losses).mean()
447
+ objective_loss.backward()
448
+ if any(parameter.grad is not None for parameter in predictor_model.context_projector.parameters()):
449
+ raise RuntimeError("Frozen Context projector unexpectedly received gradients")
450
+ grad_norm = torch.nn.utils.clip_grad_norm_(
451
+ predictor_model.parameters(), args.grad_clip
452
+ )
453
+ if not torch.isfinite(grad_norm):
454
+ raise FloatingPointError(f"Non-finite grad norm: {float(grad_norm)}")
455
+ optimizer.step()
456
+ result = {
457
+ "loss": float(objective_loss.detach()),
458
+ "hidden_mse_step1": float(hidden_losses[0]),
459
+ "hidden_mse_step2": float(hidden_losses[1]),
460
+ "hidden_mse_step3": float(hidden_losses[2]),
461
+ "velocity_mse_step1": float(velocity_losses[0]),
462
+ "velocity_mse_step2": float(velocity_losses[1]),
463
+ "velocity_mse_step3": float(velocity_losses[2]),
464
+ "latent_drift_step2": float(latent_drifts[2]),
465
+ "latent_drift_step3": float(latent_drifts[3]),
466
+ "anchor_drift_step2": float(anchor_drifts[2]),
467
+ "anchor_drift_step3": float(anchor_drifts[3]),
468
+ "grad_norm": float(grad_norm),
469
+ "record_time_s": time.perf_counter() - started,
470
+ }
471
+ del context_kv, losses, hidden_losses, velocity_losses
472
+ del latent_drifts, anchor_drifts, objective_loss, rollout_latent, rollout_anchor
473
+ return result
474
+
475
+
476
+ def main() -> None:
477
+ args = parse_args()
478
+ if args.max_steps <= 0:
479
+ raise ValueError("max_steps must be positive")
480
+ if not 0.0 < args.gate_initial_probability < 1.0:
481
+ raise ValueError("gate_initial_probability must be in (0, 1)")
482
+ if not torch.cuda.is_available():
483
+ raise RuntimeError("CUDA is required")
484
+ if not {"RANK", "WORLD_SIZE", "LOCAL_RANK"}.issubset(os.environ):
485
+ raise RuntimeError("Launch grouped teacher forcing with torchrun")
486
+
487
+ rank = int(os.environ["RANK"])
488
+ world_size = int(os.environ["WORLD_SIZE"])
489
+ local_rank = int(os.environ["LOCAL_RANK"])
490
+ if world_size != 8:
491
+ raise ValueError(f"The overfit8 experiment requires 8 ranks, got {world_size}")
492
+ torch.cuda.set_device(local_rank)
493
+ device = torch.device("cuda", local_rank)
494
+ dist.init_process_group(backend="nccl", device_id=device)
495
+ initialize_parallel_state(sp=1)
496
+ initialize_runtime_state()
497
+ random.seed(args.seed + rank)
498
+ torch.manual_seed(args.seed + rank)
499
+ torch.cuda.manual_seed_all(args.seed + rank)
500
+
501
+ data_root = args.data_root.resolve()
502
+ cases_path = args.cases_path.resolve()
503
+ manifest_path = data_root / "train_eval_manifest.jsonl"
504
+ case_ids = load_case_ids(cases_path)
505
+ if len(case_ids) != world_size:
506
+ raise ValueError("Exactly one selected case per rank is required")
507
+ case_id = case_ids[rank]
508
+ record_root, records = load_timestep_rollout_records(
509
+ manifest_path, case_id=case_id
510
+ )
511
+ required = [
512
+ data_root,
513
+ cases_path,
514
+ manifest_path,
515
+ args.teacher_checkpoint.resolve(),
516
+ args.init_training_state.resolve(),
517
+ ]
518
+ if args.resume is not None:
519
+ required.append(args.resume.resolve())
520
+ for path in required:
521
+ if not path.exists():
522
+ raise FileNotFoundError(path)
523
+
524
+ output_dir = args.output_dir.resolve()
525
+ if rank == 0:
526
+ output_dir.mkdir(parents=True, exist_ok=True)
527
+ dist.barrier()
528
+
529
+ model = HYWorldPlayPredictorV4(
530
+ PredictorV2Config(
531
+ source_block_ids=SOURCE_BLOCKS,
532
+ predictor_fusion_type=args.predictor_fusion_type,
533
+ gate_hidden_dim=args.gate_hidden_dim,
534
+ gate_initial_probability=args.gate_initial_probability,
535
+ )
536
+ )
537
+ # This selectively reads the two source blocks and fixed input/output modules
538
+ # from safetensors. It does not instantiate the Full Teacher model.
539
+ model.load_teacher_initialization(args.teacher_checkpoint.resolve())
540
+ configure_model_precision(
541
+ model,
542
+ device=device,
543
+ activation_dtype=torch.bfloat16,
544
+ fp32_trainable_params=args.fp32_trainable_params,
545
+ )
546
+ if args.resume is None:
547
+ load_stage1_initialization(model, args.init_training_state.resolve())
548
+ model.enable_gradient_checkpointing(args.gradient_checkpointing)
549
+ model.train()
550
+ if any(parameter.requires_grad for parameter in model.context_projector.parameters()):
551
+ raise RuntimeError("Context projector must remain frozen")
552
+
553
+ fusion_parameters = list(model.img_fusion.parameters()) + list(
554
+ model.residual_out.parameters()
555
+ )
556
+ block_parameters = [
557
+ parameter
558
+ for parameter in model.double_blocks.parameters()
559
+ if parameter.requires_grad
560
+ ]
561
+ optimizer = AdamW(
562
+ [
563
+ {"params": fusion_parameters, "lr": args.fusion_lr},
564
+ {"params": block_parameters, "lr": args.blocks_lr},
565
+ ],
566
+ betas=(0.9, 0.95),
567
+ weight_decay=args.weight_decay,
568
+ )
569
+ scheduler = make_scheduler(optimizer, args)
570
+ global_step = 0
571
+ epoch = 0
572
+ batch_in_epoch = 0
573
+ if args.resume is not None:
574
+ state = torch.load(args.resume, map_location="cpu", weights_only=False)
575
+ saved_args = state.get("args", {})
576
+ if saved_args.get("training_mode") != args.training_mode:
577
+ raise ValueError("Resume checkpoint training mode does not match")
578
+ if bool(saved_args.get("fp32_trainable_params")) != args.fp32_trainable_params:
579
+ raise ValueError("Resume precision mode does not match")
580
+ saved_fusion_type = state.get("predictor_config", {}).get(
581
+ "predictor_fusion_type",
582
+ saved_args.get("predictor_fusion_type", "concat"),
583
+ )
584
+ if saved_fusion_type != args.predictor_fusion_type:
585
+ raise ValueError("Resume fusion type does not match")
586
+ result = model.load_state_dict(state["model"], strict=False)
587
+ missing_trainable = [
588
+ key for key in result.missing_keys if key.startswith(TRAINABLE_PREFIXES)
589
+ ]
590
+ if result.unexpected_keys or missing_trainable:
591
+ raise RuntimeError(
592
+ "Resume model mismatch; "
593
+ f"unexpected={result.unexpected_keys}, missing={missing_trainable}"
594
+ )
595
+ optimizer.load_state_dict(state["optimizer"])
596
+ scheduler.load_state_dict(state["scheduler"])
597
+ global_step = int(state["global_step"])
598
+ epoch = int(state.get("epoch", 0))
599
+ batch_in_epoch = int(state.get("batch_in_epoch", 0))
600
+ validate_optimizer_precision(
601
+ optimizer,
602
+ fp32_trainable_params=args.fp32_trainable_params,
603
+ require_state=global_step > 0,
604
+ )
605
+ predictor = DDP(
606
+ model,
607
+ device_ids=[local_rank],
608
+ output_device=local_rank,
609
+ broadcast_buffers=False,
610
+ gradient_as_bucket_view=True,
611
+ find_unused_parameters=False,
612
+ )
613
+
614
+ detached_rollout = (
615
+ args.training_mode == "detached_timestep_rollout_offline_ffff_targets"
616
+ )
617
+ run_args = vars(args)
618
+ run_config = {
619
+ **run_args,
620
+ "data_root": str(data_root),
621
+ "cases_path": str(cases_path),
622
+ "output_dir": str(output_dir),
623
+ "world_size": world_size,
624
+ "case_ids": case_ids,
625
+ "records_per_rank": len(records),
626
+ "groups_per_optimizer_step_per_rank": 1,
627
+ "pairs_per_group": 3,
628
+ "source_blocks": list(SOURCE_BLOCKS),
629
+ "initialization": "stage1_step2000_model_only_optimizer_reset",
630
+ "runtime_full_teacher_loaded": False,
631
+ "teacher_checkpoint_usage": "selective_predictor_initialization_only",
632
+ "rollout_sidecar_loaded": False,
633
+ "context_source": "offline_bf16_prefeature_plus_offline_text_kv",
634
+ "context_projection": "frozen_v4_projector_once_per_group",
635
+ "targets": "offline_full_teacher_steps_1_2_3",
636
+ "input_state_source": (
637
+ "step1_offline_then_predictor_detached_rollout"
638
+ if detached_rollout else "offline_full_teacher_each_step"
639
+ ),
640
+ "anchor_source": (
641
+ "step1_offline_then_predictor_hidden_detached"
642
+ if detached_rollout else "offline_full_teacher_each_step"
643
+ ),
644
+ "timestep_graph": (
645
+ "P1|P2|P3_detached_state_rollout_one_mean_loss"
646
+ if detached_rollout
647
+ else "three_independent_offline_pairs_one_mean_loss"
648
+ ),
649
+ "offline_target_state_matches_rollout_input": not detached_rollout,
650
+ "offline_denoise_timesteps": list(EXPECTED_DENOISE_TIMESTEPS),
651
+ "offline_denoise_sigmas": list(EXPECTED_DENOISE_SIGMAS),
652
+ "offline_denoise_timestep_dtype": "float32",
653
+ "activation_dtype": "bfloat16",
654
+ "trainable_parameter_dtype": (
655
+ "float32" if args.fp32_trainable_params else "bfloat16"
656
+ ),
657
+ }
658
+ if rank == 0:
659
+ (output_dir / "train_config.json").write_text(
660
+ json.dumps(run_config, indent=2, ensure_ascii=False, default=str, sort_keys=True)
661
+ + "\n",
662
+ encoding="utf-8",
663
+ )
664
+ print(json.dumps(run_config, indent=2, ensure_ascii=False, default=str), flush=True)
665
+
666
+ swanlab_run = None
667
+ if rank == 0:
668
+ if args.swanlab_mode == "cloud":
669
+ swanlab.login(api_key=os.environ.get("SWANLAB_API_KEY"), save=False)
670
+ swanlab_run = swanlab.init(
671
+ project=args.swanlab_project,
672
+ workspace=args.swanlab_workspace,
673
+ experiment_name=args.swanlab_experiment,
674
+ description=(
675
+ "v4 detached timestep rollout with fixed offline FFFF targets; "
676
+ "offline prefeature path; no runtime Full Teacher"
677
+ if detached_rollout
678
+ else (
679
+ "v4 grouped teacher forcing on overfit8; fully offline Stage-1 "
680
+ "prefeature path; no runtime Full Teacher"
681
+ )
682
+ ),
683
+ tags=[
684
+ "predictor-v4",
685
+ "detached-rollout" if detached_rollout else "grouped-teacher",
686
+ "offline-prefeature",
687
+ "overfit8",
688
+ "8gpu",
689
+ f"fusion-{args.predictor_fusion_type}"[:20],
690
+ ],
691
+ config=run_config,
692
+ logdir=str(output_dir / "swanlab"),
693
+ mode=args.swanlab_mode,
694
+ )
695
+
696
+ log_path = output_dir / "train_log.jsonl"
697
+ metric_names = (
698
+ "loss",
699
+ "hidden_mse_step1",
700
+ "hidden_mse_step2",
701
+ "hidden_mse_step3",
702
+ "velocity_mse_step1",
703
+ "velocity_mse_step2",
704
+ "velocity_mse_step3",
705
+ "latent_drift_step2",
706
+ "latent_drift_step3",
707
+ "anchor_drift_step2",
708
+ "anchor_drift_step3",
709
+ "record_time_s",
710
+ )
711
+ try:
712
+ resume_epoch = epoch
713
+ resume_batch = batch_in_epoch
714
+ while global_step < args.max_steps:
715
+ order = list(range(len(records)))
716
+ random.Random(args.seed + rank + epoch * 100003).shuffle(order)
717
+ for loader_index, record_index in enumerate(order):
718
+ if epoch == resume_epoch and loader_index < resume_batch:
719
+ continue
720
+ if global_step >= args.max_steps:
721
+ break
722
+ cpu_item = load_timestep_rollout_record(
723
+ record_root,
724
+ records[record_index],
725
+ replay_step=None,
726
+ grouped_teacher_forcing=True,
727
+ source_block_ids=SOURCE_BLOCKS,
728
+ )
729
+ item = move_timestep_rollout_record(cpu_item, device)
730
+ torch.cuda.reset_peak_memory_stats(device)
731
+ train_function = (
732
+ train_detached_rollout_record
733
+ if detached_rollout else train_grouped_record
734
+ )
735
+ metrics = train_function(
736
+ predictor=predictor, predictor_model=model,
737
+ optimizer=optimizer, item=item, args=args, device=device,
738
+ )
739
+ if global_step == 0:
740
+ validate_optimizer_precision(
741
+ optimizer,
742
+ fp32_trainable_params=args.fp32_trainable_params,
743
+ require_state=True,
744
+ )
745
+ scheduler.step()
746
+ global_step += 1
747
+ batch_in_epoch = loader_index + 1
748
+
749
+ values = torch.tensor(
750
+ [metrics[name] for name in metric_names],
751
+ device=device,
752
+ dtype=torch.float64,
753
+ )
754
+ averaged = distributed_average(values)
755
+ grad_max = torch.tensor(metrics["grad_norm"], device=device)
756
+ dist.all_reduce(grad_max, op=dist.ReduceOp.MAX)
757
+ if rank == 0 and (
758
+ global_step % args.log_every == 0 or global_step == 1
759
+ ):
760
+ record_log = {
761
+ "global_step": global_step,
762
+ "epoch": epoch,
763
+ "batch_in_epoch": batch_in_epoch,
764
+ **{
765
+ name: float(averaged[index])
766
+ for index, name in enumerate(metric_names)
767
+ },
768
+ "grad_norm_max": float(grad_max),
769
+ "lr_fusion": optimizer.param_groups[0]["lr"],
770
+ "lr_blocks": optimizer.param_groups[1]["lr"],
771
+ "peak_memory_gib": torch.cuda.max_memory_allocated(device) / 2**30,
772
+ }
773
+ with log_path.open("a", encoding="utf-8") as handle:
774
+ handle.write(json.dumps(record_log, sort_keys=True) + "\n")
775
+ swanlab.log(record_log, step=global_step)
776
+ print(json.dumps(record_log, sort_keys=True), flush=True)
777
+
778
+ should_save = (
779
+ global_step % args.save_every == 0
780
+ or global_step == args.max_steps
781
+ )
782
+ if should_save:
783
+ dist.barrier()
784
+ if rank == 0:
785
+ weights_path = output_dir / f"predictor_step_{global_step:05d}.safetensors"
786
+ save_predictor_weights(
787
+ model, weights_path, floating_dtype=torch.bfloat16
788
+ )
789
+ save_training_state(
790
+ output_dir / "training_latest.pt",
791
+ {
792
+ "model": trainable_state_dict(model),
793
+ "optimizer": optimizer.state_dict(),
794
+ "scheduler": scheduler.state_dict(),
795
+ "global_step": global_step,
796
+ "epoch": epoch,
797
+ "batch_in_epoch": batch_in_epoch,
798
+ "args": run_args,
799
+ "predictor_config": model.config_dict,
800
+ "weights_path": str(weights_path),
801
+ },
802
+ )
803
+ prune_snapshots(output_dir, args.keep_snapshots)
804
+ dist.barrier()
805
+ del cpu_item, item, metrics, values, averaged, grad_max
806
+ gc.collect()
807
+ epoch += 1
808
+ batch_in_epoch = 0
809
+ resume_batch = 0
810
+ except BaseException as error:
811
+ if rank == 0 and swanlab_run is not None:
812
+ try:
813
+ swanlab.finish(error=str(error))
814
+ except Exception:
815
+ pass
816
+ raise
817
+ else:
818
+ if rank == 0 and swanlab_run is not None:
819
+ swanlab.finish()
820
+ finally:
821
+ dist.destroy_process_group()
822
+
823
+
824
+ if __name__ == "__main__":
825
+ main()