victor HF Staff commited on
Commit
e2ddf3f
·
verified ·
1 Parent(s): d3cb95c

Cache examples and simplify frontend

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +29 -0
  2. .gitignore +5 -0
  3. LICENSE-PiD-CODE +13 -0
  4. UPSTREAM_PID_README.md +245 -0
  5. app.py +25 -12
  6. candidate_examples/brick_texture.jpg +3 -0
  7. candidate_examples/contact_sheet.jpg +3 -0
  8. candidate_examples/dragonfly_macro.jpg +3 -0
  9. candidate_examples/embroidery_threads.jpg +3 -0
  10. candidate_examples/feather_barbs.jpg +3 -0
  11. candidate_examples/leaf_macro.jpg +3 -0
  12. candidate_examples/palm_bark.jpg +3 -0
  13. candidate_results/brick_texture_crop_compare.jpg +3 -0
  14. candidate_results/brick_texture_pid_preview.jpg +3 -0
  15. candidate_results/dragonfly_macro_crop_compare.jpg +3 -0
  16. candidate_results/dragonfly_macro_pid_preview.jpg +3 -0
  17. candidate_results/embroidery_threads_crop_compare.jpg +3 -0
  18. candidate_results/embroidery_threads_pid_preview.jpg +3 -0
  19. candidate_results/feather_barbs_crop_compare.jpg +3 -0
  20. candidate_results/feather_barbs_pid_preview.jpg +3 -0
  21. candidate_results/leaf_macro_crop_compare.jpg +3 -0
  22. candidate_results/leaf_macro_pid_preview.jpg +3 -0
  23. candidate_results/palm_bark_crop_compare.jpg +3 -0
  24. candidate_results/palm_bark_pid_preview.jpg +3 -0
  25. candidate_results/summary.json +104 -0
  26. example_cache/v2_example_0.json +44 -0
  27. example_cache/v2_example_1.json +23 -0
  28. example_cache/v2_example_2.json +23 -0
  29. examples/alpine_cabin.jpg +0 -0
  30. examples/bowery_waltz.jpg +0 -0
  31. examples/brick_wall_texture.jpg +3 -0
  32. examples/coyote_run.jpg +0 -0
  33. examples/golden_eagle.jpg +0 -0
  34. examples/leaf_macro_texture.jpg +3 -0
  35. examples/palm_bark_texture.jpg +3 -0
  36. examples/portrait_umbrella.jpg +0 -0
  37. examples/shove_it.jpg +0 -0
  38. index.html +81 -31
  39. pid/__init__.py +14 -0
  40. pid/_ext/__init__.py +4 -0
  41. pid/_ext/imaginaire/__init__.py +14 -0
  42. pid/_ext/imaginaire/checkpointer/__init__.py +14 -0
  43. pid/_ext/imaginaire/checkpointer/base.py +177 -0
  44. pid/_ext/imaginaire/checkpointer/dcp.py +1062 -0
  45. pid/_ext/imaginaire/checkpointer/dummy.py +47 -0
  46. pid/_ext/imaginaire/checkpointer/s3_filesystem.py +323 -0
  47. pid/_ext/imaginaire/config.py +450 -0
  48. pid/_ext/imaginaire/flags.py +52 -0
  49. pid/_ext/imaginaire/lazy_config/__init__.py +60 -0
  50. 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 = _example_cache_path(index)
250
- if not path.exists():
251
- return None
252
- try:
253
- return json.loads(path.read_text(encoding="utf-8"))
254
- except Exception:
255
- return None
 
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

  • SHA256: 7f3e6128e4a25a8c90f9c4b8655136468630ff44bf994b51caaa5f7202724e82
  • Pointer size: 131 Bytes
  • Size of remote file: 394 kB
candidate_examples/contact_sheet.jpg ADDED

Git LFS Details

  • SHA256: 8e39fa2573f98d555925a60b8b09841a03e5dff4fd71c4f225a8daef426e8951
  • Pointer size: 131 Bytes
  • Size of remote file: 107 kB
candidate_examples/dragonfly_macro.jpg ADDED

Git LFS Details

  • SHA256: c54d6565cae92878269112ec02f757b564ac2c412a3db1d3080e6f7d1661db3f
  • Pointer size: 131 Bytes
  • Size of remote file: 178 kB
candidate_examples/embroidery_threads.jpg ADDED

Git LFS Details

  • SHA256: 8a13e4523c7887c05afd3121dcf55edc3dcbf72af45e3d4cce3333aa8f5fb76d
  • Pointer size: 131 Bytes
  • Size of remote file: 166 kB
candidate_examples/feather_barbs.jpg ADDED

Git LFS Details

  • SHA256: 514fcb44e11e1a34bb7a11c5a5720738dab3d3cb8a52b7540cf82ca68e965031
  • Pointer size: 131 Bytes
  • Size of remote file: 154 kB
candidate_examples/leaf_macro.jpg ADDED

Git LFS Details

  • SHA256: b62dc038cd3913f3fa0e6b2968363d13704bf69d151cd5b657f8ac9bab4f30d8
  • Pointer size: 131 Bytes
  • Size of remote file: 451 kB
candidate_examples/palm_bark.jpg ADDED

Git LFS Details

  • SHA256: 050980bd5fa59f1d463f7ba322db666e6ee7280b1dd07a33b20b48fa05e4325e
  • Pointer size: 131 Bytes
  • Size of remote file: 600 kB
