File size: 14,021 Bytes
51ac7cc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | # prep β GameCrafter-Data-v10 β `data/gc25d_v10/` (SPEC Β§1βΒ§5)
Status 2026-10-22: code done and tested. MINI dataset built, verified and marked READY (`data/gc25d_v10_mini/READY`).
**The full prep has not been run yet**; its command is below.
**Open decision before stage 3: `ext_look_change`.** 42 % of the DMD pool has a semantic-map pop at cell 21. See *Notes for consumers*.
Review-fix pass (same day; MINI outputs unchanged, no rebuild needed):
* Source ids.png values are now asserted β {0..6,10,11}, in prep and in the verify spot check.
* Killing the main process no longer leaves orphan ranks: there is a SIGTERM handler, and ranks and workers set PDEATHSIG.
* A torn last row in `rows_rank*.jsonl` no longer breaks the merge on resume.
* The `ext_look_change` note has been corrected (it is a real pop), and verify prints the counts.
Smoke logs are in `outputs/_smoke/prep/fixer/`.
## What
| file | role |
|---|---|
| `code/gc25d/prep_v10.py` | the prep CLI: one spawned process per GPU; DataLoader workers do the CPU work; the main thread runs the VAE; atomic writes; resume; merge |
| `code/gc25d/verify_prep.py` | checks index, files and shapes on every clip, then GPU spot checks (VAE PSNR, bit-exact re-encode, cell 0, label_parts, capsule re-render IoU) |
| `code/gc25d/tests/test_prep.py` | 26 CPU tests (~10 s): block/prompt alignment, ext validity, split/drop, atomic write / resume / merge, torn-row resume, raw ids check, PDEATHSIG, VAE input LUT, one real clip (no VAE) |
The output layout follows SPEC Β§5 exactly. Additions beyond SPEC:
* `index/clips.jsonl` also has `look`, `max_frame_offset_cm`, `ext_look_change`, `mirrored_world`, `player_px_mean`, `boss_visible_frac`,
`player_hidden_frac` (non-dead frames with 0 player px). `drop_reason` is `"buried_player"` or `null`.
* `meta/*.npz` also has `bot_mode` (str [F]) and `frames` (int32 [F], absolute frame index).
* `index/` also holds `rows_rank<r>.jsonl` (per-rank rows used for resume), `prep_args.json`, `prep_stats_<time>.json` (throughput) and
`verify_report.json`.
* `latents/*.pt` stores `actions_block` as a torch uint8 tensor, so `torch.load(..., weights_only=True)` works.
## How (conventions that matter)
* Per clip, inside a worker (`OMP_NUM_THREADS=1`, `cv2.setNumThreads(1)`, one LRU-cached `RunTars` per run with shards indexed lazily):
1. Read the per-frame tar jsons sβ¦s+108.
2. `has_ext` β all 109 frames exist, `episode` and `camera_variant` are constant, and block s//4+26 exists.
3. Decode jpg (cv2, BGRβRGB) β `geometry.resample_rgb`. Decode ids.png (UNCHANGED), assert the full-resolution values β {0..6,10,11}
(`check_raw_ids`, data_v10.md Β§8.2 step 3), then `resample_ids` β `capsules.label_parts`, with the output asserted β
`model.valid_raw_ids("parts")` minus 255. Without the source check, a stray raw value in 12..21 would pass straight through
`label_parts` as a body-part id.
4. Build the meta from the jsons (`capsules_to_array`, `ground_z` = min over player capsules of endpoint z β r, `player_px`/`boss_px` from
the parts map).
5. Write `cond_ext` β `meta` β `cond`, each with `np.savez_compressed` to `*.tmp<pid>` and then `os.replace`.
* VAE in the rank's main thread: `WanVideoPipeline.from_pretrained(... ModelConfig(path=Wan2.2_VAE.pth))`, then `vae.model.float()`. The
PyTorch default TF32 flags are left alone (logged as `cudnn.allow_tf32=True matmul.allow_tf32=False`).
* Input: uint8 β [β1,1] through a 256-entry LUT computed on the CPU exactly as `infer.encode_video` does. On CUDA, `div_(127.5)` becomes a
multiply-by-reciprocal and can be 1 ulp off, which the LUT avoids.
* Encode: `vae.single_encode(x[None])`. This is the body of `vae.encode([x], tiled=False)` without its 388 MB GPUβCPUβGPU round trip.
* The latent is cast to bf16 and written to `latents/<clip>.pt` atomically. The index row is appended **after** the latent.
* **Checked:** re-encoding with the official `vae.encode([x_cpu], tiled=False)` is bit-identical on all 8 spot clips.
* Resume: a clip is skipped if it has a row and all of its outputs exist. If all outputs exist but the row is missing, the worker rebuilds the
row from the jsons and meta without decoding. Partial or tmp files are simply redone. `--merge_only` rebuilds `clips.jsonl` and `runs.json`.
A rows file left with a torn last line by a kill is closed with a `'\n'` before appending (`open_rows`). Otherwise the first resumed row
would be glued to the fragment, both would be dropped, and the final merge would fail on a "missing" clip.
* Stopping: `kill <main pid>` (SIGTERM) or Ctrl-C stops the run cleanly. Main turns SIGTERM into SystemExit, and a `finally` terminates and
joins every rank. Ranks and their DataLoader workers also set `PR_SET_PDEATHSIG` (`die_with_parent`), so they exit even if main is
SIGKILLed. Before this fix, killing main left the ranks running as orphans with PPID 1. They kept writing into the output directory and
raced a resume. Re-run the same command to resume. A kill can leave `*.tmp<pid>` files behind. They never count as outputs, so the clip
is redone, and verify warns about leftovers. Delete them with `find <out> -name '*.tmp[0-9]*' -delete` once no prep is running.
* Ranks get contiguous chunks of the sorted clip list, which keeps tar reads local and balances the load.
* `--gpus` lists indices within `CUDA_VISIBLE_DEVICES`.
## Numbers (H200, measured)
**MINI** (4 runs, 200 clips, GPUs 0,1, 12 workers/GPU, idle machine; `outputs/_smoke/prep/mini_build.log`):
* 113 s wall, including about 10 s of startup.
* **0.997 clips/s/GPU steady**, VAE-bound: VAE 0.98 s/clip, the main thread waited on data 0.002 s/clip.
* Worker CPU 3.95 s/clip: json 0.21, decode+resample 1.09, label_parts 1.94, npz 0.63. Total including the VAE process: **5.1 core-s/clip**.
* Machine CPU 5.7 % of 192 cores. GPU peak 6.5 GiB/rank. VAE load 4.7 s.
**Contention test** (`stress_*.log`). A CPU-only `--no_vae` job ran 6 ranks Γ 12 workers (72 busy workers, 17.5 clips/s, no GPU use) next to
the 2 VAE ranks, while other agents were also training on GPUs 2β7:
* The VAE ranks still ran **0.96β1.00 clips/s/GPU**. The main thread waited 0.025 s/clip.
* Worker time rose to 4.6β5.4 s/clip, at 40 % machine CPU.
* Twelve workers per GPU leave more than 2Γ CPU headroom.
**Extrapolation for the full run** (12,800 clips, 8 GPUs):
* Wall time: 12,800 / (8 Γ 0.96β1.0) = 1,600β1,670 s, plus about 1 min of startup and merge, so **β 28β30 min**.
* CPU: about 8 clips/s Γ 5β6 core-s β **40β50 busy cores** (~25 % of the machine).
* Disk read: β 23 MB/clip, so about 185 MB/s.
* Output: v10 4.11 MB/clip and v9 4.97 MB/clip (latent 3.15, cond 0.69/1.37, cond_ext 0.21/0.40, meta 0.06), so **β 59 GB**.
* Expected `has_ext`: v10 87 % / v9 84 % in MINI.
**verify_prep on MINI** (`mini_verify.log`, 141 s): 0 fails, 0 warnings.
* All 200 clips pass the per-clip checks.
* Player/boss px per frame at 832Γ480: v10 3,410/3,370, v9 3,479/4,853.
* 8 spot clips:
* Re-encode is exact (max diff 0). Latent cell 0 equals encode(frame 0) at bf16 (exact fraction 1.0; rel L2 1.7e-3 is the bf16 storage
error).
* VAE recon PSNR 21.3β36.4 dB, mean 27.2. Photoreal R10_s1512 clips score 21β22 dB; a flat-shaded v9 look scores 36. The SPEC's
23β26 dB is only typical, so the check fails below 18 dB and warns outside 21β40 dB.
* `collapse_parts(cond) == resample_ids(ids.png)` on every frame including extension frames, with 0 mismatching px. Re-running
`label_parts` from the tar json gives an exact match, and from meta 1.0 agreement on character pixels.
* Capsule re-render from meta only (`render_capsules`):
* v10 player: unoccluded median IoU **0.9978**, pooled 0.9977, GT β render 1.0000. v10 boss: 0.9967.
* v9 player: pooled IoU 0.65 because scene occlusion hides 35 % of the rendered px; on unoccluded frames it is still 0.9977.
**Review-fix smoke** (`outputs/_smoke/prep/fixer/`, GPUs 0,1):
* `pytest.log`: 26 passed in 10.4 s. A mutation check (stubbing `die_with_parent` / `open_rows` out) makes both new tests fail.
* `kill_resume_smoke.log`: prep on R10_s1501 + P9_golden_s2909, 20 clips each.
* `kill -TERM main` after the first progress line: main exits 143 and prints the resume hint. All 15 descendants (2 ranks + 12 workers +
tracker) are gone within 0.5 s.
* `kill -KILL main`: main exits 137, and all 15 descendants exit through PDEATHSIG within 0.5 s.
* Then a half row is appended to `rows_rank0.jsonl` without a newline, and the same command is re-run. The resume says 11 done / 29 to do
and exits 0, and `merge` finds all 40 clips.
* `verify_prep --spot 2`: 0 fails. The 1 warning is the 7 leftover `.tmp<pid>` files from the two kills. Re-encode is exact, PSNR is 29.8
and 23.1 dB, and the v10 render IoU is 0.998 for the player and 0.997 for the boss.
* `rawscan_no_vae.log`: `--no_vae --clips_per_run 3` over **all 256 runs**, 768 clips, about 73k ids.png frames including extension frames.
The new source-id check never fires, and the run took 83 s (10.7 clips/s on 48 CPU workers). The check costs 1.1 ms/frame, about
0.12 s/clip (~3 % of worker CPU).
* `mini_verify.log`: MINI re-verified with the updated verifier. Result: OK, 200 clips, 0 fails, 0 warns, 121 s. The 8 spot clips pass the
source-id check, and the numbers are unchanged: PSNR 21.25β36.40 dB, v10 unoccluded player IoU 0.9978. New `ext:` line: v10 has_ext 87,
of which 85 change look, and the DMD train pool keeps **1/43** v10 clips when same-look is required; v9 has_ext 84, of which 0 change look
(41/41 kept).
## Notes for consumers
* **`ext_look_change` (OPEN DECISION for the lead / distill owner, SPEC Β§2 gap).** v10_real switches the look at frame s+81, which is every
clip boundary and also the first extension frame (cell 21). A look is a biome plus lighting. The switch changes the RGB, **and it also
re-lays out the static semantic map.** An earlier version of this note said "the ids change only modestly, so DMD is unaffected". That was
wrong. It came from data_v10.md Β§9, which said "the ids continue".
* Measured on MINI cond+cond_ext (all 171 has_ext clips), 80β81 vs. other adjacent frames:
| subset | look change at 80β81 | static-class histogram L1 at 80β81 (median / p90 / max) | other frame steps (median) | biggest jump in the clip is at 80β81 | static px changing class (80β81 vs. within clip, median) |
|---|---|---|---|---|---|
| v10 | 85/87 | 0.088 / 0.22 / 0.62 | 0.0026 | 78/87 | 0.107 vs 0.042 |
| v9 | 0/84 | 0.016 | 0.015 | 2/84 | 0.112 vs 0.109 |
Example: `01_β¦__R10_s1512__003456` goes from 0.5 % to 25 % vegetation in one frame. The player and camera are continuous across the boundary.
* Reviewer scan of all 12,800 clips: v10 has_ext 4,888, of which 4,772 change look, all at offset 81. v9 has_ext 5,897, of which 0 change look.
* The stage-3 DMD train pool (has_ext, not dropped, train split) is β 10,069 clips, β 4,274 (42 %) of them with this pop at cell 21. N is in
21..27, so cell 21 is in every rollout. The bidirectional teacher never saw a mid-clip biome change, so these samples are out of its
distribution.
* **What prep does:** prep keeps SPEC Β§2 literally. `has_ext` checks episode and camera_variant only, and cond_ext is written for these clips.
The decision therefore stays reversible without re-running the 30 min prep. Every row carries `ext_look_change`, and `verify_prep` prints
the counts (`ext: β¦ dmd_pool_train_same_look=β¦`).
* **Recommended:** stage 3 drops these rows by adding `and not r.get("ext_look_change", False)` to the `need_ext` branch of
`gcdistill/data_gc.read_index`. That leaves β 5.8k train clips (β 5.7k v9, β 100 v10), which is still more than 500 steps Γ 8 = 4,000
samples. The trade-off is that DMD then sees almost no v10_real looks. The alternative is to accept the pop and amend SPEC Β§2. Either way,
the call belongs to the lead and is not made here. No v10 clip has a usable extension without the pop, because the switch lands exactly
on frame s+81.
* `ground_z` is per frame and follows the walk cycle and the terrain. In MINI, `ground_z[0]` sits a median of 12β15 cm above the clip's
minimum, and the range within a clip has p90 β 130 cm (slopes and steps). SPEC Β§7 masking uses `ground_z[0]`, which is fine, but don't
expect it to be constant.
* MINI has 0 dropped clips (scene 01/02). The 348 buried clips are all in v10 scenes 10/07/09; `test_plan_split_drop` covers the drop flag
path.
## Commands
```bash
M=/opt/dlami/nvme/zhiyangdeng/maliang
ENV="DIFFSYNTH_SKIP_DOWNLOAD=True PYTHONPATH=$M/code/gc25d:$M/code/arope HF_HOME=/opt/dlami/nvme/zhiyangdeng/.cache/huggingface TMPDIR=$M/.tmp OMP_NUM_THREADS=8"
# tests
env $ENV $M/.venv/bin/python -m pytest -q $M/code/gc25d/tests/test_prep.py
# MINI (done)
env $ENV CUDA_VISIBLE_DEVICES=0,1 $M/.venv/bin/python $M/code/gc25d/prep_v10.py --src $M/data/GameCrafter-Data-v10/data \
--out $M/data/gc25d_v10_mini --gpus 0,1 --runs R10_s1501,R10_s1512,P9C_autumn_s1941,P9_golden_s2909 --workers_per_gpu 12
env $ENV CUDA_VISIBLE_DEVICES=0,1 $M/.venv/bin/python $M/code/gc25d/verify_prep.py --root $M/data/gc25d_v10_mini --spot 8
# FULL (not run yet; β 30 min, all 8 GPUs; resumable: just re-run the same command)
cd $M && env $ENV CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 nohup $M/.venv/bin/python $M/code/gc25d/prep_v10.py \
--src $M/data/GameCrafter-Data-v10/data --out $M/data/gc25d_v10 --gpus 0,1,2,3,4,5,6,7 --workers_per_gpu 12 \
> $M/outputs/_smoke/prep/full_build.log 2>&1 &
# to stop it: kill <that PID> (SIGTERM) β ranks and workers exit too; re-run the same command to resume
# then (β 15 min: phase B β 4 min with 64 procs, 32 spot clips β 10 min)
env $ENV CUDA_VISIBLE_DEVICES=0 $M/.venv/bin/python $M/code/gc25d/verify_prep.py --root $M/data/gc25d_v10 --spot 32 --workers 64
```
|