Instructions to use Cccccz/HY with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use Cccccz/HY with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("Cccccz/HY", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
Upload batch 65: 500 files (0.01 GiB)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- tools/run_confidence_validation25_reference_single.sh +143 -0
- tools/run_offline_ffff_targets_validation25_evaluation.sh +123 -0
- tools/run_oldenv_validation_reruns_sequential.sh +76 -0
- tools/run_predictor_confidence_experiment.sh +55 -0
- tools/run_predictor_v4_atc_full_experiment.sh +24 -0
- tools/run_predictor_v4_atc_global_sequential_8gpu.sh +14 -0
- tools/run_predictor_v4_detached_timestep_rollout_offline_targets_overfit8.sh +116 -0
- tools/run_predictor_v4_grouped_teacher_forcing_overfit8.sh +99 -0
- tools/run_predictor_v4_timestep_rollout_detached_overfit8_experiments.sh +11 -0
- tools/run_predictor_v4_timestep_rollout_overfit8_experiments.sh +115 -0
- tools/run_predictor_v4_validation5_rollout_comparison.sh +176 -0
- tools/run_stage1_continued_validation25_evaluation.sh +123 -0
- tools/run_stage1_previous_condition_ablation.sh +84 -0
- tools/run_stage1_previous_condition_retrain_validation.sh +66 -0
- tools/run_vbench_atc_validation25.sh +136 -0
- tools/run_vbench_c1_threshold_validation25.sh +92 -0
- tools/run_vbench_previous_condition_none_step0300_validation25.sh +110 -0
- tools/run_vbench_previous_condition_validation25.sh +137 -0
- tools/run_vbench_token_gate_validation25.sh +83 -0
- tools/run_vbench_validation25_chunk_timestep_step2000.sh +108 -0
- tools/run_vbench_validation25_core.sh +111 -0
- tools/run_vbench_validation25_groups.sh +75 -0
- tools/run_wide_c1_r33_validation25_reference.sh +84 -0
- tools/run_wide_c2_hybrid_validation25_reference.sh +116 -0
- tools/summarize_atc_rollout_ckpt_sweep.py +37 -0
- tools/summarize_c1_threshold_validation25.py +167 -0
- tools/summarize_predictor_timestep_rollout_overfit8.py +71 -0
- tools/summarize_vbench_validation25_chunk_timestep_step2000.py +205 -0
- tools/summarize_vbench_validation25_core.py +243 -0
- tools/summarize_vbench_validation25_pair.py +211 -0
- tools/supervise_predictor_v4_fp32master_training.sh +34 -0
- tools/supervise_predictor_v4_rollout_fppf_overfit8_training.sh +34 -0
- tools/supervise_predictor_v4_rollout_overfit8_training.sh +34 -0
- tools/supervise_predictor_v4_rollout_training.sh +34 -0
- tools/supervise_previous_vbench_validation_predictors.sh +50 -0
- tools/supervise_training_tests_then_helios.sh +244 -0
- tools/sync_predictor_prefeature_to_s3.sh +44 -0
- tools/train_evaluate_predictor_confidence.py +281 -0
- tools/validate_predictor_dataset.py +52 -0
- tools/validate_predictor_prefeature_dataset.py +80 -0
- tools/validate_predictor_text_kv_all54.py +58 -0
- tools/validate_predictor_v2_dataset.py +97 -0
- tools/video_review_server.py +1031 -0
- train.md +592 -0
- train_predictor.py +271 -0
- train_predictor_disca.py +42 -0
- train_predictor_v2.py +374 -0
- train_predictor_v3.py +14 -0
- train_predictor_v4.py +1030 -0
- 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()
|