Spaces:
Sleeping
Sleeping
Cache examples and simplify frontend
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +29 -0
- .gitignore +5 -0
- LICENSE-PiD-CODE +13 -0
- UPSTREAM_PID_README.md +245 -0
- app.py +25 -12
- candidate_examples/brick_texture.jpg +3 -0
- candidate_examples/contact_sheet.jpg +3 -0
- candidate_examples/dragonfly_macro.jpg +3 -0
- candidate_examples/embroidery_threads.jpg +3 -0
- candidate_examples/feather_barbs.jpg +3 -0
- candidate_examples/leaf_macro.jpg +3 -0
- candidate_examples/palm_bark.jpg +3 -0
- candidate_results/brick_texture_crop_compare.jpg +3 -0
- candidate_results/brick_texture_pid_preview.jpg +3 -0
- candidate_results/dragonfly_macro_crop_compare.jpg +3 -0
- candidate_results/dragonfly_macro_pid_preview.jpg +3 -0
- candidate_results/embroidery_threads_crop_compare.jpg +3 -0
- candidate_results/embroidery_threads_pid_preview.jpg +3 -0
- candidate_results/feather_barbs_crop_compare.jpg +3 -0
- candidate_results/feather_barbs_pid_preview.jpg +3 -0
- candidate_results/leaf_macro_crop_compare.jpg +3 -0
- candidate_results/leaf_macro_pid_preview.jpg +3 -0
- candidate_results/palm_bark_crop_compare.jpg +3 -0
- candidate_results/palm_bark_pid_preview.jpg +3 -0
- candidate_results/summary.json +104 -0
- example_cache/v2_example_0.json +44 -0
- example_cache/v2_example_1.json +23 -0
- example_cache/v2_example_2.json +23 -0
- examples/alpine_cabin.jpg +0 -0
- examples/bowery_waltz.jpg +0 -0
- examples/brick_wall_texture.jpg +3 -0
- examples/coyote_run.jpg +0 -0
- examples/golden_eagle.jpg +0 -0
- examples/leaf_macro_texture.jpg +3 -0
- examples/palm_bark_texture.jpg +3 -0
- examples/portrait_umbrella.jpg +0 -0
- examples/shove_it.jpg +0 -0
- index.html +81 -31
- pid/__init__.py +14 -0
- pid/_ext/__init__.py +4 -0
- pid/_ext/imaginaire/__init__.py +14 -0
- pid/_ext/imaginaire/checkpointer/__init__.py +14 -0
- pid/_ext/imaginaire/checkpointer/base.py +177 -0
- pid/_ext/imaginaire/checkpointer/dcp.py +1062 -0
- pid/_ext/imaginaire/checkpointer/dummy.py +47 -0
- pid/_ext/imaginaire/checkpointer/s3_filesystem.py +323 -0
- pid/_ext/imaginaire/config.py +450 -0
- pid/_ext/imaginaire/flags.py +52 -0
- pid/_ext/imaginaire/lazy_config/__init__.py +60 -0
- pid/_ext/imaginaire/lazy_config/file_io.py +24 -0
.gitattributes
CHANGED
|
@@ -36,3 +36,32 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 36 |
examples/bowery_waltz.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
examples/coyote_run.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
examples/shove_it.mp4 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
examples/bowery_waltz.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
examples/coyote_run.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 38 |
examples/shove_it.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
candidate_examples/brick_texture.jpg filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
candidate_examples/contact_sheet.jpg filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
candidate_examples/dragonfly_macro.jpg filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
candidate_examples/embroidery_threads.jpg filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
candidate_examples/feather_barbs.jpg filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
candidate_examples/leaf_macro.jpg filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
candidate_examples/palm_bark.jpg filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
candidate_results/brick_texture_crop_compare.jpg filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
candidate_results/brick_texture_pid_preview.jpg filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
candidate_results/dragonfly_macro_crop_compare.jpg filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
candidate_results/dragonfly_macro_pid_preview.jpg filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
candidate_results/embroidery_threads_crop_compare.jpg filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
candidate_results/embroidery_threads_pid_preview.jpg filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
candidate_results/feather_barbs_crop_compare.jpg filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
candidate_results/feather_barbs_pid_preview.jpg filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
candidate_results/leaf_macro_crop_compare.jpg filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
candidate_results/leaf_macro_pid_preview.jpg filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
candidate_results/palm_bark_crop_compare.jpg filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
candidate_results/palm_bark_pid_preview.jpg filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
examples/brick_wall_texture.jpg filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
examples/leaf_macro_texture.jpg filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
examples/palm_bark_texture.jpg filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
research_outputs/alpine_absdiff_x10.png filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
research_outputs/alpine_absdiff_x8.png filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
research_outputs/alpine_cabin_compare.png filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
research_outputs/alpine_foreground_compare.png filter=lfs diff=lfs merge=lfs -text
|
| 65 |
+
research_outputs/alpine_lanczos_to_4k.png filter=lfs diff=lfs merge=lfs -text
|
| 66 |
+
research_outputs/alpine_peaks_compare.png filter=lfs diff=lfs merge=lfs -text
|
| 67 |
+
research_outputs/alpine_pid_4k.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/.cache/
|
| 2 |
+
/checkpoints/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.pyc
|
| 5 |
+
.DS_Store
|
LICENSE-PiD-CODE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright 2026 NVIDIA CORPORATION & AFFILIATES.
|
| 2 |
+
|
| 3 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
you may not use this file except in compliance with the License.
|
| 5 |
+
You may obtain a copy of the License at
|
| 6 |
+
|
| 7 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
|
| 9 |
+
Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
See the License for the specific language governing permissions and
|
| 13 |
+
limitations under the License.
|
UPSTREAM_PID_README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PiD — Pixel Diffusion Decoder
|
| 2 |
+
|
| 3 |
+
> **TL;DR** — PiD is a plug-and-play diffusion decoder that replaces VAE/RAE decoders, turning latent representations directly into super-resolved pixels in a single pass.
|
| 4 |
+
|
| 5 |
+
<p align="center">
|
| 6 |
+
<img src="figures/teaser.jpg" alt="PiD teaser" width="100%">
|
| 7 |
+
</p>
|
| 8 |
+
|
| 9 |
+
https://github.com/user-attachments/assets/a556e2d4-5de5-4bcf-9daa-80f7ea6b2124
|
| 10 |
+
|
| 11 |
+
PiD reformulates the latent-to-pixel decoder as a conditional pixel-space diffusion
|
| 12 |
+
model, unifying decoding and upsampling into a single generative module.
|
| 13 |
+
It directly denoises in high-resolution pixel
|
| 14 |
+
space and produces a super-resolved image in one pass.
|
| 15 |
+
|
| 16 |
+
**[Paper](https://arxiv.org/abs/2605.23902), [Project Page](https://research.nvidia.com/labs/sil/projects/pid/), [Model Weights](https://huggingface.co/nvidia/PiD)**
|
| 17 |
+
|
| 18 |
+
[Yifan Lu](https://yifanlu0227.github.io/),
|
| 19 |
+
[Qi Wu](https://wilsoncernwq.github.io/),
|
| 20 |
+
[Jay Zhangjie Wu](https://zhangjiewu.github.io/),
|
| 21 |
+
[Zian Wang](https://www.cs.toronto.edu/~zianwang/),
|
| 22 |
+
[Huan Ling](https://www.cs.toronto.edu/~linghuan/),
|
| 23 |
+
[Sanja Fidler](https://www.cs.utoronto.ca/~fidler/),
|
| 24 |
+
[Xuanchi Ren](https://xuanchiren.com/) <br>
|
| 25 |
+
|
| 26 |
+
## Installation
|
| 27 |
+
|
| 28 |
+
> [!TIP]
|
| 29 |
+
> **Quick Start** — if your environment already has PyTorch (with CUDA), `transformers>=4.57.x`, and `diffusers>=0.37`, you don't need to build a new conda env. Just install the small set of utility deps the inference code pulls eagerly and you're ready to run the diffusers backbones (`flux`/`flux2`/`sd3`/`zimage`):
|
| 30 |
+
>
|
| 31 |
+
> ```bash
|
| 32 |
+
> pip install hydra-core==1.3.2 omegaconf==2.3.0 \
|
| 33 |
+
> attrs einops loguru termcolor fvcore iopath pynvml wandb \
|
| 34 |
+
> imageio opencv-python-headless pandas \
|
| 35 |
+
> safetensors "huggingface-hub>=1.0" sentencepiece boto3 botocore
|
| 36 |
+
> pip install -e .
|
| 37 |
+
> ```
|
| 38 |
+
>
|
| 39 |
+
> For the `dinov2` / `siglip` backbones you additionally need the upstream RAE / Scale-RAE repos plus a couple of extra packages — see [docs/dinov2_siglip.md](docs/dinov2_siglip.md).
|
| 40 |
+
|
| 41 |
+
Full conda-managed install (preferred if you're starting from scratch):
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
conda env create -f environment.yml
|
| 45 |
+
conda activate pid
|
| 46 |
+
|
| 47 |
+
# 2. Install this package in editable mode.
|
| 48 |
+
pip install -e .
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
## Checkpoints and assets
|
| 52 |
+
|
| 53 |
+
Pretrained PiD checkpoints live under `checkpoints/`. Each diffusers backbone ships
|
| 54 |
+
two variants — the original `2k` decoder (trained at 2048px) and a `2kto4k` decoder
|
| 55 |
+
(trained with multi-resolution data bucketing 2048→3840 + an SD3-style dynamic
|
| 56 |
+
shift, intended for 1024 LDM → 4K decoding). Pick the variant at the CLI via
|
| 57 |
+
`--pid_ckpt_type {2k,2kto4k}` (default: `2k`).
|
| 58 |
+
|
| 59 |
+
### Downloading
|
| 60 |
+
|
| 61 |
+
The released decoder weights and the encoder/decoder ("VAE") weights they
|
| 62 |
+
depend on are hosted at [`nvidia/PiD`](https://huggingface.co/nvidia/PiD) on
|
| 63 |
+
the Hugging Face Hub. Pull just the `checkpoints/` tree into this repo:
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
hf download nvidia/PiD --local-dir . --include "checkpoints/*"
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
## Running inference
|
| 70 |
+
|
| 71 |
+
PiD ships two complementary entry points per backbone:
|
| 72 |
+
|
| 73 |
+
| Backbone | `from_clean_*` (image → encode → PiD) | `from_ldm_*` (text/class → LDM → PiD) |
|
| 74 |
+
|----------|---------------------------------------|---------------------------------------|
|
| 75 |
+
| flux | `from_clean_flux.py` | `from_ldm_flux.py` |
|
| 76 |
+
| flux2 | `from_clean_flux2.py` | `from_ldm_flux2.py` |
|
| 77 |
+
| sd3 | `from_clean_sd3.py` | `from_ldm_sd3.py` |
|
| 78 |
+
| zimage | reuses `flux` | `from_ldm_zimage.py` |
|
| 79 |
+
| dinov2 | `from_clean_dinov2.py` | `from_ldm_dinov2.py` |
|
| 80 |
+
| siglip | `from_clean_siglip.py` | `from_ldm_siglip.py` |
|
| 81 |
+
|
| 82 |
+
All scripts live under `pid/_src/inference/` and decode each captured latent
|
| 83 |
+
twice — once with the backbone's native VAE (baseline) and once with PiD.
|
| 84 |
+
|
| 85 |
+
> [!IMPORTANT]
|
| 86 |
+
> Picking the checkpoint variant — `--pid_ckpt_type`
|
| 87 |
+
> Every entry point accepts `--pid_ckpt_type {2k,2kto4k}` (default `2k`):
|
| 88 |
+
>
|
| 89 |
+
> - **`2k`** — the original 2048px-trained decoder.
|
| 90 |
+
> - **`2kto4k`** — the up-to-4K-resolution decoder. > > Available for `flux` / `flux2` / `sd3` / `zimage` only. Worse than `2k` at 2048px resolution.
|
| 91 |
+
>
|
| 92 |
+
> For the exact checkpoint path for each backbone, see [docs/checkpoints.md](docs/checkpoints.md).
|
| 93 |
+
> A quick sanity check that the right variant loaded: when `2kto4k` is active you
|
| 94 |
+
should see `PixelDiT dynamic shift: base_shift=4.0 base_image_size=1024` in the
|
| 95 |
+
init log; for `2k` that line is absent. Both `2k` and `2kto4k` support non-square aspect ratios.
|
| 96 |
+
|
| 97 |
+
### 📕 `from_ldm_*`: text / class → latent diffusion → PiD decode
|
| 98 |
+
|
| 99 |
+
Runs the corresponding latent-diffusion backbone on a prompt (or class id for
|
| 100 |
+
the class-conditional `dinov2` backbone), captures the intermediate `x_t` at
|
| 101 |
+
user-specified denoising steps (early LDM termination) and the final clean `x_0`, then decodes
|
| 102 |
+
each captured latent with both the native VAE / RAE decoder (baseline) and PiD.
|
| 103 |
+
|
| 104 |
+
For `flux` / `flux2` / `sd3` / `zimage` the LDM is a HuggingFace `diffusers`
|
| 105 |
+
pipeline (`FluxPipeline`, `Flux2Pipeline`, `StableDiffusion3Pipeline`,
|
| 106 |
+
`ZImagePipeline`).
|
| 107 |
+
|
| 108 |
+
For `dinov2` and `siglip` the LDM is the upstream
|
| 109 |
+
[RAE](https://github.com/bytetriper/RAE) (class-conditional ImageNet-512) or
|
| 110 |
+
[Scale-RAE](https://github.com/ZitengWangNYU/Scale-RAE) (text-conditional
|
| 111 |
+
256px) repo — see the optional-deps section below for installation.
|
| 112 |
+
|
| 113 |
+
#### Example 1 — Single-GPU, single prompt (Flux, default `2k` decoder)
|
| 114 |
+
|
| 115 |
+
```bash
|
| 116 |
+
PYTHONPATH=. python -m pid._src.inference.from_ldm_flux \
|
| 117 |
+
--prompt "A photorealistic half-body portrait of a brown tabby cat with bold stripes sitting attentively on a rustic wooden kitchen table, soft morning light streaming sideways through a large window, fine fur detail and stripe patterns sharply visible, intense amber-green eyes in razor-sharp focus, warm farmhouse kitchen softly out of focus, cinematic shallow depth of field, ultra-detailed fur texture, photorealistic" \
|
| 118 |
+
--ldm_inference_steps 28 --save_xt_steps 24 \
|
| 119 |
+
--output_dir ./results/official_demo/flux \
|
| 120 |
+
--cfg_scale 1 --pid_inference_steps 4 --scale 4
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
#### Example 2 — Single-GPU, 4K decode (Flux, `2kto4k` decoder)
|
| 124 |
+
|
| 125 |
+
Same backbone as Example 1 but with `--resolution 1024 --pid_ckpt_type 2kto4k`,
|
| 126 |
+
so the LDM produces a 1024² latent and PiD decodes it to 4K.
|
| 127 |
+
|
| 128 |
+
```bash
|
| 129 |
+
PYTHONPATH=. python -m pid._src.inference.from_ldm_flux \
|
| 130 |
+
--prompt "A photorealistic half-body portrait of a brown tabby cat with bold stripes sitting attentively on a rustic wooden kitchen table, soft morning light streaming sideways through a large window, fine fur detail and stripe patterns sharply visible, intense amber-green eyes in razor-sharp focus, warm farmhouse kitchen softly out of focus, cinematic shallow depth of field, ultra-detailed fur texture, photorealistic" \
|
| 131 |
+
--resolution 1024 --pid_ckpt_type 2kto4k \
|
| 132 |
+
--ldm_inference_steps 28 --save_xt_steps 24 \
|
| 133 |
+
--output_dir ./results/official_demo/flux_4k \
|
| 134 |
+
--cfg_scale 1 --pid_inference_steps 4 --scale 4
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
#### Example 3 — Multi-GPU with a prompt file (Z-Image)
|
| 138 |
+
|
| 139 |
+
`torchrun` shards `--prompt_file` across ranks; each rank writes to
|
| 140 |
+
`--output_dir` independently.
|
| 141 |
+
|
| 142 |
+
```bash
|
| 143 |
+
PYTHONPATH=. torchrun --nproc_per_node=4 \
|
| 144 |
+
-m pid._src.inference.from_ldm_zimage \
|
| 145 |
+
--prompt_file pid/_src/inference/prompts/prompt_creative.txt \
|
| 146 |
+
--ldm_inference_steps 50 --save_xt_steps 46 \
|
| 147 |
+
--output_dir ./results/official_demo/zimage \
|
| 148 |
+
--cfg_scale 1 --pid_inference_steps 4 --scale 4
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
#### `dinov2` / `siglip` backbones
|
| 152 |
+
|
| 153 |
+
The upstream RAE / Scale-RAE LDMs don't live in `diffusers` — see
|
| 154 |
+
[`docs/dinov2_siglip.md`](docs/dinov2_siglip.md) for setup and end-to-end
|
| 155 |
+
examples.
|
| 156 |
+
|
| 157 |
+
#### Suggested step settings per diffusers backbone
|
| 158 |
+
|
| 159 |
+
(See each script's docstring for the exact recipe.)
|
| 160 |
+
|
| 161 |
+
| Backbone | LDM steps flag | Default steps | `--save_xt_steps` (example) | Best `--save_xt_steps` |
|
| 162 |
+
|----------|-------------------------|---------------|-----------------------------|----------------------|
|
| 163 |
+
| flux | `--ldm_inference_steps` | 28 | `22 24 26` | 24 |
|
| 164 |
+
| sd3 | `--ldm_inference_steps` | 28 | `22 24 26` | 24 |
|
| 165 |
+
| flux2 | `--ldm_inference_steps` | 50 | `44 46 48` | 46 |
|
| 166 |
+
| zimage | `--ldm_inference_steps` | 50 | `44 46 48` | 46 |
|
| 167 |
+
|
| 168 |
+
---
|
| 169 |
+
### 📗 `from_clean_*`: image → VAE encode → PiD decode
|
| 170 |
+
|
| 171 |
+
No latent diffusion model is run. The input image is encode by VAE,
|
| 172 |
+
optionally corrupted with Gaussian noise at each
|
| 173 |
+
sigma in `--degrade_sigmas`, then decoded by PiD at `--scale * input_resolution`.
|
| 174 |
+
|
| 175 |
+
Single-GPU example (Flux):
|
| 176 |
+
|
| 177 |
+
```bash
|
| 178 |
+
PYTHONPATH=. python -m pid._src.inference.from_clean_flux \
|
| 179 |
+
--manifest assets/clean_image_manifest.jsonl \
|
| 180 |
+
--input_resolution 512 \
|
| 181 |
+
--degrade_sigmas 0.0 \
|
| 182 |
+
--output_dir ./results/official_demo_from_clean/flux \
|
| 183 |
+
--cfg_scale 1 --pid_inference_steps 4 --scale 4
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
You can pass a single image with `--input_path` and a prompt with `--prompt`
|
| 187 |
+
instead of `--manifest`, and a sigma sweep such as `--degrade_sigmas 0.0 0.2 0.4 0.8`
|
| 188 |
+
to decode noise-corrupted latents.
|
| 189 |
+
|
| 190 |
+
The `dinov2` / `siglip` `from_clean_*` flows take the same flags but with
|
| 191 |
+
different default resolutions and scales —
|
| 192 |
+
see [`docs/dinov2_siglip.md`](docs/dinov2_siglip.md).
|
| 193 |
+
|
| 194 |
+
### Common arguments
|
| 195 |
+
|
| 196 |
+
| Flag | Meaning |
|
| 197 |
+
|------|---------|
|
| 198 |
+
| `--pid_inference_steps`| Number of denoising steps for PiD (4 for the released distilled checkpoints) |
|
| 199 |
+
| `--scale` | PiD upscale factor (output = `baseline * scale`); 8 for Scale-RAE and 4 for other backbones |
|
| 200 |
+
| `--cfg_scale` | Classifier-free guidance scale for PiD |
|
| 201 |
+
| `--output_dir` | Where to write the side-by-side comparison images |
|
| 202 |
+
| `--seed` | Base random seed |
|
| 203 |
+
|
| 204 |
+
Multi-GPU runs use `torchrun --nproc_per_node=N`; each rank processes a shard
|
| 205 |
+
of the prompts / manifest entries and writes to `--output_dir` independently.
|
| 206 |
+
|
| 207 |
+
## Repository layout
|
| 208 |
+
|
| 209 |
+
```
|
| 210 |
+
pid/_src/inference/
|
| 211 |
+
├── from_ldm_{flux,flux2,sd3,zimage,dinov2,siglip}.py # text/class → LDM → PiD decode
|
| 212 |
+
├── from_clean_{flux,flux2,sd3,dinov2,siglip}.py # image → encode → PiD decode
|
| 213 |
+
├── _demo_common.py # shared CLI + run loop for from_ldm_*
|
| 214 |
+
├── _demo_from_clean_common.py # shared CLI + run loop for from_clean_*
|
| 215 |
+
├── checkpoint_registry.py # backbone → PiD checkpoint mapping
|
| 216 |
+
├── pipeline_registry.py # diffusers backbone → HF pipeline mapping
|
| 217 |
+
├── rae_generation.py # DINOv2-RAE LDM helpers (from_ldm_dinov2)
|
| 218 |
+
├── scale_rae_generation.py # Scale-RAE LDM helpers (from_ldm_siglip)
|
| 219 |
+
└── prompts/ # prompt files for from_ldm_*
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
## License
|
| 223 |
+
|
| 224 |
+
PiD codebase is licensed under the [Apache License 2.0](LICENSE).
|
| 225 |
+
|
| 226 |
+
## Contributing
|
| 227 |
+
|
| 228 |
+
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, code style,
|
| 229 |
+
and the DCO sign-off requirement.
|
| 230 |
+
|
| 231 |
+
## Acknowledgments
|
| 232 |
+
|
| 233 |
+
The authors would like to acknowledge [Yongsheng Yu](https://www.yongshengyu.com/) and [Wei Xiong](https://wxiong.me/) for open-sourcing [PixelDiT](https://pixeldit.github.io/)'s model and weights, and thank Product Managers [Aditya Mahajan](https://www.linkedin.com/in/aditya-mahajan1) and [Matt Cragun](https://www.linkedin.com/in/mcragun/) for their valuable support and guidance.
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
## Citation
|
| 237 |
+
|
| 238 |
+
```bibtex
|
| 239 |
+
@article{lu2026pid,
|
| 240 |
+
title={PiD: Fast and High-Resolution Latent Decoding with Pixel Diffusion},
|
| 241 |
+
author={Lu, Yifan and Wu, Qi and Wu, Jay Zhangjie and Wang, Zian and Ling, Huan and Fidler, Sanja and Ren, Xuanchi},
|
| 242 |
+
journal={arXiv preprint arXiv:2605.23902},
|
| 243 |
+
year={2026}
|
| 244 |
+
}
|
| 245 |
+
```
|
app.py
CHANGED
|
@@ -40,6 +40,7 @@ from gradio_client.data_classes import FileData
|
|
| 40 |
APP_ROOT = Path(__file__).resolve().parent
|
| 41 |
INDEX_PATH = APP_ROOT / "index.html"
|
| 42 |
EXAMPLES_DIR = APP_ROOT / "examples"
|
|
|
|
| 43 |
EXAMPLE_CACHE_DIR = Path("/tmp/marlin_example_cache")
|
| 44 |
MODEL_ID = "NemoStation/Marlin-2B"
|
| 45 |
MODEL_DESCRIPTION = (
|
|
@@ -241,18 +242,23 @@ def _zerogpu_probe() -> str:
|
|
| 241 |
return "ready"
|
| 242 |
|
| 243 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
def _example_cache_path(index: int) -> Path:
|
| 245 |
return EXAMPLE_CACHE_DIR / f"v2_example_{index}.json"
|
| 246 |
|
| 247 |
|
| 248 |
def _read_example_cache(index: int) -> dict[str, Any] | None:
|
| 249 |
-
path
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
|
|
|
| 256 |
|
| 257 |
|
| 258 |
def _write_example_cache(index: int, payload: dict[str, Any]) -> None:
|
|
@@ -282,14 +288,21 @@ async def health() -> JSONResponse:
|
|
| 282 |
|
| 283 |
@app.get("/examples.json")
|
| 284 |
async def examples_json() -> JSONResponse:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
return JSONResponse(
|
| 286 |
{
|
| 287 |
"examples": [
|
| 288 |
-
|
| 289 |
-
**example,
|
| 290 |
-
"url": f"/examples/{example['file']}",
|
| 291 |
-
"cached": _read_example_cache(i) is not None,
|
| 292 |
-
}
|
| 293 |
for i, example in enumerate(EXAMPLES)
|
| 294 |
]
|
| 295 |
}
|
|
|
|
| 40 |
APP_ROOT = Path(__file__).resolve().parent
|
| 41 |
INDEX_PATH = APP_ROOT / "index.html"
|
| 42 |
EXAMPLES_DIR = APP_ROOT / "examples"
|
| 43 |
+
EXAMPLE_STATIC_CACHE_DIR = APP_ROOT / "example_cache"
|
| 44 |
EXAMPLE_CACHE_DIR = Path("/tmp/marlin_example_cache")
|
| 45 |
MODEL_ID = "NemoStation/Marlin-2B"
|
| 46 |
MODEL_DESCRIPTION = (
|
|
|
|
| 242 |
return "ready"
|
| 243 |
|
| 244 |
|
| 245 |
+
def _static_example_cache_path(index: int) -> Path:
|
| 246 |
+
return EXAMPLE_STATIC_CACHE_DIR / f"v2_example_{index}.json"
|
| 247 |
+
|
| 248 |
+
|
| 249 |
def _example_cache_path(index: int) -> Path:
|
| 250 |
return EXAMPLE_CACHE_DIR / f"v2_example_{index}.json"
|
| 251 |
|
| 252 |
|
| 253 |
def _read_example_cache(index: int) -> dict[str, Any] | None:
|
| 254 |
+
for path in (_static_example_cache_path(index), _example_cache_path(index)):
|
| 255 |
+
if not path.exists():
|
| 256 |
+
continue
|
| 257 |
+
try:
|
| 258 |
+
return json.loads(path.read_text(encoding="utf-8"))
|
| 259 |
+
except Exception:
|
| 260 |
+
continue
|
| 261 |
+
return None
|
| 262 |
|
| 263 |
|
| 264 |
def _write_example_cache(index: int, payload: dict[str, Any]) -> None:
|
|
|
|
| 288 |
|
| 289 |
@app.get("/examples.json")
|
| 290 |
async def examples_json() -> JSONResponse:
|
| 291 |
+
def _example_payload(index: int, example: dict[str, Any]) -> dict[str, Any]:
|
| 292 |
+
thumbnail = Path(example["file"]).with_suffix(".jpg").name
|
| 293 |
+
payload = {
|
| 294 |
+
**example,
|
| 295 |
+
"url": f"/examples/{example['file']}",
|
| 296 |
+
"cached": _read_example_cache(index) is not None,
|
| 297 |
+
}
|
| 298 |
+
if (EXAMPLES_DIR / thumbnail).exists():
|
| 299 |
+
payload["thumbnail"] = f"/examples/{thumbnail}"
|
| 300 |
+
return payload
|
| 301 |
+
|
| 302 |
return JSONResponse(
|
| 303 |
{
|
| 304 |
"examples": [
|
| 305 |
+
_example_payload(i, example)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
for i, example in enumerate(EXAMPLES)
|
| 307 |
]
|
| 308 |
}
|
candidate_examples/brick_texture.jpg
ADDED
|
Git LFS Details
|
candidate_examples/contact_sheet.jpg
ADDED
|
Git LFS Details
|
candidate_examples/dragonfly_macro.jpg
ADDED
|
Git LFS Details
|
candidate_examples/embroidery_threads.jpg
ADDED
|
Git LFS Details
|
candidate_examples/feather_barbs.jpg
ADDED
|
Git LFS Details
|
candidate_examples/leaf_macro.jpg
ADDED
|
Git LFS Details
|
candidate_examples/palm_bark.jpg
ADDED
|
Git LFS Details
|
candidate_results/brick_texture_crop_compare.jpg
ADDED
|
Git LFS Details
|
candidate_results/brick_texture_pid_preview.jpg
ADDED
|
Git LFS Details
|
candidate_results/dragonfly_macro_crop_compare.jpg
ADDED
|
Git LFS Details
|
candidate_results/dragonfly_macro_pid_preview.jpg
ADDED
|
Git LFS Details
|
candidate_results/embroidery_threads_crop_compare.jpg
ADDED
|
Git LFS Details
|
candidate_results/embroidery_threads_pid_preview.jpg
ADDED
|
Git LFS Details
|
candidate_results/feather_barbs_crop_compare.jpg
ADDED
|
Git LFS Details
|
candidate_results/feather_barbs_pid_preview.jpg
ADDED
|
Git LFS Details
|
candidate_results/leaf_macro_crop_compare.jpg
ADDED
|
Git LFS Details
|
candidate_results/leaf_macro_pid_preview.jpg
ADDED
|
Git LFS Details
|
candidate_results/palm_bark_crop_compare.jpg
ADDED
|
Git LFS Details
|
candidate_results/palm_bark_pid_preview.jpg
ADDED
|
Git LFS Details
|
candidate_results/summary.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"slug": "brick_texture",
|
| 4 |
+
"status": "14.63s GPU compute | 1024x1024 input -> 4096x4096 output | 2kto4k | slider baseline: Lanczos input resize",
|
| 5 |
+
"mean_abs": 12.956155200799307,
|
| 6 |
+
"rms": 19.173621112407616,
|
| 7 |
+
"max_delta": 226,
|
| 8 |
+
"pct_gt_2": 94.4489061832428,
|
| 9 |
+
"pct_gt_5": 69.04728412628174,
|
| 10 |
+
"pct_gt_10": 41.335028409957886,
|
| 11 |
+
"crop_box": [
|
| 12 |
+
2176,
|
| 13 |
+
2368,
|
| 14 |
+
2944,
|
| 15 |
+
3136
|
| 16 |
+
],
|
| 17 |
+
"prompt": "A close-up photograph of a weathered brick wall with rough mortar lines, chipped corners, sand grains, and subtle orange-brown surface texture."
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"slug": "dragonfly_macro",
|
| 21 |
+
"status": "14.92s GPU compute | 1024x1024 input -> 4096x4096 output | 2kto4k | slider baseline: Lanczos input resize",
|
| 22 |
+
"mean_abs": 4.303538699944814,
|
| 23 |
+
"rms": 5.7793400659609215,
|
| 24 |
+
"max_delta": 133,
|
| 25 |
+
"pct_gt_2": 81.09467029571533,
|
| 26 |
+
"pct_gt_5": 24.35404062271118,
|
| 27 |
+
"pct_gt_10": 4.072648286819458,
|
| 28 |
+
"crop_box": [
|
| 29 |
+
1088,
|
| 30 |
+
1216,
|
| 31 |
+
1856,
|
| 32 |
+
1984
|
| 33 |
+
],
|
| 34 |
+
"prompt": "A macro photograph of a dragonfly with glassy compound eyes, translucent veined wings, tiny hairs, and natural outdoor light."
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"slug": "embroidery_threads",
|
| 38 |
+
"status": "15.01s GPU compute | 1024x1024 input -> 4096x4096 output | 2kto4k | slider baseline: Lanczos input resize",
|
| 39 |
+
"mean_abs": 4.275510291258494,
|
| 40 |
+
"rms": 5.43537963982756,
|
| 41 |
+
"max_delta": 80,
|
| 42 |
+
"pct_gt_2": 87.73354887962341,
|
| 43 |
+
"pct_gt_5": 27.23088264465332,
|
| 44 |
+
"pct_gt_10": 1.5419423580169678,
|
| 45 |
+
"crop_box": [
|
| 46 |
+
2560,
|
| 47 |
+
3328,
|
| 48 |
+
3328,
|
| 49 |
+
4096
|
| 50 |
+
],
|
| 51 |
+
"prompt": "A close-up photograph of embroidered fabric with raised yellow and green thread, visible woven cloth texture, stitch fibers, and curved ornamental patterns."
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"slug": "feather_barbs",
|
| 55 |
+
"status": "15.08s GPU compute | 1024x1024 input -> 4096x4096 output | 2kto4k | slider baseline: Lanczos input resize",
|
| 56 |
+
"mean_abs": 3.7417769034703574,
|
| 57 |
+
"rms": 4.578883124584203,
|
| 58 |
+
"max_delta": 84,
|
| 59 |
+
"pct_gt_2": 76.96071863174438,
|
| 60 |
+
"pct_gt_5": 20.45971155166626,
|
| 61 |
+
"pct_gt_10": 1.09291672706604,
|
| 62 |
+
"crop_box": [
|
| 63 |
+
2112,
|
| 64 |
+
3328,
|
| 65 |
+
2880,
|
| 66 |
+
4096
|
| 67 |
+
],
|
| 68 |
+
"prompt": "A close-up photograph of soft white feathers with fine barbs, layered strands, subtle shadows, and delicate down texture."
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"slug": "leaf_macro",
|
| 72 |
+
"status": "14.62s GPU compute | 1024x1024 input -> 4096x4096 output | 2kto4k | slider baseline: Lanczos input resize",
|
| 73 |
+
"mean_abs": 11.487248241901398,
|
| 74 |
+
"rms": 16.521660198582843,
|
| 75 |
+
"max_delta": 182,
|
| 76 |
+
"pct_gt_2": 97.48759865760803,
|
| 77 |
+
"pct_gt_5": 74.37732815742493,
|
| 78 |
+
"pct_gt_10": 37.26730942726135,
|
| 79 |
+
"crop_box": [
|
| 80 |
+
0,
|
| 81 |
+
1216,
|
| 82 |
+
768,
|
| 83 |
+
1984
|
| 84 |
+
],
|
| 85 |
+
"prompt": "A macro photograph of a green leaf surface with branching veins, tiny cells, crisp organic texture, and backlit detail."
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"slug": "palm_bark",
|
| 89 |
+
"status": "17.11s GPU compute | 1024x1024 input -> 4096x4096 output | 2kto4k | slider baseline: Lanczos input resize",
|
| 90 |
+
"mean_abs": 27.973096787929535,
|
| 91 |
+
"rms": 36.79712697730481,
|
| 92 |
+
"max_delta": 242,
|
| 93 |
+
"pct_gt_2": 98.77586364746094,
|
| 94 |
+
"pct_gt_5": 90.33623933792114,
|
| 95 |
+
"pct_gt_10": 74.35104846954346,
|
| 96 |
+
"crop_box": [
|
| 97 |
+
2496,
|
| 98 |
+
1664,
|
| 99 |
+
3264,
|
| 100 |
+
2432
|
| 101 |
+
],
|
| 102 |
+
"prompt": "A close-up photograph of palm tree bark with deep vertical cracks, rough fibers, beige-gray ridges, and natural wood texture."
|
| 103 |
+
}
|
| 104 |
+
]
|
example_cache/v2_example_0.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"ok": true,
|
| 3 |
+
"task": "Caption",
|
| 4 |
+
"scene": "The scene is set on a minimalist stage with a stark black background, creating a high-contrast environment that emphasizes the performers. A man and a woman are the central figures, dressed in mid-20th-century attire. The man wears a light-colored, long-sleeved jacket and matching trousers, while the woman is in a light-colored, knee-length dress with a dark belt and a small hat. The stage floor is a flat, light-colored surface that reflects the overhead lighting. The camera maintains a medium-wide shot, capturing the full bodies of both dancers as they move across the stage. The atmosphere is theatrical and energetic, characteristic of a choreographed dance performance. The lighting is focused primarily on the couple, leaving the edges of the stage in deep shadow. The man's jacket is slightly oversized, and the woman's dress has a full skirt that flares out during her movements. The overall aesthetic is that of a classic black-and-white film, with visible film grain and occasional flickers.",
|
| 5 |
+
"events": [
|
| 6 |
+
{
|
| 7 |
+
"start": 0.5,
|
| 8 |
+
"end": 1.5,
|
| 9 |
+
"description": "The man and woman walk toward each other."
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"start": 1.5,
|
| 13 |
+
"end": 2.5,
|
| 14 |
+
"description": "The woman bends down to touch the man's hand."
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"start": 2.5,
|
| 18 |
+
"end": 4.5,
|
| 19 |
+
"description": "The man and woman begin dancing together."
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"start": 4.5,
|
| 23 |
+
"end": 6.5,
|
| 24 |
+
"description": "The man spins the woman in a circle."
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"start": 6.5,
|
| 28 |
+
"end": 10.5,
|
| 29 |
+
"description": "The couple performs synchronized side-to-side steps."
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"start": 10.5,
|
| 33 |
+
"end": 14.0,
|
| 34 |
+
"description": "The man leads the woman through a final spin."
|
| 35 |
+
}
|
| 36 |
+
],
|
| 37 |
+
"span": null,
|
| 38 |
+
"raw": "Scene: The scene is set on a minimalist stage with a stark black background, creating a high-contrast environment that emphasizes the performers. A man and a woman are the central figures, dressed in mid-20th-century attire. The man wears a light-colored, long-sleeved jacket and matching trousers, while the woman is in a light-colored, knee-length dress with a dark belt and a small hat. The stage floor is a flat, light-colored surface that reflects the overhead lighting. The camera maintains a medium-wide shot, capturing the full bodies of both dancers as they move across the stage. The atmosphere is theatrical and energetic, characteristic of a choreographed dance performance. The lighting is focused primarily on the couple, leaving the edges of the stage in deep shadow. The man's jacket is slightly oversized, and the woman's dress has a full skirt that flares out during her movements. The overall aesthetic is that of a classic black-and-white film, with visible film grain and occasional flickers.\n\nEvents:\n<0.5 - 1.5> The man and woman walk toward each other.\n<1.5 - 2.5> The woman bends down to touch the man's hand.\n<2.5 - 4.5> The man and woman begin dancing together.\n<4.5 - 6.5> The man spins the woman in a circle.\n<6.5 - 10.5> The couple performs synchronized side-to-side steps.\n<10.5 - 14.0> The man leads the woman through a final spin.",
|
| 39 |
+
"status": "Caption completed in 9.1s on GPU | 14.0s video | 6 events parsed",
|
| 40 |
+
"duration": 14.0,
|
| 41 |
+
"timings": {
|
| 42 |
+
"gpu_seconds": 9.12043823604472
|
| 43 |
+
}
|
| 44 |
+
}
|
example_cache/v2_example_1.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"ok": true,
|
| 3 |
+
"task": "Find",
|
| 4 |
+
"scene": "",
|
| 5 |
+
"events": [
|
| 6 |
+
{
|
| 7 |
+
"start": 0.0,
|
| 8 |
+
"end": 1.5,
|
| 9 |
+
"description": "the skateboard spins under the rider's feet"
|
| 10 |
+
}
|
| 11 |
+
],
|
| 12 |
+
"span": {
|
| 13 |
+
"start": 0.0,
|
| 14 |
+
"end": 1.5,
|
| 15 |
+
"label": "the skateboard spins under the rider's feet"
|
| 16 |
+
},
|
| 17 |
+
"raw": "From 0.0 to 1.5.",
|
| 18 |
+
"status": "Find completed in 0.5s on GPU | 6.5s video | format_ok=True",
|
| 19 |
+
"duration": 6.5,
|
| 20 |
+
"timings": {
|
| 21 |
+
"gpu_seconds": 0.5369779840111732
|
| 22 |
+
}
|
| 23 |
+
}
|
example_cache/v2_example_2.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"ok": true,
|
| 3 |
+
"task": "Find",
|
| 4 |
+
"scene": "",
|
| 5 |
+
"events": [
|
| 6 |
+
{
|
| 7 |
+
"start": 0.0,
|
| 8 |
+
"end": 1.5,
|
| 9 |
+
"description": "the coyote runs across the scene"
|
| 10 |
+
}
|
| 11 |
+
],
|
| 12 |
+
"span": {
|
| 13 |
+
"start": 0.0,
|
| 14 |
+
"end": 1.5,
|
| 15 |
+
"label": "the coyote runs across the scene"
|
| 16 |
+
},
|
| 17 |
+
"raw": "From 0.0 to 1.5.",
|
| 18 |
+
"status": "Find completed in 0.5s on GPU | 9.9s video | format_ok=True",
|
| 19 |
+
"duration": 9.916667,
|
| 20 |
+
"timings": {
|
| 21 |
+
"gpu_seconds": 0.5370825119316578
|
| 22 |
+
}
|
| 23 |
+
}
|
examples/alpine_cabin.jpg
ADDED
|
examples/bowery_waltz.jpg
ADDED
|
examples/brick_wall_texture.jpg
ADDED
|
Git LFS Details
|
examples/coyote_run.jpg
ADDED
|
examples/golden_eagle.jpg
ADDED
|
examples/leaf_macro_texture.jpg
ADDED
|
Git LFS Details
|
examples/palm_bark_texture.jpg
ADDED
|
Git LFS Details
|
examples/portrait_umbrella.jpg
ADDED
|
examples/shove_it.jpg
ADDED
|
index.html
CHANGED
|
@@ -28,6 +28,7 @@
|
|
| 28 |
--font: "Inter", system-ui, -apple-system, "SF Pro Text", sans-serif;
|
| 29 |
}
|
| 30 |
* { box-sizing: border-box; }
|
|
|
|
| 31 |
html, body {
|
| 32 |
margin: 0;
|
| 33 |
min-height: 100%;
|
|
@@ -68,7 +69,11 @@
|
|
| 68 |
.layout {
|
| 69 |
display: grid;
|
| 70 |
grid-template-columns: 330px minmax(0, 1fr);
|
|
|
|
| 71 |
min-height: calc(100vh - 44px);
|
|
|
|
|
|
|
|
|
|
| 72 |
}
|
| 73 |
.column { min-width: 0; min-height: 0; border-right: 1px solid var(--border); }
|
| 74 |
.viewer-column { border-right: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); }
|
|
@@ -123,16 +128,22 @@
|
|
| 123 |
.example-list { display: grid; gap: 6px; }
|
| 124 |
.example-btn {
|
| 125 |
text-align: left; border: 1px solid var(--border); border-radius: var(--radius);
|
| 126 |
-
background: var(--surface); padding:
|
|
|
|
| 127 |
}
|
| 128 |
.example-btn:hover, .example-btn.active { background: var(--surface-2); border-color: var(--border-strong); }
|
| 129 |
-
.example-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
.viewer-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
| 132 |
.status { display: inline-flex; align-items: center; gap: 7px; color: var(--muted); font-size: 12px; }
|
| 133 |
.status svg { color: var(--muted-2); }
|
| 134 |
-
.viewer { padding: 18px; min-height: 0; display:
|
| 135 |
-
.stage
|
| 136 |
min-width: 0; border: 1px solid var(--border); border-radius: var(--radius);
|
| 137 |
background: var(--surface); overflow: hidden;
|
| 138 |
}
|
|
@@ -155,7 +166,7 @@
|
|
| 155 |
}
|
| 156 |
.segment.active { background: rgba(244,244,245,.72); }
|
| 157 |
.time-readout { display: flex; justify-content: space-between; color: var(--muted); font-size: 11.5px; }
|
| 158 |
-
.events { display: grid; gap: 6px;
|
| 159 |
.event-row {
|
| 160 |
width: 100%; text-align: left; border: 1px solid var(--border); border-radius: var(--radius);
|
| 161 |
background: var(--surface); padding: 9px 10px; display: grid; gap: 3px;
|
|
@@ -163,7 +174,6 @@
|
|
| 163 |
.event-row:hover, .event-row.active { background: var(--surface-2); border-color: var(--border-strong); }
|
| 164 |
.event-row time { color: var(--muted); font-size: 11.5px; }
|
| 165 |
.event-row span { color: var(--ink); }
|
| 166 |
-
.results { padding: 14px; display: grid; grid-template-rows: auto auto minmax(0,1fr); gap: 14px; }
|
| 167 |
.section { display: grid; gap: 7px; min-width: 0; }
|
| 168 |
.section h3 { margin: 0; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .05em; font-weight: 500; }
|
| 169 |
.scene, .raw {
|
|
@@ -172,6 +182,28 @@
|
|
| 172 |
padding: 10px; overflow: auto;
|
| 173 |
}
|
| 174 |
.raw { color: var(--muted); max-height: 260px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
.empty { color: var(--muted-2); }
|
| 176 |
.toast {
|
| 177 |
position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%) translateY(14px);
|
|
@@ -181,9 +213,8 @@
|
|
| 181 |
}
|
| 182 |
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
| 183 |
@media (max-width: 1080px) {
|
| 184 |
-
.layout { grid-template-columns: 1fr; }
|
| 185 |
.column { border-right: 0; border-bottom: 1px solid var(--border); }
|
| 186 |
-
.viewer { grid-template-columns: 1fr; }
|
| 187 |
}
|
| 188 |
@media (max-width: 680px) {
|
| 189 |
.topbar { padding: 0 12px; }
|
|
@@ -274,23 +305,25 @@
|
|
| 274 |
<div class="timebar" id="timebar"><div class="progress-line" id="progressLine"></div></div>
|
| 275 |
<div class="time-readout"><span id="currentTime">0.0s</span><span id="durationTime">0.0s</span></div>
|
| 276 |
<div class="events" id="eventsList"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
</div>
|
| 278 |
</section>
|
| 279 |
-
|
| 280 |
-
<aside class="results">
|
| 281 |
-
<div class="section">
|
| 282 |
-
<h3>Scene</h3>
|
| 283 |
-
<p class="scene empty" id="sceneText">No scene yet.</p>
|
| 284 |
-
</div>
|
| 285 |
-
<div class="section">
|
| 286 |
-
<h3>Status</h3>
|
| 287 |
-
<p class="scene" id="runStatus">Idle</p>
|
| 288 |
-
</div>
|
| 289 |
-
<div class="section">
|
| 290 |
-
<h3>Raw output</h3>
|
| 291 |
-
<pre class="raw" id="rawText"></pre>
|
| 292 |
-
</div>
|
| 293 |
-
</aside>
|
| 294 |
</div>
|
| 295 |
</section>
|
| 296 |
</section>
|
|
@@ -337,9 +370,19 @@
|
|
| 337 |
sceneText: $("sceneText"),
|
| 338 |
rawText: $("rawText"),
|
| 339 |
runStatus: $("runStatus"),
|
|
|
|
| 340 |
toast: $("toast"),
|
| 341 |
};
|
| 342 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
function setStatus(text, icon = "circle-dot") {
|
| 344 |
els.statusText.innerHTML = `<i data-lucide="${icon}"></i><span>${text}</span>`;
|
| 345 |
lucide.createIcons();
|
|
@@ -385,12 +428,13 @@
|
|
| 385 |
els.sceneText.classList.add("empty");
|
| 386 |
els.rawText.textContent = "";
|
| 387 |
els.runStatus.textContent = "Idle";
|
|
|
|
| 388 |
}
|
| 389 |
-
async function selectExample(index) {
|
|
|
|
| 390 |
const example = state.examples[index];
|
| 391 |
if (!example) return;
|
| 392 |
state.exampleIndex = index;
|
| 393 |
-
state.exampleMatchesPreset = true;
|
| 394 |
for (const button of els.exampleList.querySelectorAll(".example-btn")) {
|
| 395 |
button.classList.toggle("active", Number(button.dataset.index) === index);
|
| 396 |
}
|
|
@@ -400,11 +444,13 @@
|
|
| 400 |
els.tokenValue.textContent = String(example.tokens);
|
| 401 |
els.tempInput.value = example.temperature;
|
| 402 |
els.tempValue.textContent = Number(example.temperature).toFixed(1);
|
|
|
|
| 403 |
resetAnalysis();
|
| 404 |
const response = await fetch(example.url);
|
| 405 |
const blob = await response.blob();
|
| 406 |
const file = new File([blob], example.file, { type: blob.type || "video/mp4" });
|
| 407 |
setVideoFile(file, URL.createObjectURL(blob), example.title);
|
|
|
|
| 408 |
}
|
| 409 |
async function loadExamples() {
|
| 410 |
const response = await fetch("/examples.json");
|
|
@@ -416,11 +462,14 @@
|
|
| 416 |
button.className = "example-btn";
|
| 417 |
button.dataset.index = index;
|
| 418 |
button.type = "button";
|
| 419 |
-
|
|
|
|
|
|
|
|
|
|
| 420 |
button.addEventListener("click", () => selectExample(index));
|
| 421 |
els.exampleList.appendChild(button);
|
| 422 |
});
|
| 423 |
-
if (state.examples.length) selectExample(0);
|
| 424 |
}
|
| 425 |
function renderTimeline(payload) {
|
| 426 |
els.timebar.querySelectorAll(".segment").forEach((node) => node.remove());
|
|
@@ -452,7 +501,7 @@
|
|
| 452 |
row.type = "button";
|
| 453 |
row.className = "event-row";
|
| 454 |
row.dataset.index = index;
|
| 455 |
-
row.innerHTML = `<time>${seconds(event.start)} - ${seconds(event.end)}</time><span>${event.description}</span>`;
|
| 456 |
row.addEventListener("click", () => seekTo(event.start));
|
| 457 |
els.eventsList.appendChild(row);
|
| 458 |
}
|
|
@@ -465,6 +514,7 @@
|
|
| 465 |
els.sceneText.classList.toggle("empty", !payload.scene && !payload.span);
|
| 466 |
els.rawText.textContent = payload.raw || "";
|
| 467 |
els.runStatus.textContent = `${payload.status}${payload.cached ? " | cache hit" : ""}`;
|
|
|
|
| 468 |
updateActiveCaption();
|
| 469 |
}
|
| 470 |
function seekTo(time) {
|
|
@@ -487,10 +537,10 @@
|
|
| 487 |
}
|
| 488 |
if (active >= 0) {
|
| 489 |
const event = events[active];
|
| 490 |
-
els.activeCaption.innerHTML = `<small>${seconds(event.start)} - ${seconds(event.end)}</small>${event.description}`;
|
| 491 |
els.activeCaption.classList.remove("empty");
|
| 492 |
} else if (state.analysis?.scene) {
|
| 493 |
-
els.activeCaption.innerHTML = `<small>Scene</small>${state.analysis.scene}`;
|
| 494 |
els.activeCaption.classList.remove("empty");
|
| 495 |
} else {
|
| 496 |
els.activeCaption.textContent = "Run analysis to sync captions with playback.";
|
|
|
|
| 28 |
--font: "Inter", system-ui, -apple-system, "SF Pro Text", sans-serif;
|
| 29 |
}
|
| 30 |
* { box-sizing: border-box; }
|
| 31 |
+
[hidden] { display: none !important; }
|
| 32 |
html, body {
|
| 33 |
margin: 0;
|
| 34 |
min-height: 100%;
|
|
|
|
| 69 |
.layout {
|
| 70 |
display: grid;
|
| 71 |
grid-template-columns: 330px minmax(0, 1fr);
|
| 72 |
+
width: min(calc(100% - 32px), 1380px);
|
| 73 |
min-height: calc(100vh - 44px);
|
| 74 |
+
margin: 0 auto;
|
| 75 |
+
border-left: 1px solid var(--border);
|
| 76 |
+
border-right: 1px solid var(--border);
|
| 77 |
}
|
| 78 |
.column { min-width: 0; min-height: 0; border-right: 1px solid var(--border); }
|
| 79 |
.viewer-column { border-right: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); }
|
|
|
|
| 128 |
.example-list { display: grid; gap: 6px; }
|
| 129 |
.example-btn {
|
| 130 |
text-align: left; border: 1px solid var(--border); border-radius: var(--radius);
|
| 131 |
+
background: var(--surface); padding: 8px; display: grid;
|
| 132 |
+
grid-template-columns: 68px minmax(0, 1fr); gap: 10px; align-items: center;
|
| 133 |
}
|
| 134 |
.example-btn:hover, .example-btn.active { background: var(--surface-2); border-color: var(--border-strong); }
|
| 135 |
+
.example-thumb {
|
| 136 |
+
width: 68px; height: 42px; object-fit: cover; border-radius: 4px;
|
| 137 |
+
background: #050506; border: 1px solid var(--border);
|
| 138 |
+
}
|
| 139 |
+
.example-copy { min-width: 0; display: grid; gap: 2px; }
|
| 140 |
+
.example-copy strong { font-size: 12.5px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
| 141 |
+
.example-copy span { color: var(--muted); font-size: 11.5px; }
|
| 142 |
.viewer-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
| 143 |
.status { display: inline-flex; align-items: center; gap: 7px; color: var(--muted); font-size: 12px; }
|
| 144 |
.status svg { color: var(--muted-2); }
|
| 145 |
+
.viewer { padding: 18px; min-height: 0; display: block; }
|
| 146 |
+
.stage {
|
| 147 |
min-width: 0; border: 1px solid var(--border); border-radius: var(--radius);
|
| 148 |
background: var(--surface); overflow: hidden;
|
| 149 |
}
|
|
|
|
| 166 |
}
|
| 167 |
.segment.active { background: rgba(244,244,245,.72); }
|
| 168 |
.time-readout { display: flex; justify-content: space-between; color: var(--muted); font-size: 11.5px; }
|
| 169 |
+
.events { display: grid; gap: 6px; padding-right: 2px; }
|
| 170 |
.event-row {
|
| 171 |
width: 100%; text-align: left; border: 1px solid var(--border); border-radius: var(--radius);
|
| 172 |
background: var(--surface); padding: 9px 10px; display: grid; gap: 3px;
|
|
|
|
| 174 |
.event-row:hover, .event-row.active { background: var(--surface-2); border-color: var(--border-strong); }
|
| 175 |
.event-row time { color: var(--muted); font-size: 11.5px; }
|
| 176 |
.event-row span { color: var(--ink); }
|
|
|
|
| 177 |
.section { display: grid; gap: 7px; min-width: 0; }
|
| 178 |
.section h3 { margin: 0; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .05em; font-weight: 500; }
|
| 179 |
.scene, .raw {
|
|
|
|
| 182 |
padding: 10px; overflow: auto;
|
| 183 |
}
|
| 184 |
.raw { color: var(--muted); max-height: 260px; }
|
| 185 |
+
.model-details {
|
| 186 |
+
border: 1px solid var(--border); border-radius: var(--radius);
|
| 187 |
+
background: var(--surface); overflow: hidden;
|
| 188 |
+
}
|
| 189 |
+
.model-details summary {
|
| 190 |
+
min-height: 38px; padding: 0 10px; display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
| 191 |
+
cursor: pointer; color: var(--muted); list-style: none;
|
| 192 |
+
}
|
| 193 |
+
.model-details summary::-webkit-details-marker { display: none; }
|
| 194 |
+
.model-details summary span:first-child {
|
| 195 |
+
font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: .05em;
|
| 196 |
+
}
|
| 197 |
+
.model-details summary span:last-child {
|
| 198 |
+
min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 11.5px;
|
| 199 |
+
}
|
| 200 |
+
.model-details summary::after {
|
| 201 |
+
content: ""; width: 7px; height: 7px; border-right: 1px solid var(--muted); border-bottom: 1px solid var(--muted);
|
| 202 |
+
transform: rotate(45deg); flex: 0 0 auto; transition: transform .15s ease;
|
| 203 |
+
}
|
| 204 |
+
.model-details[open] summary { border-bottom: 1px solid var(--border); }
|
| 205 |
+
.model-details[open] summary::after { transform: rotate(225deg); }
|
| 206 |
+
.details-body { padding: 12px; display: grid; gap: 14px; }
|
| 207 |
.empty { color: var(--muted-2); }
|
| 208 |
.toast {
|
| 209 |
position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%) translateY(14px);
|
|
|
|
| 213 |
}
|
| 214 |
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
| 215 |
@media (max-width: 1080px) {
|
| 216 |
+
.layout { grid-template-columns: 1fr; width: 100%; border-left: 0; border-right: 0; }
|
| 217 |
.column { border-right: 0; border-bottom: 1px solid var(--border); }
|
|
|
|
| 218 |
}
|
| 219 |
@media (max-width: 680px) {
|
| 220 |
.topbar { padding: 0 12px; }
|
|
|
|
| 305 |
<div class="timebar" id="timebar"><div class="progress-line" id="progressLine"></div></div>
|
| 306 |
<div class="time-readout"><span id="currentTime">0.0s</span><span id="durationTime">0.0s</span></div>
|
| 307 |
<div class="events" id="eventsList"></div>
|
| 308 |
+
<details class="model-details" id="modelDetails">
|
| 309 |
+
<summary><span>Model output</span><span id="detailsSummary">Idle</span></summary>
|
| 310 |
+
<div class="details-body">
|
| 311 |
+
<div class="section">
|
| 312 |
+
<h3>Scene</h3>
|
| 313 |
+
<p class="scene empty" id="sceneText">No scene yet.</p>
|
| 314 |
+
</div>
|
| 315 |
+
<div class="section">
|
| 316 |
+
<h3>Status</h3>
|
| 317 |
+
<p class="scene" id="runStatus">Idle</p>
|
| 318 |
+
</div>
|
| 319 |
+
<div class="section">
|
| 320 |
+
<h3>Raw output</h3>
|
| 321 |
+
<pre class="raw" id="rawText"></pre>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
</details>
|
| 325 |
</div>
|
| 326 |
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
</div>
|
| 328 |
</section>
|
| 329 |
</section>
|
|
|
|
| 370 |
sceneText: $("sceneText"),
|
| 371 |
rawText: $("rawText"),
|
| 372 |
runStatus: $("runStatus"),
|
| 373 |
+
detailsSummary: $("detailsSummary"),
|
| 374 |
toast: $("toast"),
|
| 375 |
};
|
| 376 |
|
| 377 |
+
function escapeHtml(value) {
|
| 378 |
+
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
| 379 |
+
"&": "&",
|
| 380 |
+
"<": "<",
|
| 381 |
+
">": ">",
|
| 382 |
+
'"': """,
|
| 383 |
+
"'": "'",
|
| 384 |
+
}[char]));
|
| 385 |
+
}
|
| 386 |
function setStatus(text, icon = "circle-dot") {
|
| 387 |
els.statusText.innerHTML = `<i data-lucide="${icon}"></i><span>${text}</span>`;
|
| 388 |
lucide.createIcons();
|
|
|
|
| 428 |
els.sceneText.classList.add("empty");
|
| 429 |
els.rawText.textContent = "";
|
| 430 |
els.runStatus.textContent = "Idle";
|
| 431 |
+
els.detailsSummary.textContent = "Idle";
|
| 432 |
}
|
| 433 |
+
async function selectExample(index, options = {}) {
|
| 434 |
+
const { autoAnalyze = true } = options;
|
| 435 |
const example = state.examples[index];
|
| 436 |
if (!example) return;
|
| 437 |
state.exampleIndex = index;
|
|
|
|
| 438 |
for (const button of els.exampleList.querySelectorAll(".example-btn")) {
|
| 439 |
button.classList.toggle("active", Number(button.dataset.index) === index);
|
| 440 |
}
|
|
|
|
| 444 |
els.tokenValue.textContent = String(example.tokens);
|
| 445 |
els.tempInput.value = example.temperature;
|
| 446 |
els.tempValue.textContent = Number(example.temperature).toFixed(1);
|
| 447 |
+
state.exampleMatchesPreset = true;
|
| 448 |
resetAnalysis();
|
| 449 |
const response = await fetch(example.url);
|
| 450 |
const blob = await response.blob();
|
| 451 |
const file = new File([blob], example.file, { type: blob.type || "video/mp4" });
|
| 452 |
setVideoFile(file, URL.createObjectURL(blob), example.title);
|
| 453 |
+
if (autoAnalyze && example.cached) await analyze();
|
| 454 |
}
|
| 455 |
async function loadExamples() {
|
| 456 |
const response = await fetch("/examples.json");
|
|
|
|
| 462 |
button.className = "example-btn";
|
| 463 |
button.dataset.index = index;
|
| 464 |
button.type = "button";
|
| 465 |
+
const thumb = example.thumbnail
|
| 466 |
+
? `<img class="example-thumb" src="${escapeHtml(example.thumbnail)}" alt="" loading="lazy" />`
|
| 467 |
+
: `<span class="example-thumb"></span>`;
|
| 468 |
+
button.innerHTML = `${thumb}<span class="example-copy"><strong>${escapeHtml(example.title)}</strong><span>${escapeHtml(example.task)}${example.cached ? " - cached" : ""}</span></span>`;
|
| 469 |
button.addEventListener("click", () => selectExample(index));
|
| 470 |
els.exampleList.appendChild(button);
|
| 471 |
});
|
| 472 |
+
if (state.examples.length) await selectExample(0);
|
| 473 |
}
|
| 474 |
function renderTimeline(payload) {
|
| 475 |
els.timebar.querySelectorAll(".segment").forEach((node) => node.remove());
|
|
|
|
| 501 |
row.type = "button";
|
| 502 |
row.className = "event-row";
|
| 503 |
row.dataset.index = index;
|
| 504 |
+
row.innerHTML = `<time>${seconds(event.start)} - ${seconds(event.end)}</time><span>${escapeHtml(event.description)}</span>`;
|
| 505 |
row.addEventListener("click", () => seekTo(event.start));
|
| 506 |
els.eventsList.appendChild(row);
|
| 507 |
}
|
|
|
|
| 514 |
els.sceneText.classList.toggle("empty", !payload.scene && !payload.span);
|
| 515 |
els.rawText.textContent = payload.raw || "";
|
| 516 |
els.runStatus.textContent = `${payload.status}${payload.cached ? " | cache hit" : ""}`;
|
| 517 |
+
els.detailsSummary.textContent = payload.cached ? "Cache hit" : "Ready";
|
| 518 |
updateActiveCaption();
|
| 519 |
}
|
| 520 |
function seekTo(time) {
|
|
|
|
| 537 |
}
|
| 538 |
if (active >= 0) {
|
| 539 |
const event = events[active];
|
| 540 |
+
els.activeCaption.innerHTML = `<small>${seconds(event.start)} - ${seconds(event.end)}</small>${escapeHtml(event.description)}`;
|
| 541 |
els.activeCaption.classList.remove("empty");
|
| 542 |
} else if (state.analysis?.scene) {
|
| 543 |
+
els.activeCaption.innerHTML = `<small>Scene</small>${escapeHtml(state.analysis.scene)}`;
|
| 544 |
els.activeCaption.classList.remove("empty");
|
| 545 |
} else {
|
| 546 |
els.activeCaption.textContent = "Run analysis to sync captions with playback.";
|
pid/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
pid/_ext/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Copied from cosmos repository, with necessary modifications.
|
| 3 |
+
Original source: https://github.com/nvidia-cosmos/cosmos-predict2.5/
|
| 4 |
+
"""
|
pid/_ext/imaginaire/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
pid/_ext/imaginaire/checkpointer/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
pid/_ext/imaginaire/checkpointer/base.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
from abc import ABC, abstractmethod
|
| 18 |
+
from typing import Optional
|
| 19 |
+
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
from pid._ext.imaginaire.config import CheckpointConfig, JobConfig
|
| 23 |
+
from pid._ext.imaginaire.model import ImaginaireModel
|
| 24 |
+
from pid._ext.imaginaire.utils import callback
|
| 25 |
+
from pid._ext.imaginaire.utils.easy_io import easy_io
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class AbstractCheckpointer(ABC):
|
| 29 |
+
"""The checkpointer class. Supports checkpoint saving/loading to both local disk or object store."""
|
| 30 |
+
|
| 31 |
+
def __init__(
|
| 32 |
+
self,
|
| 33 |
+
config_checkpoint: CheckpointConfig,
|
| 34 |
+
config_job: JobConfig,
|
| 35 |
+
callbacks: Optional[callback.CallBackGroup] = None,
|
| 36 |
+
):
|
| 37 |
+
"""Constructor of the checkpointer.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
config_checkpoint (CheckpointConfig): The config object for the checkpointer.
|
| 41 |
+
"""
|
| 42 |
+
self.config_checkpoint = config_checkpoint
|
| 43 |
+
# Set the callback functions.
|
| 44 |
+
self.callbacks = callbacks
|
| 45 |
+
self.save_to_object_store = config_checkpoint.save_to_object_store.enabled
|
| 46 |
+
self.load_from_object_store = config_checkpoint.load_from_object_store.enabled
|
| 47 |
+
|
| 48 |
+
# Set checkpoint directories for local and object store paths
|
| 49 |
+
self._local_dirname = os.path.join(config_job.path_local, "checkpoints")
|
| 50 |
+
self._object_store_dirname = os.path.join(config_job.path, "checkpoints")
|
| 51 |
+
|
| 52 |
+
self.strict_resume = config_checkpoint.strict_resume
|
| 53 |
+
self.load_path = config_checkpoint.load_path or None
|
| 54 |
+
self.load_training_state = config_checkpoint.load_training_state
|
| 55 |
+
self.only_load_scheduler_state = config_checkpoint.only_load_scheduler_state
|
| 56 |
+
self.save_thread = None
|
| 57 |
+
self.verbose = config_checkpoint.verbose
|
| 58 |
+
self.keys_not_to_resume = config_checkpoint.keys_not_to_resume
|
| 59 |
+
self.broadcast_via_filesystem = config_checkpoint.broadcast_via_filesystem
|
| 60 |
+
# Create the object store client interface.
|
| 61 |
+
if config_checkpoint.load_from_object_store.enabled:
|
| 62 |
+
self.load_s3_backend_key = "_ckpt_s3_loader"
|
| 63 |
+
easy_io.set_s3_backend(
|
| 64 |
+
key="_ckpt_s3_loader",
|
| 65 |
+
backend_args={
|
| 66 |
+
"backend": "s3",
|
| 67 |
+
"path_mapping": {
|
| 68 |
+
"s3://ckpt/": f"s3://{config_checkpoint.load_from_object_store.bucket}/",
|
| 69 |
+
},
|
| 70 |
+
"s3_credential_path": config_checkpoint.load_from_object_store.credentials,
|
| 71 |
+
},
|
| 72 |
+
)
|
| 73 |
+
else:
|
| 74 |
+
self.load_s3_backend_key = None
|
| 75 |
+
|
| 76 |
+
if config_checkpoint.save_to_object_store.enabled:
|
| 77 |
+
self.save_s3_backend_key = "_ckpt_s3_saver"
|
| 78 |
+
easy_io.set_s3_backend(
|
| 79 |
+
key="_ckpt_s3_saver",
|
| 80 |
+
backend_args={
|
| 81 |
+
"backend": "s3",
|
| 82 |
+
"path_mapping": {
|
| 83 |
+
"s3://ckpt/": f"s3://{config_checkpoint.save_to_object_store.bucket}/",
|
| 84 |
+
},
|
| 85 |
+
"s3_credential_path": config_checkpoint.save_to_object_store.credentials,
|
| 86 |
+
},
|
| 87 |
+
)
|
| 88 |
+
else:
|
| 89 |
+
self.save_s3_backend_key = None
|
| 90 |
+
|
| 91 |
+
@abstractmethod
|
| 92 |
+
def save(
|
| 93 |
+
self,
|
| 94 |
+
model: ImaginaireModel,
|
| 95 |
+
optimizer: torch.optim.Optimizer,
|
| 96 |
+
scheduler: torch.optim.lr_scheduler.LRScheduler,
|
| 97 |
+
grad_scaler: torch.amp.GradScaler,
|
| 98 |
+
iteration: int,
|
| 99 |
+
) -> None:
|
| 100 |
+
pass
|
| 101 |
+
|
| 102 |
+
@abstractmethod
|
| 103 |
+
def load(
|
| 104 |
+
self,
|
| 105 |
+
model: ImaginaireModel,
|
| 106 |
+
optimizer: Optional[torch.optim.Optimizer] = None,
|
| 107 |
+
scheduler: Optional[torch.optim.lr_scheduler.LRScheduler] = None,
|
| 108 |
+
grad_scaler: Optional[torch.amp.GradScaler] = None,
|
| 109 |
+
) -> int:
|
| 110 |
+
pass
|
| 111 |
+
|
| 112 |
+
@property
|
| 113 |
+
def save_bucket(self):
|
| 114 |
+
"""Get the bucket name for saving checkpoints."""
|
| 115 |
+
return self.config_checkpoint.save_to_object_store.bucket if self.save_to_object_store else None
|
| 116 |
+
|
| 117 |
+
@property
|
| 118 |
+
def load_bucket(self):
|
| 119 |
+
"""Get the bucket name for loading checkpoints."""
|
| 120 |
+
return self.config_checkpoint.load_from_object_store.bucket if self.load_from_object_store else None
|
| 121 |
+
|
| 122 |
+
@property
|
| 123 |
+
def save_dirname(self):
|
| 124 |
+
return (
|
| 125 |
+
f"s3://{self.save_bucket}/{self._object_store_dirname}"
|
| 126 |
+
if self.save_to_object_store
|
| 127 |
+
else self._local_dirname
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
@property
|
| 131 |
+
def load_dirname(self):
|
| 132 |
+
return (
|
| 133 |
+
f"s3://{self.load_bucket}/{self._object_store_dirname}"
|
| 134 |
+
if self.load_from_object_store
|
| 135 |
+
else self._local_dirname
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
def finalize(self) -> None:
|
| 139 |
+
"""Finalize the checkpointer."""
|
| 140 |
+
if self.save_thread:
|
| 141 |
+
self.save_thread.join()
|
| 142 |
+
|
| 143 |
+
def _read_latest_checkpoint_file(self) -> str | None:
|
| 144 |
+
"""Get the file name of the latest saved checkpoint. If it doesn't exist, return None.
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
checkpoint_file (str | None): file name of the latest saved checkpoint.
|
| 148 |
+
"""
|
| 149 |
+
checkpoint_file = None
|
| 150 |
+
checkpoint_path = os.path.join(self.load_dirname, "latest_checkpoint.txt")
|
| 151 |
+
if easy_io.exists(f"{checkpoint_path}", backend_key=self.load_s3_backend_key):
|
| 152 |
+
checkpoint_file = easy_io.load(f"{checkpoint_path}", backend_key=self.load_s3_backend_key).strip()
|
| 153 |
+
|
| 154 |
+
return checkpoint_file
|
| 155 |
+
|
| 156 |
+
def _write_latest_checkpoint_file(self, checkpoint_file: str) -> None:
|
| 157 |
+
"""Track the file name of the latest saved checkpoint.
|
| 158 |
+
|
| 159 |
+
Args:
|
| 160 |
+
checkpoint_file (str): file name of the latest saved checkpoint.
|
| 161 |
+
"""
|
| 162 |
+
content = f"{checkpoint_file}\n"
|
| 163 |
+
checkpoint_path = os.path.join(self.save_dirname, "latest_checkpoint.txt")
|
| 164 |
+
easy_io.dump(
|
| 165 |
+
content,
|
| 166 |
+
checkpoint_path,
|
| 167 |
+
backend_key=self.save_s3_backend_key,
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
def _check_checkpoint_exists(self, checkpoint_path: str) -> None:
|
| 171 |
+
"""If the file checkpoint_path does not exist, raise an error.
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
checkpoint_path (str): full path to the checkpoint.
|
| 175 |
+
"""
|
| 176 |
+
if not easy_io.exists(f"{checkpoint_path}", backend_key=self.load_s3_backend_key):
|
| 177 |
+
raise FileNotFoundError(f"File not found (object store): {checkpoint_path}")
|
pid/_ext/imaginaire/checkpointer/dcp.py
ADDED
|
@@ -0,0 +1,1062 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
"""
|
| 18 |
+
Distributed checkpoint (DCP) directory structure and storage backends.
|
| 19 |
+
|
| 20 |
+
The checkpointer saves model state in a sharded format across multiple processes:
|
| 21 |
+
|
| 22 |
+
self.save_dirname/
|
| 23 |
+
├── iter_000000005/ # Checkpoint at iteration 5
|
| 24 |
+
│ ├── model/ # Model state shards
|
| 25 |
+
│ │ ├── __0_0.distcp # Shard 0 from rank 0
|
| 26 |
+
│ │ └── __1_0.distcp # Shard 1 from rank 1
|
| 27 |
+
│ ├── optim/ # Optimizer state shards
|
| 28 |
+
│ │ ├── __0_0.distcp # Shard 0 from rank 0
|
| 29 |
+
│ │ └── __1_0.distcp # Shard 1 from rank 1
|
| 30 |
+
│ ├── scheduler/ # Learning rate scheduler state
|
| 31 |
+
│ │ ├── __0_0.distcp # Shard 0 from rank 0
|
| 32 |
+
│ │ └── __1_0.distcp # Shard 1 from rank 1
|
| 33 |
+
│ └── trainer/ # Additional training state
|
| 34 |
+
│ ├── __0_0.distcp # Shard 0 from rank 0
|
| 35 |
+
│ └── __1_0.distcp # Shard 1 from rank 1
|
| 36 |
+
└── latest_checkpoint.txt # Points to most recent checkpoint folder, e.g. iter_000000005
|
| 37 |
+
|
| 38 |
+
Storage path format:
|
| 39 |
+
self.save_dirname = "{config_job.path_local}/checkpoints"
|
| 40 |
+
|
| 41 |
+
The sharded format enables efficient distributed saving/loading by:
|
| 42 |
+
1. Parallelizing I/O across processes
|
| 43 |
+
2. Reducing memory usage per process
|
| 44 |
+
3. Supporting both local and cloud storage backends
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
import enum
|
| 48 |
+
import functools
|
| 49 |
+
import multiprocessing
|
| 50 |
+
import os
|
| 51 |
+
import queue
|
| 52 |
+
import re
|
| 53 |
+
import time
|
| 54 |
+
import warnings
|
| 55 |
+
from collections import namedtuple
|
| 56 |
+
from multiprocessing import get_context
|
| 57 |
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast
|
| 58 |
+
|
| 59 |
+
import torch
|
| 60 |
+
import torch.distributed
|
| 61 |
+
import torch.distributed.checkpoint as dcp
|
| 62 |
+
from torch import nn
|
| 63 |
+
from torch.distributed.checkpoint import FileSystemReader, FileSystemWriter
|
| 64 |
+
from torch.distributed.checkpoint._storage_utils import _storage_setup
|
| 65 |
+
from torch.distributed.checkpoint.default_planner import DefaultSavePlanner
|
| 66 |
+
from torch.distributed.checkpoint.logger import _dcp_method_logger
|
| 67 |
+
from torch.distributed.checkpoint.state_dict import (
|
| 68 |
+
StateDictOptions,
|
| 69 |
+
get_model_state_dict,
|
| 70 |
+
get_optimizer_state_dict,
|
| 71 |
+
set_model_state_dict,
|
| 72 |
+
set_optimizer_state_dict,
|
| 73 |
+
)
|
| 74 |
+
from torch.distributed.checkpoint.stateful import Stateful
|
| 75 |
+
from torch.distributed.checkpoint.storage import StorageReader
|
| 76 |
+
from torch.distributed.checkpoint.utils import _api_bc_check, _DistWrapper, _profile
|
| 77 |
+
from torch.distributed.tensor import distribute_tensor
|
| 78 |
+
|
| 79 |
+
from pid._ext.imaginaire.checkpointer.base import AbstractCheckpointer
|
| 80 |
+
from pid._ext.imaginaire.checkpointer.s3_filesystem import S3StorageReader, S3StorageWriter
|
| 81 |
+
from pid._ext.imaginaire.config import CheckpointConfig, JobConfig
|
| 82 |
+
from pid._ext.imaginaire.model import ImaginaireModel
|
| 83 |
+
from pid._ext.imaginaire.utils import callback, distributed, log, misc
|
| 84 |
+
from pid._ext.imaginaire.utils.easy_io import easy_io
|
| 85 |
+
|
| 86 |
+
try:
|
| 87 |
+
"""
|
| 88 |
+
We override the default load function to skip loadding _extra_state keys created by transformer-engine.
|
| 89 |
+
"""
|
| 90 |
+
from torch.distributed.checkpoint.default_planner import DefaultLoadPlanner as _DefaultLoadPlanner
|
| 91 |
+
from torch.distributed.checkpoint.default_planner import (
|
| 92 |
+
DTensor,
|
| 93 |
+
LoadPlan,
|
| 94 |
+
_create_read_items,
|
| 95 |
+
_version,
|
| 96 |
+
flatten_state_dict,
|
| 97 |
+
)
|
| 98 |
+
from torch.distributed.checkpoint.metadata import Metadata, TensorStorageMetadata
|
| 99 |
+
|
| 100 |
+
def create_default_local_load_plan(state_dict: dict[str, Any], metadata: Metadata, strict: bool = True) -> LoadPlan:
|
| 101 |
+
requests = []
|
| 102 |
+
"""
|
| 103 |
+
Create the ``LoadPlan`` used by DefaultLoadPlanner.
|
| 104 |
+
|
| 105 |
+
It produces one read item per value in ``state_dict`` using the metadata in ``metadata``.
|
| 106 |
+
|
| 107 |
+
The default behavior is to match key exactly between state_dict and metadata.
|
| 108 |
+
It handles resharding by issuing multiple read requests against storage in order to match
|
| 109 |
+
load requirements.
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
for fqn, obj in state_dict.items():
|
| 113 |
+
if fqn.endswith("._extra_state"): # dirty TE attention package!
|
| 114 |
+
continue
|
| 115 |
+
# ignore state_dict keys which do not exist in `state_dict` if strict=False
|
| 116 |
+
if fqn not in metadata.state_dict_metadata:
|
| 117 |
+
if strict:
|
| 118 |
+
raise RuntimeError(f"Missing key in checkpoint state_dict: {fqn}.")
|
| 119 |
+
else:
|
| 120 |
+
log.warning(f"Local load plan: Missing key in checkpoint state_dict: {fqn}.")
|
| 121 |
+
continue
|
| 122 |
+
|
| 123 |
+
md = metadata.state_dict_metadata[fqn]
|
| 124 |
+
|
| 125 |
+
if (
|
| 126 |
+
isinstance(md, TensorStorageMetadata)
|
| 127 |
+
and getattr(obj, "size", None) is not None
|
| 128 |
+
and md.size != obj.size()
|
| 129 |
+
):
|
| 130 |
+
if not strict:
|
| 131 |
+
log.critical(f"Size mismatch between saved {md.size} and current: {obj.size()} for {fqn}")
|
| 132 |
+
continue
|
| 133 |
+
else:
|
| 134 |
+
raise ValueError(
|
| 135 |
+
f"Size mismatch between saved {md.size} and current: {obj.size()} for {fqn}",
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# Since DTensor supports submesh, adding extra check to ensure _create_read_items()
|
| 139 |
+
# gets called only when the current rank is part of the mesh for the corresponding DTensor.
|
| 140 |
+
if isinstance(obj, DTensor):
|
| 141 |
+
if obj.device_mesh.get_coordinate() is not None:
|
| 142 |
+
requests += _create_read_items(fqn, md, obj)
|
| 143 |
+
else:
|
| 144 |
+
requests += _create_read_items(fqn, md, obj)
|
| 145 |
+
|
| 146 |
+
return LoadPlan(requests)
|
| 147 |
+
|
| 148 |
+
class DefaultLoadPlanner(_DefaultLoadPlanner):
|
| 149 |
+
def create_local_plan(self) -> LoadPlan:
|
| 150 |
+
assert self.metadata is not None
|
| 151 |
+
if self.flatten_state_dict:
|
| 152 |
+
# To support checkpoints that are saved before v2.4, we have to
|
| 153 |
+
# differentiate if the missing keys are due to old checkpoints.
|
| 154 |
+
# The contracts are:
|
| 155 |
+
# 1. There are 3 cases when we found a missing key.
|
| 156 |
+
# 1.1 Actual missing key, but allow_partial_load is False
|
| 157 |
+
# 1.2 Actual missing key, but allow_partial load is True
|
| 158 |
+
# 1.3 Old checkpoint, but allow_partial_load is False
|
| 159 |
+
# 1.4 Old checkpoint, but allow_partial_load is True
|
| 160 |
+
# 2. If we found a missing key, we first convert the keys back to
|
| 161 |
+
# the key format of v2.3
|
| 162 |
+
# 3. If the previous missing keys are in the v2.3 keys, we assume
|
| 163 |
+
# this is a old checkpoint.
|
| 164 |
+
# 4. Pass the state_dict to `create_default_local_load_plan()`,
|
| 165 |
+
# which has the logic to check missing for allow_partial_load.
|
| 166 |
+
# So for 1.2 and 1.4 cases, we delegate allow_partial_load check to
|
| 167 |
+
# `create_default_local_load_plan()`. The logic here is to determine
|
| 168 |
+
# whether the checkpoint belong to 2.3 (or before) or 2.4 (or after).
|
| 169 |
+
current_keys = set(self.state_dict.keys())
|
| 170 |
+
load_keys = set(self.metadata.state_dict_metadata.keys())
|
| 171 |
+
missing_keys = load_keys - current_keys
|
| 172 |
+
if missing_keys:
|
| 173 |
+
_version._derived_version = "2_3"
|
| 174 |
+
old_state_dict, old_mappings = flatten_state_dict(self.original_state_dict)
|
| 175 |
+
old_keys = set(old_state_dict.keys())
|
| 176 |
+
if old_keys & missing_keys:
|
| 177 |
+
self.state_dict, self.mappings = old_state_dict, old_mappings
|
| 178 |
+
# _derived_version is only used by flatten_state_dict now.
|
| 179 |
+
# Set it back to None so that later we can save to a new version.
|
| 180 |
+
_version._derived_version = None
|
| 181 |
+
|
| 182 |
+
return create_default_local_load_plan(self.state_dict, self.metadata, not self.allow_partial_load)
|
| 183 |
+
|
| 184 |
+
log.info("for the back comptiable pytorch! New DefaultLoadPlanner class is created.")
|
| 185 |
+
|
| 186 |
+
@_dcp_method_logger(log_exceptions=True)
|
| 187 |
+
@_api_bc_check
|
| 188 |
+
def load(
|
| 189 |
+
state_dict: dict[str, Any],
|
| 190 |
+
*,
|
| 191 |
+
checkpoint_id: Union[str, os.PathLike, None] = None,
|
| 192 |
+
storage_reader: Optional[StorageReader] = None,
|
| 193 |
+
planner: Optional[DefaultLoadPlanner] = None,
|
| 194 |
+
process_group: Optional[torch.distributed.ProcessGroup] = None,
|
| 195 |
+
no_dist: bool = False,
|
| 196 |
+
) -> None:
|
| 197 |
+
"""
|
| 198 |
+
We override the default load function to perform a load plan check for mismatched/missing keys.
|
| 199 |
+
==========================Original Doc string=====================================
|
| 200 |
+
Load a checkpoint into a distributed state dict in SPMD style.
|
| 201 |
+
|
| 202 |
+
Each rank must have the same keys in their ``state_dict`` provided to this
|
| 203 |
+
API. Mismatched keys may result in hangs or errors. If unsure, you can use
|
| 204 |
+
the ``utils._assert_same_keys`` API to check (but may incur communication
|
| 205 |
+
costs).
|
| 206 |
+
|
| 207 |
+
Each rank will try to read the least amount of data necessary
|
| 208 |
+
to fullfill the requested `state_dict`. When loading :class:`ShardedTensor`
|
| 209 |
+
or :class:`DTensor` instances, each rank only reads data for their local shards.
|
| 210 |
+
|
| 211 |
+
For each ``Stateful`` object (having both a ``state_dict`` and a ``load_state_dict``),
|
| 212 |
+
load will first call ``state_dict`` before attempting deserialization, followed by
|
| 213 |
+
``load_state_dict`` once the deserialization is complete.
|
| 214 |
+
For each non-``Stateful`` object, load will deserailize the object, and then replace
|
| 215 |
+
it in the ``state_dict`` with the deserialized object.
|
| 216 |
+
|
| 217 |
+
.. warning::
|
| 218 |
+
All tensors in ``state_dict`` must be allocated on their
|
| 219 |
+
destination device *prior to* calling this function.
|
| 220 |
+
|
| 221 |
+
All non-tensor data is loaded using `torch.load()` and modified in place
|
| 222 |
+
on state_dict.
|
| 223 |
+
|
| 224 |
+
.. warning::
|
| 225 |
+
Users must call `load_state_dict` on the root module to ensure load
|
| 226 |
+
pos-processing and non-tensor data properly propagates.
|
| 227 |
+
|
| 228 |
+
.. note:
|
| 229 |
+
If no process group is initialized, this function will assume the intent
|
| 230 |
+
is to load a checkpoint into the local process. This can be useful in the
|
| 231 |
+
case of local inference, and when using regular Tensors (as opposed to DTensor
|
| 232 |
+
or ShardedTensor)
|
| 233 |
+
|
| 234 |
+
.. note:
|
| 235 |
+
Rank 0 is assumed to be the coordinator rank.
|
| 236 |
+
|
| 237 |
+
Args:
|
| 238 |
+
state_dict (Dict[str, Any]): The state_dict to load the checkpoint into.
|
| 239 |
+
checkpoint_id (Union[str, os.PathLike, None]):
|
| 240 |
+
The ID of this checkpoint instance. The meaning of the checkpoint_id
|
| 241 |
+
depends on the storage. It can be a path to a folder or to a file.
|
| 242 |
+
It can also be a key if the storage is a key-value store.
|
| 243 |
+
(Default: ``None``)
|
| 244 |
+
storage_reader (Optional[StorageReader]):
|
| 245 |
+
Instance of StorageWriter used to perform reads. If this is not
|
| 246 |
+
specified, DCP will automatically infer the reader based on the
|
| 247 |
+
checkpoint_id. If checkpoint_id is also None, an exception will
|
| 248 |
+
be raised. (Default: ``None``)
|
| 249 |
+
planner (Optional[LoadPlanner]):
|
| 250 |
+
Instance of LoadPlanner. If this is not specificed, the default
|
| 251 |
+
planner will be used. (Default: ``None``)
|
| 252 |
+
process_group (Optional[ProcessGroup]):
|
| 253 |
+
ProcessGroup to be used for cross-rank synchronization.
|
| 254 |
+
(Default: ``None``)
|
| 255 |
+
no_dist (bool): If ``True``, this function will assume the intent is to load
|
| 256 |
+
a checkpoint without using cross-rank synchronization. (Default: ``False``)
|
| 257 |
+
Returns:
|
| 258 |
+
None.
|
| 259 |
+
|
| 260 |
+
Examples
|
| 261 |
+
>>> # xdoctest: +SKIP
|
| 262 |
+
>>> my_model = MyModule()
|
| 263 |
+
>>> optimizer = Adagrad(my_model.parameters())
|
| 264 |
+
>>> model_state_dict = my_model.state_dict()
|
| 265 |
+
>>> fs_storage_reader = torch.distributed.checkpoint.FileSystemReader(
|
| 266 |
+
... "/checkpoint/1"
|
| 267 |
+
... )
|
| 268 |
+
|
| 269 |
+
>>> torch.distributed.checkpoint.load_state_dict(
|
| 270 |
+
>>> state_dict=model_state_dict,
|
| 271 |
+
>>> storage_reader=fs_storage_reader,
|
| 272 |
+
>>> )
|
| 273 |
+
|
| 274 |
+
>>> # module.load_state_dict() function might have customized steps
|
| 275 |
+
>>> # to flush the state_dict, must call it to
|
| 276 |
+
>>> # ensure correct behavior.
|
| 277 |
+
>>> my_model.load_state_dict(model_state_dict)
|
| 278 |
+
|
| 279 |
+
.. note::
|
| 280 |
+
load_state_dict uses collectives to coordinate reads across ranks.
|
| 281 |
+
For NCCL-based process groups, internal tensor representations of
|
| 282 |
+
objects must be moved to the GPU device before communication takes place.
|
| 283 |
+
In this case, the device used is given by ``torch.cuda.current_device()``
|
| 284 |
+
and it is the user's responsibility to ensure that this is set so that each
|
| 285 |
+
rank has an individual GPU, via ``torch.cuda.set_device()``.
|
| 286 |
+
"""
|
| 287 |
+
|
| 288 |
+
no_dist = no_dist or (not torch.distributed.is_available()) or (not torch.distributed.is_initialized())
|
| 289 |
+
if no_dist:
|
| 290 |
+
warnings.warn(
|
| 291 |
+
"torch.distributed is disabled, unavailable or uninitialized, assuming the intent is to load in a single process."
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
with _profile():
|
| 295 |
+
storage_reader = cast(StorageReader, _storage_setup(storage_reader, checkpoint_id, reader=True))
|
| 296 |
+
|
| 297 |
+
# All ranks must have the same keys in their `state_dict` provided to
|
| 298 |
+
# this API. See documentation for more details.
|
| 299 |
+
# Here we simply sort the keys to ensure that all ranks load values in
|
| 300 |
+
# the same order.
|
| 301 |
+
keys = sorted(state_dict.keys())
|
| 302 |
+
|
| 303 |
+
statetful_sd = {}
|
| 304 |
+
for key in keys:
|
| 305 |
+
if key not in state_dict:
|
| 306 |
+
continue
|
| 307 |
+
elem = state_dict[key]
|
| 308 |
+
statetful_sd[key] = elem.state_dict() if isinstance(elem, Stateful) else elem
|
| 309 |
+
|
| 310 |
+
_load_state_dict(
|
| 311 |
+
state_dict=statetful_sd,
|
| 312 |
+
storage_reader=storage_reader,
|
| 313 |
+
process_group=process_group,
|
| 314 |
+
no_dist=no_dist,
|
| 315 |
+
planner=planner,
|
| 316 |
+
)
|
| 317 |
+
for key in keys:
|
| 318 |
+
if key not in state_dict:
|
| 319 |
+
continue
|
| 320 |
+
elem = state_dict[key]
|
| 321 |
+
if isinstance(elem, Stateful):
|
| 322 |
+
# If the state_dict is a Stateful object,
|
| 323 |
+
# DCP does an in-place load in the original state dict.
|
| 324 |
+
elem.load_state_dict(statetful_sd[key])
|
| 325 |
+
else:
|
| 326 |
+
# Otherwise, replace the state_dict with the loaded state_dict.
|
| 327 |
+
state_dict[key] = statetful_sd[key]
|
| 328 |
+
|
| 329 |
+
def _load_state_dict(
|
| 330 |
+
state_dict: dict[str, Any],
|
| 331 |
+
storage_reader: StorageReader,
|
| 332 |
+
process_group: Optional[torch.distributed.ProcessGroup] = None,
|
| 333 |
+
coordinator_rank: int = 0,
|
| 334 |
+
no_dist: bool = False,
|
| 335 |
+
planner: Optional[DefaultLoadPlanner] = None,
|
| 336 |
+
) -> None:
|
| 337 |
+
torch._C._log_api_usage_once("torch.distributed.checkpoint.load_state_dict")
|
| 338 |
+
|
| 339 |
+
distW = _DistWrapper(process_group, not no_dist, coordinator_rank)
|
| 340 |
+
if planner is None:
|
| 341 |
+
planner = DefaultLoadPlanner()
|
| 342 |
+
|
| 343 |
+
ckpt_kwargs = {}
|
| 344 |
+
if (ckpt_id := getattr(storage_reader, "checkpoint_id", None)) is not None:
|
| 345 |
+
ckpt_kwargs["checkpoint_id"] = ckpt_id
|
| 346 |
+
ckpt_kwargs["process_group"] = distW.group
|
| 347 |
+
|
| 348 |
+
@_dcp_method_logger(**ckpt_kwargs)
|
| 349 |
+
def local_step():
|
| 350 |
+
assert planner is not None
|
| 351 |
+
metadata = storage_reader.read_metadata()
|
| 352 |
+
planner.set_up_planner(state_dict, metadata, distW.is_coordinator)
|
| 353 |
+
storage_reader.set_up_storage_reader(metadata, distW.is_coordinator)
|
| 354 |
+
|
| 355 |
+
local_plan = planner.create_local_plan()
|
| 356 |
+
local_plan = storage_reader.prepare_local_plan(local_plan)
|
| 357 |
+
return local_plan
|
| 358 |
+
|
| 359 |
+
@_dcp_method_logger(**ckpt_kwargs)
|
| 360 |
+
def global_step(all_local_plans):
|
| 361 |
+
assert planner is not None
|
| 362 |
+
all_local_plans = planner.create_global_plan(all_local_plans)
|
| 363 |
+
all_local_plans = storage_reader.prepare_global_plan(all_local_plans)
|
| 364 |
+
return all_local_plans
|
| 365 |
+
|
| 366 |
+
central_plan: LoadPlan = distW.reduce_scatter("plan", local_step, global_step)
|
| 367 |
+
if distW.is_coordinator:
|
| 368 |
+
# Compare central_plan items with storage_reader.storage_data keys
|
| 369 |
+
dest_fqns = set()
|
| 370 |
+
storage_fqns = set()
|
| 371 |
+
|
| 372 |
+
# Extract FQNs from central_plan items
|
| 373 |
+
for item in central_plan.items:
|
| 374 |
+
if hasattr(item, "dest_index") and hasattr(item.dest_index, "fqn"):
|
| 375 |
+
dest_fqns.add(item.dest_index.fqn)
|
| 376 |
+
if hasattr(item, "storage_index") and hasattr(item.storage_index, "fqn"):
|
| 377 |
+
storage_fqns.add(item.storage_index.fqn)
|
| 378 |
+
|
| 379 |
+
# Get storage data keys
|
| 380 |
+
storage_data_keys = set()
|
| 381 |
+
if hasattr(storage_reader, "storage_data") and storage_reader.storage_data is not None:
|
| 382 |
+
storage_data_keys = set(item[0].fqn for item in storage_reader.storage_data.items())
|
| 383 |
+
state_dict_keys = set(state_dict.keys())
|
| 384 |
+
# Compare sets and log differences
|
| 385 |
+
# Remove any item that has "_extra_state" as substring in the sets
|
| 386 |
+
state_dict_keys = {fqn for fqn in state_dict_keys if "_extra_state" not in fqn}
|
| 387 |
+
dest_fqns = {fqn for fqn in dest_fqns if "_extra_state" not in fqn}
|
| 388 |
+
storage_fqns = {fqn for fqn in storage_fqns if "_extra_state" not in fqn}
|
| 389 |
+
storage_data_keys = {fqn for fqn in storage_data_keys if "_extra_state" not in fqn}
|
| 390 |
+
|
| 391 |
+
log.info("=== Load Plan FQN Analysis ===")
|
| 392 |
+
log.info(f"State Dict FQNs count: {len(state_dict_keys)}")
|
| 393 |
+
log.info(f"Destination FQNs count (without _extra_state): {len(dest_fqns)}")
|
| 394 |
+
log.info(f"Loaded FQNs count (without _extra_state): {len(storage_fqns)}")
|
| 395 |
+
log.info(f"In Storage keys count (without _extra_state): {len(storage_data_keys)}")
|
| 396 |
+
|
| 397 |
+
# Find missing keys in each direction
|
| 398 |
+
state_dict_missing_from_dest = state_dict_keys - dest_fqns
|
| 399 |
+
storage_data_missing_from_storage_fqns = storage_data_keys - storage_fqns
|
| 400 |
+
|
| 401 |
+
if state_dict_missing_from_dest:
|
| 402 |
+
log.info(
|
| 403 |
+
f"State Dict FQNs missing from load plan ({len(state_dict_missing_from_dest)} items): {sorted(state_dict_missing_from_dest)}"
|
| 404 |
+
)
|
| 405 |
+
else:
|
| 406 |
+
log.info("✓ All State Dict FQNs found in storage_data")
|
| 407 |
+
|
| 408 |
+
if storage_data_missing_from_storage_fqns:
|
| 409 |
+
# If there are more than 100 "net_ema" keys in storage_data_missing_from_storage_fqns, summarize them
|
| 410 |
+
net_ema_keys = {k for k in storage_data_missing_from_storage_fqns if "net_ema" in k}
|
| 411 |
+
if len(net_ema_keys) > 100:
|
| 412 |
+
storage_data_missing_from_storage_fqns = storage_data_missing_from_storage_fqns - net_ema_keys
|
| 413 |
+
storage_data_missing_from_storage_fqns = set(
|
| 414 |
+
storage_data_missing_from_storage_fqns
|
| 415 |
+
) # ensure set type
|
| 416 |
+
storage_data_missing_from_storage_fqns.add("net_ema")
|
| 417 |
+
log.info(
|
| 418 |
+
f"Summarized {len(net_ema_keys)} 'net_ema' keys as 'net_ema' in missing storage data keys."
|
| 419 |
+
)
|
| 420 |
+
log.info(
|
| 421 |
+
f"Storage data keys not loaded by load plan ({len(storage_data_missing_from_storage_fqns)} items): {sorted(storage_data_missing_from_storage_fqns)}"
|
| 422 |
+
)
|
| 423 |
+
else:
|
| 424 |
+
log.info("✓ All storage data keys found in Loaded FQNs")
|
| 425 |
+
|
| 426 |
+
log.info("=== End Load Plan FQN Analysis ===")
|
| 427 |
+
|
| 428 |
+
@_dcp_method_logger(**ckpt_kwargs)
|
| 429 |
+
def read_data():
|
| 430 |
+
assert planner is not None
|
| 431 |
+
final_local_plan = planner.finish_plan(central_plan)
|
| 432 |
+
all_reads = storage_reader.read_data(final_local_plan, planner)
|
| 433 |
+
|
| 434 |
+
all_reads.wait()
|
| 435 |
+
return None
|
| 436 |
+
|
| 437 |
+
_ = distW.all_gather("read", read_data)
|
| 438 |
+
|
| 439 |
+
except ImportError as e:
|
| 440 |
+
from torch.distributed.checkpoint.default_planner import DefaultLoadPlanner
|
| 441 |
+
|
| 442 |
+
log.critical(f"{e}, using default planner")
|
| 443 |
+
|
| 444 |
+
StateDictItemPath = namedtuple("StateDictItemPath", ["state_dict", "save_path"])
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
class ModelWrapper(Stateful):
|
| 448 |
+
"""Wrapper for model state dict handling with DCP (Distributed Checkpoint).
|
| 449 |
+
|
| 450 |
+
Usage flow: state_dict() -> dcp.load() -> load_state_dict()
|
| 451 |
+
|
| 452 |
+
When load_ema_to_reg=True, the three steps work together to load EMA weights from
|
| 453 |
+
checkpoint into the regular model:
|
| 454 |
+
1. state_dict(): prepare keys so dcp.load() reads EMA weights from checkpoint
|
| 455 |
+
2. dcp.load(): fills the state_dict with checkpoint data (in-place)
|
| 456 |
+
3. load_state_dict(): remap loaded EMA weights back to regular model keys, then apply
|
| 457 |
+
"""
|
| 458 |
+
|
| 459 |
+
def __init__(self, model: Union[nn.Module, List[nn.Module]], load_ema_to_reg: bool = False):
|
| 460 |
+
self.model = [model] if isinstance(model, nn.Module) else model
|
| 461 |
+
self.load_ema_to_reg = load_ema_to_reg
|
| 462 |
+
if self.load_ema_to_reg:
|
| 463 |
+
assert isinstance(model, ImaginaireModel), (
|
| 464 |
+
f"replicate_ema_to_reg requires an ImaginaireModel, got {type(model).__name__}"
|
| 465 |
+
)
|
| 466 |
+
|
| 467 |
+
def state_dict(self) -> Dict[str, Any]:
|
| 468 |
+
"""Build a state_dict whose keys will be matched against checkpoint keys by dcp.load().
|
| 469 |
+
|
| 470 |
+
When load_ema_to_reg=True, we manipulate the keys so that dcp.load() reads the
|
| 471 |
+
checkpoint's net_ema.* weights (instead of net.*):
|
| 472 |
+
|
| 473 |
+
Case A - ema.enabled=False (model has no EMA module, only net.*):
|
| 474 |
+
Rename net.* -> net_ema.* so dcp.load() matches checkpoint's net_ema.* keys.
|
| 475 |
+
Result after dcp.load(): state_dict has net_ema.* filled with checkpoint EMA weights.
|
| 476 |
+
|
| 477 |
+
Case B - ema.enabled=True (model has both net.* and net_ema.*):
|
| 478 |
+
Drop net.* keys entirely, keep net_ema.* (and other keys like fake_score.*).
|
| 479 |
+
Result after dcp.load(): net_ema.* filled from checkpoint; other keys (e.g. fake_score.*)
|
| 480 |
+
loaded if present in checkpoint, otherwise unchanged.
|
| 481 |
+
"""
|
| 482 |
+
_state_dict = {k: v for sd in map(get_model_state_dict, self.model) for k, v in sd.items()}
|
| 483 |
+
if self.load_ema_to_reg:
|
| 484 |
+
if not self.model[0].config.ema.enabled:
|
| 485 |
+
# Case A: model only has net.* -> rename to net_ema.* to match checkpoint EMA keys.
|
| 486 |
+
# Auxiliary keys (aux_losses.*, teacher_align_loss.*, etc.) are passed through as-is.
|
| 487 |
+
all_keys = list(_state_dict.keys())
|
| 488 |
+
for k in all_keys:
|
| 489 |
+
if k.startswith("net."):
|
| 490 |
+
_state_dict[k.replace("net.", "net_ema.", 1)] = _state_dict.pop(k)
|
| 491 |
+
else:
|
| 492 |
+
# Case B: model has net.* + net_ema.* -> drop net.*, only load net_ema.* from ckpt
|
| 493 |
+
log.warning("EMA is enabled, will only load EMA weights from checkpoint file.")
|
| 494 |
+
all_keys = list(_state_dict.keys())
|
| 495 |
+
for k in all_keys:
|
| 496 |
+
if k.startswith("net_ema."):
|
| 497 |
+
break
|
| 498 |
+
else:
|
| 499 |
+
raise ValueError("No EMA keys found in state_dict")
|
| 500 |
+
_state_dict = {k: _state_dict[k] for k in all_keys if not k.startswith("net.")}
|
| 501 |
+
|
| 502 |
+
if hasattr(self.model[0].config, "lora_config") and self.model[0].config.lora_config.enabled:
|
| 503 |
+
"""
|
| 504 |
+
When using LoRA, `inject_adapter_in_model` modifies the target modules in place.
|
| 505 |
+
For example, `blocks[0].attn.q_proj.weight` will be modified to `blocks[0].attn.q_proj.base_layer.weight`.
|
| 506 |
+
This means that the model will have the key `blocks[0].attn.q_proj.base_layer.weight`,
|
| 507 |
+
but the checkpoint will have the key `blocks[0].attn.q_proj.weight`.
|
| 508 |
+
We need to map the model key to the checkpoint key.
|
| 509 |
+
"""
|
| 510 |
+
self.checkpoint_to_model_key = {}
|
| 511 |
+
mapping_keys = {"base_layer.": "", "base_model.model.": ""}
|
| 512 |
+
keys_to_update = []
|
| 513 |
+
for k in _state_dict.keys():
|
| 514 |
+
new_key = k
|
| 515 |
+
for from_key, to_key in mapping_keys.items():
|
| 516 |
+
new_key = new_key.replace(from_key, to_key)
|
| 517 |
+
if new_key != k:
|
| 518 |
+
keys_to_update.append((k, new_key))
|
| 519 |
+
self.checkpoint_to_model_key[new_key] = k
|
| 520 |
+
for k, new_key in keys_to_update:
|
| 521 |
+
_state_dict[new_key] = _state_dict.pop(k)
|
| 522 |
+
|
| 523 |
+
return _state_dict
|
| 524 |
+
|
| 525 |
+
def load_state_dict(self, state_dict: Dict[str, Any]) -> None:
|
| 526 |
+
"""Apply the checkpoint-loaded state_dict back to the model.
|
| 527 |
+
|
| 528 |
+
When load_ema_to_reg=True, remap the loaded EMA weights to regular model keys:
|
| 529 |
+
|
| 530 |
+
Case A - ema.enabled=False:
|
| 531 |
+
state_dict has net_ema.* (loaded from checkpoint) -> rename back to net.* for the model.
|
| 532 |
+
|
| 533 |
+
Case B - ema.enabled=True:
|
| 534 |
+
state_dict has net_ema.* (loaded from checkpoint) -> clone each net_ema.* as net.*
|
| 535 |
+
so the regular model gets the same EMA weights. net_ema.* is also kept.
|
| 536 |
+
"""
|
| 537 |
+
if hasattr(self.model[0].config, "lora_config") and self.model[0].config.lora_config.enabled:
|
| 538 |
+
if hasattr(self, "checkpoint_to_model_key"):
|
| 539 |
+
for checkpoint_key, model_key in self.checkpoint_to_model_key.items():
|
| 540 |
+
state_dict[model_key] = state_dict.pop(checkpoint_key)
|
| 541 |
+
else:
|
| 542 |
+
raise ValueError("checkpoint_to_model_key is not set by `state_dict`")
|
| 543 |
+
|
| 544 |
+
if self.load_ema_to_reg:
|
| 545 |
+
if not self.model[0].config.ema.enabled:
|
| 546 |
+
# Case A: rename net_ema.* back to net.* so set_model_state_dict can apply them.
|
| 547 |
+
# Auxiliary keys (aux_losses.*, teacher_align_loss.*, etc.) are passed through as-is.
|
| 548 |
+
all_keys = list(state_dict.keys())
|
| 549 |
+
for k in all_keys:
|
| 550 |
+
if k.startswith("net_ema."):
|
| 551 |
+
state_dict[k.replace("net_ema.", "net.", 1)] = state_dict.pop(k)
|
| 552 |
+
else:
|
| 553 |
+
# Case B: clone net_ema.* -> net.* so regular model gets EMA weights too.
|
| 554 |
+
# Auxiliary keys are kept unchanged.
|
| 555 |
+
log.warning("EMA is enabled, will load EMA weights to regular model weights")
|
| 556 |
+
all_keys = list(state_dict.keys())
|
| 557 |
+
for k in all_keys:
|
| 558 |
+
if k.startswith("net_ema."):
|
| 559 |
+
state_dict[k.replace("net_ema.", "net.", 1)] = torch.clone(state_dict[k])
|
| 560 |
+
func = functools.partial(
|
| 561 |
+
set_model_state_dict,
|
| 562 |
+
model_state_dict=state_dict,
|
| 563 |
+
options=StateDictOptions(strict=False),
|
| 564 |
+
)
|
| 565 |
+
list(map(func, self.model))
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
class OptimizerWrapper(Stateful):
|
| 569 |
+
"""
|
| 570 |
+
Wrapper for optimizer state dict handling with DCP (Distributed Checkpoint).
|
| 571 |
+
|
| 572 |
+
Note: For multi-optimizer models (e.g., distillation), use DistillationCheckpointer
|
| 573 |
+
from pid._src.checkpointer.dcp_distill which has fallback handling for
|
| 574 |
+
FSDP2 parameter ID mismatches.
|
| 575 |
+
"""
|
| 576 |
+
|
| 577 |
+
def __init__(
|
| 578 |
+
self,
|
| 579 |
+
model: Union[nn.Module, List[nn.Module]],
|
| 580 |
+
optim: Union[torch.optim.Optimizer, List[torch.optim.Optimizer]],
|
| 581 |
+
) -> None:
|
| 582 |
+
self.model = [model] if isinstance(model, nn.Module) else model
|
| 583 |
+
self.optim = [optim] if isinstance(optim, torch.optim.Optimizer) else optim
|
| 584 |
+
|
| 585 |
+
def state_dict(self) -> Dict[str, Any]:
|
| 586 |
+
func = functools.partial(
|
| 587 |
+
get_optimizer_state_dict,
|
| 588 |
+
options=StateDictOptions(flatten_optimizer_state_dict=True),
|
| 589 |
+
)
|
| 590 |
+
return {k: v for sd in map(func, self.model, self.optim) for k, v in sd.items()}
|
| 591 |
+
|
| 592 |
+
def load_state_dict(self, state_dict: Dict[str, Any]) -> None:
|
| 593 |
+
func = functools.partial(
|
| 594 |
+
set_optimizer_state_dict,
|
| 595 |
+
optim_state_dict=state_dict,
|
| 596 |
+
options=StateDictOptions(flatten_optimizer_state_dict=True),
|
| 597 |
+
)
|
| 598 |
+
list(map(func, self.model, self.optim))
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
class AsyncMode(str, enum.Enum):
|
| 602 |
+
DISABLED = "disabled"
|
| 603 |
+
ASYNC_WITH_PINNED_MEM = "async_with_pinned_mem"
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
class Terminate:
|
| 607 |
+
pass
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
class SaveDone:
|
| 611 |
+
def __init__(self, iteration: int, elapsed_time: float, succeeded: bool):
|
| 612 |
+
self.iteration = iteration
|
| 613 |
+
self.elapsed_time = elapsed_time
|
| 614 |
+
self.succeeded = succeeded
|
| 615 |
+
|
| 616 |
+
def __str__(self):
|
| 617 |
+
return f"SaveDone(iteration={self.iteration}, elapsed_time={self.elapsed_time}, succeeded={self.succeeded})"
|
| 618 |
+
|
| 619 |
+
|
| 620 |
+
def save_checkpoint_in_background(
|
| 621 |
+
receiver_queue: multiprocessing.Queue,
|
| 622 |
+
sender_queue: multiprocessing.Queue,
|
| 623 |
+
checkpoint_config: CheckpointConfig,
|
| 624 |
+
job_config: JobConfig,
|
| 625 |
+
) -> None:
|
| 626 |
+
"""
|
| 627 |
+
Handles model checkpoint saving in a separate background process using PyTorch's distributed functionality.
|
| 628 |
+
This function runs in a dedicated process to avoid blocking the main training loop.
|
| 629 |
+
|
| 630 |
+
Args:
|
| 631 |
+
receiver_queue: Queue to receive state dictionaries and commands from the main process
|
| 632 |
+
sender_queue: Queue to send completion signals back to the main process
|
| 633 |
+
checkpoint_config: Configuration settings for checkpoint saving behavior
|
| 634 |
+
job_config: Configuration settings for the training job
|
| 635 |
+
|
| 636 |
+
Flow:
|
| 637 |
+
1. Initializes distributed processing environment
|
| 638 |
+
2. Continuously waits for state dictionaries to save
|
| 639 |
+
3. Saves checkpoints asynchronously
|
| 640 |
+
4. Signals completion back to main process
|
| 641 |
+
5. Terminates when receiving a Terminate signal
|
| 642 |
+
|
| 643 |
+
Raises:
|
| 644 |
+
AssertionError: If received object is neither Terminate signal nor valid state dict tuple
|
| 645 |
+
|
| 646 |
+
Note:
|
| 647 |
+
- Uses a different port than the main process to avoid conflicts
|
| 648 |
+
- Disables TorchElastic agent store for checkpoint operations
|
| 649 |
+
- Automatically cleans up distributed process group on exit
|
| 650 |
+
"""
|
| 651 |
+
# Configure distributed environment
|
| 652 |
+
os.environ["MASTER_PORT"] = str(int(os.environ["MASTER_PORT"]) + 2)
|
| 653 |
+
os.environ["TORCHELASTIC_USE_AGENT_STORE"] = "False"
|
| 654 |
+
|
| 655 |
+
# Set up GPU device and distributed processing
|
| 656 |
+
torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))
|
| 657 |
+
distributed.init()
|
| 658 |
+
|
| 659 |
+
# Initialize checkpointing mechanism
|
| 660 |
+
checkpoint_handler = DistributedCheckpointer(checkpoint_config, job_config, None, disable_async=True)
|
| 661 |
+
|
| 662 |
+
try:
|
| 663 |
+
while True:
|
| 664 |
+
log.debug("Checkpoint background process is ready for next task, waiting for new state_dict")
|
| 665 |
+
received_data = receiver_queue.get()
|
| 666 |
+
log.debug("Received new state_dict")
|
| 667 |
+
|
| 668 |
+
if isinstance(received_data, Terminate):
|
| 669 |
+
log.info("Received termination signal in checkpoint background process, closing sender queue")
|
| 670 |
+
sender_queue.put(Terminate())
|
| 671 |
+
sender_queue.close()
|
| 672 |
+
return
|
| 673 |
+
|
| 674 |
+
assert isinstance(received_data, tuple), "Received data must be a tuple of (state_dict, checkpoint_path)"
|
| 675 |
+
state_dict, checkpoint_path = received_data
|
| 676 |
+
|
| 677 |
+
# Save checkpoint and measure time taken
|
| 678 |
+
start_time = time.monotonic()
|
| 679 |
+
iteration = state_dict["trainer"][0]["iteration"]
|
| 680 |
+
elapsed_time = 0
|
| 681 |
+
succeeded = False
|
| 682 |
+
try:
|
| 683 |
+
checkpoint_handler.save_state_dict_worker(state_dict, checkpoint_path)
|
| 684 |
+
elapsed_time = time.monotonic() - start_time
|
| 685 |
+
log.info(
|
| 686 |
+
f"Checkpoint saved successfully in background process. Time taken: {elapsed_time:.2f} seconds, iteration: {iteration}"
|
| 687 |
+
)
|
| 688 |
+
succeeded = True
|
| 689 |
+
except Exception as e:
|
| 690 |
+
log.error(f"Error saving checkpoint to {checkpoint_path}: {e}")
|
| 691 |
+
# continue because if the thread exits, the main thread keeps on adding to the queue
|
| 692 |
+
finally:
|
| 693 |
+
if elapsed_time == 0:
|
| 694 |
+
elapsed_time = time.monotonic() - start_time
|
| 695 |
+
sender_queue.put(SaveDone(iteration, elapsed_time, succeeded))
|
| 696 |
+
|
| 697 |
+
finally:
|
| 698 |
+
log.info("Cleaning up: destroying distributed process group")
|
| 699 |
+
torch.distributed.destroy_process_group()
|
| 700 |
+
|
| 701 |
+
|
| 702 |
+
class DistributedCheckpointer(AbstractCheckpointer):
|
| 703 |
+
KEYS_TO_SAVE = ["model", "optim", "scheduler", "trainer"]
|
| 704 |
+
|
| 705 |
+
def __init__(
|
| 706 |
+
self,
|
| 707 |
+
config_checkpoint: CheckpointConfig,
|
| 708 |
+
config_job: JobConfig,
|
| 709 |
+
callbacks: Optional[callback.CallBackGroup] = None,
|
| 710 |
+
disable_async: bool = False,
|
| 711 |
+
):
|
| 712 |
+
super().__init__(config_checkpoint, config_job, callbacks)
|
| 713 |
+
self.config_checkpoint = config_checkpoint
|
| 714 |
+
if config_checkpoint.dcp_async_mode_enabled:
|
| 715 |
+
self.async_mode = AsyncMode.ASYNC_WITH_PINNED_MEM
|
| 716 |
+
else:
|
| 717 |
+
self.async_mode = AsyncMode.DISABLED
|
| 718 |
+
|
| 719 |
+
if disable_async:
|
| 720 |
+
self.async_mode = AsyncMode.DISABLED
|
| 721 |
+
|
| 722 |
+
if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM:
|
| 723 |
+
ctx = get_context("spawn")
|
| 724 |
+
self.mp_queue_send = ctx.Queue()
|
| 725 |
+
self.mp_queue_recv = ctx.Queue()
|
| 726 |
+
self.mp = ctx.Process(
|
| 727 |
+
target=save_checkpoint_in_background,
|
| 728 |
+
args=(
|
| 729 |
+
self.mp_queue_send,
|
| 730 |
+
self.mp_queue_recv,
|
| 731 |
+
config_checkpoint,
|
| 732 |
+
config_job,
|
| 733 |
+
),
|
| 734 |
+
daemon=True,
|
| 735 |
+
)
|
| 736 |
+
self.mp.start()
|
| 737 |
+
self.cpu_offload_state_dict = None
|
| 738 |
+
self.staging = False
|
| 739 |
+
self.staging_ckpt_file = None
|
| 740 |
+
self.staging_stream = torch.cuda.Stream()
|
| 741 |
+
|
| 742 |
+
def keys_to_resume_during_load(self) -> Tuple[Set, Union[str, None]]:
|
| 743 |
+
latest_checkpoint_file = self._read_latest_checkpoint_file()
|
| 744 |
+
|
| 745 |
+
resume_keys = []
|
| 746 |
+
|
| 747 |
+
if latest_checkpoint_file is not None:
|
| 748 |
+
# 1. Resume training from latest_checkpoint.txt under the same name.
|
| 749 |
+
checkpoint_path = os.path.join(self.load_dirname, latest_checkpoint_file)
|
| 750 |
+
resume_keys.extend(self.KEYS_TO_SAVE)
|
| 751 |
+
else:
|
| 752 |
+
if self.load_path and not self.load_path.endswith(".pth"):
|
| 753 |
+
# 2. Load the module weights specified by config_checkpoint.path.
|
| 754 |
+
checkpoint_path = self.load_path
|
| 755 |
+
if self.load_s3_backend_key:
|
| 756 |
+
checkpoint_path = f"s3://{self.config_checkpoint.load_from_object_store.bucket}/{checkpoint_path}"
|
| 757 |
+
if not re.search(r"/checkpoints/iter_\d{9}/?$", checkpoint_path):
|
| 758 |
+
old_ckpt_path = checkpoint_path
|
| 759 |
+
# If path doesn't end with specific checkpoint, read latest checkpoint file
|
| 760 |
+
latest_ckpt_path = os.path.join(checkpoint_path, "checkpoints/latest_checkpoint.txt")
|
| 761 |
+
if easy_io.exists(latest_ckpt_path, backend_key=self.load_s3_backend_key):
|
| 762 |
+
checkpoint_file = easy_io.load(
|
| 763 |
+
latest_ckpt_path, backend_key=self.load_s3_backend_key
|
| 764 |
+
).strip()
|
| 765 |
+
checkpoint_path = f"{checkpoint_path}/checkpoints/{checkpoint_file}"
|
| 766 |
+
else:
|
| 767 |
+
log.warning(
|
| 768 |
+
f"Latest checkpoint file {latest_ckpt_path} not found, load from {old_ckpt_path}"
|
| 769 |
+
)
|
| 770 |
+
checkpoint_path = old_ckpt_path
|
| 771 |
+
if self.load_training_state:
|
| 772 |
+
resume_keys.extend(self.KEYS_TO_SAVE)
|
| 773 |
+
else:
|
| 774 |
+
resume_keys.append("model")
|
| 775 |
+
if self.only_load_scheduler_state:
|
| 776 |
+
resume_keys.append("scheduler")
|
| 777 |
+
elif self.load_path and self.load_path.endswith(".pth"):
|
| 778 |
+
checkpoint_path = self.load_path
|
| 779 |
+
else:
|
| 780 |
+
checkpoint_path = None
|
| 781 |
+
if len(self.keys_not_to_resume) > 0:
|
| 782 |
+
for key in self.keys_not_to_resume:
|
| 783 |
+
assert key in self.KEYS_TO_SAVE, f"Invalid key to resume: {key} not in {self.KEYS_TO_SAVE}"
|
| 784 |
+
resume_keys = [key for key in resume_keys if key not in self.keys_not_to_resume]
|
| 785 |
+
is_self_resume = latest_checkpoint_file is not None
|
| 786 |
+
return set(resume_keys), checkpoint_path, is_self_resume
|
| 787 |
+
|
| 788 |
+
@misc.timer("checkpoint loading")
|
| 789 |
+
def load(
|
| 790 |
+
self,
|
| 791 |
+
model: ImaginaireModel,
|
| 792 |
+
optimizer: torch.optim.Optimizer | None = None,
|
| 793 |
+
scheduler: torch.optim.lr_scheduler.LRScheduler | None = None,
|
| 794 |
+
grad_scaler: torch.amp.GradScaler | None = None,
|
| 795 |
+
) -> int:
|
| 796 |
+
if self.callbacks is not None:
|
| 797 |
+
self.callbacks.on_load_checkpoint_start(model)
|
| 798 |
+
|
| 799 |
+
resume_keys, checkpoint_path, is_self_resume = self.keys_to_resume_during_load()
|
| 800 |
+
resume_keys = sorted(resume_keys)
|
| 801 |
+
log.info(f"Resuming ckpt {checkpoint_path} with keys: {resume_keys}")
|
| 802 |
+
|
| 803 |
+
iteration = 0
|
| 804 |
+
|
| 805 |
+
if checkpoint_path is not None and not checkpoint_path.endswith(".pth"):
|
| 806 |
+
self._check_checkpoint_exists(checkpoint_path)
|
| 807 |
+
for key in resume_keys:
|
| 808 |
+
load_planner = DefaultLoadPlanner(allow_partial_load=True)
|
| 809 |
+
cur_key_ckpt_full_path = os.path.join(checkpoint_path, key)
|
| 810 |
+
log.info(f"Start loading checkpoint from {checkpoint_path}")
|
| 811 |
+
storage_reader = self.get_storage_reader(cur_key_ckpt_full_path)
|
| 812 |
+
torch.distributed.barrier()
|
| 813 |
+
log.info(f"starting {cur_key_ckpt_full_path}", rank0_only=False)
|
| 814 |
+
if key == "model":
|
| 815 |
+
log.info("- Loading the model...")
|
| 816 |
+
should_replicate_ema = (
|
| 817 |
+
self.config_checkpoint.replicate_ema_to_reg_in_training
|
| 818 |
+
and not is_self_resume
|
| 819 |
+
and not self.load_training_state
|
| 820 |
+
)
|
| 821 |
+
if should_replicate_ema:
|
| 822 |
+
ckpt_metadata = storage_reader.read_metadata()
|
| 823 |
+
ckpt_keys = set(ckpt_metadata.state_dict_metadata.keys())
|
| 824 |
+
if not any(k.startswith("net_ema.") for k in ckpt_keys):
|
| 825 |
+
raise ValueError(
|
| 826 |
+
f"replicate_ema_to_reg_in_training is True, but checkpoint at "
|
| 827 |
+
f"{cur_key_ckpt_full_path} does not contain EMA weights (no 'net_ema.*' keys). "
|
| 828 |
+
f"Set replicate_ema_to_reg_in_training=False or use a checkpoint with EMA weights."
|
| 829 |
+
)
|
| 830 |
+
_model_wrapper = ModelWrapper(model, load_ema_to_reg=should_replicate_ema)
|
| 831 |
+
_state_dict = _model_wrapper.state_dict()
|
| 832 |
+
load(_state_dict, storage_reader=storage_reader, planner=load_planner)
|
| 833 |
+
_model_wrapper.load_state_dict(_state_dict)
|
| 834 |
+
elif key == "optim":
|
| 835 |
+
log.info("- Loading the optimizer...")
|
| 836 |
+
# Note: For multi-optimizer models (e.g., distillation), use DistillationCheckpointer
|
| 837 |
+
# from pid._src.checkpointer.dcp_distill instead.
|
| 838 |
+
_optim_wrapper = OptimizerWrapper(model, optimizer)
|
| 839 |
+
_state_dict = _optim_wrapper.state_dict()
|
| 840 |
+
dcp.load(
|
| 841 |
+
_state_dict,
|
| 842 |
+
storage_reader=storage_reader,
|
| 843 |
+
planner=load_planner,
|
| 844 |
+
)
|
| 845 |
+
_optim_wrapper.load_state_dict(_state_dict)
|
| 846 |
+
elif key == "scheduler":
|
| 847 |
+
log.info("- Loading the scheduler...")
|
| 848 |
+
# Note: For multi-scheduler models (e.g., distillation), use DistillationCheckpointer
|
| 849 |
+
# from pid._src.checkpointer.dcp_distill instead.
|
| 850 |
+
_state_dict = scheduler.state_dict()
|
| 851 |
+
dcp.load(
|
| 852 |
+
_state_dict,
|
| 853 |
+
storage_reader=storage_reader,
|
| 854 |
+
planner=load_planner,
|
| 855 |
+
)
|
| 856 |
+
scheduler.load_state_dict(_state_dict)
|
| 857 |
+
elif key == "trainer":
|
| 858 |
+
log.info("- Loading the trainer...")
|
| 859 |
+
_state_dict = {
|
| 860 |
+
"grad_scaler": grad_scaler.state_dict(),
|
| 861 |
+
"iteration": iteration,
|
| 862 |
+
}
|
| 863 |
+
dcp.load(
|
| 864 |
+
_state_dict,
|
| 865 |
+
storage_reader=storage_reader,
|
| 866 |
+
planner=load_planner,
|
| 867 |
+
)
|
| 868 |
+
grad_scaler.load_state_dict(_state_dict["grad_scaler"])
|
| 869 |
+
iteration = _state_dict["iteration"]
|
| 870 |
+
else:
|
| 871 |
+
raise ValueError(f"Invalid key: {key}. not support to resume.")
|
| 872 |
+
if self.callbacks is not None:
|
| 873 |
+
self.callbacks.on_load_checkpoint(model, state_dict=_state_dict)
|
| 874 |
+
log.info(f"Loaded checkpoint from {checkpoint_path} in iteration {iteration}")
|
| 875 |
+
elif checkpoint_path is not None and checkpoint_path.endswith(".pth"):
|
| 876 |
+
state = easy_io.load(checkpoint_path)
|
| 877 |
+
model_state = model.net.state_dict()
|
| 878 |
+
|
| 879 |
+
for k, v in list(state.items()):
|
| 880 |
+
tgt = model_state.get(k, None)
|
| 881 |
+
if tgt is None:
|
| 882 |
+
continue
|
| 883 |
+
# If target param/buffer is a DTensor and checkpoint value is not, distribute it
|
| 884 |
+
if isinstance(tgt, DTensor) and not isinstance(v, DTensor):
|
| 885 |
+
# Match device, dtype, and placements from the target DTensor
|
| 886 |
+
v = v.to(tgt.device, dtype=tgt.dtype, copy=False)
|
| 887 |
+
v = distribute_tensor(v, tgt.device_mesh, tgt.placements)
|
| 888 |
+
state[k] = v
|
| 889 |
+
# If target is a plain Tensor but checkpoint is a DTensor, bring it local
|
| 890 |
+
if not isinstance(tgt, DTensor) and isinstance(v, DTensor):
|
| 891 |
+
state[k] = v.to_local().to(tgt.device, dtype=tgt.dtype, copy=False)
|
| 892 |
+
|
| 893 |
+
model.load_state_dict(state, strict=False, pretrain_copy=True)
|
| 894 |
+
# Clear unused reserved memory from fp32
|
| 895 |
+
torch.cuda.empty_cache()
|
| 896 |
+
log.critical(f"Loaded checkpoint from {checkpoint_path}. **** This only happen at iteration 0 **** ")
|
| 897 |
+
else:
|
| 898 |
+
log.info("Training from scratch.")
|
| 899 |
+
torch.cuda.empty_cache()
|
| 900 |
+
|
| 901 |
+
if self.callbacks is not None:
|
| 902 |
+
self.callbacks.on_load_checkpoint_end(model, iteration=iteration, checkpoint_path=checkpoint_path)
|
| 903 |
+
return iteration
|
| 904 |
+
|
| 905 |
+
def _async_with_pinned_memory(self, checkpoint_file: str, state_dict: Dict[str, Tuple[Any, str]]) -> None:
|
| 906 |
+
try:
|
| 907 |
+
from torch.distributed._state_dict_utils import _copy_state_dict, _create_cpu_state_dict
|
| 908 |
+
except ImportError as e:
|
| 909 |
+
raise ImportError(
|
| 910 |
+
"Please install the latest PyTorch nightly to use async checkpointing with pinned memory."
|
| 911 |
+
) from e
|
| 912 |
+
if self.cpu_offload_state_dict is None:
|
| 913 |
+
log.debug(f"Preparing the CPU memory, {time.monotonic()=}.:.2f")
|
| 914 |
+
self.cpu_offload_state_dict = _create_cpu_state_dict(state_dict, pin_memory=True, share_memory=True)
|
| 915 |
+
|
| 916 |
+
log.debug(f"Staging the state_dict, {time.monotonic()=}.:.2f")
|
| 917 |
+
with torch.cuda.stream(self.staging_stream):
|
| 918 |
+
self.cpu_offload_state_dict = _copy_state_dict(
|
| 919 |
+
state_dict,
|
| 920 |
+
self.cpu_offload_state_dict,
|
| 921 |
+
non_blocking=True,
|
| 922 |
+
)
|
| 923 |
+
self.staging = True
|
| 924 |
+
self.staging_ckpt_file = checkpoint_file
|
| 925 |
+
|
| 926 |
+
self.maybe_wait_for_staging()
|
| 927 |
+
|
| 928 |
+
def maybe_wait_for_staging(self) -> None:
|
| 929 |
+
if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM and self.staging:
|
| 930 |
+
if not self.staging_stream.query():
|
| 931 |
+
self.staging_stream.synchronize()
|
| 932 |
+
|
| 933 |
+
def sync_func():
|
| 934 |
+
self.mp_queue_send.put_nowait((self.cpu_offload_state_dict, self.staging_ckpt_file))
|
| 935 |
+
|
| 936 |
+
sync_func()
|
| 937 |
+
self.staging = False
|
| 938 |
+
|
| 939 |
+
def get_previous_checkpoint_results(self, wait_for: int = 0) -> None:
|
| 940 |
+
"""Get the results of previously submitted checkpoints and pass them to callbacks if checkpoint succeeded"""
|
| 941 |
+
if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM:
|
| 942 |
+
try:
|
| 943 |
+
start_time = time.monotonic()
|
| 944 |
+
while not self.mp_queue_recv.empty() or wait_for > 0:
|
| 945 |
+
try:
|
| 946 |
+
ret = self.mp_queue_recv.get(timeout=1)
|
| 947 |
+
if isinstance(ret, Terminate):
|
| 948 |
+
log.info("Received termination event from checkpoint background process")
|
| 949 |
+
break
|
| 950 |
+
save_done: SaveDone = ret
|
| 951 |
+
log.logger.info(f"Received checkpoint save result: {save_done}")
|
| 952 |
+
if self.callbacks is not None and save_done.succeeded:
|
| 953 |
+
self.callbacks.on_save_checkpoint_success(
|
| 954 |
+
iteration=save_done.iteration, elapsed_time=save_done.elapsed_time
|
| 955 |
+
)
|
| 956 |
+
except queue.Empty:
|
| 957 |
+
elapsed_time = time.monotonic() - start_time
|
| 958 |
+
if elapsed_time > wait_for:
|
| 959 |
+
break
|
| 960 |
+
except (EOFError, BrokenPipeError):
|
| 961 |
+
log.info("Queue was closed by checkpoint background process")
|
| 962 |
+
|
| 963 |
+
def get_storage_writer(self, checkpoint_path: str) -> Union[S3StorageWriter, FileSystemWriter]:
|
| 964 |
+
if self.save_to_object_store:
|
| 965 |
+
return S3StorageWriter(
|
| 966 |
+
credential_path=self.config_checkpoint.save_to_object_store.credentials,
|
| 967 |
+
path=checkpoint_path,
|
| 968 |
+
)
|
| 969 |
+
return FileSystemWriter(path=checkpoint_path)
|
| 970 |
+
|
| 971 |
+
def get_storage_reader(self, checkpoint_path: str) -> Union[S3StorageReader, FileSystemReader]:
|
| 972 |
+
if self.load_from_object_store:
|
| 973 |
+
return S3StorageReader(
|
| 974 |
+
credential_path=self.config_checkpoint.load_from_object_store.credentials,
|
| 975 |
+
path=checkpoint_path,
|
| 976 |
+
)
|
| 977 |
+
return FileSystemReader(checkpoint_path)
|
| 978 |
+
|
| 979 |
+
def save_state_dict_worker(self, to_save_dict: Dict[str, Tuple[Any, str]], checkpoint_file: str) -> None:
|
| 980 |
+
for k, (v, full_checkpoint_path) in to_save_dict.items():
|
| 981 |
+
storage_writer = self.get_storage_writer(full_checkpoint_path)
|
| 982 |
+
dcp.save(
|
| 983 |
+
v,
|
| 984 |
+
storage_writer=storage_writer,
|
| 985 |
+
planner=DefaultSavePlanner(dedup_save_to_lowest_rank=True),
|
| 986 |
+
)
|
| 987 |
+
|
| 988 |
+
if distributed.is_rank0():
|
| 989 |
+
print(f"Saving last checkpoint file {checkpoint_file}")
|
| 990 |
+
self._write_latest_checkpoint_file(checkpoint_file)
|
| 991 |
+
|
| 992 |
+
log.critical(f"Saved checkpoint to {os.path.join(self.save_dirname, checkpoint_file)}", rank0_only=True)
|
| 993 |
+
|
| 994 |
+
def save(
|
| 995 |
+
self,
|
| 996 |
+
model: ImaginaireModel,
|
| 997 |
+
optimizer: torch.optim.Optimizer,
|
| 998 |
+
scheduler: torch.optim.lr_scheduler.LRScheduler,
|
| 999 |
+
grad_scaler: torch.amp.GradScaler,
|
| 1000 |
+
iteration: int,
|
| 1001 |
+
) -> None:
|
| 1002 |
+
"""Save network weights, optimizer parameters, scheduler parameters to a checkpoint.
|
| 1003 |
+
|
| 1004 |
+
Note: For multi-optimizer models (e.g., distillation), use DistillationCheckpointer
|
| 1005 |
+
from pid._src.checkpointer.dcp_distill instead.
|
| 1006 |
+
|
| 1007 |
+
Args:
|
| 1008 |
+
model (ImaginaireModel): The PyTorch model.
|
| 1009 |
+
optimizer (torch.optim.Optimizer): The model optimizer.
|
| 1010 |
+
scheduler (torch.optim.lr_scheduler.LRScheduler): The optimization scheduler.
|
| 1011 |
+
grad_scaler (torch.amp.GradScaler): The gradient scaler (for mixed precision training).
|
| 1012 |
+
iteration (int): Current iteration number.
|
| 1013 |
+
"""
|
| 1014 |
+
if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM:
|
| 1015 |
+
self.get_previous_checkpoint_results(wait_for=0)
|
| 1016 |
+
|
| 1017 |
+
if self.callbacks is not None:
|
| 1018 |
+
self.callbacks.on_save_checkpoint_start(model, iteration)
|
| 1019 |
+
|
| 1020 |
+
checkpoint_file = f"iter_{iteration:09}"
|
| 1021 |
+
|
| 1022 |
+
to_save_dict = {
|
| 1023 |
+
"model": ModelWrapper(model).state_dict(),
|
| 1024 |
+
"optim": OptimizerWrapper(model, optimizer).state_dict(),
|
| 1025 |
+
"scheduler": scheduler.state_dict(),
|
| 1026 |
+
"trainer": {
|
| 1027 |
+
"grad_scaler": grad_scaler.state_dict(),
|
| 1028 |
+
"iteration": iteration,
|
| 1029 |
+
},
|
| 1030 |
+
}
|
| 1031 |
+
|
| 1032 |
+
for key in to_save_dict.keys():
|
| 1033 |
+
output_dirname = os.path.join(self.save_dirname, f"iter_{iteration:09}/{key}")
|
| 1034 |
+
to_save_dict[key] = (to_save_dict[key], output_dirname)
|
| 1035 |
+
|
| 1036 |
+
if self.callbacks is not None:
|
| 1037 |
+
self.callbacks.on_save_checkpoint(model, state_dict=to_save_dict)
|
| 1038 |
+
|
| 1039 |
+
if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM:
|
| 1040 |
+
self._async_with_pinned_memory(checkpoint_file, to_save_dict)
|
| 1041 |
+
else:
|
| 1042 |
+
start_time = time.monotonic()
|
| 1043 |
+
try:
|
| 1044 |
+
self.save_state_dict_worker(to_save_dict, checkpoint_file)
|
| 1045 |
+
finally:
|
| 1046 |
+
if self.callbacks is not None:
|
| 1047 |
+
self.callbacks.on_save_checkpoint_success(
|
| 1048 |
+
iteration=iteration, elapsed_time=time.monotonic() - start_time
|
| 1049 |
+
)
|
| 1050 |
+
|
| 1051 |
+
# This measures exposed (synchronous) checkpoint time, on_save_checkpoint_success()
|
| 1052 |
+
# is instead called to measure the entire duration for asynchronous checkpoint for the async case too.
|
| 1053 |
+
if self.callbacks is not None:
|
| 1054 |
+
self.callbacks.on_save_checkpoint_end(model=None, iteration=iteration)
|
| 1055 |
+
|
| 1056 |
+
def finalize(self) -> None:
|
| 1057 |
+
super().finalize()
|
| 1058 |
+
if self.async_mode == AsyncMode.ASYNC_WITH_PINNED_MEM:
|
| 1059 |
+
if self.mp and self.mp.is_alive():
|
| 1060 |
+
self.mp_queue_send.put(Terminate())
|
| 1061 |
+
self.get_previous_checkpoint_results(wait_for=60)
|
| 1062 |
+
self.mp.join()
|
pid/_ext/imaginaire/checkpointer/dummy.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
from typing import Optional
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
import torch.distributed
|
| 20 |
+
|
| 21 |
+
from pid._ext.imaginaire.checkpointer.base import AbstractCheckpointer
|
| 22 |
+
from pid._ext.imaginaire.model import ImaginaireModel
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class Checkpointer(AbstractCheckpointer):
|
| 26 |
+
"""
|
| 27 |
+
A dummy checkpointer that does not save or load anything. This is useful for debugging jobs or share workload with collobrators.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def save(
|
| 31 |
+
self,
|
| 32 |
+
model: ImaginaireModel,
|
| 33 |
+
optimizer: torch.optim.Optimizer,
|
| 34 |
+
scheduler: torch.optim.lr_scheduler.LRScheduler,
|
| 35 |
+
grad_scaler: torch.amp.GradScaler,
|
| 36 |
+
iteration: int,
|
| 37 |
+
) -> None:
|
| 38 |
+
pass
|
| 39 |
+
|
| 40 |
+
def load(
|
| 41 |
+
self,
|
| 42 |
+
model: ImaginaireModel,
|
| 43 |
+
optimizer: Optional[torch.optim.Optimizer] = None,
|
| 44 |
+
scheduler: Optional[torch.optim.lr_scheduler.LRScheduler] = None,
|
| 45 |
+
grad_scaler: Optional[torch.amp.GradScaler] = None,
|
| 46 |
+
) -> int:
|
| 47 |
+
return 0
|
pid/_ext/imaginaire/checkpointer/s3_filesystem.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import io
|
| 17 |
+
import json
|
| 18 |
+
import os
|
| 19 |
+
import time
|
| 20 |
+
from contextlib import contextmanager
|
| 21 |
+
from typing import Generator, Union
|
| 22 |
+
from urllib.parse import urlparse
|
| 23 |
+
|
| 24 |
+
import boto3
|
| 25 |
+
from botocore.config import Config
|
| 26 |
+
from botocore.exceptions import ClientError
|
| 27 |
+
from torch.distributed.checkpoint import FileSystemReader, FileSystemWriter
|
| 28 |
+
from torch.distributed.checkpoint.filesystem import FileSystemBase
|
| 29 |
+
|
| 30 |
+
from pid._ext.imaginaire.utils import log
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class S3Stream(io.BytesIO):
|
| 34 |
+
"""
|
| 35 |
+
Workaround for PyTorch manually closing the stream before we can upload it to S3. We override the close() as noop
|
| 36 |
+
and instead call our own _true_close() method to close the stream after we are done using it.
|
| 37 |
+
The commit at fault is https://github.com/pytorch/pytorch/commit/9c909bf3bb122db2cce95e2eb7459bbe50dfa15a
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
def close(self):
|
| 41 |
+
self.flush()
|
| 42 |
+
# No close
|
| 43 |
+
|
| 44 |
+
def _true_close(self):
|
| 45 |
+
super().close()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class S3FileSystem(FileSystemBase):
|
| 49 |
+
"""Implementation of FileSystemBase for AWS S3 storage."""
|
| 50 |
+
|
| 51 |
+
def __init__(
|
| 52 |
+
self,
|
| 53 |
+
credential_path: str,
|
| 54 |
+
max_attempts: int = 20,
|
| 55 |
+
initial_backoff: float = 1.0,
|
| 56 |
+
max_backoff: float = 30.0,
|
| 57 |
+
backoff_factor: float = 2.0,
|
| 58 |
+
) -> None:
|
| 59 |
+
"""
|
| 60 |
+
Initialize S3FileSystem with retry configuration.
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
credential_path: Path to AWS credentials JSON file
|
| 64 |
+
max_attempts: Maximum number of retry attempts
|
| 65 |
+
initial_backoff: Initial backoff time in seconds
|
| 66 |
+
max_backoff: Maximum backoff time in seconds
|
| 67 |
+
backoff_factor: Multiplicative factor for backoff time
|
| 68 |
+
"""
|
| 69 |
+
with open(credential_path, "r") as f:
|
| 70 |
+
conf = json.load(f)
|
| 71 |
+
|
| 72 |
+
# Configure boto3 with retry settings
|
| 73 |
+
config = Config(
|
| 74 |
+
retries=dict(max_attempts=max_attempts, mode="adaptive"), # Adaptive mode automatically handles throttling
|
| 75 |
+
connect_timeout=60,
|
| 76 |
+
read_timeout=60,
|
| 77 |
+
request_checksum_calculation="when_required", # Data integrity check for uploads and downloads
|
| 78 |
+
response_checksum_validation="when_required", # Data integrity check for uploads and downloads
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
self.s3_client = boto3.client("s3", config=config, **conf)
|
| 82 |
+
self.max_attempts = max_attempts
|
| 83 |
+
self.initial_backoff = initial_backoff
|
| 84 |
+
self.max_backoff = max_backoff
|
| 85 |
+
self.backoff_factor = backoff_factor
|
| 86 |
+
|
| 87 |
+
def _retry_with_backoff(self, operation_func, *args, **kwargs):
|
| 88 |
+
"""
|
| 89 |
+
Execute an operation with exponential backoff retry logic.
|
| 90 |
+
|
| 91 |
+
Args:
|
| 92 |
+
operation_func: Function to execute
|
| 93 |
+
*args: Positional arguments for the function
|
| 94 |
+
**kwargs: Keyword arguments for the function
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Result of the operation function
|
| 98 |
+
|
| 99 |
+
Raises:
|
| 100 |
+
Exception: If all retry attempts fail
|
| 101 |
+
"""
|
| 102 |
+
last_exception = None
|
| 103 |
+
backoff = self.initial_backoff
|
| 104 |
+
|
| 105 |
+
for attempt in range(self.max_attempts):
|
| 106 |
+
try:
|
| 107 |
+
return operation_func(*args, **kwargs)
|
| 108 |
+
except ClientError as e:
|
| 109 |
+
error_code = e.response.get("Error", {}).get("Code", "")
|
| 110 |
+
log.info(f"S3 Filesystem: Received ClientError: {error_code}", rank0_only=False)
|
| 111 |
+
|
| 112 |
+
# Handle specific error cases
|
| 113 |
+
if error_code in ["SlowDown", "ThrottlingException", "RequestLimitExceeded", "InternalError"]:
|
| 114 |
+
last_exception = e
|
| 115 |
+
if attempt < self.max_attempts - 1: # Don't sleep on last attempt
|
| 116 |
+
current_backoff = min(backoff, self.max_backoff)
|
| 117 |
+
log.info(f"S3 Filesystem: Retrying in {current_backoff} seconds", rank0_only=False)
|
| 118 |
+
time.sleep(current_backoff)
|
| 119 |
+
backoff *= self.backoff_factor
|
| 120 |
+
continue
|
| 121 |
+
# For other client errors, raise immediately
|
| 122 |
+
raise
|
| 123 |
+
except Exception as e:
|
| 124 |
+
log.info(f"S3 Filesystem: Received Exception: {str(e)}", rank0_only=False)
|
| 125 |
+
last_exception = e
|
| 126 |
+
if attempt < self.max_attempts - 1:
|
| 127 |
+
current_backoff = min(backoff, self.max_backoff)
|
| 128 |
+
log.info(f"S3 Filesystem: Retrying in {current_backoff} seconds", rank0_only=False)
|
| 129 |
+
time.sleep(current_backoff)
|
| 130 |
+
backoff *= self.backoff_factor
|
| 131 |
+
continue
|
| 132 |
+
|
| 133 |
+
raise last_exception
|
| 134 |
+
|
| 135 |
+
@contextmanager
|
| 136 |
+
def create_stream(self, path: Union[str, os.PathLike], mode: str) -> Generator[io.IOBase, None, None]:
|
| 137 |
+
"""Create a stream for reading from or writing to S3 with retry logic."""
|
| 138 |
+
path_str = str(path)
|
| 139 |
+
bucket, key = self._parse_s3_uri(path_str)
|
| 140 |
+
log.info(f"S3 Filesystem: Creating stream for {key} in bucket {bucket}", rank0_only=False)
|
| 141 |
+
|
| 142 |
+
if mode == "rb":
|
| 143 |
+
stream = io.BytesIO()
|
| 144 |
+
try:
|
| 145 |
+
|
| 146 |
+
def download_operation():
|
| 147 |
+
self.s3_client.download_fileobj(bucket, key, stream)
|
| 148 |
+
stream.seek(0)
|
| 149 |
+
|
| 150 |
+
log.info(f"S3 Filesystem: Downloading {key} from bucket {bucket}", rank0_only=False)
|
| 151 |
+
self._retry_with_backoff(download_operation)
|
| 152 |
+
log.info("S3 Filesystem: Download complete", rank0_only=False)
|
| 153 |
+
yield stream
|
| 154 |
+
finally:
|
| 155 |
+
stream.close()
|
| 156 |
+
elif mode == "wb":
|
| 157 |
+
stream = S3Stream()
|
| 158 |
+
try:
|
| 159 |
+
yield stream
|
| 160 |
+
|
| 161 |
+
def upload_operation():
|
| 162 |
+
stream.seek(0)
|
| 163 |
+
self.s3_client.upload_fileobj(stream, bucket, key)
|
| 164 |
+
|
| 165 |
+
log.info(f"S3 Filesystem: Uploading {key} to bucket {bucket}", rank0_only=False)
|
| 166 |
+
self._retry_with_backoff(upload_operation)
|
| 167 |
+
log.info("S3 Filesystem: Upload complete", rank0_only=False)
|
| 168 |
+
finally:
|
| 169 |
+
stream._true_close()
|
| 170 |
+
else:
|
| 171 |
+
raise ValueError(f"Unsupported mode: {mode}")
|
| 172 |
+
|
| 173 |
+
def concat_path(self, path: Union[str, os.PathLike], suffix: str) -> Union[str, os.PathLike]:
|
| 174 |
+
"""Concatenate S3 path with suffix."""
|
| 175 |
+
path_str = str(path)
|
| 176 |
+
if path_str.endswith("/"):
|
| 177 |
+
return f"{path_str}{suffix}"
|
| 178 |
+
return f"{path_str}/{suffix}"
|
| 179 |
+
|
| 180 |
+
def init_path(self, path: Union[str, os.PathLike]) -> Union[str, os.PathLike]:
|
| 181 |
+
"""Initialize and validate S3 path."""
|
| 182 |
+
path_str = str(path)
|
| 183 |
+
if not path_str.startswith("s3://"):
|
| 184 |
+
raise ValueError(f"Invalid S3 URI: {path_str}. Must start with 's3://'")
|
| 185 |
+
return path_str
|
| 186 |
+
|
| 187 |
+
def rename(self, path: Union[str, os.PathLike], new_path: Union[str, os.PathLike]) -> None:
|
| 188 |
+
"""Rename (move) an object in S3 with retry logic."""
|
| 189 |
+
src_bucket, src_key = self._parse_s3_uri(str(path))
|
| 190 |
+
dst_bucket, dst_key = self._parse_s3_uri(str(new_path))
|
| 191 |
+
|
| 192 |
+
def copy_operation():
|
| 193 |
+
copy_source = {"Bucket": src_bucket, "Key": src_key}
|
| 194 |
+
self.s3_client.copy(copy_source, dst_bucket, dst_key)
|
| 195 |
+
|
| 196 |
+
self._retry_with_backoff(copy_operation)
|
| 197 |
+
|
| 198 |
+
def delete_operation():
|
| 199 |
+
self.s3_client.delete_object(Bucket=src_bucket, Key=src_key)
|
| 200 |
+
|
| 201 |
+
self._retry_with_backoff(delete_operation)
|
| 202 |
+
|
| 203 |
+
def mkdir(self, path: Union[str, os.PathLike]) -> None:
|
| 204 |
+
"""
|
| 205 |
+
Create a "directory" in S3.
|
| 206 |
+
|
| 207 |
+
Note: S3 doesn't have real directories, but we can create an empty object
|
| 208 |
+
with a trailing slash to simulate a directory.
|
| 209 |
+
"""
|
| 210 |
+
# Creating same buckets from different ranks can cause rate limit issues in GCP.
|
| 211 |
+
# In object store, we don't need to create a directory.
|
| 212 |
+
pass
|
| 213 |
+
|
| 214 |
+
@classmethod
|
| 215 |
+
def validate_checkpoint_id(cls, checkpoint_id: Union[str, os.PathLike]) -> bool:
|
| 216 |
+
"""Validate if the checkpoint_id is a valid S3 URI."""
|
| 217 |
+
checkpoint_id_str = str(checkpoint_id)
|
| 218 |
+
try:
|
| 219 |
+
if not checkpoint_id_str.startswith("s3://"):
|
| 220 |
+
return False
|
| 221 |
+
parsed = urlparse(checkpoint_id_str)
|
| 222 |
+
return bool(parsed.netloc and parsed.path) # Must have bucket and key
|
| 223 |
+
except Exception:
|
| 224 |
+
return False
|
| 225 |
+
|
| 226 |
+
def exists(self, path: Union[str, os.PathLike]) -> bool:
|
| 227 |
+
"""Check if an object exists in S3 with retry logic."""
|
| 228 |
+
bucket, key = self._parse_s3_uri(str(path))
|
| 229 |
+
try:
|
| 230 |
+
|
| 231 |
+
def head_operation():
|
| 232 |
+
self.s3_client.head_object(Bucket=bucket, Key=key)
|
| 233 |
+
|
| 234 |
+
self._retry_with_backoff(head_operation)
|
| 235 |
+
return True
|
| 236 |
+
except ClientError as e:
|
| 237 |
+
if e.response.get("Error", {}).get("Code", "") == "404":
|
| 238 |
+
return False
|
| 239 |
+
raise
|
| 240 |
+
|
| 241 |
+
def rm_file(self, path: Union[str, os.PathLike]) -> None:
|
| 242 |
+
"""Remove a file from S3 with retry logic."""
|
| 243 |
+
bucket, key = self._parse_s3_uri(str(path))
|
| 244 |
+
|
| 245 |
+
def delete_operation():
|
| 246 |
+
self.s3_client.delete_object(Bucket=bucket, Key=key)
|
| 247 |
+
|
| 248 |
+
self._retry_with_backoff(delete_operation)
|
| 249 |
+
|
| 250 |
+
def _parse_s3_uri(self, uri: str) -> tuple[str, str]:
|
| 251 |
+
"""
|
| 252 |
+
Parse an S3 URI into bucket and key.
|
| 253 |
+
|
| 254 |
+
Args:
|
| 255 |
+
uri: S3 URI in the format s3://bucket-name/key
|
| 256 |
+
|
| 257 |
+
Returns:
|
| 258 |
+
Tuple of (bucket_name, key)
|
| 259 |
+
|
| 260 |
+
Raises:
|
| 261 |
+
ValueError: If the URI is invalid
|
| 262 |
+
"""
|
| 263 |
+
uri = uri if isinstance(uri, str) else str(uri)
|
| 264 |
+
if not uri.startswith("s3://"):
|
| 265 |
+
raise ValueError(f"Invalid S3 URI: {uri}. Must start with 's3://'")
|
| 266 |
+
|
| 267 |
+
parsed = urlparse(uri)
|
| 268 |
+
bucket = parsed.netloc
|
| 269 |
+
|
| 270 |
+
# Remove leading slash from key
|
| 271 |
+
key = parsed.path.lstrip("/")
|
| 272 |
+
|
| 273 |
+
if not bucket:
|
| 274 |
+
raise ValueError(f"Invalid S3 URI: {uri}. No bucket specified")
|
| 275 |
+
|
| 276 |
+
return bucket, key
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
class S3StorageWriter(FileSystemWriter):
|
| 280 |
+
def __init__(
|
| 281 |
+
self,
|
| 282 |
+
credential_path: str,
|
| 283 |
+
path: str,
|
| 284 |
+
**kwargs,
|
| 285 |
+
) -> None:
|
| 286 |
+
"""
|
| 287 |
+
Initialize an S3 writer for distributed checkpointing.
|
| 288 |
+
|
| 289 |
+
Args:
|
| 290 |
+
region (str): The AWS region for S3.
|
| 291 |
+
path (str): The S3 URI to write checkpoints to.
|
| 292 |
+
kwargs (dict): Keyword arguments to pass to the parent :class:`FileSystemWriter`.
|
| 293 |
+
"""
|
| 294 |
+
super().__init__(
|
| 295 |
+
path=path,
|
| 296 |
+
sync_files=False,
|
| 297 |
+
**kwargs,
|
| 298 |
+
)
|
| 299 |
+
self.fs = S3FileSystem(credential_path) # type: ignore
|
| 300 |
+
self.path = self.fs.init_path(path)
|
| 301 |
+
|
| 302 |
+
@classmethod
|
| 303 |
+
def validate_checkpoint_id(cls, checkpoint_id: Union[str, os.PathLike]) -> bool:
|
| 304 |
+
return S3FileSystem.validate_checkpoint_id(checkpoint_id)
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
class S3StorageReader(FileSystemReader):
|
| 308 |
+
def __init__(self, credential_path: str, path: Union[str, os.PathLike]) -> None:
|
| 309 |
+
"""
|
| 310 |
+
Initialize an S3 reader for distributed checkpointing.
|
| 311 |
+
|
| 312 |
+
Args:
|
| 313 |
+
region (str): The AWS region for S3.
|
| 314 |
+
path (Union[str, os.PathLike]): The S3 path to read checkpoints from.
|
| 315 |
+
"""
|
| 316 |
+
super().__init__(path)
|
| 317 |
+
self.fs = S3FileSystem(credential_path) # type: ignore
|
| 318 |
+
self.path = self.fs.init_path(path)
|
| 319 |
+
self.sync_files = False
|
| 320 |
+
|
| 321 |
+
@classmethod
|
| 322 |
+
def validate_checkpoint_id(cls, checkpoint_id: Union[str, os.PathLike]) -> bool:
|
| 323 |
+
return S3FileSystem.validate_checkpoint_id(checkpoint_id)
|
pid/_ext/imaginaire/config.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""Training config system for Imaginare4"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import os
|
| 21 |
+
from typing import Any, Dict, Optional, Type, TypeVar, Union
|
| 22 |
+
|
| 23 |
+
import attrs
|
| 24 |
+
import torch
|
| 25 |
+
import torch.utils.data
|
| 26 |
+
import torch.utils.data.distributed
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
from megatron.core import ModelParallelConfig
|
| 30 |
+
|
| 31 |
+
USE_MEGATRON = True
|
| 32 |
+
except ImportError:
|
| 33 |
+
USE_MEGATRON = False
|
| 34 |
+
print("Megatron-core is not installed.")
|
| 35 |
+
|
| 36 |
+
from pid._ext.imaginaire.lazy_config import LazyCall as L
|
| 37 |
+
from pid._ext.imaginaire.lazy_config import LazyDict
|
| 38 |
+
from pid._ext.imaginaire.utils import callback, distributed
|
| 39 |
+
from pid._ext.imaginaire.utils.misc import Color
|
| 40 |
+
|
| 41 |
+
T = TypeVar("T")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _is_attrs_instance(obj: object) -> bool:
|
| 45 |
+
"""
|
| 46 |
+
Helper function to check if an object is an instance of an attrs-defined class.
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
obj: The object to check.
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
bool: True if the object is an instance of an attrs-defined class, False otherwise.
|
| 53 |
+
"""
|
| 54 |
+
return hasattr(obj, "__attrs_attrs__")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def make_freezable(cls: T) -> T:
|
| 58 |
+
"""
|
| 59 |
+
A decorator that adds the capability to freeze instances of an attrs-defined class.
|
| 60 |
+
|
| 61 |
+
NOTE: This requires the wrapped attrs to be defined with attrs.define(slots=False) because we need
|
| 62 |
+
to hack on a "_is_frozen" attribute.
|
| 63 |
+
|
| 64 |
+
This decorator enhances an attrs-defined class with the ability to be "frozen" at runtime.
|
| 65 |
+
Once an instance is frozen, its attributes cannot be changed. It also recursively freezes
|
| 66 |
+
any attrs-defined objects that are attributes of the class.
|
| 67 |
+
|
| 68 |
+
Usage:
|
| 69 |
+
@make_freezable
|
| 70 |
+
@attrs.define(slots=False)
|
| 71 |
+
class MyClass:
|
| 72 |
+
attribute1: int
|
| 73 |
+
attribute2: str
|
| 74 |
+
|
| 75 |
+
obj = MyClass(1, 'a')
|
| 76 |
+
obj.freeze() # Freeze the instance
|
| 77 |
+
obj.attribute1 = 2 # Raises AttributeError
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
cls: The class to be decorated.
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
The decorated class with added freezing capability.
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
if not hasattr(cls, "__dict__"):
|
| 87 |
+
raise TypeError(
|
| 88 |
+
"make_freezable cannot be used with classes that do not define __dict__. Make sure that the wrapped "
|
| 89 |
+
"class was defined with `@attrs.define(slots=False)`"
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
original_setattr = cls.__setattr__
|
| 93 |
+
|
| 94 |
+
def setattr_override(self, key, value) -> None: # noqa: ANN001
|
| 95 |
+
"""
|
| 96 |
+
Override __setattr__ to allow modifications during initialization
|
| 97 |
+
and prevent modifications once the instance is frozen.
|
| 98 |
+
"""
|
| 99 |
+
if hasattr(self, "_is_frozen") and self._is_frozen and key != "_is_frozen":
|
| 100 |
+
raise AttributeError("Cannot modify frozen instance")
|
| 101 |
+
original_setattr(self, key, value) # type: ignore
|
| 102 |
+
|
| 103 |
+
cls.__setattr__ = setattr_override # type: ignore
|
| 104 |
+
|
| 105 |
+
def freeze(self: object) -> None:
|
| 106 |
+
"""
|
| 107 |
+
Freeze the instance and all its attrs-defined attributes.
|
| 108 |
+
"""
|
| 109 |
+
for _, value in attrs.asdict(self, recurse=False).items():
|
| 110 |
+
if _is_attrs_instance(value) and hasattr(value, "freeze"):
|
| 111 |
+
value.freeze()
|
| 112 |
+
self._is_frozen = True # type: ignore
|
| 113 |
+
|
| 114 |
+
cls.freeze = freeze # type: ignore
|
| 115 |
+
|
| 116 |
+
return cls
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _pretty_print_attrs_instance(obj: object, indent: int = 0, use_color: bool = False) -> str:
|
| 120 |
+
"""
|
| 121 |
+
Recursively pretty prints attrs objects with color.
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
assert attrs.has(obj.__class__)
|
| 125 |
+
|
| 126 |
+
lines: list[str] = []
|
| 127 |
+
for attribute in attrs.fields(obj.__class__):
|
| 128 |
+
value = getattr(obj, attribute.name)
|
| 129 |
+
if attrs.has(value.__class__):
|
| 130 |
+
if use_color:
|
| 131 |
+
lines.append(" " * indent + Color.cyan("* ") + Color.green(attribute.name) + ":")
|
| 132 |
+
else:
|
| 133 |
+
lines.append(" " * indent + "* " + attribute.name + ":")
|
| 134 |
+
lines.append(_pretty_print_attrs_instance(value, indent + 1, use_color))
|
| 135 |
+
else:
|
| 136 |
+
if use_color:
|
| 137 |
+
lines.append(
|
| 138 |
+
" " * indent + Color.cyan("* ") + Color.green(attribute.name) + ": " + Color.yellow(value)
|
| 139 |
+
)
|
| 140 |
+
else:
|
| 141 |
+
lines.append(" " * indent + "* " + attribute.name + ": " + str(value))
|
| 142 |
+
return "\n".join(lines)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def pretty_print_overrides(overrides: Optional[list[str]] = None, use_color: bool = False) -> str:
|
| 146 |
+
"""
|
| 147 |
+
Pretty prints overrides.
|
| 148 |
+
"""
|
| 149 |
+
|
| 150 |
+
lines: list[str] = []
|
| 151 |
+
lines.append(Color.cyan("* ") + Color.green("overrides") + ": ")
|
| 152 |
+
for override in overrides:
|
| 153 |
+
if override == "--":
|
| 154 |
+
continue
|
| 155 |
+
if override.startswith("~"):
|
| 156 |
+
attribute_name = override[1:]
|
| 157 |
+
attribute_value = None
|
| 158 |
+
else:
|
| 159 |
+
attribute_name, attribute_value = override.split("=")
|
| 160 |
+
if use_color:
|
| 161 |
+
lines.append(" " + Color.cyan("* ") + Color.green(attribute_name) + ": " + Color.yellow(attribute_value))
|
| 162 |
+
else:
|
| 163 |
+
lines.append(" " + "* " + attribute_name + ": " + str(attribute_value))
|
| 164 |
+
|
| 165 |
+
return "\n".join(lines)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
@make_freezable
|
| 169 |
+
@attrs.define(slots=False) # slots=False is required for make_freezable. See the make_freezable notes for more info.
|
| 170 |
+
class ObjectStoreConfig:
|
| 171 |
+
# Whether the file I/O is from object store instead of local disk.
|
| 172 |
+
enabled: bool = False
|
| 173 |
+
# Path to the object store credentials file.
|
| 174 |
+
credentials: str = ""
|
| 175 |
+
# Object store bucket to read from / write to the objects.
|
| 176 |
+
bucket: str = ""
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@make_freezable
|
| 180 |
+
@attrs.define(slots=False)
|
| 181 |
+
class JobConfig:
|
| 182 |
+
# Project name.
|
| 183 |
+
project: str = ""
|
| 184 |
+
# Experiment name.
|
| 185 |
+
group: str = ""
|
| 186 |
+
# Run/job name.
|
| 187 |
+
name: str = ""
|
| 188 |
+
# W&B mode, can be "online", or "disabled".
|
| 189 |
+
wandb_mode: str = "online"
|
| 190 |
+
|
| 191 |
+
@property
|
| 192 |
+
def path(self) -> str:
|
| 193 |
+
return f"{self.project}/{self.group}/{self.name}"
|
| 194 |
+
|
| 195 |
+
@property
|
| 196 |
+
def path_local(self) -> str:
|
| 197 |
+
local_root = os.environ.get("IMAGINAIRE_OUTPUT_ROOT", "/tmp/imaginaire4-output")
|
| 198 |
+
return f"{local_root}/{self.path}"
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
@make_freezable
|
| 202 |
+
@attrs.define(slots=False)
|
| 203 |
+
class EMAConfig:
|
| 204 |
+
# Enable tracking a set of exponential moving average (EMA) weights.
|
| 205 |
+
enabled: bool = False
|
| 206 |
+
# EMA decay rate.
|
| 207 |
+
beta: float = 0.9999
|
| 208 |
+
# Enable removing "_orig_mod-" from buffer names that is added by torch.compile
|
| 209 |
+
torch_compile_buffer_renaming: bool = False
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
@make_freezable
|
| 213 |
+
@attrs.define(slots=False)
|
| 214 |
+
class PowerEMAConfig:
|
| 215 |
+
# Enable tracking a set of exponential moving average (EMA) weights.
|
| 216 |
+
enabled: bool = False
|
| 217 |
+
# EDM2 paper EMA decay rate.
|
| 218 |
+
s: float = 0.1
|
| 219 |
+
# Enable removing "_orig_mod-" from buffer names that is added by torch.compile
|
| 220 |
+
torch_compile_buffer_renaming: bool = False
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
@make_freezable
|
| 224 |
+
@attrs.define(slots=False)
|
| 225 |
+
class DDPConfig:
|
| 226 |
+
# Traverse the computation graph to find parameters that don't receive gradients.
|
| 227 |
+
find_unused_parameters: bool = False
|
| 228 |
+
# Set to True if the computation graph does not change during the whole training loop.
|
| 229 |
+
static_graph: bool = True
|
| 230 |
+
# Set to True if we want to synchronize buffers. Set to False if the sync is going to be handled elsewhere.
|
| 231 |
+
broadcast_buffers: bool = True
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
@make_freezable
|
| 235 |
+
@attrs.define(slots=False)
|
| 236 |
+
class CuDNNConfig:
|
| 237 |
+
# Set to True for better reproducibility of the results (only using deterministic cudnn functions).
|
| 238 |
+
deterministic: bool = False
|
| 239 |
+
# If set to True, cudnn will benchmark several algorithms and pick the fastest one.
|
| 240 |
+
benchmark: bool = True
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
@make_freezable
|
| 244 |
+
@attrs.define(slots=False)
|
| 245 |
+
class JITConfig:
|
| 246 |
+
# Enable exporting a JIT compiled model.
|
| 247 |
+
enabled: bool = False
|
| 248 |
+
# Input tensor shape, for example input.
|
| 249 |
+
input_shape: Union[list[int], None] = None
|
| 250 |
+
# Device to compile onto.
|
| 251 |
+
device: str = "cuda"
|
| 252 |
+
# # Data type to compile onto.
|
| 253 |
+
dtype: str = "bfloat16"
|
| 254 |
+
# Strict mode for PyTorch JIT.
|
| 255 |
+
strict: bool = True
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
@make_freezable
|
| 259 |
+
@attrs.define(slots=False)
|
| 260 |
+
class CheckpointConfig:
|
| 261 |
+
# possible checkpoint class
|
| 262 |
+
type: Optional[Dict] = None
|
| 263 |
+
# for dcp, whether to use async mode
|
| 264 |
+
dcp_async_mode_enabled: bool = False
|
| 265 |
+
# Configs for saving the checkpoints to object store.
|
| 266 |
+
save_to_object_store: ObjectStoreConfig = attrs.field(factory=ObjectStoreConfig)
|
| 267 |
+
# Save the checkpoint every N iterations.
|
| 268 |
+
save_iter: int = 999999999
|
| 269 |
+
# Configs for loading the checkpoints from object store.
|
| 270 |
+
load_from_object_store: ObjectStoreConfig = attrs.field(factory=ObjectStoreConfig)
|
| 271 |
+
# Path of model weights to resume the checkpoint from.
|
| 272 |
+
load_path: str = ""
|
| 273 |
+
# Whether to load the training states (optimizer/scheduler/grad-scaler) from the checkpoint path.
|
| 274 |
+
load_training_state: bool = False
|
| 275 |
+
# Whether to load the scheduler state only from the checkpoint path. If load_training_state is True, this will be ignored.
|
| 276 |
+
only_load_scheduler_state: bool = False
|
| 277 |
+
# Load state_dict to the models in strict mode.
|
| 278 |
+
strict_resume: bool = True
|
| 279 |
+
# Configs for JIT compiling EMA model.
|
| 280 |
+
jit: JITConfig = attrs.field(factory=JITConfig)
|
| 281 |
+
# Print detailed information during checkpoint saving/loading.
|
| 282 |
+
verbose: bool = True
|
| 283 |
+
# keys not to resume from the checkpoint, choices: ["model", "optim", "scheduler", "trainer"]
|
| 284 |
+
keys_not_to_resume: list[str] = []
|
| 285 |
+
# Whether to use the local filesystem for broadcasting checkpoint data (used for Tensor Parallel Checkpointer).
|
| 286 |
+
broadcast_via_filesystem: bool = False
|
| 287 |
+
load_ema_to_reg: bool = False # used in inference, load EMA weights to regular model
|
| 288 |
+
replicate_ema_to_reg_in_training: bool = False # When loading external ckpt for new-stage training, copy EMA weights to regular model. If self checkpoint, will take no effect.
|
| 289 |
+
# In dcp planner, skip the weight shape check, load weights into the model even weight shape is different
|
| 290 |
+
dcp_allow_mismatched_size: bool = False
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
@make_freezable
|
| 294 |
+
@attrs.define(slots=False)
|
| 295 |
+
class NVTXConfig:
|
| 296 |
+
"""Config for NVTX ranges used in the main training loop.
|
| 297 |
+
|
| 298 |
+
See tutorials/nanogpt for more details on how to integrate profiling into your model."""
|
| 299 |
+
|
| 300 |
+
# Enable the NVTX ranges.
|
| 301 |
+
enabled: bool = False
|
| 302 |
+
# Synchronize everything in each NVTX range.
|
| 303 |
+
cuda_synchronize: bool = False
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
@make_freezable
|
| 307 |
+
@attrs.define(slots=False)
|
| 308 |
+
class StragglerDetectionConfig:
|
| 309 |
+
"""Config for Straggler detection tool: https://gitlab-master.nvidia.com/dl/gwe/fault_tolerance_related/straggler/-/tree/cupti?ref_type=heads"""
|
| 310 |
+
|
| 311 |
+
# Enable the Straggler Detection.
|
| 312 |
+
enabled: bool = False
|
| 313 |
+
# How frequently should the Straggler reports be generated.
|
| 314 |
+
report_freq: int = 100
|
| 315 |
+
# How frequently iterations should be profiled
|
| 316 |
+
profile_freq: int = 1
|
| 317 |
+
# What is the maximum relative difference between GPUs after they are considered stragglers
|
| 318 |
+
max_diff: float = 2.0
|
| 319 |
+
# Should the error be raised when straggler is detected
|
| 320 |
+
raise_error: bool = True
|
| 321 |
+
# Analyze kernels in the forward pass.
|
| 322 |
+
analyze_forward: bool = True
|
| 323 |
+
# Analyze kernels in the backward pass.
|
| 324 |
+
analyze_backward: bool = True
|
| 325 |
+
# Analyze kernels in the optimizer.
|
| 326 |
+
analyze_optimizer: bool = True
|
| 327 |
+
# Analyze dataloading time.
|
| 328 |
+
analyze_dataloading: bool = True
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
@make_freezable
|
| 332 |
+
@attrs.define(slots=False)
|
| 333 |
+
class Profiling:
|
| 334 |
+
enable_profiling: bool = False
|
| 335 |
+
enable_memory_snapshot: bool = False
|
| 336 |
+
save_s3: bool = False
|
| 337 |
+
profile_freq: int = 1
|
| 338 |
+
# Target ranks for profiling, each entry must be >=0 and < world_size.
|
| 339 |
+
target_ranks: list[int] = list(range(8))
|
| 340 |
+
# Set `record_shape` and `profile_memory` to False to reduce profile size.
|
| 341 |
+
record_shape: bool = False
|
| 342 |
+
profile_memory: bool = False
|
| 343 |
+
with_stack: bool = True
|
| 344 |
+
with_modules: bool = True
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
@make_freezable
|
| 348 |
+
@attrs.define(slots=False)
|
| 349 |
+
class TrainerConfig:
|
| 350 |
+
from pid._ext.imaginaire.trainer import ImaginaireTrainer
|
| 351 |
+
|
| 352 |
+
type: Type[ImaginaireTrainer] = ImaginaireTrainer
|
| 353 |
+
# Set the callback class.
|
| 354 |
+
# Defaults to the callbacks below.
|
| 355 |
+
callbacks: LazyDict = LazyDict(
|
| 356 |
+
dict(
|
| 357 |
+
ema=L(callback.EMAModelCallback)(),
|
| 358 |
+
progress_bar=L(callback.ProgressBarCallback)(),
|
| 359 |
+
wandb=L(callback.WandBCallback)(),
|
| 360 |
+
)
|
| 361 |
+
)
|
| 362 |
+
# distributed parallelism strategy
|
| 363 |
+
distributed_parallelism: str = "ddp"
|
| 364 |
+
# Distributed data parallel configs.
|
| 365 |
+
ddp: DDPConfig = attrs.field(factory=DDPConfig)
|
| 366 |
+
# cuDNN configs.
|
| 367 |
+
cudnn: CuDNNConfig = attrs.field(factory=CuDNNConfig)
|
| 368 |
+
# Set the random seed.
|
| 369 |
+
seed: int = 0
|
| 370 |
+
# Set the random seed based on current timestamp
|
| 371 |
+
timestamp_seed: bool = False
|
| 372 |
+
# Gradient scaler arguments (for torch.amp.GradScaler).
|
| 373 |
+
grad_scaler_args: dict = attrs.field(factory=lambda: dict(enabled=False))
|
| 374 |
+
# Maximum number of iterations to train the model.
|
| 375 |
+
max_iter: int = 999999999
|
| 376 |
+
# Maximum number of iterations to validate the model. If None, validate on the entire dataset.
|
| 377 |
+
max_val_iter: int | None = None
|
| 378 |
+
# How often we log the training stats.
|
| 379 |
+
logging_iter: int = 100
|
| 380 |
+
# Whether we want to run the validation routines.
|
| 381 |
+
run_validation: bool = True
|
| 382 |
+
# How often we evaluate on the validation set.
|
| 383 |
+
validation_iter: int = 999999999
|
| 384 |
+
# Kill the process after N seconds since the last iteration (usually means dead job).
|
| 385 |
+
timeout_period: int = 999999999
|
| 386 |
+
# Tensor memory organization format.
|
| 387 |
+
memory_format: torch.memory_format = torch.preserve_format
|
| 388 |
+
# Gradient accumulation (update step every N iteration).
|
| 389 |
+
grad_accum_iter: int = 1
|
| 390 |
+
# Straggler Detection config
|
| 391 |
+
straggler_detection: StragglerDetectionConfig = attrs.field(factory=StragglerDetectionConfig)
|
| 392 |
+
# Profiling config
|
| 393 |
+
profiling: Profiling = attrs.field(factory=Profiling)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
@make_freezable
|
| 397 |
+
@attrs.define(slots=False)
|
| 398 |
+
class Config:
|
| 399 |
+
"""Config for an imaginaire4 job.
|
| 400 |
+
|
| 401 |
+
See /README.md/Configuration System for more info.
|
| 402 |
+
"""
|
| 403 |
+
|
| 404 |
+
# Model configs.
|
| 405 |
+
model: LazyDict
|
| 406 |
+
# Optimizer configs.
|
| 407 |
+
optimizer: LazyDict
|
| 408 |
+
# Scheduler configs.
|
| 409 |
+
scheduler: LazyDict
|
| 410 |
+
# Training data configs.
|
| 411 |
+
dataloader_train: LazyDict
|
| 412 |
+
# Validation data configs.
|
| 413 |
+
dataloader_val: LazyDict
|
| 414 |
+
|
| 415 |
+
# Training job configs.
|
| 416 |
+
job: JobConfig = attrs.field(factory=JobConfig)
|
| 417 |
+
|
| 418 |
+
# Trainer configs.
|
| 419 |
+
trainer: TrainerConfig = attrs.field(factory=TrainerConfig)
|
| 420 |
+
|
| 421 |
+
if USE_MEGATRON:
|
| 422 |
+
# Megatron-Core configs
|
| 423 |
+
model_parallel: ModelParallelConfig = attrs.field(factory=ModelParallelConfig)
|
| 424 |
+
else:
|
| 425 |
+
model_parallel: None = None
|
| 426 |
+
|
| 427 |
+
# Checkpointer configs.
|
| 428 |
+
checkpoint: CheckpointConfig = attrs.field(factory=CheckpointConfig)
|
| 429 |
+
|
| 430 |
+
# enable upload reproducible setup to s3
|
| 431 |
+
upload_reproducible_setup: bool = False
|
| 432 |
+
|
| 433 |
+
def pretty_print(self, use_color: bool = False) -> str:
|
| 434 |
+
return _pretty_print_attrs_instance(self, 0, use_color)
|
| 435 |
+
|
| 436 |
+
def to_dict(self) -> dict[str, Any]:
|
| 437 |
+
return attrs.asdict(self)
|
| 438 |
+
|
| 439 |
+
def validate(self) -> None:
|
| 440 |
+
"""Validate that the config has all required fields."""
|
| 441 |
+
|
| 442 |
+
# broadcast job.name across all ranks to make sure it is consistent
|
| 443 |
+
# otherwise, unaligned job names leads unaligned path to save checkpoints
|
| 444 |
+
job_name_tensor = torch.ByteTensor(bytearray(self.job.name, "utf-8")).cuda()
|
| 445 |
+
distributed.broadcast(job_name_tensor, 0)
|
| 446 |
+
self.job.name = job_name_tensor.cpu().numpy().tobytes().decode("utf-8")
|
| 447 |
+
|
| 448 |
+
assert self.job.project != ""
|
| 449 |
+
assert self.job.group != ""
|
| 450 |
+
assert self.job.name != ""
|
pid/_ext/imaginaire/flags.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""Feature flags."""
|
| 17 |
+
|
| 18 |
+
import os
|
| 19 |
+
from dataclasses import dataclass
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _parse_bool(value: str) -> bool:
|
| 23 |
+
"""Parse string to a boolean."""
|
| 24 |
+
return value.lower() in ["true", "1", "yes", "y"]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
INTERNAL = _parse_bool(os.environ.get("COSMOS_INTERNAL", "0"))
|
| 28 |
+
"""Whether to enable internal (nvidia-only) features."""
|
| 29 |
+
|
| 30 |
+
SMOKE = _parse_bool(os.environ.get("COSMOS_SMOKE", "0"))
|
| 31 |
+
"""Whether to enable smoke test.
|
| 32 |
+
|
| 33 |
+
Disables expensive operations such as checkpoint loading.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
VERBOSE = _parse_bool(os.environ.get("COSMOS_VERBOSE", "0"))
|
| 37 |
+
"""Whether to enable verbose output."""
|
| 38 |
+
|
| 39 |
+
EXPERIMENTAL_CHECKPOINTS = _parse_bool(os.environ.get("COSMOS_EXPERIMENTAL_CHECKPOINTS", "0"))
|
| 40 |
+
"""Whether to enable experimental checkpoints."""
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@dataclass
|
| 44 |
+
class Flags:
|
| 45 |
+
internal: bool = INTERNAL
|
| 46 |
+
smoke: bool = SMOKE
|
| 47 |
+
verbose: bool = VERBOSE
|
| 48 |
+
experimental_checkpoints: bool = EXPERIMENTAL_CHECKPOINTS
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
FLAGS = Flags()
|
| 52 |
+
"""Convenience object for accessing flags."""
|
pid/_ext/imaginaire/lazy_config/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Facebook, Inc. and its affiliates.
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
from omegaconf import DictConfig, OmegaConf
|
| 5 |
+
|
| 6 |
+
from pid._ext.imaginaire.lazy_config.instantiate import instantiate
|
| 7 |
+
from pid._ext.imaginaire.lazy_config.lazy import LazyCall, LazyConfig
|
| 8 |
+
from pid._ext.imaginaire.lazy_config.omegaconf_patch import to_object
|
| 9 |
+
|
| 10 |
+
OmegaConf.to_object = to_object
|
| 11 |
+
|
| 12 |
+
PLACEHOLDER = None
|
| 13 |
+
LazyDict = DictConfig
|
| 14 |
+
|
| 15 |
+
__all__ = ["instantiate", "LazyCall", "LazyConfig", "PLACEHOLDER", "LazyDict"]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
DOC_BUILDING = os.getenv("_DOC_BUILDING", False) # set in docs/conf.py
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def fixup_module_metadata(module_name, namespace, keys=None):
|
| 22 |
+
"""
|
| 23 |
+
Fix the __qualname__ of module members to be their exported api name, so
|
| 24 |
+
when they are referenced in docs, sphinx can find them. Reference:
|
| 25 |
+
https://github.com/python-trio/trio/blob/6754c74eacfad9cc5c92d5c24727a2f3b620624e/trio/_util.py#L216-L241
|
| 26 |
+
"""
|
| 27 |
+
if not DOC_BUILDING:
|
| 28 |
+
return
|
| 29 |
+
seen_ids = set()
|
| 30 |
+
|
| 31 |
+
def fix_one(qualname, name, obj):
|
| 32 |
+
# avoid infinite recursion (relevant when using
|
| 33 |
+
# typing.Generic, for example)
|
| 34 |
+
if id(obj) in seen_ids:
|
| 35 |
+
return
|
| 36 |
+
seen_ids.add(id(obj))
|
| 37 |
+
|
| 38 |
+
mod = getattr(obj, "__module__", None)
|
| 39 |
+
if mod is not None and (mod.startswith(module_name) or mod.startswith("fvcore.")):
|
| 40 |
+
obj.__module__ = module_name
|
| 41 |
+
# Modules, unlike everything else in Python, put fully-qualitied
|
| 42 |
+
# names into their __name__ attribute. We check for "." to avoid
|
| 43 |
+
# rewriting these.
|
| 44 |
+
if hasattr(obj, "__name__") and "." not in obj.__name__:
|
| 45 |
+
obj.__name__ = name
|
| 46 |
+
obj.__qualname__ = qualname
|
| 47 |
+
if isinstance(obj, type):
|
| 48 |
+
for attr_name, attr_value in obj.__dict__.items():
|
| 49 |
+
fix_one(objname + "." + attr_name, attr_name, attr_value)
|
| 50 |
+
|
| 51 |
+
if keys is None:
|
| 52 |
+
keys = namespace.keys()
|
| 53 |
+
for objname in keys:
|
| 54 |
+
if not objname.startswith("_"):
|
| 55 |
+
obj = namespace[objname]
|
| 56 |
+
fix_one(objname, objname, obj)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
fixup_module_metadata(__name__, globals(), __all__)
|
| 60 |
+
del fixup_module_metadata
|
pid/_ext/imaginaire/lazy_config/file_io.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
| 2 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
from iopath.common.file_io import HTTPURLHandler, OneDrivePathHandler, PathHandler
|
| 17 |
+
from iopath.common.file_io import PathManager as PathManagerBase
|
| 18 |
+
|
| 19 |
+
__all__ = ["PathManager", "PathHandler"]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
PathManager = PathManagerBase()
|
| 23 |
+
PathManager.register_handler(HTTPURLHandler())
|
| 24 |
+
PathManager.register_handler(OneDrivePathHandler())
|