candidate_results/brick_texture_crop_compare.jpg ADDED

Git LFS Details

  • SHA256: 25c365688706adbe5f31feaaa7d06a97393c3dc2f094252f0071bb63ba893e53
  • Pointer size: 131 Bytes
  • Size of remote file: 494 kB
candidate_results/brick_texture_pid_preview.jpg ADDED

Git LFS Details

  • SHA256: b17e7111cbcdacde01e5e9f8b9580cf7d26a2f3980dd35c3043407af670bf0f5
  • Pointer size: 131 Bytes
  • Size of remote file: 453 kB
candidate_results/dragonfly_macro_crop_compare.jpg ADDED

Git LFS Details

  • SHA256: dfa15a76bd5d2551fe5fb26d1b0f0b548654d4f6430a7ff1508c168c07b31609
  • Pointer size: 131 Bytes
  • Size of remote file: 340 kB
candidate_results/dragonfly_macro_pid_preview.jpg ADDED

Git LFS Details

  • SHA256: c5762260daab8f7bd3dd76237bb6f8f7898393c4c5922addd43c130807331848
  • Pointer size: 131 Bytes
  • Size of remote file: 200 kB
candidate_results/embroidery_threads_crop_compare.jpg ADDED

Git LFS Details

  • SHA256: d9867e0880ae9c7921cd94d3e08a23031069d1cb8f69936058ad37f2255fc1d1
  • Pointer size: 131 Bytes
  • Size of remote file: 214 kB
candidate_results/embroidery_threads_pid_preview.jpg ADDED

Git LFS Details

  • SHA256: 7ea7b1e9f37df7c3274a2c92dbbc996bd33894bbd50ac9219ed4731329059e64
  • Pointer size: 131 Bytes
  • Size of remote file: 185 kB
candidate_results/feather_barbs_crop_compare.jpg ADDED

Git LFS Details

  • SHA256: 5e6c68eb5c41c14873b2ade095585d790a5979e98cb3ff799f1328e25737eae5
  • Pointer size: 131 Bytes
  • Size of remote file: 209 kB
candidate_results/feather_barbs_pid_preview.jpg ADDED

Git LFS Details

  • SHA256: 968aca05520ef4bb53ce4b905e2d9922ef071e36e77bf80ca30c94d654896f3f
  • Pointer size: 131 Bytes
  • Size of remote file: 174 kB
candidate_results/leaf_macro_crop_compare.jpg ADDED

Git LFS Details

  • SHA256: 2e072ec15ee875afe9e7201d19d581a5da21b1a5160550f22c5ef31498ea7225
  • Pointer size: 131 Bytes
  • Size of remote file: 437 kB
candidate_results/leaf_macro_pid_preview.jpg ADDED

Git LFS Details

  • SHA256: 0147e6a63f73fb9501bd2b2aea248902e36f162c795f898be1167ac91522c4d6
  • Pointer size: 131 Bytes
  • Size of remote file: 523 kB
candidate_results/palm_bark_crop_compare.jpg ADDED

Git LFS Details

  • SHA256: 5ba8dd3ea4cf2ae3b845604dedcf244a79b012927aedceb32e04f8937a299518
  • Pointer size: 131 Bytes
  • Size of remote file: 505 kB
candidate_results/palm_bark_pid_preview.jpg ADDED

Git LFS Details

  • SHA256: 06ecda0faf66d4fb936dc494270917b4f176b546efca47d8e66867f31781e1d1
  • Pointer size: 131 Bytes
  • Size of remote file: 663 kB
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

  • SHA256: 7f3e6128e4a25a8c90f9c4b8655136468630ff44bf994b51caaa5f7202724e82
  • Pointer size: 131 Bytes
  • Size of remote file: 394 kB
examples/coyote_run.jpg ADDED
examples/golden_eagle.jpg ADDED
examples/leaf_macro_texture.jpg ADDED

Git LFS Details

  • SHA256: b62dc038cd3913f3fa0e6b2968363d13704bf69d151cd5b657f8ac9bab4f30d8
  • Pointer size: 131 Bytes
  • Size of remote file: 451 kB
examples/palm_bark_texture.jpg ADDED

Git LFS Details

  • SHA256: 050980bd5fa59f1d463f7ba322db666e6ee7280b1dd07a33b20b48fa05e4325e
  • Pointer size: 131 Bytes
  • Size of remote file: 600 kB
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: 9px 10px; display: grid; gap: 2px;
 
127
  }
128
  .example-btn:hover, .example-btn.active { background: var(--surface-2); border-color: var(--border-strong); }
129
- .example-btn strong { font-size: 12.5px; font-weight: 500; }
130
- .example-btn span { color: var(--muted); font-size: 11.5px; }
 
 
 
 
 
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: grid; grid-template-columns: minmax(0, 1.1fr) minmax(300px, .9fr); gap: 18px; }
135
- .stage, .results {
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; max-height: 320px; overflow: auto; padding-right: 2px; }
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
- button.innerHTML = `<strong>${example.title}</strong><span>${example.task}${example.cached ? " - cached" : ""}</span>`;
 
 
 
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
+ "&": "&amp;",
380
+ "<": "&lt;",
381
+ ">": "&gt;",
382
+ '"': "&quot;",
383
+ "'": "&#039;",
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())